From 165c98dbd1dc43515d914749267b59e51e7955c6 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:07:44 -0500 Subject: [PATCH 01/10] README, _cli: Add a `-S`, `--strict` mode --- README.md | 5 ++++- pip_audit/_cli.py | 21 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 668b6eaf..45b80e3c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ python -m pip install pip-audit ``` usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] [-f {columns,json,cyclonedx-json,cyclonedx-xml}] - [-s {osv,pypi}] [-d] [--desc {on,off,auto}] + [-s {osv,pypi}] [-d] [-S] [--desc {on,off,auto}] [--cache-dir CACHE_DIR] [--progress-spinner {on,off}] [--timeout TIMEOUT] @@ -49,6 +49,8 @@ optional arguments: against (default: pypi) -d, --dry-run collect all dependencies but do not perform the auditing step (default: False) + -S, --strict fail the entire audit if dependency collection on any + dependency (default: False) --desc {on,off,auto} include a description for each vulnerability; `auto` defaults to `on` for the `json` format. This flag has no effect on the `cyclonedx-json` or `cyclonedx-xml` @@ -59,6 +61,7 @@ optional arguments: --progress-spinner {on,off} display a progress spinner (default: on) --timeout TIMEOUT set the socket timeout (default: 15) + ``` diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 62ca6ac3..c949e0b2 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -9,7 +9,7 @@ import sys from contextlib import ExitStack from pathlib import Path -from typing import List, Optional, cast +from typing import List, NoReturn, Optional, cast from pip_audit import __version__ from pip_audit._audit import AuditOptions, Auditor @@ -117,6 +117,14 @@ def __str__(self): return self.value +def _fatal(msg: str) -> NoReturn: + """ + Log a fatal error to the standard error stream and exit. + """ + print(f"Fatal: {msg}", file=sys.stderr) + sys.exit(1) + + def audit() -> None: """ The primary entrypoint for `pip-audit`. @@ -163,6 +171,12 @@ def audit() -> None: action="store_true", help="collect all dependencies but do not perform the auditing step", ) + parser.add_argument( + "-S", + "--strict", + action="store_true", + help="fail the entire audit if dependency collection on any dependency", + ) parser.add_argument( "--desc", type=VulnerabilityDescriptionChoice, @@ -214,7 +228,10 @@ def audit() -> None: if state is not None: if spec.is_skipped(): spec = cast(SkippedDependency, spec) - state.update_state(f"Skipping {spec.name}: {spec.skip_reason}") + if args.strict: + _fatal(f"{spec.name}: {spec.skip_reason}") + else: + state.update_state(f"Skipping {spec.name}: {spec.skip_reason}") else: spec = cast(ResolvedDependency, spec) state.update_state(f"Auditing {spec.name} ({spec.version})") From e090fdab98a676032da9ef3e4c5bdd6e445f877f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:11:55 -0500 Subject: [PATCH 02/10] README: fix `--help` --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 45b80e3c..12b0c105 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,6 @@ optional arguments: --progress-spinner {on,off} display a progress spinner (default: on) --timeout TIMEOUT set the socket timeout (default: 15) - ``` From 69754537c13bf638ac2a093632edba4990634676 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:32:00 -0500 Subject: [PATCH 03/10] _cli: Use logger.error for _fatal --- pip_audit/_cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index c949e0b2..3ff35929 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -121,7 +121,9 @@ def _fatal(msg: str) -> NoReturn: """ Log a fatal error to the standard error stream and exit. """ - print(f"Fatal: {msg}", file=sys.stderr) + # NOTE: We buffer the logger when the progress spinner is active, + # ensuring that the fatal message is formatted on its own line. + logger.error(f"Fatal: {msg}", file=sys.stderr) sys.exit(1) From 065f4cfc7f850b4f405d1f6b31a6ef0539e01099 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:32:59 -0500 Subject: [PATCH 04/10] _cli: Fix logger.error call --- pip_audit/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 3ff35929..83c3d17d 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -123,7 +123,7 @@ def _fatal(msg: str) -> NoReturn: """ # NOTE: We buffer the logger when the progress spinner is active, # ensuring that the fatal message is formatted on its own line. - logger.error(f"Fatal: {msg}", file=sys.stderr) + logger.error(f"Fatal: {msg}") sys.exit(1) From 3a1b6994557139ff8c06c2fcd54276454eca7daf Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:33:49 -0500 Subject: [PATCH 05/10] _cli: Drop the "Fatal: " prefix for logged errors The logger already provides a reasonable prefix. --- pip_audit/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 83c3d17d..58073e68 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -123,7 +123,7 @@ def _fatal(msg: str) -> NoReturn: """ # NOTE: We buffer the logger when the progress spinner is active, # ensuring that the fatal message is formatted on its own line. - logger.error(f"Fatal: {msg}") + logger.error(msg) sys.exit(1) From 9627575f230c598909a534d9a80529f4bf4541aa Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:37:40 -0500 Subject: [PATCH 06/10] README, _cli: Fix a small typo --- README.md | 6 +++--- pip_audit/_cli.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 12b0c105..3d038b9e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] audit the Python environment for dependencies with known vulnerabilities -optional arguments: +options: -h, --help show this help message and exit -V, --version show program's version number and exit -l, --local show only results for dependencies in the local @@ -49,8 +49,8 @@ optional arguments: against (default: pypi) -d, --dry-run collect all dependencies but do not perform the auditing step (default: False) - -S, --strict fail the entire audit if dependency collection on any - dependency (default: False) + -S, --strict fail the entire audit if dependency collection fails + on any dependency (default: False) --desc {on,off,auto} include a description for each vulnerability; `auto` defaults to `on` for the `json` format. This flag has no effect on the `cyclonedx-json` or `cyclonedx-xml` diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 58073e68..a128fce4 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -177,7 +177,7 @@ def audit() -> None: "-S", "--strict", action="store_true", - help="fail the entire audit if dependency collection on any dependency", + help="fail the entire audit if dependency collection fails on any dependency", ) parser.add_argument( "--desc", From 57d244a2daadd656b15a4c5b902bb1e4c4f2c013 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:44:20 -0500 Subject: [PATCH 07/10] README: fix `--help` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d038b9e..bf4b1f7a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] audit the Python environment for dependencies with known vulnerabilities -options: +optional arguments: -h, --help show this help message and exit -V, --version show program's version number and exit -l, --local show only results for dependencies in the local From e852761c42490a7df624643758c762fbb1862fa1 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:44:28 -0500 Subject: [PATCH 08/10] pip_audit, test: use debug for skipped dep messages --- pip_audit/_dependency_source/pip.py | 2 +- pip_audit/_service/pypi.py | 2 +- test/dependency_source/test_pip.py | 6 +++--- test/service/test_pypi.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pip_audit/_dependency_source/pip.py b/pip_audit/_dependency_source/pip.py index 86c85073..e7aea739 100644 --- a/pip_audit/_dependency_source/pip.py +++ b/pip_audit/_dependency_source/pip.py @@ -74,7 +74,7 @@ def collect(self) -> Iterator[Dependency]: "Package has invalid version and could not be audited: " f"{dist.name} ({dist.version})" ) - logger.warning(f"Warning: {skip_reason}") + logger.debug(skip_reason) dep = SkippedDependency(name=dist.name, skip_reason=skip_reason) yield dep except Exception as e: diff --git a/pip_audit/_service/pypi.py b/pip_audit/_service/pypi.py index e5bc7eba..a1a70ea5 100644 --- a/pip_audit/_service/pypi.py +++ b/pip_audit/_service/pypi.py @@ -168,7 +168,7 @@ def query(self, spec: Dependency) -> Tuple[Dependency, List[VulnerabilityResult] "Dependency not found on PyPI and could not be audited: " f"{spec.canonical_name} ({spec.version})" ) - logger.warning(f"Warning: {skip_reason}") + logger.debug(skip_reason) return SkippedDependency(name=spec.name, skip_reason=skip_reason), [] raise ServiceError from http_error diff --git a/test/dependency_source/test_pip.py b/test/dependency_source/test_pip.py index df8b1170..3b9af5d9 100644 --- a/test/dependency_source/test_pip.py +++ b/test/dependency_source/test_pip.py @@ -49,7 +49,7 @@ def explode(): def test_pip_source_invalid_version(monkeypatch): - logger = pretend.stub(warning=pretend.call_recorder(lambda s: None)) + logger = pretend.stub(debug=pretend.call_recorder(lambda s: None)) monkeypatch.setattr(pip, "logger", logger) source = pip.PipSource() @@ -60,7 +60,7 @@ class MockDistribution: version: str # Return a distribution with a version that doesn't conform to PEP 440. - # We should log a warning and skip it. + # We should log a debug message and skip it. def mock_installed_distributions(local: bool) -> Dict[str, MockDistribution]: return { "pytest": MockDistribution("pytest", "0.1"), @@ -71,7 +71,7 @@ def mock_installed_distributions(local: bool) -> Dict[str, MockDistribution]: monkeypatch.setattr(pip_api, "installed_distributions", mock_installed_distributions) specs = list(source.collect()) - assert len(logger.warning.calls) == 1 + assert len(logger.debug.calls) == 1 assert len(specs) == 3 assert ResolvedDependency(name="pytest", version=Version("0.1")) in specs assert ( diff --git a/test/service/test_pypi.py b/test/service/test_pypi.py index 9f5efb36..b720aed3 100644 --- a/test/service/test_pypi.py +++ b/test/service/test_pypi.py @@ -49,8 +49,8 @@ def test_pypi_multiple_pkg(cache_dir): def test_pypi_http_notfound(monkeypatch, cache_dir): # If we get a "not found" response, that means that we're querying a package or version that - # isn't known to PyPI. If that's the case, we should just log a warning and continue on with - # the audit. + # isn't known to PyPI. If that's the case, we should just log a debug message and continue on + # with the audit. def get_error_response(): class MockResponse: # 404: Not Found @@ -64,7 +64,7 @@ def raise_for_status(self): monkeypatch.setattr( service.pypi, "_get_cached_session", lambda _: get_mock_session(get_error_response) ) - logger = pretend.stub(warning=pretend.call_recorder(lambda s: None)) + logger = pretend.stub(debug=pretend.call_recorder(lambda s: None)) monkeypatch.setattr(service.pypi, "logger", logger) pypi = service.PyPIService(cache_dir) @@ -80,7 +80,7 @@ def raise_for_status(self): assert skipped_dep in results assert dep not in results assert len(results[skipped_dep]) == 0 - assert len(logger.warning.calls) == 1 + assert len(logger.debug.calls) == 1 def test_pypi_http_error(monkeypatch, cache_dir): From 3278d6cf2b952da52fc1aaf59f9748a1d7c3cff3 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:57:50 -0500 Subject: [PATCH 09/10] README, _cli: judicious use of `--help` metavars --- README.md | 20 ++++++++++---------- pip_audit/_cli.py | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index bf4b1f7a..537b8f69 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,13 @@ python -m pip install pip-audit ``` -usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] - [-f {columns,json,cyclonedx-json,cyclonedx-xml}] - [-s {osv,pypi}] [-d] [-S] [--desc {on,off,auto}] - [--cache-dir CACHE_DIR] [--progress-spinner {on,off}] - [--timeout TIMEOUT] +usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] [-f FORMAT] [-s SERVICE] + [-d] [-S] [--desc {on,off,auto}] [--cache-dir CACHE_DIR] + [--progress-spinner {on,off}] [--timeout TIMEOUT] audit the Python environment for dependencies with known vulnerabilities -optional arguments: +options: -h, --help show this help message and exit -V, --version show program's version number and exit -l, --local show only results for dependencies in the local @@ -42,11 +40,13 @@ optional arguments: -r REQUIREMENTS, --requirement REQUIREMENTS audit the given requirements file; this option can be used multiple times (default: None) - -f {columns,json,cyclonedx-json,cyclonedx-xml}, --format {columns,json,cyclonedx-json,cyclonedx-xml} - the format to emit audit results in (default: columns) - -s {osv,pypi}, --vulnerability-service {osv,pypi} + -f FORMAT, --format FORMAT + the format to emit audit results in (choices: columns, + json, cyclonedx-json, cyclonedx-xml) (default: + columns) + -s SERVICE, --vulnerability-service SERVICE the vulnerability service to audit dependencies - against (default: pypi) + against (choices: osv, pypi) (default: pypi) -d, --dry-run collect all dependencies but do not perform the auditing step (default: False) -S, --strict fail the entire audit if dependency collection fails diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index a128fce4..17781df0 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -9,7 +9,7 @@ import sys from contextlib import ExitStack from pathlib import Path -from typing import List, NoReturn, Optional, cast +from typing import List, NoReturn, Optional, Type, cast from pip_audit import __version__ from pip_audit._audit import AuditOptions, Auditor @@ -117,6 +117,13 @@ def __str__(self): return self.value +def _enum_help(msg: str, e: Type[enum.Enum]) -> str: + """ + Render a `--help`-style string for the given enumeration. + """ + return f"{msg} (choices: {', '.join(str(v) for v in e)})" + + def _fatal(msg: str) -> NoReturn: """ Log a fatal error to the standard error stream and exit. @@ -157,7 +164,8 @@ def audit() -> None: type=OutputFormatChoice, choices=OutputFormatChoice, default=OutputFormatChoice.Columns, - help="the format to emit audit results in", + metavar="FORMAT", + help=_enum_help("the format to emit audit results in", OutputFormatChoice), ) parser.add_argument( "-s", @@ -165,7 +173,10 @@ def audit() -> None: type=VulnerabilityServiceChoice, choices=VulnerabilityServiceChoice, default=VulnerabilityServiceChoice.Pypi, - help="the vulnerability service to audit dependencies against", + metavar="SERVICE", + help=_enum_help( + "the vulnerability service to audit dependencies against", VulnerabilityServiceChoice + ), ) parser.add_argument( "-d", From 15f853dc6c8af887a4987c42d585626a68662899 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 30 Nov 2021 15:59:35 -0500 Subject: [PATCH 10/10] README: fix `--help` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 537b8f69..875d5625 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] [-f FORMAT] [-s SERVICE] audit the Python environment for dependencies with known vulnerabilities -options: +optional arguments: -h, --help show this help message and exit -V, --version show program's version number and exit -l, --local show only results for dependencies in the local