dt-sbom-scanner 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dt_sbom_scanner/AnsiColors.py +22 -0
- dt_sbom_scanner/__init__.py +0 -0
- dt_sbom_scanner/sbom_utils.py +180 -0
- dt_sbom_scanner/scan.py +1038 -0
- dt_sbom_scanner-1.8.0.dist-info/LICENSE.txt +21 -0
- dt_sbom_scanner-1.8.0.dist-info/METADATA +265 -0
- dt_sbom_scanner-1.8.0.dist-info/RECORD +9 -0
- dt_sbom_scanner-1.8.0.dist-info/WHEEL +4 -0
- dt_sbom_scanner-1.8.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class AnsiColors:
|
|
2
|
+
BLACK = "\033[0;30m"
|
|
3
|
+
RED = "\033[0;31m"
|
|
4
|
+
GREEN = "\033[0;32m"
|
|
5
|
+
YELLOW = "\033[0;33m"
|
|
6
|
+
BLUE = "\033[0;34m"
|
|
7
|
+
PURPLE = "\033[0;35m"
|
|
8
|
+
CYAN = "\033[0;36m"
|
|
9
|
+
WHITE = "\033[0;37m"
|
|
10
|
+
|
|
11
|
+
HGRAY = "\033[90m"
|
|
12
|
+
HRED = "\033[91m"
|
|
13
|
+
HGREEN = "\033[92m"
|
|
14
|
+
HYELLOW = "\033[93m"
|
|
15
|
+
HBLUE = "\033[94m"
|
|
16
|
+
HPURPLE = "\033[95m"
|
|
17
|
+
HCYAN = "\033[96m"
|
|
18
|
+
HWHITE = "\033[97m"
|
|
19
|
+
|
|
20
|
+
RESET = "\033[0m"
|
|
21
|
+
BOLD = "\033[1m"
|
|
22
|
+
UNDERLINE = "\033[4m"
|
|
File without changes
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from urllib.parse import quote_plus, unquote_plus
|
|
5
|
+
from warnings import catch_warnings
|
|
6
|
+
|
|
7
|
+
from cyclonedx.model import Property
|
|
8
|
+
from cyclonedx.model.bom import Bom
|
|
9
|
+
from cyclonedx.model.component import Component
|
|
10
|
+
from cyclonedx.output import OutputFormat, make_outputter
|
|
11
|
+
from cyclonedx.schema import SchemaVersion
|
|
12
|
+
|
|
13
|
+
from dt_sbom_scanner.AnsiColors import AnsiColors
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_bom(file: Path) -> Bom:
|
|
17
|
+
"""
|
|
18
|
+
Loads SBOM from file
|
|
19
|
+
"""
|
|
20
|
+
# NOTE: This is a hack to fix missing bom_ref in Component
|
|
21
|
+
component_init = Component.__init__
|
|
22
|
+
|
|
23
|
+
def component_patched(self, **kwargs):
|
|
24
|
+
if "bom_ref" not in kwargs:
|
|
25
|
+
print(
|
|
26
|
+
f"{AnsiColors.YELLOW}⚠{AnsiColors.RESET} missing 'bom_ref' in component {AnsiColors.HGRAY}{kwargs.get('name')}@{kwargs.get('version')}{AnsiColors.RESET} ({kwargs['type'].value}): fix"
|
|
27
|
+
)
|
|
28
|
+
kwargs["bom_ref"] = kwargs["name"]
|
|
29
|
+
component_init(self, **kwargs)
|
|
30
|
+
|
|
31
|
+
Component.__init__ = component_patched
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with catch_warnings(record=True) as warnings:
|
|
35
|
+
with open(file) as reader:
|
|
36
|
+
if file.suffix == ".xml":
|
|
37
|
+
bom = Bom.from_xml(reader)
|
|
38
|
+
else:
|
|
39
|
+
# NOTE: This is a hack to remove conflicting fields
|
|
40
|
+
# https://github.com/CycloneDX/cyclonedx-python-lib/issues/578
|
|
41
|
+
raw_json = json.load(reader)
|
|
42
|
+
for component in raw_json.get("components", []):
|
|
43
|
+
component.pop("evidence", None)
|
|
44
|
+
raw_json.pop("annotations", None)
|
|
45
|
+
raw_json.pop("formulation", None)
|
|
46
|
+
bom = Bom.from_json(raw_json)
|
|
47
|
+
|
|
48
|
+
# Restore original method
|
|
49
|
+
Component.__init__ = component_init
|
|
50
|
+
|
|
51
|
+
bom.validate()
|
|
52
|
+
|
|
53
|
+
if warnings:
|
|
54
|
+
for w in warnings:
|
|
55
|
+
print(
|
|
56
|
+
f"{AnsiColors.YELLOW}⚠{AnsiColors.RESET} l#{w.lineno}: {w.message}"
|
|
57
|
+
)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise ValueError(f"Error while loading SBOM: {file.name}") from e
|
|
60
|
+
|
|
61
|
+
return bom
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def trim_purls(sbom: Bom, limit: int = 0) -> None:
|
|
65
|
+
"""Tries to trim PURLs by removing longest qualifiers"""
|
|
66
|
+
if limit <= 0:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
for component in sbom.components:
|
|
70
|
+
purl = component.purl
|
|
71
|
+
if not purl:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
purl_orig = str(purl)
|
|
75
|
+
# url encode params if not already
|
|
76
|
+
for key in purl.qualifiers:
|
|
77
|
+
purl.qualifiers[key] = quote_plus(unquote_plus(purl.qualifiers[key]))
|
|
78
|
+
|
|
79
|
+
purl_trunc = str(purl)
|
|
80
|
+
if len(purl_trunc) < limit:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
while purl.qualifiers and len(purl_trunc) >= limit:
|
|
84
|
+
longest_key = max(
|
|
85
|
+
purl.qualifiers, key=lambda key: len(purl.qualifiers[key])
|
|
86
|
+
)
|
|
87
|
+
purl.qualifiers.pop(longest_key)
|
|
88
|
+
purl_trunc = str(purl)
|
|
89
|
+
|
|
90
|
+
if len(str(purl)) >= limit:
|
|
91
|
+
print(
|
|
92
|
+
f"{AnsiColors.YELLOW}⚠{AnsiColors.RESET} trimmed {purl_orig} -> {AnsiColors.HGRAY}{purl_trunc}{AnsiColors.RESET} but still exceeds limit ({limit})"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
print(
|
|
96
|
+
f"{AnsiColors.GREEN}✓{AnsiColors.RESET} successfully trimmed {purl_orig} -> {AnsiColors.HGRAY}{purl_trunc}{AnsiColors.RESET}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def serialize(
|
|
101
|
+
bom: Bom, format=OutputFormat.JSON, schema_version=SchemaVersion.V1_5
|
|
102
|
+
) -> str:
|
|
103
|
+
return make_outputter(bom, format, schema_version).output_as_string()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def to_json(bom: Bom, schema_version=SchemaVersion.V1_5) -> str:
|
|
107
|
+
return make_outputter(bom, OutputFormat.JSON, schema_version).output_as_string()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def to_xml(bom: Bom, schema_version=SchemaVersion.V1_5) -> str:
|
|
111
|
+
return make_outputter(bom, OutputFormat.XML, schema_version).output_as_string()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def save_bom(bom: Bom, file: Path, schema_version=SchemaVersion.V1_5) -> None:
|
|
115
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
return make_outputter(
|
|
117
|
+
bom,
|
|
118
|
+
OutputFormat.XML if file.suffix == ".xml" else OutputFormat.JSON,
|
|
119
|
+
schema_version,
|
|
120
|
+
).output_to_file(file, allow_overwrite=True)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# def cleanup(self, file: TextIO) -> str:
|
|
124
|
+
# """Cleans up a single SBOM for import into Dependency-Track"""
|
|
125
|
+
# bom = self.load(file)
|
|
126
|
+
# return self.output(bom)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def merge_boms(
|
|
130
|
+
root_name: str,
|
|
131
|
+
root_version: Optional[str],
|
|
132
|
+
root_group: Optional[str],
|
|
133
|
+
boms: list[Bom],
|
|
134
|
+
) -> Bom:
|
|
135
|
+
"""Merges multiple SBOMs into a single SBOM"""
|
|
136
|
+
merged = Bom()
|
|
137
|
+
root = merged.metadata.component = Component(
|
|
138
|
+
name=root_name, version=root_version, group=root_group
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
for bom in boms:
|
|
142
|
+
merged.metadata.authors.update(bom.metadata.authors)
|
|
143
|
+
|
|
144
|
+
merged.services.update(bom.services)
|
|
145
|
+
merged.vulnerabilities.update(bom.vulnerabilities)
|
|
146
|
+
|
|
147
|
+
depended = set()
|
|
148
|
+
for dependency in bom.dependencies:
|
|
149
|
+
if dependency.dependencies:
|
|
150
|
+
merged.register_dependency(
|
|
151
|
+
Component(name=dependency.ref.value, bom_ref=dependency.ref),
|
|
152
|
+
[
|
|
153
|
+
Component(name=d.ref.value, bom_ref=d.ref)
|
|
154
|
+
for d in dependency.dependencies
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
depended.update(d.ref for d in dependency.dependencies)
|
|
158
|
+
|
|
159
|
+
def add_component(component: Component, parent: Optional[Component]):
|
|
160
|
+
if all(c.bom_ref != component.bom_ref for c in merged.components):
|
|
161
|
+
if component in merged.components:
|
|
162
|
+
# allow duplicated component by adding an unique metadata
|
|
163
|
+
component.properties.add(
|
|
164
|
+
Property(
|
|
165
|
+
name="dt:merge-deduplicate", value=component.bom_ref.value
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
merged.components.add(component)
|
|
169
|
+
if parent and component.bom_ref not in depended:
|
|
170
|
+
merged.register_dependency(parent, [component])
|
|
171
|
+
for child in component.components:
|
|
172
|
+
add_component(child, component)
|
|
173
|
+
component.components.clear()
|
|
174
|
+
|
|
175
|
+
if bom.metadata.component:
|
|
176
|
+
add_component(bom.metadata.component, root)
|
|
177
|
+
for component in bom.components:
|
|
178
|
+
add_component(component, bom.metadata.component)
|
|
179
|
+
|
|
180
|
+
return merged
|