-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
markdown: Add Markdown formatter (#312)
* markdown: Add initial implementation of `MarkdownFormat` * markdown: Add skipped dependencies table * markdown: Minor formatting fixes * test: Add basic unit test for markdown formatting * markdown, test: Fill out remaining coverage * test: Fix expected output * README: Update help text * CHANGELOG: Add entry for Markdown formatting * format/markdown: slight tweaks Markdown allows raw newlines as line separators, so we don't bother with `os.linesep`. We also use f-strings, where possible. Signed-off-by: William Woodruff <[email protected]> * README: add markdown to feature list Signed-off-by: William Woodruff <[email protected]> * markdown: Remove unused import Co-authored-by: William Woodruff <[email protected]>
- Loading branch information
1 parent
969d243
commit 8157d28
Showing
6 changed files
with
253 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
""" | ||
Functionality for formatting vulnerability results as a Markdown table. | ||
""" | ||
|
||
from textwrap import dedent | ||
from typing import Dict, List, Optional, cast | ||
|
||
from packaging.version import Version | ||
|
||
import pip_audit._fix as fix | ||
import pip_audit._service as service | ||
|
||
from .interface import VulnerabilityFormat | ||
|
||
|
||
class MarkdownFormat(VulnerabilityFormat): | ||
""" | ||
An implementation of `VulnerabilityFormat` that formats vulnerability results as a set of | ||
Markdown tables. | ||
""" | ||
|
||
def __init__(self, output_desc: 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. | ||
""" | ||
self.output_desc = output_desc | ||
|
||
@property | ||
def is_manifest(self) -> bool: | ||
""" | ||
See `VulnerabilityFormat.is_manifest`. | ||
""" | ||
return False | ||
|
||
def format( | ||
self, | ||
result: Dict[service.Dependency, List[service.VulnerabilityResult]], | ||
fixes: List[fix.FixVersion], | ||
) -> str: | ||
""" | ||
Returns a Markdown formatted string representing a set of vulnerability results and applied | ||
fixes. | ||
""" | ||
output = self._format_vuln_results(result, fixes) | ||
skipped_deps_output = self._format_skipped_deps(result) | ||
if skipped_deps_output: | ||
# If we wrote the results table already, we need to add some line breaks to ensure that | ||
# the skipped dependency table renders correctly. | ||
if output: | ||
output += "\n" | ||
output += skipped_deps_output | ||
return output | ||
|
||
def _format_vuln_results( | ||
self, | ||
result: Dict[service.Dependency, List[service.VulnerabilityResult]], | ||
fixes: List[fix.FixVersion], | ||
) -> str: | ||
header = "Name | Version | ID | Fix Versions" | ||
border = "--- | --- | --- | ---" | ||
if fixes: | ||
header += " | Applied Fix" | ||
border += " | ---" | ||
if self.output_desc: | ||
header += " | Description" | ||
border += " | ---" | ||
|
||
vuln_rows: List[str] = [] | ||
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_rows.append(self._format_vuln(dep, vuln, applied_fix)) | ||
|
||
if not vuln_rows: | ||
return str() | ||
|
||
return ( | ||
dedent( | ||
f""" | ||
{header} | ||
{border} | ||
""" | ||
) | ||
+ "\n".join(vuln_rows) | ||
) | ||
|
||
def _format_vuln( | ||
self, | ||
dep: service.ResolvedDependency, | ||
vuln: service.VulnerabilityResult, | ||
applied_fix: Optional[fix.FixVersion], | ||
) -> str: | ||
vuln_text = ( | ||
f"{dep.canonical_name} | {dep.version} | {vuln.id} | " | ||
f"{self._format_fix_versions(vuln.fix_versions)}" | ||
) | ||
if applied_fix is not None: | ||
vuln_text += f" | {self._format_applied_fix(applied_fix)}" | ||
if self.output_desc: | ||
vuln_text += f" | {vuln.description}" | ||
return vuln_text | ||
|
||
def _format_fix_versions(self, fix_versions: List[Version]) -> str: | ||
return ",".join([str(version) for version in fix_versions]) | ||
|
||
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})" | ||
) | ||
|
||
def _format_skipped_deps( | ||
self, result: Dict[service.Dependency, List[service.VulnerabilityResult]] | ||
) -> str: | ||
header = "Name | Skip Reason" | ||
border = "--- | ---" | ||
|
||
skipped_dep_rows: List[str] = [] | ||
for dep, _ in result.items(): | ||
if dep.is_skipped(): | ||
dep = cast(service.SkippedDependency, dep) | ||
skipped_dep_rows.append(self._format_skipped_dep(dep)) | ||
|
||
if not skipped_dep_rows: | ||
return str() | ||
|
||
return ( | ||
dedent( | ||
f""" | ||
{header} | ||
{border} | ||
""" | ||
) | ||
+ "\n".join(skipped_dep_rows) | ||
) | ||
|
||
def _format_skipped_dep(self, dep: service.SkippedDependency) -> str: | ||
return f"{dep.name} | {dep.skip_reason}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import pytest | ||
|
||
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) | ||
assert not fmt.is_manifest | ||
|
||
|
||
def test_markdown(vuln_data): | ||
markdown_format = format.MarkdownFormat(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""" | ||
assert markdown_format.format(vuln_data, list()) == expected_markdown | ||
|
||
|
||
def test_markdown_no_desc(vuln_data): | ||
markdown_format = format.MarkdownFormat(False) | ||
expected_markdown = """ | ||
Name | Version | ID | Fix Versions | ||
--- | --- | --- | --- | ||
foo | 1.0 | VULN-0 | 1.1,1.4 | ||
foo | 1.0 | VULN-1 | 1.0 | ||
bar | 0.1 | VULN-2 | """ | ||
assert markdown_format.format(vuln_data, list()) == expected_markdown | ||
|
||
|
||
def test_markdown_skipped_dep(vuln_data_skipped_dep): | ||
markdown_format = format.MarkdownFormat(False) | ||
expected_markdown = """ | ||
Name | Version | ID | Fix Versions | ||
--- | --- | --- | --- | ||
foo | 1.0 | VULN-0 | 1.1,1.4 | ||
Name | Skip Reason | ||
--- | --- | ||
bar | skip-reason""" | ||
assert markdown_format.format(vuln_data_skipped_dep, list()) == expected_markdown | ||
|
||
|
||
def test_markdown_no_vuln_data(no_vuln_data): | ||
markdown_format = format.MarkdownFormat(False) | ||
expected_markdown = str() | ||
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) | ||
expected_markdown = """ | ||
Name | Skip Reason | ||
--- | --- | ||
bar | skip-reason""" | ||
assert markdown_format.format(no_vuln_data_skipped_dep, list()) == expected_markdown | ||
|
||
|
||
def test_markdown_fix(vuln_data, fix_data): | ||
markdown_format = format.MarkdownFormat(False) | ||
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)""" | ||
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) | ||
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""" | ||
assert markdown_format.format(vuln_data, skipped_fix_data) == expected_markdown |