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/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()) 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)