ngx-security-audit 1.0.0 → 1.1.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/package.json +1 -1
- package/src/reporters/html-reporter.js +583 -140
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ngx-security-audit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Enterprise-grade Angular security auditing tool. Static analysis scanner that detects XSS, CSRF, injection, sensitive data exposure, misconfigurations and 40+ security vulnerabilities in Angular projects. Generates detailed reports with severity levels for CI/CD pipeline integration.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -2,190 +2,633 @@
|
|
|
2
2
|
|
|
3
3
|
const { SEVERITY, SEVERITY_LABELS } = require('../utils/severity');
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
const SEV_COLORS = {
|
|
6
|
+
[SEVERITY.CRITICAL]: '#ef4444',
|
|
7
|
+
[SEVERITY.HIGH]: '#f97316',
|
|
8
|
+
[SEVERITY.MEDIUM]: '#eab308',
|
|
9
|
+
[SEVERITY.LOW]: '#3b82f6',
|
|
10
|
+
[SEVERITY.INFO]: '#6b7280',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const OWASP_LABELS = {
|
|
14
|
+
A01: 'Broken Access Control',
|
|
15
|
+
A02: 'Cryptographic Failures',
|
|
16
|
+
A03: 'Injection',
|
|
17
|
+
A04: 'Insecure Design',
|
|
18
|
+
A05: 'Security Misconfiguration',
|
|
19
|
+
A06: 'Vulnerable Components',
|
|
20
|
+
A07: 'Auth Failures',
|
|
21
|
+
A08: 'Software Integrity',
|
|
22
|
+
A09: 'Logging Failures',
|
|
23
|
+
A10: 'SSRF',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function escapeHtml(str) {
|
|
27
|
+
if (!str) return '';
|
|
28
|
+
return String(str)
|
|
29
|
+
.replace(/&/g, '&')
|
|
30
|
+
.replace(/</g, '<')
|
|
31
|
+
.replace(/>/g, '>')
|
|
32
|
+
.replace(/"/g, '"')
|
|
33
|
+
.replace(/'/g, ''');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── SVG Helpers ────────────────────────────────────── */
|
|
37
|
+
|
|
38
|
+
function buildDonutChart(summary) {
|
|
39
|
+
const data = [
|
|
40
|
+
{ label: 'Critical', value: summary.critical, color: SEV_COLORS.critical },
|
|
41
|
+
{ label: 'High', value: summary.high, color: SEV_COLORS.high },
|
|
42
|
+
{ label: 'Medium', value: summary.medium, color: SEV_COLORS.medium },
|
|
43
|
+
{ label: 'Low', value: summary.low, color: SEV_COLORS.low },
|
|
44
|
+
{ label: 'Info', value: summary.info, color: SEV_COLORS.info },
|
|
45
|
+
].filter((d) => d.value > 0);
|
|
46
|
+
|
|
47
|
+
if (data.length === 0) {
|
|
48
|
+
return `<svg viewBox="0 0 200 200" width="200" height="200">
|
|
49
|
+
<circle cx="100" cy="100" r="80" fill="none" stroke="#22c55e" stroke-width="30"/>
|
|
50
|
+
<text x="100" y="105" text-anchor="middle" fill="#22c55e" font-size="18" font-weight="800">CLEAN</text>
|
|
51
|
+
</svg>`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const total = data.reduce((s, d) => s + d.value, 0);
|
|
55
|
+
const R = 80, CX = 100, CY = 100;
|
|
56
|
+
const circumference = 2 * Math.PI * R;
|
|
57
|
+
let offset = 0;
|
|
58
|
+
const arcs = data.map((d) => {
|
|
59
|
+
const pct = d.value / total;
|
|
60
|
+
const dash = pct * circumference;
|
|
61
|
+
const gap = circumference - dash;
|
|
62
|
+
const arc = `<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="${d.color}" stroke-width="30" stroke-dasharray="${dash.toFixed(2)} ${gap.toFixed(2)}" stroke-dashoffset="${(-offset).toFixed(2)}" transform="rotate(-90 ${CX} ${CY})"/>`;
|
|
63
|
+
offset += dash;
|
|
64
|
+
return arc;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return `<svg viewBox="0 0 200 200" width="200" height="200">
|
|
68
|
+
${arcs.join('\n ')}
|
|
69
|
+
<circle cx="${CX}" cy="${CY}" r="60" fill="#0f172a"/>
|
|
70
|
+
<text x="${CX}" y="${CY - 5}" text-anchor="middle" fill="#f1f5f9" font-size="28" font-weight="800">${total}</text>
|
|
71
|
+
<text x="${CX}" y="${CY + 16}" text-anchor="middle" fill="#94a3b8" font-size="11">issues</text>
|
|
72
|
+
</svg>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildBarChart(summary) {
|
|
76
|
+
const bars = [
|
|
77
|
+
{ label: 'Critical', value: summary.critical, color: SEV_COLORS.critical },
|
|
78
|
+
{ label: 'High', value: summary.high, color: SEV_COLORS.high },
|
|
79
|
+
{ label: 'Medium', value: summary.medium, color: SEV_COLORS.medium },
|
|
80
|
+
{ label: 'Low', value: summary.low, color: SEV_COLORS.low },
|
|
81
|
+
{ label: 'Info', value: summary.info, color: SEV_COLORS.info },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const maxVal = Math.max(...bars.map((b) => b.value), 1);
|
|
85
|
+
const barW = 48, gap = 16, chartH = 160, padTop = 20, padBottom = 30;
|
|
86
|
+
const totalW = bars.length * barW + (bars.length - 1) * gap + 40;
|
|
87
|
+
|
|
88
|
+
const rects = bars.map((b, i) => {
|
|
89
|
+
const x = 20 + i * (barW + gap);
|
|
90
|
+
const h = (b.value / maxVal) * (chartH - padTop);
|
|
91
|
+
const y = chartH - h;
|
|
92
|
+
return `
|
|
93
|
+
<rect x="${x}" y="${y}" width="${barW}" height="${h}" rx="6" fill="${b.color}" opacity="0.85"/>
|
|
94
|
+
<text x="${x + barW / 2}" y="${y - 6}" text-anchor="middle" fill="#f1f5f9" font-size="13" font-weight="700">${b.value}</text>
|
|
95
|
+
<text x="${x + barW / 2}" y="${chartH + 16}" text-anchor="middle" fill="#94a3b8" font-size="10">${b.label}</text>`;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return `<svg viewBox="0 0 ${totalW} ${chartH + padBottom}" width="${totalW}" height="${chartH + padBottom}">
|
|
99
|
+
<line x1="15" y1="${chartH}" x2="${totalW - 15}" y2="${chartH}" stroke="#334155" stroke-width="1"/>
|
|
100
|
+
${rects.join('')}
|
|
101
|
+
</svg>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildScoreGauge(score) {
|
|
105
|
+
const R = 70, CX = 90, CY = 90;
|
|
106
|
+
const arc = Math.PI * 1.5; // 270° arc
|
|
107
|
+
const circumference = arc * R;
|
|
108
|
+
const filled = (score / 100) * circumference;
|
|
109
|
+
const gap = circumference - filled;
|
|
110
|
+
const color = score >= 80 ? '#22c55e' : score >= 60 ? '#eab308' : '#ef4444';
|
|
111
|
+
|
|
112
|
+
return `<svg viewBox="0 0 180 180" width="180" height="180">
|
|
113
|
+
<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="#1e293b" stroke-width="14"
|
|
114
|
+
stroke-dasharray="${circumference.toFixed(2)} 9999" stroke-dashoffset="0"
|
|
115
|
+
transform="rotate(135 ${CX} ${CY})" stroke-linecap="round"/>
|
|
116
|
+
<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="${color}" stroke-width="14"
|
|
117
|
+
stroke-dasharray="${filled.toFixed(2)} ${gap.toFixed(2)}" stroke-dashoffset="0"
|
|
118
|
+
transform="rotate(135 ${CX} ${CY})" stroke-linecap="round"/>
|
|
119
|
+
<text x="${CX}" y="${CY - 2}" text-anchor="middle" fill="${color}" font-size="36" font-weight="900">${score}</text>
|
|
120
|
+
<text x="${CX}" y="${CY + 18}" text-anchor="middle" fill="#94a3b8" font-size="12">/ 100</text>
|
|
121
|
+
</svg>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildCategoryChart(findings) {
|
|
125
|
+
const cats = {};
|
|
126
|
+
for (const f of findings) {
|
|
127
|
+
cats[f.category] = (cats[f.category] || 0) + 1;
|
|
128
|
+
}
|
|
129
|
+
const entries = Object.entries(cats).sort((a, b) => b[1] - a[1]);
|
|
130
|
+
if (entries.length === 0) return '';
|
|
131
|
+
|
|
132
|
+
const maxVal = Math.max(...entries.map((e) => e[1]), 1);
|
|
133
|
+
const barH = 28, gap = 8, padLeft = 150, padRight = 50;
|
|
134
|
+
const totalH = entries.length * (barH + gap) + 10;
|
|
135
|
+
const chartW = 500;
|
|
136
|
+
const barArea = chartW - padLeft - padRight;
|
|
137
|
+
const colors = ['#38bdf8', '#818cf8', '#c084fc', '#f472b6', '#fb923c', '#34d399', '#fbbf24'];
|
|
138
|
+
|
|
139
|
+
const rows = entries.map(([cat, count], i) => {
|
|
140
|
+
const y = i * (barH + gap) + 5;
|
|
141
|
+
const w = (count / maxVal) * barArea;
|
|
142
|
+
const c = colors[i % colors.length];
|
|
143
|
+
return `
|
|
144
|
+
<text x="${padLeft - 10}" y="${y + barH / 2 + 4}" text-anchor="end" fill="#cbd5e1" font-size="12">${escapeHtml(cat)}</text>
|
|
145
|
+
<rect x="${padLeft}" y="${y}" width="${w}" height="${barH}" rx="6" fill="${c}" opacity="0.8"/>
|
|
146
|
+
<text x="${padLeft + w + 8}" y="${y + barH / 2 + 4}" fill="#f1f5f9" font-size="12" font-weight="700">${count}</text>`;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return `<svg viewBox="0 0 ${chartW} ${totalH}" width="${chartW}" height="${totalH}">
|
|
150
|
+
${rows.join('')}
|
|
151
|
+
</svg>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildOwaspRadar(findings) {
|
|
155
|
+
const mappings = {
|
|
156
|
+
'XSS': ['A03'],
|
|
157
|
+
'Injection': ['A03'],
|
|
158
|
+
'Authentication': ['A01', 'A07'],
|
|
159
|
+
'Sensitive Data': ['A02', 'A09'],
|
|
160
|
+
'HTTP Security': ['A02', 'A05'],
|
|
161
|
+
'Angular Config': ['A04', 'A05', 'A06', 'A10'],
|
|
162
|
+
'Best Practices': ['A01', 'A03', 'A04', 'A06'],
|
|
15
163
|
};
|
|
16
164
|
|
|
17
|
-
const
|
|
165
|
+
const owaspCounts = {};
|
|
166
|
+
const owaspKeys = ['A01', 'A02', 'A03', 'A04', 'A05', 'A06', 'A07', 'A09', 'A10'];
|
|
167
|
+
owaspKeys.forEach((k) => (owaspCounts[k] = 0));
|
|
18
168
|
|
|
19
|
-
const
|
|
169
|
+
for (const f of findings) {
|
|
170
|
+
const tags = mappings[f.category] || [];
|
|
171
|
+
for (const t of tags) {
|
|
172
|
+
if (owaspCounts[t] !== undefined) owaspCounts[t]++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const n = owaspKeys.length;
|
|
177
|
+
const CX = 160, CY = 160, R = 120;
|
|
178
|
+
const maxVal = Math.max(...Object.values(owaspCounts), 1);
|
|
179
|
+
|
|
180
|
+
// Grid circles
|
|
181
|
+
const gridCircles = [0.25, 0.5, 0.75, 1].map((p) =>
|
|
182
|
+
`<circle cx="${CX}" cy="${CY}" r="${R * p}" fill="none" stroke="#334155" stroke-width="0.5"/>`
|
|
183
|
+
).join('\n ');
|
|
184
|
+
|
|
185
|
+
// Axis lines and labels
|
|
186
|
+
const axes = owaspKeys.map((key, i) => {
|
|
187
|
+
const angle = (2 * Math.PI * i) / n - Math.PI / 2;
|
|
188
|
+
const x2 = CX + R * Math.cos(angle);
|
|
189
|
+
const y2 = CY + R * Math.sin(angle);
|
|
190
|
+
const lx = CX + (R + 22) * Math.cos(angle);
|
|
191
|
+
const ly = CY + (R + 22) * Math.sin(angle);
|
|
192
|
+
return `
|
|
193
|
+
<line x1="${CX}" y1="${CY}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="#334155" stroke-width="0.5"/>
|
|
194
|
+
<text x="${lx.toFixed(1)}" y="${(ly + 4).toFixed(1)}" text-anchor="middle" fill="#94a3b8" font-size="9">${key}</text>`;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Data polygon
|
|
198
|
+
const points = owaspKeys.map((key, i) => {
|
|
199
|
+
const angle = (2 * Math.PI * i) / n - Math.PI / 2;
|
|
200
|
+
const r = (owaspCounts[key] / maxVal) * R;
|
|
201
|
+
return `${(CX + r * Math.cos(angle)).toFixed(1)},${(CY + r * Math.sin(angle)).toFixed(1)}`;
|
|
202
|
+
}).join(' ');
|
|
203
|
+
|
|
204
|
+
// Data dots
|
|
205
|
+
const dots = owaspKeys.map((key, i) => {
|
|
206
|
+
const angle = (2 * Math.PI * i) / n - Math.PI / 2;
|
|
207
|
+
const r = (owaspCounts[key] / maxVal) * R;
|
|
208
|
+
const x = CX + r * Math.cos(angle);
|
|
209
|
+
const y = CY + r * Math.sin(angle);
|
|
210
|
+
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3" fill="#38bdf8"/>`;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return `<svg viewBox="0 0 320 320" width="320" height="320">
|
|
214
|
+
${gridCircles}
|
|
215
|
+
${axes.join('')}
|
|
216
|
+
<polygon points="${points}" fill="rgba(56,189,248,0.15)" stroke="#38bdf8" stroke-width="2"/>
|
|
217
|
+
${dots.join('\n ')}
|
|
218
|
+
</svg>`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildFileHeatmap(findings) {
|
|
222
|
+
const fileCounts = {};
|
|
223
|
+
for (const f of findings) {
|
|
224
|
+
const short = f.file ? f.file.replace(/\\/g, '/').split('/').slice(-2).join('/') : 'unknown';
|
|
225
|
+
fileCounts[short] = (fileCounts[short] || 0) + 1;
|
|
226
|
+
}
|
|
227
|
+
const entries = Object.entries(fileCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
228
|
+
if (entries.length === 0) return '';
|
|
229
|
+
|
|
230
|
+
const maxVal = Math.max(...entries.map((e) => e[1]), 1);
|
|
231
|
+
const barH = 26, gap = 6, padLeft = 200, padRight = 50;
|
|
232
|
+
const totalH = entries.length * (barH + gap) + 10;
|
|
233
|
+
const chartW = 560;
|
|
234
|
+
const barArea = chartW - padLeft - padRight;
|
|
235
|
+
|
|
236
|
+
const rows = entries.map(([file, count], i) => {
|
|
237
|
+
const y = i * (barH + gap) + 5;
|
|
238
|
+
const w = Math.max(4, (count / maxVal) * barArea);
|
|
239
|
+
const heat = count / maxVal;
|
|
240
|
+
const color = heat > 0.66 ? '#ef4444' : heat > 0.33 ? '#f97316' : '#eab308';
|
|
241
|
+
return `
|
|
242
|
+
<text x="${padLeft - 10}" y="${y + barH / 2 + 4}" text-anchor="end" fill="#cbd5e1" font-size="11" font-family="monospace">${escapeHtml(file)}</text>
|
|
243
|
+
<rect x="${padLeft}" y="${y}" width="${w}" height="${barH}" rx="5" fill="${color}" opacity="0.75"/>
|
|
244
|
+
<text x="${padLeft + w + 8}" y="${y + barH / 2 + 4}" fill="#f1f5f9" font-size="11" font-weight="700">${count}</text>`;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return `<svg viewBox="0 0 ${chartW} ${totalH}" width="100%" height="${totalH}" preserveAspectRatio="xMinYMin meet">
|
|
248
|
+
${rows.join('')}
|
|
249
|
+
</svg>`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ── Executive Summary ──────────────────────────────── */
|
|
253
|
+
|
|
254
|
+
function buildExecutiveSummary(result) {
|
|
255
|
+
const { critical, high, medium, low, info, total } = result.summary;
|
|
256
|
+
const urgent = critical + high;
|
|
257
|
+
const riskLevel = critical > 0 ? 'CRITICAL' : high > 0 ? 'HIGH' : medium > 0 ? 'MODERATE' : 'LOW';
|
|
258
|
+
const riskColor = critical > 0 ? '#ef4444' : high > 0 ? '#f97316' : medium > 0 ? '#eab308' : '#22c55e';
|
|
259
|
+
|
|
260
|
+
const bullets = [];
|
|
261
|
+
if (critical > 0) bullets.push(`<li><strong style="color:#ef4444">${critical} critical</strong> vulnerability${critical > 1 ? 'ies' : 'y'} requiring immediate remediation.</li>`);
|
|
262
|
+
if (high > 0) bullets.push(`<li><strong style="color:#f97316">${high} high-severity</strong> issue${high > 1 ? 's' : ''} that should be addressed before production.</li>`);
|
|
263
|
+
if (medium > 0) bullets.push(`<li><strong style="color:#eab308">${medium} medium</strong> finding${medium > 1 ? 's' : ''} recommended for next sprint.</li>`);
|
|
264
|
+
if (low + info > 0) bullets.push(`<li><strong style="color:#3b82f6">${low + info} low/informational</strong> item${(low + info) > 1 ? 's' : ''} for continuous improvement.</li>`);
|
|
265
|
+
if (total === 0) bullets.push('<li style="color:#22c55e"><strong>No security issues detected.</strong> The application passed all checks.</li>');
|
|
266
|
+
|
|
267
|
+
return `
|
|
268
|
+
<div class="exec-summary">
|
|
269
|
+
<h2>📋 Executive Summary</h2>
|
|
270
|
+
<div class="exec-grid">
|
|
271
|
+
<div class="exec-card">
|
|
272
|
+
<div class="exec-label">Overall Risk</div>
|
|
273
|
+
<div class="exec-value" style="color:${riskColor}">${riskLevel}</div>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="exec-card">
|
|
276
|
+
<div class="exec-label">Urgent Actions</div>
|
|
277
|
+
<div class="exec-value" style="color:${urgent > 0 ? '#ef4444' : '#22c55e'}">${urgent}</div>
|
|
278
|
+
</div>
|
|
279
|
+
<div class="exec-card">
|
|
280
|
+
<div class="exec-label">Total Findings</div>
|
|
281
|
+
<div class="exec-value">${total}</div>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="exec-card">
|
|
284
|
+
<div class="exec-label">Attack Surface</div>
|
|
285
|
+
<div class="exec-value">${result.filesScanned} files</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<ul class="exec-bullets">${bullets.join('\n ')}</ul>
|
|
289
|
+
</div>`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ── Top Risks + Remediation ────────────────────────── */
|
|
293
|
+
|
|
294
|
+
function buildTopRisks(findings) {
|
|
295
|
+
const top = findings
|
|
20
296
|
.sort((a, b) => {
|
|
21
297
|
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
22
298
|
return (order[a.severity] || 5) - (order[b.severity] || 5);
|
|
23
299
|
})
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
300
|
+
.slice(0, 5);
|
|
301
|
+
|
|
302
|
+
if (top.length === 0) return '';
|
|
303
|
+
|
|
304
|
+
const rows = top.map((f, i) => `
|
|
305
|
+
<tr>
|
|
306
|
+
<td class="rank">#${i + 1}</td>
|
|
307
|
+
<td><span class="badge-sm" style="background:${SEV_COLORS[f.severity]}">${SEVERITY_LABELS[f.severity]}</span></td>
|
|
308
|
+
<td class="rule-id-cell">${escapeHtml(f.ruleId)}</td>
|
|
309
|
+
<td>${escapeHtml(f.message).substring(0, 100)}</td>
|
|
310
|
+
<td class="rec-cell">${escapeHtml(f.recommendation || 'See documentation')}</td>
|
|
311
|
+
</tr>`).join('');
|
|
312
|
+
|
|
313
|
+
return `
|
|
314
|
+
<div class="section-block">
|
|
315
|
+
<h2>🔥 Top Risks & Remediation</h2>
|
|
316
|
+
<div class="table-wrap">
|
|
317
|
+
<table class="data-table">
|
|
318
|
+
<thead><tr><th>#</th><th>Severity</th><th>Rule</th><th>Issue</th><th>Recommended Fix</th></tr></thead>
|
|
319
|
+
<tbody>${rows}</tbody>
|
|
320
|
+
</table>
|
|
37
321
|
</div>
|
|
38
|
-
|
|
39
|
-
|
|
322
|
+
</div>`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* ── OWASP Compliance Table ─────────────────────────── */
|
|
326
|
+
|
|
327
|
+
function buildOwaspTable(findings) {
|
|
328
|
+
const mappings = {
|
|
329
|
+
'XSS': ['A03'],
|
|
330
|
+
'Injection': ['A03'],
|
|
331
|
+
'Authentication': ['A01', 'A07'],
|
|
332
|
+
'Sensitive Data': ['A02', 'A09'],
|
|
333
|
+
'HTTP Security': ['A02', 'A05'],
|
|
334
|
+
'Angular Config': ['A04', 'A05', 'A06', 'A10'],
|
|
335
|
+
'Best Practices': ['A01', 'A03', 'A04', 'A06'],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const owaspCounts = {};
|
|
339
|
+
const owaspKeys = ['A01', 'A02', 'A03', 'A04', 'A05', 'A06', 'A07', 'A08', 'A09', 'A10'];
|
|
340
|
+
owaspKeys.forEach((k) => (owaspCounts[k] = 0));
|
|
341
|
+
|
|
342
|
+
for (const f of findings) {
|
|
343
|
+
const tags = mappings[f.category] || [];
|
|
344
|
+
for (const t of tags) {
|
|
345
|
+
if (owaspCounts[t] !== undefined) owaspCounts[t]++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const rows = owaspKeys.map((k) => {
|
|
350
|
+
const count = owaspCounts[k];
|
|
351
|
+
const status = count === 0 ? '<span style="color:#22c55e">✅ Pass</span>' : `<span style="color:#ef4444">⚠️ ${count} issue${count > 1 ? 's' : ''}</span>`;
|
|
352
|
+
return `<tr><td><strong>${k}</strong></td><td>${OWASP_LABELS[k] || k}</td><td>${status}</td></tr>`;
|
|
353
|
+
}).join('');
|
|
354
|
+
|
|
355
|
+
return `
|
|
356
|
+
<div class="section-block">
|
|
357
|
+
<h2>🌐 OWASP Top 10 Compliance</h2>
|
|
358
|
+
<div class="table-wrap">
|
|
359
|
+
<table class="data-table"><thead><tr><th>ID</th><th>Category</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>
|
|
360
|
+
</div>
|
|
361
|
+
</div>`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ── Main Report ────────────────────────────────────── */
|
|
365
|
+
|
|
366
|
+
function htmlReport(result) {
|
|
367
|
+
const scoreColor = result.score >= 80 ? '#22c55e' : result.score >= 60 ? '#eab308' : '#ef4444';
|
|
368
|
+
|
|
369
|
+
const sortedFindings = [...result.findings].sort((a, b) => {
|
|
370
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
371
|
+
return (order[a.severity] || 5) - (order[b.severity] || 5);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const findingsHtml = sortedFindings.map((f) => `
|
|
375
|
+
<div class="finding finding-${f.severity}">
|
|
376
|
+
<div class="finding-header">
|
|
377
|
+
<span class="badge" style="background:${SEV_COLORS[f.severity]}">${SEVERITY_LABELS[f.severity]}</span>
|
|
378
|
+
<span class="rule-id">${escapeHtml(f.ruleId)}</span>
|
|
379
|
+
<span class="cat-tag">${escapeHtml(f.category)}</span>
|
|
380
|
+
</div>
|
|
381
|
+
<p class="finding-message">${escapeHtml(f.message)}</p>
|
|
382
|
+
<div class="finding-meta">📁 ${escapeHtml(f.file)}${f.line ? `:${f.line}` : ''}</div>
|
|
383
|
+
${f.code ? `<pre class="code-block"><code>${escapeHtml(f.code)}</code></pre>` : ''}
|
|
384
|
+
${f.recommendation ? `<div class="recommendation">💡 <strong>Fix:</strong> ${escapeHtml(f.recommendation)}</div>` : ''}
|
|
385
|
+
</div>`).join('\n');
|
|
40
386
|
|
|
41
387
|
return `<!DOCTYPE html>
|
|
42
388
|
<html lang="en">
|
|
43
389
|
<head>
|
|
44
390
|
<meta charset="UTF-8">
|
|
45
391
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
46
|
-
<title>
|
|
392
|
+
<title>Security Report — ${escapeHtml(result.projectName || 'Angular Project')}</title>
|
|
47
393
|
<style>
|
|
48
|
-
:root
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.
|
|
70
|
-
.
|
|
71
|
-
.
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
394
|
+
:root{--bg:#0f172a;--surface:#1e293b;--surface2:#334155;--text:#f1f5f9;--text-dim:#94a3b8;--accent:#38bdf8;--accent2:#818cf8;--success:#22c55e;--danger:#ef4444;--border:#475569;--gradient:linear-gradient(135deg,#38bdf8,#818cf8,#c084fc)}
|
|
395
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
396
|
+
body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.7}
|
|
397
|
+
a{color:var(--accent)}
|
|
398
|
+
code,pre{font-family:'Cascadia Code','Fira Code','JetBrains Mono',monospace}
|
|
399
|
+
.container{max-width:1280px;margin:0 auto;padding:2rem}
|
|
400
|
+
|
|
401
|
+
/* Print */
|
|
402
|
+
@media print{body{background:#fff;color:#111}.container{max-width:100%}nav,.no-print{display:none!important}.surface,.section-block,.finding,.exec-card,.info-card,.score-section,.chart-panel{background:#f8fafc!important;border-color:#d1d5db!important;color:#111!important}h1,h2,h3,.score-number,.exec-value,.count{color:#111!important;-webkit-text-fill-color:#111!important}}
|
|
403
|
+
|
|
404
|
+
/* Nav */
|
|
405
|
+
nav{position:sticky;top:0;z-index:100;background:rgba(15,23,42,0.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border);padding:0.75rem 0}
|
|
406
|
+
nav .container{display:flex;align-items:center;justify-content:space-between;padding-top:0;padding-bottom:0}
|
|
407
|
+
nav .logo{font-weight:800;font-size:1.1rem;background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
|
408
|
+
.nav-links{display:flex;gap:1.5rem}
|
|
409
|
+
.nav-links a{color:var(--text-dim);font-size:0.85rem;font-weight:500;text-decoration:none}
|
|
410
|
+
.nav-links a:hover{color:var(--text)}
|
|
411
|
+
.print-btn{background:var(--surface2);color:var(--text);border:1px solid var(--border);padding:0.4rem 1rem;border-radius:8px;font-size:0.8rem;cursor:pointer;font-weight:600}
|
|
412
|
+
.print-btn:hover{background:var(--accent);color:var(--bg);border-color:var(--accent)}
|
|
413
|
+
|
|
414
|
+
/* Header */
|
|
415
|
+
.report-header{text-align:center;padding:3rem 0 2rem;border-bottom:1px solid var(--border);margin-bottom:2rem}
|
|
416
|
+
.report-header h1{font-size:2.8rem;font-weight:900;background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:0.5rem}
|
|
417
|
+
.report-header .subtitle{color:var(--text-dim);font-size:1.1rem}
|
|
418
|
+
.report-header .ts{color:var(--text-dim);font-size:0.85rem;margin-top:0.5rem}
|
|
419
|
+
|
|
420
|
+
/* Info strip */
|
|
421
|
+
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:2rem}
|
|
422
|
+
.info-card{background:var(--surface);border-radius:12px;padding:1.2rem 1.5rem;border:1px solid var(--border)}
|
|
423
|
+
.info-card label{color:var(--text-dim);font-size:0.78rem;text-transform:uppercase;letter-spacing:0.06em}
|
|
424
|
+
.info-card .value{font-size:1.2rem;font-weight:700;margin-top:0.2rem}
|
|
425
|
+
|
|
426
|
+
/* Score section */
|
|
427
|
+
.score-section{text-align:center;padding:2.5rem;background:var(--surface);border-radius:16px;margin-bottom:2rem;border:1px solid var(--border)}
|
|
428
|
+
.score-grade{font-size:1.5rem;color:${scoreColor};font-weight:800;margin-top:0.5rem}
|
|
429
|
+
.status{display:inline-block;padding:0.5rem 2rem;border-radius:50px;font-weight:700;font-size:1rem;margin-top:1rem}
|
|
430
|
+
.status-pass{background:rgba(34,197,94,0.12);color:#22c55e;border:2px solid #22c55e}
|
|
431
|
+
.status-fail{background:rgba(239,68,68,0.12);color:#ef4444;border:2px solid #ef4444}
|
|
432
|
+
|
|
433
|
+
/* Charts */
|
|
434
|
+
.charts-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1.5rem;margin-bottom:2rem}
|
|
435
|
+
.chart-panel{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:1.5rem;display:flex;flex-direction:column;align-items:center}
|
|
436
|
+
.chart-panel h3{font-size:1rem;margin-bottom:1rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.04em;font-weight:600}
|
|
437
|
+
|
|
438
|
+
/* Executive summary */
|
|
439
|
+
.exec-summary{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:2rem;margin-bottom:2rem}
|
|
440
|
+
.exec-summary h2{font-size:1.4rem;margin-bottom:1rem}
|
|
441
|
+
.exec-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:1.5rem}
|
|
442
|
+
.exec-card{background:var(--bg);border-radius:12px;padding:1.2rem;text-align:center;border:1px solid var(--border)}
|
|
443
|
+
.exec-label{color:var(--text-dim);font-size:0.8rem;text-transform:uppercase;letter-spacing:0.04em}
|
|
444
|
+
.exec-value{font-size:1.6rem;font-weight:800;margin-top:0.3rem}
|
|
445
|
+
.exec-bullets{list-style:none;padding:0}
|
|
446
|
+
.exec-bullets li{padding:0.4rem 0;padding-left:1.2rem;position:relative;font-size:0.95rem}
|
|
447
|
+
.exec-bullets li::before{content:'›';position:absolute;left:0;color:var(--accent);font-weight:800}
|
|
448
|
+
|
|
449
|
+
/* Summary cards */
|
|
450
|
+
.summary-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1rem;margin-bottom:2rem}
|
|
451
|
+
.summary-card{background:var(--surface);border-radius:12px;padding:1.2rem;text-align:center;border:1px solid var(--border);transition:transform 0.2s}
|
|
452
|
+
.summary-card:hover{transform:translateY(-2px)}
|
|
453
|
+
.summary-card .count{font-size:2.2rem;font-weight:800}
|
|
454
|
+
.summary-card .label{color:var(--text-dim);font-size:0.8rem;text-transform:uppercase;margin-top:0.2rem}
|
|
455
|
+
|
|
456
|
+
/* Section blocks */
|
|
457
|
+
.section-block{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:2rem;margin-bottom:2rem}
|
|
458
|
+
.section-block h2{font-size:1.4rem;margin-bottom:1.2rem}
|
|
459
|
+
|
|
460
|
+
/* Tables */
|
|
461
|
+
.table-wrap{overflow-x:auto;border-radius:10px;border:1px solid var(--border);background:var(--bg)}
|
|
462
|
+
.data-table{width:100%;border-collapse:collapse}
|
|
463
|
+
.data-table th{text-align:left;padding:0.6rem 1rem;color:var(--text-dim);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;border-bottom:2px solid var(--border)}
|
|
464
|
+
.data-table td{padding:0.6rem 1rem;border-bottom:1px solid var(--border);font-size:0.88rem}
|
|
465
|
+
.data-table tr:hover{background:var(--surface)}
|
|
466
|
+
.rank{font-weight:800;color:var(--accent);font-size:1rem}
|
|
467
|
+
.rule-id-cell{font-family:monospace;color:var(--accent);font-size:0.85rem}
|
|
468
|
+
.rec-cell{font-size:0.82rem;color:#86efac;max-width:250px}
|
|
469
|
+
.badge-sm{padding:0.15rem 0.6rem;border-radius:50px;color:white;font-size:0.7rem;font-weight:700;text-transform:uppercase}
|
|
470
|
+
|
|
471
|
+
/* Findings */
|
|
472
|
+
.findings-section{margin-bottom:2rem}
|
|
473
|
+
.findings-section > h2{font-size:1.5rem;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:2px solid var(--accent)}
|
|
474
|
+
.finding{background:var(--surface);border-radius:12px;padding:1.5rem;margin-bottom:1rem;border:1px solid var(--border);border-left:4px solid var(--border)}
|
|
475
|
+
.finding-critical{border-left-color:#ef4444}
|
|
476
|
+
.finding-high{border-left-color:#f97316}
|
|
477
|
+
.finding-medium{border-left-color:#eab308}
|
|
478
|
+
.finding-low{border-left-color:#3b82f6}
|
|
479
|
+
.finding-info{border-left-color:#6b7280}
|
|
480
|
+
.finding-header{display:flex;align-items:center;gap:0.75rem;margin-bottom:0.75rem;flex-wrap:wrap}
|
|
481
|
+
.badge{padding:0.2rem 0.75rem;border-radius:50px;color:white;font-size:0.72rem;font-weight:700;text-transform:uppercase}
|
|
482
|
+
.rule-id{font-family:monospace;color:var(--accent);font-size:0.88rem}
|
|
483
|
+
.cat-tag{color:var(--text-dim);font-size:0.82rem;background:var(--surface2);padding:0.12rem 0.5rem;border-radius:4px}
|
|
484
|
+
.finding-message{margin-bottom:0.75rem}
|
|
485
|
+
.finding-meta{color:var(--text-dim);font-size:0.82rem;margin-bottom:0.75rem;font-family:monospace}
|
|
486
|
+
.code-block{background:#0d1117;border-radius:8px;padding:1rem;overflow-x:auto;margin:0.75rem 0;border:1px solid #30363d}
|
|
487
|
+
.code-block code{font-size:0.82rem;color:#c9d1d9;white-space:pre}
|
|
488
|
+
.recommendation{background:rgba(34,197,94,0.08);border:1px solid rgba(34,197,94,0.2);border-radius:8px;padding:0.75rem 1rem;font-size:0.88rem;color:#86efac}
|
|
489
|
+
|
|
490
|
+
/* Filter */
|
|
491
|
+
.filter-bar{display:flex;gap:0.5rem;margin-bottom:1.5rem;flex-wrap:wrap}
|
|
492
|
+
.filter-btn{padding:0.4rem 1rem;border-radius:50px;font-size:0.82rem;font-weight:600;cursor:pointer;background:var(--surface2);color:var(--text-dim);border:1px solid var(--border);transition:all 0.2s}
|
|
493
|
+
.filter-btn:hover,.filter-btn.active{background:var(--accent);color:var(--bg);border-color:var(--accent)}
|
|
494
|
+
|
|
495
|
+
/* Footer */
|
|
496
|
+
footer{text-align:center;padding:2rem 0;color:var(--text-dim);border-top:1px solid var(--border);margin-top:2rem;font-size:0.82rem}
|
|
497
|
+
footer strong{color:var(--accent)}
|
|
498
|
+
|
|
499
|
+
@media(max-width:768px){
|
|
500
|
+
.summary-grid{grid-template-columns:repeat(2,1fr)}
|
|
501
|
+
.info-grid{grid-template-columns:1fr 1fr}
|
|
502
|
+
.report-header h1{font-size:1.8rem}
|
|
503
|
+
.charts-row{grid-template-columns:1fr}
|
|
101
504
|
}
|
|
102
505
|
</style>
|
|
103
506
|
</head>
|
|
104
507
|
<body>
|
|
508
|
+
|
|
509
|
+
<nav class="no-print">
|
|
510
|
+
<div class="container">
|
|
511
|
+
<span class="logo">🛡️ ngx-security-audit</span>
|
|
512
|
+
<div class="nav-links">
|
|
513
|
+
<a href="#summary">Summary</a>
|
|
514
|
+
<a href="#charts">Charts</a>
|
|
515
|
+
<a href="#owasp">OWASP</a>
|
|
516
|
+
<a href="#findings">Findings</a>
|
|
517
|
+
<button class="print-btn" onclick="window.print()">🖨️ Print / PDF</button>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</nav>
|
|
521
|
+
|
|
105
522
|
<div class="container">
|
|
106
|
-
<header>
|
|
107
|
-
<h1>🛡️ NGX Security Audit Report</h1>
|
|
108
|
-
<p class="subtitle">Angular Application Security Analysis powered by ngx-security-audit</p>
|
|
109
|
-
</header>
|
|
110
523
|
|
|
524
|
+
<!-- Header -->
|
|
525
|
+
<div class="report-header">
|
|
526
|
+
<h1>🛡️ Security Audit Report</h1>
|
|
527
|
+
<p class="subtitle">${escapeHtml(result.projectName || 'Angular Project')} — Comprehensive Security Analysis</p>
|
|
528
|
+
<p class="ts">Generated ${new Date(result.scanDate).toLocaleString()} by <strong>ngx-security-audit v1.1.0</strong></p>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
<!-- Project Info -->
|
|
111
532
|
<div class="info-grid">
|
|
112
|
-
<div class="info-card">
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
</div>
|
|
116
|
-
<div class="info-card">
|
|
117
|
-
|
|
118
|
-
<div class="value">${escapeHtml(result.angularVersion || 'Unknown')}</div>
|
|
119
|
-
</div>
|
|
120
|
-
<div class="info-card">
|
|
121
|
-
<label>Scan Date</label>
|
|
122
|
-
<div class="value">${new Date(result.scanDate).toLocaleDateString()}</div>
|
|
123
|
-
</div>
|
|
124
|
-
<div class="info-card">
|
|
125
|
-
<label>Files Scanned</label>
|
|
126
|
-
<div class="value">${result.filesScanned}</div>
|
|
127
|
-
</div>
|
|
128
|
-
<div class="info-card">
|
|
129
|
-
<label>Rules Executed</label>
|
|
130
|
-
<div class="value">${result.rulesExecuted}</div>
|
|
131
|
-
</div>
|
|
533
|
+
<div class="info-card"><label>Project</label><div class="value">${escapeHtml(result.projectName || 'Angular Project')}</div></div>
|
|
534
|
+
<div class="info-card"><label>Angular</label><div class="value">${escapeHtml(result.angularVersion || 'N/A')}</div></div>
|
|
535
|
+
<div class="info-card"><label>Scan Date</label><div class="value">${new Date(result.scanDate).toLocaleDateString()}</div></div>
|
|
536
|
+
<div class="info-card"><label>Files Scanned</label><div class="value">${result.filesScanned}</div></div>
|
|
537
|
+
<div class="info-card"><label>Rules Executed</label><div class="value">${result.rulesExecuted}</div></div>
|
|
538
|
+
<div class="info-card"><label>Threshold</label><div class="value" style="text-transform:uppercase">${escapeHtml(result.threshold)}</div></div>
|
|
132
539
|
</div>
|
|
133
540
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
</div>
|
|
541
|
+
<!-- Score -->
|
|
542
|
+
<div class="score-section" id="summary">
|
|
543
|
+
${buildScoreGauge(result.score)}
|
|
138
544
|
<div class="score-grade">Grade: ${result.grade}</div>
|
|
139
545
|
<div class="status ${result.passed ? 'status-pass' : 'status-fail'}">
|
|
140
|
-
${result.passed ? '✅ PASSED' : '❌ FAILED'}
|
|
546
|
+
${result.passed ? '✅ AUDIT PASSED' : '❌ AUDIT FAILED'}
|
|
141
547
|
</div>
|
|
142
548
|
</div>
|
|
143
549
|
|
|
550
|
+
<!-- Executive Summary -->
|
|
551
|
+
${buildExecutiveSummary(result)}
|
|
552
|
+
|
|
553
|
+
<!-- Severity Cards -->
|
|
144
554
|
<div class="summary-grid">
|
|
145
|
-
<div class="summary-card">
|
|
146
|
-
|
|
147
|
-
|
|
555
|
+
<div class="summary-card"><div class="count" style="color:#ef4444">${result.summary.critical}</div><div class="label">Critical</div></div>
|
|
556
|
+
<div class="summary-card"><div class="count" style="color:#f97316">${result.summary.high}</div><div class="label">High</div></div>
|
|
557
|
+
<div class="summary-card"><div class="count" style="color:#eab308">${result.summary.medium}</div><div class="label">Medium</div></div>
|
|
558
|
+
<div class="summary-card"><div class="count" style="color:#3b82f6">${result.summary.low}</div><div class="label">Low</div></div>
|
|
559
|
+
<div class="summary-card"><div class="count" style="color:#6b7280">${result.summary.info}</div><div class="label">Info</div></div>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<!-- Charts -->
|
|
563
|
+
<div class="charts-row" id="charts">
|
|
564
|
+
<div class="chart-panel">
|
|
565
|
+
<h3>Severity Distribution</h3>
|
|
566
|
+
${buildDonutChart(result.summary)}
|
|
148
567
|
</div>
|
|
149
|
-
<div class="
|
|
150
|
-
<
|
|
151
|
-
|
|
568
|
+
<div class="chart-panel">
|
|
569
|
+
<h3>Severity Breakdown</h3>
|
|
570
|
+
${buildBarChart(result.summary)}
|
|
152
571
|
</div>
|
|
153
|
-
<div class="
|
|
154
|
-
<
|
|
155
|
-
|
|
572
|
+
<div class="chart-panel">
|
|
573
|
+
<h3>OWASP Radar</h3>
|
|
574
|
+
${buildOwaspRadar(result.findings)}
|
|
156
575
|
</div>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div class="charts-row">
|
|
579
|
+
<div class="chart-panel">
|
|
580
|
+
<h3>Findings by Category</h3>
|
|
581
|
+
${buildCategoryChart(result.findings) || '<p style="color:var(--text-dim)">No findings</p>'}
|
|
160
582
|
</div>
|
|
161
|
-
<div class="
|
|
162
|
-
<
|
|
163
|
-
<
|
|
583
|
+
<div class="chart-panel">
|
|
584
|
+
<h3>Hotspot Files</h3>
|
|
585
|
+
${buildFileHeatmap(result.findings) || '<p style="color:var(--text-dim)">No findings</p>'}
|
|
164
586
|
</div>
|
|
165
587
|
</div>
|
|
166
588
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
589
|
+
<!-- Top Risks -->
|
|
590
|
+
${buildTopRisks(sortedFindings)}
|
|
591
|
+
|
|
592
|
+
<!-- OWASP Table -->
|
|
593
|
+
<div id="owasp"></div>
|
|
594
|
+
${buildOwaspTable(result.findings)}
|
|
595
|
+
|
|
596
|
+
<!-- Findings -->
|
|
597
|
+
<div class="findings-section" id="findings">
|
|
598
|
+
<h2>📝 Detailed Findings (${result.summary.total})</h2>
|
|
599
|
+
|
|
600
|
+
<div class="filter-bar no-print">
|
|
601
|
+
<button class="filter-btn active" onclick="filterFindings('all',this)">All (${result.summary.total})</button>
|
|
602
|
+
<button class="filter-btn" onclick="filterFindings('critical',this)">Critical (${result.summary.critical})</button>
|
|
603
|
+
<button class="filter-btn" onclick="filterFindings('high',this)">High (${result.summary.high})</button>
|
|
604
|
+
<button class="filter-btn" onclick="filterFindings('medium',this)">Medium (${result.summary.medium})</button>
|
|
605
|
+
<button class="filter-btn" onclick="filterFindings('low',this)">Low (${result.summary.low})</button>
|
|
606
|
+
<button class="filter-btn" onclick="filterFindings('info',this)">Info (${result.summary.info})</button>
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
${result.findings.length === 0 ? '<p style="color:#22c55e;text-align:center;padding:2rem;">🎉 No security issues found! Your Angular application passed all checks.</p>' : findingsHtml}
|
|
170
610
|
</div>
|
|
171
611
|
|
|
172
612
|
<footer>
|
|
173
|
-
<p>Generated by <strong>ngx-security-audit v1.
|
|
174
|
-
<p>OWASP Top 10 Aligned
|
|
613
|
+
<p>Generated by <strong>ngx-security-audit v1.1.0</strong> — ${new Date(result.scanDate).toISOString()}</p>
|
|
614
|
+
<p>OWASP Top 10 2021 Aligned · ${result.rulesExecuted} Security Rules · CWE Mapped · Angular-Specific Analysis</p>
|
|
615
|
+
<p style="margin-top:0.5rem">© ${new Date().getFullYear()} ngx-security-audit — MIT License</p>
|
|
175
616
|
</footer>
|
|
617
|
+
|
|
176
618
|
</div>
|
|
619
|
+
|
|
620
|
+
<script>
|
|
621
|
+
function filterFindings(sev, btn) {
|
|
622
|
+
document.querySelectorAll('.filter-btn').forEach(function(b){b.classList.remove('active')});
|
|
623
|
+
btn.classList.add('active');
|
|
624
|
+
document.querySelectorAll('.finding').forEach(function(el) {
|
|
625
|
+
if (sev === 'all') { el.style.display = ''; return; }
|
|
626
|
+
el.style.display = el.classList.contains('finding-' + sev) ? '' : 'none';
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
</script>
|
|
177
630
|
</body>
|
|
178
631
|
</html>`;
|
|
179
632
|
}
|
|
180
633
|
|
|
181
|
-
function escapeHtml(str) {
|
|
182
|
-
if (!str) return '';
|
|
183
|
-
return String(str)
|
|
184
|
-
.replace(/&/g, '&')
|
|
185
|
-
.replace(/</g, '<')
|
|
186
|
-
.replace(/>/g, '>')
|
|
187
|
-
.replace(/"/g, '"')
|
|
188
|
-
.replace(/'/g, ''');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
634
|
module.exports = htmlReport;
|