Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pip_audit: initial SBOM support via CycloneDX #109

Merged
merged 15 commits into from
Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions pip_audit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from pip_audit.audit import AuditOptions, Auditor
from pip_audit.dependency_source import PipSource, RequirementSource, ResolveLibResolver
from pip_audit.format import ColumnsFormat, JsonFormat, VulnerabilityFormat
from pip_audit.format import ColumnsFormat, CycloneDxFormat, JsonFormat, VulnerabilityFormat
from pip_audit.service import OsvService, PyPIService, VulnerabilityService
from pip_audit.state import AuditSpinner
from pip_audit.util import assert_never
Expand All @@ -31,12 +31,15 @@ class OutputFormatChoice(str, enum.Enum):

Columns = "columns"
Json = "json"
CycloneDx = "cyclonedx"

def to_format(self, output_desc: bool) -> VulnerabilityFormat:
if self is OutputFormatChoice.Columns:
return ColumnsFormat(output_desc)
elif self is OutputFormatChoice.Json:
return JsonFormat(output_desc)
elif self is OutputFormatChoice.CycloneDx:
return CycloneDxFormat()
else:
assert_never(self)

Expand Down Expand Up @@ -195,12 +198,14 @@ def audit():
vuln_count = 0
for (spec, vulns) in auditor.audit(source):
if state is not None:
state.update_state(f"Auditing {spec.package} ({spec.version})")
state.update_state(f"Auditing {spec.name} ({spec.version})")
result[spec] = vulns
if len(vulns) > 0:
pkg_count += 1
vuln_count += len(vulns)

# TODO(ww): Refine this: we should always output if our output format is an SBOM
# or other manifest format (like the default JSON format).
if vuln_count > 0:
print(f"Found {vuln_count} known vulnerabilities in {pkg_count} packages", file=sys.stderr)
print(formatter.format(result))
Expand Down
1 change: 1 addition & 0 deletions pip_audit/format/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .columns import ColumnsFormat # noqa: F401
from .cyclonedx import CycloneDxFormat # noqa: F401
from .interface import VulnerabilityFormat # noqa: F401
from .json import JsonFormat # noqa: F401
47 changes: 47 additions & 0 deletions pip_audit/format/cyclonedx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Dict, List

from cyclonedx import output # type: ignore
from cyclonedx.model.bom import Bom # type: ignore
from cyclonedx.model.component import Component # type: ignore
from cyclonedx.model.vulnerability import Vulnerability # type: ignore
from cyclonedx.parser import BaseParser # type: ignore

import pip_audit.service as service

from .interface import VulnerabilityFormat


class PipAuditResultParser(BaseParser):
def __init__(self, result: Dict[service.Dependency, List[service.VulnerabilityResult]]):
super().__init__()

for (dep, vulns) in result.items():
c = Component(name=dep.name, version=str(dep.version))

# TODO(ww): Figure out if/how we want to include other dependency
# metadata in the BOM, such as author, license, etc.
woodruffw marked this conversation as resolved.
Show resolved Hide resolved

for vuln in vulns:
# TODO(ww): Figure out if/how we want to include other vulnerability
# metadata in the BOM, such as source (OSV/PyPI/etc), URL, etc.
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
c.add_vulnerability(
Vulnerability(
id=vuln.id,
description=vuln.description,
advisories=[f"Upgrade: {v}" for v in vuln.fix_versions],
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
recommendations=["Upgrade"],
)
)

self._components.append(c)


class CycloneDxFormat(VulnerabilityFormat):
def format(self, result: Dict[service.Dependency, List[service.VulnerabilityResult]]) -> str:
parser = PipAuditResultParser(result)
bom = Bom.from_parser(parser)

# TODO(ww): Configurable output format.
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
formatter = output.get_instance(bom=bom, output_format=output.OutputFormat.JSON)

return formatter.output_as_string() # type: ignore
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"html5lib>=1.1",
"CacheControl==0.12.6",
"lockfile>=0.12.2",
"cyclonedx-python-lib==0.10",
],
extras_require={
"dev": [
Expand Down