From 11d27864cb11d82f188c5e812bacd5a5373744b8 Mon Sep 17 00:00:00 2001 From: Peter Boulos Date: Wed, 10 Jan 2024 01:25:40 -0500 Subject: [PATCH] Add aliases to output. (#721) * Added aliases to JSON output. * Added aliases to column and markdown formats. * Added --aliases flag to documentation and included examples. * Allow alias IDs to be toggled for JSON output; updated documentation. * Fix `--aliases` flag description to match README --------- Co-authored-by: pboulos Co-authored-by: Alex Cameron --- CHANGELOG.md | 6 +++ README.md | 35 ++++++++++++++--- pip_audit/_cli.py | 46 +++++++++++++++++++--- pip_audit/_format/columns.py | 10 ++++- pip_audit/_format/json.py | 8 +++- pip_audit/_format/markdown.py | 11 +++++- test/conftest.py | 2 +- test/format/conftest.py | 8 ++-- test/format/test_columns.py | 66 +++++++++++++++++-------------- test/format/test_json.py | 73 ++++++++++++++++++++++++++++++----- test/format/test_markdown.py | 67 ++++++++++++++++++-------------- test/test_cli.py | 24 +++++++++++- 12 files changed, 270 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 454a3c28..e8250e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ All versions prior to 0.0.9 are untracked. ## [Unreleased] +### Added + +* `pip-audit` now includes vulnerability aliases when `--format=json` is used, + and also includes them in other output formats if specified by adding the + flag `--aliases` + ## [2.6.3] ### Fixed diff --git a/README.md b/README.md index a96ff114..e3e8182b 100644 --- a/README.md +++ b/README.md @@ -131,12 +131,12 @@ python -m pip_audit --help ``` usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENT] [-f FORMAT] [-s SERVICE] [-d] - [-S] [--desc [{on,off,auto}]] [--cache-dir CACHE_DIR] - [--progress-spinner {on,off}] [--timeout TIMEOUT] - [--path PATH] [-v] [--fix] [--require-hashes] - [--index-url INDEX_URL] [--extra-index-url URL] - [--skip-editable] [--no-deps] [-o FILE] [--ignore-vuln ID] - [--disable-pip] + [-S] [--desc [{on,off,auto}]] [--aliases [{on,off,auto}]] + [--cache-dir CACHE_DIR] [--progress-spinner {on,off}] + [--timeout TIMEOUT] [--path PATH] [-v] [--fix] + [--require-hashes] [--index-url INDEX_URL] + [--extra-index-url URL] [--skip-editable] [--no-deps] + [-o FILE] [--ignore-vuln ID] [--disable-pip] [project_path] audit the Python environment for dependencies with known vulnerabilities @@ -171,6 +171,11 @@ optional arguments: defaults to `on` for the `json` format. This flag has no effect on the `cyclonedx-json` or `cyclonedx-xml` formats. (default: auto) + --aliases [{on,off,auto}] + includes alias IDs for each vulnerability; `auto` + defaults to `on` for the `json` format. This flag has + no effect on the `cyclonedx-json` or `cyclonedx-xml` + formats. (default: auto) --cache-dir CACHE_DIR the directory to use as an HTTP cache for PyPI; uses the `pip` HTTP cache by default (default: None) @@ -274,6 +279,16 @@ Flask 0.5 PYSEC-2019-179 1.0 Flask 0.5 PYSEC-2018-66 0.12.3 ``` +Audit dependencies including aliases: +``` +$ pip-audit --aliases +Found 2 known vulnerabilities in 1 package +Name Version ID Fix Versions Aliases +---- ------- -------------- ------------ ------------------------------------- +Flask 0.5 PYSEC-2019-179 1.0 CVE-2019-1010083, GHSA-5wv5-4vpf-pj6m +Flask 0.5 PYSEC-2018-66 0.12.3 CVE-2018-1000656, GHSA-562c-5r94-xh97 +``` + Audit dependencies including descriptions: ``` $ pip-audit --desc @@ -298,6 +313,10 @@ Found 2 known vulnerabilities in 1 package "fix_versions": [ "1.0" ], + "aliases": [ + "CVE-2019-1010083", + "GHSA-5wv5-4vpf-pj6m" + ], "description": "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." }, { @@ -305,6 +324,10 @@ Found 2 known vulnerabilities in 1 package "fix_versions": [ "0.12.3" ], + "aliases": [ + "CVE-2018-1000656", + "GHSA-562c-5r94-xh97" + ], "description": "The Pallets Project flask version Before 0.12.3 contains a CWE-20: Improper Input Validation vulnerability in flask that can result in Large amount of memory usage possibly leading to denial of service. This attack appear to be exploitable via Attacker provides JSON data in incorrect encoding. This vulnerability appears to have been fixed in 0.12.3. NOTE: this may overlap CVE-2019-1010083." } ] diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 18fa2e05..39a6b500 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -71,17 +71,17 @@ class OutputFormatChoice(str, enum.Enum): CycloneDxXml = "cyclonedx-xml" Markdown = "markdown" - def to_format(self, output_desc: bool) -> VulnerabilityFormat: + def to_format(self, output_desc: bool, output_aliases: bool) -> VulnerabilityFormat: if self is OutputFormatChoice.Columns: - return ColumnsFormat(output_desc) + return ColumnsFormat(output_desc, output_aliases) elif self is OutputFormatChoice.Json: - return JsonFormat(output_desc) + return JsonFormat(output_desc, output_aliases) elif self is OutputFormatChoice.CycloneDxJson: return CycloneDxFormat(inner_format=CycloneDxFormat.InnerFormat.Json) elif self is OutputFormatChoice.CycloneDxXml: return CycloneDxFormat(inner_format=CycloneDxFormat.InnerFormat.Xml) elif self is OutputFormatChoice.Markdown: - return MarkdownFormat(output_desc) + return MarkdownFormat(output_desc, output_aliases) else: assert_never(self) # pragma: no cover @@ -134,6 +134,30 @@ def __str__(self) -> str: return self.value +@enum.unique +class VulnerabilityAliasChoice(str, enum.Enum): + """ + Whether or not vulnerability aliases should be added to the `pip-audit` output. + """ + + On = "on" + Off = "off" + Auto = "auto" + + def to_bool(self, format_: OutputFormatChoice) -> bool: + if self is VulnerabilityAliasChoice.On: + return True + elif self is VulnerabilityAliasChoice.Off: + return False + elif self is VulnerabilityAliasChoice.Auto: + return bool(format_ is OutputFormatChoice.Json) + else: + assert_never(self) # pragma: no cover + + def __str__(self) -> str: + return self.value + + @enum.unique class ProgressSpinnerChoice(str, enum.Enum): """ @@ -241,6 +265,17 @@ def _parser() -> argparse.ArgumentParser: # pragma: no cover "`auto` defaults to `on` for the `json` format. This flag has no " "effect on the `cyclonedx-json` or `cyclonedx-xml` formats.", ) + parser.add_argument( + "--aliases", + type=VulnerabilityAliasChoice, + choices=VulnerabilityAliasChoice, + nargs="?", + const=VulnerabilityAliasChoice.On, + default=VulnerabilityAliasChoice.Auto, + help="includes alias IDs for each vulnerability; " + "`auto` defaults to `on` for the `json` format. This flag has no " + "effect on the `cyclonedx-json` or `cyclonedx-xml` formats.", + ) parser.add_argument( "--cache-dir", type=Path, @@ -385,7 +420,8 @@ def audit() -> None: # pragma: no cover service = args.vulnerability_service.to_service(args.timeout, args.cache_dir) output_desc = args.desc.to_bool(args.format) - formatter = args.format.to_format(output_desc) + output_aliases = args.aliases.to_bool(args.format) + formatter = args.format.to_format(output_desc, output_aliases) # Check for flags that are only valid with requirements files if args.requirements is None: diff --git a/pip_audit/_format/columns.py b/pip_audit/_format/columns.py index b071a8f1..988fc35d 100644 --- a/pip_audit/_format/columns.py +++ b/pip_audit/_format/columns.py @@ -33,14 +33,18 @@ class ColumnsFormat(VulnerabilityFormat): columns. """ - def __init__(self, output_desc: bool): + def __init__(self, output_desc: bool, output_aliases: bool): """ Create a new `ColumnFormat`. `output_desc` is a flag to determine whether descriptions for each vulnerability should be included in the output as they can be quite long and make the output difficult to read. + + `output_aliases` is a flag to determine whether aliases (such as CVEs) for each + vulnerability should be included in the output. """ self.output_desc = output_desc + self.output_aliases = output_aliases @property def is_manifest(self) -> bool: @@ -64,6 +68,8 @@ def format( header = ["Name", "Version", "ID", "Fix Versions"] if fixes: header.append("Applied Fix") + if self.output_aliases: + header.append("Aliases") if self.output_desc: header.append("Description") vuln_data.append(header) @@ -131,6 +137,8 @@ def _format_vuln( ] if applied_fix is not None: vuln_data.append(self._format_applied_fix(applied_fix)) + if self.output_aliases: + vuln_data.append(", ".join(vuln.aliases)) if self.output_desc: vuln_data.append(vuln.description) return vuln_data diff --git a/pip_audit/_format/json.py b/pip_audit/_format/json.py index 4c918526..86d3586f 100644 --- a/pip_audit/_format/json.py +++ b/pip_audit/_format/json.py @@ -18,14 +18,18 @@ class JsonFormat(VulnerabilityFormat): JSON objects. """ - def __init__(self, output_desc: bool): + def __init__(self, output_desc: bool, output_aliases: bool): """ Create a new `JsonFormat`. `output_desc` is a flag to determine whether descriptions for each vulnerability should be included in the output as they can be quite long and make the output difficult to read. + + `output_aliases` is a flag to determine whether aliases (such as CVEs) for each + vulnerability should be included in the output. """ self.output_desc = output_desc + self.output_aliases = output_aliases @property def is_manifest(self) -> bool: @@ -78,6 +82,8 @@ def _format_vuln(self, vuln: service.VulnerabilityResult) -> dict[str, Any]: "id": vuln.id, "fix_versions": [str(version) for version in vuln.fix_versions], } + if self.output_aliases: + vuln_json["aliases"] = list(vuln.aliases) if self.output_desc: vuln_json["description"] = vuln.description return vuln_json diff --git a/pip_audit/_format/markdown.py b/pip_audit/_format/markdown.py index 4a5d520d..bac8d6b5 100644 --- a/pip_audit/_format/markdown.py +++ b/pip_audit/_format/markdown.py @@ -21,14 +21,18 @@ class MarkdownFormat(VulnerabilityFormat): Markdown tables. """ - def __init__(self, output_desc: bool) -> None: + def __init__(self, output_desc: bool, output_aliases: bool) -> None: """ Create a new `MarkdownFormat`. `output_desc` is a flag to determine whether descriptions for each vulnerability should be included in the output as they can be quite long and make the output difficult to read. + + `output_aliases` is a flag to determine whether aliases (such as CVEs) for each + vulnerability should be included in the output. """ self.output_desc = output_desc + self.output_aliases = output_aliases @property def is_manifest(self) -> bool: @@ -66,6 +70,9 @@ def _format_vuln_results( if fixes: header += " | Applied Fix" border += " | ---" + if self.output_aliases: + header += " | Aliases" + border += " | ---" if self.output_desc: header += " | Description" border += " | ---" @@ -101,6 +108,8 @@ def _format_vuln( ) if applied_fix is not None: vuln_text += f" | {self._format_applied_fix(applied_fix)}" + if self.output_aliases: + vuln_text += f" | {', '.join(vuln.aliases)}" if self.output_desc: vuln_text += f" | {vuln.description}" return vuln_text diff --git a/test/conftest.py b/test/conftest.py index d0ca5c84..2e416b16 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -51,8 +51,8 @@ def query(self, spec): VulnerabilityResult( id="fake-id", description="this is not a real result", - fix_versions=[fixed], aliases=set(), + fix_versions=[fixed], ) ] diff --git a/test/format/conftest.py b/test/format/conftest.py index 6b49e690..57cec452 100644 --- a/test/format/conftest.py +++ b/test/format/conftest.py @@ -19,13 +19,13 @@ Version("1.1"), Version("1.4"), ], - aliases=set(), + aliases={"CVE-0000-00000"}, ), service.VulnerabilityResult( id="VULN-1", description="The second vulnerability", fix_versions=[Version("1.0")], - aliases=set(), + aliases={"CVE-0000-00001"}, ), ], _RESOLVED_DEP_BAR: [ @@ -33,7 +33,7 @@ id="VULN-2", description="The third vulnerability", fix_versions=[], - aliases=set(), + aliases={"CVE-0000-00002"}, ) ], } @@ -47,7 +47,7 @@ Version("1.1"), Version("1.4"), ], - aliases=set(), + aliases={"CVE-0000-00000"}, ), ], _SKIPPED_DEP: [], diff --git a/test/format/test_columns.py b/test/format/test_columns.py index 9bdf7c18..cd70cf50 100644 --- a/test/format/test_columns.py +++ b/test/format/test_columns.py @@ -3,24 +3,34 @@ import pip_audit._format as format -@pytest.mark.parametrize("output_desc", [True, False]) -def test_columns_not_manifest(output_desc): - fmt = format.ColumnsFormat(output_desc) +@pytest.mark.parametrize("output_desc, output_aliases", ([True, False], [True, False])) +def test_columns_not_manifest(output_desc, output_aliases): + fmt = format.ColumnsFormat(output_desc, output_aliases) assert not fmt.is_manifest def test_columns(vuln_data): - columns_format = format.ColumnsFormat(True) - expected_columns = """Name Version ID Fix Versions Description ----- ------- ------ ------------ ------------------------ -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""" + columns_format = format.ColumnsFormat(True, True) + expected_columns = """Name Version ID Fix Versions Aliases Description +---- ------- ------ ------------ -------------- ------------------------ +foo 1.0 VULN-0 1.1,1.4 CVE-0000-00000 The first vulnerability +foo 1.0 VULN-1 1.0 CVE-0000-00001 The second vulnerability +bar 0.1 VULN-2 CVE-0000-00002 The third vulnerability""" assert columns_format.format(vuln_data, list()) == expected_columns def test_columns_no_desc(vuln_data): - columns_format = format.ColumnsFormat(False) + columns_format = format.ColumnsFormat(False, True) + expected_columns = """Name Version ID Fix Versions Aliases +---- ------- ------ ------------ -------------- +foo 1.0 VULN-0 1.1,1.4 CVE-0000-00000 +foo 1.0 VULN-1 1.0 CVE-0000-00001 +bar 0.1 VULN-2 CVE-0000-00002""" + assert columns_format.format(vuln_data, list()) == expected_columns + + +def test_columns_no_desc_no_aliases(vuln_data): + columns_format = format.ColumnsFormat(False, False) expected_columns = """Name Version ID Fix Versions ---- ------- ------ ------------ foo 1.0 VULN-0 1.1,1.4 @@ -30,10 +40,10 @@ def test_columns_no_desc(vuln_data): def test_columns_skipped_dep(vuln_data_skipped_dep): - columns_format = format.ColumnsFormat(False) - expected_columns = """Name Version ID Fix Versions ----- ------- ------ ------------ -foo 1.0 VULN-0 1.1,1.4 + columns_format = format.ColumnsFormat(False, True) + expected_columns = """Name Version ID Fix Versions Aliases +---- ------- ------ ------------ -------------- +foo 1.0 VULN-0 1.1,1.4 CVE-0000-00000 Name Skip Reason ---- ----------- bar skip-reason""" @@ -41,13 +51,13 @@ def test_columns_skipped_dep(vuln_data_skipped_dep): def test_columns_no_vuln_data(no_vuln_data): - columns_format = format.ColumnsFormat(False) + columns_format = format.ColumnsFormat(False, True) expected_columns = "" assert columns_format.format(no_vuln_data, list()) == expected_columns def test_column_no_vuln_data_skipped_dep(no_vuln_data_skipped_dep): - columns_format = format.ColumnsFormat(False) + columns_format = format.ColumnsFormat(False, True) expected_columns = """Name Skip Reason ---- ----------- bar skip-reason""" @@ -55,20 +65,20 @@ def test_column_no_vuln_data_skipped_dep(no_vuln_data_skipped_dep): 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)""" + columns_format = format.ColumnsFormat(False, True) + expected_columns = """Name Version ID Fix Versions Applied Fix Aliases +---- ------- ------ ------------ -------------------------------------- -------------- +foo 1.0 VULN-0 1.1,1.4 Successfully upgraded foo (1.0 => 1.8) CVE-0000-00000 +foo 1.0 VULN-1 1.0 Successfully upgraded foo (1.0 => 1.8) CVE-0000-00001 +bar 0.1 VULN-2 Successfully upgraded bar (0.1 => 0.3) CVE-0000-00002""" 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""" + columns_format = format.ColumnsFormat(False, True) + expected_columns = """Name Version ID Fix Versions Applied Fix Aliases +---- ------- ------ ------------ -------------------------------------- -------------- +foo 1.0 VULN-0 1.1,1.4 Successfully upgraded foo (1.0 => 1.8) CVE-0000-00000 +foo 1.0 VULN-1 1.0 Successfully upgraded foo (1.0 => 1.8) CVE-0000-00001 +bar 0.1 VULN-2 Failed to fix bar (0.1): skip-reason CVE-0000-00002""" assert columns_format.format(vuln_data, skipped_fix_data) == expected_columns diff --git a/test/format/test_json.py b/test/format/test_json.py index f366cb31..2cc9ac4b 100644 --- a/test/format/test_json.py +++ b/test/format/test_json.py @@ -5,15 +5,15 @@ import pip_audit._format as format -@pytest.mark.parametrize("output_desc", [True, False]) -def test_json_manifest(output_desc): - fmt = format.JsonFormat(output_desc) +@pytest.mark.parametrize("output_desc, output_aliases", ([True, False], [True, False])) +def test_json_manifest(output_desc, output_aliases): + fmt = format.JsonFormat(output_desc, output_aliases) assert fmt.is_manifest def test_json(vuln_data): - json_format = format.JsonFormat(True) + json_format = format.JsonFormat(True, True) expected_json = { "dependencies": [ { @@ -26,11 +26,13 @@ def test_json(vuln_data): "1.1", "1.4", ], + "aliases": ["CVE-0000-00000"], "description": "The first vulnerability", }, { "id": "VULN-1", "fix_versions": ["1.0"], + "aliases": ["CVE-0000-00001"], "description": "The second vulnerability", }, ], @@ -42,6 +44,7 @@ def test_json(vuln_data): { "id": "VULN-2", "fix_versions": [], + "aliases": ["CVE-0000-00002"], "description": "The third vulnerability", } ], @@ -53,7 +56,7 @@ def test_json(vuln_data): def test_json_no_desc(vuln_data): - json_format = format.JsonFormat(False) + json_format = format.JsonFormat(False, True) expected_json = { "dependencies": [ { @@ -66,17 +69,62 @@ def test_json_no_desc(vuln_data): "1.1", "1.4", ], + "aliases": ["CVE-0000-00000"], }, { "id": "VULN-1", "fix_versions": ["1.0"], + "aliases": ["CVE-0000-00001"], }, ], }, { "name": "bar", "version": "0.1", - "vulns": [{"id": "VULN-2", "fix_versions": []}], + "vulns": [ + { + "id": "VULN-2", + "fix_versions": [], + "aliases": ["CVE-0000-00002"], + } + ], + }, + ], + "fixes": [], + } + assert json_format.format(vuln_data, list()) == json.dumps(expected_json) + + +def test_json_no_desc_no_aliases(vuln_data): + json_format = format.JsonFormat(False, False) + 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": [], @@ -85,7 +133,7 @@ def test_json_no_desc(vuln_data): def test_json_skipped_dep(vuln_data_skipped_dep): - json_format = format.JsonFormat(False) + json_format = format.JsonFormat(False, True) expected_json = { "dependencies": [ { @@ -98,6 +146,7 @@ def test_json_skipped_dep(vuln_data_skipped_dep): "1.1", "1.4", ], + "aliases": ["CVE-0000-00000"], }, ], }, @@ -112,7 +161,7 @@ def test_json_skipped_dep(vuln_data_skipped_dep): def test_json_fix(vuln_data, fix_data): - json_format = format.JsonFormat(True) + json_format = format.JsonFormat(True, True) expected_json = { "dependencies": [ { @@ -125,11 +174,13 @@ def test_json_fix(vuln_data, fix_data): "1.1", "1.4", ], + "aliases": ["CVE-0000-00000"], "description": "The first vulnerability", }, { "id": "VULN-1", "fix_versions": ["1.0"], + "aliases": ["CVE-0000-00001"], "description": "The second vulnerability", }, ], @@ -141,6 +192,7 @@ def test_json_fix(vuln_data, fix_data): { "id": "VULN-2", "fix_versions": [], + "aliases": ["CVE-0000-00002"], "description": "The third vulnerability", } ], @@ -163,7 +215,7 @@ def test_json_fix(vuln_data, fix_data): def test_json_skipped_fix(vuln_data, skipped_fix_data): - json_format = format.JsonFormat(True) + json_format = format.JsonFormat(True, True) expected_json = { "dependencies": [ { @@ -176,11 +228,13 @@ def test_json_skipped_fix(vuln_data, skipped_fix_data): "1.1", "1.4", ], + "aliases": ["CVE-0000-00000"], "description": "The first vulnerability", }, { "id": "VULN-1", "fix_versions": ["1.0"], + "aliases": ["CVE-0000-00001"], "description": "The second vulnerability", }, ], @@ -192,6 +246,7 @@ def test_json_skipped_fix(vuln_data, skipped_fix_data): { "id": "VULN-2", "fix_versions": [], + "aliases": ["CVE-0000-00002"], "description": "The third vulnerability", } ], diff --git a/test/format/test_markdown.py b/test/format/test_markdown.py index 6473f951..226ff1de 100644 --- a/test/format/test_markdown.py +++ b/test/format/test_markdown.py @@ -3,25 +3,36 @@ import pip_audit._format as format -@pytest.mark.parametrize("output_desc", [True, False]) -def test_columns_not_manifest(output_desc): - fmt = format.MarkdownFormat(output_desc) +@pytest.mark.parametrize("output_desc, output_aliases", ([True, False], [True, False])) +def test_columns_not_manifest(output_desc, output_aliases): + fmt = format.MarkdownFormat(output_desc, output_aliases) assert not fmt.is_manifest def test_markdown(vuln_data): - markdown_format = format.MarkdownFormat(True) + markdown_format = format.MarkdownFormat(True, True) expected_markdown = """ -Name | Version | ID | Fix Versions | Description ---- | --- | --- | --- | --- -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""" +Name | Version | ID | Fix Versions | Aliases | Description +--- | --- | --- | --- | --- | --- +foo | 1.0 | VULN-0 | 1.1,1.4 | CVE-0000-00000 | The first vulnerability +foo | 1.0 | VULN-1 | 1.0 | CVE-0000-00001 | The second vulnerability +bar | 0.1 | VULN-2 | | CVE-0000-00002 | The third vulnerability""" assert markdown_format.format(vuln_data, list()) == expected_markdown def test_markdown_no_desc(vuln_data): - markdown_format = format.MarkdownFormat(False) + markdown_format = format.MarkdownFormat(False, True) + expected_markdown = """ +Name | Version | ID | Fix Versions | Aliases +--- | --- | --- | --- | --- +foo | 1.0 | VULN-0 | 1.1,1.4 | CVE-0000-00000 +foo | 1.0 | VULN-1 | 1.0 | CVE-0000-00001 +bar | 0.1 | VULN-2 | | CVE-0000-00002""" + assert markdown_format.format(vuln_data, list()) == expected_markdown + + +def test_markdown_no_desc_no_aliases(vuln_data): + markdown_format = format.MarkdownFormat(False, False) expected_markdown = """ Name | Version | ID | Fix Versions --- | --- | --- | --- @@ -32,11 +43,11 @@ def test_markdown_no_desc(vuln_data): def test_markdown_skipped_dep(vuln_data_skipped_dep): - markdown_format = format.MarkdownFormat(False) + markdown_format = format.MarkdownFormat(False, True) expected_markdown = """ -Name | Version | ID | Fix Versions ---- | --- | --- | --- -foo | 1.0 | VULN-0 | 1.1,1.4 +Name | Version | ID | Fix Versions | Aliases +--- | --- | --- | --- | --- +foo | 1.0 | VULN-0 | 1.1,1.4 | CVE-0000-00000 Name | Skip Reason --- | --- @@ -45,13 +56,13 @@ def test_markdown_skipped_dep(vuln_data_skipped_dep): def test_markdown_no_vuln_data(no_vuln_data): - markdown_format = format.MarkdownFormat(False) + markdown_format = format.MarkdownFormat(False, True) expected_markdown = "" assert markdown_format.format(no_vuln_data, list()) == expected_markdown def test_markdown_no_vuln_data_skipped_dep(no_vuln_data_skipped_dep): - markdown_format = format.MarkdownFormat(False) + markdown_format = format.MarkdownFormat(False, True) expected_markdown = """ Name | Skip Reason --- | --- @@ -60,22 +71,22 @@ def test_markdown_no_vuln_data_skipped_dep(no_vuln_data_skipped_dep): def test_markdown_fix(vuln_data, fix_data): - markdown_format = format.MarkdownFormat(False) + markdown_format = format.MarkdownFormat(False, True) expected_markdown = """ -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)""" +Name | Version | ID | Fix Versions | Applied Fix | Aliases +--- | --- | --- | --- | --- | --- +foo | 1.0 | VULN-0 | 1.1,1.4 | Successfully upgraded foo (1.0 => 1.8) | CVE-0000-00000 +foo | 1.0 | VULN-1 | 1.0 | Successfully upgraded foo (1.0 => 1.8) | CVE-0000-00001 +bar | 0.1 | VULN-2 | | Successfully upgraded bar (0.1 => 0.3) | CVE-0000-00002""" assert markdown_format.format(vuln_data, fix_data) == expected_markdown def test_markdown_skipped_fix(vuln_data, skipped_fix_data): - markdown_format = format.MarkdownFormat(False) + markdown_format = format.MarkdownFormat(False, True) expected_markdown = """ -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""" +Name | Version | ID | Fix Versions | Applied Fix | Aliases +--- | --- | --- | --- | --- | --- +foo | 1.0 | VULN-0 | 1.1,1.4 | Successfully upgraded foo (1.0 => 1.8) | CVE-0000-00000 +foo | 1.0 | VULN-1 | 1.0 | Successfully upgraded foo (1.0 => 1.8) | CVE-0000-00001 +bar | 0.1 | VULN-2 | | Failed to fix bar (0.1): skip-reason | CVE-0000-00002""" assert markdown_format.format(vuln_data, skipped_fix_data) == expected_markdown diff --git a/test/test_cli.py b/test/test_cli.py index 5335d381..b6ca5a78 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -5,6 +5,7 @@ from pip_audit._cli import ( OutputFormatChoice, ProgressSpinnerChoice, + VulnerabilityAliasChoice, VulnerabilityDescriptionChoice, VulnerabilityServiceChoice, ) @@ -13,8 +14,10 @@ class TestOutputFormatChoice: def test_to_format_is_exhaustive(self): for choice in OutputFormatChoice: - assert choice.to_format(False) is not None - assert choice.to_format(True) is not None + assert choice.to_format(False, False) is not None + assert choice.to_format(True, True) is not None + assert choice.to_format(False, True) is not None + assert choice.to_format(True, False) is not None def test_str(self): for choice in OutputFormatChoice: @@ -44,6 +47,23 @@ def test_str(self): assert str(choice) == choice.value +class TestVulnerabilityAliasChoice: + def test_to_bool_is_exhaustive(self): + for choice in VulnerabilityAliasChoice: + assert choice.to_bool(OutputFormatChoice.Json) in {True, False} + assert choice.to_bool(OutputFormatChoice.Markdown) in {True, False} + assert choice.to_bool(OutputFormatChoice.Columns) in {True, False} + assert choice.to_bool(OutputFormatChoice.CycloneDxJson) in {True, False} + assert choice.to_bool(OutputFormatChoice.CycloneDxXml) in {True, False} + + def test_auto_to_bool_for_json(self): + assert VulnerabilityAliasChoice.Auto.to_bool(OutputFormatChoice.Json) is True + + def test_str(self): + for choice in VulnerabilityAliasChoice: + assert str(choice) == choice.value + + class TestProgressSpinnerChoice: def test_bool(self): assert bool(ProgressSpinnerChoice.On)