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.
- ds_xbom_lib-6.0.0a2.dist-info/METADATA +21 -0
- ds_xbom_lib-6.0.0a2.dist-info/RECORD +7 -0
- ds_xbom_lib-6.0.0a2.dist-info/WHEEL +5 -0
- ds_xbom_lib-6.0.0a2.dist-info/top_level.txt +1 -0
- xbom_lib/__init__.py +57 -0
- xbom_lib/blint.py +67 -0
- xbom_lib/cdxgen.py +492 -0
|
@@ -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 @@
|
|
|
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
|