From 698b5fdb2d96a225c939a18cc739f0af06f3e098 Mon Sep 17 00:00:00 2001 From: Adrian Torres Date: Tue, 10 May 2022 19:05:58 +0200 Subject: [PATCH 1/2] pip_audit: provide has_any_id API for VulnerabilityResult This API allows developers to query whether a VulnerabilityResult contains at least one of the vulnerability IDs passed in as argument either as its ID or as one of the aliases. This method will return True if the vulnerability IDs passed in as argument intersect with the VulnerabilityResult's aliases + ID, False otherwise. --- pip_audit/_service/interface.py | 6 ++++++ test/service/test_interface.py | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/pip_audit/_service/interface.py b/pip_audit/_service/interface.py index 92f05507..3978ebb9 100644 --- a/pip_audit/_service/interface.py +++ b/pip_audit/_service/interface.py @@ -114,6 +114,12 @@ def merge_aliases(self, other: VulnerabilityResult) -> VulnerabilityResult: self.id, self.description, self.fix_versions, self.aliases | other.aliases - {self.id} ) + def has_any_id(self, ids: Set[str]) -> bool: + """ + Returns whether ids intersects with {id} | aliases. + """ + return bool(ids & (self.aliases | {self.id})) + class VulnerabilityService(ABC): """ diff --git a/test/service/test_interface.py b/test/service/test_interface.py index b10725e4..828eb401 100644 --- a/test/service/test_interface.py +++ b/test/service/test_interface.py @@ -62,3 +62,14 @@ def test_vulnerability_result_update_aliases(): merged = result1.merge_aliases(result2) assert merged.id == "FOO" assert merged.aliases == {"BAR", "BAZ", "ZAP", "QUUX"} + + +def test_vulnerability_result_has_any_id(): + result = VulnerabilityResult( + id="FOO", description="bar", fix_versions=[Version("1.0.0")], aliases={"BAR", "BAZ", "QUUX"} + ) + + assert result.has_any_id({"FOO"}) + assert result.has_any_id({"ham", "eggs", "BAZ"}) + assert not result.has_any_id({"zilch"}) + assert not result.has_any_id(set()) From 38ef6973e181bc12b6986c521693df2570994746 Mon Sep 17 00:00:00 2001 From: Adrian Torres Date: Tue, 10 May 2022 17:15:20 +0200 Subject: [PATCH 2/2] cli: allow ignoring specific vulnerability IDs This commit introduces a new flag to pip-audit's CLI (--ignore-vuln) that takes a string representing a vulnerability id, if any of the packages scanned detect said vulnerability, it will be ignored in the final report. This allows users of pip-audit to ignore certain vulnerabilities that they may deem not exploitable for whatever reason. The flag can be used multiple times to ignore multiple vulnerabilities. Closes #245 --- CHANGELOG.md | 8 ++++++++ README.md | 4 ++++ pip_audit/_cli.py | 26 ++++++++++++++++++++++++-- test/test_cli.py | 9 ++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7453b3c6..94a446ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). All versions prior to 0.0.9 are untracked. +## [Unreleased] + +### Added + +* CLI: The `--ignore-vuln` option has been added, allowing users to + specify vulnerability IDs to ignore during the final report. + ([#275](https://github.com/trailofbits/pip-audit/pull/275)) + ## [2.2.1] - 2022-05-02 ### Fixed diff --git a/README.md b/README.md index d34e928d..e44ef475 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] [-f FORMAT] [-s SERVICE] [--path PATHS] [-v] [--fix] [--require-hashes] [--index-url INDEX_URL] [--extra-index-url EXTRA_INDEX_URLS] [--skip-editable] [--no-deps] [-o FILE] + [--ignore-vuln IGNORE_VULNS] [project_path] audit the Python environment for dependencies with known vulnerabilities @@ -142,6 +143,9 @@ optional arguments: False) -o FILE, --output FILE output results to the given file (default: None) + --ignore-vuln IGNORE_VULNS + ignore a specific vulnerability by its vulnerability + ID (default: []) ``` diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 83789527..daa01e88 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -287,6 +287,14 @@ def _parser() -> argparse.ArgumentParser: # argparse's default renderer uses __repr__ and produces # a pretty unpleasant help message. ) + parser.add_argument( + "--ignore-vuln", + type=str, + action="append", + dest="ignore_vulns", + default=[], + help="ignore a specific vulnerability by its vulnerability ID", + ) return parser @@ -390,6 +398,8 @@ def audit() -> None: pkg_count = 0 vuln_count = 0 skip_count = 0 + vuln_ignore_count = 0 + vulns_to_ignore = set(args.ignore_vulns) for (spec, vulns) in auditor.audit(source): if spec.is_skipped(): spec = cast(SkippedDependency, spec) @@ -401,6 +411,10 @@ def audit() -> None: else: spec = cast(ResolvedDependency, spec) state.update_state(f"Auditing {spec.name} ({spec.version})") + if vulns_to_ignore: + filtered_vulns = [v for v in vulns if not v.has_any_id(vulns_to_ignore)] + vuln_ignore_count += len(vulns) - len(filtered_vulns) + vulns = filtered_vulns result[spec] = vulns if len(vulns) > 0: pkg_count += 1 @@ -442,7 +456,8 @@ def audit() -> None: if vuln_count > 0: summary_msg = ( f"Found {vuln_count} known " - f"{'vulnerability' if vuln_count == 1 else 'vulnerabilities'} " + f"{'vulnerability' if vuln_count == 1 else 'vulnerabilities'}" + f"{(vuln_ignore_count and ', ignored %d ' % vuln_ignore_count) or ' '}" f"in {pkg_count} {'package' if pkg_count == 1 else 'packages'}" ) if args.fix: @@ -457,7 +472,14 @@ def audit() -> None: if pkg_count != fixed_pkg_count: sys.exit(1) else: - print("No known vulnerabilities found", file=sys.stderr) + summary_msg = "No known vulnerabilities found" + if vuln_ignore_count: + summary_msg += f", {vuln_ignore_count} ignored" + + print( + summary_msg, + file=sys.stderr, + ) # If our output format is a "manifest" format we always emit it, # even if nothing other than a dependency summary is present. if skip_count > 0 or formatter.is_manifest: diff --git a/test/test_cli.py b/test/test_cli.py index c8c961b2..46a2b382 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -10,9 +10,12 @@ ([], 1, 1, "Found 1 known vulnerability in 1 package"), ([], 2, 1, "Found 2 known vulnerabilities in 1 package"), ([], 2, 2, "Found 2 known vulnerabilities in 2 packages"), + (["--ignore-vuln", "bar"], 2, 2, "Found 2 known vulnerabilities, ignored 1 in 2 packages"), (["--fix"], 1, 1, "fixed 1 vulnerability in 1 package"), (["--fix"], 2, 1, "fixed 2 vulnerabilities in 1 package"), (["--fix"], 2, 2, "fixed 2 vulnerabilities in 2 packages"), + ([], 0, 0, "No known vulnerabilities found"), + (["--ignore-vuln", "bar"], 0, 1, "No known vulnerabilities found, 1 ignored"), ], ) def test_plurals(capsys, monkeypatch, args, vuln_count, pkg_count, expected): @@ -30,11 +33,15 @@ def test_plurals(capsys, monkeypatch, args, vuln_count, pkg_count, expected): canonical_name="something" + str(i), version=1, ), - [pretend.stub(fix_versions=[2], id="foo")] * (vuln_count // pkg_count), + [pretend.stub(fix_versions=[2], id="foo", aliases=set(), has_any_id=lambda x: False)] + * (vuln_count // pkg_count), ) for i in range(pkg_count) ] + if "--ignore-vuln" in args: + result[0][1].append(pretend.stub(id="bar", aliases=set(), has_any_id=lambda x: True)) + auditor = pretend.stub(audit=lambda a: result) monkeypatch.setattr(pip_audit._cli, "Auditor", lambda *a, **kw: auditor)