dependencyiq 2.0.0 → 2.2.0

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.
@@ -1,199 +1,199 @@
1
- /**
2
- * Fleet Dashboard: the cross-project view GitHub's org-wide "Security
3
- * Overview" is the closest public precedent for — every project in a
4
- * GitLab group, in one page, sorted by real risk.
5
- *
6
- * Still a static page (GitLab Pages, no backend) — the data is a
7
- * build-time snapshot (fleetAggregator.js), not a live query. What's
8
- * genuinely new compared to the per-project dashboard is real client-side
9
- * interactivity over that snapshot: sortable columns and a search box,
10
- * implemented as a small inline <script> with no external library, so
11
- * the "nothing to fetch, nothing that can 404" property is preserved —
12
- * the interactivity works entirely on data already embedded in the page.
13
- *
14
- * The NOT ONBOARDED section is deliberate, not an afterthought: a
15
- * project with no dependencyiq-report.json artifact has never run this
16
- * tool, which is a meaningfully different fact from "ran it and found
17
- * zero issues." Collapsing the two would make fleet-wide adoption
18
- * progress invisible.
19
- */
20
-
21
- const { SHARED_STYLES, escapeHtml, priorityColor } = require('./dashboardGenerator');
22
-
23
- function fmtDate(iso) {
24
- if (!iso) return 'unknown';
25
- return new Date(iso).toISOString().slice(0, 16).replace('T', ' ') + ' UTC';
26
- }
27
-
28
- function renderOnboardedRow(entry) {
29
- const { project, snapshot } = entry;
30
- const top = snapshot.topFindings?.[0];
31
- const topLabel = top ? `${escapeHtml(top.package)} (${top.priority})` : 'none';
32
- return `<tr data-name="${escapeHtml(project.pathWithNamespace.toLowerCase())}"
33
- data-score="${snapshot.topRiskScore || 0}"
34
- data-findings="${snapshot.totalFindings || 0}"
35
- data-urgent="${snapshot.urgentCount || 0}">
36
- <td><a href="https://gitlab.com/${escapeHtml(project.pathWithNamespace)}" target="_blank" rel="noopener">${escapeHtml(project.pathWithNamespace)}</a></td>
37
- <td>${snapshot.totalFindings || 0}</td>
38
- <td>${snapshot.urgentCount || 0}</td>
39
- <td>${snapshot.highCount || 0}</td>
40
- <td>${snapshot.unusedCount || 0}</td>
41
- <td><span style="color:${priorityColor(top?.priority)}; font-weight:700;">${snapshot.topRiskScore || 0}</span></td>
42
- <td>${topLabel}</td>
43
- <td>${fmtDate(snapshot.generatedAt)}</td>
44
- </tr>`;
45
- }
46
-
47
- function renderNotOnboardedRow(entry) {
48
- return `<tr>
49
- <td><a href="https://gitlab.com/${escapeHtml(entry.project.pathWithNamespace)}" target="_blank" rel="noopener">${escapeHtml(entry.project.pathWithNamespace)}</a></td>
50
- <td colspan="6" style="color: var(--text-muted);">${escapeHtml(entry.reason)}</td>
51
- </tr>`;
52
- }
53
-
54
- /**
55
- * @param {Object} report - { available, rollup } from fleetAggregator.buildFleetReport,
56
- * or { available: false, reason } when the report itself couldn't be built
57
- * @param {Object} meta - { groupId, groupName }
58
- * @returns {string} a complete, self-contained HTML document
59
- */
60
- function generateFleetDashboardHtml(report, meta = {}) {
61
- if (!report.available) {
62
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
63
- <title>DependencyIQ Fleet Dashboard — Unavailable</title>
64
- <style>${SHARED_STYLES}</style></head>
65
- <body><div style="max-width:640px;margin:4rem auto;padding:0 1.5rem;">
66
- <h1>Fleet Dashboard unavailable</h1>
67
- <p style="color: var(--text-muted);">${escapeHtml(report.reason)}</p>
68
- </div></body></html>`;
69
- }
70
-
71
- const { rollup } = report;
72
- const onboardedRows = rollup.onboarded.map(renderOnboardedRow).join('\n');
73
- const notOnboardedRows = rollup.notOnboarded.map(renderNotOnboardedRow).join('\n');
74
- const adoptionPercent = rollup.totalProjects > 0 ? Math.round((rollup.onboardedCount / rollup.totalProjects) * 100) : 0;
75
-
76
- return `<!DOCTYPE html>
77
- <html lang="en">
78
- <head>
79
- <meta charset="UTF-8">
80
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
81
- <title>DependencyIQ Fleet Dashboard${meta.groupName ? ` — ${escapeHtml(meta.groupName)}` : ''}</title>
82
- <style>
83
- ${SHARED_STYLES}
84
- .page { max-width: 1180px; margin: 0 auto; padding: 0 1.5rem 3rem; }
85
- header.page-header { padding: 1.75rem 0 1.25rem; border-bottom: 1px solid var(--border); margin-bottom: 1.75rem; }
86
- .brand-title { font-size: 1.25rem; font-weight: 700; }
87
- .brand-subtitle { color: var(--text-muted); font-size: 0.85rem; margin-top: 0.15rem; }
88
- .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.85rem; margin-bottom: 1.75rem; }
89
- .stat { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius); padding: 1rem 1.1rem; }
90
- .stat .num { font-size: 1.7rem; font-weight: 700; line-height: 1.1; }
91
- .stat .label { color: var(--text-muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; margin-top: 0.3rem; }
92
- .toolbar { display: flex; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: center; }
93
- #search { flex: 1 1 240px; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 0.55rem 0.85rem; border-radius: var(--radius-sm); font-size: 0.9rem; }
94
- table { width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
95
- th, td { padding: 0.65rem 0.85rem; text-align: left; border-bottom: 1px solid var(--border-soft); font-size: 0.88rem; }
96
- th { cursor: pointer; user-select: none; color: var(--text-muted); font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.03em; }
97
- th:hover { color: var(--text); }
98
- th.sorted-asc::after { content: ' \\2191'; }
99
- th.sorted-desc::after { content: ' \\2193'; }
100
- tr:last-child td { border-bottom: none; }
101
- .section-title { font-size: 0.95rem; font-weight: 600; margin: 2rem 0 0.75rem; }
102
- .empty-note { color: var(--text-muted); font-size: 0.85rem; padding: 1rem; }
103
- </style>
104
- </head>
105
- <body>
106
- <div class="page">
107
- <header class="page-header">
108
- <div class="brand-title">DependencyIQ Fleet Dashboard${meta.groupName ? ` — ${escapeHtml(meta.groupName)}` : ''}</div>
109
- <div class="brand-subtitle">Generated ${fmtDate(rollup.generatedAt)} · build-time snapshot from each project's own analyze_vulnerabilities CI artifact, not a live cross-project query</div>
110
- </header>
111
-
112
- <div class="stats">
113
- <div class="stat"><div class="num">${rollup.onboardedCount}/${rollup.totalProjects}</div><div class="label">Projects onboarded (${adoptionPercent}%)</div></div>
114
- <div class="stat"><div class="num">${rollup.totalFindings}</div><div class="label">Total findings, fleet-wide</div></div>
115
- <div class="stat"><div class="num">${rollup.urgentCount}</div><div class="label">Urgent findings</div></div>
116
- <div class="stat"><div class="num">${rollup.highCount}</div><div class="label">High findings</div></div>
117
- <div class="stat"><div class="num">${rollup.unusedCount}</div><div class="label">Removable (unused) deps</div></div>
118
- </div>
119
-
120
- <div class="toolbar">
121
- <input id="search" type="text" placeholder="Filter by project path...">
122
- </div>
123
-
124
- <table id="fleet-table">
125
- <thead>
126
- <tr>
127
- <th data-key="name">Project</th>
128
- <th data-key="findings">Findings</th>
129
- <th data-key="urgent">Urgent</th>
130
- <th>High</th>
131
- <th>Unused</th>
132
- <th data-key="score">Top risk score</th>
133
- <th>Top finding</th>
134
- <th>Last scanned</th>
135
- </tr>
136
- </thead>
137
- <tbody>
138
- ${onboardedRows || '<tr><td colspan="8" class="empty-note">No onboarded projects yet — see "Not yet onboarded" below.</td></tr>'}
139
- </tbody>
140
- </table>
141
-
142
- <div class="section-title">Not yet onboarded (${rollup.notOnboardedCount})</div>
143
- <table>
144
- <tbody>
145
- ${notOnboardedRows || '<tr><td class="empty-note">Every project in this group has onboarded DependencyIQ.</td></tr>'}
146
- </tbody>
147
- </table>
148
- </div>
149
-
150
- <script>
151
- (function () {
152
- var table = document.getElementById('fleet-table');
153
- var tbody = table.querySelector('tbody');
154
- var searchBox = document.getElementById('search');
155
- var sortState = { key: null, dir: 1 };
156
-
157
- function applySearch() {
158
- var q = searchBox.value.trim().toLowerCase();
159
- Array.prototype.forEach.call(tbody.querySelectorAll('tr'), function (row) {
160
- var name = row.dataset.name || '';
161
- row.style.display = (!q || name.indexOf(q) !== -1) ? '' : 'none';
162
- });
163
- }
164
-
165
- function applySort(key) {
166
- var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr')).filter(function (r) { return r.dataset.name !== undefined; });
167
- var dir = (sortState.key === key) ? -sortState.dir : 1;
168
- sortState = { key: key, dir: dir };
169
-
170
- rows.sort(function (a, b) {
171
- var av = key === 'name' ? a.dataset.name : Number(a.dataset[key] || 0);
172
- var bv = key === 'name' ? b.dataset.name : Number(b.dataset[key] || 0);
173
- if (av < bv) return -1 * dir;
174
- if (av > bv) return 1 * dir;
175
- return 0;
176
- });
177
-
178
- rows.forEach(function (r) { tbody.appendChild(r); });
179
-
180
- Array.prototype.forEach.call(table.querySelectorAll('th'), function (th) {
181
- th.classList.remove('sorted-asc', 'sorted-desc');
182
- });
183
- var activeTh = table.querySelector('th[data-key="' + key + '"]');
184
- if (activeTh) activeTh.classList.add(dir === 1 ? 'sorted-asc' : 'sorted-desc');
185
- }
186
-
187
- searchBox.addEventListener('input', applySearch);
188
- Array.prototype.forEach.call(table.querySelectorAll('th[data-key]'), function (th) {
189
- th.addEventListener('click', function () { applySort(th.dataset.key); });
190
- });
191
-
192
- applySort('score');
193
- })();
194
- </script>
195
- </body>
196
- </html>`;
197
- }
198
-
199
- module.exports = { generateFleetDashboardHtml };
1
+ /**
2
+ * Fleet Dashboard: the cross-project view GitHub's org-wide "Security
3
+ * Overview" is the closest public precedent for — every project in a
4
+ * GitLab group, in one page, sorted by real risk.
5
+ *
6
+ * Still a static page (GitLab Pages, no backend) — the data is a
7
+ * build-time snapshot (fleetAggregator.js), not a live query. What's
8
+ * genuinely new compared to the per-project dashboard is real client-side
9
+ * interactivity over that snapshot: sortable columns and a search box,
10
+ * implemented as a small inline <script> with no external library, so
11
+ * the "nothing to fetch, nothing that can 404" property is preserved —
12
+ * the interactivity works entirely on data already embedded in the page.
13
+ *
14
+ * The NOT ONBOARDED section is deliberate, not an afterthought: a
15
+ * project with no dependencyiq-report.json artifact has never run this
16
+ * tool, which is a meaningfully different fact from "ran it and found
17
+ * zero issues." Collapsing the two would make fleet-wide adoption
18
+ * progress invisible.
19
+ */
20
+
21
+ const { SHARED_STYLES, escapeHtml, priorityColor } = require('./dashboardGenerator');
22
+
23
+ function fmtDate(iso) {
24
+ if (!iso) return 'unknown';
25
+ return new Date(iso).toISOString().slice(0, 16).replace('T', ' ') + ' UTC';
26
+ }
27
+
28
+ function renderOnboardedRow(entry) {
29
+ const { project, snapshot } = entry;
30
+ const top = snapshot.topFindings?.[0];
31
+ const topLabel = top ? `${escapeHtml(top.package)} (${top.priority})` : 'none';
32
+ return `<tr data-name="${escapeHtml(project.pathWithNamespace.toLowerCase())}"
33
+ data-score="${snapshot.topRiskScore || 0}"
34
+ data-findings="${snapshot.totalFindings || 0}"
35
+ data-urgent="${snapshot.urgentCount || 0}">
36
+ <td><a href="https://gitlab.com/${escapeHtml(project.pathWithNamespace)}" target="_blank" rel="noopener">${escapeHtml(project.pathWithNamespace)}</a></td>
37
+ <td>${snapshot.totalFindings || 0}</td>
38
+ <td>${snapshot.urgentCount || 0}</td>
39
+ <td>${snapshot.highCount || 0}</td>
40
+ <td>${snapshot.unusedCount || 0}</td>
41
+ <td><span style="color:${priorityColor(top?.priority)}; font-weight:700;">${snapshot.topRiskScore || 0}</span></td>
42
+ <td>${topLabel}</td>
43
+ <td>${fmtDate(snapshot.generatedAt)}</td>
44
+ </tr>`;
45
+ }
46
+
47
+ function renderNotOnboardedRow(entry) {
48
+ return `<tr>
49
+ <td><a href="https://gitlab.com/${escapeHtml(entry.project.pathWithNamespace)}" target="_blank" rel="noopener">${escapeHtml(entry.project.pathWithNamespace)}</a></td>
50
+ <td colspan="6" style="color: var(--text-muted);">${escapeHtml(entry.reason)}</td>
51
+ </tr>`;
52
+ }
53
+
54
+ /**
55
+ * @param {Object} report - { available, rollup } from fleetAggregator.buildFleetReport,
56
+ * or { available: false, reason } when the report itself couldn't be built
57
+ * @param {Object} meta - { groupId, groupName }
58
+ * @returns {string} a complete, self-contained HTML document
59
+ */
60
+ function generateFleetDashboardHtml(report, meta = {}) {
61
+ if (!report.available) {
62
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
63
+ <title>DependencyIQ Fleet Dashboard — Unavailable</title>
64
+ <style>${SHARED_STYLES}</style></head>
65
+ <body><div style="max-width:640px;margin:4rem auto;padding:0 1.5rem;">
66
+ <h1>Fleet Dashboard unavailable</h1>
67
+ <p style="color: var(--text-muted);">${escapeHtml(report.reason)}</p>
68
+ </div></body></html>`;
69
+ }
70
+
71
+ const { rollup } = report;
72
+ const onboardedRows = rollup.onboarded.map(renderOnboardedRow).join('\n');
73
+ const notOnboardedRows = rollup.notOnboarded.map(renderNotOnboardedRow).join('\n');
74
+ const adoptionPercent = rollup.totalProjects > 0 ? Math.round((rollup.onboardedCount / rollup.totalProjects) * 100) : 0;
75
+
76
+ return `<!DOCTYPE html>
77
+ <html lang="en">
78
+ <head>
79
+ <meta charset="UTF-8">
80
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
81
+ <title>DependencyIQ Fleet Dashboard${meta.groupName ? ` — ${escapeHtml(meta.groupName)}` : ''}</title>
82
+ <style>
83
+ ${SHARED_STYLES}
84
+ .page { max-width: 1180px; margin: 0 auto; padding: 0 1.5rem 3rem; }
85
+ header.page-header { padding: 1.75rem 0 1.25rem; border-bottom: 1px solid var(--border); margin-bottom: 1.75rem; }
86
+ .brand-title { font-size: 1.25rem; font-weight: 700; }
87
+ .brand-subtitle { color: var(--text-muted); font-size: 0.85rem; margin-top: 0.15rem; }
88
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.85rem; margin-bottom: 1.75rem; }
89
+ .stat { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius); padding: 1rem 1.1rem; }
90
+ .stat .num { font-size: 1.7rem; font-weight: 700; line-height: 1.1; }
91
+ .stat .label { color: var(--text-muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; margin-top: 0.3rem; }
92
+ .toolbar { display: flex; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: center; }
93
+ #search { flex: 1 1 240px; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 0.55rem 0.85rem; border-radius: var(--radius-sm); font-size: 0.9rem; }
94
+ table { width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
95
+ th, td { padding: 0.65rem 0.85rem; text-align: left; border-bottom: 1px solid var(--border-soft); font-size: 0.88rem; }
96
+ th { cursor: pointer; user-select: none; color: var(--text-muted); font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.03em; }
97
+ th:hover { color: var(--text); }
98
+ th.sorted-asc::after { content: ' \\2191'; }
99
+ th.sorted-desc::after { content: ' \\2193'; }
100
+ tr:last-child td { border-bottom: none; }
101
+ .section-title { font-size: 0.95rem; font-weight: 600; margin: 2rem 0 0.75rem; }
102
+ .empty-note { color: var(--text-muted); font-size: 0.85rem; padding: 1rem; }
103
+ </style>
104
+ </head>
105
+ <body>
106
+ <div class="page">
107
+ <header class="page-header">
108
+ <div class="brand-title">DependencyIQ Fleet Dashboard${meta.groupName ? ` — ${escapeHtml(meta.groupName)}` : ''}</div>
109
+ <div class="brand-subtitle">Generated ${fmtDate(rollup.generatedAt)} · build-time snapshot from each project's own analyze_vulnerabilities CI artifact, not a live cross-project query</div>
110
+ </header>
111
+
112
+ <div class="stats">
113
+ <div class="stat"><div class="num">${rollup.onboardedCount}/${rollup.totalProjects}</div><div class="label">Projects onboarded (${adoptionPercent}%)</div></div>
114
+ <div class="stat"><div class="num">${rollup.totalFindings}</div><div class="label">Total findings, fleet-wide</div></div>
115
+ <div class="stat"><div class="num">${rollup.urgentCount}</div><div class="label">Urgent findings</div></div>
116
+ <div class="stat"><div class="num">${rollup.highCount}</div><div class="label">High findings</div></div>
117
+ <div class="stat"><div class="num">${rollup.unusedCount}</div><div class="label">Removable (unused) deps</div></div>
118
+ </div>
119
+
120
+ <div class="toolbar">
121
+ <input id="search" type="text" placeholder="Filter by project path...">
122
+ </div>
123
+
124
+ <table id="fleet-table">
125
+ <thead>
126
+ <tr>
127
+ <th data-key="name">Project</th>
128
+ <th data-key="findings">Findings</th>
129
+ <th data-key="urgent">Urgent</th>
130
+ <th>High</th>
131
+ <th>Unused</th>
132
+ <th data-key="score">Top risk score</th>
133
+ <th>Top finding</th>
134
+ <th>Last scanned</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ ${onboardedRows || '<tr><td colspan="8" class="empty-note">No onboarded projects yet — see "Not yet onboarded" below.</td></tr>'}
139
+ </tbody>
140
+ </table>
141
+
142
+ <div class="section-title">Not yet onboarded (${rollup.notOnboardedCount})</div>
143
+ <table>
144
+ <tbody>
145
+ ${notOnboardedRows || '<tr><td class="empty-note">Every project in this group has onboarded DependencyIQ.</td></tr>'}
146
+ </tbody>
147
+ </table>
148
+ </div>
149
+
150
+ <script>
151
+ (function () {
152
+ var table = document.getElementById('fleet-table');
153
+ var tbody = table.querySelector('tbody');
154
+ var searchBox = document.getElementById('search');
155
+ var sortState = { key: null, dir: 1 };
156
+
157
+ function applySearch() {
158
+ var q = searchBox.value.trim().toLowerCase();
159
+ Array.prototype.forEach.call(tbody.querySelectorAll('tr'), function (row) {
160
+ var name = row.dataset.name || '';
161
+ row.style.display = (!q || name.indexOf(q) !== -1) ? '' : 'none';
162
+ });
163
+ }
164
+
165
+ function applySort(key) {
166
+ var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr')).filter(function (r) { return r.dataset.name !== undefined; });
167
+ var dir = (sortState.key === key) ? -sortState.dir : 1;
168
+ sortState = { key: key, dir: dir };
169
+
170
+ rows.sort(function (a, b) {
171
+ var av = key === 'name' ? a.dataset.name : Number(a.dataset[key] || 0);
172
+ var bv = key === 'name' ? b.dataset.name : Number(b.dataset[key] || 0);
173
+ if (av < bv) return -1 * dir;
174
+ if (av > bv) return 1 * dir;
175
+ return 0;
176
+ });
177
+
178
+ rows.forEach(function (r) { tbody.appendChild(r); });
179
+
180
+ Array.prototype.forEach.call(table.querySelectorAll('th'), function (th) {
181
+ th.classList.remove('sorted-asc', 'sorted-desc');
182
+ });
183
+ var activeTh = table.querySelector('th[data-key="' + key + '"]');
184
+ if (activeTh) activeTh.classList.add(dir === 1 ? 'sorted-asc' : 'sorted-desc');
185
+ }
186
+
187
+ searchBox.addEventListener('input', applySearch);
188
+ Array.prototype.forEach.call(table.querySelectorAll('th[data-key]'), function (th) {
189
+ th.addEventListener('click', function () { applySort(th.dataset.key); });
190
+ });
191
+
192
+ applySort('score');
193
+ })();
194
+ </script>
195
+ </body>
196
+ </html>`;
197
+ }
198
+
199
+ module.exports = { generateFleetDashboardHtml };
@@ -1,103 +1,103 @@
1
- /**
2
- * Fleet snapshot: the per-project artifact a Fleet Dashboard aggregates
3
- * across a whole GitLab group.
4
- *
5
- * A static page can only ever show a build-time snapshot, never a live
6
- * query across projects it doesn't own — so each project's own CI run
7
- * writes one small, honest JSON file (`dependencyiq-report.json`) as a
8
- * normal job artifact, and the Fleet Dashboard (fleetAggregator.js) later
9
- * fetches the latest one from every project in a group via GitLab's Job
10
- * Artifacts API. A project that has never produced this file has never
11
- * "onboarded" DependencyIQ — that is a real, meaningful distinction from
12
- * "onboarded and found zero issues," and the aggregator never collapses
13
- * the two.
14
- *
15
- * Deliberately small: full per-file evidence/affectedFiles arrays stay
16
- * in the per-project dashboard (dashboardGenerator.js); this snapshot
17
- * only carries what a cross-project rollup table actually needs, so
18
- * fetching it from N projects stays cheap.
19
- */
20
-
21
- const fs = require('fs');
22
-
23
- const SCHEMA_VERSION = 1;
24
- const TOP_FINDINGS_LIMIT = 5;
25
-
26
- /**
27
- * @param {Object} analyzeResult - the result of agent.js's analyzeRepository()
28
- * @param {Object} meta - { projectId, projectPath, ecosystems }
29
- * @returns {Object} a compact, fleet-aggregatable snapshot
30
- */
31
- function buildFleetSnapshot(analyzeResult, meta = {}) {
32
- const vulnerabilities = analyzeResult?.vulnerabilities || [];
33
- const execSummary = buildSummaryFields(vulnerabilities);
34
-
35
- const topFindings = [...vulnerabilities]
36
- .sort((a, b) => (b.riskScore?.score || 0) - (a.riskScore?.score || 0))
37
- .slice(0, TOP_FINDINGS_LIMIT)
38
- .map(v => ({
39
- package: v.package,
40
- ecosystem: v.ecosystem,
41
- severity: v.severity,
42
- cvss: v.cvss,
43
- score: v.riskScore?.score ?? null,
44
- priority: v.riskScore?.priority || 'UNSCORED',
45
- recommendation: v.recommendation || null,
46
- exposureDataSource: v.riskScore?.exposureDataSource || 'unavailable',
47
- isDirect: v.dependencyChain?.available ? v.dependencyChain.isDirect : null,
48
- }));
49
-
50
- return {
51
- schemaVersion: SCHEMA_VERSION,
52
- generatedAt: new Date().toISOString(),
53
- projectId: meta.projectId || null,
54
- projectPath: meta.projectPath || null,
55
- ecosystems: meta.ecosystems || [],
56
- ...execSummary,
57
- topFindings,
58
- };
59
- }
60
-
61
- function buildSummaryFields(vulnerabilities) {
62
- const priorityOf = v => v.riskScore?.priority || 'UNSCORED';
63
- return {
64
- totalFindings: vulnerabilities.length,
65
- urgentCount: vulnerabilities.filter(v => priorityOf(v) === 'URGENT').length,
66
- highCount: vulnerabilities.filter(v => priorityOf(v) === 'HIGH').length,
67
- unusedCount: vulnerabilities.filter(v => priorityOf(v) === 'UNUSED').length,
68
- topRiskScore: vulnerabilities.reduce((max, v) => Math.max(max, v.riskScore?.score || 0), 0),
69
- };
70
- }
71
-
72
- /**
73
- * @param {Object} snapshot - from buildFleetSnapshot()
74
- * @param {string} outPath - file path to write, e.g. "dependencyiq-report.json"
75
- */
76
- function writeFleetSnapshot(snapshot, outPath) {
77
- fs.writeFileSync(outPath, JSON.stringify(snapshot, null, 2) + '\n');
78
- }
79
-
80
- /**
81
- * Validate that a fetched JSON blob is actually a snapshot this version
82
- * of the aggregator understands, rather than trusting it blindly — a
83
- * stale artifact from a future/incompatible schema should report
84
- * unavailable, not be silently misread.
85
- * @returns {Object|null} the snapshot if valid, otherwise null
86
- */
87
- function parseFleetSnapshot(rawJson) {
88
- let parsed;
89
- try {
90
- parsed = JSON.parse(rawJson);
91
- } catch {
92
- return null;
93
- }
94
- if (!parsed || parsed.schemaVersion !== SCHEMA_VERSION) return null;
95
- return parsed;
96
- }
97
-
98
- module.exports = {
99
- SCHEMA_VERSION,
100
- buildFleetSnapshot,
101
- writeFleetSnapshot,
102
- parseFleetSnapshot,
103
- };
1
+ /**
2
+ * Fleet snapshot: the per-project artifact a Fleet Dashboard aggregates
3
+ * across a whole GitLab group.
4
+ *
5
+ * A static page can only ever show a build-time snapshot, never a live
6
+ * query across projects it doesn't own — so each project's own CI run
7
+ * writes one small, honest JSON file (`dependencyiq-report.json`) as a
8
+ * normal job artifact, and the Fleet Dashboard (fleetAggregator.js) later
9
+ * fetches the latest one from every project in a group via GitLab's Job
10
+ * Artifacts API. A project that has never produced this file has never
11
+ * "onboarded" DependencyIQ — that is a real, meaningful distinction from
12
+ * "onboarded and found zero issues," and the aggregator never collapses
13
+ * the two.
14
+ *
15
+ * Deliberately small: full per-file evidence/affectedFiles arrays stay
16
+ * in the per-project dashboard (dashboardGenerator.js); this snapshot
17
+ * only carries what a cross-project rollup table actually needs, so
18
+ * fetching it from N projects stays cheap.
19
+ */
20
+
21
+ const fs = require('fs');
22
+
23
+ const SCHEMA_VERSION = 1;
24
+ const TOP_FINDINGS_LIMIT = 5;
25
+
26
+ /**
27
+ * @param {Object} analyzeResult - the result of agent.js's analyzeRepository()
28
+ * @param {Object} meta - { projectId, projectPath, ecosystems }
29
+ * @returns {Object} a compact, fleet-aggregatable snapshot
30
+ */
31
+ function buildFleetSnapshot(analyzeResult, meta = {}) {
32
+ const vulnerabilities = analyzeResult?.vulnerabilities || [];
33
+ const execSummary = buildSummaryFields(vulnerabilities);
34
+
35
+ const topFindings = [...vulnerabilities]
36
+ .sort((a, b) => (b.riskScore?.score || 0) - (a.riskScore?.score || 0))
37
+ .slice(0, TOP_FINDINGS_LIMIT)
38
+ .map(v => ({
39
+ package: v.package,
40
+ ecosystem: v.ecosystem,
41
+ severity: v.severity,
42
+ cvss: v.cvss,
43
+ score: v.riskScore?.score ?? null,
44
+ priority: v.riskScore?.priority || 'UNSCORED',
45
+ recommendation: v.recommendation || null,
46
+ exposureDataSource: v.riskScore?.exposureDataSource || 'unavailable',
47
+ isDirect: v.dependencyChain?.available ? v.dependencyChain.isDirect : null,
48
+ }));
49
+
50
+ return {
51
+ schemaVersion: SCHEMA_VERSION,
52
+ generatedAt: new Date().toISOString(),
53
+ projectId: meta.projectId || null,
54
+ projectPath: meta.projectPath || null,
55
+ ecosystems: meta.ecosystems || [],
56
+ ...execSummary,
57
+ topFindings,
58
+ };
59
+ }
60
+
61
+ function buildSummaryFields(vulnerabilities) {
62
+ const priorityOf = v => v.riskScore?.priority || 'UNSCORED';
63
+ return {
64
+ totalFindings: vulnerabilities.length,
65
+ urgentCount: vulnerabilities.filter(v => priorityOf(v) === 'URGENT').length,
66
+ highCount: vulnerabilities.filter(v => priorityOf(v) === 'HIGH').length,
67
+ unusedCount: vulnerabilities.filter(v => priorityOf(v) === 'UNUSED').length,
68
+ topRiskScore: vulnerabilities.reduce((max, v) => Math.max(max, v.riskScore?.score || 0), 0),
69
+ };
70
+ }
71
+
72
+ /**
73
+ * @param {Object} snapshot - from buildFleetSnapshot()
74
+ * @param {string} outPath - file path to write, e.g. "dependencyiq-report.json"
75
+ */
76
+ function writeFleetSnapshot(snapshot, outPath) {
77
+ fs.writeFileSync(outPath, JSON.stringify(snapshot, null, 2) + '\n');
78
+ }
79
+
80
+ /**
81
+ * Validate that a fetched JSON blob is actually a snapshot this version
82
+ * of the aggregator understands, rather than trusting it blindly — a
83
+ * stale artifact from a future/incompatible schema should report
84
+ * unavailable, not be silently misread.
85
+ * @returns {Object|null} the snapshot if valid, otherwise null
86
+ */
87
+ function parseFleetSnapshot(rawJson) {
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(rawJson);
91
+ } catch {
92
+ return null;
93
+ }
94
+ if (!parsed || parsed.schemaVersion !== SCHEMA_VERSION) return null;
95
+ return parsed;
96
+ }
97
+
98
+ module.exports = {
99
+ SCHEMA_VERSION,
100
+ buildFleetSnapshot,
101
+ writeFleetSnapshot,
102
+ parseFleetSnapshot,
103
+ };