agentshield-sdk 7.2.0 → 7.3.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.
- package/CHANGELOG.md +90 -1
- package/README.md +38 -5
- package/bin/agent-shield.js +19 -0
- package/package.json +8 -4
- package/src/attack-genome.js +536 -0
- package/src/attack-replay.js +246 -0
- package/src/audit.js +619 -0
- package/src/behavioral-dna.js +762 -0
- package/src/circuit-breaker.js +321 -321
- package/src/compliance-authority.js +803 -0
- package/src/detector-core.js +3 -3
- package/src/distributed.js +403 -359
- package/src/errors.js +9 -0
- package/src/evolution-simulator.js +650 -0
- package/src/flight-recorder.js +379 -0
- package/src/fuzzer.js +764 -764
- package/src/herd-immunity.js +521 -0
- package/src/index.js +28 -11
- package/src/intent-firewall.js +775 -0
- package/src/main.js +135 -2
- package/src/mcp-security-runtime.js +36 -10
- package/src/mcp-server.js +12 -8
- package/src/middleware.js +306 -208
- package/src/multi-agent.js +421 -404
- package/src/pii.js +404 -390
- package/src/real-attack-datasets.js +246 -0
- package/src/report-generator.js +640 -0
- package/src/soc-dashboard.js +394 -0
- package/src/stream-scanner.js +34 -4
- package/src/supply-chain.js +667 -0
- package/src/testing.js +505 -505
- package/src/threat-intel-federation.js +343 -0
- package/src/utils.js +199 -83
- package/types/index.d.ts +374 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield - Visual HTML Security Report Generator
|
|
5
|
+
*
|
|
6
|
+
* Produces a Lighthouse-style HTML report from a SecurityAudit result.
|
|
7
|
+
* Self-contained HTML with inline CSS and SVG. No external dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { SecurityAudit } = require('./audit');
|
|
11
|
+
* const { generateHTMLReport, generateReportFile } = require('./report-generator');
|
|
12
|
+
* const report = new SecurityAudit().run();
|
|
13
|
+
* generateReportFile(report, 'shield-report.html');
|
|
14
|
+
*
|
|
15
|
+
* @module report-generator
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { getGrade } = require('./utils');
|
|
21
|
+
|
|
22
|
+
// =========================================================================
|
|
23
|
+
// Color helpers
|
|
24
|
+
// =========================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the gauge color based on score.
|
|
28
|
+
* @param {number} score - 0-100
|
|
29
|
+
* @returns {string} CSS color
|
|
30
|
+
*/
|
|
31
|
+
function getScoreColor(score) {
|
|
32
|
+
if (score > 80) return '#0cce6b';
|
|
33
|
+
if (score > 50) return '#ffa400';
|
|
34
|
+
return '#ff4e42';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the severity badge color.
|
|
39
|
+
* @param {string} severity
|
|
40
|
+
* @returns {string} CSS color
|
|
41
|
+
*/
|
|
42
|
+
function getSeverityColor(severity) {
|
|
43
|
+
const map = {
|
|
44
|
+
critical: '#ff4e42',
|
|
45
|
+
high: '#ff6d3a',
|
|
46
|
+
medium: '#ffa400',
|
|
47
|
+
low: '#0cce6b',
|
|
48
|
+
};
|
|
49
|
+
return map[severity] || '#888';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the severity background color (lighter).
|
|
54
|
+
* @param {string} severity
|
|
55
|
+
* @returns {string} CSS color
|
|
56
|
+
*/
|
|
57
|
+
function getSeverityBg(severity) {
|
|
58
|
+
const map = {
|
|
59
|
+
critical: 'rgba(255,78,66,0.15)',
|
|
60
|
+
high: 'rgba(255,109,58,0.15)',
|
|
61
|
+
medium: 'rgba(255,164,0,0.15)',
|
|
62
|
+
low: 'rgba(12,206,107,0.15)',
|
|
63
|
+
};
|
|
64
|
+
return map[severity] || 'rgba(136,136,136,0.15)';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =========================================================================
|
|
68
|
+
// SVG generators
|
|
69
|
+
// =========================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate a circular gauge SVG for the shield score.
|
|
73
|
+
* @param {number} score - 0-100
|
|
74
|
+
* @param {string} grade - Letter grade
|
|
75
|
+
* @returns {string} SVG markup
|
|
76
|
+
*/
|
|
77
|
+
function renderGaugeSVG(score, grade) {
|
|
78
|
+
const color = getScoreColor(score);
|
|
79
|
+
const radius = 56;
|
|
80
|
+
const circumference = 2 * Math.PI * radius;
|
|
81
|
+
const offset = circumference - (score / 100) * circumference;
|
|
82
|
+
|
|
83
|
+
return `<svg class="gauge" width="160" height="160" viewBox="0 0 160 160">
|
|
84
|
+
<circle cx="80" cy="80" r="${radius}" fill="none" stroke="#2a2a3e" stroke-width="10"/>
|
|
85
|
+
<circle cx="80" cy="80" r="${radius}" fill="none" stroke="${color}" stroke-width="10"
|
|
86
|
+
stroke-dasharray="${circumference}" stroke-dashoffset="${offset}"
|
|
87
|
+
stroke-linecap="round" transform="rotate(-90 80 80)"
|
|
88
|
+
style="transition: stroke-dashoffset 0.6s ease;"/>
|
|
89
|
+
<text x="80" y="72" text-anchor="middle" fill="${color}" font-size="36" font-weight="700">${score}</text>
|
|
90
|
+
<text x="80" y="94" text-anchor="middle" fill="#ccc" font-size="14" font-weight="500">${grade}</text>
|
|
91
|
+
</svg>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate a horizontal bar chart SVG for category breakdown.
|
|
96
|
+
* @param {object} categoryStats - { category: { detected, total, missed } }
|
|
97
|
+
* @returns {string} SVG markup
|
|
98
|
+
*/
|
|
99
|
+
function renderCategoryBarsSVG(categoryStats) {
|
|
100
|
+
const entries = Object.entries(categoryStats);
|
|
101
|
+
const barHeight = 28;
|
|
102
|
+
const labelWidth = 200;
|
|
103
|
+
const barMaxWidth = 280;
|
|
104
|
+
const rowHeight = 40;
|
|
105
|
+
const svgHeight = entries.length * rowHeight + 20;
|
|
106
|
+
|
|
107
|
+
let bars = '';
|
|
108
|
+
entries.forEach(([cat, stats], i) => {
|
|
109
|
+
const rate = stats.total > 0 ? stats.detected / stats.total : 0;
|
|
110
|
+
const pct = Math.round(rate * 100);
|
|
111
|
+
const barWidth = Math.max(2, rate * barMaxWidth);
|
|
112
|
+
const color = getScoreColor(pct);
|
|
113
|
+
const y = i * rowHeight + 16;
|
|
114
|
+
const label = cat.replace(/_/g, ' ');
|
|
115
|
+
|
|
116
|
+
bars += `
|
|
117
|
+
<text x="0" y="${y + 18}" fill="#ccc" font-size="13" class="cat-label">${label}</text>
|
|
118
|
+
<rect x="${labelWidth}" y="${y + 4}" width="${barMaxWidth}" height="${barHeight - 6}" rx="4" fill="#2a2a3e"/>
|
|
119
|
+
<rect x="${labelWidth}" y="${y + 4}" width="${barWidth}" height="${barHeight - 6}" rx="4" fill="${color}"/>
|
|
120
|
+
<text x="${labelWidth + barMaxWidth + 10}" y="${y + 18}" fill="#ccc" font-size="13" font-weight="600">${pct}%</text>
|
|
121
|
+
<text x="${labelWidth + barMaxWidth + 52}" y="${y + 18}" fill="#888" font-size="11">(${stats.detected}/${stats.total})</text>`;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return `<svg class="category-bars" width="100%" viewBox="0 0 580 ${svgHeight}" preserveAspectRatio="xMinYMin meet">
|
|
125
|
+
${bars}
|
|
126
|
+
</svg>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =========================================================================
|
|
130
|
+
// HTML sections
|
|
131
|
+
// =========================================================================
|
|
132
|
+
|
|
133
|
+
function renderSeverityDistribution(findings) {
|
|
134
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
135
|
+
for (const f of findings) {
|
|
136
|
+
if (counts[f.severity] !== undefined) {
|
|
137
|
+
counts[f.severity]++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const pills = Object.entries(counts).map(([sev, count]) => {
|
|
142
|
+
return `<span class="pill" style="background:${getSeverityBg(sev)};color:${getSeverityColor(sev)};">
|
|
143
|
+
${sev.toUpperCase()} <strong>${count}</strong>
|
|
144
|
+
</span>`;
|
|
145
|
+
}).join('\n ');
|
|
146
|
+
|
|
147
|
+
return `<div class="severity-dist">${pills}</div>`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderTopFindings(findings, limit = 15) {
|
|
151
|
+
if (!findings || findings.length === 0) {
|
|
152
|
+
return '<p class="no-findings">No missed attacks found. All attacks were detected.</p>';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sorted = [...findings].sort((a, b) => {
|
|
156
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
157
|
+
return (order[a.severity] || 4) - (order[b.severity] || 4);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const rows = sorted.slice(0, limit).map(f => {
|
|
161
|
+
const escapedAttack = escapeHtml(f.attack);
|
|
162
|
+
const escapedRec = escapeHtml(f.recommendation);
|
|
163
|
+
return `<div class="finding">
|
|
164
|
+
<div class="finding-header">
|
|
165
|
+
<span class="badge" style="background:${getSeverityColor(f.severity)};">${f.severity.toUpperCase()}</span>
|
|
166
|
+
<span class="finding-category">${f.category.replace(/_/g, ' ')}</span>
|
|
167
|
+
<span class="finding-type">${f.type}</span>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="finding-attack">${escapedAttack}</div>
|
|
170
|
+
<div class="finding-rec">Fix: ${escapedRec}</div>
|
|
171
|
+
</div>`;
|
|
172
|
+
}).join('\n ');
|
|
173
|
+
|
|
174
|
+
const extra = findings.length > limit
|
|
175
|
+
? `<p class="more-findings">...and ${findings.length - limit} more findings</p>`
|
|
176
|
+
: '';
|
|
177
|
+
|
|
178
|
+
return rows + extra;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function escapeHtml(str) {
|
|
182
|
+
if (!str) return '';
|
|
183
|
+
return str
|
|
184
|
+
.replace(/&/g, '&')
|
|
185
|
+
.replace(/</g, '<')
|
|
186
|
+
.replace(/>/g, '>')
|
|
187
|
+
.replace(/"/g, '"');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// =========================================================================
|
|
191
|
+
// Main HTML generator
|
|
192
|
+
// =========================================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Generate a self-contained HTML security report.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} auditReport - AuditReport instance from SecurityAudit.run()
|
|
198
|
+
* @param {object} [options]
|
|
199
|
+
* @param {string} [options.title] - Report title
|
|
200
|
+
* @param {number} [options.maxFindings] - Maximum findings to show (default 15)
|
|
201
|
+
* @returns {string} Complete HTML string
|
|
202
|
+
*/
|
|
203
|
+
function generateHTMLReport(auditReport, options = {}) {
|
|
204
|
+
const title = options.title || 'Agent Shield Security Report';
|
|
205
|
+
const maxFindings = options.maxFindings || 15;
|
|
206
|
+
const score = auditReport.score || 0;
|
|
207
|
+
const grade = auditReport.grade || getGrade(score);
|
|
208
|
+
const detectionRate = auditReport.detectionRate != null
|
|
209
|
+
? auditReport.detectionRate.toFixed(1)
|
|
210
|
+
: '0.0';
|
|
211
|
+
const timestamp = new Date().toISOString();
|
|
212
|
+
|
|
213
|
+
const gaugeColor = getScoreColor(score);
|
|
214
|
+
|
|
215
|
+
return `<!DOCTYPE html>
|
|
216
|
+
<html lang="en">
|
|
217
|
+
<head>
|
|
218
|
+
<meta charset="UTF-8">
|
|
219
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
220
|
+
<title>${escapeHtml(title)}</title>
|
|
221
|
+
<style>
|
|
222
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
223
|
+
|
|
224
|
+
body {
|
|
225
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
226
|
+
background: #1a1a2e;
|
|
227
|
+
color: #e0e0e0;
|
|
228
|
+
line-height: 1.6;
|
|
229
|
+
min-height: 100vh;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.container {
|
|
233
|
+
max-width: 900px;
|
|
234
|
+
margin: 0 auto;
|
|
235
|
+
padding: 24px 20px 48px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Header */
|
|
239
|
+
.report-header {
|
|
240
|
+
text-align: center;
|
|
241
|
+
padding: 32px 0 24px;
|
|
242
|
+
border-bottom: 1px solid #2a2a3e;
|
|
243
|
+
margin-bottom: 32px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.report-header h1 {
|
|
247
|
+
font-size: 24px;
|
|
248
|
+
font-weight: 700;
|
|
249
|
+
color: #fff;
|
|
250
|
+
margin-bottom: 4px;
|
|
251
|
+
letter-spacing: -0.3px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.report-header .subtitle {
|
|
255
|
+
font-size: 14px;
|
|
256
|
+
color: #888;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Score hero */
|
|
260
|
+
.score-hero {
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
justify-content: center;
|
|
264
|
+
gap: 48px;
|
|
265
|
+
padding: 32px 0;
|
|
266
|
+
flex-wrap: wrap;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.gauge { display: block; }
|
|
270
|
+
|
|
271
|
+
.score-details {
|
|
272
|
+
display: flex;
|
|
273
|
+
flex-direction: column;
|
|
274
|
+
gap: 12px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.score-stat {
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: baseline;
|
|
280
|
+
gap: 10px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.score-stat .label {
|
|
284
|
+
font-size: 13px;
|
|
285
|
+
color: #888;
|
|
286
|
+
min-width: 120px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.score-stat .value {
|
|
290
|
+
font-size: 20px;
|
|
291
|
+
font-weight: 700;
|
|
292
|
+
color: #fff;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.score-stat .value.green { color: #0cce6b; }
|
|
296
|
+
.score-stat .value.yellow { color: #ffa400; }
|
|
297
|
+
.score-stat .value.red { color: #ff4e42; }
|
|
298
|
+
|
|
299
|
+
/* Cards */
|
|
300
|
+
.card {
|
|
301
|
+
background: #16213e;
|
|
302
|
+
border: 1px solid #2a2a3e;
|
|
303
|
+
border-radius: 10px;
|
|
304
|
+
padding: 24px;
|
|
305
|
+
margin-bottom: 24px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.card h2 {
|
|
309
|
+
font-size: 16px;
|
|
310
|
+
font-weight: 700;
|
|
311
|
+
color: #fff;
|
|
312
|
+
margin-bottom: 16px;
|
|
313
|
+
padding-bottom: 10px;
|
|
314
|
+
border-bottom: 1px solid #2a2a3e;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Category bars */
|
|
318
|
+
.cat-label { text-transform: capitalize; }
|
|
319
|
+
|
|
320
|
+
.category-bars { display: block; width: 100%; }
|
|
321
|
+
|
|
322
|
+
/* Severity pills */
|
|
323
|
+
.severity-dist {
|
|
324
|
+
display: flex;
|
|
325
|
+
gap: 12px;
|
|
326
|
+
flex-wrap: wrap;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.pill {
|
|
330
|
+
display: inline-flex;
|
|
331
|
+
align-items: center;
|
|
332
|
+
gap: 6px;
|
|
333
|
+
padding: 6px 14px;
|
|
334
|
+
border-radius: 20px;
|
|
335
|
+
font-size: 13px;
|
|
336
|
+
font-weight: 600;
|
|
337
|
+
letter-spacing: 0.5px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.pill strong {
|
|
341
|
+
font-size: 18px;
|
|
342
|
+
font-weight: 800;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* Findings */
|
|
346
|
+
.finding {
|
|
347
|
+
padding: 14px 0;
|
|
348
|
+
border-bottom: 1px solid #2a2a3e;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.finding:last-child { border-bottom: none; }
|
|
352
|
+
|
|
353
|
+
.finding-header {
|
|
354
|
+
display: flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
gap: 10px;
|
|
357
|
+
margin-bottom: 6px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.badge {
|
|
361
|
+
display: inline-block;
|
|
362
|
+
padding: 2px 8px;
|
|
363
|
+
border-radius: 4px;
|
|
364
|
+
font-size: 11px;
|
|
365
|
+
font-weight: 700;
|
|
366
|
+
color: #fff;
|
|
367
|
+
text-transform: uppercase;
|
|
368
|
+
letter-spacing: 0.5px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.finding-category {
|
|
372
|
+
font-size: 13px;
|
|
373
|
+
color: #ccc;
|
|
374
|
+
font-weight: 600;
|
|
375
|
+
text-transform: capitalize;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.finding-type {
|
|
379
|
+
font-size: 11px;
|
|
380
|
+
color: #666;
|
|
381
|
+
margin-left: auto;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.finding-attack {
|
|
385
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
386
|
+
font-size: 12px;
|
|
387
|
+
color: #aaa;
|
|
388
|
+
background: #1a1a2e;
|
|
389
|
+
padding: 6px 10px;
|
|
390
|
+
border-radius: 4px;
|
|
391
|
+
margin: 6px 0;
|
|
392
|
+
word-break: break-word;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.finding-rec {
|
|
396
|
+
font-size: 12px;
|
|
397
|
+
color: #70a0ff;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.no-findings {
|
|
401
|
+
color: #0cce6b;
|
|
402
|
+
font-weight: 600;
|
|
403
|
+
padding: 16px 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.more-findings {
|
|
407
|
+
color: #888;
|
|
408
|
+
font-size: 13px;
|
|
409
|
+
padding-top: 12px;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Metadata */
|
|
413
|
+
.meta-grid {
|
|
414
|
+
display: grid;
|
|
415
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
416
|
+
gap: 16px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.meta-item {
|
|
420
|
+
background: #1a1a2e;
|
|
421
|
+
border-radius: 8px;
|
|
422
|
+
padding: 14px;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.meta-item .meta-label {
|
|
426
|
+
font-size: 11px;
|
|
427
|
+
color: #888;
|
|
428
|
+
text-transform: uppercase;
|
|
429
|
+
letter-spacing: 0.5px;
|
|
430
|
+
margin-bottom: 4px;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.meta-item .meta-value {
|
|
434
|
+
font-size: 18px;
|
|
435
|
+
font-weight: 700;
|
|
436
|
+
color: #fff;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Verdict */
|
|
440
|
+
.verdict {
|
|
441
|
+
text-align: center;
|
|
442
|
+
padding: 20px;
|
|
443
|
+
border-radius: 8px;
|
|
444
|
+
font-size: 16px;
|
|
445
|
+
font-weight: 700;
|
|
446
|
+
margin-top: 8px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.verdict.pass { background: rgba(12,206,107,0.1); color: #0cce6b; border: 1px solid rgba(12,206,107,0.3); }
|
|
450
|
+
.verdict.warn { background: rgba(255,164,0,0.1); color: #ffa400; border: 1px solid rgba(255,164,0,0.3); }
|
|
451
|
+
.verdict.fail { background: rgba(255,78,66,0.1); color: #ff4e42; border: 1px solid rgba(255,78,66,0.3); }
|
|
452
|
+
|
|
453
|
+
/* Footer */
|
|
454
|
+
.report-footer {
|
|
455
|
+
text-align: center;
|
|
456
|
+
padding: 24px 0 0;
|
|
457
|
+
margin-top: 32px;
|
|
458
|
+
border-top: 1px solid #2a2a3e;
|
|
459
|
+
font-size: 12px;
|
|
460
|
+
color: #666;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.report-footer strong { color: #888; }
|
|
464
|
+
|
|
465
|
+
/* Responsive */
|
|
466
|
+
@media (max-width: 640px) {
|
|
467
|
+
.score-hero { flex-direction: column; gap: 24px; }
|
|
468
|
+
.meta-grid { grid-template-columns: 1fr 1fr; }
|
|
469
|
+
.severity-dist { flex-direction: column; align-items: flex-start; }
|
|
470
|
+
.container { padding: 16px 12px 32px; }
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Print */
|
|
474
|
+
@media print {
|
|
475
|
+
body { background: #fff; color: #222; }
|
|
476
|
+
.container { max-width: 100%; padding: 0; }
|
|
477
|
+
.card { background: #f9f9f9; border-color: #ddd; break-inside: avoid; }
|
|
478
|
+
.report-header { border-color: #ddd; }
|
|
479
|
+
.report-header h1 { color: #111; }
|
|
480
|
+
.score-stat .value { color: #111; }
|
|
481
|
+
.finding-attack { background: #f0f0f0; color: #333; }
|
|
482
|
+
.finding-rec { color: #336; }
|
|
483
|
+
.meta-item { background: #f0f0f0; }
|
|
484
|
+
.meta-item .meta-value { color: #111; }
|
|
485
|
+
.report-footer { border-color: #ddd; color: #999; }
|
|
486
|
+
.cat-label { fill: #333 !important; }
|
|
487
|
+
.pill { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
|
488
|
+
.badge { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
|
489
|
+
}
|
|
490
|
+
</style>
|
|
491
|
+
</head>
|
|
492
|
+
<body>
|
|
493
|
+
<div class="container">
|
|
494
|
+
|
|
495
|
+
<div class="report-header">
|
|
496
|
+
<h1>${escapeHtml(title)}</h1>
|
|
497
|
+
<div class="subtitle">Pre-deployment security audit powered by Agent Shield</div>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<!-- Score Hero -->
|
|
501
|
+
<div class="score-hero">
|
|
502
|
+
${renderGaugeSVG(score, grade)}
|
|
503
|
+
<div class="score-details">
|
|
504
|
+
<div class="score-stat">
|
|
505
|
+
<span class="label">Shield Score</span>
|
|
506
|
+
<span class="value" style="color:${gaugeColor}">${score}/100</span>
|
|
507
|
+
</div>
|
|
508
|
+
<div class="score-stat">
|
|
509
|
+
<span class="label">Grade</span>
|
|
510
|
+
<span class="value" style="color:${gaugeColor}">${grade}</span>
|
|
511
|
+
</div>
|
|
512
|
+
<div class="score-stat">
|
|
513
|
+
<span class="label">Detection Rate</span>
|
|
514
|
+
<span class="value" style="color:${gaugeColor}">${detectionRate}%</span>
|
|
515
|
+
</div>
|
|
516
|
+
<div class="score-stat">
|
|
517
|
+
<span class="label">Attacks Tested</span>
|
|
518
|
+
<span class="value">${auditReport.totalAttacks || 0}</span>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<!-- Category Breakdown -->
|
|
524
|
+
<div class="card">
|
|
525
|
+
<h2>Category Breakdown</h2>
|
|
526
|
+
${renderCategoryBarsSVG(auditReport.categoryStats || {})}
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<!-- Severity Distribution -->
|
|
530
|
+
<div class="card">
|
|
531
|
+
<h2>Threat Severity Distribution</h2>
|
|
532
|
+
${renderSeverityDistribution(auditReport.findings || [])}
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
<!-- Top Findings -->
|
|
536
|
+
<div class="card">
|
|
537
|
+
<h2>Top Findings</h2>
|
|
538
|
+
${renderTopFindings(auditReport.findings || [], maxFindings)}
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<!-- Scan Metadata -->
|
|
542
|
+
<div class="card">
|
|
543
|
+
<h2>Scan Metadata</h2>
|
|
544
|
+
<div class="meta-grid">
|
|
545
|
+
<div class="meta-item">
|
|
546
|
+
<div class="meta-label">Scan Duration</div>
|
|
547
|
+
<div class="meta-value">${auditReport.elapsed || 0}ms</div>
|
|
548
|
+
</div>
|
|
549
|
+
<div class="meta-item">
|
|
550
|
+
<div class="meta-label">Total Attacks</div>
|
|
551
|
+
<div class="meta-value">${auditReport.totalAttacks || 0}</div>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="meta-item">
|
|
554
|
+
<div class="meta-label">Detected</div>
|
|
555
|
+
<div class="meta-value" style="color:#0cce6b">${auditReport.totalDetected || 0}</div>
|
|
556
|
+
</div>
|
|
557
|
+
<div class="meta-item">
|
|
558
|
+
<div class="meta-label">Missed</div>
|
|
559
|
+
<div class="meta-value" style="color:${(auditReport.totalMissed || 0) > 0 ? '#ff4e42' : '#0cce6b'}">${auditReport.totalMissed || 0}</div>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="meta-item">
|
|
562
|
+
<div class="meta-label">Categories</div>
|
|
563
|
+
<div class="meta-value">${Object.keys(auditReport.categoryStats || {}).length}</div>
|
|
564
|
+
</div>
|
|
565
|
+
<div class="meta-item">
|
|
566
|
+
<div class="meta-label">Sensitivity</div>
|
|
567
|
+
<div class="meta-value">${auditReport.sensitivity || 'high'}</div>
|
|
568
|
+
</div>
|
|
569
|
+
<div class="meta-item">
|
|
570
|
+
<div class="meta-label">Mutations</div>
|
|
571
|
+
<div class="meta-value">${auditReport.includedMutations ? 'Yes' : 'No'}</div>
|
|
572
|
+
</div>
|
|
573
|
+
<div class="meta-item">
|
|
574
|
+
<div class="meta-label">Patterns Checked</div>
|
|
575
|
+
<div class="meta-value">${auditReport.totalAttacks || 0}</div>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<!-- Verdict -->
|
|
581
|
+
<div class="card">
|
|
582
|
+
<h2>Verdict</h2>
|
|
583
|
+
${renderVerdict(score)}
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
<div class="report-footer">
|
|
587
|
+
Generated by <strong>Agent Shield</strong> on ${timestamp}
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
</div>
|
|
591
|
+
</body>
|
|
592
|
+
</html>`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Render the verdict section.
|
|
597
|
+
* @param {number} score
|
|
598
|
+
* @returns {string}
|
|
599
|
+
*/
|
|
600
|
+
function renderVerdict(score) {
|
|
601
|
+
if (score >= 95) {
|
|
602
|
+
return '<div class="verdict pass">READY FOR PRODUCTION</div>';
|
|
603
|
+
}
|
|
604
|
+
if (score >= 80) {
|
|
605
|
+
return '<div class="verdict warn">NEEDS IMPROVEMENT - address critical findings before deploying</div>';
|
|
606
|
+
}
|
|
607
|
+
if (score >= 60) {
|
|
608
|
+
return '<div class="verdict fail">NOT READY - significant security gaps detected</div>';
|
|
609
|
+
}
|
|
610
|
+
return '<div class="verdict fail">CRITICAL RISK - do not deploy without major remediation</div>';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// =========================================================================
|
|
614
|
+
// File writer
|
|
615
|
+
// =========================================================================
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Generate an HTML report and write it to a file.
|
|
619
|
+
*
|
|
620
|
+
* @param {object} auditReport - AuditReport instance from SecurityAudit.run()
|
|
621
|
+
* @param {string} outputPath - File path to write the HTML report to
|
|
622
|
+
* @param {object} [options] - Options passed to generateHTMLReport
|
|
623
|
+
* @returns {string} The absolute path of the written file
|
|
624
|
+
*/
|
|
625
|
+
function generateReportFile(auditReport, outputPath, options = {}) {
|
|
626
|
+
const html = generateHTMLReport(auditReport, options);
|
|
627
|
+
const resolved = path.resolve(outputPath);
|
|
628
|
+
fs.writeFileSync(resolved, html, 'utf-8');
|
|
629
|
+
console.log(`[Agent Shield] HTML report written to ${resolved}`);
|
|
630
|
+
return resolved;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// =========================================================================
|
|
634
|
+
// EXPORTS
|
|
635
|
+
// =========================================================================
|
|
636
|
+
|
|
637
|
+
module.exports = {
|
|
638
|
+
generateHTMLReport,
|
|
639
|
+
generateReportFile,
|
|
640
|
+
};
|