ds-xbom-lib 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 ds-xbom-lib might be problematic. Click here for more details.

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