owasp-depscan 5.4.8__py3-none-any.whl → 6.0.0a2__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.
Potentially problematic release.
This version of owasp-depscan might be problematic. Click here for more details.
- depscan/__init__.py +8 -0
- depscan/cli.py +719 -827
- depscan/cli_options.py +302 -0
- depscan/lib/audit.py +3 -1
- depscan/lib/bom.py +390 -288
- depscan/lib/config.py +86 -337
- depscan/lib/explainer.py +363 -98
- depscan/lib/license.py +11 -10
- depscan/lib/logger.py +65 -17
- depscan/lib/package_query/__init__.py +0 -0
- depscan/lib/package_query/cargo_pkg.py +124 -0
- depscan/lib/package_query/metadata.py +170 -0
- depscan/lib/package_query/npm_pkg.py +345 -0
- depscan/lib/package_query/pkg_query.py +195 -0
- depscan/lib/package_query/pypi_pkg.py +113 -0
- depscan/lib/tomlparse.py +116 -0
- depscan/lib/utils.py +34 -188
- owasp_depscan-6.0.0a2.dist-info/METADATA +390 -0
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info}/RECORD +28 -25
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info}/WHEEL +1 -1
- vendor/choosealicense.com/_licenses/cern-ohl-p-2.0.txt +1 -1
- vendor/choosealicense.com/_licenses/cern-ohl-s-2.0.txt +1 -1
- vendor/choosealicense.com/_licenses/cern-ohl-w-2.0.txt +2 -2
- vendor/choosealicense.com/_licenses/mit-0.txt +1 -1
- vendor/spdx/json/licenses.json +904 -677
- depscan/lib/analysis.py +0 -1550
- depscan/lib/csaf.py +0 -1860
- depscan/lib/normalize.py +0 -312
- depscan/lib/orasclient.py +0 -142
- depscan/lib/pkg_query.py +0 -532
- owasp_depscan-5.4.8.dist-info/METADATA +0 -580
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info}/entry_points.txt +0 -0
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info/licenses}/LICENSE +0 -0
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info}/top_level.txt +0 -0
depscan/lib/bom.py
CHANGED
|
@@ -1,46 +1,24 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import os
|
|
3
|
-
import shlex
|
|
4
2
|
import shutil
|
|
5
|
-
import subprocess
|
|
6
3
|
import sys
|
|
4
|
+
import uuid
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
7
|
from urllib.parse import unquote_plus
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
from blint.cyclonedx.spec import CycloneDX
|
|
10
|
+
from custom_json_diff.lib.utils import json_load, json_dump
|
|
10
11
|
from defusedxml.ElementTree import parse
|
|
12
|
+
from xbom_lib.blint import BlintGenerator
|
|
13
|
+
from xbom_lib.cdxgen import (
|
|
14
|
+
CdxgenGenerator,
|
|
15
|
+
CdxgenImageBasedGenerator,
|
|
16
|
+
CdxgenServerGenerator,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
|
-
from depscan.lib.logger import LOG
|
|
13
|
-
from depscan.lib.utils import cleanup_license_string
|
|
14
|
-
|
|
15
|
-
headers = {
|
|
16
|
-
"Content-Type": "application/json",
|
|
17
|
-
"Accept-Encoding": "gzip",
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def exec_tool(args, cwd=None, stdout=subprocess.PIPE):
|
|
22
|
-
"""
|
|
23
|
-
Convenience method to invoke cli tools
|
|
24
|
-
|
|
25
|
-
:param args: Command line arguments
|
|
26
|
-
:param cwd: Working directory
|
|
27
|
-
:param stdout: Specifies stdout of command
|
|
28
|
-
"""
|
|
29
|
-
try:
|
|
30
|
-
LOG.debug('⚡︎ Executing "%s"', " ".join(args))
|
|
31
|
-
cp = subprocess.run(
|
|
32
|
-
args,
|
|
33
|
-
stdout=stdout,
|
|
34
|
-
stderr=subprocess.STDOUT,
|
|
35
|
-
cwd=cwd,
|
|
36
|
-
env=os.environ.copy(),
|
|
37
|
-
shell=sys.platform == "win32",
|
|
38
|
-
encoding="utf-8",
|
|
39
|
-
check=False,
|
|
40
|
-
)
|
|
41
|
-
LOG.debug(cp.stdout)
|
|
42
|
-
except Exception as e:
|
|
43
|
-
LOG.exception(e)
|
|
19
|
+
from depscan.lib.logger import LOG, SPINNER, console
|
|
20
|
+
from depscan.lib.utils import cleanup_license_string
|
|
21
|
+
from typing import Dict, Optional
|
|
44
22
|
|
|
45
23
|
|
|
46
24
|
def parse_bom_ref(bomstr, licenses=None):
|
|
@@ -95,15 +73,13 @@ def get_licenses(ele):
|
|
|
95
73
|
"""
|
|
96
74
|
license_list = []
|
|
97
75
|
namespace = "{http://cyclonedx.org/schema/bom/1.5}"
|
|
98
|
-
for data in ele.findall(
|
|
99
|
-
f"{namespace}licenses/{namespace}license/{namespace}id"
|
|
100
|
-
):
|
|
76
|
+
for data in ele.findall(f"{namespace}licenses/{namespace}license/{namespace}id"):
|
|
101
77
|
license_list.append(data.text)
|
|
102
78
|
if not license_list:
|
|
103
79
|
for data in ele.findall(
|
|
104
80
|
f"{namespace}licenses/{namespace}license/{namespace}name"
|
|
105
81
|
):
|
|
106
|
-
if data and data.text:
|
|
82
|
+
if data is not None and data.text:
|
|
107
83
|
ld_list = [data.text]
|
|
108
84
|
if "http" in data.text:
|
|
109
85
|
ld_list = [
|
|
@@ -161,40 +137,49 @@ def get_pkg_list_json(jsonfile):
|
|
|
161
137
|
return List of dicts representing extracted packages
|
|
162
138
|
"""
|
|
163
139
|
pkgs = []
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
licenses
|
|
170
|
-
|
|
171
|
-
if not vendor:
|
|
172
|
-
vendor = ""
|
|
173
|
-
if comp.get("licenses"):
|
|
174
|
-
for lic in comp.get("licenses"):
|
|
175
|
-
license_obj = lic
|
|
176
|
-
# licenses has list of dict with either license
|
|
177
|
-
# or expression as key Only license is supported
|
|
178
|
-
# for now
|
|
179
|
-
if lic.get("license"):
|
|
180
|
-
license_obj = lic.get("license")
|
|
181
|
-
if license_obj.get("id"):
|
|
182
|
-
licenses.append(license_obj.get("id"))
|
|
183
|
-
elif license_obj.get("name"):
|
|
184
|
-
licenses.append(
|
|
185
|
-
cleanup_license_string(
|
|
186
|
-
license_obj.get("name")
|
|
187
|
-
)
|
|
188
|
-
)
|
|
189
|
-
pkgs.append(
|
|
190
|
-
{**comp, "vendor": vendor, "licenses": licenses}
|
|
191
|
-
)
|
|
192
|
-
except Exception:
|
|
193
|
-
# Ignore json errors
|
|
194
|
-
pass
|
|
140
|
+
if bom_data := json_load(jsonfile, log=LOG):
|
|
141
|
+
if bom_data.get("components"):
|
|
142
|
+
for comp in bom_data.get("components", []):
|
|
143
|
+
licenses, vendor, url = get_license_vendor_url(comp)
|
|
144
|
+
pkgs.append(
|
|
145
|
+
{**comp, "vendor": vendor, "licenses": licenses, "url": url}
|
|
146
|
+
)
|
|
195
147
|
return pkgs
|
|
196
148
|
|
|
197
149
|
|
|
150
|
+
def get_license_vendor_url(comp):
|
|
151
|
+
licenses = []
|
|
152
|
+
vendor = comp.get("group") or ""
|
|
153
|
+
if comp.get("licenses"):
|
|
154
|
+
for lic in comp.get("licenses"):
|
|
155
|
+
license_obj = lic
|
|
156
|
+
if lic.get("license"):
|
|
157
|
+
license_obj = lic.get("license")
|
|
158
|
+
if license_obj.get("id"):
|
|
159
|
+
licenses.append(license_obj.get("id"))
|
|
160
|
+
elif license_obj.get("name"):
|
|
161
|
+
licenses.append(cleanup_license_string(license_obj.get("name")))
|
|
162
|
+
url = ""
|
|
163
|
+
for aref in comp.get("externalReferences", []):
|
|
164
|
+
if aref.get("type") in (
|
|
165
|
+
"vcs",
|
|
166
|
+
"issue-tracker",
|
|
167
|
+
"website",
|
|
168
|
+
"bom",
|
|
169
|
+
"source-distribution",
|
|
170
|
+
"distribution",
|
|
171
|
+
"distribution-intake",
|
|
172
|
+
"build-system",
|
|
173
|
+
"model-card",
|
|
174
|
+
"evidence",
|
|
175
|
+
"formulation",
|
|
176
|
+
):
|
|
177
|
+
url = aref.get("url", "")
|
|
178
|
+
break
|
|
179
|
+
return licenses, vendor, url
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Unused
|
|
198
183
|
def get_pkg_list(xmlfile):
|
|
199
184
|
"""Method to parse the bom xml file and convert into packages list
|
|
200
185
|
|
|
@@ -232,247 +217,364 @@ def get_pkg_by_type(pkg_list, pkg_type):
|
|
|
232
217
|
if not pkg_list:
|
|
233
218
|
return []
|
|
234
219
|
return [
|
|
235
|
-
pkg
|
|
236
|
-
for pkg in pkg_list
|
|
237
|
-
if pkg.get("purl", "").startswith("pkg:" + pkg_type)
|
|
220
|
+
pkg for pkg in pkg_list if pkg.get("purl", "").startswith("pkg:" + pkg_type)
|
|
238
221
|
]
|
|
239
222
|
|
|
240
223
|
|
|
241
|
-
def
|
|
224
|
+
def create_bom(bom_file, src_dir=".", options=None):
|
|
242
225
|
"""
|
|
243
|
-
|
|
226
|
+
Method to create BOM file by executing cdxgen command
|
|
244
227
|
|
|
245
|
-
:param
|
|
246
|
-
:
|
|
228
|
+
:param bom_file: BOM file
|
|
229
|
+
:param src_dir: Source directory
|
|
230
|
+
:param options: Additional options for generating the BOM file.
|
|
231
|
+
:returns: True if the command was executed. False if the executable was
|
|
232
|
+
not found.
|
|
247
233
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
234
|
+
if not options:
|
|
235
|
+
options = {}
|
|
236
|
+
# Get the various options and filenames
|
|
237
|
+
techniques = options.get("techniques") or []
|
|
238
|
+
lifecycles = options.get("lifecycles") or []
|
|
239
|
+
project_type_list = options.get("project_type") or []
|
|
240
|
+
bom_engine = options.get("bom_engine", "")
|
|
241
|
+
lifecycle_analysis_mode = options.get("lifecycle_analysis_mode", False)
|
|
242
|
+
# Detect if blint needs to be used for the given project type, technique, and lifecycle.
|
|
243
|
+
# For binaries, generate an sbom with blint directly
|
|
244
|
+
if (
|
|
245
|
+
bom_engine == "BlintGenerator"
|
|
246
|
+
or "binary-analysis" in techniques
|
|
247
|
+
or "post-build" in lifecycles
|
|
248
|
+
or any([t in ("binary", "apk") for t in project_type_list])
|
|
249
|
+
):
|
|
250
|
+
return create_blint_bom(bom_file, src_dir, options=options)
|
|
251
|
+
cdxgen_server = options.get("cdxgen_server")
|
|
252
|
+
cdxgen_lib = CdxgenGenerator
|
|
253
|
+
|
|
254
|
+
# Should we call cdxgen server
|
|
255
|
+
if cdxgen_server or bom_engine == "CdxgenServerGenerator":
|
|
256
|
+
if not cdxgen_server:
|
|
257
|
+
LOG.error(
|
|
258
|
+
"Pass the `--cdxgen-server` argument to use the cdxgen server for BOM generation. Alternatively, use `--bom-engine auto` or `--bom-engine CdxgenGenerator`."
|
|
264
259
|
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
"%s command not found. Please install using npm install "
|
|
268
|
-
"@cyclonedx/cdxgen or set PATH variable",
|
|
269
|
-
cdxgen_cmd,
|
|
270
|
-
)
|
|
271
|
-
return False
|
|
272
|
-
try:
|
|
273
|
-
cdxgen_cmd = local_bin
|
|
274
|
-
# Set the plugins directory as an environment variable
|
|
275
|
-
os.environ["CDXGEN_PLUGINS_DIR"] = resource_path("local_bin")
|
|
276
|
-
return cdxgen_cmd
|
|
277
|
-
except Exception:
|
|
278
|
-
return None
|
|
279
|
-
else:
|
|
280
|
-
return cdxgen_cmd
|
|
260
|
+
return False
|
|
261
|
+
cdxgen_lib = CdxgenServerGenerator
|
|
281
262
|
else:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
cdxgen_cmd = local_bin
|
|
298
|
-
# Set the plugins directory as an environment variable
|
|
299
|
-
os.environ["CDXGEN_PLUGINS_DIR"] = (
|
|
300
|
-
resource_path("local_bin")
|
|
301
|
-
if sys.platform != "win32"
|
|
302
|
-
else resource_path(
|
|
303
|
-
os.path.join(
|
|
304
|
-
lbin,
|
|
305
|
-
"\\npm\\node_modules\\@cyclonedx\\cdxgen\\node_modules\\@cyclonedx\\cdxgen-plugins-bin\\plugins",
|
|
263
|
+
# Prefer the new image based generators if docker command is available in auto mode
|
|
264
|
+
if bom_engine == "CdxgenImageBasedGenerator":
|
|
265
|
+
cdxgen_lib = CdxgenImageBasedGenerator
|
|
266
|
+
elif bom_engine == "auto":
|
|
267
|
+
# Prefer local CLI while scanning container images
|
|
268
|
+
if any(
|
|
269
|
+
[
|
|
270
|
+
t in ("docker", "podman", "oci", "os", "hardware")
|
|
271
|
+
for t in project_type_list
|
|
272
|
+
]
|
|
273
|
+
):
|
|
274
|
+
cdxgen_lib = CdxgenGenerator
|
|
275
|
+
if lifecycle_analysis_mode:
|
|
276
|
+
LOG.warning(
|
|
277
|
+
"Lifecycle analysis is not supported for oci and os project types."
|
|
306
278
|
)
|
|
307
|
-
|
|
279
|
+
lifecycle_analysis_mode = True
|
|
280
|
+
elif (
|
|
281
|
+
shutil.which(os.getenv("DOCKER_CMD", "docker"))
|
|
282
|
+
and sys.platform != "win32"
|
|
283
|
+
):
|
|
284
|
+
cdxgen_lib = CdxgenImageBasedGenerator
|
|
285
|
+
# We now have the cdxgen library to use.
|
|
286
|
+
# For lifecycle analysis, we need to generate multiple BOM files
|
|
287
|
+
if lifecycle_analysis_mode:
|
|
288
|
+
return create_lifecycle_boms(cdxgen_lib, src_dir, options)
|
|
289
|
+
# Invoke the cdxgen library directly
|
|
290
|
+
with console.status(
|
|
291
|
+
f"Generating BOM for the source '{src_dir}' with cdxgen.", spinner=SPINNER
|
|
292
|
+
):
|
|
293
|
+
bom_result = cdxgen_lib(
|
|
294
|
+
src_dir, bom_file, logger=LOG, options=options
|
|
295
|
+
).generate()
|
|
296
|
+
if not bom_result.success:
|
|
297
|
+
LOG.info(
|
|
298
|
+
"The cdxgen invocation was unsuccessful. Try generating the BOM separately."
|
|
308
299
|
)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return None
|
|
300
|
+
LOG.debug(bom_result.command_output)
|
|
301
|
+
return bom_result.success and os.path.exists(bom_file)
|
|
312
302
|
|
|
313
303
|
|
|
314
|
-
def
|
|
304
|
+
def create_blint_bom(
|
|
305
|
+
bom_file: str, src_dir: str = ".", options: Optional[Dict] = None
|
|
306
|
+
) -> bool:
|
|
315
307
|
"""
|
|
316
|
-
Method to create BOM file by
|
|
308
|
+
Method to create BOM file by using blint
|
|
317
309
|
|
|
318
|
-
:param project_type: Project type
|
|
319
310
|
:param bom_file: BOM file
|
|
320
311
|
:param src_dir: Source directory
|
|
321
|
-
:param deep: A boolean flag indicating whether to perform a deep scan.
|
|
322
312
|
:param options: Additional options for generating the BOM file.
|
|
323
|
-
:returns: True if the
|
|
324
|
-
not found.
|
|
313
|
+
:returns: True if the bom was generated successfully. False otherwise.
|
|
325
314
|
"""
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
with
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
LOG.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
315
|
+
if options is None:
|
|
316
|
+
options = {}
|
|
317
|
+
reachability_analyzer = options.get("reachability_analyzer")
|
|
318
|
+
# The side effect is that we will almost always run blint in deep mode
|
|
319
|
+
if reachability_analyzer != "off" and not options.get("deep"):
|
|
320
|
+
options["deep"] = True
|
|
321
|
+
blint_lib = BlintGenerator(src_dir, bom_file, logger=LOG, options=options)
|
|
322
|
+
with console.status(
|
|
323
|
+
f"Generating BOM for the source '{src_dir}' with blint.", spinner=SPINNER
|
|
324
|
+
):
|
|
325
|
+
bom_result = blint_lib.generate()
|
|
326
|
+
if not bom_result.success:
|
|
327
|
+
LOG.info(
|
|
328
|
+
"The blint invocation was unsuccessful. Try generating the BOM separately."
|
|
329
|
+
)
|
|
330
|
+
# Empty SBOM is fine if there are no binaries in the project.
|
|
331
|
+
elif bom_result.bom_obj and isinstance(bom_result.bom_obj, CycloneDX):
|
|
332
|
+
if (
|
|
333
|
+
not bom_result.bom_obj.components
|
|
334
|
+
and not bom_result.bom_obj.dependencies
|
|
335
|
+
):
|
|
336
|
+
LOG.debug("Empty SBOM received from blint.")
|
|
337
|
+
return bom_result.success and os.path.exists(bom_file)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def create_lifecycle_boms(cdxgen_lib, src_dir, options):
|
|
341
|
+
"""
|
|
342
|
+
Method to create multiple BOM files for each lifecycle
|
|
343
|
+
|
|
344
|
+
:param cdxgen_lib: cdxgen library to use
|
|
345
|
+
:param src_dir: Source directory
|
|
346
|
+
:param options: Additional options for generating the BOM files
|
|
347
|
+
"""
|
|
348
|
+
lifecycles = options.get("lifecycles", []) or []
|
|
349
|
+
if lifecycles:
|
|
350
|
+
LOG.warning(
|
|
351
|
+
"Ignoring the `lifecycles` argument, as it is not required for lifecycle analysis."
|
|
352
|
+
)
|
|
353
|
+
any_success = False
|
|
354
|
+
prebuild_bom_file = options.get("prebuild_bom_file")
|
|
355
|
+
build_bom_file = options.get("build_bom_file")
|
|
356
|
+
postbuild_bom_file = options.get("postbuild_bom_file")
|
|
357
|
+
container_bom_file = options.get("container_bom_file")
|
|
358
|
+
reachability_analyzer = options.get("reachability_analyzer")
|
|
359
|
+
with console.status(
|
|
360
|
+
f"Generating lifecycle-specific BOMs for {src_dir}.", spinner=SPINNER
|
|
361
|
+
) as status:
|
|
362
|
+
# Start with build BOM generation.
|
|
363
|
+
# This would help atom compute reachable slices from a build perspective without getting confused
|
|
364
|
+
# about the pre-build state.
|
|
365
|
+
status.update(f"Generating build BOM for '{src_dir}' with cdxgen.")
|
|
366
|
+
coptions = {**options, "deep": "true", "lifecycles": ["build"]}
|
|
367
|
+
# We must also run it under research profile to help the reachability analyzer
|
|
368
|
+
# This logic could get refactored in the future
|
|
369
|
+
if reachability_analyzer != "off" and options.get("profile") != "research":
|
|
370
|
+
coptions["profile"] = "research"
|
|
371
|
+
bom_result = cdxgen_lib(
|
|
372
|
+
src_dir, build_bom_file, logger=LOG, options=coptions
|
|
373
|
+
).generate()
|
|
374
|
+
if not bom_result.success or not os.path.exists(build_bom_file):
|
|
375
|
+
LOG.debug(
|
|
376
|
+
"The cdxgen invocation was unsuccessful. Trying pre-build lifecycle."
|
|
377
|
+
)
|
|
378
|
+
LOG.debug(bom_result.command_output)
|
|
379
|
+
else:
|
|
380
|
+
any_success = True
|
|
381
|
+
# pre-build
|
|
382
|
+
status.update(f"Now generating pre-build BOM for '{src_dir}' with cdxgen.")
|
|
383
|
+
coptions = {**options, "deep": "false", "lifecycles": ["pre-build"]}
|
|
384
|
+
bom_result = cdxgen_lib(
|
|
385
|
+
src_dir, prebuild_bom_file, logger=LOG, options=coptions
|
|
386
|
+
).generate()
|
|
387
|
+
if not bom_result.success or not os.path.exists(prebuild_bom_file):
|
|
388
|
+
LOG.debug(
|
|
389
|
+
"The cdxgen invocation was unsuccessful. Trying the build lifecycle."
|
|
390
|
+
)
|
|
391
|
+
LOG.debug(bom_result.command_output)
|
|
392
|
+
else:
|
|
393
|
+
any_success = True
|
|
394
|
+
# container bom. For this we need the image name.
|
|
395
|
+
container_image_name = os.getenv("DEPSCAN_SOURCE_IMAGE") or options.get(
|
|
396
|
+
"source_image"
|
|
397
|
+
)
|
|
398
|
+
if container_image_name:
|
|
399
|
+
status.update(f"Generating container BOM for '{src_dir}' with cdxgen.")
|
|
400
|
+
coptions = {**options, "deep": "true", "project_type": ["oci"]}
|
|
401
|
+
if container_image_name == src_dir:
|
|
372
402
|
LOG.info(
|
|
373
|
-
"
|
|
374
|
-
"generate one locally."
|
|
403
|
+
"Set the environment variable DEPSCAN_SOURCE_IMAGE to the name of the container image to include its components."
|
|
375
404
|
)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if cdxgen_cmd:
|
|
400
|
-
exec_tool(
|
|
401
|
-
args,
|
|
402
|
-
src_dir
|
|
403
|
-
if project_type not in ("docker", "oci", "container")
|
|
404
|
-
and src_dir
|
|
405
|
-
and os.path.isdir(src_dir)
|
|
406
|
-
else None,
|
|
405
|
+
bom_result = cdxgen_lib(
|
|
406
|
+
container_image_name, container_bom_file, logger=LOG, options=coptions
|
|
407
|
+
).generate()
|
|
408
|
+
if not bom_result.success or not os.path.exists(container_bom_file):
|
|
409
|
+
LOG.debug(
|
|
410
|
+
"The cdxgen invocation was unsuccessful. Trying for the next lifecycle."
|
|
411
|
+
)
|
|
412
|
+
LOG.debug(bom_result.command_output)
|
|
413
|
+
else:
|
|
414
|
+
any_success = True
|
|
415
|
+
else:
|
|
416
|
+
LOG.debug(
|
|
417
|
+
"Set the environment variable DEPSCAN_SOURCE_IMAGE to the name of the container image to include its components."
|
|
418
|
+
)
|
|
419
|
+
status.update("Preparing blint for post-build BOM generation.")
|
|
420
|
+
# post-build BOM with blint
|
|
421
|
+
coptions = {**options, "deep": False, "use_blintdb": False, "lifecycles": ["post-build"]}
|
|
422
|
+
# What if the build directory is different to the source
|
|
423
|
+
build_dir = os.getenv("DEPSCAN_BUILD_DIR") or options.get("build_dir") or src_dir
|
|
424
|
+
res = create_blint_bom(postbuild_bom_file, build_dir, options=coptions)
|
|
425
|
+
if not res or not os.path.exists(postbuild_bom_file):
|
|
426
|
+
LOG.debug(
|
|
427
|
+
"The blint invocation was unsuccessful. Try building this project prior to invoking depscan. Alternatively, check if this project generates binary artefacts."
|
|
407
428
|
)
|
|
408
429
|
else:
|
|
409
|
-
|
|
410
|
-
return
|
|
430
|
+
any_success = True
|
|
431
|
+
return any_success
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def create_empty_vdr(pkg_list, ds_version):
|
|
435
|
+
components = pkg_list or []
|
|
436
|
+
bom_data = update_tools_metadata(None, None, ds_version)
|
|
437
|
+
return {**bom_data, "components": components}
|
|
411
438
|
|
|
412
439
|
|
|
413
|
-
def
|
|
440
|
+
def update_tools_metadata(tools, bom_data, ds_version):
|
|
414
441
|
"""
|
|
415
|
-
|
|
442
|
+
Helper function to add depscan information as metadata
|
|
443
|
+
:param tools: Tools section of the SBOM
|
|
444
|
+
:param bom_data: SBOM data
|
|
445
|
+
:param ds_version: depscan version
|
|
446
|
+
:return: None
|
|
447
|
+
"""
|
|
448
|
+
if not bom_data:
|
|
449
|
+
now_utc = datetime.now(timezone.utc)
|
|
450
|
+
bom_data = {
|
|
451
|
+
"bomFormat": "CycloneDX",
|
|
452
|
+
"specVersion": "1.6",
|
|
453
|
+
"serialNumber": f"urn:uuid:{uuid.uuid4()}",
|
|
454
|
+
"version": 1,
|
|
455
|
+
"metadata": {
|
|
456
|
+
"timestamp": now_utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
components = tools.get("components", []) if tools else []
|
|
460
|
+
needs_ds_component = (
|
|
461
|
+
len([c for c in components if c.get("name") == "owasp-depscan"]) == 0
|
|
462
|
+
)
|
|
463
|
+
if needs_ds_component:
|
|
464
|
+
ds_purl = f"pkg:pypi/owasp-depscan@{ds_version}"
|
|
465
|
+
components.append(
|
|
466
|
+
{
|
|
467
|
+
"type": "application",
|
|
468
|
+
"name": "owasp-depscan",
|
|
469
|
+
"version": ds_version,
|
|
470
|
+
"purl": ds_purl,
|
|
471
|
+
"bom-ref": ds_purl,
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
bom_data["metadata"]["tools"] = {"components": components}
|
|
475
|
+
return bom_data
|
|
416
476
|
|
|
417
|
-
|
|
418
|
-
|
|
477
|
+
|
|
478
|
+
def export_bom(bom_data, ds_version, pkg_vulnerabilities, vdr_file):
|
|
419
479
|
"""
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
480
|
+
Exports the Bill of Materials (BOM) data along with package vulnerabilities
|
|
481
|
+
to a Vulnerability Data Report (VDR) file.
|
|
482
|
+
|
|
483
|
+
:param bom_data: SBOM data
|
|
484
|
+
:param ds_version: depscan version
|
|
485
|
+
:param pkg_vulnerabilities: Package vulnerabilities
|
|
486
|
+
:param vdr_file: VDR file path
|
|
487
|
+
"""
|
|
488
|
+
# Add depscan information as metadata
|
|
489
|
+
metadata = bom_data.get("metadata", {})
|
|
490
|
+
tools = metadata.get("tools", {})
|
|
491
|
+
bom_version = str(bom_data.get("version", 0))
|
|
492
|
+
# Update the version
|
|
493
|
+
if bom_version.isdigit():
|
|
494
|
+
bom_data["version"] = int(bom_version) + 1
|
|
495
|
+
# Update the tools section
|
|
496
|
+
if isinstance(tools, dict):
|
|
497
|
+
bom_data = update_tools_metadata(tools, bom_data, ds_version)
|
|
498
|
+
bom_data = trim_vdr_bom_data(bom_data)
|
|
499
|
+
bom_data["vulnerabilities"] = pkg_vulnerabilities
|
|
500
|
+
json_dump(
|
|
501
|
+
vdr_file,
|
|
502
|
+
bom_data,
|
|
503
|
+
compact=True,
|
|
504
|
+
error_msg=f"Unable to generate VDR file at {vdr_file}",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def trim_vdr_bom_data(bom_data):
|
|
509
|
+
components = bom_data.get("components")
|
|
510
|
+
if not components:
|
|
511
|
+
return bom_data
|
|
512
|
+
metadata = bom_data.get("metadata")
|
|
513
|
+
if metadata and metadata.get("properties"):
|
|
514
|
+
del metadata["properties"]
|
|
515
|
+
bom_data["metadata"] = metadata
|
|
516
|
+
new_components = {}
|
|
517
|
+
component_identities = defaultdict(list)
|
|
518
|
+
for comp in components:
|
|
519
|
+
identity_evidences = comp.get("evidence", {}).get("identity", []) or []
|
|
520
|
+
if isinstance(identity_evidences, dict):
|
|
521
|
+
identity_evidences = [identity_evidences]
|
|
522
|
+
for p in (
|
|
523
|
+
"properties",
|
|
524
|
+
"signature",
|
|
525
|
+
"url",
|
|
526
|
+
"vendor",
|
|
527
|
+
"licenses", # We need a better logic to retain licenses here
|
|
528
|
+
):
|
|
529
|
+
if comp.get(p) is not None:
|
|
530
|
+
del comp[p]
|
|
531
|
+
ref = comp.get("bom-ref") or comp.get("purl")
|
|
532
|
+
# This is an error condition really
|
|
533
|
+
if not ref:
|
|
534
|
+
continue
|
|
535
|
+
component_identities[ref] += identity_evidences
|
|
536
|
+
if not new_components.get(ref):
|
|
537
|
+
new_components[ref] = comp
|
|
538
|
+
vdr_components = []
|
|
539
|
+
for ref, comp in new_components.items():
|
|
540
|
+
identity_evidences = component_identities[ref]
|
|
541
|
+
comp["evidence"] = {"identity": identity_evidences}
|
|
542
|
+
vdr_components.append(comp)
|
|
543
|
+
bom_data["components"] = vdr_components
|
|
544
|
+
for p in (
|
|
545
|
+
"annotations",
|
|
546
|
+
"signature",
|
|
547
|
+
):
|
|
548
|
+
if bom_data.get(p):
|
|
549
|
+
del bom_data[p]
|
|
550
|
+
return bom_data
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def annotate_vdr(vdr_file, txt_report_file):
|
|
554
|
+
if (
|
|
555
|
+
not vdr_file
|
|
556
|
+
or not txt_report_file
|
|
557
|
+
or not os.path.exists(vdr_file)
|
|
558
|
+
or not os.path.exists(txt_report_file)
|
|
559
|
+
):
|
|
560
|
+
return
|
|
561
|
+
vdr = json_load(vdr_file)
|
|
562
|
+
metadata = vdr.get("metadata", {})
|
|
563
|
+
tools = metadata.get("tools", {}).get("components", {})
|
|
564
|
+
with open(txt_report_file, errors="ignore", encoding="utf-8") as txt_fp:
|
|
565
|
+
report = txt_fp.read()
|
|
566
|
+
annotations = vdr.get("annotations", []) or []
|
|
567
|
+
depscan_annotation = {
|
|
568
|
+
"subjects": [vdr.get("serialNumber")],
|
|
569
|
+
"annotator": {"component": tools[-1] if len(tools) > 0 else {}},
|
|
570
|
+
"timestamp": metadata.get("timestamp"),
|
|
571
|
+
"text": report,
|
|
572
|
+
}
|
|
573
|
+
annotations.append(depscan_annotation)
|
|
574
|
+
vdr["annotations"] = annotations
|
|
575
|
+
json_dump(
|
|
576
|
+
vdr_file,
|
|
577
|
+
vdr,
|
|
578
|
+
compact=True,
|
|
579
|
+
error_msg=f"Unable to add annotations to the VDR file at {vdr_file}",
|
|
580
|
+
)
|