Skip to content

Commit 278cf30

Browse files
authored
Handle non-interactive shells gracefully (#158)
1 parent a3cc7a7 commit 278cf30

File tree

11 files changed

+86
-18
lines changed

11 files changed

+86
-18
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ $ scfw run poetry add git+https://github.com/DataDog/guarddog
9393

9494
For `pip install` commands, packages will be installed in the same environment (virtual or global) in which the command was run.
9595

96+
Several command-line options of the `run` subcommand are noteworthy:
97+
98+
* `--dry-run`: Verify any installation targets but do not run the package manager command. The exit code indicates whether there were findings of any severity
99+
100+
* `--allow-on-warning` and `--block-on-warning`: Non-interactively allow or block commands, respectively, with only warning-level findings. Setting the environment variable `SCFW_ON_WARNING` to `"ALLOW"` or `"BLOCK"` achieves the same effect, with the CLI options taking priority over the environment variable when both are used
101+
102+
* `--error-on-block`: Treat blocked commands as errors (useful for scripting)
103+
104+
Run `scfw run --help` to see all available command-line options.
105+
96106
### Audit installed packages
97107

98108
Supply-Chain Firewall can also use its verifiers to audit installed packages:

scfw/configure/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from argparse import Namespace
66

7-
from scfw.configure.constants import * # noqa
87
import scfw.configure.dd_agent as dd_agent
98
import scfw.configure.env as env
109
import scfw.configure.interactive as interactive

scfw/configure/dd_agent.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
"""
44

55
import json
6+
import logging
67
from pathlib import Path
78
import shutil
89
import subprocess
910

10-
from scfw.configure.constants import DD_SERVICE, DD_SOURCE
11+
from scfw.constants import DD_SERVICE, DD_SOURCE
12+
13+
_log = logging.getLogger(__name__)
1114

1215

1316
def configure_agent_logging(port: str):
@@ -21,7 +24,7 @@ def configure_agent_logging(port: str):
2124
ValueError: An invalid port number was provided.
2225
RuntimeError: An error occurred while querying the Agent's status.
2326
"""
24-
if not (0 < int(port) < 65536):
27+
if not (0 < int(port) < 2 ** 16):
2528
raise ValueError("Invalid port number provided for Datadog Agent logging")
2629

2730
config_file = (
@@ -37,8 +40,10 @@ def configure_agent_logging(port: str):
3740

3841
if not scfw_config_dir.is_dir():
3942
scfw_config_dir.mkdir()
43+
_log.info(f"Created directory {scfw_config_dir} for Datadog Agent configuration")
4044
with open(scfw_config_file, 'w') as f:
4145
f.write(config_file)
46+
_log.info(f"Wrote file {scfw_config_file} with Datadog Agent configuration")
4247

4348

4449
def remove_agent_logging():
@@ -51,13 +56,15 @@ def remove_agent_logging():
5156
scfw_config_dir = _dd_agent_scfw_config_dir()
5257

5358
if not scfw_config_dir.is_dir():
59+
_log.info("No Datadog Agent configuration directory to remove")
5460
return
5561

5662
try:
5763
shutil.rmtree(scfw_config_dir)
64+
_log.info(f"Deleted directory {scfw_config_dir} with Datadog Agent configuration")
5865
except Exception:
5966
raise RuntimeError(
60-
"Failed to delete Datadog Agent configuration directory for Supply-Chain Firewall"
67+
f"Failed to delete directory {scfw_config_dir} with Datadog Agent configuration for Supply-Chain Firewall"
6168
)
6269

6370

@@ -71,19 +78,30 @@ def _dd_agent_scfw_config_dir() -> Path:
7178
7279
Raises:
7380
RuntimeError:
74-
Unable to query Datadog Agent status to read the location of its
75-
global configuration directory.
81+
* Unable to query Datadog Agent status to read the location of its global
82+
configuration directory
83+
* Datadog Agent global configuration directory is not set or does not exist
84+
ValueError: Failed to parse Datadog Agent status JSON report.
7685
"""
7786
try:
7887
agent_status = subprocess.run(
7988
["datadog-agent", "status", "--json"], check=True, text=True, capture_output=True
8089
)
81-
agent_config_dir = json.loads(agent_status.stdout).get("config", {}).get("confd_path", "")
90+
config_confd_path = json.loads(agent_status.stdout).get("config", {}).get("confd_path")
91+
agent_config_dir = Path(config_confd_path) if config_confd_path else None
8292

8393
except subprocess.CalledProcessError:
8494
raise RuntimeError(
8595
"Unable to query Datadog Agent status: please ensure the Agent is running. "
8696
"Linux users may need sudo to run this command."
8797
)
8898

89-
return Path(agent_config_dir) / "scfw.d"
99+
except json.JSONDecodeError:
100+
raise ValueError("Failed to parse Datadog Agent status report as JSON")
101+
102+
if not (agent_config_dir and agent_config_dir.is_dir()):
103+
raise RuntimeError(
104+
"Datadog Agent global configuration directory is not set or does not exist"
105+
)
106+
107+
return agent_config_dir / "scfw.d"

scfw/configure/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pathlib import Path
77
import re
88

9-
from scfw.configure.constants import DD_AGENT_PORT_VAR, DD_API_KEY_VAR, DD_LOG_LEVEL_VAR, SCFW_HOME_VAR
9+
from scfw.constants import DD_AGENT_PORT_VAR, DD_API_KEY_VAR, DD_LOG_LEVEL_VAR, SCFW_HOME_VAR
1010

1111
_log = logging.getLogger(__name__)
1212

scfw/configure/interactive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import inquirer # type: ignore
1111

12-
from scfw.configure.constants import DD_API_KEY_VAR
12+
from scfw.constants import DD_API_KEY_VAR
1313
from scfw.logger import FirewallAction
1414

1515
GREETING = (

scfw/configure/constants.py renamed to scfw/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
forward firewall logs to the local Datadog Agent.
3434
"""
3535

36+
ON_WARNING_VAR = "SCFW_ON_WARNING"
37+
"""
38+
The environment variable under which the firewall looks for the user's choice of
39+
`FirewallAction` to take for commands with only warning-level findings.
40+
"""
41+
3642
SCFW_HOME_VAR = "SCFW_HOME"
3743
"""
3844
The environment variable under which the firewall looks for its home (cache) directory.

scfw/firewall.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from argparse import Namespace
66
import inquirer # type: ignore
77
import logging
8+
import os
9+
import sys
810

11+
from scfw.constants import ON_WARNING_VAR
912
from scfw.logger import FirewallAction
1013
from scfw.loggers import FirewallLoggers
1114
from scfw.package_manager import UnsupportedVersionError
@@ -65,10 +68,7 @@ def run_firewall(args: Namespace) -> int:
6568

6669
if not args.dry_run and warning_report:
6770
print(warning_report)
68-
if (
69-
not args.allow_on_warning
70-
and (args.block_on_warning or not inquirer.confirm("Proceed with installation?", default=False))
71-
):
71+
if _get_warning_action(args.allow_on_warning, args.block_on_warning) == FirewallAction.BLOCK:
7272
loggers.log_firewall_action(
7373
package_manager.ecosystem(),
7474
package_manager.name(),
@@ -128,3 +128,38 @@ def run_firewall(args: Namespace) -> int:
128128
warned=False,
129129
)
130130
return package_manager.run_command(args.command)
131+
132+
133+
def _get_warning_action(cli_allow_choice: bool, cli_block_choice: bool) -> FirewallAction:
134+
"""
135+
Return the `FirewallAction` that should be taken for `WARNING`-level findings.
136+
137+
Args:
138+
cli_allow_choice:
139+
A `bool` indicating whether the user selected `--allow-on-warning` on the command-line.
140+
cli_block_choice:
141+
A `bool` indicating whether the user selected `--block-on-warning` on the command-line.
142+
143+
Returns:
144+
The `FirewallAction` that should be taken based on the user's configured choices or, if
145+
no choice has been made, on the user's runtime (interactive) decision.
146+
"""
147+
if cli_block_choice:
148+
return FirewallAction.BLOCK
149+
if cli_allow_choice:
150+
return FirewallAction.ALLOW
151+
152+
if (action := os.getenv(ON_WARNING_VAR)):
153+
try:
154+
return FirewallAction.from_string(action)
155+
except Exception:
156+
_log.warning(f"Ignoring invalid firewall action {ON_WARNING_VAR}='{action}'")
157+
158+
if not sys.stdin.isatty():
159+
_log.warning(
160+
"Non-interactive terminal and no predefined action for WARNING findings: defaulting to BLOCK"
161+
)
162+
return FirewallAction.BLOCK
163+
164+
user_confirmed = inquirer.confirm("Proceed with installation?", default=False)
165+
return FirewallAction.ALLOW if user_confirmed else FirewallAction.BLOCK

scfw/loggers/dd_agent_logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import os
77
import socket
88

9-
from scfw.configure import DD_AGENT_PORT_VAR
9+
from scfw.constants import DD_AGENT_PORT_VAR
1010
from scfw.logger import FirewallLogger
1111
from scfw.loggers.dd_logger import DDLogFormatter, DDLogger
1212

scfw/loggers/dd_api_logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from datadog_api_client.v2.model.http_log_item import HTTPLogItem
1313

1414
import scfw
15-
from scfw.configure import DD_API_KEY_VAR, DD_ENV, DD_SERVICE, DD_SOURCE
15+
from scfw.constants import DD_API_KEY_VAR, DD_ENV, DD_SERVICE, DD_SOURCE
1616
from scfw.logger import FirewallLogger
1717
from scfw.loggers.dd_logger import DDLogFormatter, DDLogger
1818

scfw/loggers/dd_logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import dotenv
1212

1313
import scfw
14-
from scfw.configure import DD_ENV, DD_LOG_LEVEL_VAR, DD_SERVICE, DD_SOURCE
14+
from scfw.constants import DD_ENV, DD_LOG_LEVEL_VAR, DD_SERVICE, DD_SOURCE
1515
from scfw.ecosystem import ECOSYSTEM
1616
from scfw.logger import FirewallAction, FirewallLogger
1717
from scfw.package import Package

0 commit comments

Comments
 (0)