Skip to content

Commit a444455

Browse files
authored
[SECRES-2471] Fix logging to respect configured log level (#37)
1 parent 8140a3b commit a444455

File tree

11 files changed

+54
-47
lines changed

11 files changed

+54
-47
lines changed

README.md

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
![Test](https://github.com/DataDog/supply-chain-firewall/actions/workflows/test.yaml/badge.svg)
44
![Code quality](https://github.com/DataDog/supply-chain-firewall/actions/workflows/code_quality.yaml/badge.svg)
55

6-
The supply-chain firewall is a command-line tool for preventing the installation of malicious PyPI and npm packages. It is intended primarily for use by engineers to protect their development workstations from compromise in a supply-chain attack.
6+
<p align="center">
7+
<img src="./images/logo.png" alt="Supply-Chain Firewall" width="300" />
8+
</p>
79

8-
![scfw demo usage](images/demo.png)
10+
Supply-Chain Firewall is a command-line tool for preventing the installation of malicious PyPI and npm packages. It is intended primarily for use by engineers to protect their development workstations from compromise in a supply-chain attack.
911

10-
The firewall collects all targets that would be installed by a given `pip` or `npm` command and checks them against reputable sources of data on open source malware and vulnerabilities. The command is automatically blocked when any data source finds that any target is malicious. In cases where a data source reports other findings for a target, the findings are presented to the user along with a prompt confirming intent to proceed with the installation.
12+
![scfw demo usage](images/demo.gif)
13+
14+
Supply-Chain Firewall collects all targets that would be installed by a given `pip` or `npm` command and checks them against reputable sources of data on open-source malware and vulnerabilities. The command is automatically blocked when any data source finds that any target is malicious. In cases where a data source reports other findings for a target, they are presented to the user along with a prompt confirming intent to proceed with the installation.
1115

1216
Default data sources include:
1317

@@ -16,7 +20,7 @@ Default data sources include:
1620

1721
Users may also implement verifiers for alternative data sources. A template for implementating custom verifiers may be found in `examples/verifier.py`. Details may also be found in the API documentation.
1822

19-
The principal goal of the supply-chain firewall is to block 100% of installations of known-malicious packages within the purview of its data sources.
23+
The principal goal of Supply-Chain Firewall is to block 100% of installations of known-malicious packages within the purview of its data sources.
2024

2125
## Getting started
2226

@@ -33,12 +37,12 @@ make install
3337
To check whether the installation succeeded, run the following command and verify that you see output similar to the following.
3438
```bash
3539
$ scfw --version
36-
1.0.0
40+
1.0.1
3741
```
3842

3943
### Post-installation steps
4044

41-
To get the most out of the supply-chain firewall, it is recommended to run the `scfw configure` command after installation. This script will walk you through configuring your environment so that all `pip` or `npm` commands are passively run through the firewall as well as enabling Datadog logging, described in more detail below.
45+
To get the most out of Supply-Chain Firewall, it is recommended to run the `scfw configure` command after installation. This script will walk you through configuring your environment so that all `pip` or `npm` commands are passively run through `scfw` as well as enabling Datadog logging, described in more detail below.
4246

4347
```bash
4448
$ scfw configure
@@ -47,40 +51,45 @@ $ scfw configure
4751

4852
### Compatibility
4953

50-
The supply-chain firewall is compatible with `pip >= 22.2` and `npm >= 7.0`. In keeping with its goal of blocking 100% of known-malicious package installations, the firewall will refuse to run with an incompatible version of `pip` or `npm`. Please upgrade to or verify that you are running a compatible version of `pip` or `npm` before using this tool.
54+
| Package manager | Compatible versions |
55+
| :---------------: | :-------------------: |
56+
| npm | >= 7.0 |
57+
| pip | >= 22.2 |
58+
59+
In keeping with its goal of blocking 100% of known-malicious package installations, `scfw` will refuse to run with an incompatible version of a supported package manager. Please upgrade to or verify that you are running a compatible version before using this tool.
5160

52-
Currently, the supply-chain firewall is only fully supported on macOS systems, though it should run as intended on most common Linux distributions. It is currently not supported on Windows.
61+
Currently, Supply-Chain Firewall is only fully supported on macOS systems, though it should run as intended on most common Linux distributions. It is currently not supported on Windows.
5362

5463
## Usage
5564

56-
To use the supply-chain firewall, just prepend `scfw run` to the `pip install` or `npm install` command you want to run.
65+
To use Supply-Chain Firewall, prepend `scfw run` to the `pip install` or `npm install` command you want to run.
5766

5867
```
5968
$ scfw run npm install react
6069
$ scfw run pip install -r requirements.txt
6170
```
6271

63-
For `pip install` commands, the firewall will install packages in the same environment (virtual or global) in which the command was run.
72+
For `pip install` commands, packages will be installed in the same environment (virtual or global) in which the command was run.
6473

65-
## Limitations
74+
### Limitations
6675

67-
Unlike `pip`, a variety of `npm` operations beyond `npm install` can end up installing new packages. For now, only `npm install` commands are in scope for analysis with the supply-chain firewall. We are hoping to extend the firewall's purview to other "installish" `npm` commands over time.
76+
Unlike `pip`, a variety of `npm` operations beyond `npm install` can end up installing new packages. For now, only `npm install` commands are in Supply-Chain Firewall's scope. We are hoping to extend the tool's purview to other "installish" `npm` commands over time.
6877

6978
## Datadog Logs integration
7079

71-
The supply-chain firewall can optionally send logs of blocked and successful installations to Datadog.
80+
Supply-Chain Firewall can optionally send logs of blocked and successful installations to Datadog.
7281

7382
![scfw datadog log](images/datadog_log.png)
7483

7584
To opt in, set the environment variable `DD_API_KEY` to your Datadog API key, either directly in your shell environment or in a `.env` file in the current working directory. A logging level may also be selected by setting the environment variable `SCFW_DD_LOG_LEVEL` to one of `ALLOW`, `ABORT` or `BLOCK`. The `BLOCK` level only logs blocked installations, `ABORT` logs blocked and aborted installations, and `ALLOW` logs these as well as successful installations. The `BLOCK` level is set by default, i.e., when `SCFW_DD_LOG_LEVEL` is either not set or does not contain a valid log level.
7685

7786
You can also use the `scfw configure` command to walk through the steps of configuring your environment for Datadog logging.
7887

79-
The firewall can integrate with user-supplied loggers. A template for implementating a custom logger may be found in `examples/logger.py`. Refer to the API documentation for details.
88+
Supply-Chain Firewall can integrate with user-supplied loggers. A template for implementating a custom logger may be found in `examples/logger.py`. Refer to the API documentation for details.
8089

8190
## Development
8291

83-
We welcome community contributions to the supply-chain firewall. Refer to the [CONTRIBUTING](./CONTRIBUTING.md) guide for instructions on building the API documentation and setting up for development.
92+
We welcome community contributions to Supply-Chain Firewall. Refer to the [CONTRIBUTING](./CONTRIBUTING.md) guide for instructions on building the API documentation and setting up for development.
8493

8594
## Maintainers
8695

images/datadog_log.png

-12.9 KB
Loading

images/demo.gif

464 KB
Loading

images/demo.png

-558 KB
Binary file not shown.

images/logo.png

165 KB
Loading

scfw/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
A supply-chain "firewall" for preventing the installation of vulnerable or malicious `pip` and `npm` packages.
33
"""
44

5-
__version__ = "1.0.0"
5+
__version__ = "1.0.1"

scfw/cli.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def _cli() -> ArgumentParser:
130130
help="Desired logging level (default: %(default)s, options: %(choices)s)"
131131
)
132132

133-
subparsers = parser.add_subparsers(dest="subcommand")
133+
subparsers = parser.add_subparsers(dest="subcommand", required=True)
134134

135135
for subcommand in Subcommand:
136136
subparser = subparsers.add_parser(subcommand.value, **subcommand._parser_spec())
@@ -168,16 +168,15 @@ def _parse_command_line(argv: list[str]) -> tuple[Optional[Namespace], str]:
168168
try:
169169
args = parser.parse_args(argv[1:hinge])
170170

171-
# TODO(ikretz): Use `Subcommand` here instead of strings
172171
# Only allow a package manager `command` argument when
173172
# the user selected the `run` subcommand
174-
match args.subcommand == "run", hinge == len(argv):
175-
case True, False:
176-
# `run` subcommand with `command` argument
173+
match Subcommand(args.subcommand), argv[hinge:]:
174+
case Subcommand.Run, []:
175+
raise ArgumentError
176+
case Subcommand.Run, _:
177177
args_dict = vars(args)
178178
args_dict["command"] = argv[hinge:]
179-
case False, True:
180-
# Non-`run` subcommand, no `command` argument
179+
case _, []:
181180
pass
182181
case _:
183182
raise ArgumentError

scfw/configure.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ def run_configure(args: Namespace) -> int:
5050
print(_GREETING)
5151

5252
answers = inquirer.prompt(_get_questions())
53-
if (config := _format_answers(answers)):
54-
for file in [Path.home() / file for file in _CONFIG_FILES]:
55-
_update_config_file(file, config)
53+
for file in [Path.home() / file for file in _CONFIG_FILES]:
54+
_update_config_file(file, _format_answers(answers))
5655

5756
print(_EPILOGUE)
5857

@@ -141,7 +140,11 @@ def enclose(config: str) -> str:
141140
with open(config_file) as f:
142141
contents = f.read()
143142

144-
updated = re.sub(f"{_BLOCK_START}(.*?){_BLOCK_END}", enclose(config), contents, flags=re.DOTALL)
143+
pattern = f"{_BLOCK_START}(.*?){_BLOCK_END}"
144+
if not config:
145+
pattern = f"\n{pattern}\n"
146+
147+
updated = re.sub(pattern, enclose(config) if config else '', contents, flags=re.DOTALL)
145148
if updated == contents and config not in contents:
146149
updated = f"{contents}\n{enclose(config)}\n"
147150

scfw/loggers/dd_logger.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import socket
88

9+
import scfw
910
from scfw.configure import DD_API_KEY_VAR, DD_LOG_LEVEL_VAR
1011
from scfw.ecosystem import ECOSYSTEM
1112
from scfw.logger import FirewallAction, FirewallLogger
@@ -32,7 +33,7 @@ class _DDLogHandler(logging.Handler):
3233
"""
3334
DD_SOURCE = "scfw"
3435
DD_ENV = "dev"
35-
DD_VERSION = "0.1.0"
36+
DD_VERSION = scfw.__version__
3637

3738
def __init__(self):
3839
super().__init__()
@@ -48,10 +49,8 @@ def emit(self, record):
4849
env = self.DD_ENV
4950
if not (service := os.getenv("DD_SERVICE")):
5051
service = record.__dict__.get("ecosystem", self.DD_SOURCE)
51-
if not (version := os.getenv("DD_VERSION")):
52-
version = self.DD_VERSION
5352

54-
usm_tags = {f"env:{env}", f"version:{version}"}
53+
usm_tags = {f"env:{env}", f"version:{self.DD_VERSION}"}
5554

5655
targets = record.__dict__.get("targets", {})
5756
target_tags = set(map(lambda e: f"target:{e}", targets))

scfw/main.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import scfw.configure as configure
1111
import scfw.firewall as firewall
1212

13+
_log = logging.getLogger(__name__)
14+
1315

1416
def main() -> int:
1517
"""
@@ -21,14 +23,13 @@ def main() -> int:
2123
args, help = cli.parse_command_line()
2224

2325
if not args:
24-
print(help)
26+
print(help, end='')
2527
return 0
2628

27-
log = _root_logger()
28-
log.setLevel(args.log_level)
29+
_configure_logging(args.log_level)
2930

30-
log.info(f"Starting supply-chain firewall on {time.asctime(time.localtime())}")
31-
log.debug(f"Command line: {vars(args)}")
31+
_log.info(f"Starting Supply-Chain Firewall on {time.asctime(time.localtime())}")
32+
_log.debug(f"Command line: {vars(args)}")
3233

3334
match Subcommand(args.subcommand):
3435
case Subcommand.Configure:
@@ -39,17 +40,17 @@ def main() -> int:
3940
return 0
4041

4142

42-
def _root_logger() -> logging.Logger:
43+
def _configure_logging(level: int) -> None:
4344
"""
44-
Configure the root logger and return a handle to it.
45+
Configure the root logger.
4546
46-
Returns:
47-
A handle to the configured root logger.
47+
Args:
48+
level: The log level selected by the user.
4849
"""
4950
handler = logging.StreamHandler()
51+
handler.addFilter(logging.Filter(name="scfw"))
5052
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
5153

5254
log = logging.getLogger()
5355
log.addHandler(handler)
54-
55-
return log
56+
log.setLevel(level)

0 commit comments

Comments
 (0)