java-dependency-analyzer 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- java_dependency_analyzer/__init__.py +11 -0
- java_dependency_analyzer/cache/__init__.py +11 -0
- java_dependency_analyzer/cache/db.py +101 -0
- java_dependency_analyzer/cache/vulnerability_cache.py +156 -0
- java_dependency_analyzer/cli.py +394 -0
- java_dependency_analyzer/models/__init__.py +11 -0
- java_dependency_analyzer/models/dependency.py +80 -0
- java_dependency_analyzer/models/report.py +108 -0
- java_dependency_analyzer/parsers/__init__.py +11 -0
- java_dependency_analyzer/parsers/base.py +150 -0
- java_dependency_analyzer/parsers/gradle_dep_tree_parser.py +125 -0
- java_dependency_analyzer/parsers/gradle_parser.py +206 -0
- java_dependency_analyzer/parsers/maven_dep_tree_parser.py +123 -0
- java_dependency_analyzer/parsers/maven_parser.py +182 -0
- java_dependency_analyzer/reporters/__init__.py +11 -0
- java_dependency_analyzer/reporters/base.py +33 -0
- java_dependency_analyzer/reporters/html_reporter.py +82 -0
- java_dependency_analyzer/reporters/json_reporter.py +52 -0
- java_dependency_analyzer/reporters/templates/report.html +406 -0
- java_dependency_analyzer/resolvers/__init__.py +11 -0
- java_dependency_analyzer/resolvers/transitive.py +276 -0
- java_dependency_analyzer/scanners/__init__.py +11 -0
- java_dependency_analyzer/scanners/base.py +102 -0
- java_dependency_analyzer/scanners/ghsa_scanner.py +204 -0
- java_dependency_analyzer/scanners/osv_scanner.py +167 -0
- java_dependency_analyzer/util/__init__.py +11 -0
- java_dependency_analyzer/util/logger.py +48 -0
- java_dependency_analyzer-1.0.0.dist-info/METADATA +193 -0
- java_dependency_analyzer-1.0.0.dist-info/RECORD +31 -0
- java_dependency_analyzer-1.0.0.dist-info/WHEEL +4 -0
- java_dependency_analyzer-1.0.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Java Dependency Vulnerability Report</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; color: #333; }
|
|
9
|
+
h1 { color: #2c3e50; border-bottom: 3px solid #e74c3c; padding-bottom: 10px; }
|
|
10
|
+
h2 { color: #2c3e50; margin-top: 30px; }
|
|
11
|
+
.summary { display: flex; gap: 20px; flex-wrap: wrap; margin: 20px 0; }
|
|
12
|
+
.card {
|
|
13
|
+
background: #fff; border-radius: 8px; padding: 20px; min-width: 150px;
|
|
14
|
+
box-shadow: 0 2px 4px rgba(0,0,0,.1); text-align: center;
|
|
15
|
+
cursor: pointer; transition: box-shadow 0.2s, transform 0.15s, border 0.15s;
|
|
16
|
+
border: 2px solid transparent; user-select: none;
|
|
17
|
+
}
|
|
18
|
+
.card:hover { box-shadow: 0 4px 14px rgba(0,0,0,.18); transform: translateY(-2px); }
|
|
19
|
+
.card.active { border-color: #2980b9; box-shadow: 0 0 0 3px rgba(41,128,185,.25); }
|
|
20
|
+
.card .number { font-size: 2.5em; font-weight: bold; }
|
|
21
|
+
.card.danger .number { color: #e74c3c; }
|
|
22
|
+
.card.warning .number { color: #f39c12; }
|
|
23
|
+
.card.safe .number { color: #27ae60; }
|
|
24
|
+
.card .label { font-size: 0.85em; color: #666; margin-top: 4px; }
|
|
25
|
+
.meta { color: #666; font-size: 0.9em; margin-bottom: 20px; }
|
|
26
|
+
.filter-notice {
|
|
27
|
+
display: none; background: #eaf3fb; border-left: 4px solid #2980b9;
|
|
28
|
+
padding: 8px 14px; margin-bottom: 12px; border-radius: 4px;
|
|
29
|
+
font-size: 0.9em; color: #2c3e50;
|
|
30
|
+
}
|
|
31
|
+
.filter-notice.visible { display: flex; align-items: center; gap: 10px; }
|
|
32
|
+
.filter-notice button {
|
|
33
|
+
background: none; border: none; color: #2980b9; cursor: pointer;
|
|
34
|
+
font-size: 0.95em; padding: 0; text-decoration: underline;
|
|
35
|
+
}
|
|
36
|
+
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,.1); }
|
|
37
|
+
thead { background: #2c3e50; color: #fff; }
|
|
38
|
+
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; }
|
|
39
|
+
tr:last-child td { border-bottom: none; }
|
|
40
|
+
tr:hover td { background: #f9f9f9; }
|
|
41
|
+
tr.hidden { display: none; }
|
|
42
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; font-weight: bold; }
|
|
43
|
+
.badge-critical { background: #e74c3c; color: #fff; }
|
|
44
|
+
.badge-high { background: #e67e22; color: #fff; }
|
|
45
|
+
.badge-medium { background: #f39c12; color: #fff; }
|
|
46
|
+
.badge-low { background: #3498db; color: #fff; }
|
|
47
|
+
.badge-unknown { background: #95a5a6; color: #fff; }
|
|
48
|
+
.no-vulns { color: #27ae60; font-style: italic; }
|
|
49
|
+
a { color: #2980b9; text-decoration: none; }
|
|
50
|
+
a:hover { text-decoration: underline; }
|
|
51
|
+
details summary { cursor: pointer; font-weight: bold; color: #2c3e50; }
|
|
52
|
+
.vuln-row { background: #fff8f8; }
|
|
53
|
+
.vuln-cve { font-family: monospace; font-weight: bold; }
|
|
54
|
+
.footer { margin-top: 40px; text-align: center; color: #aaa; font-size: 0.8em; }
|
|
55
|
+
/* Dependency tree visuals */
|
|
56
|
+
.dep-name { white-space: nowrap; }
|
|
57
|
+
.dep-root > a { font-weight: 600; color: #2c3e50; }
|
|
58
|
+
.dep-root-dot { color: #2c3e50; margin-right: 6px; font-size: 1.1em; }
|
|
59
|
+
.tree-prefix { font-family: 'Courier New', monospace; color: #bbb; font-size: 0.9em; letter-spacing: 0; white-space: pre; }
|
|
60
|
+
.tree-connector { color: #aaa; font-family: 'Courier New', monospace; margin-right: 4px; }
|
|
61
|
+
tr[data-depth="0"] td:first-child { background: #fafafa; }
|
|
62
|
+
tr[data-depth="1"] td:first-child { background: #f5f8ff; }
|
|
63
|
+
tr[data-depth="2"] td:first-child { background: #f0f5ff; }
|
|
64
|
+
/* Collapsible tree */
|
|
65
|
+
.collapsed-hidden { display: none; }
|
|
66
|
+
.collapse-toggle {
|
|
67
|
+
display: inline-block; width: 16px; text-align: center;
|
|
68
|
+
cursor: pointer; color: #999; font-size: 0.85em;
|
|
69
|
+
user-select: none; margin-right: 2px;
|
|
70
|
+
transition: transform 0.15s;
|
|
71
|
+
}
|
|
72
|
+
.collapse-toggle:hover { color: #2980b9; }
|
|
73
|
+
.tree-controls { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
74
|
+
.btn-tree {
|
|
75
|
+
background: #fff; border: 1px solid #ccc; border-radius: 4px;
|
|
76
|
+
padding: 4px 12px; font-size: 0.85em; cursor: pointer; color: #2c3e50;
|
|
77
|
+
transition: background 0.15s, border-color 0.15s;
|
|
78
|
+
}
|
|
79
|
+
.btn-tree:hover { background: #eaf3fb; border-color: #2980b9; color: #2980b9; }
|
|
80
|
+
</style>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<h1>Java Dependency Vulnerability Report</h1>
|
|
84
|
+
<p class="meta">
|
|
85
|
+
<strong>Source:</strong> {{ result.source_file }}
|
|
86
|
+
<strong>Scanned at:</strong> {{ result.scanned_at }}
|
|
87
|
+
</p>
|
|
88
|
+
|
|
89
|
+
<h2>Summary</h2>
|
|
90
|
+
<div class="summary">
|
|
91
|
+
<div class="card active" data-filter="all">
|
|
92
|
+
<div class="number">{{ result.total_dependencies }}</div>
|
|
93
|
+
<div class="label">Total Dependencies</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="card {% if result.total_vulnerabilities > 0 %}danger{% else %}safe{% endif %}" data-filter="has-vulns">
|
|
96
|
+
<div class="number">{{ result.total_vulnerabilities }}</div>
|
|
97
|
+
<div class="label">Total Vulnerabilities</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="card {% if result.vulnerable_dependencies|length > 0 %}warning{% else %}safe{% endif %}" data-filter="vuln-deps">
|
|
100
|
+
<div class="number">{{ result.vulnerable_dependencies|length }}</div>
|
|
101
|
+
<div class="label">Vulnerable Dependencies</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{% if result.total_vulnerabilities == 0 %}
|
|
106
|
+
<p style="color: #27ae60; font-size: 1.1em;">✔ No vulnerabilities found across all dependencies.</p>
|
|
107
|
+
{% endif %}
|
|
108
|
+
|
|
109
|
+
<h2>Dependency Tree</h2>
|
|
110
|
+
<div class="filter-notice" id="filter-notice">
|
|
111
|
+
<span id="filter-notice-text"></span>
|
|
112
|
+
<button onclick="resetFilter()">Show all</button>
|
|
113
|
+
</div>
|
|
114
|
+
<div id="dep-tree-section">
|
|
115
|
+
<div class="tree-controls" id="tree-controls">
|
|
116
|
+
<button class="btn-tree" onclick="expandAll()">Expand All</button>
|
|
117
|
+
<button class="btn-tree" onclick="collapseAll()">Collapse All</button>
|
|
118
|
+
</div>
|
|
119
|
+
<table id="dep-table">
|
|
120
|
+
<thead>
|
|
121
|
+
<tr>
|
|
122
|
+
<th>Dependency</th>
|
|
123
|
+
<th>Version</th>
|
|
124
|
+
<th>Scope</th>
|
|
125
|
+
<th>Vulnerabilities</th>
|
|
126
|
+
</tr>
|
|
127
|
+
</thead>
|
|
128
|
+
<tbody>
|
|
129
|
+
{% for dep in all_deps %}
|
|
130
|
+
<tr class="dep-row" data-depth="{{ dep.depth }}" data-vuln-count="{{ dep.vulnerabilities | length }}">
|
|
131
|
+
<td class="dep-name">
|
|
132
|
+
{% if dep.depth == 0 %}
|
|
133
|
+
<span class="dep-root">
|
|
134
|
+
<span class="dep-root-dot">●</span>
|
|
135
|
+
<a href="https://repo1.maven.org/maven2/{{ dep.group_id | replace('.', '/') }}/{{ dep.artifact_id }}/{{ dep.version }}/"
|
|
136
|
+
target="_blank" rel="noopener noreferrer">{{ dep.group_id }}:{{ dep.artifact_id }}</a>
|
|
137
|
+
</span>
|
|
138
|
+
{% else %}
|
|
139
|
+
<span class="tree-prefix">{{ ' ' * (dep.depth - 1) }}</span><span class="tree-connector">└─</span><a href="https://repo1.maven.org/maven2/{{ dep.group_id | replace('.', '/') }}/{{ dep.artifact_id }}/{{ dep.version }}/"
|
|
140
|
+
target="_blank" rel="noopener noreferrer">{{ dep.group_id }}:{{ dep.artifact_id }}</a>
|
|
141
|
+
{% endif %}
|
|
142
|
+
</td>
|
|
143
|
+
<td>{{ dep.version }}</td>
|
|
144
|
+
<td>{{ dep.scope }}</td>
|
|
145
|
+
<td>
|
|
146
|
+
{% if dep.vulnerabilities %}
|
|
147
|
+
{% for vuln in dep.vulnerabilities %}
|
|
148
|
+
<details>
|
|
149
|
+
<summary>
|
|
150
|
+
<span class="vuln-cve">{{ vuln.cve_id }}</span>
|
|
151
|
+
<span class="badge badge-{{ vuln.severity | lower | replace('cvss', '') | replace(' ', '') | truncate(8, true, '') }}">
|
|
152
|
+
{{ vuln.severity }}
|
|
153
|
+
</span>
|
|
154
|
+
</summary>
|
|
155
|
+
<p>{{ vuln.summary }}</p>
|
|
156
|
+
{% if vuln.affected_versions %}
|
|
157
|
+
<p><strong>Affected versions:</strong> {{ vuln.affected_versions | join(', ') }}</p>
|
|
158
|
+
{% endif %}
|
|
159
|
+
{% if vuln.reference_url %}
|
|
160
|
+
<p><a href="{{ vuln.reference_url }}" target="_blank" rel="noopener noreferrer">
|
|
161
|
+
More info →
|
|
162
|
+
</a></p>
|
|
163
|
+
{% endif %}
|
|
164
|
+
<p><em>Source: {{ vuln.source }}</em></p>
|
|
165
|
+
</details>
|
|
166
|
+
{% endfor %}
|
|
167
|
+
{% else %}
|
|
168
|
+
<span class="no-vulns">None found</span>
|
|
169
|
+
{% endif %}
|
|
170
|
+
</td>
|
|
171
|
+
</tr>
|
|
172
|
+
{% endfor %}
|
|
173
|
+
</tbody>
|
|
174
|
+
</table>
|
|
175
|
+
</div><!-- #dep-tree-section -->
|
|
176
|
+
|
|
177
|
+
<div id="vuln-list-section" style="display:none;">
|
|
178
|
+
{% if result.vulnerable_dependencies %}
|
|
179
|
+
<table>
|
|
180
|
+
<thead>
|
|
181
|
+
<tr>
|
|
182
|
+
<th>Dependency</th>
|
|
183
|
+
<th>Version</th>
|
|
184
|
+
<th>Scope</th>
|
|
185
|
+
<th>Vulnerabilities</th>
|
|
186
|
+
</tr>
|
|
187
|
+
</thead>
|
|
188
|
+
<tbody>
|
|
189
|
+
{% for dep in result.vulnerable_dependencies %}
|
|
190
|
+
<tr>
|
|
191
|
+
<td>
|
|
192
|
+
<a href="https://repo1.maven.org/maven2/{{ dep.group_id | replace('.', '/') }}/{{ dep.artifact_id }}/{{ dep.version }}/"
|
|
193
|
+
target="_blank" rel="noopener noreferrer">{{ dep.group_id }}:{{ dep.artifact_id }}</a>
|
|
194
|
+
</td>
|
|
195
|
+
<td>{{ dep.version }}</td>
|
|
196
|
+
<td>{{ dep.scope }}</td>
|
|
197
|
+
<td>
|
|
198
|
+
{% for vuln in dep.vulnerabilities %}
|
|
199
|
+
<details>
|
|
200
|
+
<summary>
|
|
201
|
+
<span class="vuln-cve">{{ vuln.cve_id }}</span>
|
|
202
|
+
<span class="badge badge-{{ vuln.severity | lower | replace('cvss', '') | replace(' ', '') | truncate(8, true, '') }}">
|
|
203
|
+
{{ vuln.severity }}
|
|
204
|
+
</span>
|
|
205
|
+
</summary>
|
|
206
|
+
<p>{{ vuln.summary }}</p>
|
|
207
|
+
{% if vuln.affected_versions %}
|
|
208
|
+
<p><strong>Affected versions:</strong> {{ vuln.affected_versions | join(', ') }}</p>
|
|
209
|
+
{% endif %}
|
|
210
|
+
{% if vuln.reference_url %}
|
|
211
|
+
<p><a href="{{ vuln.reference_url }}" target="_blank" rel="noopener noreferrer">
|
|
212
|
+
More info →
|
|
213
|
+
</a></p>
|
|
214
|
+
{% endif %}
|
|
215
|
+
<p><em>Source: {{ vuln.source }}</em></p>
|
|
216
|
+
</details>
|
|
217
|
+
{% endfor %}
|
|
218
|
+
</td>
|
|
219
|
+
</tr>
|
|
220
|
+
{% endfor %}
|
|
221
|
+
</tbody>
|
|
222
|
+
</table>
|
|
223
|
+
{% else %}
|
|
224
|
+
<p style="color: #27ae60; font-size: 1.1em;">✔ No vulnerable dependencies found.</p>
|
|
225
|
+
{% endif %}
|
|
226
|
+
</div><!-- #vuln-list-section -->
|
|
227
|
+
|
|
228
|
+
<div class="footer">
|
|
229
|
+
Generated by Java Dependency Analyzer v1.0.0 — Ron Webb
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<script>
|
|
233
|
+
const rows = Array.from(document.querySelectorAll('.dep-row'));
|
|
234
|
+
const notice = document.getElementById('filter-notice');
|
|
235
|
+
const noticeText = document.getElementById('filter-notice-text');
|
|
236
|
+
const treeControls = document.getElementById('tree-controls');
|
|
237
|
+
|
|
238
|
+
// ── Collapse state ──────────────────────────────────────────────────────────
|
|
239
|
+
const collapsedRows = new Set(); // indices of rows the user collapsed
|
|
240
|
+
let filterActive = false;
|
|
241
|
+
|
|
242
|
+
function getDepth(row) {
|
|
243
|
+
return parseInt(row.dataset.depth || '0', 10);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Return indices of all consecutive descendants of rows[i] (depth > rows[i].depth). */
|
|
247
|
+
function getDescendantIndices(i) {
|
|
248
|
+
const parentDepth = getDepth(rows[i]);
|
|
249
|
+
const indices = [];
|
|
250
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
251
|
+
if (getDepth(rows[j]) > parentDepth) {
|
|
252
|
+
indices.push(j);
|
|
253
|
+
} else {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return indices;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Collapse rows[i]: hide all descendants, update toggle icon. */
|
|
261
|
+
function collapseRow(i) {
|
|
262
|
+
collapsedRows.add(i);
|
|
263
|
+
getDescendantIndices(i).forEach(j => {
|
|
264
|
+
rows[j].classList.add('collapsed-hidden');
|
|
265
|
+
});
|
|
266
|
+
const toggle = rows[i].querySelector('.collapse-toggle');
|
|
267
|
+
if (toggle) toggle.textContent = '▶';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Expand rows[i]: show direct children (but not children of sub-collapsed rows). */
|
|
271
|
+
function expandRow(i) {
|
|
272
|
+
collapsedRows.delete(i);
|
|
273
|
+
const parentDepth = getDepth(rows[i]);
|
|
274
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
275
|
+
const d = getDepth(rows[j]);
|
|
276
|
+
if (d <= parentDepth) break;
|
|
277
|
+
if (d === parentDepth + 1) {
|
|
278
|
+
// direct child — always visible
|
|
279
|
+
rows[j].classList.remove('collapsed-hidden');
|
|
280
|
+
} else {
|
|
281
|
+
// deeper descendant — only visible if its own parent-chain is not collapsed
|
|
282
|
+
const isInsideCollapsed = [...collapsedRows].some(ci => {
|
|
283
|
+
if (ci >= j) return false;
|
|
284
|
+
const desc = getDescendantIndices(ci);
|
|
285
|
+
return desc.includes(j);
|
|
286
|
+
});
|
|
287
|
+
if (isInsideCollapsed) {
|
|
288
|
+
rows[j].classList.add('collapsed-hidden');
|
|
289
|
+
} else {
|
|
290
|
+
rows[j].classList.remove('collapsed-hidden');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const toggle = rows[i].querySelector('.collapse-toggle');
|
|
295
|
+
if (toggle) toggle.textContent = '▼';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function toggleRow(i) {
|
|
299
|
+
if (collapsedRows.has(i)) {
|
|
300
|
+
expandRow(i);
|
|
301
|
+
} else {
|
|
302
|
+
collapseRow(i);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function collapseAll() {
|
|
307
|
+
rows.forEach((row, i) => {
|
|
308
|
+
if (row.dataset.hasChildren === 'true') collapseRow(i);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function expandAll() {
|
|
313
|
+
collapsedRows.clear();
|
|
314
|
+
rows.forEach(row => row.classList.remove('collapsed-hidden'));
|
|
315
|
+
rows.forEach(row => {
|
|
316
|
+
const toggle = row.querySelector('.collapse-toggle');
|
|
317
|
+
if (toggle) toggle.textContent = '▼';
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Re-apply collapsed-hidden state (e.g. after filter reset). */
|
|
322
|
+
function renderCollapsedState() {
|
|
323
|
+
rows.forEach(row => row.classList.remove('collapsed-hidden'));
|
|
324
|
+
collapsedRows.forEach(i => {
|
|
325
|
+
getDescendantIndices(i).forEach(j => {
|
|
326
|
+
rows[j].classList.add('collapsed-hidden');
|
|
327
|
+
});
|
|
328
|
+
const toggle = rows[i].querySelector('.collapse-toggle');
|
|
329
|
+
if (toggle) toggle.textContent = '▶';
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Filter ──────────────────────────────────────────────────────────────────
|
|
334
|
+
const depTreeSection = document.getElementById('dep-tree-section');
|
|
335
|
+
const vulnListSection = document.getElementById('vuln-list-section');
|
|
336
|
+
|
|
337
|
+
function applyFilter(filterType, labelText) {
|
|
338
|
+
if (filterType === 'all') {
|
|
339
|
+
rows.forEach(row => row.classList.remove('hidden'));
|
|
340
|
+
filterActive = false;
|
|
341
|
+
depTreeSection.style.display = '';
|
|
342
|
+
vulnListSection.style.display = 'none';
|
|
343
|
+
treeControls.style.display = '';
|
|
344
|
+
notice.classList.remove('visible');
|
|
345
|
+
renderCollapsedState();
|
|
346
|
+
} else if (filterType === 'vuln-deps') {
|
|
347
|
+
// Show plain deduplicated vulnerable-deps list; hide the tree
|
|
348
|
+
depTreeSection.style.display = 'none';
|
|
349
|
+
vulnListSection.style.display = '';
|
|
350
|
+
filterActive = true;
|
|
351
|
+
noticeText.textContent = `Showing ${labelText}`;
|
|
352
|
+
notice.classList.add('visible');
|
|
353
|
+
} else {
|
|
354
|
+
// has-vulns: filter the tree rows in-place
|
|
355
|
+
depTreeSection.style.display = '';
|
|
356
|
+
vulnListSection.style.display = 'none';
|
|
357
|
+
rows.forEach(row => row.classList.remove('collapsed-hidden'));
|
|
358
|
+
rows.forEach(row => {
|
|
359
|
+
const vulnCount = parseInt(row.dataset.vulnCount || '0', 10);
|
|
360
|
+
if (vulnCount > 0) {
|
|
361
|
+
row.classList.remove('hidden');
|
|
362
|
+
} else {
|
|
363
|
+
row.classList.add('hidden');
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
filterActive = true;
|
|
367
|
+
treeControls.style.display = 'none';
|
|
368
|
+
const visible = rows.filter(r => !r.classList.contains('hidden')).length;
|
|
369
|
+
noticeText.textContent = `Showing ${visible} ${labelText}`;
|
|
370
|
+
notice.classList.add('visible');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function resetFilter() {
|
|
375
|
+
document.querySelectorAll('.card').forEach(c => c.classList.remove('active'));
|
|
376
|
+
document.querySelector('.card[data-filter="all"]').classList.add('active');
|
|
377
|
+
applyFilter('all', '');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
document.querySelectorAll('.card[data-filter]').forEach(card => {
|
|
381
|
+
card.addEventListener('click', function () {
|
|
382
|
+
document.querySelectorAll('.card').forEach(c => c.classList.remove('active'));
|
|
383
|
+
this.classList.add('active');
|
|
384
|
+
const filter = this.dataset.filter;
|
|
385
|
+
const label = this.querySelector('.label').textContent;
|
|
386
|
+
applyFilter(filter, label.toLowerCase());
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ── Initialise collapse toggles ─────────────────────────────────────────────
|
|
391
|
+
rows.forEach((row, i) => {
|
|
392
|
+
const nextRow = rows[i + 1];
|
|
393
|
+
if (nextRow && getDepth(nextRow) > getDepth(row)) {
|
|
394
|
+
row.dataset.hasChildren = 'true';
|
|
395
|
+
const toggle = document.createElement('span');
|
|
396
|
+
toggle.className = 'collapse-toggle';
|
|
397
|
+
toggle.textContent = '▼';
|
|
398
|
+
toggle.addEventListener('click', e => { e.stopPropagation(); toggleRow(i); });
|
|
399
|
+
const depNameCell = row.querySelector('.dep-name');
|
|
400
|
+
depNameCell.insertBefore(toggle, depNameCell.firstChild);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
// Default: all expanded (nothing in collapsedRows, no collapsed-hidden classes)
|
|
404
|
+
</script>
|
|
405
|
+
</body>
|
|
406
|
+
</html>
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
transitive module.
|
|
3
|
+
|
|
4
|
+
Resolves transitive dependencies by fetching POMs from Maven Central.
|
|
5
|
+
|
|
6
|
+
:author: Ron Webb
|
|
7
|
+
:since: 1.0.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from lxml import etree
|
|
14
|
+
|
|
15
|
+
from ..models.dependency import Dependency
|
|
16
|
+
from ..parsers.base import RUNTIME_SCOPES
|
|
17
|
+
from ..util.logger import setup_logger
|
|
18
|
+
|
|
19
|
+
__author__ = "Ron Webb"
|
|
20
|
+
__since__ = "1.0.0"
|
|
21
|
+
|
|
22
|
+
_logger = setup_logger(__name__)
|
|
23
|
+
|
|
24
|
+
_MAVEN_CENTRAL = "https://repo1.maven.org/maven2"
|
|
25
|
+
_MAX_DEPTH = 5
|
|
26
|
+
|
|
27
|
+
# Scopes that do NOT propagate transitively to the consumer
|
|
28
|
+
_NON_TRANSITIVE_SCOPES = frozenset({"test", "provided", "system", "import"})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TransitiveResolver:
|
|
32
|
+
"""
|
|
33
|
+
Resolves transitive dependencies by recursively fetching POM files from Maven Central.
|
|
34
|
+
|
|
35
|
+
Uses an in-memory cache to avoid redundant network requests for the same artifact.
|
|
36
|
+
|
|
37
|
+
:author: Ron Webb
|
|
38
|
+
:since: 1.0.0
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, client: httpx.Client | None = None) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Initialise the resolver with an optional shared httpx client.
|
|
44
|
+
|
|
45
|
+
:author: Ron Webb
|
|
46
|
+
:since: 1.0.0
|
|
47
|
+
"""
|
|
48
|
+
self._client = client or httpx.Client(timeout=30)
|
|
49
|
+
self._cache: dict[str, list[Dependency]] = {}
|
|
50
|
+
|
|
51
|
+
def resolve(
|
|
52
|
+
self,
|
|
53
|
+
dependency: Dependency,
|
|
54
|
+
depth: int = 0,
|
|
55
|
+
_visited: set[str] | None = None,
|
|
56
|
+
) -> Dependency:
|
|
57
|
+
"""
|
|
58
|
+
Recursively resolve transitive dependencies for the given dependency.
|
|
59
|
+
|
|
60
|
+
Modifies the dependency in-place by populating
|
|
61
|
+
``dependency.transitive_dependencies`` and setting depth on children.
|
|
62
|
+
|
|
63
|
+
Uses a *visited* set to skip any coordinate that has already been
|
|
64
|
+
processed in the current resolution tree, preventing infinite loops
|
|
65
|
+
caused by circular dependency declarations.
|
|
66
|
+
|
|
67
|
+
:author: Ron Webb
|
|
68
|
+
:since: 1.0.0
|
|
69
|
+
"""
|
|
70
|
+
if _visited is None:
|
|
71
|
+
_visited = set()
|
|
72
|
+
|
|
73
|
+
if depth >= _MAX_DEPTH:
|
|
74
|
+
_logger.debug("Max depth %d reached at %s", _MAX_DEPTH, dependency.coordinates)
|
|
75
|
+
return dependency
|
|
76
|
+
|
|
77
|
+
key = dependency.coordinates
|
|
78
|
+
if key in _visited:
|
|
79
|
+
_logger.debug("Already visited, skipping: %s", key)
|
|
80
|
+
return dependency
|
|
81
|
+
_visited.add(key)
|
|
82
|
+
|
|
83
|
+
if key in self._cache:
|
|
84
|
+
dependency.transitive_dependencies = [
|
|
85
|
+
Dependency(
|
|
86
|
+
group_id=d.group_id,
|
|
87
|
+
artifact_id=d.artifact_id,
|
|
88
|
+
version=d.version,
|
|
89
|
+
scope=d.scope,
|
|
90
|
+
depth=depth + 1,
|
|
91
|
+
)
|
|
92
|
+
for d in self._cache[key]
|
|
93
|
+
]
|
|
94
|
+
return dependency
|
|
95
|
+
|
|
96
|
+
pom_url = self._pom_url(dependency)
|
|
97
|
+
pom_content = self._fetch_pom(pom_url)
|
|
98
|
+
if pom_content is None:
|
|
99
|
+
return dependency
|
|
100
|
+
|
|
101
|
+
direct_children = self._parse_pom_dependencies(pom_content, dependency)
|
|
102
|
+
self._cache[key] = direct_children
|
|
103
|
+
|
|
104
|
+
for child in direct_children:
|
|
105
|
+
child.depth = depth + 1
|
|
106
|
+
self.resolve(child, depth + 1, _visited)
|
|
107
|
+
|
|
108
|
+
dependency.transitive_dependencies = direct_children
|
|
109
|
+
return dependency
|
|
110
|
+
|
|
111
|
+
def resolve_all(self, dependencies: list[Dependency]) -> list[Dependency]:
|
|
112
|
+
"""
|
|
113
|
+
Resolve transitive dependencies for a list of direct dependencies.
|
|
114
|
+
|
|
115
|
+
A single visited set is shared across all top-level dependencies so
|
|
116
|
+
that any coordinate already resolved in a previous branch is not
|
|
117
|
+
fetched or recursed into again.
|
|
118
|
+
|
|
119
|
+
:author: Ron Webb
|
|
120
|
+
:since: 1.0.0
|
|
121
|
+
"""
|
|
122
|
+
visited: set[str] = set()
|
|
123
|
+
for dep in dependencies:
|
|
124
|
+
self.resolve(dep, depth=0, _visited=visited)
|
|
125
|
+
return dependencies
|
|
126
|
+
|
|
127
|
+
def _pom_url(self, dep: Dependency) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Build the Maven Central POM URL for the given dependency.
|
|
130
|
+
|
|
131
|
+
:author: Ron Webb
|
|
132
|
+
:since: 1.0.0
|
|
133
|
+
"""
|
|
134
|
+
return (
|
|
135
|
+
f"{_MAVEN_CENTRAL}/{dep.maven_path}"
|
|
136
|
+
f"/{dep.artifact_id}-{dep.version}.pom"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _fetch_pom(self, url: str) -> bytes | None:
|
|
140
|
+
"""
|
|
141
|
+
Fetch the POM file at the given URL; return raw bytes or None on failure.
|
|
142
|
+
|
|
143
|
+
:author: Ron Webb
|
|
144
|
+
:since: 1.0.0
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
response = self._client.get(url)
|
|
148
|
+
if response.status_code == 200:
|
|
149
|
+
return response.content
|
|
150
|
+
_logger.debug("POM not found (HTTP %d): %s", response.status_code, url)
|
|
151
|
+
except httpx.RequestError as exc:
|
|
152
|
+
_logger.warning("Network error fetching POM %s: %s", url, exc)
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
def _parse_pom_dependencies(
|
|
156
|
+
self, pom_content: bytes, parent: Dependency
|
|
157
|
+
) -> list[Dependency]:
|
|
158
|
+
"""
|
|
159
|
+
Parse a POM's <dependencies> section and return runtime-scoped children.
|
|
160
|
+
|
|
161
|
+
:author: Ron Webb
|
|
162
|
+
:since: 1.0.0
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
root = etree.fromstring(pom_content) # nosec B320 # pylint: disable=c-extension-no-member
|
|
166
|
+
except etree.XMLSyntaxError as exc: # pylint: disable=c-extension-no-member
|
|
167
|
+
_logger.warning("Could not parse POM for %s: %s", parent.coordinates, exc)
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
ns_map = {"m": "http://maven.apache.org/POM/4.0.0"}
|
|
171
|
+
ns_prefix = "m:" if root.tag.startswith("{") else ""
|
|
172
|
+
|
|
173
|
+
def find(node: etree._Element, tag: str) -> etree._Element | None:
|
|
174
|
+
return node.find(f"{ns_prefix}{tag}", ns_map) if ns_prefix else node.find(tag)
|
|
175
|
+
|
|
176
|
+
def text(node: etree._Element, tag: str) -> str:
|
|
177
|
+
child = find(node, tag)
|
|
178
|
+
return child.text.strip() if child is not None and child.text else ""
|
|
179
|
+
|
|
180
|
+
# Extract properties for version variable substitution
|
|
181
|
+
properties = self._extract_pom_properties(root, ns_prefix, ns_map)
|
|
182
|
+
# Add parent version as fallback
|
|
183
|
+
properties.setdefault("project.version", parent.version)
|
|
184
|
+
properties.setdefault("version", parent.version)
|
|
185
|
+
|
|
186
|
+
deps_container = find(root, "dependencies")
|
|
187
|
+
if deps_container is None:
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
children: list[Dependency] = []
|
|
191
|
+
for dep_el in (
|
|
192
|
+
deps_container.findall(f"{ns_prefix}dependency", ns_map)
|
|
193
|
+
if ns_prefix
|
|
194
|
+
else deps_container.findall("dependency")
|
|
195
|
+
):
|
|
196
|
+
child = self._parse_dep_el(dep_el, properties, text)
|
|
197
|
+
if child is not None:
|
|
198
|
+
children.append(child)
|
|
199
|
+
|
|
200
|
+
return children
|
|
201
|
+
|
|
202
|
+
def _extract_pom_properties(
|
|
203
|
+
self,
|
|
204
|
+
root: etree._Element,
|
|
205
|
+
ns_prefix: str,
|
|
206
|
+
ns_map: dict[str, str],
|
|
207
|
+
) -> dict[str, str]:
|
|
208
|
+
"""
|
|
209
|
+
Extract <properties> entries from a POM element.
|
|
210
|
+
|
|
211
|
+
:author: Ron Webb
|
|
212
|
+
:since: 1.0.0
|
|
213
|
+
"""
|
|
214
|
+
props: dict[str, str] = {}
|
|
215
|
+
props_el = (
|
|
216
|
+
root.find("m:properties", ns_map)
|
|
217
|
+
if ns_prefix
|
|
218
|
+
else root.find("properties")
|
|
219
|
+
)
|
|
220
|
+
if props_el is not None:
|
|
221
|
+
for child in props_el:
|
|
222
|
+
if not isinstance(child.tag, str): # skip comments and PIs
|
|
223
|
+
continue
|
|
224
|
+
local = etree.QName(child.tag).localname # pylint: disable=c-extension-no-member
|
|
225
|
+
if child.text:
|
|
226
|
+
props[local] = child.text.strip()
|
|
227
|
+
return props
|
|
228
|
+
|
|
229
|
+
def _parse_dep_el(
|
|
230
|
+
self,
|
|
231
|
+
dep_el: etree._Element,
|
|
232
|
+
properties: dict[str, str],
|
|
233
|
+
text_fn, # callable is fine; typing via Callable is imported from collections.abc
|
|
234
|
+
) -> Dependency | None:
|
|
235
|
+
"""
|
|
236
|
+
Parse a single dependency XML element into a Dependency, or return None to skip it.
|
|
237
|
+
|
|
238
|
+
:author: Ron Webb
|
|
239
|
+
:since: 1.0.0
|
|
240
|
+
"""
|
|
241
|
+
group_id = self._resolve_property(text_fn(dep_el, "groupId"), properties)
|
|
242
|
+
artifact_id = self._resolve_property(text_fn(dep_el, "artifactId"), properties)
|
|
243
|
+
version = self._resolve_property(text_fn(dep_el, "version"), properties)
|
|
244
|
+
scope = text_fn(dep_el, "scope") or "compile"
|
|
245
|
+
optional_raw = text_fn(dep_el, "optional")
|
|
246
|
+
optional = optional_raw.lower() == "true"
|
|
247
|
+
|
|
248
|
+
if not group_id or not artifact_id or not version:
|
|
249
|
+
return None
|
|
250
|
+
if scope in _NON_TRANSITIVE_SCOPES:
|
|
251
|
+
return None
|
|
252
|
+
if optional:
|
|
253
|
+
return None
|
|
254
|
+
if scope not in RUNTIME_SCOPES:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
return Dependency(
|
|
258
|
+
group_id=group_id,
|
|
259
|
+
artifact_id=artifact_id,
|
|
260
|
+
version=version,
|
|
261
|
+
scope=scope,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def _resolve_property(self, value: str, properties: dict[str, str]) -> str:
|
|
265
|
+
"""
|
|
266
|
+
Replace ${key} placeholders using the provided properties dictionary.
|
|
267
|
+
|
|
268
|
+
:author: Ron Webb
|
|
269
|
+
:since: 1.0.0
|
|
270
|
+
"""
|
|
271
|
+
if not value or "${" not in value:
|
|
272
|
+
return value
|
|
273
|
+
for match in re.finditer(r"\$\{([^}]+)\}", value):
|
|
274
|
+
key = match.group(1)
|
|
275
|
+
value = value.replace(match.group(0), properties.get(key, ""))
|
|
276
|
+
return value
|