owasp-depscan 5.4.8__py3-none-any.whl → 6.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of owasp-depscan might be problematic. Click here for more details.
- depscan/__init__.py +8 -0
- depscan/cli.py +719 -827
- depscan/cli_options.py +302 -0
- depscan/lib/audit.py +3 -1
- depscan/lib/bom.py +390 -288
- depscan/lib/config.py +86 -337
- depscan/lib/explainer.py +363 -98
- depscan/lib/license.py +11 -10
- depscan/lib/logger.py +65 -17
- depscan/lib/package_query/__init__.py +0 -0
- depscan/lib/package_query/cargo_pkg.py +124 -0
- depscan/lib/package_query/metadata.py +170 -0
- depscan/lib/package_query/npm_pkg.py +345 -0
- depscan/lib/package_query/pkg_query.py +195 -0
- depscan/lib/package_query/pypi_pkg.py +113 -0
- depscan/lib/tomlparse.py +116 -0
- depscan/lib/utils.py +34 -188
- owasp_depscan-6.0.0a2.dist-info/METADATA +390 -0
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info}/RECORD +28 -25
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info}/WHEEL +1 -1
- vendor/choosealicense.com/_licenses/cern-ohl-p-2.0.txt +1 -1
- vendor/choosealicense.com/_licenses/cern-ohl-s-2.0.txt +1 -1
- vendor/choosealicense.com/_licenses/cern-ohl-w-2.0.txt +2 -2
- vendor/choosealicense.com/_licenses/mit-0.txt +1 -1
- vendor/spdx/json/licenses.json +904 -677
- depscan/lib/analysis.py +0 -1550
- depscan/lib/csaf.py +0 -1860
- depscan/lib/normalize.py +0 -312
- depscan/lib/orasclient.py +0 -142
- depscan/lib/pkg_query.py +0 -532
- owasp_depscan-5.4.8.dist-info/METADATA +0 -580
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info}/entry_points.txt +0 -0
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info/licenses}/LICENSE +0 -0
- {owasp_depscan-5.4.8.dist-info → owasp_depscan-6.0.0a2.dist-info}/top_level.txt +0 -0
depscan/lib/explainer.py
CHANGED
|
@@ -1,69 +1,186 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
1
|
import re
|
|
4
|
-
|
|
2
|
+
import glob
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from custom_json_diff.lib.utils import json_load
|
|
5
5
|
from rich import box
|
|
6
6
|
from rich.markdown import Markdown
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
from rich.tree import Tree
|
|
9
9
|
|
|
10
|
-
from depscan.lib.config import
|
|
11
|
-
|
|
10
|
+
from depscan.lib.config import (
|
|
11
|
+
COMMON_CHECK_TAGS,
|
|
12
|
+
max_purl_per_flow,
|
|
13
|
+
max_reachable_explanations,
|
|
14
|
+
max_purls_reachable_explanations,
|
|
15
|
+
max_source_reachable_explanations,
|
|
16
|
+
max_sink_reachable_explanations,
|
|
17
|
+
)
|
|
18
|
+
from depscan.lib.logger import console, LOG
|
|
12
19
|
|
|
13
20
|
|
|
14
|
-
def explain(
|
|
15
|
-
project_type,
|
|
16
|
-
src_dir,
|
|
17
|
-
reachables_slices_file,
|
|
18
|
-
vdr_file,
|
|
19
|
-
pkg_vulnerabilities,
|
|
20
|
-
pkg_group_rows,
|
|
21
|
-
direct_purls,
|
|
22
|
-
reached_purls,
|
|
23
|
-
):
|
|
21
|
+
def explain(project_type, src_dir, bom_dir, vdr_file, vdr_result, explanation_mode):
|
|
24
22
|
"""
|
|
25
|
-
Explain the analysis and findings
|
|
23
|
+
Explain the analysis and findings based on the explanation mode.
|
|
26
24
|
|
|
27
25
|
:param project_type: Project type
|
|
28
26
|
:param src_dir: Source directory
|
|
29
|
-
:param
|
|
30
|
-
:param vdr_file: VDR file
|
|
31
|
-
:param
|
|
32
|
-
:param
|
|
33
|
-
:param direct_purls: Dict containing packages used directly
|
|
34
|
-
:param reached_purls: Dict containing packages identified via reachables slicing
|
|
27
|
+
:param bom_dir: BOM directory
|
|
28
|
+
:param vdr_file: VDR file
|
|
29
|
+
:param vdr_result: VDR Result
|
|
30
|
+
:param explanation_mode: Explanation mode
|
|
35
31
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
pattern_methods = {}
|
|
33
|
+
has_any_explanation = False
|
|
34
|
+
has_any_crypto_flows = False
|
|
35
|
+
slices_files = glob.glob(f"{bom_dir}/**/*reachables.slices.json", recursive=True)
|
|
36
|
+
openapi_spec_files = None
|
|
37
|
+
# Should we explain the endpoints and Code Hotspots
|
|
38
|
+
if explanation_mode in (
|
|
39
|
+
"Endpoints",
|
|
40
|
+
"EndpointsAndReachables",
|
|
40
41
|
):
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
openapi_spec_files = glob.glob(f"{bom_dir}/*openapi*.json", recursive=False)
|
|
43
|
+
if not openapi_spec_files:
|
|
44
|
+
openapi_spec_files = glob.glob(f"{src_dir}/*openapi*.json", recursive=False)
|
|
45
|
+
if openapi_spec_files:
|
|
46
|
+
rsection = Markdown("""## Service Endpoints
|
|
47
|
+
|
|
48
|
+
The following endpoints and code hotspots were identified by depscan. Verify that proper authentication and authorization mechanisms are in place to secure them.""")
|
|
49
|
+
console.print(rsection)
|
|
50
|
+
for ospec in openapi_spec_files:
|
|
51
|
+
pattern_methods = print_endpoints(ospec)
|
|
52
|
+
# Return early for endpoints only explanations
|
|
53
|
+
if explanation_mode in ("Endpoints",):
|
|
54
|
+
return
|
|
55
|
+
section_title = (
|
|
56
|
+
"Non-Reachable Flows"
|
|
57
|
+
if explanation_mode in ("NonReachables",)
|
|
58
|
+
else "Reachable Flows"
|
|
59
|
+
)
|
|
60
|
+
for sf in slices_files:
|
|
61
|
+
if (reachables_data := json_load(sf, log=LOG)) and reachables_data.get(
|
|
62
|
+
"reachables"
|
|
63
|
+
):
|
|
64
|
+
if explanation_mode in ("NonReachables",):
|
|
46
65
|
rsection = Markdown(
|
|
47
|
-
"""##
|
|
66
|
+
f"""## {section_title}
|
|
48
67
|
|
|
49
|
-
Below are
|
|
68
|
+
Below are several data flows deemed safe and non-reachable. Use the provided tips to confirm this assessment.
|
|
50
69
|
"""
|
|
51
70
|
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
elif pattern_methods:
|
|
72
|
+
rsection = Markdown(
|
|
73
|
+
f"""## {section_title}
|
|
74
|
+
|
|
75
|
+
Below are some reachable flows, including those accessible via endpoints, identified by depscan. Use the generated OpenAPI specification to evaluate these endpoints for vulnerabilities and risk.
|
|
76
|
+
"""
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
rsection = Markdown(
|
|
80
|
+
f"""## {section_title}
|
|
81
|
+
|
|
82
|
+
Below are several data flows identified by depscan, including reachable ones. Use the tips provided to strengthen your application’s security posture.
|
|
83
|
+
"""
|
|
55
84
|
)
|
|
85
|
+
has_explanation, has_crypto_flows, tips = explain_reachables(
|
|
86
|
+
explanation_mode,
|
|
87
|
+
reachables_data,
|
|
88
|
+
project_type,
|
|
89
|
+
vdr_result,
|
|
90
|
+
rsection if not has_any_explanation else None,
|
|
91
|
+
)
|
|
92
|
+
if not has_any_explanation and has_explanation:
|
|
93
|
+
has_any_explanation = True
|
|
94
|
+
if not has_any_crypto_flows and has_crypto_flows:
|
|
95
|
+
has_any_crypto_flows = True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _track_usage_targets(usage_targets, usages_object):
|
|
99
|
+
for k, v in usages_object.items():
|
|
100
|
+
for file, lines in v.items():
|
|
101
|
+
for l in lines:
|
|
102
|
+
usage_targets.add(f"{file}#{l}")
|
|
56
103
|
|
|
57
104
|
|
|
58
|
-
def
|
|
105
|
+
def print_endpoints(ospec):
|
|
106
|
+
if not ospec:
|
|
107
|
+
return
|
|
108
|
+
paths = json_load(ospec).get("paths") or {}
|
|
109
|
+
pattern_methods = defaultdict(list)
|
|
110
|
+
pattern_usage_targets = defaultdict(set)
|
|
111
|
+
for pattern, path_obj in paths.items():
|
|
112
|
+
usage_targets = set()
|
|
113
|
+
http_method_added = False
|
|
114
|
+
for k, v in path_obj.items():
|
|
115
|
+
if k == "parameters":
|
|
116
|
+
continue
|
|
117
|
+
# Java, JavaScript, Python etc
|
|
118
|
+
if k == "x-atom-usages":
|
|
119
|
+
_track_usage_targets(usage_targets, v)
|
|
120
|
+
continue
|
|
121
|
+
if isinstance(v, dict) and v.get("x-atom-usages"):
|
|
122
|
+
_track_usage_targets(usage_targets, v.get("x-atom-usages"))
|
|
123
|
+
pattern_methods[pattern].append(k)
|
|
124
|
+
http_method_added = True
|
|
125
|
+
pattern_usage_targets[pattern] = usage_targets
|
|
126
|
+
# We see an endpoint, but do not know the HTTP methods.
|
|
127
|
+
# Let's track them as empty
|
|
128
|
+
if not http_method_added and usage_targets:
|
|
129
|
+
pattern_methods[pattern].append("")
|
|
130
|
+
caption = ""
|
|
131
|
+
if pattern_methods:
|
|
132
|
+
caption = f"Identified Endpoints: {len(pattern_methods.keys())}"
|
|
133
|
+
rtable = Table(
|
|
134
|
+
box=box.DOUBLE_EDGE,
|
|
135
|
+
show_lines=True,
|
|
136
|
+
title="Endpoints",
|
|
137
|
+
caption=caption,
|
|
138
|
+
)
|
|
139
|
+
for c in ("URL Pattern", "HTTP Methods", "Code Hotspots"):
|
|
140
|
+
rtable.add_column(header=c, vertical="top")
|
|
141
|
+
for k, v in pattern_methods.items():
|
|
142
|
+
v.sort()
|
|
143
|
+
sorted_areas = list(pattern_usage_targets[k])
|
|
144
|
+
sorted_areas.sort()
|
|
145
|
+
rtable.add_row(k, ("\n".join(v)).upper(), "\n".join(sorted_areas))
|
|
146
|
+
if pattern_methods:
|
|
147
|
+
console.print()
|
|
148
|
+
console.print(rtable)
|
|
149
|
+
return pattern_methods
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def is_cpp_flow(flows):
|
|
153
|
+
if not flows:
|
|
154
|
+
return False
|
|
155
|
+
attempts = 0
|
|
156
|
+
for idx, aflow in enumerate(flows):
|
|
157
|
+
if aflow.get("parentFileName", "").endswith(".c") or aflow.get(
|
|
158
|
+
"parentFileName", ""
|
|
159
|
+
).endswith(".cpp"):
|
|
160
|
+
return True
|
|
161
|
+
attempts += 1
|
|
162
|
+
if attempts > 3:
|
|
163
|
+
return False
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def explain_reachables(
|
|
168
|
+
explanation_mode, reachables, project_type, vdr_result, header_section=None
|
|
169
|
+
):
|
|
59
170
|
""""""
|
|
60
171
|
reachable_explanations = 0
|
|
61
172
|
checked_flows = 0
|
|
173
|
+
has_crypto_flows = False
|
|
174
|
+
purls_reachable_explanations = defaultdict(int)
|
|
175
|
+
source_reachable_explanations = defaultdict(int)
|
|
176
|
+
sink_reachable_explanations = defaultdict(int)
|
|
177
|
+
has_explanation = False
|
|
178
|
+
header_shown = False
|
|
62
179
|
for areach in reachables.get("reachables", []):
|
|
63
180
|
if (
|
|
64
181
|
not areach.get("flows")
|
|
65
182
|
or len(areach.get("flows")) < 2
|
|
66
|
-
or not areach.get("purls")
|
|
183
|
+
or (not areach.get("purls") and not is_cpp_flow(areach.get("flows")))
|
|
67
184
|
):
|
|
68
185
|
continue
|
|
69
186
|
# Focus only on the prioritized list if available
|
|
@@ -74,11 +191,55 @@ def explain_reachables(reachables, pkg_group_rows, project_type):
|
|
|
74
191
|
# is_prioritized = True
|
|
75
192
|
# if not is_prioritized:
|
|
76
193
|
# continue
|
|
77
|
-
|
|
78
|
-
|
|
194
|
+
(
|
|
195
|
+
flow_tree,
|
|
196
|
+
comment,
|
|
197
|
+
source_sink_desc,
|
|
198
|
+
source_code_str,
|
|
199
|
+
sink_code_str,
|
|
200
|
+
has_check_tag,
|
|
201
|
+
is_endpoint_reachable,
|
|
202
|
+
is_crypto_flow,
|
|
203
|
+
) = explain_flows(
|
|
204
|
+
explanation_mode,
|
|
205
|
+
areach.get("flows"),
|
|
206
|
+
areach.get("purls"),
|
|
207
|
+
project_type,
|
|
208
|
+
vdr_result,
|
|
79
209
|
)
|
|
80
210
|
if not source_sink_desc or not flow_tree:
|
|
81
211
|
continue
|
|
212
|
+
# In non-reachables mode, we are not interested in reachable flows.
|
|
213
|
+
if (
|
|
214
|
+
explanation_mode
|
|
215
|
+
and explanation_mode in ("NonReachables",)
|
|
216
|
+
and not has_check_tag
|
|
217
|
+
):
|
|
218
|
+
continue
|
|
219
|
+
if (
|
|
220
|
+
source_code_str
|
|
221
|
+
and source_reachable_explanations[source_code_str] + 1
|
|
222
|
+
> max_source_reachable_explanations
|
|
223
|
+
):
|
|
224
|
+
continue
|
|
225
|
+
if (
|
|
226
|
+
sink_code_str
|
|
227
|
+
and sink_reachable_explanations[sink_code_str] + 1
|
|
228
|
+
> max_sink_reachable_explanations
|
|
229
|
+
):
|
|
230
|
+
continue
|
|
231
|
+
purls_str = ",".join(sorted(areach.get("purls", [])))
|
|
232
|
+
if (
|
|
233
|
+
purls_str
|
|
234
|
+
and purls_reachable_explanations[purls_str] + 1
|
|
235
|
+
> max_purls_reachable_explanations
|
|
236
|
+
):
|
|
237
|
+
continue
|
|
238
|
+
if not has_explanation:
|
|
239
|
+
has_explanation = True
|
|
240
|
+
# Did we find any crypto flows
|
|
241
|
+
if is_crypto_flow and not has_crypto_flows:
|
|
242
|
+
has_crypto_flows = True
|
|
82
243
|
rtable = Table(
|
|
83
244
|
box=box.DOUBLE_EDGE,
|
|
84
245
|
show_lines=True,
|
|
@@ -90,31 +251,74 @@ def explain_reachables(reachables, pkg_group_rows, project_type):
|
|
|
90
251
|
)
|
|
91
252
|
rtable.add_column(header="Flow", vertical="top")
|
|
92
253
|
rtable.add_row(flow_tree)
|
|
254
|
+
# Print the header first in case we haven't
|
|
255
|
+
if not header_shown and header_section:
|
|
256
|
+
console.print()
|
|
257
|
+
console.print(header_section)
|
|
258
|
+
header_shown = True
|
|
93
259
|
console.print()
|
|
94
260
|
console.print(rtable)
|
|
95
261
|
reachable_explanations += 1
|
|
262
|
+
if purls_str:
|
|
263
|
+
purls_reachable_explanations[purls_str] += 1
|
|
264
|
+
if source_code_str:
|
|
265
|
+
source_reachable_explanations[source_code_str] += 1
|
|
266
|
+
if sink_code_str:
|
|
267
|
+
sink_reachable_explanations[sink_code_str] += 1
|
|
96
268
|
if has_check_tag:
|
|
97
269
|
checked_flows += 1
|
|
98
270
|
if reachable_explanations + 1 > max_reachable_explanations:
|
|
99
271
|
break
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
272
|
+
tips = """## Secure Design Tips"""
|
|
273
|
+
if explanation_mode in ("NonReachables",):
|
|
274
|
+
tips += """
|
|
275
|
+
- Automate tests (including fuzzing) to verify validation, sanitization, encoding, and encryption.
|
|
276
|
+
- Align the implementation with the original architecture and threat models to ensure security compliance.
|
|
277
|
+
- Extract reusable methods into a shared library for organization-wide use.
|
|
278
|
+
"""
|
|
279
|
+
elif has_explanation:
|
|
280
|
+
if has_crypto_flows:
|
|
281
|
+
tips += """
|
|
282
|
+
- Generate a Cryptographic BOM with cdxgen and monitor it in Dependency-Track.
|
|
283
|
+
"""
|
|
284
|
+
elif checked_flows:
|
|
285
|
+
tips += """
|
|
286
|
+
- Review the validation and sanitization methods used in the application.
|
|
287
|
+
- To enhance the security posture, implement a common validation middleware.
|
|
288
|
+
"""
|
|
289
|
+
elif purls_reachable_explanations:
|
|
104
290
|
tips += """
|
|
105
|
-
-
|
|
106
|
-
- To improve the security posture, implement a common validation middleware.
|
|
291
|
+
- Consider implementing a common validation and sanitization library to reduce the risk of exploitability.
|
|
107
292
|
"""
|
|
108
293
|
else:
|
|
109
294
|
tips += """
|
|
110
|
-
-
|
|
295
|
+
- Enhance your unit and integration tests to cover the flows listed above.
|
|
296
|
+
- Continuously fuzz the parser and validation functions with diverse payloads.
|
|
111
297
|
"""
|
|
298
|
+
if tips:
|
|
112
299
|
rsection = Markdown(tips)
|
|
113
300
|
console.print(rsection)
|
|
301
|
+
return has_explanation, has_crypto_flows, tips
|
|
114
302
|
|
|
115
303
|
|
|
116
|
-
def flow_to_source_sink(idx, flow, purls, project_type):
|
|
304
|
+
def flow_to_source_sink(idx, flow, purls, project_type, vdr_result):
|
|
117
305
|
""" """
|
|
306
|
+
endpoint_reached_purls = {}
|
|
307
|
+
reached_services = {}
|
|
308
|
+
if vdr_result:
|
|
309
|
+
endpoint_reached_purls = vdr_result.endpoint_reached_purls
|
|
310
|
+
reached_services = vdr_result.reached_services
|
|
311
|
+
is_endpoint_reachable = False
|
|
312
|
+
possible_reachable_service = False
|
|
313
|
+
tags = flow.get("tags", [])
|
|
314
|
+
is_crypto_flow = "crypto" in tags or "crypto-generate" in tags
|
|
315
|
+
method_in_emoji = ":right_arrow_curving_left:"
|
|
316
|
+
for p in purls:
|
|
317
|
+
if endpoint_reached_purls and endpoint_reached_purls.get(p):
|
|
318
|
+
is_endpoint_reachable = True
|
|
319
|
+
method_in_emoji = ":heavy_large_circle: "
|
|
320
|
+
if reached_services and reached_services.get(p):
|
|
321
|
+
possible_reachable_service = True
|
|
118
322
|
source_sink_desc = ""
|
|
119
323
|
param_name = flow.get("name")
|
|
120
324
|
method_str = "method"
|
|
@@ -130,14 +334,14 @@ def flow_to_source_sink(idx, flow, purls, project_type):
|
|
|
130
334
|
if parent_method in ("handleRequest",):
|
|
131
335
|
method_str = f"handler {method_str}"
|
|
132
336
|
elif parent_method in ("__construct", "__init"):
|
|
133
|
-
method_str =
|
|
337
|
+
method_str = "constructor"
|
|
134
338
|
elif project_type in ("php",) and parent_method.startswith("__"):
|
|
135
339
|
method_str = f"magic {method_str}"
|
|
136
340
|
if flow.get("label") == "METHOD_PARAMETER_IN":
|
|
137
341
|
if param_name:
|
|
138
|
-
source_sink_desc = f"""{param_str} [red]{param_name}[/red]
|
|
342
|
+
source_sink_desc = f"""{param_str} [red]{param_name}[/red] {method_in_emoji} to the {method_str} [bold]{parent_method}[/bold]"""
|
|
139
343
|
else:
|
|
140
|
-
source_sink_desc = f"""{method_str.capitalize()} [red]{parent_method}[/red]
|
|
344
|
+
source_sink_desc = f"""{method_str.capitalize()} [red]{parent_method}[/red] {method_in_emoji}"""
|
|
141
345
|
elif flow.get("label") == "CALL" and flow.get("isExternal"):
|
|
142
346
|
method_full_name = flow.get("fullName", "")
|
|
143
347
|
if not method_full_name.startswith("<"):
|
|
@@ -146,9 +350,11 @@ def flow_to_source_sink(idx, flow, purls, project_type):
|
|
|
146
350
|
source_sink_desc = flow.get("code").split("\n")[0]
|
|
147
351
|
elif project_type not in ("java") and flow.get("label") == "IDENTIFIER":
|
|
148
352
|
source_sink_desc = flow.get("code").split("\n")[0]
|
|
353
|
+
if source_sink_desc.endswith("("):
|
|
354
|
+
source_sink_desc = f":diamond_suit: {source_sink_desc})"
|
|
149
355
|
# Try to understand the source a bit more
|
|
150
356
|
if source_sink_desc.startswith("require("):
|
|
151
|
-
source_sink_desc = "
|
|
357
|
+
source_sink_desc = "The flow originates from a module import."
|
|
152
358
|
elif (
|
|
153
359
|
".use(" in source_sink_desc
|
|
154
360
|
or ".subscribe(" in source_sink_desc
|
|
@@ -156,21 +362,41 @@ def flow_to_source_sink(idx, flow, purls, project_type):
|
|
|
156
362
|
or ".emit(" in source_sink_desc
|
|
157
363
|
or " => {" in source_sink_desc
|
|
158
364
|
):
|
|
159
|
-
source_sink_desc = "
|
|
365
|
+
source_sink_desc = "The flow originates from a callback function."
|
|
160
366
|
elif (
|
|
161
|
-
"middleware" in source_sink_desc.lower()
|
|
162
|
-
or "route" in source_sink_desc.lower()
|
|
367
|
+
"middleware" in source_sink_desc.lower() or "route" in source_sink_desc.lower()
|
|
163
368
|
):
|
|
164
|
-
source_sink_desc = "
|
|
369
|
+
source_sink_desc = "The flow originates from middleware."
|
|
370
|
+
elif len(purls) == 0:
|
|
371
|
+
if tags:
|
|
372
|
+
source_sink_desc = (
|
|
373
|
+
f"{source_sink_desc} can be used to reach packages with tags `{tags}`"
|
|
374
|
+
)
|
|
165
375
|
elif len(purls) == 1:
|
|
166
|
-
|
|
167
|
-
f"{source_sink_desc} can be used to reach this package."
|
|
168
|
-
|
|
376
|
+
if is_endpoint_reachable:
|
|
377
|
+
source_sink_desc = f"{source_sink_desc} can be used to reach this package from certain endpoints."
|
|
378
|
+
elif source_sink_desc:
|
|
379
|
+
if is_crypto_flow:
|
|
380
|
+
source_sink_desc = "Reachable crypto-flow."
|
|
381
|
+
else:
|
|
382
|
+
source_sink_desc = "Reachable data-flow."
|
|
169
383
|
else:
|
|
170
|
-
|
|
171
|
-
f"{source_sink_desc} can be used to reach {len(purls)} packages."
|
|
172
|
-
|
|
173
|
-
|
|
384
|
+
if is_endpoint_reachable:
|
|
385
|
+
source_sink_desc = f"{source_sink_desc} can be used to reach {len(purls)} packages from certain endpoints."
|
|
386
|
+
else:
|
|
387
|
+
if source_sink_desc:
|
|
388
|
+
source_sink_desc = (
|
|
389
|
+
f"{source_sink_desc} can be used to reach {len(purls)} packages."
|
|
390
|
+
)
|
|
391
|
+
elif is_crypto_flow:
|
|
392
|
+
source_sink_desc = (
|
|
393
|
+
f"{len(purls)} packages reachable from this crypto-flow."
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
source_sink_desc = (
|
|
397
|
+
f"{len(purls)} packages reachable from this data-flow."
|
|
398
|
+
)
|
|
399
|
+
return source_sink_desc, is_endpoint_reachable, is_crypto_flow
|
|
174
400
|
|
|
175
401
|
|
|
176
402
|
def filter_tags(tags):
|
|
@@ -178,14 +404,30 @@ def filter_tags(tags):
|
|
|
178
404
|
tags = [
|
|
179
405
|
atag
|
|
180
406
|
for atag in tags.split(", ")
|
|
181
|
-
if atag
|
|
182
|
-
not in ("RESOLVED_MEMBER", "UNKNOWN_METHOD", "UNKNOWN_TYPE_DECL")
|
|
407
|
+
if atag not in ("RESOLVED_MEMBER", "UNKNOWN_METHOD", "UNKNOWN_TYPE_DECL")
|
|
183
408
|
]
|
|
184
409
|
return ", ".join(tags)
|
|
185
410
|
return tags
|
|
186
411
|
|
|
187
412
|
|
|
188
|
-
def
|
|
413
|
+
def is_filterable_code(project_type, code):
|
|
414
|
+
match project_type:
|
|
415
|
+
case "js" | "ts" | "javascript" | "typescript" | "bom":
|
|
416
|
+
for c in (
|
|
417
|
+
"console.log",
|
|
418
|
+
"thoughtLog(",
|
|
419
|
+
"_tmp_",
|
|
420
|
+
"LOG.debug(",
|
|
421
|
+
"options.get(",
|
|
422
|
+
"RET",
|
|
423
|
+
"this.",
|
|
424
|
+
):
|
|
425
|
+
if code and code.startswith(c):
|
|
426
|
+
return True
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def flow_to_str(explanation_mode, flow, project_type):
|
|
189
431
|
""""""
|
|
190
432
|
has_check_tag = False
|
|
191
433
|
file_loc = ""
|
|
@@ -194,60 +436,65 @@ def flow_to_str(flow, project_type):
|
|
|
194
436
|
and flow.get("lineNumber")
|
|
195
437
|
and not flow.get("parentFileName").startswith("unknown")
|
|
196
438
|
):
|
|
197
|
-
file_loc = f
|
|
439
|
+
file_loc = f"{flow.get('parentFileName').replace('src/main/java/', '').replace('src/main/scala/', '')}#{flow.get('lineNumber')} "
|
|
198
440
|
node_desc = flow.get("code").split("\n")[0]
|
|
441
|
+
if node_desc.endswith("("):
|
|
442
|
+
node_desc = f":diamond_suit: {node_desc})"
|
|
199
443
|
tags = filter_tags(flow.get("tags"))
|
|
200
444
|
if flow.get("label") == "METHOD_PARAMETER_IN":
|
|
201
445
|
param_name = flow.get("name")
|
|
202
446
|
if param_name == "this":
|
|
203
447
|
param_name = ""
|
|
204
|
-
node_desc = f
|
|
448
|
+
node_desc = f"{flow.get('parentMethodName')}([red]{param_name}[/red]) :right_arrow_curving_left:"
|
|
205
449
|
if tags:
|
|
206
|
-
node_desc =
|
|
207
|
-
f"{node_desc}\n[bold]Tags:[/bold] [italic]{tags}[/italic]\n"
|
|
208
|
-
)
|
|
450
|
+
node_desc = f"{node_desc}\n[bold]Tags:[/bold] [italic]{tags}[/italic]\n"
|
|
209
451
|
elif flow.get("label") == "IDENTIFIER":
|
|
210
452
|
if node_desc.startswith("<"):
|
|
211
453
|
node_desc = flow.get("name")
|
|
212
454
|
if tags:
|
|
213
|
-
node_desc =
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if tags:
|
|
217
|
-
for ctag in (
|
|
218
|
-
"validation",
|
|
219
|
-
"encode",
|
|
220
|
-
"encrypt",
|
|
221
|
-
"sanitize",
|
|
222
|
-
"authentication",
|
|
223
|
-
"authorization",
|
|
224
|
-
):
|
|
455
|
+
node_desc = f"{node_desc}\n[bold]Tags:[/bold] [italic]{tags}[/italic]\n"
|
|
456
|
+
if tags and not is_filterable_code(project_type, node_desc):
|
|
457
|
+
for ctag in COMMON_CHECK_TAGS:
|
|
225
458
|
if ctag in tags:
|
|
226
459
|
has_check_tag = True
|
|
227
460
|
break
|
|
228
461
|
if has_check_tag:
|
|
229
|
-
|
|
462
|
+
if explanation_mode in ("NonReachables",):
|
|
463
|
+
node_desc = f"[bold][green]{node_desc}[/green][/bold]"
|
|
464
|
+
else:
|
|
465
|
+
node_desc = f"[green]{node_desc}[/green]"
|
|
466
|
+
flow_str = (
|
|
467
|
+
f"""[gray37]{file_loc}[/gray37]{node_desc}"""
|
|
468
|
+
if not is_filterable_code(project_type, node_desc)
|
|
469
|
+
else ""
|
|
470
|
+
)
|
|
230
471
|
return (
|
|
231
472
|
file_loc,
|
|
232
|
-
|
|
473
|
+
flow_str,
|
|
474
|
+
node_desc,
|
|
233
475
|
has_check_tag,
|
|
234
476
|
)
|
|
235
477
|
|
|
236
478
|
|
|
237
|
-
def explain_flows(flows, purls, project_type):
|
|
479
|
+
def explain_flows(explanation_mode, flows, purls, project_type, vdr_result):
|
|
238
480
|
""""""
|
|
239
481
|
tree = None
|
|
240
482
|
comments = []
|
|
241
483
|
if len(purls) > max_purl_per_flow:
|
|
242
484
|
comments.append(
|
|
243
|
-
":exclamation_mark: Refactor this flow to
|
|
485
|
+
":exclamation_mark: Refactor this flow to minimize the use of external libraries."
|
|
244
486
|
)
|
|
245
|
-
|
|
246
|
-
|
|
487
|
+
if purls:
|
|
488
|
+
purls_str = "\n".join(purls)
|
|
489
|
+
comments.append(f"[info]Reachable Packages:[/info]\n{purls_str}")
|
|
247
490
|
added_flows = []
|
|
491
|
+
added_node_desc = []
|
|
248
492
|
has_check_tag = False
|
|
249
493
|
last_file_loc = None
|
|
250
494
|
source_sink_desc = ""
|
|
495
|
+
last_code = ""
|
|
496
|
+
source_code_str = ""
|
|
497
|
+
sink_code_str = ""
|
|
251
498
|
for idx, aflow in enumerate(flows):
|
|
252
499
|
# For java, we are only interested in identifiers with tags to keep the flows simple to understand
|
|
253
500
|
if (
|
|
@@ -256,28 +503,46 @@ def explain_flows(flows, purls, project_type):
|
|
|
256
503
|
and not aflow.get("tags")
|
|
257
504
|
):
|
|
258
505
|
continue
|
|
506
|
+
curr_code = aflow.get("code", "").split("\n")[0]
|
|
507
|
+
if idx == 0:
|
|
508
|
+
source_code_str = curr_code
|
|
509
|
+
if idx == len(flows):
|
|
510
|
+
sink_code_str = curr_code
|
|
511
|
+
if last_code and last_code == curr_code:
|
|
512
|
+
continue
|
|
513
|
+
last_code = curr_code
|
|
259
514
|
if not source_sink_desc:
|
|
260
|
-
source_sink_desc =
|
|
261
|
-
idx, aflow, purls, project_type
|
|
515
|
+
source_sink_desc, is_endpoint_reachable, is_crypto_flow = (
|
|
516
|
+
flow_to_source_sink(idx, aflow, purls, project_type, vdr_result)
|
|
262
517
|
)
|
|
263
|
-
file_loc, flow_str, has_check_tag_flow = flow_to_str(
|
|
264
|
-
aflow, project_type
|
|
518
|
+
file_loc, flow_str, node_desc, has_check_tag_flow = flow_to_str(
|
|
519
|
+
explanation_mode, aflow, project_type
|
|
265
520
|
)
|
|
266
|
-
if last_file_loc == file_loc:
|
|
521
|
+
if last_file_loc and last_file_loc == file_loc:
|
|
267
522
|
continue
|
|
268
523
|
last_file_loc = file_loc
|
|
269
|
-
if flow_str in added_flows:
|
|
524
|
+
if flow_str in added_flows or node_desc in added_node_desc:
|
|
270
525
|
continue
|
|
271
526
|
added_flows.append(flow_str)
|
|
527
|
+
added_node_desc.append(node_desc)
|
|
272
528
|
if not tree:
|
|
273
529
|
tree = Tree(flow_str)
|
|
274
530
|
else:
|
|
275
531
|
tree.add(flow_str)
|
|
276
532
|
if has_check_tag_flow:
|
|
277
533
|
has_check_tag = True
|
|
278
|
-
if has_check_tag:
|
|
534
|
+
if has_check_tag and explanation_mode not in ("NonReachables",):
|
|
279
535
|
comments.insert(
|
|
280
536
|
0,
|
|
281
|
-
":white_medium_small_square:
|
|
537
|
+
":white_medium_small_square: Verify that the mitigation(s) used in this flow are valid and appropriate for your security requirements.",
|
|
282
538
|
)
|
|
283
|
-
return
|
|
539
|
+
return (
|
|
540
|
+
tree,
|
|
541
|
+
"\n".join(comments),
|
|
542
|
+
source_sink_desc,
|
|
543
|
+
source_code_str,
|
|
544
|
+
sink_code_str,
|
|
545
|
+
has_check_tag,
|
|
546
|
+
is_endpoint_reachable,
|
|
547
|
+
is_crypto_flow,
|
|
548
|
+
)
|
depscan/lib/license.py
CHANGED
|
@@ -50,14 +50,15 @@ def bulk_lookup(license_dict, pkg_list):
|
|
|
50
50
|
pkg_key = pkg["name"] + "@" + pkg["version"]
|
|
51
51
|
if pkg.get("vendor"):
|
|
52
52
|
pkg_key = pkg.get("vendor") + ":" + pkg["name"] + "@" + pkg["version"]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
lic
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
if pkg.get("licenses"):
|
|
54
|
+
for lic in pkg.get("licenses"):
|
|
55
|
+
if lic == "X11":
|
|
56
|
+
lic = "MIT"
|
|
57
|
+
elif "MIT" in lic:
|
|
58
|
+
lic = "MIT"
|
|
59
|
+
curr_list = pkg_licenses.get(pkg_key, [])
|
|
60
|
+
match_lic = license_dict.get(lic)
|
|
61
|
+
if match_lic:
|
|
62
|
+
curr_list.append(match_lic)
|
|
63
|
+
pkg_licenses[pkg_key] = curr_list
|
|
63
64
|
return pkg_licenses
|