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.
@@ -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