owasp-depscan 5.5.0__py3-none-any.whl → 6.0.0a3__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.
- 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 +387 -289
- depscan/lib/config.py +86 -337
- depscan/lib/explainer.py +389 -101
- 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.0a3.dist-info/METADATA +388 -0
- {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info}/RECORD +28 -25
- {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.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 -1554
- 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.5.0.dist-info/METADATA +0 -580
- {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info}/entry_points.txt +0 -0
- {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info/licenses}/LICENSE +0 -0
- {owasp_depscan-5.5.0.dist-info → owasp_depscan-6.0.0a3.dist-info}/top_level.txt +0 -0
depscan/cli.py
CHANGED
|
@@ -1,710 +1,439 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
1
|
+
#!/usr/bin/env python3 -W ignore::DeprecationWarning
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import contextlib
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
8
|
import tempfile
|
|
9
|
+
from typing import List
|
|
9
10
|
|
|
10
|
-
from
|
|
11
|
-
|
|
11
|
+
from analysis_lib import (
|
|
12
|
+
ReachabilityAnalysisKV,
|
|
13
|
+
VdrAnalysisKV,
|
|
14
|
+
)
|
|
15
|
+
from analysis_lib.csaf import export_csaf, write_toml
|
|
16
|
+
from analysis_lib.search import get_pkgs_by_scope
|
|
17
|
+
from analysis_lib.utils import (
|
|
18
|
+
get_all_bom_files,
|
|
19
|
+
get_all_pkg_list,
|
|
20
|
+
get_pkg_list,
|
|
21
|
+
licenses_risk_table,
|
|
22
|
+
pkg_risks_table,
|
|
23
|
+
summary_stats,
|
|
24
|
+
)
|
|
25
|
+
from analysis_lib.vdr import VDRAnalyzer
|
|
26
|
+
from analysis_lib.reachability import get_reachability_impl
|
|
27
|
+
from custom_json_diff.lib.utils import file_write, json_load
|
|
12
28
|
from rich.panel import Panel
|
|
13
29
|
from rich.terminal_theme import DEFAULT_TERMINAL_THEME, MONOKAI
|
|
14
30
|
from vdb.lib import config
|
|
15
|
-
from vdb.lib import
|
|
16
|
-
from vdb.lib.gha import GitHubSource
|
|
17
|
-
from vdb.lib.nvd import NvdSource
|
|
18
|
-
from vdb.lib.osv import OSVSource
|
|
31
|
+
from vdb.lib import db6 as db_lib
|
|
19
32
|
from vdb.lib.utils import parse_purl
|
|
20
33
|
|
|
21
|
-
from depscan
|
|
22
|
-
from depscan.
|
|
23
|
-
|
|
24
|
-
analyse_licenses,
|
|
25
|
-
analyse_pkg_risks,
|
|
26
|
-
find_purl_usages,
|
|
27
|
-
jsonl_report,
|
|
28
|
-
prepare_vdr,
|
|
29
|
-
suggest_version,
|
|
30
|
-
summary_stats,
|
|
31
|
-
)
|
|
34
|
+
from depscan import get_version
|
|
35
|
+
from depscan.cli_options import build_parser
|
|
36
|
+
from depscan.lib import explainer, utils
|
|
32
37
|
from depscan.lib.audit import audit, risk_audit, risk_audit_map, type_audit_map
|
|
33
38
|
from depscan.lib.bom import (
|
|
39
|
+
annotate_vdr,
|
|
40
|
+
create_empty_vdr,
|
|
34
41
|
create_bom,
|
|
42
|
+
export_bom,
|
|
35
43
|
get_pkg_by_type,
|
|
36
|
-
get_pkg_list,
|
|
37
|
-
submit_bom,
|
|
38
44
|
)
|
|
39
45
|
from depscan.lib.config import (
|
|
46
|
+
DEPSCAN_DEFAULT_VDR_FILE,
|
|
40
47
|
UNIVERSAL_SCAN_TYPE,
|
|
48
|
+
VDB_AGE_HOURS,
|
|
41
49
|
license_data_dir,
|
|
50
|
+
pkg_max_risk_score,
|
|
42
51
|
spdx_license_list,
|
|
52
|
+
vdb_database_url,
|
|
43
53
|
)
|
|
44
|
-
from depscan.lib.csaf import export_csaf, write_toml
|
|
45
54
|
from depscan.lib.license import build_license_data, bulk_lookup
|
|
46
|
-
from depscan.lib.logger import DEBUG, LOG, console
|
|
47
|
-
from depscan.lib.orasclient import download_image
|
|
55
|
+
from depscan.lib.logger import DEBUG, LOG, SPINNER, console, IS_CI
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
if sys.platform == "win32" and os.environ.get("PYTHONIOENCODING") is None:
|
|
58
|
+
sys.stdin.reconfigure(encoding="utf-8")
|
|
59
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
60
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
53
61
|
|
|
54
62
|
LOGO = """
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
██║ ██║██╔══╝ ██╔═══╝ ╚════██║██║ ██╔══██║██║╚██╗██║
|
|
59
|
-
██████╔╝███████╗██║ ███████║╚██████╗██║ ██║██║ ╚████║
|
|
60
|
-
╚═════╝ ╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝
|
|
63
|
+
_| _ ._ _ _ _. ._
|
|
64
|
+
(_| (/_ |_) _> (_ (_| | |
|
|
65
|
+
|
|
|
61
66
|
"""
|
|
62
67
|
|
|
68
|
+
QUART_AVAILABLE = False
|
|
69
|
+
try:
|
|
70
|
+
from quart import Quart, request
|
|
71
|
+
|
|
72
|
+
app = Quart(__name__, static_folder=None)
|
|
73
|
+
app.config.from_prefixed_env()
|
|
74
|
+
app.config["PROVIDE_AUTOMATIC_OPTIONS"] = True
|
|
75
|
+
QUART_AVAILABLE = True
|
|
76
|
+
except ImportError:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
ORAS_AVAILABLE = False
|
|
80
|
+
try:
|
|
81
|
+
from vdb.lib.orasclient import download_image
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
ORAS_AVAILABLE = True
|
|
84
|
+
except ImportError:
|
|
85
|
+
pass
|
|
66
86
|
|
|
67
87
|
|
|
68
88
|
def build_args():
|
|
69
89
|
"""
|
|
70
90
|
Constructs command line arguments for the depscan tool
|
|
71
91
|
"""
|
|
72
|
-
parser =
|
|
73
|
-
description="Fully open-source security and license audit for "
|
|
74
|
-
"application dependencies and container images based on "
|
|
75
|
-
"known vulnerabilities and advisories.",
|
|
76
|
-
epilog="Visit https://github.com/owasp-dep-scan/dep-scan to learn more.",
|
|
77
|
-
)
|
|
78
|
-
parser.add_argument(
|
|
79
|
-
"--no-banner",
|
|
80
|
-
action="store_true",
|
|
81
|
-
default=False,
|
|
82
|
-
dest="no_banner",
|
|
83
|
-
help="Do not display the logo and donation banner. Please make a donation to OWASP before using this argument.",
|
|
84
|
-
)
|
|
85
|
-
parser.add_argument(
|
|
86
|
-
"--cache",
|
|
87
|
-
action="store_true",
|
|
88
|
-
default=False,
|
|
89
|
-
dest="cache",
|
|
90
|
-
help="Cache vulnerability information in platform specific "
|
|
91
|
-
"user_data_dir",
|
|
92
|
-
)
|
|
93
|
-
parser.add_argument(
|
|
94
|
-
"--csaf",
|
|
95
|
-
action="store_true",
|
|
96
|
-
default=False,
|
|
97
|
-
dest="csaf",
|
|
98
|
-
help="Generate a OASIS CSAF VEX document",
|
|
99
|
-
)
|
|
100
|
-
parser.add_argument(
|
|
101
|
-
"--sync",
|
|
102
|
-
action="store_true",
|
|
103
|
-
default=False,
|
|
104
|
-
dest="sync",
|
|
105
|
-
help="Sync to receive the latest vulnerability data. Should have "
|
|
106
|
-
"invoked cache first.",
|
|
107
|
-
)
|
|
108
|
-
parser.add_argument(
|
|
109
|
-
"--profile",
|
|
110
|
-
default="generic",
|
|
111
|
-
choices=(
|
|
112
|
-
"appsec",
|
|
113
|
-
"research",
|
|
114
|
-
"operational",
|
|
115
|
-
"threat-modeling",
|
|
116
|
-
"license-compliance",
|
|
117
|
-
"generic",
|
|
118
|
-
),
|
|
119
|
-
dest="profile",
|
|
120
|
-
help="Profile to use while generating the BOM.",
|
|
121
|
-
)
|
|
122
|
-
parser.add_argument(
|
|
123
|
-
"--no-suggest",
|
|
124
|
-
action="store_false",
|
|
125
|
-
default="True",
|
|
126
|
-
dest="suggest",
|
|
127
|
-
help="Disable suggest mode",
|
|
128
|
-
)
|
|
129
|
-
parser.add_argument(
|
|
130
|
-
"--risk-audit",
|
|
131
|
-
action="store_true",
|
|
132
|
-
default=os.getenv("ENABLE_OSS_RISK", "") in ("true", "1"),
|
|
133
|
-
dest="risk_audit",
|
|
134
|
-
help="Perform package risk audit (slow operation). Npm only.",
|
|
135
|
-
)
|
|
136
|
-
parser.add_argument(
|
|
137
|
-
"--cdxgen-args",
|
|
138
|
-
default=os.getenv("CDXGEN_ARGS"),
|
|
139
|
-
dest="cdxgen_args",
|
|
140
|
-
help="Additional arguments to pass to cdxgen",
|
|
141
|
-
)
|
|
142
|
-
parser.add_argument(
|
|
143
|
-
"--private-ns",
|
|
144
|
-
dest="private_ns",
|
|
145
|
-
default=os.getenv("PKG_PRIVATE_NAMESPACE"),
|
|
146
|
-
help="Private namespace to use while performing oss risk audit. "
|
|
147
|
-
"Private packages should not be available in public registries "
|
|
148
|
-
"by default. Comma separated values accepted.",
|
|
149
|
-
)
|
|
150
|
-
parser.add_argument(
|
|
151
|
-
"-t",
|
|
152
|
-
"--type",
|
|
153
|
-
dest="project_type",
|
|
154
|
-
default=os.getenv("DEPSCAN_PROJECT_TYPE"),
|
|
155
|
-
help="Override project type if auto-detection is incorrect",
|
|
156
|
-
)
|
|
157
|
-
parser.add_argument(
|
|
158
|
-
"--bom",
|
|
159
|
-
dest="bom",
|
|
160
|
-
help="Examine using the given Software Bill-of-Materials (SBOM) file "
|
|
161
|
-
"in CycloneDX format. Use cdxgen command to produce one.",
|
|
162
|
-
)
|
|
163
|
-
parser.add_argument(
|
|
164
|
-
"-i",
|
|
165
|
-
"--src",
|
|
166
|
-
dest="src_dir_image",
|
|
167
|
-
help="Source directory or container image or binary file",
|
|
168
|
-
)
|
|
169
|
-
parser.add_argument(
|
|
170
|
-
"-o",
|
|
171
|
-
"--report_file",
|
|
172
|
-
dest="report_file",
|
|
173
|
-
help="DEPRECATED. Use reports directory since multiple files are "
|
|
174
|
-
"created. Report filename with directory",
|
|
175
|
-
)
|
|
176
|
-
parser.add_argument(
|
|
177
|
-
"--reports-dir",
|
|
178
|
-
default=os.getenv(
|
|
179
|
-
"DEPSCAN_REPORTS_DIR", os.path.join(os.getcwd(), "reports")
|
|
180
|
-
),
|
|
181
|
-
dest="reports_dir",
|
|
182
|
-
help="Reports directory",
|
|
183
|
-
)
|
|
184
|
-
parser.add_argument(
|
|
185
|
-
"--report-template",
|
|
186
|
-
dest="report_template",
|
|
187
|
-
help="Jinja template file used for rendering a custom report",
|
|
188
|
-
)
|
|
189
|
-
parser.add_argument(
|
|
190
|
-
"--report-name",
|
|
191
|
-
default="rendered.report",
|
|
192
|
-
dest="report_name",
|
|
193
|
-
help="Filename of the custom report written to the --reports-dir",
|
|
194
|
-
)
|
|
195
|
-
parser.add_argument(
|
|
196
|
-
"--no-error",
|
|
197
|
-
action="store_true",
|
|
198
|
-
default=False,
|
|
199
|
-
dest="noerror",
|
|
200
|
-
help="UNUSED: Continue on error to prevent build from breaking",
|
|
201
|
-
)
|
|
202
|
-
parser.add_argument(
|
|
203
|
-
"--no-license-scan",
|
|
204
|
-
action="store_true",
|
|
205
|
-
default=False,
|
|
206
|
-
dest="no_license_scan",
|
|
207
|
-
help="UNUSED: dep-scan doesn't perform license scanning by default",
|
|
208
|
-
)
|
|
209
|
-
parser.add_argument(
|
|
210
|
-
"--deep",
|
|
211
|
-
action="store_true",
|
|
212
|
-
default=False,
|
|
213
|
-
dest="deep_scan",
|
|
214
|
-
help="Perform deep scan by passing this --deep argument to cdxgen. "
|
|
215
|
-
"Useful while scanning docker images and OS packages.",
|
|
216
|
-
)
|
|
217
|
-
parser.add_argument(
|
|
218
|
-
"--no-universal",
|
|
219
|
-
action="store_true",
|
|
220
|
-
default=False,
|
|
221
|
-
dest="non_universal_scan",
|
|
222
|
-
help="Depscan would attempt to perform a single universal scan "
|
|
223
|
-
"instead of individual scans per language type.",
|
|
224
|
-
)
|
|
225
|
-
parser.add_argument(
|
|
226
|
-
"--no-vuln-table",
|
|
227
|
-
action="store_true",
|
|
228
|
-
default=False,
|
|
229
|
-
dest="no_vuln_table",
|
|
230
|
-
help="Do not print the table with the full list of vulnerabilities. "
|
|
231
|
-
"This can help reduce console output.",
|
|
232
|
-
)
|
|
233
|
-
parser.add_argument(
|
|
234
|
-
"--threatdb-server",
|
|
235
|
-
default=os.getenv("THREATDB_SERVER_URL"),
|
|
236
|
-
dest="threatdb_server",
|
|
237
|
-
help="ThreatDB server url. Eg: https://api.sbom.cx",
|
|
238
|
-
)
|
|
239
|
-
parser.add_argument(
|
|
240
|
-
"--threatdb-username",
|
|
241
|
-
default=os.getenv("THREATDB_USERNAME"),
|
|
242
|
-
dest="threatdb_username",
|
|
243
|
-
help="ThreatDB username",
|
|
244
|
-
)
|
|
245
|
-
parser.add_argument(
|
|
246
|
-
"--threatdb-password",
|
|
247
|
-
default=os.getenv("THREATDB_PASSWORD"),
|
|
248
|
-
dest="threatdb_password",
|
|
249
|
-
help="ThreatDB password",
|
|
250
|
-
)
|
|
251
|
-
parser.add_argument(
|
|
252
|
-
"--threatdb-token",
|
|
253
|
-
default=os.getenv("THREATDB_ACCESS_TOKEN"),
|
|
254
|
-
dest="threatdb_token",
|
|
255
|
-
help="ThreatDB token for token based submission",
|
|
256
|
-
)
|
|
257
|
-
parser.add_argument(
|
|
258
|
-
"--server",
|
|
259
|
-
action="store_true",
|
|
260
|
-
default=False,
|
|
261
|
-
dest="server_mode",
|
|
262
|
-
help="Run depscan as a server",
|
|
263
|
-
)
|
|
264
|
-
parser.add_argument(
|
|
265
|
-
"--server-host",
|
|
266
|
-
default=os.getenv("DEPSCAN_HOST", "127.0.0.1"),
|
|
267
|
-
dest="server_host",
|
|
268
|
-
help="depscan server host",
|
|
269
|
-
)
|
|
270
|
-
parser.add_argument(
|
|
271
|
-
"--server-port",
|
|
272
|
-
default=os.getenv("DEPSCAN_PORT", "7070"),
|
|
273
|
-
dest="server_port",
|
|
274
|
-
help="depscan server port",
|
|
275
|
-
)
|
|
276
|
-
parser.add_argument(
|
|
277
|
-
"--cdxgen-server",
|
|
278
|
-
default=os.getenv("CDXGEN_SERVER_URL"),
|
|
279
|
-
dest="cdxgen_server",
|
|
280
|
-
help="cdxgen server url. Eg: http://cdxgen:9090",
|
|
281
|
-
)
|
|
282
|
-
parser.add_argument(
|
|
283
|
-
"--debug",
|
|
284
|
-
action="store_true",
|
|
285
|
-
default=False,
|
|
286
|
-
dest="enable_debug",
|
|
287
|
-
help="Run depscan in debug mode.",
|
|
288
|
-
)
|
|
289
|
-
parser.add_argument(
|
|
290
|
-
"--explain",
|
|
291
|
-
action="store_true",
|
|
292
|
-
default=False,
|
|
293
|
-
dest="explain",
|
|
294
|
-
help="Makes depscan to explain the various analysis. Useful for creating detailed reports.",
|
|
295
|
-
)
|
|
296
|
-
parser.add_argument(
|
|
297
|
-
"--reachables-slices-file",
|
|
298
|
-
dest="reachables_slices_file",
|
|
299
|
-
help="Path for the reachables slices file created by atom.",
|
|
300
|
-
)
|
|
301
|
-
parser.add_argument(
|
|
302
|
-
"--purl",
|
|
303
|
-
dest="search_purl",
|
|
304
|
-
help="Scan a single package url.",
|
|
305
|
-
)
|
|
306
|
-
parser.add_argument(
|
|
307
|
-
"-v",
|
|
308
|
-
"--version",
|
|
309
|
-
help="Display the version",
|
|
310
|
-
action="version",
|
|
311
|
-
version="%(prog)s " + utils.get_version(),
|
|
312
|
-
)
|
|
92
|
+
parser = build_parser()
|
|
313
93
|
return parser.parse_args()
|
|
314
94
|
|
|
315
95
|
|
|
316
|
-
def
|
|
317
|
-
"""
|
|
318
|
-
Method to search packages in our vulnerability database
|
|
319
|
-
|
|
320
|
-
:param db: Reference to db
|
|
321
|
-
:param project_type: Project Type
|
|
322
|
-
:param pkg_list: List of packages
|
|
323
|
-
:param suggest_mode: True if package fix version should be normalized across
|
|
324
|
-
findings
|
|
325
|
-
:returns: A list of package issue objects or dictionaries.
|
|
326
|
-
A dictionary mapping package names to their aliases.
|
|
327
|
-
A dictionary mapping packages to their suggested fix versions.
|
|
328
|
-
A dictionary mapping package URLs to their aliases.
|
|
329
|
-
"""
|
|
330
|
-
if not pkg_list:
|
|
331
|
-
LOG.debug("Empty package search attempted!")
|
|
332
|
-
else:
|
|
333
|
-
LOG.debug("Scanning %d oss dependencies for issues", len(pkg_list))
|
|
334
|
-
results, pkg_aliases, purl_aliases = utils.search_pkgs(
|
|
335
|
-
db, project_type, pkg_list
|
|
336
|
-
)
|
|
337
|
-
# pkg_aliases is a dict that can be used to find the original vendor and
|
|
338
|
-
# package name This way we consistently use the same names used by the
|
|
339
|
-
# caller irrespective of how the result was obtained
|
|
340
|
-
sug_version_dict = {}
|
|
341
|
-
if suggest_mode:
|
|
342
|
-
# From the results identify optimal max version
|
|
343
|
-
sug_version_dict = suggest_version(results, pkg_aliases, purl_aliases)
|
|
344
|
-
if sug_version_dict:
|
|
345
|
-
LOG.debug(
|
|
346
|
-
"Adjusting fix version based on the initial suggestion %s",
|
|
347
|
-
sug_version_dict,
|
|
348
|
-
)
|
|
349
|
-
# Recheck packages
|
|
350
|
-
sug_pkg_list = []
|
|
351
|
-
for k, v in sug_version_dict.items():
|
|
352
|
-
if not v:
|
|
353
|
-
continue
|
|
354
|
-
vendor = ""
|
|
355
|
-
version = v
|
|
356
|
-
# Key is already a purl
|
|
357
|
-
if k.startswith("pkg:"):
|
|
358
|
-
try:
|
|
359
|
-
purl_obj = parse_purl(k)
|
|
360
|
-
vendor = purl_obj.get("namespace")
|
|
361
|
-
if not vendor:
|
|
362
|
-
vendor = purl_obj.get("type") or ""
|
|
363
|
-
name = purl_obj.get("name") or ""
|
|
364
|
-
version = purl_obj.get("version") or ""
|
|
365
|
-
sug_pkg_list.append(
|
|
366
|
-
{
|
|
367
|
-
"vendor": vendor,
|
|
368
|
-
"name": name,
|
|
369
|
-
"version": version,
|
|
370
|
-
"purl": k,
|
|
371
|
-
}
|
|
372
|
-
)
|
|
373
|
-
continue
|
|
374
|
-
except Exception:
|
|
375
|
-
pass
|
|
376
|
-
tmp_a = k.split(":")
|
|
377
|
-
if len(tmp_a) == 3:
|
|
378
|
-
vendor = tmp_a[0]
|
|
379
|
-
name = tmp_a[1]
|
|
380
|
-
else:
|
|
381
|
-
name = tmp_a[0]
|
|
382
|
-
# De-alias the vendor and package name
|
|
383
|
-
full_pkg = f"{vendor}:{name}:{version}"
|
|
384
|
-
full_pkg = pkg_aliases.get(full_pkg, full_pkg)
|
|
385
|
-
vendor, name, version = full_pkg.split(":")
|
|
386
|
-
sug_pkg_list.append(
|
|
387
|
-
{"vendor": vendor, "name": name, "version": version}
|
|
388
|
-
)
|
|
389
|
-
LOG.debug(
|
|
390
|
-
"Re-checking our suggestion to ensure there are no further "
|
|
391
|
-
"vulnerabilities"
|
|
392
|
-
)
|
|
393
|
-
override_results, _, _ = utils.search_pkgs(
|
|
394
|
-
db, project_type, sug_pkg_list
|
|
395
|
-
)
|
|
396
|
-
if override_results:
|
|
397
|
-
new_sug_dict = suggest_version(override_results)
|
|
398
|
-
LOG.debug("Received override results: %s", new_sug_dict)
|
|
399
|
-
for nk, nv in new_sug_dict.items():
|
|
400
|
-
sug_version_dict[nk] = nv
|
|
401
|
-
return results, pkg_aliases, sug_version_dict, purl_aliases
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def summarise(
|
|
96
|
+
def vdr_analyze_summarize(
|
|
405
97
|
project_type,
|
|
406
98
|
results,
|
|
407
|
-
|
|
408
|
-
purl_aliases,
|
|
409
|
-
sug_version_dict,
|
|
99
|
+
suggest_mode,
|
|
410
100
|
scoped_pkgs,
|
|
411
|
-
report_file,
|
|
412
101
|
bom_file,
|
|
102
|
+
bom_dir,
|
|
103
|
+
pkg_list,
|
|
104
|
+
reachability_analyzer,
|
|
105
|
+
reachability_options,
|
|
413
106
|
no_vuln_table=False,
|
|
414
|
-
|
|
415
|
-
|
|
107
|
+
fuzzy_search=False,
|
|
108
|
+
search_order=None,
|
|
416
109
|
):
|
|
417
110
|
"""
|
|
418
|
-
Method to
|
|
419
|
-
:param project_type: Project type
|
|
420
|
-
:param results: Scan or audit results
|
|
421
|
-
:param
|
|
422
|
-
:param
|
|
423
|
-
:param
|
|
424
|
-
:param
|
|
425
|
-
:param
|
|
426
|
-
:param
|
|
111
|
+
Method to perform VDR analysis followed by summarization.
|
|
112
|
+
:param project_type: Project type.
|
|
113
|
+
:param results: Scan or audit results.
|
|
114
|
+
:param suggest_mode: Normalize fix versions automatically.
|
|
115
|
+
:param scoped_pkgs: Dict containing package scopes.
|
|
116
|
+
:param bom_file: Single BOM file.
|
|
117
|
+
:param bom_dir: Directory containining bom files.
|
|
118
|
+
:param pkg_list: Direct list of packages when the bom file is empty.
|
|
119
|
+
:param reachability_analyzer: Reachability Analyzer specified.
|
|
120
|
+
:param reachability_options: Reachability Analyzer options.
|
|
427
121
|
:param no_vuln_table: Boolean to indicate if the results should get printed
|
|
428
|
-
to the console
|
|
122
|
+
to the console.
|
|
123
|
+
:param fuzzy_search: Perform fuzzy search.
|
|
124
|
+
:param search_order: Search order.
|
|
125
|
+
|
|
429
126
|
:return: A dict of vulnerability and severity summary statistics
|
|
430
127
|
"""
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
128
|
+
pkg_vulnerabilities = []
|
|
129
|
+
summary = {}
|
|
130
|
+
direct_purls = {}
|
|
131
|
+
reached_purls = {}
|
|
132
|
+
reached_services = {}
|
|
133
|
+
endpoint_reached_purls = {}
|
|
134
|
+
# Perform the reachability analysis first
|
|
135
|
+
reach_result = get_reachability_impl(
|
|
136
|
+
reachability_analyzer, reachability_options
|
|
137
|
+
).process()
|
|
138
|
+
# We now have reachability results, OpenAPI endpoints, BOMs, and component scope information.
|
|
139
|
+
if reach_result and reach_result.success:
|
|
140
|
+
direct_purls = reach_result.direct_purls
|
|
141
|
+
reached_purls = reach_result.reached_purls
|
|
142
|
+
reached_services = reach_result.reached_services
|
|
143
|
+
endpoint_reached_purls = reach_result.endpoint_reached_purls
|
|
144
|
+
console.record = True
|
|
145
|
+
# We might already have the needed slices files when we reach here.
|
|
146
|
+
options = VdrAnalysisKV(
|
|
444
147
|
project_type,
|
|
445
148
|
results,
|
|
446
|
-
pkg_aliases,
|
|
447
|
-
purl_aliases,
|
|
448
|
-
|
|
149
|
+
pkg_aliases={},
|
|
150
|
+
purl_aliases={},
|
|
151
|
+
suggest_mode=suggest_mode,
|
|
449
152
|
scoped_pkgs=scoped_pkgs,
|
|
450
153
|
no_vuln_table=no_vuln_table,
|
|
451
154
|
bom_file=bom_file,
|
|
155
|
+
bom_dir=bom_dir,
|
|
156
|
+
pkg_list=pkg_list,
|
|
452
157
|
direct_purls=direct_purls,
|
|
453
158
|
reached_purls=reached_purls,
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
LOG.debug(
|
|
492
|
-
"VDR file %s generated successfully", vdr_file
|
|
493
|
-
)
|
|
494
|
-
except Exception:
|
|
495
|
-
LOG.warning("Unable to generate VDR file for this scan")
|
|
496
|
-
summary = summary_stats(results)
|
|
497
|
-
return summary, vdr_file, pkg_vulnerabilities, pkg_group_rows
|
|
498
|
-
|
|
159
|
+
reached_services=reached_services,
|
|
160
|
+
endpoint_reached_purls=endpoint_reached_purls,
|
|
161
|
+
console=console,
|
|
162
|
+
logger=LOG,
|
|
163
|
+
fuzzy_search=fuzzy_search,
|
|
164
|
+
search_order=search_order,
|
|
165
|
+
)
|
|
166
|
+
ds_version = get_version()
|
|
167
|
+
vdr_result = VDRAnalyzer(vdr_options=options).process()
|
|
168
|
+
vdr_file = bom_file.replace(".cdx.json", ".vdr.json") if bom_file else None
|
|
169
|
+
if not vdr_file and bom_dir:
|
|
170
|
+
vdr_file = os.path.join(bom_dir, DEPSCAN_DEFAULT_VDR_FILE)
|
|
171
|
+
if vdr_result.success:
|
|
172
|
+
pkg_vulnerabilities = vdr_result.pkg_vulnerabilities
|
|
173
|
+
cdx_vdr_data = None
|
|
174
|
+
# Always create VDR files even when empty
|
|
175
|
+
if pkg_vulnerabilities is not None:
|
|
176
|
+
# Case 1: Single BOM file resulting in a single VDR file
|
|
177
|
+
if bom_file:
|
|
178
|
+
cdx_vdr_data = json_load(bom_file, log=LOG)
|
|
179
|
+
# Case 2: Multiple BOM files in a bom directory
|
|
180
|
+
elif bom_dir:
|
|
181
|
+
cdx_vdr_data = create_empty_vdr(pkg_list, ds_version)
|
|
182
|
+
if cdx_vdr_data:
|
|
183
|
+
export_bom(cdx_vdr_data, ds_version, pkg_vulnerabilities, vdr_file)
|
|
184
|
+
LOG.debug(f"The VDR file '{vdr_file}' was created successfully.")
|
|
185
|
+
else:
|
|
186
|
+
LOG.debug(
|
|
187
|
+
f"VDR file '{vdr_file}' was not created for the type {project_type}."
|
|
188
|
+
)
|
|
189
|
+
summary = summary_stats(pkg_vulnerabilities)
|
|
190
|
+
elif bom_dir or bom_file or pkg_list:
|
|
191
|
+
if project_type != "bom":
|
|
192
|
+
LOG.info("No vulnerabilities found for project type '%s'!", project_type)
|
|
193
|
+
else:
|
|
194
|
+
LOG.info("No vulnerabilities found!")
|
|
195
|
+
return summary, vdr_file, vdr_result
|
|
499
196
|
|
|
500
|
-
@app.get("/")
|
|
501
|
-
async def index():
|
|
502
|
-
"""
|
|
503
197
|
|
|
504
|
-
|
|
198
|
+
def set_project_types(args, src_dir):
|
|
505
199
|
"""
|
|
506
|
-
|
|
200
|
+
Detects the project types and perform the right type of scan
|
|
507
201
|
|
|
202
|
+
:param args: cli arguments
|
|
203
|
+
:param src_dir: source directory
|
|
508
204
|
|
|
509
|
-
|
|
510
|
-
|
|
205
|
+
:return: A tuple containing the package list, the parsed package URL object,
|
|
206
|
+
and the list of project types.
|
|
511
207
|
"""
|
|
208
|
+
pkg_list, purl_obj = [], {}
|
|
209
|
+
project_types_list: List[str] = []
|
|
210
|
+
if args.search_purl:
|
|
211
|
+
purl_obj = parse_purl(args.search_purl)
|
|
212
|
+
purl_obj["purl"] = args.search_purl
|
|
213
|
+
purl_obj["vendor"] = purl_obj.get("namespace")
|
|
214
|
+
if purl_obj.get("type"):
|
|
215
|
+
project_types_list = [purl_obj.get("type", "")]
|
|
216
|
+
pkg_list = [purl_obj]
|
|
217
|
+
elif args.bom or args.bom_dir:
|
|
218
|
+
project_types_list = ["bom"]
|
|
219
|
+
elif args.project_type:
|
|
220
|
+
project_types_list = (
|
|
221
|
+
args.project_type
|
|
222
|
+
if isinstance(args.project_type, list)
|
|
223
|
+
else args.project_type.split(",")
|
|
224
|
+
)
|
|
225
|
+
if len(project_types_list) == 1 and "," in project_types_list[0]:
|
|
226
|
+
project_types_list = project_types_list[0].split(",")
|
|
227
|
+
elif not args.non_universal_scan:
|
|
228
|
+
project_types_list = [UNIVERSAL_SCAN_TYPE]
|
|
229
|
+
else:
|
|
230
|
+
project_types_list = utils.detect_project_type(src_dir)
|
|
231
|
+
return pkg_list, project_types_list
|
|
512
232
|
|
|
513
|
-
:return: a JSON response indicating the status of the caching operation.
|
|
514
|
-
"""
|
|
515
|
-
db = db_lib.get()
|
|
516
|
-
if not db_lib.index_count(db["index_file"]):
|
|
517
|
-
paths_list = download_image()
|
|
518
|
-
if paths_list:
|
|
519
|
-
return {
|
|
520
|
-
"error": "false",
|
|
521
|
-
"message": "vulnerability database cached successfully",
|
|
522
|
-
}
|
|
523
|
-
else:
|
|
524
|
-
return {
|
|
525
|
-
"error": "true",
|
|
526
|
-
"message": "vulnerability database was not cached",
|
|
527
|
-
}
|
|
528
|
-
return {
|
|
529
|
-
"error": "false",
|
|
530
|
-
"message": "vulnerability database already exists",
|
|
531
|
-
}
|
|
532
233
|
|
|
234
|
+
if QUART_AVAILABLE:
|
|
533
235
|
|
|
534
|
-
@app.
|
|
535
|
-
async def
|
|
536
|
-
|
|
537
|
-
:return: A JSON response containing the SBOM file path and a list of
|
|
538
|
-
vulnerabilities found in the scanned packages
|
|
539
|
-
"""
|
|
540
|
-
q = request.args
|
|
541
|
-
params = await request.get_json()
|
|
542
|
-
uploaded_bom_file = await request.files
|
|
236
|
+
@app.get("/")
|
|
237
|
+
async def index():
|
|
238
|
+
"""
|
|
543
239
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
project_type = None
|
|
548
|
-
results = []
|
|
549
|
-
db = db_lib.get()
|
|
550
|
-
profile = "generic"
|
|
551
|
-
deep = False
|
|
552
|
-
if q.get("url"):
|
|
553
|
-
url = q.get("url")
|
|
554
|
-
if q.get("path"):
|
|
555
|
-
path = q.get("path")
|
|
556
|
-
if q.get("multiProject"):
|
|
557
|
-
multi_project = q.get("multiProject", "").lower() in ("true", "1")
|
|
558
|
-
if q.get("deep"):
|
|
559
|
-
deep = q.get("deep", "").lower() in ("true", "1")
|
|
560
|
-
if q.get("type"):
|
|
561
|
-
project_type = q.get("type")
|
|
562
|
-
if q.get("profile"):
|
|
563
|
-
profile = q.get("profile")
|
|
564
|
-
if params is not None:
|
|
565
|
-
if not url and params.get("url"):
|
|
566
|
-
url = params.get("url")
|
|
567
|
-
if not path and params.get("path"):
|
|
568
|
-
path = params.get("path")
|
|
569
|
-
if not multi_project and params.get("multiProject"):
|
|
570
|
-
multi_project = params.get("multiProject", "").lower() in (
|
|
571
|
-
"true",
|
|
572
|
-
"1",
|
|
573
|
-
)
|
|
574
|
-
if not deep and params.get("deep"):
|
|
575
|
-
deep = params.get("deep", "").lower() in (
|
|
576
|
-
"true",
|
|
577
|
-
"1",
|
|
578
|
-
)
|
|
579
|
-
if not project_type and params.get("type"):
|
|
580
|
-
project_type = params.get("type")
|
|
581
|
-
if not profile and params.get("profile"):
|
|
582
|
-
profile = params.get("profile")
|
|
240
|
+
:return: An empty dictionary
|
|
241
|
+
"""
|
|
242
|
+
return {}
|
|
583
243
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
"message": "path or url or a bom file upload is required",
|
|
588
|
-
}, 400
|
|
589
|
-
if not project_type:
|
|
590
|
-
return {"error": "true", "message": "project type is required"}, 400
|
|
244
|
+
@app.get("/download-vdb")
|
|
245
|
+
async def download_vdb():
|
|
246
|
+
"""
|
|
591
247
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
248
|
+
:return: a JSON response indicating the status of the caching operation.
|
|
249
|
+
"""
|
|
250
|
+
if db_lib.needs_update(days=0, hours=VDB_AGE_HOURS, default_status=False):
|
|
251
|
+
if not ORAS_AVAILABLE:
|
|
252
|
+
return {
|
|
253
|
+
"error": "true",
|
|
254
|
+
"message": "The oras package must be installed to automatically download the vulnerability database. Install depscan using `pip install owasp-depscan[all]` or use the official container image.",
|
|
255
|
+
}
|
|
256
|
+
if download_image(vdb_database_url, config.DATA_DIR):
|
|
257
|
+
return {
|
|
258
|
+
"error": "false",
|
|
259
|
+
"message": "vulnerability database downloaded successfully",
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
595
262
|
"error": "true",
|
|
596
|
-
"message": "
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
263
|
+
"message": "vulnerability database did not get downloaded correctly. Check the server logs.",
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
"error": "false",
|
|
267
|
+
"message": "vulnerability database already exists",
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@app.route("/scan", methods=["GET", "POST"])
|
|
271
|
+
async def run_scan():
|
|
272
|
+
"""
|
|
273
|
+
:return: A JSON response containing the SBOM file path and a list of
|
|
274
|
+
vulnerabilities found in the scanned packages
|
|
275
|
+
"""
|
|
276
|
+
q = request.args
|
|
277
|
+
params = await request.get_json()
|
|
278
|
+
uploaded_bom_file = await request.files
|
|
279
|
+
|
|
280
|
+
url = None
|
|
281
|
+
path = None
|
|
282
|
+
multi_project = None
|
|
283
|
+
project_type = None
|
|
284
|
+
results = []
|
|
285
|
+
profile = "generic"
|
|
286
|
+
deep = False
|
|
287
|
+
suggest_mode = True if q.get("suggest") in ("true", "1") else False
|
|
288
|
+
fuzzy_search = True if q.get("fuzzy_search") in ("true", "1") else False
|
|
289
|
+
if q.get("url"):
|
|
290
|
+
url = q.get("url")
|
|
291
|
+
if q.get("path"):
|
|
292
|
+
path = q.get("path")
|
|
293
|
+
if q.get("multiProject"):
|
|
294
|
+
multi_project = q.get("multiProject", "").lower() in ("true", "1")
|
|
295
|
+
if q.get("deep"):
|
|
296
|
+
deep = q.get("deep", "").lower() in ("true", "1")
|
|
297
|
+
if q.get("type"):
|
|
298
|
+
project_type = q.get("type")
|
|
299
|
+
if q.get("profile"):
|
|
300
|
+
profile = q.get("profile")
|
|
301
|
+
if params is not None:
|
|
302
|
+
if not url and params.get("url"):
|
|
303
|
+
url = params.get("url")
|
|
304
|
+
if not path and params.get("path"):
|
|
305
|
+
path = params.get("path")
|
|
306
|
+
if not multi_project and params.get("multiProject"):
|
|
307
|
+
multi_project = params.get("multiProject", "").lower() in (
|
|
308
|
+
"true",
|
|
309
|
+
"1",
|
|
310
|
+
)
|
|
311
|
+
if not deep and params.get("deep"):
|
|
312
|
+
deep = params.get("deep", "").lower() in (
|
|
313
|
+
"true",
|
|
314
|
+
"1",
|
|
315
|
+
)
|
|
316
|
+
if not project_type and params.get("type"):
|
|
317
|
+
project_type = params.get("type")
|
|
318
|
+
if not profile and params.get("profile"):
|
|
319
|
+
profile = params.get("profile")
|
|
606
320
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
except Exception as e:
|
|
616
|
-
LOG.info(e)
|
|
321
|
+
if not path and not url and (uploaded_bom_file.get("file", None) is None):
|
|
322
|
+
return {
|
|
323
|
+
"error": "true",
|
|
324
|
+
"message": "path or url or a bom file upload is required",
|
|
325
|
+
}, 400
|
|
326
|
+
if not project_type:
|
|
327
|
+
return {"error": "true", "message": "project type is required"}, 400
|
|
328
|
+
if db_lib.needs_update(days=0, hours=VDB_AGE_HOURS, default_status=False):
|
|
617
329
|
return (
|
|
618
330
|
{
|
|
619
331
|
"error": "true",
|
|
620
|
-
"message": "
|
|
332
|
+
"message": "Vulnerability database is empty. Prepare the "
|
|
333
|
+
"vulnerability database by invoking /download-vdb endpoint "
|
|
334
|
+
"before running scans.",
|
|
621
335
|
},
|
|
622
|
-
|
|
336
|
+
500,
|
|
623
337
|
{"Content-Type": "application/json"},
|
|
624
338
|
)
|
|
625
339
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
tmp_bom_file = tempfile.NamedTemporaryFile(
|
|
629
|
-
delete=False, suffix=f".bom.{bom_file_suffix}"
|
|
630
|
-
)
|
|
631
|
-
with open(tmp_bom_file.name, "w", encoding="utf-8") as f:
|
|
632
|
-
f.write(bom_file_content)
|
|
633
|
-
path = tmp_bom_file.name
|
|
634
|
-
|
|
635
|
-
# Path points to a project directory
|
|
636
|
-
# Bug# 233. Path could be a url
|
|
637
|
-
if url or (path and os.path.isdir(path)):
|
|
638
|
-
with tempfile.NamedTemporaryFile(
|
|
639
|
-
delete=False, suffix=".bom.json"
|
|
640
|
-
) as bfp:
|
|
641
|
-
bom_status = create_bom(
|
|
642
|
-
project_type,
|
|
643
|
-
bfp.name,
|
|
644
|
-
path,
|
|
645
|
-
deep,
|
|
646
|
-
{
|
|
647
|
-
"url": url,
|
|
648
|
-
"path": path,
|
|
649
|
-
"type": project_type,
|
|
650
|
-
"multiProject": multi_project,
|
|
651
|
-
"cdxgen_server": cdxgen_server,
|
|
652
|
-
"profile": profile,
|
|
653
|
-
},
|
|
654
|
-
)
|
|
655
|
-
if bom_status:
|
|
656
|
-
LOG.debug("BOM file was generated successfully at %s", bfp.name)
|
|
657
|
-
bom_file_path = bfp.name
|
|
340
|
+
cdxgen_server = app.config.get("CDXGEN_SERVER_URL")
|
|
341
|
+
bom_file_path = None
|
|
658
342
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
343
|
+
if uploaded_bom_file.get("file", None) is not None:
|
|
344
|
+
bom_file = uploaded_bom_file["file"]
|
|
345
|
+
bom_file_content = bom_file.read().decode("utf-8")
|
|
346
|
+
try:
|
|
347
|
+
_ = json.loads(bom_file_content)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
LOG.info(e)
|
|
350
|
+
return (
|
|
351
|
+
{
|
|
352
|
+
"error": "true",
|
|
353
|
+
"message": "The uploaded file must be a valid JSON or XML.",
|
|
354
|
+
},
|
|
355
|
+
400,
|
|
356
|
+
{"Content-Type": "application/json"},
|
|
357
|
+
)
|
|
663
358
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if project_type in type_audit_map:
|
|
669
|
-
audit_results = audit(project_type, pkg_list)
|
|
670
|
-
if audit_results:
|
|
671
|
-
results = results + audit_results
|
|
672
|
-
vdb_results, pkg_aliases, sug_version_dict, purl_aliases = scan(
|
|
673
|
-
db, project_type, pkg_list, True
|
|
674
|
-
)
|
|
675
|
-
if vdb_results:
|
|
676
|
-
results += vdb_results
|
|
677
|
-
results = [r.to_dict() for r in results]
|
|
678
|
-
bom_data = None
|
|
679
|
-
with open(bom_file_path, encoding="utf-8") as fp:
|
|
680
|
-
bom_data = json.load(fp)
|
|
681
|
-
if not bom_data:
|
|
682
|
-
return (
|
|
683
|
-
{
|
|
684
|
-
"error": "true",
|
|
685
|
-
"message": "Unable to generate SBOM. Check your input path or url.",
|
|
686
|
-
},
|
|
687
|
-
400,
|
|
688
|
-
{"Content-Type": "application/json"},
|
|
359
|
+
LOG.debug("Processing uploaded file")
|
|
360
|
+
bom_file_suffix = str(bom_file.filename).rsplit(".", maxsplit=1)[-1]
|
|
361
|
+
tmp_bom_file = tempfile.NamedTemporaryFile(
|
|
362
|
+
delete=False, suffix=f".bom.{bom_file_suffix}"
|
|
689
363
|
)
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
364
|
+
path = tmp_bom_file.name
|
|
365
|
+
file_write(path, bom_file_content)
|
|
366
|
+
|
|
367
|
+
# Path points to a project directory
|
|
368
|
+
# Bug# 233. Path could be a url
|
|
369
|
+
if url or (path and os.path.isdir(path)):
|
|
370
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".bom.json") as bfp:
|
|
371
|
+
project_type_list = project_type.split(",")
|
|
372
|
+
bom_status = create_bom(
|
|
373
|
+
bfp.name,
|
|
374
|
+
path,
|
|
375
|
+
{
|
|
376
|
+
"url": url,
|
|
377
|
+
"path": path,
|
|
378
|
+
"project_type": project_type_list,
|
|
379
|
+
"multiProject": multi_project,
|
|
380
|
+
"cdxgen_server": cdxgen_server,
|
|
381
|
+
"profile": profile,
|
|
382
|
+
"deep": deep,
|
|
383
|
+
},
|
|
384
|
+
)
|
|
385
|
+
if bom_status:
|
|
386
|
+
LOG.debug("BOM file was generated successfully at %s", bfp.name)
|
|
387
|
+
bom_file_path = bfp.name
|
|
706
388
|
|
|
707
|
-
|
|
389
|
+
# Path points to a SBOM file
|
|
390
|
+
else:
|
|
391
|
+
if os.path.exists(path):
|
|
392
|
+
bom_file_path = path
|
|
393
|
+
# Direct purl-based lookups are not supported yet.
|
|
394
|
+
if bom_file_path is not None:
|
|
395
|
+
pkg_list, _ = get_pkg_list(bom_file_path)
|
|
396
|
+
# Here we are assuming there will be only one type
|
|
397
|
+
if project_type in type_audit_map:
|
|
398
|
+
audit_results = audit(project_type, pkg_list)
|
|
399
|
+
if audit_results:
|
|
400
|
+
results = results + audit_results
|
|
401
|
+
if not pkg_list:
|
|
402
|
+
LOG.debug("Empty package search attempted!")
|
|
403
|
+
else:
|
|
404
|
+
LOG.debug("Scanning %d oss dependencies for issues", len(pkg_list))
|
|
405
|
+
bom_data = json_load(bom_file_path)
|
|
406
|
+
if not bom_data:
|
|
407
|
+
return (
|
|
408
|
+
{
|
|
409
|
+
"error": "true",
|
|
410
|
+
"message": "Unable to generate SBOM. Check your input path or url.",
|
|
411
|
+
},
|
|
412
|
+
400,
|
|
413
|
+
{"Content-Type": "application/json"},
|
|
414
|
+
)
|
|
415
|
+
options = VdrAnalysisKV(
|
|
416
|
+
project_type,
|
|
417
|
+
results,
|
|
418
|
+
pkg_aliases={},
|
|
419
|
+
purl_aliases={},
|
|
420
|
+
suggest_mode=suggest_mode,
|
|
421
|
+
scoped_pkgs={},
|
|
422
|
+
no_vuln_table=True,
|
|
423
|
+
bom_file=bom_file_path,
|
|
424
|
+
pkg_list=[],
|
|
425
|
+
direct_purls={},
|
|
426
|
+
reached_purls={},
|
|
427
|
+
console=console,
|
|
428
|
+
logger=LOG,
|
|
429
|
+
fuzzy_search=fuzzy_search,
|
|
430
|
+
)
|
|
431
|
+
vdr_result = VDRAnalyzer(vdr_options=options).process()
|
|
432
|
+
if vdr_result.success:
|
|
433
|
+
pkg_vulnerabilities = vdr_result.pkg_vulnerabilities
|
|
434
|
+
if pkg_vulnerabilities:
|
|
435
|
+
bom_data["vulnerabilities"] = pkg_vulnerabilities
|
|
436
|
+
return json.dumps(bom_data), 200, {"Content-Type": "application/json"}
|
|
708
437
|
return (
|
|
709
438
|
{
|
|
710
439
|
"error": "true",
|
|
@@ -714,48 +443,50 @@ async def run_scan():
|
|
|
714
443
|
{"Content-Type": "application/json"},
|
|
715
444
|
)
|
|
716
445
|
|
|
446
|
+
def run_server(args):
|
|
447
|
+
"""
|
|
448
|
+
Run depscan as server
|
|
717
449
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
port=args.server_port,
|
|
732
|
-
debug=os.getenv("SCAN_DEBUG_MODE") == "debug"
|
|
733
|
-
or os.getenv("AT_DEBUG_MODE") == "debug",
|
|
734
|
-
use_reloader=False,
|
|
735
|
-
)
|
|
450
|
+
:param args: Command line arguments passed to the function.
|
|
451
|
+
"""
|
|
452
|
+
print(LOGO)
|
|
453
|
+
console.print(
|
|
454
|
+
f"Depscan server running on {args.server_host}:{args.server_port}"
|
|
455
|
+
)
|
|
456
|
+
app.config["CDXGEN_SERVER_URL"] = args.cdxgen_server
|
|
457
|
+
app.run(
|
|
458
|
+
host=args.server_host,
|
|
459
|
+
port=args.server_port,
|
|
460
|
+
debug=os.getenv("SCAN_DEBUG_MODE") == "debug",
|
|
461
|
+
use_reloader=False,
|
|
462
|
+
)
|
|
736
463
|
|
|
737
464
|
|
|
738
|
-
def
|
|
465
|
+
def run_depscan(args):
|
|
739
466
|
"""
|
|
740
467
|
Detects the project type, performs various scans and audits,
|
|
741
468
|
and generates reports based on the results.
|
|
742
469
|
"""
|
|
743
|
-
args = build_args()
|
|
744
470
|
perform_risk_audit = args.risk_audit
|
|
745
471
|
# declare variables that get initialized only conditionally
|
|
746
472
|
(
|
|
747
473
|
summary,
|
|
748
474
|
vdr_file,
|
|
749
475
|
bom_file,
|
|
476
|
+
prebuild_bom_file,
|
|
477
|
+
build_bom_file,
|
|
478
|
+
postbuild_bom_file,
|
|
479
|
+
container_bom_file,
|
|
480
|
+
operations_bom_file,
|
|
750
481
|
pkg_list,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
) = (None, None, None, None, None, None)
|
|
482
|
+
all_pkg_vulnerabilities,
|
|
483
|
+
all_pkg_group_rows,
|
|
484
|
+
) = (None, None, None, None, None, None, None, None, None, [], {})
|
|
754
485
|
if (
|
|
755
486
|
os.getenv("CI")
|
|
487
|
+
and not os.getenv("GITHUB_REPOSITORY", "").lower().startswith("owasp")
|
|
756
488
|
and not args.no_banner
|
|
757
|
-
and not os.getenv("INPUT_THANK_YOU", "")
|
|
758
|
-
== ("I have sponsored OWASP-dep-scan.")
|
|
489
|
+
and not os.getenv("INPUT_THANK_YOU", "") == "I have sponsored OWASP-dep-scan."
|
|
759
490
|
):
|
|
760
491
|
console.print(
|
|
761
492
|
Panel(
|
|
@@ -764,14 +495,49 @@ def main():
|
|
|
764
495
|
expand=False,
|
|
765
496
|
)
|
|
766
497
|
)
|
|
767
|
-
# Should we
|
|
498
|
+
# Should we be quiet
|
|
499
|
+
if args.quiet:
|
|
500
|
+
args.explain = False
|
|
501
|
+
LOG.disabled = True
|
|
502
|
+
args.enable_debug = False
|
|
503
|
+
os.environ["SCAN_DEBUG_MODE"] = "off"
|
|
504
|
+
os.environ["CDXGEN_DEBUG_MODE"] = "off"
|
|
505
|
+
console.quiet = True
|
|
506
|
+
args.no_vuln_table = True
|
|
507
|
+
# Should we enable debug
|
|
768
508
|
if args.enable_debug:
|
|
769
|
-
os.environ["
|
|
509
|
+
os.environ["SCAN_DEBUG_MODE"] = "debug"
|
|
510
|
+
os.environ["CDXGEN_DEBUG_MODE"] = "debug"
|
|
770
511
|
LOG.setLevel(DEBUG)
|
|
771
512
|
if args.server_mode:
|
|
772
|
-
|
|
513
|
+
if QUART_AVAILABLE:
|
|
514
|
+
return run_server(args)
|
|
515
|
+
else:
|
|
516
|
+
LOG.info(
|
|
517
|
+
"The required packages for server mode are unavailable. Reinstall depscan using `pip install owasp-depscan[all]`."
|
|
518
|
+
)
|
|
519
|
+
return False
|
|
773
520
|
if not args.no_banner:
|
|
774
|
-
|
|
521
|
+
with contextlib.suppress(UnicodeEncodeError):
|
|
522
|
+
print(LOGO)
|
|
523
|
+
# Break early if the user prefers CPE-based searches
|
|
524
|
+
search_order = args.search_order
|
|
525
|
+
if search_order:
|
|
526
|
+
if search_order.startswith("c") and not args.bom and not args.bom_dir:
|
|
527
|
+
LOG.warning(
|
|
528
|
+
"To perform CPE-based searches, the SBOM must include a CPE identifier for each component. Generate the SBOM using a compatible tool such as Syft or Trivy, and invoke depscan with the --bom or --bom-dir argument."
|
|
529
|
+
)
|
|
530
|
+
LOG.info(
|
|
531
|
+
"Alternatively, run depscan without the `--search-order` argument to perform PURL-based searches. This method is more accurate and recommended."
|
|
532
|
+
)
|
|
533
|
+
sys.exit(1)
|
|
534
|
+
elif search_order.startswith("u") and not os.getenv("FETCH_LICENSE"):
|
|
535
|
+
LOG.warning(
|
|
536
|
+
"To perform URL-based searches, the SBOM must include externalReferences with a URL. Set the environment variable `FETCH_LICENSE=true` to force cdxgen to populate this attribute."
|
|
537
|
+
)
|
|
538
|
+
LOG.info(
|
|
539
|
+
"Alternatively, include the project type `-t license` to ensure this attribute is populated."
|
|
540
|
+
)
|
|
775
541
|
src_dir = args.src_dir_image
|
|
776
542
|
if not src_dir or src_dir == ".":
|
|
777
543
|
if src_dir == "." or args.search_purl:
|
|
@@ -779,9 +545,51 @@ def main():
|
|
|
779
545
|
# Try to infer from the bom file
|
|
780
546
|
elif args.bom and os.path.exists(args.bom):
|
|
781
547
|
src_dir = os.path.dirname(os.path.realpath(args.bom))
|
|
548
|
+
elif args.bom_dir and os.path.exists(args.bom_dir):
|
|
549
|
+
src_dir = os.path.realpath(args.bom_dir)
|
|
782
550
|
else:
|
|
783
551
|
src_dir = os.getcwd()
|
|
784
552
|
reports_dir = args.reports_dir
|
|
553
|
+
# User has not provided an explicit reports_dir. Reuse the bom_dir
|
|
554
|
+
if not reports_dir and args.bom_dir:
|
|
555
|
+
reports_dir = os.path.realpath(args.bom_dir)
|
|
556
|
+
# Are we running for a BOM directory
|
|
557
|
+
bom_dir_mode = args.bom_dir and os.path.exists(args.bom_dir)
|
|
558
|
+
# Are we running with a config file
|
|
559
|
+
config_file_mode = args.config and os.path.exists(args.config)
|
|
560
|
+
depscan_options = {**vars(args)}
|
|
561
|
+
depscan_options["src_dir"] = src_dir
|
|
562
|
+
depscan_options["reports_dir"] = reports_dir
|
|
563
|
+
# Is the user looking for semantic analysis?
|
|
564
|
+
# We can default to this when run against a BOM directory
|
|
565
|
+
if (
|
|
566
|
+
args.reachability_analyzer == "SemanticReachability"
|
|
567
|
+
) and args.vuln_analyzer != "LifecycleAnalyzer":
|
|
568
|
+
LOG.debug(
|
|
569
|
+
"Automatically switching to the `LifecycleAnalyzer` for vulnerability analysis."
|
|
570
|
+
)
|
|
571
|
+
depscan_options["vuln_analyzer"] = "LifecycleAnalyzer"
|
|
572
|
+
args.vuln_analyzer = "LifecycleAnalyzer"
|
|
573
|
+
# Should we download the latest vdb.
|
|
574
|
+
if db_lib.needs_update(
|
|
575
|
+
days=0,
|
|
576
|
+
hours=VDB_AGE_HOURS,
|
|
577
|
+
default_status=db_lib.get_db_file_metadata is not None,
|
|
578
|
+
):
|
|
579
|
+
if ORAS_AVAILABLE:
|
|
580
|
+
with console.status(
|
|
581
|
+
f"Downloading the latest vulnerability database to {config.DATA_DIR}. Please wait ...",
|
|
582
|
+
spinner=SPINNER,
|
|
583
|
+
) as vdb_download_status:
|
|
584
|
+
if not IS_CI:
|
|
585
|
+
vdb_download_status.stop()
|
|
586
|
+
# This line may exit with an exception if the database cannot be downloaded.
|
|
587
|
+
# Example: urllib3.exceptions.IncompleteRead, urllib3.exceptions.ProtocolError, requests.exceptions.ChunkedEncodingError
|
|
588
|
+
download_image(vdb_database_url, config.DATA_DIR)
|
|
589
|
+
else:
|
|
590
|
+
LOG.warning(
|
|
591
|
+
"The latest vulnerability database is not found. Follow the documentation to manually download it."
|
|
592
|
+
)
|
|
785
593
|
if args.csaf:
|
|
786
594
|
toml_file_path = os.getenv(
|
|
787
595
|
"DEPSCAN_CSAF_TEMPLATE", os.path.join(src_dir, "csaf.toml")
|
|
@@ -789,9 +597,7 @@ def main():
|
|
|
789
597
|
if not os.path.exists(toml_file_path):
|
|
790
598
|
LOG.info("CSAF toml not found, creating template in %s", src_dir)
|
|
791
599
|
write_toml(toml_file_path)
|
|
792
|
-
LOG.info(
|
|
793
|
-
"Please fill out the toml with your details and rerun depscan."
|
|
794
|
-
)
|
|
600
|
+
LOG.info("Please fill out the toml with your details and rerun depscan.")
|
|
795
601
|
LOG.info(
|
|
796
602
|
"Check out our CSAF documentation for an explanation of "
|
|
797
603
|
"this feature. https://github.com/owasp-dep-scan/dep-scan"
|
|
@@ -803,32 +609,24 @@ def main():
|
|
|
803
609
|
"depscan."
|
|
804
610
|
)
|
|
805
611
|
sys.exit(0)
|
|
806
|
-
|
|
807
|
-
if args.
|
|
808
|
-
project_types_list = args.project_type.split(",")
|
|
809
|
-
elif args.search_purl:
|
|
612
|
+
pkg_list, project_types_list = set_project_types(args, src_dir)
|
|
613
|
+
if args.search_purl:
|
|
810
614
|
# Automatically enable risk audit for single purl searches
|
|
811
615
|
perform_risk_audit = True
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
project_types_list = ["bom"]
|
|
819
|
-
elif not args.non_universal_scan:
|
|
820
|
-
project_types_list = [UNIVERSAL_SCAN_TYPE]
|
|
821
|
-
else:
|
|
822
|
-
project_types_list = utils.detect_project_type(src_dir)
|
|
823
|
-
db = db_lib.get()
|
|
824
|
-
run_cacher = args.cache
|
|
825
|
-
areport_file = (
|
|
826
|
-
args.report_file
|
|
827
|
-
if args.report_file
|
|
828
|
-
else os.path.join(reports_dir, "depscan.json")
|
|
616
|
+
# Construct the various report files
|
|
617
|
+
html_report_file = depscan_options.get(
|
|
618
|
+
"html_report_file", os.path.join(reports_dir, "depscan.html")
|
|
619
|
+
)
|
|
620
|
+
pdf_report_file = depscan_options.get(
|
|
621
|
+
"pdf_report_file", os.path.join(reports_dir, "depscan.pdf")
|
|
829
622
|
)
|
|
830
|
-
|
|
831
|
-
|
|
623
|
+
txt_report_file = depscan_options.get(
|
|
624
|
+
"txt_report_file", os.path.join(reports_dir, "depscan.txt")
|
|
625
|
+
)
|
|
626
|
+
run_config_file = os.path.join(reports_dir, "depscan.toml.sample")
|
|
627
|
+
depscan_options["html_report_file"] = html_report_file
|
|
628
|
+
depscan_options["pdf_report_file"] = pdf_report_file
|
|
629
|
+
depscan_options["txt_report_file"] = txt_report_file
|
|
832
630
|
# Create reports directory
|
|
833
631
|
if reports_dir and not os.path.exists(reports_dir):
|
|
834
632
|
os.makedirs(reports_dir, exist_ok=True)
|
|
@@ -846,41 +644,126 @@ def main():
|
|
|
846
644
|
expand=False,
|
|
847
645
|
)
|
|
848
646
|
)
|
|
647
|
+
# Let’s create a sample configuration file based on the CLI options used.
|
|
648
|
+
if not config_file_mode:
|
|
649
|
+
run_config = {**depscan_options}
|
|
650
|
+
del run_config["no_banner"]
|
|
651
|
+
write_toml(run_config_file, run_config, write_version=False)
|
|
652
|
+
LOG.debug(
|
|
653
|
+
f"Created a sample depscan config file at '{run_config_file}', based on this run."
|
|
654
|
+
)
|
|
655
|
+
# We have everything needed to start the composition analysis. There are many approaches to implementing an SCA tool.
|
|
656
|
+
# Our style of analysis is comparable to that of an intelligent Hubble telescope or a rover—examining the same subject through multiple optics, colors, and depths to gain a deeper understanding.
|
|
657
|
+
# We begin by iterating over the project types provided or assumed.
|
|
849
658
|
for project_type in project_types_list:
|
|
850
659
|
results = []
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
660
|
+
vuln_analyzer = args.vuln_analyzer
|
|
661
|
+
# Are we performing a lifecycle analysis
|
|
662
|
+
if not args.search_purl and (
|
|
663
|
+
vuln_analyzer == "LifecycleAnalyzer"
|
|
664
|
+
or (vuln_analyzer == "auto" and bom_dir_mode)
|
|
665
|
+
):
|
|
666
|
+
if args.reachability_analyzer == "SemanticReachability":
|
|
667
|
+
if not args.bom_dir:
|
|
668
|
+
LOG.info(
|
|
669
|
+
"Semantic Reachability analysis requested for project type '%s'. This might take a while ...",
|
|
670
|
+
project_type,
|
|
671
|
+
)
|
|
672
|
+
else:
|
|
673
|
+
LOG.info(
|
|
674
|
+
"Attempting semantic analysis using existing data at '%s'",
|
|
675
|
+
args.bom_dir,
|
|
676
|
+
)
|
|
677
|
+
else:
|
|
678
|
+
LOG.info(
|
|
679
|
+
"Lifecycle-based vulnerability analysis requested for project type '%s'. This might take a while ...",
|
|
680
|
+
project_type,
|
|
681
|
+
)
|
|
682
|
+
prebuild_bom_file = os.path.join(
|
|
683
|
+
reports_dir, f"sbom-prebuild-{project_type}.cdx.json"
|
|
684
|
+
)
|
|
685
|
+
build_bom_file = os.path.join(
|
|
686
|
+
reports_dir, f"sbom-build-{project_type}.cdx.json"
|
|
687
|
+
)
|
|
688
|
+
postbuild_bom_file = os.path.join(
|
|
689
|
+
reports_dir, f"sbom-postbuild-{project_type}.cdx.json"
|
|
690
|
+
)
|
|
691
|
+
# We support only one container SBOM per project.
|
|
692
|
+
# Projects that rely on docker compose and multiple services require some thinking
|
|
693
|
+
container_bom_file = os.path.join(
|
|
694
|
+
reports_dir, f"sbom-container-{project_type}.cdx.json"
|
|
695
|
+
)
|
|
696
|
+
operations_bom_file = os.path.join(
|
|
697
|
+
reports_dir, f"sbom-operations-{project_type}.cdx.json"
|
|
698
|
+
)
|
|
699
|
+
if vuln_analyzer == "auto":
|
|
700
|
+
vuln_analyzer = "LifecycleAnalyzer"
|
|
701
|
+
depscan_options["vuln_analyzer"] = "LifecycleAnalyzer"
|
|
702
|
+
# We need to set the following two values to make the rest of the code correctly use
|
|
703
|
+
# the generated BOM files after lifecycle analysis
|
|
704
|
+
depscan_options["lifecycle_analysis_mode"] = True
|
|
705
|
+
if not args.bom_dir:
|
|
706
|
+
args.bom_dir = os.path.realpath(reports_dir)
|
|
707
|
+
# If the user opts out of lifecycle analysis, we need to maintain multiple SBOMs based on the project type.
|
|
708
|
+
bom_file = os.path.join(reports_dir, f"sbom-{project_type}.cdx.json")
|
|
709
|
+
risk_report_file = os.path.join(
|
|
710
|
+
reports_dir, f"depscan-risk-{project_type}.json"
|
|
854
711
|
)
|
|
855
712
|
# Are we scanning a single purl
|
|
856
713
|
if args.search_purl:
|
|
857
714
|
bom_file = None
|
|
858
715
|
creation_status = True
|
|
859
716
|
# Are we scanning a bom file
|
|
717
|
+
###################
|
|
718
|
+
# Note to students and researchers benchmarking depscan:
|
|
719
|
+
# we’ve seen attempts to run depscan using SBOMs generated by tools like Syft, Trivy, etc.
|
|
720
|
+
# It’s important to understand that not all SBOMs contain the same level of detail.
|
|
721
|
+
# Component PURLs can differ slightly, especially in qualifiers.
|
|
722
|
+
#
|
|
723
|
+
# For container SBOMs, qualifiers like distro_name and distro_version are critical for accurate results.
|
|
724
|
+
# Tools like Syft and Trivy often include internal metadata—such as vendor IDs or fabricated CPE strings—to brute-force vulnerability matches.
|
|
725
|
+
# Because of these inconsistencies, it’s not possible to achieve identical results with depscan when using a non-cdxgen or non-blint SBOM.
|
|
726
|
+
# If in doubt, speak to us before benchmarking depscan. Don’t run depscan with default settings and expect magic.
|
|
727
|
+
# SCA and xBOM are complex domains that require understanding, configuration, and continuous learning.
|
|
728
|
+
###################
|
|
860
729
|
elif args.bom and os.path.exists(args.bom):
|
|
861
730
|
bom_file = args.bom
|
|
862
731
|
creation_status = True
|
|
732
|
+
# Are we scanning a bom directory
|
|
733
|
+
elif bom_dir_mode:
|
|
734
|
+
bom_file = None
|
|
735
|
+
creation_status = True
|
|
863
736
|
else:
|
|
864
|
-
|
|
865
|
-
# The bom file has to be called bom.json for atom reachables to work :(
|
|
866
|
-
bom_file = os.path.join(src_dir, "bom.json")
|
|
867
|
-
else:
|
|
868
|
-
bom_file = report_file.replace("depscan-", "sbom-")
|
|
737
|
+
# Create a bom for each project type
|
|
869
738
|
creation_status = create_bom(
|
|
870
|
-
project_type,
|
|
871
739
|
bom_file,
|
|
872
740
|
src_dir,
|
|
873
|
-
args.deep_scan,
|
|
874
741
|
{
|
|
875
|
-
|
|
876
|
-
"
|
|
877
|
-
"
|
|
742
|
+
**depscan_options,
|
|
743
|
+
"project_type": [project_type],
|
|
744
|
+
"bom_file": bom_file,
|
|
745
|
+
"prebuild_bom_file": prebuild_bom_file,
|
|
746
|
+
"build_bom_file": build_bom_file,
|
|
747
|
+
"postbuild_bom_file": postbuild_bom_file,
|
|
748
|
+
"container_bom_file": container_bom_file,
|
|
749
|
+
"operations_bom_file": operations_bom_file,
|
|
878
750
|
},
|
|
879
751
|
)
|
|
880
752
|
if not creation_status:
|
|
881
|
-
LOG.
|
|
753
|
+
LOG.warning(
|
|
754
|
+
"The BOM file `%s` was not created successfully. Set the `SCAN_DEBUG_MODE=debug` environment variable to troubleshoot.",
|
|
755
|
+
bom_file,
|
|
756
|
+
)
|
|
882
757
|
continue
|
|
883
|
-
|
|
758
|
+
# We have a BOM directory. Let’s aggregate all packages from every file within it.
|
|
759
|
+
if args.bom_dir:
|
|
760
|
+
LOG.debug(
|
|
761
|
+
"Collecting components from all the BOM files at %s",
|
|
762
|
+
args.bom_dir,
|
|
763
|
+
)
|
|
764
|
+
pkg_list = get_all_pkg_list(args.bom_dir)
|
|
765
|
+
# We are working with a single BOM file and will collect all packages from it accordingly.
|
|
766
|
+
elif bom_file:
|
|
884
767
|
LOG.debug("Scanning using the bom file %s", bom_file)
|
|
885
768
|
if not args.bom:
|
|
886
769
|
LOG.info(
|
|
@@ -888,11 +771,15 @@ def main():
|
|
|
888
771
|
"depscan with --bom %s instead of -i",
|
|
889
772
|
bom_file,
|
|
890
773
|
)
|
|
891
|
-
pkg_list = get_pkg_list(bom_file)
|
|
892
|
-
if not pkg_list:
|
|
893
|
-
LOG.
|
|
774
|
+
pkg_list, _ = get_pkg_list(bom_file)
|
|
775
|
+
if not pkg_list and not args.bom_dir:
|
|
776
|
+
LOG.info(
|
|
777
|
+
"No packages were found in the project. Try generating the BOM manually or use the `CdxgenImageBasedGenerator` engine."
|
|
778
|
+
)
|
|
894
779
|
continue
|
|
895
|
-
|
|
780
|
+
# Depending on the SBOM tool used, there may be details about component usage and scopes. Let’s analyze and interpret that information.
|
|
781
|
+
scoped_pkgs = get_pkgs_by_scope(pkg_list)
|
|
782
|
+
# Is the user interested in seeing license risks? Handle that first before any security-related analysis.
|
|
896
783
|
if (
|
|
897
784
|
os.getenv("FETCH_LICENSE", "") in (True, "1", "true")
|
|
898
785
|
or "license" in args.profile
|
|
@@ -902,50 +789,51 @@ def main():
|
|
|
902
789
|
pkg_list=pkg_list,
|
|
903
790
|
)
|
|
904
791
|
license_report_file = os.path.join(
|
|
905
|
-
reports_dir, "license-
|
|
792
|
+
reports_dir, f"license-{project_type}.json"
|
|
906
793
|
)
|
|
907
|
-
|
|
794
|
+
ltable = licenses_risk_table(
|
|
908
795
|
project_type, licenses_results, license_report_file
|
|
909
796
|
)
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
f"Performing OSS Risk Audit for packages from "
|
|
916
|
-
f"{src_dir}\nNo of packages [bold]{len(pkg_list)}"
|
|
917
|
-
f"[/bold]. This will take a while ...",
|
|
918
|
-
title="OSS Risk Audit",
|
|
919
|
-
expand=False,
|
|
920
|
-
)
|
|
921
|
-
)
|
|
922
|
-
try:
|
|
923
|
-
risk_results = risk_audit(
|
|
924
|
-
project_type,
|
|
925
|
-
scoped_pkgs,
|
|
926
|
-
args.private_ns,
|
|
927
|
-
pkg_list,
|
|
928
|
-
)
|
|
929
|
-
analyse_pkg_risks(
|
|
930
|
-
project_type,
|
|
931
|
-
scoped_pkgs,
|
|
932
|
-
risk_results,
|
|
933
|
-
risk_report_file,
|
|
934
|
-
)
|
|
935
|
-
except Exception as e:
|
|
936
|
-
LOG.error(e)
|
|
937
|
-
LOG.error("Risk audit was not successful")
|
|
938
|
-
else:
|
|
797
|
+
if ltable and not args.no_vuln_table:
|
|
798
|
+
console.print(ltable)
|
|
799
|
+
# Do we support OSS risk audit for this type? If yes, proceed with the relevant checks.
|
|
800
|
+
if perform_risk_audit and project_type in risk_audit_map:
|
|
801
|
+
if len(pkg_list) > 1:
|
|
939
802
|
console.print(
|
|
940
803
|
Panel(
|
|
941
|
-
"
|
|
942
|
-
"
|
|
943
|
-
"
|
|
944
|
-
title="Risk Audit
|
|
804
|
+
f"Performing OSS Risk Audit for packages from "
|
|
805
|
+
f"{src_dir}\nNo of packages [bold]{len(pkg_list)}"
|
|
806
|
+
f"[/bold]. This will take a while ...",
|
|
807
|
+
title="OSS Risk Audit",
|
|
945
808
|
expand=False,
|
|
946
809
|
)
|
|
947
810
|
)
|
|
948
|
-
|
|
811
|
+
try:
|
|
812
|
+
risk_results = risk_audit(
|
|
813
|
+
project_type,
|
|
814
|
+
scoped_pkgs,
|
|
815
|
+
args.private_ns,
|
|
816
|
+
pkg_list,
|
|
817
|
+
)
|
|
818
|
+
rtable, report_data = pkg_risks_table(
|
|
819
|
+
project_type,
|
|
820
|
+
scoped_pkgs,
|
|
821
|
+
risk_results,
|
|
822
|
+
pkg_max_risk_score=pkg_max_risk_score,
|
|
823
|
+
risk_report_file=risk_report_file,
|
|
824
|
+
)
|
|
825
|
+
if not args.no_vuln_table and report_data and rtable:
|
|
826
|
+
console.print(rtable)
|
|
827
|
+
except Exception as e:
|
|
828
|
+
LOG.error(e)
|
|
829
|
+
LOG.error("Risk audit was not successful")
|
|
830
|
+
# Do we support remote audit for this type?
|
|
831
|
+
# Remote audits can improve results for some project types like npm by fetching vulnerabilities that might not yet be in our database.
|
|
832
|
+
# In v6, remote audit is disabled by default and gets enabled with risk audit
|
|
833
|
+
#
|
|
834
|
+
# NOTE: Enabling risk audit may lead to some precision loss in reachability results.
|
|
835
|
+
# This is a known limitation with no immediate plan for resolution.
|
|
836
|
+
if perform_risk_audit and project_type in type_audit_map:
|
|
949
837
|
LOG.debug(
|
|
950
838
|
"Performing remote audit for %s of type %s",
|
|
951
839
|
src_dir,
|
|
@@ -955,9 +843,7 @@ def main():
|
|
|
955
843
|
try:
|
|
956
844
|
audit_results = audit(project_type, pkg_list)
|
|
957
845
|
if audit_results:
|
|
958
|
-
LOG.debug(
|
|
959
|
-
"Remote audit yielded %d results", len(audit_results)
|
|
960
|
-
)
|
|
846
|
+
LOG.debug("Remote audit yielded %d results", len(audit_results))
|
|
961
847
|
results = results + audit_results
|
|
962
848
|
except Exception as e:
|
|
963
849
|
LOG.error("Remote audit was not successful")
|
|
@@ -965,7 +851,7 @@ def main():
|
|
|
965
851
|
results = []
|
|
966
852
|
# In case of docker, bom, or universal type, check if there are any
|
|
967
853
|
# npm packages that can be audited remotely
|
|
968
|
-
if project_type in (
|
|
854
|
+
if perform_risk_audit and project_type in (
|
|
969
855
|
"podman",
|
|
970
856
|
"docker",
|
|
971
857
|
"oci",
|
|
@@ -987,129 +873,135 @@ def main():
|
|
|
987
873
|
except Exception as e:
|
|
988
874
|
LOG.error("Remote audit was not successful")
|
|
989
875
|
LOG.error(e)
|
|
990
|
-
if not db_lib.index_count(db["index_file"]):
|
|
991
|
-
run_cacher = True
|
|
992
876
|
else:
|
|
993
|
-
LOG.debug(
|
|
994
|
-
"Vulnerability database loaded from %s", config.vdb_bin_file
|
|
995
|
-
)
|
|
996
|
-
|
|
997
|
-
sources_list = [OSVSource(), NvdSource()]
|
|
998
|
-
github_token = os.environ.get("GITHUB_TOKEN")
|
|
999
|
-
if github_token and os.getenv("CI"):
|
|
1000
|
-
try:
|
|
1001
|
-
github_client = github.GitHub(github_token)
|
|
1002
|
-
|
|
1003
|
-
if not github_client.can_authenticate():
|
|
1004
|
-
LOG.info(
|
|
1005
|
-
"The GitHub personal access token supplied appears to be invalid or expired. Please see: https://github.com/owasp-dep-scan/dep-scan#github-security-advisory"
|
|
1006
|
-
)
|
|
1007
|
-
else:
|
|
1008
|
-
sources_list.insert(0, GitHubSource())
|
|
1009
|
-
scopes = github_client.get_token_scopes()
|
|
1010
|
-
if scopes:
|
|
1011
|
-
LOG.warning(
|
|
1012
|
-
"The GitHub personal access token was granted more permissions than is necessary for depscan to operate, including the scopes of: %s. It is recommended to use a dedicated token with only the minimum scope necesary for depscan to operate. Please see: https://github.com/owasp-dep-scan/dep-scan#github-security-advisory",
|
|
1013
|
-
", ".join(scopes),
|
|
1014
|
-
)
|
|
1015
|
-
except Exception:
|
|
1016
|
-
pass
|
|
1017
|
-
if run_cacher:
|
|
1018
|
-
paths_list = download_image()
|
|
1019
|
-
LOG.debug("VDB data is stored at: %s", paths_list)
|
|
1020
|
-
run_cacher = False
|
|
1021
|
-
db = db_lib.get()
|
|
1022
|
-
elif args.sync:
|
|
1023
|
-
for s in sources_list:
|
|
1024
|
-
LOG.debug("Syncing %s", s.__class__.__name__)
|
|
1025
|
-
try:
|
|
1026
|
-
s.download_recent()
|
|
1027
|
-
except NotImplementedError:
|
|
1028
|
-
pass
|
|
1029
|
-
run_cacher = False
|
|
877
|
+
LOG.debug("Vulnerability database loaded from %s", config.VDB_BIN_FILE)
|
|
1030
878
|
if len(pkg_list) > 1:
|
|
1031
|
-
|
|
1032
|
-
"
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
879
|
+
if project_type == "bom":
|
|
880
|
+
LOG.info("Scanning CycloneDX xBOMs and atom slices")
|
|
881
|
+
elif args.bom:
|
|
882
|
+
LOG.info(
|
|
883
|
+
"Scanning %s with type %s",
|
|
884
|
+
args.bom,
|
|
885
|
+
project_type,
|
|
886
|
+
)
|
|
887
|
+
else:
|
|
888
|
+
LOG.info(
|
|
889
|
+
"Scanning %s with type %s",
|
|
890
|
+
src_dir,
|
|
891
|
+
project_type,
|
|
892
|
+
)
|
|
893
|
+
# We could be dealing with multiple bom files
|
|
894
|
+
bom_files = (
|
|
895
|
+
get_all_bom_files(args.bom_dir)
|
|
896
|
+
if args.bom_dir
|
|
897
|
+
else [bom_file]
|
|
898
|
+
if bom_file
|
|
899
|
+
else []
|
|
1044
900
|
)
|
|
1045
|
-
|
|
1046
|
-
|
|
901
|
+
if not pkg_list and not bom_files:
|
|
902
|
+
LOG.debug("Empty package search attempted!")
|
|
903
|
+
elif bom_files:
|
|
904
|
+
LOG.debug("Scanning %d bom files for issues", len(bom_files))
|
|
905
|
+
else:
|
|
906
|
+
LOG.debug("Scanning %d oss dependencies for issues", len(pkg_list))
|
|
907
|
+
# There are many ways to perform reachability analysis.
|
|
908
|
+
# Most tools—including commercial ones—rely on a vulnerability database with affected modules (sinks) to detect reachable flows.
|
|
909
|
+
# This has several downsides:
|
|
910
|
+
# 1. These databases are often incomplete and manually maintained.
|
|
911
|
+
# 2. If a CVE or ADP enhancement isn’t available yet, reachability won’t be detected.
|
|
912
|
+
#
|
|
913
|
+
# In contrast, depscan computes reachable flows (via atom) without relying on vulnerability data upfront.
|
|
914
|
+
# It then identifies a smaller subset of those flows that are actually vulnerable.
|
|
915
|
+
# From there, we can further narrow it down to flows that are Endpoint-Reachable, Exploitable, Container-Escapable, etc.
|
|
916
|
+
reachability_analyzer = depscan_options.get("reachability_analyzer")
|
|
917
|
+
reachability_options = None
|
|
918
|
+
if (
|
|
919
|
+
reachability_analyzer and reachability_analyzer != "off"
|
|
920
|
+
) or depscan_options.get("profile") != "generic":
|
|
921
|
+
reachability_options = ReachabilityAnalysisKV(
|
|
922
|
+
project_types=[project_type],
|
|
923
|
+
src_dir=src_dir,
|
|
924
|
+
bom_dir=args.bom_dir or reports_dir,
|
|
925
|
+
require_multi_usage=depscan_options.get("require_multi_usage", False),
|
|
926
|
+
source_tags=depscan_options.get("source_tags"),
|
|
927
|
+
sink_tags=depscan_options.get("sink_tags"),
|
|
928
|
+
)
|
|
929
|
+
# Let’s proceed with the VDR analysis.
|
|
930
|
+
summary, vdr_file, vdr_result = vdr_analyze_summarize(
|
|
1047
931
|
project_type,
|
|
1048
932
|
results,
|
|
1049
|
-
|
|
1050
|
-
purl_aliases,
|
|
1051
|
-
sug_version_dict,
|
|
933
|
+
suggest_mode=args.suggest,
|
|
1052
934
|
scoped_pkgs=scoped_pkgs,
|
|
1053
|
-
|
|
1054
|
-
|
|
935
|
+
bom_file=bom_files[0] if len(bom_files) == 1 else None,
|
|
936
|
+
bom_dir=args.bom_dir,
|
|
937
|
+
pkg_list=pkg_list,
|
|
938
|
+
reachability_analyzer=reachability_analyzer,
|
|
939
|
+
reachability_options=reachability_options,
|
|
1055
940
|
no_vuln_table=args.no_vuln_table,
|
|
1056
|
-
|
|
1057
|
-
|
|
941
|
+
fuzzy_search=depscan_options.get("fuzzy_search", False),
|
|
942
|
+
search_order=depscan_options.get("search_order"),
|
|
1058
943
|
)
|
|
944
|
+
if vdr_result.pkg_vulnerabilities:
|
|
945
|
+
all_pkg_vulnerabilities += vdr_result.pkg_vulnerabilities
|
|
946
|
+
if vdr_result.prioritized_pkg_vuln_trees:
|
|
947
|
+
all_pkg_group_rows.update(vdr_result.prioritized_pkg_vuln_trees)
|
|
1059
948
|
# Explain the results
|
|
1060
949
|
if args.explain:
|
|
1061
950
|
explainer.explain(
|
|
1062
951
|
project_type,
|
|
1063
952
|
src_dir,
|
|
1064
|
-
args.
|
|
953
|
+
args.bom_dir or reports_dir,
|
|
1065
954
|
vdr_file,
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
955
|
+
vdr_result,
|
|
956
|
+
args.explanation_mode,
|
|
957
|
+
)
|
|
958
|
+
else:
|
|
959
|
+
LOG.debug(
|
|
960
|
+
"Pass the `--explain` argument to get a detailed explanation of the analysis."
|
|
1070
961
|
)
|
|
1071
962
|
# CSAF VEX export
|
|
1072
963
|
if args.csaf:
|
|
1073
964
|
export_csaf(
|
|
1074
|
-
|
|
965
|
+
vdr_result,
|
|
1075
966
|
src_dir,
|
|
1076
967
|
reports_dir,
|
|
1077
|
-
|
|
968
|
+
vdr_file,
|
|
1078
969
|
)
|
|
970
|
+
console.record = True
|
|
971
|
+
# Export the console output
|
|
1079
972
|
console.save_html(
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
),
|
|
973
|
+
html_report_file,
|
|
974
|
+
clear=False,
|
|
975
|
+
theme=(MONOKAI if os.getenv("USE_DARK_THEME") else DEFAULT_TERMINAL_THEME),
|
|
1084
976
|
)
|
|
1085
|
-
|
|
977
|
+
console.save_text(txt_report_file, clear=False)
|
|
978
|
+
utils.export_pdf(html_report_file, pdf_report_file)
|
|
979
|
+
# This logic needs refactoring
|
|
1086
980
|
# render report into template if wished
|
|
1087
981
|
if args.report_template and os.path.isfile(args.report_template):
|
|
1088
982
|
utils.render_template_report(
|
|
1089
983
|
vdr_file=vdr_file,
|
|
1090
984
|
bom_file=bom_file,
|
|
1091
|
-
pkg_vulnerabilities=
|
|
1092
|
-
pkg_group_rows=
|
|
985
|
+
pkg_vulnerabilities=all_pkg_vulnerabilities,
|
|
986
|
+
pkg_group_rows=all_pkg_group_rows,
|
|
1093
987
|
summary=summary,
|
|
1094
988
|
template_file=args.report_template,
|
|
1095
989
|
result_file=os.path.join(reports_dir, args.report_name),
|
|
990
|
+
depscan_options=depscan_options,
|
|
1096
991
|
)
|
|
1097
992
|
elif args.report_template:
|
|
1098
993
|
LOG.warning(
|
|
1099
994
|
"Template file %s doesn't exist, custom report not created.",
|
|
1100
995
|
args.report_template,
|
|
1101
996
|
)
|
|
1102
|
-
#
|
|
1103
|
-
if args.
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
"threatdb_token": args.threatdb_token,
|
|
1111
|
-
},
|
|
1112
|
-
)
|
|
997
|
+
# Should we include the generated text report as an annotation in the VDR file?
|
|
998
|
+
if args.explain or args.annotate:
|
|
999
|
+
annotate_vdr(vdr_file, txt_report_file)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def main():
|
|
1003
|
+
cli_args = build_args()
|
|
1004
|
+
run_depscan(cli_args)
|
|
1113
1005
|
|
|
1114
1006
|
|
|
1115
1007
|
if __name__ == "__main__":
|