ds-xbom-lib 6.0.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,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: ds-xbom-lib
3
+ Version: 6.0.0
4
+ Summary: xBOM library for owasp depscan
5
+ Author-email: Team AppThreat <cloud@appthreat.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/owasp-dep-scan/dep-scan
8
+ Project-URL: Bug-Tracker, https://github.com/owasp-dep-scan/dep-scan/issues
9
+ Project-URL: Funding, https://owasp.org/donate/?reponame=www-project-dep-scan&title=OWASP+depscan
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
@@ -0,0 +1,7 @@
1
+ xbom_lib/__init__.py,sha256=RSypMYLMPgkG6tKplLKIeeuu7eHnFEi31qlHns2S4Wk,1548
2
+ xbom_lib/blint.py,sha256=7bnmLhBWbjq7hl1BDEIeLXcZvHGcqBdKHUZGSnjyutw,1971
3
+ xbom_lib/cdxgen.py,sha256=1x42CrWELXniWNf9Iz_oHq4OvRJ2Dp108t73-yq8fRk,20668
4
+ ds_xbom_lib-6.0.0.dist-info/METADATA,sha256=oC0j7NP-5fQ6MAmBiCJdGMXmYukKbnAKdldbFwKrfXM,941
5
+ ds_xbom_lib-6.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ ds_xbom_lib-6.0.0.dist-info/top_level.txt,sha256=Oftitt49k3n5iTEN2Xt1IXuP9oFOJt5B_x-xCSBvaHY,9
7
+ ds_xbom_lib-6.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ xbom_lib
xbom_lib/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ from abc import ABC, abstractmethod
2
+ from logging import Logger
3
+ from typing import Optional, Dict, Any
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class BOMResult:
10
+ """
11
+ Data class representing the result of BOM generation.
12
+ """
13
+
14
+ success: bool = False
15
+ command_output: Optional[str] = None
16
+ bom_obj: Optional[Any] = None
17
+
18
+
19
+ class XBOMGenerator(ABC):
20
+ """
21
+ Base class for generating xBOM (Bill of Materials).
22
+
23
+ Attributes:
24
+ source_dir (str): Directory containing source files.
25
+ bom_file (str): Output BOM file path.
26
+ logger (Optional[logger]): Logger object
27
+ options (Optional[Dict[str, Any]]): Additional options for generation.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ source_dir: str,
33
+ bom_file: str,
34
+ logger: Optional[Logger] = None,
35
+ options: Optional[Dict[str, Any]] = None,
36
+ ) -> None:
37
+ """
38
+ Initialize the xBOMGenerator.
39
+
40
+ Args:
41
+ source_dir (str): The source directory.
42
+ bom_file (str): The BOM file path.
43
+ logger ():
44
+ options (Optional[Dict[str, Any]]): Additional generation options.
45
+ """
46
+ self.source_dir = source_dir
47
+ self.bom_file = bom_file
48
+ self.logger = logger
49
+ self.options = options if options is not None else {}
50
+
51
+ @abstractmethod
52
+ def generate(self) -> BOMResult:
53
+ """
54
+ Generate the BOM.
55
+ Must be implemented by subclasses.
56
+ """
57
+ raise NotImplementedError("Subclasses must implement this method.")
xbom_lib/blint.py ADDED
@@ -0,0 +1,59 @@
1
+ import os
2
+ import shutil
3
+ import tempfile
4
+
5
+ from xbom_lib import BOMResult, XBOMGenerator
6
+
7
+ BLINT_AVAILABLE = False
8
+ try:
9
+ from blint.lib.runners import run_sbom_mode
10
+ from blint.config import BlintOptions, BLINTDB_IMAGE_URL
11
+ from blint.lib.utils import blintdb_setup
12
+
13
+ BLINT_AVAILABLE = True
14
+ except ImportError:
15
+ pass
16
+
17
+
18
+ class BlintGenerator(XBOMGenerator):
19
+ """
20
+ Generate xBOM using blint
21
+ """
22
+
23
+ def generate(self) -> BOMResult:
24
+ """
25
+ Generate the BOM using blint.
26
+ """
27
+ if not BLINT_AVAILABLE:
28
+ return BOMResult(
29
+ success=False,
30
+ command_output="The required packages for binary SBOM generation are not available. Reinstall depscan using `pip install owasp-depscan[all]`.",
31
+ )
32
+ src_dir = self.source_dir
33
+ bom_file = self.bom_file
34
+ temp_reports_dir = tempfile.mkdtemp(
35
+ prefix="blint-reports-",
36
+ dir=os.getenv("DEPSCAN_TEMP_DIR")
37
+ or os.getenv("RUNNER_TEMP")
38
+ or os.getenv("AGENT_TEMPDIRECTORY"),
39
+ )
40
+ os.environ["BLINT_TEMP_DIR"] = temp_reports_dir
41
+ project_type_list = self.options.get("project_type") or []
42
+ blint_options = BlintOptions(
43
+ deep_mode=self.options.get("deep", True),
44
+ sbom_mode=True,
45
+ db_mode=os.getenv("USE_BLINTDB", "") in ("true", "1"),
46
+ no_reviews=True,
47
+ no_error=True,
48
+ quiet_mode=True,
49
+ src_dir_image=src_dir.split(","),
50
+ stdout_mode=False,
51
+ reports_dir=temp_reports_dir,
52
+ use_blintdb=self.options.get("use_blintdb", False),
53
+ image_url=self.options.get("blintdb_image_url", BLINTDB_IMAGE_URL),
54
+ sbom_output=bom_file,
55
+ )
56
+ blintdb_setup(blint_options)
57
+ sbom = run_sbom_mode(blint_options)
58
+ shutil.rmtree(temp_reports_dir, ignore_errors=True)
59
+ return BOMResult(success=True, bom_obj=sbom)
xbom_lib/cdxgen.py ADDED
@@ -0,0 +1,499 @@
1
+ import json
2
+ import os
3
+ import shlex
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+ from logging import Logger
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ import httpx
12
+
13
+ from xbom_lib import BOMResult, XBOMGenerator
14
+
15
+ cdxgen_server_headers = {
16
+ "Content-Type": "application/json",
17
+ "Accept-Encoding": "gzip",
18
+ }
19
+
20
+ # cdxgen timeout. Increased to 30 minutes
21
+ CDXGEN_TIMEOUT_MS = os.getenv("CDXGEN_TIMEOUT_MS", str(int(30 * 60 * 1000)))
22
+
23
+ # version of cdxgen to use
24
+ CDXGEN_IMAGE_VERSION = os.getenv("CDXGEN_IMAGE_VERSION", "v12.0.0")
25
+ CDXGEN_IMAGE_ROLLING_VERSION = os.getenv("CDXGEN_IMAGE_ROLLING_VERSION", "v12")
26
+
27
+ # cdxgen default image to use
28
+ DEFAULT_IMAGE_NAME = (
29
+ "default-secure"
30
+ if os.getenv("CDXGEN_SECURE_MODE", "") in ("true", "1")
31
+ else "default"
32
+ )
33
+
34
+ # cdxgen official image namespaces
35
+ OFFICIAL_IMAGE_NAMESPACES = (
36
+ "ghcr.io/cyclonedx/",
37
+ "ghcr.io/appthreat/",
38
+ "ghcr.io/owasp-dep-scan/",
39
+ )
40
+
41
+ PROJECT_TYPE_IMAGE = {
42
+ "default": f"ghcr.io/cyclonedx/cdxgen:{CDXGEN_IMAGE_VERSION}",
43
+ "deno": f"ghcr.io/cyclonedx/cdxgen-deno:{CDXGEN_IMAGE_VERSION}",
44
+ "bun": f"ghcr.io/cyclonedx/cdxgen-bun:{CDXGEN_IMAGE_VERSION}",
45
+ "default-secure": f"ghcr.io/cyclonedx/cdxgen-secure:{CDXGEN_IMAGE_VERSION}",
46
+ "java": f"ghcr.io/cyclonedx/cdxgen-temurin-java21:{CDXGEN_IMAGE_VERSION}",
47
+ "java24": f"ghcr.io/cyclonedx/cdxgen:{CDXGEN_IMAGE_VERSION}",
48
+ "android": f"ghcr.io/cyclonedx/cdxgen:{CDXGEN_IMAGE_VERSION}",
49
+ "java8": f"ghcr.io/cyclonedx/cdxgen-temurin-java8:{CDXGEN_IMAGE_VERSION}",
50
+ "java11-slim": f"ghcr.io/cyclonedx/cdxgen-java11-slim:{CDXGEN_IMAGE_VERSION}",
51
+ "java11": f"ghcr.io/cyclonedx/cdxgen-java11:{CDXGEN_IMAGE_VERSION}",
52
+ "java17": f"ghcr.io/cyclonedx/cdxgen-java17:{CDXGEN_IMAGE_VERSION}",
53
+ "java17-slim": f"ghcr.io/cyclonedx/cdxgen-java17-slim:{CDXGEN_IMAGE_VERSION}",
54
+ "java21": f"ghcr.io/cyclonedx/cdxgen-temurin-java21:{CDXGEN_IMAGE_VERSION}",
55
+ "node20": f"ghcr.io/cyclonedx/cdxgen-node20:{CDXGEN_IMAGE_VERSION}",
56
+ "python39": f"ghcr.io/cyclonedx/cdxgen-python39:{CDXGEN_IMAGE_VERSION}",
57
+ "python310": f"ghcr.io/cyclonedx/cdxgen-python310:{CDXGEN_IMAGE_VERSION}",
58
+ "python311": f"ghcr.io/cyclonedx/cdxgen-python311:{CDXGEN_IMAGE_VERSION}",
59
+ "python312": f"ghcr.io/cyclonedx/cdxgen-python312:{CDXGEN_IMAGE_VERSION}",
60
+ "python": f"ghcr.io/cyclonedx/cdxgen-python312:{CDXGEN_IMAGE_VERSION}",
61
+ "swift": f"ghcr.io/cyclonedx/cdxgen-debian-swift6:{CDXGEN_IMAGE_VERSION}",
62
+ "swift6": f"ghcr.io/cyclonedx/cdxgen-debian-swift6:{CDXGEN_IMAGE_VERSION}",
63
+ "ruby26": f"ghcr.io/cyclonedx/cdxgen-debian-ruby26:{CDXGEN_IMAGE_ROLLING_VERSION}",
64
+ "ruby33": f"ghcr.io/cyclonedx/cdxgen-debian-ruby33:{CDXGEN_IMAGE_ROLLING_VERSION}",
65
+ "ruby34": f"ghcr.io/cyclonedx/cdxgen-debian-ruby34:{CDXGEN_IMAGE_ROLLING_VERSION}",
66
+ "ruby": f"ghcr.io/cyclonedx/cdxgen-debian-ruby34:{CDXGEN_IMAGE_ROLLING_VERSION}",
67
+ "dotnet-core": f"ghcr.io/cyclonedx/cdxgen-debian-dotnet6:{CDXGEN_IMAGE_VERSION}",
68
+ "dotnet-framework": f"ghcr.io/cyclonedx/cdxgen-debian-dotnet6:{CDXGEN_IMAGE_VERSION}",
69
+ "dotnet6": f"ghcr.io/cyclonedx/cdxgen-debian-dotnet6:{CDXGEN_IMAGE_VERSION}",
70
+ "dotnet7": f"ghcr.io/cyclonedx/cdxgen-dotnet7:{CDXGEN_IMAGE_VERSION}",
71
+ "dotnet8": f"ghcr.io/cyclonedx/cdxgen-debian-dotnet8:{CDXGEN_IMAGE_VERSION}",
72
+ "dotnet9": f"ghcr.io/cyclonedx/cdxgen-debian-dotnet9:{CDXGEN_IMAGE_VERSION}",
73
+ "dotnet": f"ghcr.io/cyclonedx/cdxgen-debian-dotnet9:{CDXGEN_IMAGE_VERSION}",
74
+ }
75
+
76
+
77
+ def get_env_options_value(options: Dict, k: str, default: Optional[str] = None) -> str:
78
+ return os.getenv(k.upper(), options.get(k.lower(), default))
79
+
80
+
81
+ def get_image_for_type(options: Dict, project_type: str | list | None) -> str:
82
+ if not project_type:
83
+ return DEFAULT_IMAGE_NAME
84
+ project_types: list[str] = (
85
+ project_type if isinstance(project_type, list) else [project_type]
86
+ )
87
+ ptype = project_types[0] if len(project_types) == 1 else DEFAULT_IMAGE_NAME
88
+ default_img = PROJECT_TYPE_IMAGE.get(ptype, PROJECT_TYPE_IMAGE[DEFAULT_IMAGE_NAME])
89
+ return get_env_options_value(
90
+ options,
91
+ f"cdxgen_image_{ptype}",
92
+ default_img,
93
+ )
94
+
95
+
96
+ def needs_latest_image(image_name):
97
+ return any(
98
+ image_name.startswith(ns) for ns in OFFICIAL_IMAGE_NAMESPACES
99
+ ) or image_name.endswith((":latest", ":master", ":main", ":v11"))
100
+
101
+
102
+ def resource_path(relative_path):
103
+ """
104
+ Determine the absolute path of a resource file based on its relative path.
105
+
106
+ :param relative_path: Relative path of the resource file.
107
+ :return: Absolute path of the resource file
108
+ """
109
+ try:
110
+ base_path = sys._MEIPASS
111
+ except Exception:
112
+ base_path = os.path.dirname(__file__)
113
+ return os.path.join(base_path, relative_path)
114
+
115
+
116
+ def exec_tool(
117
+ args: List[str],
118
+ cwd: Optional[str] = None,
119
+ env: Optional[Dict] = None,
120
+ stdout: int = subprocess.PIPE,
121
+ logger: Optional[Logger] = None,
122
+ ) -> BOMResult:
123
+ """
124
+ Convenience method to invoke cli tools
125
+
126
+ :param args: Command line arguments
127
+ :param cwd: Working directory
128
+ :param env: Environment variables
129
+ :param stdout: Specifies stdout of command
130
+ :param logger: Logger object
131
+ """
132
+ if env is None:
133
+ env = os.environ.copy()
134
+ result = BOMResult(success=True)
135
+ try:
136
+ if logger and stdout != subprocess.DEVNULL:
137
+ logger.debug("Executing '%s'", " ".join(args))
138
+ cp = subprocess.run(
139
+ args,
140
+ stdout=stdout,
141
+ stderr=subprocess.STDOUT,
142
+ cwd=cwd,
143
+ env=env,
144
+ shell=sys.platform == "win32",
145
+ encoding="utf-8",
146
+ check=False,
147
+ )
148
+ result.command_output = cp.stdout
149
+ if logger and stdout != subprocess.DEVNULL:
150
+ logger.debug(cp.stdout)
151
+ except Exception as e:
152
+ result.success = False
153
+ result.command_output = f"Exception while running cdxgen: {e}"
154
+ return result
155
+
156
+
157
+ def find_cdxgen_cmd(use_bin=True, logger: Optional[Logger] = None):
158
+ if use_bin:
159
+ cdxgen_cmd = os.environ.get("CDXGEN_CMD", "cdxgen")
160
+ if not shutil.which(cdxgen_cmd):
161
+ local_bin = resource_path(
162
+ os.path.join(
163
+ "local_bin",
164
+ "cdxgen.exe" if sys.platform == "win32" else "cdxgen",
165
+ )
166
+ )
167
+ if not os.path.exists(local_bin):
168
+ if logger:
169
+ logger.info(
170
+ "%s command not found. Please install using npm install "
171
+ "@cyclonedx/cdxgen or set PATH variable",
172
+ cdxgen_cmd,
173
+ )
174
+ return False
175
+ cdxgen_cmd = local_bin
176
+ # Set the plugins directory as an environment variable
177
+ os.environ["CDXGEN_PLUGINS_DIR"] = resource_path("local_bin")
178
+ return cdxgen_cmd
179
+ else:
180
+ return cdxgen_cmd
181
+ else:
182
+ lbin = os.getenv("APPDATA") if sys.platform == "win32" else "local_bin"
183
+ local_bin = resource_path(
184
+ os.path.join(
185
+ f"{lbin}\\npm\\" if sys.platform == "win32" else "local_bin",
186
+ "cdxgen" if sys.platform != "win32" else "cdxgen.cmd",
187
+ )
188
+ )
189
+ if not os.path.exists(local_bin):
190
+ if logger:
191
+ logger.info(
192
+ "%s command not found. Please install using npm install "
193
+ "@cyclonedx/cdxgen or set PATH variable",
194
+ local_bin,
195
+ )
196
+ return None
197
+ cdxgen_cmd = local_bin
198
+ # Set the plugins directory as an environment variable
199
+ os.environ["CDXGEN_PLUGINS_DIR"] = (
200
+ resource_path("local_bin")
201
+ if sys.platform != "win32"
202
+ else resource_path(
203
+ os.path.join(
204
+ lbin,
205
+ "\\npm\\node_modules\\@cyclonedx\\cdxgen\\node_modules\\@cyclonedx\\cdxgen-plugins-bin\\plugins",
206
+ )
207
+ )
208
+ )
209
+ return cdxgen_cmd
210
+
211
+
212
+ def set_slices_args(project_type_list, args, dir):
213
+ if len(project_type_list) == 1:
214
+ for s in ("deps", "usages", "data-flow", "reachables", "semantics"):
215
+ args.append(f"--{s}-slices-file")
216
+ args.append(os.path.join(dir, f"{project_type_list[0]}-{s}.slices.json"))
217
+
218
+
219
+ class CdxgenGenerator(XBOMGenerator):
220
+ """
221
+ Concrete implementation of XBOMGenerator using cdxgen.
222
+ """
223
+
224
+ def generate(self) -> BOMResult:
225
+ """
226
+ Generate the BOM using the cdxgen tool.
227
+ """
228
+ options = self.options
229
+ project_type_list = self.options.get("project_type", [])
230
+ techniques = self.options.get("techniques", []) or []
231
+ lifecycles = self.options.get("lifecycles", []) or []
232
+ env = os.environ.copy()
233
+ # Implement the BOM generation logic using cdxgen.
234
+ cdxgen_cmd = find_cdxgen_cmd(logger=self.logger)
235
+ if not cdxgen_cmd:
236
+ cdxgen_cmd = find_cdxgen_cmd(False, logger=self.logger)
237
+ if not cdxgen_cmd:
238
+ cdxgen_cmd = "cdxgen"
239
+ project_type_args: list[str] = [f"-t {item}" for item in project_type_list]
240
+ technique_args: list[str] = [f"--technique {item}" for item in techniques]
241
+ args: list[str] = [cdxgen_cmd]
242
+ args = args + (" ".join(project_type_args).split())
243
+ args = args + ["-o", self.bom_file]
244
+ if technique_args:
245
+ args = args + (" ".join(technique_args).split())
246
+ if options.get("deep"):
247
+ args.append("--deep")
248
+ if options.get("profile"):
249
+ args.append("--profile")
250
+ args.append(options.get("profile", ""))
251
+ set_slices_args(project_type_list, args, os.path.dirname(self.bom_file))
252
+ if options.get("profile") not in ("generic",):
253
+ # This would help create openapi spec file inside the reports directory
254
+ env["ATOM_TOOLS_WORK_DIR"] = os.path.realpath(
255
+ os.path.dirname(self.bom_file)
256
+ )
257
+ env["ATOM_TOOLS_OPENAPI_FILENAME"] = (
258
+ f"{project_type_list[0]}-openapi.json"
259
+ )
260
+ if options.get("cdxgen_args"):
261
+ args += shlex.split(options.get("cdxgen_args", ""))
262
+ if len(lifecycles) == 1:
263
+ args = args + ["--lifecycle", lifecycles[0]]
264
+ # Bug #233 - Source directory could be None when working with url
265
+ if self.source_dir:
266
+ args.append(self.source_dir)
267
+ # Setup cdxgen thought logging
268
+ if self.options.get("explain"):
269
+ env["CDXGEN_THINK_MODE"] = "true"
270
+ # Manage cdxgen temp directory
271
+ cdxgen_temp_dir = None
272
+ if not os.getenv("CDXGEN_TEMP_DIR"):
273
+ cdxgen_temp_dir = tempfile.mkdtemp(
274
+ prefix="cdxgen-temp-", dir=os.getenv("DEPSCAN_TEMP_DIR")
275
+ )
276
+ env["CDXGEN_TEMP_DIR"] = cdxgen_temp_dir
277
+ env["CDXGEN_TIMEOUT_MS"] = CDXGEN_TIMEOUT_MS
278
+ if cdxgen_cmd:
279
+ bom_result = exec_tool(
280
+ args,
281
+ self.source_dir
282
+ if not any(
283
+ t in project_type_list for t in ("docker", "oci", "container")
284
+ )
285
+ and self.source_dir
286
+ and os.path.isdir(self.source_dir)
287
+ else None,
288
+ env,
289
+ logger=self.logger,
290
+ )
291
+ else:
292
+ bom_result = BOMResult(
293
+ success=False, command_output="Unable to locate cdxgen command."
294
+ )
295
+ if cdxgen_temp_dir:
296
+ shutil.rmtree(cdxgen_temp_dir, ignore_errors=True)
297
+ return bom_result
298
+
299
+
300
+ class CdxgenServerGenerator(CdxgenGenerator):
301
+ """
302
+ cdxgen generator that use a local cdxgen server for execution.
303
+ """
304
+
305
+ def generate(self) -> BOMResult:
306
+ """
307
+ Generate the BOM with cdxgen server.
308
+ """
309
+ options = self.options
310
+ cdxgen_server = self.options.get("cdxgen_server")
311
+ if not cdxgen_server:
312
+ return BOMResult(
313
+ success=False,
314
+ command_output="Pass the `--cdxgen-server` argument to use the cdxgen server for BOM generation.",
315
+ )
316
+ project_type_list = self.options.get("project_type", [])
317
+ src_dir = self.source_dir
318
+ if not src_dir and self.options.get("path"):
319
+ src_dir = self.options.get("path")
320
+ with httpx.Client(http2=True, base_url=cdxgen_server, timeout=180) as client:
321
+ sbom_url = f"{cdxgen_server}/sbom"
322
+ if self.logger:
323
+ self.logger.debug("Invoking cdxgen server at %s", sbom_url)
324
+ try:
325
+ r = client.post(
326
+ sbom_url,
327
+ json={
328
+ **options,
329
+ "url": options.get("url", ""),
330
+ "path": options.get("path", src_dir),
331
+ "type": ",".join(project_type_list),
332
+ "multiProject": options.get("multiProject", ""),
333
+ },
334
+ headers=cdxgen_server_headers,
335
+ )
336
+ if r.status_code == httpx.codes.OK:
337
+ try:
338
+ json_response = r.json()
339
+ if json_response:
340
+ with open(self.bom_file, "w", encoding="utf-8") as fp:
341
+ json.dump(json_response, fp)
342
+ return BOMResult(success=os.path.exists(self.bom_file))
343
+ except Exception as je:
344
+ return BOMResult(
345
+ success=False,
346
+ command_output=f"Unable to generate SBOM via cdxgen server due to {str(je)}",
347
+ )
348
+ else:
349
+ return BOMResult(
350
+ success=False,
351
+ command_output=f"Unable to generate SBOM via cdxgen server due to {str(r.status_code)}",
352
+ )
353
+ except Exception as e:
354
+ if self.logger:
355
+ self.logger.error(e)
356
+ return BOMResult(
357
+ success=False,
358
+ command_output="Unable to generate SBOM with cdxgen server. Trying to generate one locally.",
359
+ )
360
+
361
+
362
+ class CdxgenImageBasedGenerator(CdxgenGenerator):
363
+ """
364
+ cdxgen generator that use container images for execution.
365
+ """
366
+
367
+ def __init__(
368
+ self,
369
+ source_dir: str,
370
+ bom_file: str,
371
+ logger: Optional[Logger] = None,
372
+ options: Optional[Dict[str, Any]] = None,
373
+ ) -> None:
374
+ super().__init__(source_dir, bom_file, logger, options)
375
+ cdxgen_temp_dir = os.getenv("CDXGEN_TEMP_DIR")
376
+ if not cdxgen_temp_dir:
377
+ cdxgen_temp_dir = tempfile.mkdtemp(
378
+ prefix="cdxgen-temp-", dir=os.getenv("DEPSCAN_TEMP_DIR")
379
+ )
380
+ os.environ["CDXGEN_TEMP_DIR"] = cdxgen_temp_dir
381
+ self.cdxgen_temp_dir = cdxgen_temp_dir
382
+
383
+ def _container_run_cmd(self) -> Tuple[str, List[str]]:
384
+ """
385
+ Generate a container run command for the given project type, source directory, and output file
386
+ """
387
+ project_type_list = self.options.get("project_type", []) or []
388
+ techniques = self.options.get("techniques") or []
389
+ lifecycles = self.options.get("lifecycles") or []
390
+ image_output_dir = "/reports"
391
+ app_input_dir = "/app"
392
+ container_command = get_env_options_value(self.options, "DOCKER_CMD", "docker")
393
+ image_name = get_image_for_type(self.options, project_type_list)
394
+ run_command_args = [
395
+ container_command,
396
+ "run",
397
+ "--rm",
398
+ "--quiet",
399
+ "--workdir",
400
+ app_input_dir,
401
+ ]
402
+ output_file = os.path.basename(self.bom_file)
403
+ output_dir = os.path.realpath(os.path.dirname(self.bom_file))
404
+ # Setup environment variables
405
+ for k, _ in os.environ.items():
406
+ if (
407
+ k.startswith("CDXGEN_")
408
+ or k.startswith("GIT")
409
+ or k in ("FETCH_LICENSE",)
410
+ ):
411
+ run_command_args += ["-e", k]
412
+ run_command_args += ["-e", f"CDXGEN_TIMEOUT_MS={CDXGEN_TIMEOUT_MS}"]
413
+ # Enabling license fetch will improve metadata such as tags and description
414
+ # These will help with semantic reachability analysis
415
+ if self.options.get("profile") not in ("generic",):
416
+ # This would help create openapi spec file inside the reports directory
417
+ run_command_args += ["-e", f"ATOM_TOOLS_WORK_DIR={image_output_dir}"]
418
+ run_command_args += [
419
+ "-e",
420
+ f"ATOM_TOOLS_OPENAPI_FILENAME={project_type_list[0]}-openapi.json",
421
+ ]
422
+ run_command_args += ["-e", "CDXGEN_IN_CONTAINER=true"]
423
+ # Do not repeat the sponsorship banner. Please note that cdxgen and depscan are separate projects, so they ideally require separate sponsorships.
424
+ run_command_args += ["-e", "CDXGEN_NO_BANNER=true"]
425
+ # Do not repeat the CDXGEN_DEBUG_MODE environment variable
426
+ if os.getenv("SCAN_DEBUG_MODE") == "debug" and not os.getenv(
427
+ "CDXGEN_DEBUG_MODE"
428
+ ):
429
+ run_command_args += ["-e", "CDXGEN_DEBUG_MODE=debug"]
430
+ # Extra args like --platform=linux/amd64
431
+ if os.getenv("DEPSCAN_DOCKER_ARGS"):
432
+ run_command_args += os.getenv("DEPSCAN_DOCKER_ARGS", "").split(" ")
433
+ # Setup volume mounts
434
+ # Mount source directory as /app
435
+ if os.path.isdir(self.source_dir):
436
+ run_command_args += [
437
+ "-v",
438
+ f"{os.path.realpath(self.source_dir)}:{app_input_dir}:rw",
439
+ ]
440
+ else:
441
+ run_command_args.append(self.source_dir)
442
+ run_command_args += ["-v", f"{self.cdxgen_temp_dir}:/tmp:rw"]
443
+ run_command_args += [
444
+ "-v",
445
+ f"{output_dir}:{image_output_dir}:rw",
446
+ ]
447
+ # Mount the home directory as /root. Can be used for performance reasons.
448
+ if self.options.get("insecure_mount_home"):
449
+ run_command_args += ["-v", f"""{os.path.expanduser("~")}:/root:r"""]
450
+ run_command_args.append(image_name)
451
+ # output file mapped to the inside the image
452
+ run_command_args += ["-o", f"{image_output_dir}/{output_file}"]
453
+ # cdxgen args
454
+ technique_args = [f"--technique {item}" for item in techniques]
455
+ if technique_args:
456
+ run_command_args += " ".join(technique_args).split()
457
+ project_type_args = [f"-t {item}" for item in project_type_list]
458
+ if project_type_args:
459
+ run_command_args += " ".join(project_type_args).split()
460
+ if self.options.get("profile"):
461
+ run_command_args.append("--profile")
462
+ run_command_args.append(self.options.get("profile", ""))
463
+ set_slices_args(project_type_list, run_command_args, image_output_dir)
464
+ if len(lifecycles) == 1:
465
+ run_command_args += ["--lifecycle", lifecycles[0]]
466
+ if self.options.get("deep", "") in ("true", "1"):
467
+ run_command_args.append("--deep")
468
+ if self.options.get("cdxgen_args"):
469
+ run_command_args += shlex.split(self.options.get("cdxgen_args", ""))
470
+ return image_name, run_command_args
471
+
472
+ def generate(self) -> BOMResult:
473
+ """
474
+ Generate the BOM with official container images.
475
+ """
476
+ container_command = get_env_options_value(self.options, "DOCKER_CMD", "docker")
477
+ if not shutil.which(container_command):
478
+ return BOMResult(
479
+ success=False,
480
+ command_output=f"{container_command} command not found. Pass `--bom-engine CdxgenGenerator` to force depscan to use the local cdxgen CLI.",
481
+ )
482
+ image_name, run_command_args = self._container_run_cmd()
483
+ # Should we pull the most recent image
484
+ if needs_latest_image(image_name):
485
+ if self.logger:
486
+ self.logger.debug(
487
+ f"Pulling the image {image_name} using {container_command}."
488
+ )
489
+ exec_tool(
490
+ [container_command, "pull", "--quiet", image_name], logger=self.logger
491
+ )
492
+ if self.logger:
493
+ self.logger.debug(f"Executing {' '.join(run_command_args)}")
494
+ bom_result = exec_tool(
495
+ run_command_args, cwd=None, env=os.environ.copy(), logger=self.logger
496
+ )
497
+ if self.cdxgen_temp_dir:
498
+ shutil.rmtree(self.cdxgen_temp_dir, ignore_errors=True)
499
+ return bom_result