From b9c44051bf2ff40c710d9286352ac8b6654ced3d Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 11:17:27 +1100 Subject: [PATCH 01/10] _format: Add `--fix` information to `VulnerabilityService` interface --- pip_audit/_cli.py | 2 +- pip_audit/_format/columns.py | 7 ++++++- pip_audit/_format/cyclonedx.py | 13 ++++++++++++- pip_audit/_format/interface.py | 5 ++++- pip_audit/_format/json.py | 7 ++++++- test/format/test_columns.py | 7 +++---- test/format/test_cyclonedx.py | 6 +++--- test/format/test_json.py | 6 +++--- 8 files changed, 38 insertions(+), 15 deletions(-) diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index a5608509..6a8fb97b 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -313,7 +313,7 @@ def audit() -> None: f" and fixed {fixed_vuln_count} vulnerabilities in {fixed_pkg_count} packages" ) print(summary_msg, file=sys.stderr) - print(formatter.format(result)) + print(formatter.format(result, fixes)) if pkg_count != fixed_pkg_count: sys.exit(1) else: diff --git a/pip_audit/_format/columns.py b/pip_audit/_format/columns.py index 69ccee0c..c40a22c7 100644 --- a/pip_audit/_format/columns.py +++ b/pip_audit/_format/columns.py @@ -7,6 +7,7 @@ from packaging.version import Version +import pip_audit._fix as fix import pip_audit._service as service from .interface import VulnerabilityFormat @@ -39,7 +40,11 @@ def __init__(self, output_desc: bool): """ self.output_desc = output_desc - def format(self, result: Dict[service.Dependency, List[service.VulnerabilityResult]]) -> str: + def format( + self, + result: Dict[service.Dependency, List[service.VulnerabilityResult]], + fixes: List[fix.FixVersion], + ) -> str: """ Returns a column formatted string for a given mapping of dependencies to vulnerability results. diff --git a/pip_audit/_format/cyclonedx.py b/pip_audit/_format/cyclonedx.py index 1b91bdff..6ce19b95 100644 --- a/pip_audit/_format/cyclonedx.py +++ b/pip_audit/_format/cyclonedx.py @@ -3,6 +3,7 @@ """ import enum +import logging from typing import Dict, List, cast from cyclonedx import output @@ -11,10 +12,13 @@ from cyclonedx.model.vulnerability import Vulnerability from cyclonedx.parser import BaseParser +import pip_audit._fix as fix import pip_audit._service as service from .interface import VulnerabilityFormat +logger = logging.getLogger(__name__) + class _PipAuditResultParser(BaseParser): def __init__(self, result: Dict[service.Dependency, List[service.VulnerabilityResult]]): @@ -64,13 +68,20 @@ def __init__(self, inner_format: "CycloneDxFormat.InnerFormat"): self._inner_format = inner_format - def format(self, result: Dict[service.Dependency, List[service.VulnerabilityResult]]) -> str: + def format( + self, + result: Dict[service.Dependency, List[service.VulnerabilityResult]], + fixes: List[fix.FixVersion], + ) -> str: """ Returns a CycloneDX formatted string for a given mapping of dependencies to vulnerability results. See `VulnerabilityFormat.format`. """ + if fixes: + logger.warn("--fix output is unsupported by CycloneDX formats") + parser = _PipAuditResultParser(result) bom = Bom.from_parser(parser) diff --git a/pip_audit/_format/interface.py b/pip_audit/_format/interface.py index 0d3dc553..a9300c6d 100644 --- a/pip_audit/_format/interface.py +++ b/pip_audit/_format/interface.py @@ -4,6 +4,7 @@ from abc import ABC from typing import Dict, List +import pip_audit._fix as fix import pip_audit._service as service @@ -13,7 +14,9 @@ class VulnerabilityFormat(ABC): """ def format( - self, result: Dict[service.Dependency, List[service.VulnerabilityResult]] + self, + result: Dict[service.Dependency, List[service.VulnerabilityResult]], + fixes: List[fix.FixVersion], ) -> str: # pragma: no cover """ Convert a mapping of dependencies to vulnerabilities into a string. diff --git a/pip_audit/_format/json.py b/pip_audit/_format/json.py index c21594b6..48b4e2a7 100644 --- a/pip_audit/_format/json.py +++ b/pip_audit/_format/json.py @@ -5,6 +5,7 @@ import json from typing import Any, Dict, List, cast +import pip_audit._fix as fix import pip_audit._service as service from .interface import VulnerabilityFormat @@ -25,7 +26,11 @@ def __init__(self, output_desc: bool): """ self.output_desc = output_desc - def format(self, result: Dict[service.Dependency, List[service.VulnerabilityResult]]) -> str: + def format( + self, + result: Dict[service.Dependency, List[service.VulnerabilityResult]], + fixes: List[fix.FixVersion], + ) -> str: """ Returns a JSON formatted string for a given mapping of dependencies to vulnerability results. diff --git a/test/format/test_columns.py b/test/format/test_columns.py index df392117..1a1d58cc 100644 --- a/test/format/test_columns.py +++ b/test/format/test_columns.py @@ -8,7 +8,7 @@ def test_columns(vuln_data): foo 1.0 VULN-0 1.1,1.4 The first vulnerability foo 1.0 VULN-1 1.0 The second vulnerability bar 0.1 VULN-2 The third vulnerability""" - assert columns_format.format(vuln_data) == expected_columns + assert columns_format.format(vuln_data, list()) == expected_columns def test_columns_no_desc(vuln_data): @@ -18,7 +18,7 @@ def test_columns_no_desc(vuln_data): foo 1.0 VULN-0 1.1,1.4 foo 1.0 VULN-1 1.0 bar 0.1 VULN-2""" - assert columns_format.format(vuln_data) == expected_columns + assert columns_format.format(vuln_data, list()) == expected_columns def test_columns_skipped_dep(vuln_data_skipped_dep): @@ -29,5 +29,4 @@ def test_columns_skipped_dep(vuln_data_skipped_dep): Name Skip Reason ---- ----------- bar skip-reason""" - print(columns_format.format(vuln_data_skipped_dep)) - assert columns_format.format(vuln_data_skipped_dep) == expected_columns + assert columns_format.format(vuln_data_skipped_dep, list()) == expected_columns diff --git a/test/format/test_cyclonedx.py b/test/format/test_cyclonedx.py index 114e13c7..6aee526b 100644 --- a/test/format/test_cyclonedx.py +++ b/test/format/test_cyclonedx.py @@ -9,7 +9,7 @@ def test_cyclonedx_inner_json(vuln_data): # We don't test CycloneDX's formatting/layout decisions, only that # the formatter emits correct JSON when initialized in JSON mode. - assert json.loads(formatter.format(vuln_data)) is not None + assert json.loads(formatter.format(vuln_data, list())) is not None def test_cyclonedx_inner_xml(vuln_data): @@ -17,11 +17,11 @@ def test_cyclonedx_inner_xml(vuln_data): # We don't test CycloneDX's formatting/layout decisions, only that # the formatter emits correct XML when initialized in XML mode. - assert ET.fromstring(formatter.format(vuln_data)) is not None + assert ET.fromstring(formatter.format(vuln_data, list())) is not None def test_cyclonedx_skipped_dep(vuln_data_skipped_dep): formatter = CycloneDxFormat(inner_format=CycloneDxFormat.InnerFormat.Json) # Just test that a skipped dependency doesn't cause the formatter to blow up - assert json.loads(formatter.format(vuln_data_skipped_dep)) is not None + assert json.loads(formatter.format(vuln_data_skipped_dep, list())) is not None diff --git a/test/format/test_json.py b/test/format/test_json.py index dff8cf0a..2e341c39 100644 --- a/test/format/test_json.py +++ b/test/format/test_json.py @@ -33,7 +33,7 @@ def test_json(vuln_data): ], }, ] - assert json_format.format(vuln_data) == json.dumps(expected_json) + assert json_format.format(vuln_data, list()) == json.dumps(expected_json) def test_json_no_desc(vuln_data): @@ -62,7 +62,7 @@ def test_json_no_desc(vuln_data): "vulns": [{"id": "VULN-2", "fix_versions": []}], }, ] - assert json_format.format(vuln_data) == json.dumps(expected_json) + assert json_format.format(vuln_data, list()) == json.dumps(expected_json) def test_json_skipped_dep(vuln_data_skipped_dep): @@ -86,4 +86,4 @@ def test_json_skipped_dep(vuln_data_skipped_dep): "skip_reason": "skip-reason", }, ] - assert json_format.format(vuln_data_skipped_dep) == json.dumps(expected_json) + assert json_format.format(vuln_data_skipped_dep, list()) == json.dumps(expected_json) From 705686c6e2c54f5a5b4ed91b1c95c10a43ef969b Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 11:37:57 +1100 Subject: [PATCH 02/10] json: Add fixes to JSON format --- pip_audit/_format/json.py | 25 ++++++- test/format/test_json.py | 151 ++++++++++++++++++++------------------ 2 files changed, 103 insertions(+), 73 deletions(-) diff --git a/pip_audit/_format/json.py b/pip_audit/_format/json.py index 48b4e2a7..ca2fe69b 100644 --- a/pip_audit/_format/json.py +++ b/pip_audit/_format/json.py @@ -37,9 +37,15 @@ def format( See `VulnerabilityFormat.format`. """ - output_json = [] + output_json = {} + dep_json = [] for dep, vulns in result.items(): - output_json.append(self._format_dep(dep, vulns)) + dep_json.append(self._format_dep(dep, vulns)) + output_json["dependencies"] = dep_json + fix_json = [] + for f in fixes: + fix_json.append(self._format_fix(f)) + output_json["fixes"] = fix_json return json.dumps(output_json) def _format_dep( @@ -67,3 +73,18 @@ def _format_vuln(self, vuln: service.VulnerabilityResult) -> Dict[str, Any]: if self.output_desc: vuln_json["description"] = vuln.description return vuln_json + + def _format_fix(self, fix_version: fix.FixVersion) -> Dict[str, Any]: + if fix_version.is_skipped(): + fix_version = cast(fix.SkippedFixVersion, fix_version) + return { + "name": fix_version.dep.canonical_name, + "version": str(fix_version.dep.version), + "skip_reason": fix_version.skip_reason, + } + fix_version = cast(fix.ResolvedFixVersion, fix_version) + return { + "name": fix_version.dep.canonical_name, + "old_version": str(fix_version.dep.version), + "new_version": str(fix_version.version), + } diff --git a/test/format/test_json.py b/test/format/test_json.py index 2e341c39..1bcc9874 100644 --- a/test/format/test_json.py +++ b/test/format/test_json.py @@ -5,85 +5,94 @@ def test_json(vuln_data): json_format = format.JsonFormat(True) - expected_json = [ - { - "name": "foo", - "version": "1.0", - "vulns": [ - { - "id": "VULN-0", - "fix_versions": [ - "1.1", - "1.4", - ], - "description": "The first vulnerability", - }, - { - "id": "VULN-1", - "fix_versions": ["1.0"], - "description": "The second vulnerability", - }, - ], - }, - { - "name": "bar", - "version": "0.1", - "vulns": [ - {"id": "VULN-2", "fix_versions": [], "description": "The third vulnerability"} - ], - }, - ] + expected_json = { + "dependencies": [ + { + "name": "foo", + "version": "1.0", + "vulns": [ + { + "id": "VULN-0", + "fix_versions": [ + "1.1", + "1.4", + ], + "description": "The first vulnerability", + }, + { + "id": "VULN-1", + "fix_versions": ["1.0"], + "description": "The second vulnerability", + }, + ], + }, + { + "name": "bar", + "version": "0.1", + "vulns": [ + {"id": "VULN-2", "fix_versions": [], "description": "The third vulnerability"} + ], + }, + ], + "fixes": [], + } assert json_format.format(vuln_data, list()) == json.dumps(expected_json) def test_json_no_desc(vuln_data): json_format = format.JsonFormat(False) - expected_json = [ - { - "name": "foo", - "version": "1.0", - "vulns": [ - { - "id": "VULN-0", - "fix_versions": [ - "1.1", - "1.4", - ], - }, - { - "id": "VULN-1", - "fix_versions": ["1.0"], - }, - ], - }, - { - "name": "bar", - "version": "0.1", - "vulns": [{"id": "VULN-2", "fix_versions": []}], - }, - ] + expected_json = { + "dependencies": [ + { + "name": "foo", + "version": "1.0", + "vulns": [ + { + "id": "VULN-0", + "fix_versions": [ + "1.1", + "1.4", + ], + }, + { + "id": "VULN-1", + "fix_versions": ["1.0"], + }, + ], + }, + { + "name": "bar", + "version": "0.1", + "vulns": [{"id": "VULN-2", "fix_versions": []}], + }, + ], + "fixes": [], + } assert json_format.format(vuln_data, list()) == json.dumps(expected_json) def test_json_skipped_dep(vuln_data_skipped_dep): json_format = format.JsonFormat(False) - expected_json = [ - { - "name": "foo", - "version": "1.0", - "vulns": [ - { - "id": "VULN-0", - "fix_versions": [ - "1.1", - "1.4", - ], - }, - ], - }, - { - "name": "bar", - "skip_reason": "skip-reason", - }, - ] + expected_json = { + "dependencies": [ + { + "name": "foo", + "version": "1.0", + "vulns": [ + { + "id": "VULN-0", + "fix_versions": [ + "1.1", + "1.4", + ], + }, + ], + }, + { + "name": "bar", + "skip_reason": "skip-reason", + }, + ], + "fixes": [], + } assert json_format.format(vuln_data_skipped_dep, list()) == json.dumps(expected_json) From 83d81ec80086c88c188394126f2d52e63acbb1bc Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 13:04:45 +1100 Subject: [PATCH 03/10] columns: Add fixes to Columns format --- pip_audit/_format/columns.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pip_audit/_format/columns.py b/pip_audit/_format/columns.py index c40a22c7..5a7fbb20 100644 --- a/pip_audit/_format/columns.py +++ b/pip_audit/_format/columns.py @@ -3,7 +3,7 @@ """ from itertools import zip_longest -from typing import Any, Dict, Iterable, List, Tuple, cast +from typing import Any, Dict, Iterable, List, Optional, Tuple, cast from packaging.version import Version @@ -55,13 +55,16 @@ def format( header = ["Name", "Version", "ID", "Fix Versions"] if self.output_desc: header.append("Description") + if fixes: + header.append("Applied Fix") vuln_data.append(header) for dep, vulns in result.items(): if dep.is_skipped(): continue dep = cast(service.ResolvedDependency, dep) + applied_fix = next((f for f in fixes if f.dep == dep), None) for vuln in vulns: - vuln_data.append(self._format_vuln(dep, vuln)) + vuln_data.append(self._format_vuln(dep, vuln, applied_fix)) vuln_strings, sizes = tabulate(vuln_data) @@ -101,7 +104,10 @@ def format( return columns_string def _format_vuln( - self, dep: service.ResolvedDependency, vuln: service.VulnerabilityResult + self, + dep: service.ResolvedDependency, + vuln: service.VulnerabilityResult, + applied_fix: Optional[fix.FixVersion], ) -> List[Any]: vuln_data = [ dep.canonical_name, @@ -111,6 +117,8 @@ def _format_vuln( ] if self.output_desc: vuln_data.append(vuln.description) + if applied_fix is not None: + vuln_data.append(self._format_applied_fix(applied_fix)) return vuln_data def _format_fix_versions(self, fix_versions: List[Version]) -> str: @@ -121,3 +129,16 @@ def _format_skipped_dep(self, dep: service.SkippedDependency) -> List[Any]: dep.canonical_name, dep.skip_reason, ] + + def _format_applied_fix(self, applied_fix: fix.FixVersion) -> str: + if applied_fix.is_skipped(): + applied_fix = cast(fix.SkippedFixVersion, applied_fix) + return ( + f"Failed to fix {applied_fix.dep.canonical_name} ({applied_fix.dep.version}): " + f"{applied_fix.skip_reason}" + ) + applied_fix = cast(fix.ResolvedFixVersion, applied_fix) + return ( + f"Successfully upgraded {applied_fix.dep.canonical_name} ({applied_fix.dep.version} " + f"=> {applied_fix.version})" + ) From c88b51b67a98f6b469dd0ec8ce113c82fcb31a2b Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 14:31:37 +1100 Subject: [PATCH 04/10] columns: Place applied fix column in front of description --- pip_audit/_format/columns.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pip_audit/_format/columns.py b/pip_audit/_format/columns.py index 5a7fbb20..fc808c93 100644 --- a/pip_audit/_format/columns.py +++ b/pip_audit/_format/columns.py @@ -53,10 +53,10 @@ def format( """ vuln_data: List[List[Any]] = [] header = ["Name", "Version", "ID", "Fix Versions"] - if self.output_desc: - header.append("Description") if fixes: header.append("Applied Fix") + if self.output_desc: + header.append("Description") vuln_data.append(header) for dep, vulns in result.items(): if dep.is_skipped(): @@ -115,10 +115,10 @@ def _format_vuln( vuln.id, self._format_fix_versions(vuln.fix_versions), ] - if self.output_desc: - vuln_data.append(vuln.description) if applied_fix is not None: vuln_data.append(self._format_applied_fix(applied_fix)) + if self.output_desc: + vuln_data.append(vuln.description) return vuln_data def _format_fix_versions(self, fix_versions: List[Version]) -> str: From 996b5526b99cc34b14393527215832291927a46b Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 15:17:14 +1100 Subject: [PATCH 05/10] test: Test columns formatting --- test/format/conftest.py | 33 +++++++++++++++++++++++++++++---- test/format/test_columns.py | 20 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/test/format/conftest.py b/test/format/conftest.py index b3ae6004..189589f9 100644 --- a/test/format/conftest.py +++ b/test/format/conftest.py @@ -3,10 +3,15 @@ import pytest from packaging.version import Version +import pip_audit._fix as fix import pip_audit._service as service +_RESOLVED_DEP_FOO = service.ResolvedDependency(name="foo", version=Version("1.0")) +_RESOLVED_DEP_BAR = service.ResolvedDependency(name="bar", version=Version("0.1")) +_SKIPPED_DEP = service.SkippedDependency(name="bar", skip_reason="skip-reason") + _TEST_VULN_DATA: Dict[service.Dependency, List[service.VulnerabilityResult]] = { - service.ResolvedDependency(name="foo", version=Version("1.0")): [ + _RESOLVED_DEP_FOO: [ service.VulnerabilityResult( id="VULN-0", description="The first vulnerability", @@ -21,7 +26,7 @@ fix_versions=[Version("1.0")], ), ], - service.ResolvedDependency(name="bar", version=Version("0.1")): [ + _RESOLVED_DEP_BAR: [ service.VulnerabilityResult( id="VULN-2", description="The third vulnerability", @@ -31,7 +36,7 @@ } _TEST_VULN_DATA_SKIPPED_DEP: Dict[service.Dependency, List[service.VulnerabilityResult]] = { - service.ResolvedDependency(name="foo", version=Version("1.0")): [ + _RESOLVED_DEP_FOO: [ service.VulnerabilityResult( id="VULN-0", description="The first vulnerability", @@ -41,9 +46,19 @@ ], ), ], - service.SkippedDependency(name="bar", skip_reason="skip-reason"): [], + _SKIPPED_DEP: [], } +_TEST_FIX_DATA: List[fix.FixVersion] = [ + fix.ResolvedFixVersion(dep=_RESOLVED_DEP_FOO, version=Version("1.8")), + fix.ResolvedFixVersion(dep=_RESOLVED_DEP_BAR, version=Version("0.3")), +] + +_TEST_SKIPPED_FIX_DATA: List[fix.FixVersion] = [ + fix.ResolvedFixVersion(dep=_RESOLVED_DEP_FOO, version=Version("1.8")), + fix.SkippedFixVersion(dep=_RESOLVED_DEP_BAR, skip_reason="skip-reason"), +] + @pytest.fixture(autouse=True) def vuln_data(): @@ -53,3 +68,13 @@ def vuln_data(): @pytest.fixture(autouse=True) def vuln_data_skipped_dep(): return _TEST_VULN_DATA_SKIPPED_DEP + + +@pytest.fixture(autouse=True) +def fix_data(): + return _TEST_FIX_DATA + + +@pytest.fixture(autouse=True) +def skipped_fix_data(): + return _TEST_SKIPPED_FIX_DATA diff --git a/test/format/test_columns.py b/test/format/test_columns.py index 1a1d58cc..f6a4d33e 100644 --- a/test/format/test_columns.py +++ b/test/format/test_columns.py @@ -30,3 +30,23 @@ def test_columns_skipped_dep(vuln_data_skipped_dep): ---- ----------- bar skip-reason""" assert columns_format.format(vuln_data_skipped_dep, list()) == expected_columns + + +def test_columns_fix(vuln_data, fix_data): + columns_format = format.ColumnsFormat(False) + expected_columns = """Name Version ID Fix Versions Applied Fix +---- ------- ------ ------------ -------------------------------------- +foo 1.0 VULN-0 1.1,1.4 Successfully upgraded foo (1.0 => 1.8) +foo 1.0 VULN-1 1.0 Successfully upgraded foo (1.0 => 1.8) +bar 0.1 VULN-2 Successfully upgraded bar (0.1 => 0.3)""" + assert columns_format.format(vuln_data, fix_data) == expected_columns + + +def test_columns_skipped_fix(vuln_data, skipped_fix_data): + columns_format = format.ColumnsFormat(False) + expected_columns = """Name Version ID Fix Versions Applied Fix +---- ------- ------ ------------ -------------------------------------- +foo 1.0 VULN-0 1.1,1.4 Successfully upgraded foo (1.0 => 1.8) +foo 1.0 VULN-1 1.0 Successfully upgraded foo (1.0 => 1.8) +bar 0.1 VULN-2 Failed to fix bar (0.1): skip-reason""" + assert columns_format.format(vuln_data, skipped_fix_data) == expected_columns From 3c47fd78f7c4209cdf6a9561c9912fd064d3d489 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 15:37:13 +1100 Subject: [PATCH 06/10] test: Test JSON formatting of fixes --- test/format/test_json.py | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/test/format/test_json.py b/test/format/test_json.py index 1bcc9874..f6948525 100644 --- a/test/format/test_json.py +++ b/test/format/test_json.py @@ -96,3 +96,93 @@ def test_json_skipped_dep(vuln_data_skipped_dep): "fixes": [], } assert json_format.format(vuln_data_skipped_dep, list()) == json.dumps(expected_json) + + +def test_json_fix(vuln_data, fix_data): + json_format = format.JsonFormat(True) + expected_json = { + "dependencies": [ + { + "name": "foo", + "version": "1.0", + "vulns": [ + { + "id": "VULN-0", + "fix_versions": [ + "1.1", + "1.4", + ], + "description": "The first vulnerability", + }, + { + "id": "VULN-1", + "fix_versions": ["1.0"], + "description": "The second vulnerability", + }, + ], + }, + { + "name": "bar", + "version": "0.1", + "vulns": [ + {"id": "VULN-2", "fix_versions": [], "description": "The third vulnerability"} + ], + }, + ], + "fixes": [ + { + "name": "foo", + "old_version": "1.0", + "new_version": "1.8", + }, + { + "name": "bar", + "old_version": "0.1", + "new_version": "0.3", + }, + ], + } + assert json_format.format(vuln_data, fix_data) == json.dumps(expected_json) + + +def test_json_skipped_fix(vuln_data, skipped_fix_data): + json_format = format.JsonFormat(True) + expected_json = { + "dependencies": [ + { + "name": "foo", + "version": "1.0", + "vulns": [ + { + "id": "VULN-0", + "fix_versions": [ + "1.1", + "1.4", + ], + "description": "The first vulnerability", + }, + { + "id": "VULN-1", + "fix_versions": ["1.0"], + "description": "The second vulnerability", + }, + ], + }, + { + "name": "bar", + "version": "0.1", + "vulns": [ + {"id": "VULN-2", "fix_versions": [], "description": "The third vulnerability"} + ], + }, + ], + "fixes": [ + { + "name": "foo", + "old_version": "1.0", + "new_version": "1.8", + }, + {"name": "bar", "version": "0.1", "skip_reason": "skip-reason"}, + ], + } + assert json_format.format(vuln_data, skipped_fix_data) == json.dumps(expected_json) From 07d24424df4f911ad38419ebdf4a14c8ef8ffe74 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 15:47:55 +1100 Subject: [PATCH 07/10] test: Check that CycloneDX logs a warning when fixes are provided --- pip_audit/_format/cyclonedx.py | 2 +- test/format/test_cyclonedx.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pip_audit/_format/cyclonedx.py b/pip_audit/_format/cyclonedx.py index 6ce19b95..607f1789 100644 --- a/pip_audit/_format/cyclonedx.py +++ b/pip_audit/_format/cyclonedx.py @@ -80,7 +80,7 @@ def format( See `VulnerabilityFormat.format`. """ if fixes: - logger.warn("--fix output is unsupported by CycloneDX formats") + logger.warning("--fix output is unsupported by CycloneDX formats") parser = _PipAuditResultParser(result) bom = Bom.from_parser(parser) diff --git a/test/format/test_cyclonedx.py b/test/format/test_cyclonedx.py index 6aee526b..58c14523 100644 --- a/test/format/test_cyclonedx.py +++ b/test/format/test_cyclonedx.py @@ -1,6 +1,8 @@ import json import xml.etree.ElementTree as ET +import pretend # type: ignore + from pip_audit._format import CycloneDxFormat @@ -25,3 +27,16 @@ def test_cyclonedx_skipped_dep(vuln_data_skipped_dep): # Just test that a skipped dependency doesn't cause the formatter to blow up assert json.loads(formatter.format(vuln_data_skipped_dep, list())) is not None + + +def test_cyclonedx_fix(monkeypatch, vuln_data, fix_data): + import pip_audit._format.cyclonedx as cyclonedx + + logger = pretend.stub(warning=pretend.call_recorder(lambda s: None)) + monkeypatch.setattr(cyclonedx, "logger", logger) + + formatter = CycloneDxFormat(inner_format=CycloneDxFormat.InnerFormat.Json) + assert json.loads(formatter.format(vuln_data, fix_data)) is not None + + # The CycloneDX format doesn't support fixes so we expect to log a warning + assert len(logger.warning.calls) == 1 From 9764d704c68c97643002d5b64ba1fd8ce85a8db7 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 15:52:46 +1100 Subject: [PATCH 08/10] README: Update example output --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 33754fef..6625e1e2 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ No known vulnerabilities found Audit dependencies when there are vulnerabilities present: ``` $ pip-audit -Found 2 known vulnerabilities in 1 packages +Found 2 known vulnerabilities in 1 package Name Version ID Fix Versions ---- ------- -------------- ------------ Flask 0.5 PYSEC-2019-179 1.0 @@ -158,7 +158,7 @@ Flask 0.5 PYSEC-2018-66 0.12.3 Audit dependencies including descriptions: ``` $ pip-audit --desc -Found 2 known vulnerabilities in 1 packages +Found 2 known vulnerabilities in 1 package Name Version ID Fix Versions Description ---- ------- -------------- ------------ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Flask 0.5 PYSEC-2019-179 1.0 The Pallets Project Flask before 1.0 is affected by: unexpected memory usage. The impact is: denial of service. The attack vector is: crafted encoded JSON data. The fixed version is: 1. NOTE: this may overlap CVE-2018-1000656. @@ -168,7 +168,7 @@ Flask 0.5 PYSEC-2018-66 0.12.3 The Pallets Project flask version Befo Audit dependencies in JSON format: ``` $ pip-audit -f json | jq -Found 2 known vulnerabilities in 1 packages +Found 2 known vulnerabilities in 1 package [ { "name": "flask", @@ -221,11 +221,11 @@ Found 2 known vulnerabilities in 1 packages Audit and attempt to automatically upgrade vulnerable dependencies: ``` $ pip-audit --fix -Found 2 known vulnerabilities in 1 packages and fixed 2 vulnerabilities in 1 packages -Name Version ID Fix Versions ------ ------- -------------- ------------ -Flask 0.5 PYSEC-2019-179 1.0 -Flask 0.5 PYSEC-2018-66 0.12.3 +Found 2 known vulnerabilities in 1 package and fixed 2 vulnerabilities in 1 package +Name Version ID Fix Versions Applied Fix +----- ------- -------------- ------------ ---------------------------------------- +flask 0.5 PYSEC-2019-179 1.0 Successfully upgraded flask (0.5 => 1.0) +flask 0.5 PYSEC-2018-66 0.12.3 Successfully upgraded flask (0.5 => 1.0) ``` ## Security Model From 3bd1493f03c21448be33813431322d4c6ae1b6ca Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 14 Jan 2022 22:03:17 +1100 Subject: [PATCH 09/10] test: Fix CLI test --- test/test_cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_cli.py b/test/test_cli.py index e21a8b3e..c8c961b2 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -38,7 +38,9 @@ def test_plurals(capsys, monkeypatch, args, vuln_count, pkg_count, expected): auditor = pretend.stub(audit=lambda a: result) monkeypatch.setattr(pip_audit._cli, "Auditor", lambda *a, **kw: auditor) - resolve_fix_versions = [pretend.stub(is_skipped=lambda: False, dep=spec) for spec, _ in result] + resolve_fix_versions = [ + pretend.stub(is_skipped=lambda: False, dep=spec, version=2) for spec, _ in result + ] monkeypatch.setattr(pip_audit._cli, "resolve_fix_versions", lambda *a: resolve_fix_versions) try: From 758f546aeee205c37253dd895852af9a790448b4 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 14 Jan 2022 11:26:54 -0500 Subject: [PATCH 10/10] CHANGELOG: record changes --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be74e3e5..87ce2153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ All versions prior to 0.0.9 are untracked. * CLI: The `--fix` flag has been added, allowing users to attempt to automatically upgrade any vulnerable dependencies to the first safe version - available (#[212](https://github.com/trailofbits/pip-audit/pull/212)) + available ([#212](https://github.com/trailofbits/pip-audit/pull/212), + [#222](https://github.com/trailofbits/pip-audit/pull/222)) ### Changed