aws-security-mcp 0.3.0 → 0.3.1
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/dist/bin/aws-security-mcp.js +670 -0
- package/dist/bin/aws-security-mcp.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +672 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -19192,6 +19192,640 @@ function generateMlps3Report(scanResults) {
|
|
|
19192
19192
|
return lines.join("\n");
|
|
19193
19193
|
}
|
|
19194
19194
|
|
|
19195
|
+
// src/tools/html-report.ts
|
|
19196
|
+
function esc2(s) {
|
|
19197
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
19198
|
+
}
|
|
19199
|
+
function calcScore(summary) {
|
|
19200
|
+
const raw = 100 - summary.critical * 15 - summary.high * 5 - summary.medium * 2 - summary.low * 0.5;
|
|
19201
|
+
return Math.max(0, Math.min(100, Math.round(raw)));
|
|
19202
|
+
}
|
|
19203
|
+
function formatDuration2(start, end) {
|
|
19204
|
+
const ms = new Date(end).getTime() - new Date(start).getTime();
|
|
19205
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
19206
|
+
const secs = Math.round(ms / 1e3);
|
|
19207
|
+
if (secs < 60) return `${secs}s`;
|
|
19208
|
+
return `${Math.floor(secs / 60)}m ${secs % 60}s`;
|
|
19209
|
+
}
|
|
19210
|
+
var SEV_COLOR = {
|
|
19211
|
+
CRITICAL: "#ef4444",
|
|
19212
|
+
HIGH: "#f97316",
|
|
19213
|
+
MEDIUM: "#eab308",
|
|
19214
|
+
LOW: "#22c55e"
|
|
19215
|
+
};
|
|
19216
|
+
var SEVERITY_ORDER2 = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
|
|
19217
|
+
function scoreColor(score) {
|
|
19218
|
+
if (score >= 80) return "#22c55e";
|
|
19219
|
+
if (score >= 50) return "#eab308";
|
|
19220
|
+
return "#ef4444";
|
|
19221
|
+
}
|
|
19222
|
+
function sharedCss() {
|
|
19223
|
+
return `
|
|
19224
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
19225
|
+
body{background:#0f172a;color:#f8fafc;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.6;font-size:14px}
|
|
19226
|
+
.container{max-width:900px;margin:0 auto;padding:40px 24px}
|
|
19227
|
+
header{text-align:center;margin-bottom:40px;border-bottom:1px solid #334155;padding-bottom:24px}
|
|
19228
|
+
header h1{font-size:28px;font-weight:700;margin-bottom:8px;letter-spacing:-0.5px}
|
|
19229
|
+
.meta{color:#94a3b8;font-size:13px}
|
|
19230
|
+
.disclaimer{color:#94a3b8;font-size:12px;font-style:italic;margin-top:8px;max-width:640px;margin-left:auto;margin-right:auto}
|
|
19231
|
+
h2{font-size:20px;font-weight:600;margin:32px 0 16px;padding-bottom:8px;border-bottom:1px solid #334155}
|
|
19232
|
+
h3{font-size:16px;font-weight:600;margin:16px 0 8px}
|
|
19233
|
+
h4{font-size:14px;font-weight:600;margin:12px 0 4px}
|
|
19234
|
+
.card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:20px;margin-bottom:16px}
|
|
19235
|
+
.summary{display:flex;gap:24px;margin-bottom:32px;flex-wrap:wrap}
|
|
19236
|
+
.score-card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px 32px;text-align:center;flex:0 0 auto}
|
|
19237
|
+
.score-value{font-size:48px;font-weight:700}
|
|
19238
|
+
.score-label{color:#94a3b8;font-size:13px;margin-top:4px}
|
|
19239
|
+
.severity-stats{display:flex;gap:12px;flex-wrap:wrap;flex:1;align-items:center;justify-content:center}
|
|
19240
|
+
.stat-card{border-radius:8px;padding:16px 20px;text-align:center;min-width:100px;border:1px solid #334155;background:#1e293b}
|
|
19241
|
+
.stat-count{font-size:28px;font-weight:700}
|
|
19242
|
+
.stat-label{font-size:12px;color:#94a3b8;margin-top:2px}
|
|
19243
|
+
.stat-critical .stat-count{color:#ef4444}
|
|
19244
|
+
.stat-high .stat-count{color:#f97316}
|
|
19245
|
+
.stat-medium .stat-count{color:#eab308}
|
|
19246
|
+
.stat-low .stat-count{color:#22c55e}
|
|
19247
|
+
.charts{display:flex;gap:24px;margin-bottom:32px;flex-wrap:wrap;justify-content:center}
|
|
19248
|
+
.chart-box{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:20px;flex:1;min-width:280px}
|
|
19249
|
+
.chart-title{font-size:14px;font-weight:600;margin-bottom:12px;text-align:center;color:#cbd5e1}
|
|
19250
|
+
.sev-critical{border-left-color:#ef4444}
|
|
19251
|
+
.sev-high{border-left-color:#f97316}
|
|
19252
|
+
.sev-medium{border-left-color:#eab308}
|
|
19253
|
+
.sev-low{border-left-color:#22c55e}
|
|
19254
|
+
.badge{display:inline-block;padding:2px 10px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:0.5px;color:#fff}
|
|
19255
|
+
.badge-critical{background:#ef4444}
|
|
19256
|
+
.badge-high{background:#f97316}
|
|
19257
|
+
.badge-medium{background:#eab308;color:#1e293b}
|
|
19258
|
+
.badge-low{background:#22c55e;color:#1e293b}
|
|
19259
|
+
.finding-title{font-size:15px;font-weight:600;margin-bottom:8px}
|
|
19260
|
+
.finding-detail{color:#cbd5e1;font-size:13px;margin-bottom:4px}
|
|
19261
|
+
.finding-detail strong{color:#f8fafc}
|
|
19262
|
+
.remediation-steps{margin-top:8px;padding-left:20px}
|
|
19263
|
+
.remediation-steps li{color:#cbd5e1;font-size:13px;margin-bottom:4px}
|
|
19264
|
+
table{width:100%;border-collapse:collapse;margin-bottom:16px}
|
|
19265
|
+
th{background:#334155;color:#f8fafc;padding:10px 12px;text-align:left;font-size:13px;font-weight:600}
|
|
19266
|
+
td{padding:8px 12px;border-bottom:1px solid #334155;font-size:13px;color:#cbd5e1}
|
|
19267
|
+
tr:hover td{background:rgba(51,65,85,0.3)}
|
|
19268
|
+
.recommendations ol{padding-left:24px}
|
|
19269
|
+
.recommendations li{margin-bottom:8px;color:#cbd5e1;font-size:13px}
|
|
19270
|
+
.priority-p0{color:#ef4444;font-weight:700}
|
|
19271
|
+
.priority-p1{color:#f97316;font-weight:700}
|
|
19272
|
+
.priority-p2{color:#eab308;font-weight:700}
|
|
19273
|
+
.priority-p3{color:#22c55e;font-weight:700}
|
|
19274
|
+
footer{margin-top:48px;padding-top:24px;border-top:1px solid #334155;text-align:center}
|
|
19275
|
+
footer p{color:#64748b;font-size:12px;margin-bottom:4px}
|
|
19276
|
+
.check-item{display:flex;align-items:flex-start;gap:8px;padding:8px 12px;border-radius:6px;margin-bottom:4px;font-size:14px}
|
|
19277
|
+
.check-pass{background:rgba(34,197,94,0.1)}
|
|
19278
|
+
.check-fail{background:rgba(239,68,68,0.1)}
|
|
19279
|
+
.check-unknown{background:rgba(148,163,184,0.1)}
|
|
19280
|
+
.check-icon{font-size:16px;flex-shrink:0}
|
|
19281
|
+
.check-name{font-weight:500}
|
|
19282
|
+
.check-findings{margin-left:28px;margin-top:4px}
|
|
19283
|
+
.check-findings li{color:#94a3b8;font-size:12px;margin-bottom:2px;list-style:none}
|
|
19284
|
+
.no-findings{text-align:center;padding:40px;color:#22c55e;font-size:18px;font-weight:600}
|
|
19285
|
+
.finding-fold{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:12px;border-left:4px solid;overflow:hidden}
|
|
19286
|
+
.finding-fold>summary{cursor:pointer;padding:12px 20px;display:flex;align-items:center;gap:12px;list-style:none;user-select:none}
|
|
19287
|
+
.finding-fold>summary::-webkit-details-marker{display:none}
|
|
19288
|
+
.finding-fold>summary::marker{content:""}
|
|
19289
|
+
.finding-fold>summary .badge{margin-bottom:0}
|
|
19290
|
+
.finding-fold>summary::after{content:"\\25B6";font-size:10px;color:#64748b;flex-shrink:0;transition:transform 0.2s}
|
|
19291
|
+
.finding-fold[open]>summary::after{transform:rotate(90deg)}
|
|
19292
|
+
.finding-fold[open]>summary{border-bottom:1px solid #334155}
|
|
19293
|
+
.finding-body{padding:12px 20px 16px}
|
|
19294
|
+
.finding-summary-title{font-weight:600;font-size:14px;flex:1}
|
|
19295
|
+
.finding-summary-score{color:#94a3b8;font-size:13px;font-weight:600;white-space:nowrap}
|
|
19296
|
+
.top5-card{display:flex;gap:16px;background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;margin-bottom:16px;border-left:4px solid}
|
|
19297
|
+
.top5-card .badge{margin-bottom:0}
|
|
19298
|
+
.top5-rank{font-size:28px;font-weight:800;color:#475569;min-width:44px;display:flex;align-items:flex-start;justify-content:center}
|
|
19299
|
+
.top5-content{flex:1}
|
|
19300
|
+
.top5-title{font-size:17px;font-weight:700;margin:8px 0}
|
|
19301
|
+
.top5-detail{color:#cbd5e1;font-size:13px;margin-bottom:4px}
|
|
19302
|
+
.top5-detail strong{color:#f8fafc}
|
|
19303
|
+
.top5-remediation{margin-top:8px;padding-left:20px}
|
|
19304
|
+
.top5-remediation li{color:#cbd5e1;font-size:13px;margin-bottom:4px}
|
|
19305
|
+
.trend-section{margin-bottom:32px}
|
|
19306
|
+
.trend-chart{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:20px;margin-bottom:16px}
|
|
19307
|
+
.trend-title{font-size:14px;font-weight:600;margin-bottom:12px;text-align:center;color:#cbd5e1}
|
|
19308
|
+
.category-fold{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:16px;overflow:hidden}
|
|
19309
|
+
.category-fold>summary{cursor:pointer;padding:16px 20px;display:flex;align-items:center;gap:12px;list-style:none;font-size:18px;font-weight:600;user-select:none}
|
|
19310
|
+
.category-fold>summary::-webkit-details-marker{display:none}
|
|
19311
|
+
.category-fold>summary::marker{content:""}
|
|
19312
|
+
.category-fold>summary::after{content:"\\25B6";font-size:12px;color:#64748b;flex-shrink:0;transition:transform 0.2s}
|
|
19313
|
+
.category-fold[open]>summary::after{transform:rotate(90deg)}
|
|
19314
|
+
.category-fold[open]>summary{border-bottom:1px solid #334155}
|
|
19315
|
+
.category-body{padding:12px 20px 16px}
|
|
19316
|
+
.category-title{flex:1}
|
|
19317
|
+
.category-stats{display:inline-flex;gap:12px;font-size:13px}
|
|
19318
|
+
.category-stat-pass{color:#22c55e}
|
|
19319
|
+
.category-stat-fail{color:#ef4444}
|
|
19320
|
+
.category-stat-unknown{color:#94a3b8}
|
|
19321
|
+
@media print{
|
|
19322
|
+
body{background:#fff;color:#1e293b;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
|
19323
|
+
.container{max-width:100%;padding:20px}
|
|
19324
|
+
.card,.score-card,.stat-card,.chart-box,.finding-fold,.top5-card,.trend-chart,.category-fold{background:#fff;border:1px solid #e2e8f0}
|
|
19325
|
+
.badge{border:1px solid}
|
|
19326
|
+
header{border-bottom-color:#e2e8f0}
|
|
19327
|
+
h2{border-bottom-color:#e2e8f0}
|
|
19328
|
+
th{background:#f1f5f9;color:#1e293b}
|
|
19329
|
+
td{border-bottom-color:#e2e8f0;color:#475569}
|
|
19330
|
+
footer{border-top-color:#e2e8f0}
|
|
19331
|
+
.meta,.disclaimer{color:#64748b}
|
|
19332
|
+
.finding-detail,.top5-detail{color:#475569}
|
|
19333
|
+
.finding-detail strong,.top5-detail strong{color:#1e293b}
|
|
19334
|
+
.stat-label,.score-label{color:#64748b}
|
|
19335
|
+
.chart-title,.trend-title{color:#475569}
|
|
19336
|
+
.remediation-steps li,.top5-remediation li{color:#475569}
|
|
19337
|
+
.recommendations li{color:#475569}
|
|
19338
|
+
.check-findings li{color:#64748b}
|
|
19339
|
+
.finding-fold,.top5-card,.category-fold{break-inside:avoid}
|
|
19340
|
+
.check-item{break-inside:avoid}
|
|
19341
|
+
svg text{fill:#1e293b !important}
|
|
19342
|
+
.finding-fold[open]>summary,.category-fold[open]>summary{border-bottom-color:#e2e8f0}
|
|
19343
|
+
details{display:block}
|
|
19344
|
+
details>summary{display:block}
|
|
19345
|
+
details>.finding-body,details>.category-body{display:block !important}
|
|
19346
|
+
}
|
|
19347
|
+
`;
|
|
19348
|
+
}
|
|
19349
|
+
function donutChart(summary) {
|
|
19350
|
+
const total = summary.totalFindings;
|
|
19351
|
+
const r = 80;
|
|
19352
|
+
const circ = 2 * Math.PI * r;
|
|
19353
|
+
if (total === 0) {
|
|
19354
|
+
return [
|
|
19355
|
+
'<svg viewBox="0 0 200 200" width="200" height="200">',
|
|
19356
|
+
' <circle cx="100" cy="100" r="80" fill="none" stroke="#334155" stroke-width="20"/>',
|
|
19357
|
+
' <text x="100" y="105" text-anchor="middle" fill="#22c55e" font-size="24" font-weight="700">0</text>',
|
|
19358
|
+
"</svg>"
|
|
19359
|
+
].join("\n");
|
|
19360
|
+
}
|
|
19361
|
+
const segments = [
|
|
19362
|
+
{ count: summary.critical, color: SEV_COLOR.CRITICAL },
|
|
19363
|
+
{ count: summary.high, color: SEV_COLOR.HIGH },
|
|
19364
|
+
{ count: summary.medium, color: SEV_COLOR.MEDIUM },
|
|
19365
|
+
{ count: summary.low, color: SEV_COLOR.LOW }
|
|
19366
|
+
].filter((s) => s.count > 0);
|
|
19367
|
+
let offset = 0;
|
|
19368
|
+
const circles = segments.map((s) => {
|
|
19369
|
+
const arc = s.count / total * circ;
|
|
19370
|
+
const el = `<circle cx="100" cy="100" r="80" fill="none" stroke="${s.color}" stroke-width="20" stroke-dasharray="${arc.toFixed(2)} ${(circ - arc).toFixed(2)}" stroke-dashoffset="${(-offset).toFixed(2)}" transform="rotate(-90 100 100)"/>`;
|
|
19371
|
+
offset += arc;
|
|
19372
|
+
return el;
|
|
19373
|
+
});
|
|
19374
|
+
return [
|
|
19375
|
+
'<svg viewBox="0 0 200 200" width="200" height="200">',
|
|
19376
|
+
...circles.map((c) => ` ${c}`),
|
|
19377
|
+
` <text x="100" y="105" text-anchor="middle" fill="#f8fafc" font-size="28" font-weight="700">${total}</text>`,
|
|
19378
|
+
"</svg>"
|
|
19379
|
+
].join("\n");
|
|
19380
|
+
}
|
|
19381
|
+
function barChart(modules) {
|
|
19382
|
+
const withFindings = modules.filter((m) => m.findingsCount > 0).sort((a, b) => b.findingsCount - a.findingsCount).slice(0, 12);
|
|
19383
|
+
if (withFindings.length === 0) {
|
|
19384
|
+
return [
|
|
19385
|
+
'<svg viewBox="0 0 400 50" width="100%">',
|
|
19386
|
+
' <text x="200" y="30" text-anchor="middle" fill="#22c55e" font-size="14" font-weight="600">All modules clean</text>',
|
|
19387
|
+
"</svg>"
|
|
19388
|
+
].join("\n");
|
|
19389
|
+
}
|
|
19390
|
+
const maxCount = withFindings[0].findingsCount;
|
|
19391
|
+
const barH = 22;
|
|
19392
|
+
const gap = 6;
|
|
19393
|
+
const labelW = 160;
|
|
19394
|
+
const maxBarW = 190;
|
|
19395
|
+
const height = withFindings.length * (barH + gap);
|
|
19396
|
+
const bars = withFindings.map((m, i) => {
|
|
19397
|
+
const y = i * (barH + gap);
|
|
19398
|
+
const w = Math.max(4, m.findingsCount / maxCount * maxBarW);
|
|
19399
|
+
const worstSev = m.findings.reduce((worst, f) => {
|
|
19400
|
+
const idx = SEVERITY_ORDER2.indexOf(f.severity);
|
|
19401
|
+
return idx < SEVERITY_ORDER2.indexOf(worst) ? f.severity : worst;
|
|
19402
|
+
}, "LOW");
|
|
19403
|
+
const color = SEV_COLOR[worstSev];
|
|
19404
|
+
return [
|
|
19405
|
+
`<text x="${labelW - 8}" y="${y + barH / 2 + 4}" text-anchor="end" fill="#94a3b8" font-size="11">${esc2(m.module)}</text>`,
|
|
19406
|
+
`<rect x="${labelW}" y="${y}" width="${w.toFixed(1)}" height="${barH}" rx="3" fill="${color}" opacity="0.85"/>`,
|
|
19407
|
+
`<text x="${labelW + w + 6}" y="${y + barH / 2 + 4}" fill="#f8fafc" font-size="11">${m.findingsCount}</text>`
|
|
19408
|
+
].join("\n");
|
|
19409
|
+
});
|
|
19410
|
+
return [
|
|
19411
|
+
`<svg viewBox="0 0 400 ${height}" width="100%">`,
|
|
19412
|
+
...bars,
|
|
19413
|
+
"</svg>"
|
|
19414
|
+
].join("\n");
|
|
19415
|
+
}
|
|
19416
|
+
function findingsTrendChart(history) {
|
|
19417
|
+
const entries = history.slice(-30);
|
|
19418
|
+
if (entries.length < 2) return "";
|
|
19419
|
+
const W = 800;
|
|
19420
|
+
const H = 260;
|
|
19421
|
+
const pad = { top: 30, right: 20, bottom: 50, left: 50 };
|
|
19422
|
+
const plotW = W - pad.left - pad.right;
|
|
19423
|
+
const plotH = H - pad.top - pad.bottom;
|
|
19424
|
+
const maxVal = Math.max(
|
|
19425
|
+
1,
|
|
19426
|
+
...entries.flatMap((e) => [e.critical, e.high, e.medium, e.low])
|
|
19427
|
+
);
|
|
19428
|
+
const xPos = (i) => pad.left + i / Math.max(1, entries.length - 1) * plotW;
|
|
19429
|
+
const yPos = (v) => pad.top + plotH - v / maxVal * plotH;
|
|
19430
|
+
const lines = [
|
|
19431
|
+
{ key: "critical", color: "#ef4444", label: "Critical" },
|
|
19432
|
+
{ key: "high", color: "#f97316", label: "High" },
|
|
19433
|
+
{ key: "medium", color: "#eab308", label: "Medium" },
|
|
19434
|
+
{ key: "low", color: "#22c55e", label: "Low" }
|
|
19435
|
+
];
|
|
19436
|
+
const polylines = lines.map((line) => {
|
|
19437
|
+
const pts = entries.map(
|
|
19438
|
+
(e, i) => `${xPos(i).toFixed(1)},${yPos(e[line.key]).toFixed(1)}`
|
|
19439
|
+
).join(" ");
|
|
19440
|
+
return `<polyline points="${pts}" fill="none" stroke="${line.color}" stroke-width="2" stroke-linejoin="round"/>`;
|
|
19441
|
+
}).join("\n ");
|
|
19442
|
+
const xLabels = entries.map((e, i) => {
|
|
19443
|
+
if (i % 5 !== 0 && i !== entries.length - 1) return "";
|
|
19444
|
+
return `<text x="${xPos(i).toFixed(1)}" y="${H - 8}" text-anchor="middle" fill="#94a3b8" font-size="10">${e.date.slice(5)}</text>`;
|
|
19445
|
+
}).filter(Boolean).join("\n ");
|
|
19446
|
+
const ySteps = 5;
|
|
19447
|
+
const yLabels = Array.from({ length: ySteps + 1 }, (_, i) => {
|
|
19448
|
+
const val = Math.round(maxVal / ySteps * i);
|
|
19449
|
+
return [
|
|
19450
|
+
`<text x="${pad.left - 8}" y="${yPos(val).toFixed(1)}" text-anchor="end" fill="#94a3b8" font-size="10" dominant-baseline="middle">${val}</text>`,
|
|
19451
|
+
`<line x1="${pad.left}" y1="${yPos(val).toFixed(1)}" x2="${W - pad.right}" y2="${yPos(val).toFixed(1)}" stroke="#334155" stroke-width="0.5"/>`
|
|
19452
|
+
].join("\n ");
|
|
19453
|
+
}).join("\n ");
|
|
19454
|
+
const legend = lines.map((line, i) => {
|
|
19455
|
+
const lx = pad.left + i * 110;
|
|
19456
|
+
return `<rect x="${lx}" y="8" width="14" height="3" rx="1" fill="${line.color}"/><text x="${lx + 18}" y="12" fill="#94a3b8" font-size="10">${line.label}</text>`;
|
|
19457
|
+
}).join("\n ");
|
|
19458
|
+
return [
|
|
19459
|
+
`<svg viewBox="0 0 ${W} ${H}" width="100%" preserveAspectRatio="xMidYMid meet">`,
|
|
19460
|
+
` ${legend}`,
|
|
19461
|
+
` ${yLabels}`,
|
|
19462
|
+
` ${polylines}`,
|
|
19463
|
+
` ${xLabels}`,
|
|
19464
|
+
"</svg>"
|
|
19465
|
+
].join("\n");
|
|
19466
|
+
}
|
|
19467
|
+
function scoreTrendChart(history) {
|
|
19468
|
+
const entries = history.slice(-30);
|
|
19469
|
+
if (entries.length < 2) return "";
|
|
19470
|
+
const W = 800;
|
|
19471
|
+
const H = 220;
|
|
19472
|
+
const pad = { top: 20, right: 20, bottom: 50, left: 50 };
|
|
19473
|
+
const plotW = W - pad.left - pad.right;
|
|
19474
|
+
const plotH = H - pad.top - pad.bottom;
|
|
19475
|
+
const xPos = (i) => pad.left + i / Math.max(1, entries.length - 1) * plotW;
|
|
19476
|
+
const yPos = (v) => pad.top + plotH - v / 100 * plotH;
|
|
19477
|
+
const pts = entries.map((e, i) => `${xPos(i).toFixed(1)},${yPos(e.score).toFixed(1)}`).join(" ");
|
|
19478
|
+
const zones = [
|
|
19479
|
+
`<rect x="${pad.left}" y="${yPos(100).toFixed(1)}" width="${plotW}" height="${(yPos(80) - yPos(100)).toFixed(1)}" fill="#22c55e" opacity="0.06"/>`,
|
|
19480
|
+
`<rect x="${pad.left}" y="${yPos(80).toFixed(1)}" width="${plotW}" height="${(yPos(50) - yPos(80)).toFixed(1)}" fill="#eab308" opacity="0.06"/>`,
|
|
19481
|
+
`<rect x="${pad.left}" y="${yPos(50).toFixed(1)}" width="${plotW}" height="${(yPos(0) - yPos(50)).toFixed(1)}" fill="#ef4444" opacity="0.06"/>`
|
|
19482
|
+
].join("\n ");
|
|
19483
|
+
const xLabels = entries.map((e, i) => {
|
|
19484
|
+
if (i % 5 !== 0 && i !== entries.length - 1) return "";
|
|
19485
|
+
return `<text x="${xPos(i).toFixed(1)}" y="${H - 8}" text-anchor="middle" fill="#94a3b8" font-size="10">${e.date.slice(5)}</text>`;
|
|
19486
|
+
}).filter(Boolean).join("\n ");
|
|
19487
|
+
const yVals = [0, 25, 50, 75, 100];
|
|
19488
|
+
const yLabels = yVals.map(
|
|
19489
|
+
(val) => `<text x="${pad.left - 8}" y="${yPos(val).toFixed(1)}" text-anchor="end" fill="#94a3b8" font-size="10" dominant-baseline="middle">${val}</text>
|
|
19490
|
+
<line x1="${pad.left}" y1="${yPos(val).toFixed(1)}" x2="${W - pad.right}" y2="${yPos(val).toFixed(1)}" stroke="#334155" stroke-width="0.5"/>`
|
|
19491
|
+
).join("\n ");
|
|
19492
|
+
return [
|
|
19493
|
+
`<svg viewBox="0 0 ${W} ${H}" width="100%" preserveAspectRatio="xMidYMid meet">`,
|
|
19494
|
+
` ${zones}`,
|
|
19495
|
+
` ${yLabels}`,
|
|
19496
|
+
` <polyline points="${pts}" fill="none" stroke="#60a5fa" stroke-width="2.5" stroke-linejoin="round"/>`,
|
|
19497
|
+
` ${xLabels}`,
|
|
19498
|
+
"</svg>"
|
|
19499
|
+
].join("\n");
|
|
19500
|
+
}
|
|
19501
|
+
function generateHtmlReport(scanResults, history) {
|
|
19502
|
+
const { summary, modules, accountId, region, scanStart, scanEnd } = scanResults;
|
|
19503
|
+
const date5 = scanStart.split("T")[0];
|
|
19504
|
+
const duration3 = formatDuration2(scanStart, scanEnd);
|
|
19505
|
+
const score = calcScore(summary);
|
|
19506
|
+
const allFindings = modules.flatMap(
|
|
19507
|
+
(m) => m.findings.map((f) => ({ ...f, module: f.module ?? m.module }))
|
|
19508
|
+
);
|
|
19509
|
+
let top5Html = "";
|
|
19510
|
+
if (allFindings.length > 0) {
|
|
19511
|
+
const top5 = [...allFindings].sort((a, b) => b.riskScore - a.riskScore).slice(0, 5);
|
|
19512
|
+
const cards = top5.map(
|
|
19513
|
+
(f, i) => `
|
|
19514
|
+
<div class="top5-card sev-${esc2(f.severity.toLowerCase())}">
|
|
19515
|
+
<div class="top5-rank">#${i + 1}</div>
|
|
19516
|
+
<div class="top5-content">
|
|
19517
|
+
<span class="badge badge-${esc2(f.severity.toLowerCase())}">${esc2(f.severity)}</span>
|
|
19518
|
+
<div class="top5-title">${esc2(f.title)}</div>
|
|
19519
|
+
<div class="top5-detail"><strong>Resource:</strong> ${esc2(f.resourceId)}</div>
|
|
19520
|
+
<div class="top5-detail"><strong>Impact:</strong> ${esc2(f.impact)}</div>
|
|
19521
|
+
<div class="top5-detail"><strong>Risk Score:</strong> ${f.riskScore}/10</div>
|
|
19522
|
+
<h4>Remediation</h4>
|
|
19523
|
+
<ol class="top5-remediation">${f.remediationSteps.map((s) => `<li>${esc2(s)}</li>`).join("")}</ol>
|
|
19524
|
+
</div>
|
|
19525
|
+
</div>`
|
|
19526
|
+
).join("\n");
|
|
19527
|
+
top5Html = `
|
|
19528
|
+
<section>
|
|
19529
|
+
<h2>Top ${top5.length} Highest Risk Findings</h2>
|
|
19530
|
+
${cards}
|
|
19531
|
+
</section>`;
|
|
19532
|
+
}
|
|
19533
|
+
let findingsHtml;
|
|
19534
|
+
if (summary.totalFindings === 0) {
|
|
19535
|
+
findingsHtml = '<div class="no-findings">No security issues found.</div>';
|
|
19536
|
+
} else {
|
|
19537
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
19538
|
+
for (const sev of SEVERITY_ORDER2) grouped.set(sev, []);
|
|
19539
|
+
for (const f of allFindings) grouped.get(f.severity).push(f);
|
|
19540
|
+
const sections = SEVERITY_ORDER2.map((sev) => {
|
|
19541
|
+
const findings = grouped.get(sev);
|
|
19542
|
+
if (findings.length === 0) return "";
|
|
19543
|
+
findings.sort((a, b) => b.riskScore - a.riskScore);
|
|
19544
|
+
const openAttr = sev === "CRITICAL" ? " open" : "";
|
|
19545
|
+
const cards = findings.map(
|
|
19546
|
+
(f) => `
|
|
19547
|
+
<details class="finding-fold sev-${sev.toLowerCase()}"${openAttr}>
|
|
19548
|
+
<summary>
|
|
19549
|
+
<span class="badge badge-${sev.toLowerCase()}">${sev}</span>
|
|
19550
|
+
<span class="finding-summary-title">${esc2(f.title)}</span>
|
|
19551
|
+
<span class="finding-summary-score">${f.riskScore}/10</span>
|
|
19552
|
+
</summary>
|
|
19553
|
+
<div class="finding-body">
|
|
19554
|
+
<div class="finding-detail"><strong>Resource:</strong> ${esc2(f.resourceId)}</div>
|
|
19555
|
+
<div class="finding-detail"><strong>Description:</strong> ${esc2(f.description)}</div>
|
|
19556
|
+
<div class="finding-detail"><strong>Impact:</strong> ${esc2(f.impact)}</div>
|
|
19557
|
+
<h4>Remediation</h4>
|
|
19558
|
+
<ol class="remediation-steps">${f.remediationSteps.map((s) => `<li>${esc2(s)}</li>`).join("")}</ol>
|
|
19559
|
+
</div>
|
|
19560
|
+
</details>`
|
|
19561
|
+
).join("\n");
|
|
19562
|
+
return `<h3>${sev.charAt(0)}${sev.slice(1).toLowerCase()} (${findings.length})</h3>
|
|
19563
|
+
${cards}`;
|
|
19564
|
+
}).filter(Boolean).join("\n");
|
|
19565
|
+
findingsHtml = sections;
|
|
19566
|
+
}
|
|
19567
|
+
let trendHtml = "";
|
|
19568
|
+
if (history && history.length >= 2) {
|
|
19569
|
+
trendHtml = `
|
|
19570
|
+
<section class="trend-section">
|
|
19571
|
+
<h2>30-Day Trends</h2>
|
|
19572
|
+
<div class="trend-chart">
|
|
19573
|
+
<div class="trend-title">Findings by Severity</div>
|
|
19574
|
+
${findingsTrendChart(history)}
|
|
19575
|
+
</div>
|
|
19576
|
+
<div class="trend-chart">
|
|
19577
|
+
<div class="trend-title">Security Score</div>
|
|
19578
|
+
${scoreTrendChart(history)}
|
|
19579
|
+
</div>
|
|
19580
|
+
</section>`;
|
|
19581
|
+
}
|
|
19582
|
+
const statsRows = modules.map(
|
|
19583
|
+
(m) => `<tr><td>${esc2(m.module)}</td><td>${m.resourcesScanned}</td><td>${m.findingsCount}</td><td>${m.status === "success" ? "✓" : "✗"}</td></tr>`
|
|
19584
|
+
).join("\n");
|
|
19585
|
+
let recsHtml = "";
|
|
19586
|
+
if (summary.totalFindings > 0) {
|
|
19587
|
+
const sorted = [...allFindings].sort((a, b) => b.riskScore - a.riskScore);
|
|
19588
|
+
const items = sorted.map((f) => {
|
|
19589
|
+
const pc = f.priority.toLowerCase();
|
|
19590
|
+
const rem = f.remediationSteps[0] ?? "Review and remediate.";
|
|
19591
|
+
return `<li><span class="priority-${esc2(pc)}">[${esc2(f.priority)}]</span> ${esc2(f.title)}: ${esc2(rem)}</li>`;
|
|
19592
|
+
}).join("\n");
|
|
19593
|
+
recsHtml = `
|
|
19594
|
+
<section class="recommendations">
|
|
19595
|
+
<h2>Recommendations (Priority Order)</h2>
|
|
19596
|
+
<ol>${items}</ol>
|
|
19597
|
+
</section>`;
|
|
19598
|
+
}
|
|
19599
|
+
return `<!DOCTYPE html>
|
|
19600
|
+
<html lang="en">
|
|
19601
|
+
<head>
|
|
19602
|
+
<meta charset="UTF-8">
|
|
19603
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
19604
|
+
<title>AWS Security Scan Report — ${esc2(date5)}</title>
|
|
19605
|
+
<style>${sharedCss()}</style>
|
|
19606
|
+
</head>
|
|
19607
|
+
<body>
|
|
19608
|
+
<div class="container">
|
|
19609
|
+
|
|
19610
|
+
<header>
|
|
19611
|
+
<h1>🛡️ AWS Security Scan Report</h1>
|
|
19612
|
+
<div class="meta">Account: ${esc2(accountId)} | Region: ${esc2(region)} | ${esc2(date5)} | Duration: ${esc2(duration3)}</div>
|
|
19613
|
+
</header>
|
|
19614
|
+
|
|
19615
|
+
<section class="summary">
|
|
19616
|
+
<div class="score-card">
|
|
19617
|
+
<div class="score-value" style="color:${scoreColor(score)}">${score}</div>
|
|
19618
|
+
<div class="score-label">Security Score</div>
|
|
19619
|
+
</div>
|
|
19620
|
+
<div class="severity-stats">
|
|
19621
|
+
<div class="stat-card stat-critical"><div class="stat-count">${summary.critical}</div><div class="stat-label">Critical</div></div>
|
|
19622
|
+
<div class="stat-card stat-high"><div class="stat-count">${summary.high}</div><div class="stat-label">High</div></div>
|
|
19623
|
+
<div class="stat-card stat-medium"><div class="stat-count">${summary.medium}</div><div class="stat-label">Medium</div></div>
|
|
19624
|
+
<div class="stat-card stat-low"><div class="stat-count">${summary.low}</div><div class="stat-label">Low</div></div>
|
|
19625
|
+
</div>
|
|
19626
|
+
</section>
|
|
19627
|
+
|
|
19628
|
+
<section class="charts">
|
|
19629
|
+
<div class="chart-box">
|
|
19630
|
+
<div class="chart-title">Severity Distribution</div>
|
|
19631
|
+
<div style="text-align:center">${donutChart(summary)}</div>
|
|
19632
|
+
</div>
|
|
19633
|
+
<div class="chart-box">
|
|
19634
|
+
<div class="chart-title">Findings by Module</div>
|
|
19635
|
+
${barChart(modules)}
|
|
19636
|
+
</div>
|
|
19637
|
+
</section>
|
|
19638
|
+
|
|
19639
|
+
${trendHtml}
|
|
19640
|
+
|
|
19641
|
+
${top5Html}
|
|
19642
|
+
|
|
19643
|
+
<section>
|
|
19644
|
+
<h2>Scan Statistics</h2>
|
|
19645
|
+
<table>
|
|
19646
|
+
<thead><tr><th>Module</th><th>Resources</th><th>Findings</th><th>Status</th></tr></thead>
|
|
19647
|
+
<tbody>${statsRows}</tbody>
|
|
19648
|
+
</table>
|
|
19649
|
+
</section>
|
|
19650
|
+
|
|
19651
|
+
<section>
|
|
19652
|
+
<h2>All Findings by Severity</h2>
|
|
19653
|
+
${findingsHtml}
|
|
19654
|
+
</section>
|
|
19655
|
+
|
|
19656
|
+
${recsHtml}
|
|
19657
|
+
|
|
19658
|
+
<footer>
|
|
19659
|
+
<p>Generated by AWS Security MCP Server v0.3.0</p>
|
|
19660
|
+
<p>This report is for informational purposes only.</p>
|
|
19661
|
+
</footer>
|
|
19662
|
+
|
|
19663
|
+
</div>
|
|
19664
|
+
</body>
|
|
19665
|
+
</html>`;
|
|
19666
|
+
}
|
|
19667
|
+
function generateMlps3HtmlReport(scanResults, history) {
|
|
19668
|
+
const { accountId, region, scanStart } = scanResults;
|
|
19669
|
+
const date5 = scanStart.split("T")[0];
|
|
19670
|
+
const scanTime = scanStart.replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
|
19671
|
+
const allFindings = scanResults.modules.flatMap(
|
|
19672
|
+
(m) => m.findings.map((f) => ({ ...f, module: f.module ?? m.module }))
|
|
19673
|
+
);
|
|
19674
|
+
const scanModules = scanResults.modules.map((m) => ({
|
|
19675
|
+
module: m.module,
|
|
19676
|
+
status: m.status
|
|
19677
|
+
}));
|
|
19678
|
+
const results = MLPS_CHECKS.map(
|
|
19679
|
+
(check2) => evaluateCheck(check2, allFindings, scanModules)
|
|
19680
|
+
);
|
|
19681
|
+
const passCount = results.filter((r) => r.status === "pass").length;
|
|
19682
|
+
const failCount = results.filter((r) => r.status === "fail").length;
|
|
19683
|
+
const unknownCount = results.filter((r) => r.status === "unknown").length;
|
|
19684
|
+
const checkedTotal = passCount + failCount;
|
|
19685
|
+
const percent = checkedTotal > 0 ? Math.round(passCount / checkedTotal * 100) : 0;
|
|
19686
|
+
let trendHtml = "";
|
|
19687
|
+
if (history && history.length >= 2) {
|
|
19688
|
+
trendHtml = `
|
|
19689
|
+
<section class="trend-section">
|
|
19690
|
+
<h2>30\u65E5\u8D8B\u52BF</h2>
|
|
19691
|
+
<div class="trend-chart">
|
|
19692
|
+
<div class="trend-title">\u6309\u4E25\u91CD\u6027\u5206\u7C7B\u7684\u53D1\u73B0</div>
|
|
19693
|
+
${findingsTrendChart(history)}
|
|
19694
|
+
</div>
|
|
19695
|
+
<div class="trend-chart">
|
|
19696
|
+
<div class="trend-title">\u5B89\u5168\u8BC4\u5206</div>
|
|
19697
|
+
${scoreTrendChart(history)}
|
|
19698
|
+
</div>
|
|
19699
|
+
</section>`;
|
|
19700
|
+
}
|
|
19701
|
+
const categorySections = CATEGORY_ORDER.map((category) => {
|
|
19702
|
+
const sectionTitle = CATEGORY_SECTION[category];
|
|
19703
|
+
const categoryResults = results.filter(
|
|
19704
|
+
(r) => r.check.category === category
|
|
19705
|
+
);
|
|
19706
|
+
if (categoryResults.length === 0) return "";
|
|
19707
|
+
const catPass = categoryResults.filter((r) => r.status === "pass").length;
|
|
19708
|
+
const catFail = categoryResults.filter((r) => r.status === "fail").length;
|
|
19709
|
+
const catUnknown = categoryResults.filter(
|
|
19710
|
+
(r) => r.status === "unknown"
|
|
19711
|
+
).length;
|
|
19712
|
+
const hasFailure = catFail > 0;
|
|
19713
|
+
const openAttr = hasFailure ? " open" : "";
|
|
19714
|
+
const byId = /* @__PURE__ */ new Map();
|
|
19715
|
+
for (const r of categoryResults) {
|
|
19716
|
+
const existing = byId.get(r.check.id) ?? [];
|
|
19717
|
+
existing.push(r);
|
|
19718
|
+
byId.set(r.check.id, existing);
|
|
19719
|
+
}
|
|
19720
|
+
const groups = [...byId.entries()].map(([checkId, checkResults]) => {
|
|
19721
|
+
const items = checkResults.map((r) => {
|
|
19722
|
+
const icon = r.status === "pass" ? "✔" : r.status === "fail" ? "✘" : "⚠";
|
|
19723
|
+
const cls = `check-${r.status}`;
|
|
19724
|
+
const label = r.status === "unknown" ? " (\u672A\u68C0\u67E5)" : "";
|
|
19725
|
+
let findingsHtml = "";
|
|
19726
|
+
if (r.status === "fail" && r.relatedFindings.length > 0) {
|
|
19727
|
+
const items2 = r.relatedFindings.slice(0, 3).map(
|
|
19728
|
+
(f) => `<li>${esc2(f.severity)}: ${esc2(f.title)}</li>`
|
|
19729
|
+
);
|
|
19730
|
+
if (r.relatedFindings.length > 3) {
|
|
19731
|
+
items2.push(
|
|
19732
|
+
`<li>... \u53CA\u5176\u4ED6 ${r.relatedFindings.length - 3} \u9879</li>`
|
|
19733
|
+
);
|
|
19734
|
+
}
|
|
19735
|
+
findingsHtml = `<ul class="check-findings">${items2.join("")}</ul>`;
|
|
19736
|
+
}
|
|
19737
|
+
return `<div class="check-item ${cls}"><span class="check-icon">${icon}</span><span class="check-name">${esc2(r.check.name)}${label}</span></div>${findingsHtml}`;
|
|
19738
|
+
}).join("\n");
|
|
19739
|
+
return `<h3>${esc2(checkId)} ${esc2(checkResults[0].check.name)}</h3>
|
|
19740
|
+
${items}`;
|
|
19741
|
+
}).join("\n");
|
|
19742
|
+
const statsHtml = [
|
|
19743
|
+
catPass > 0 ? `<span class="category-stat-pass">✓ ${catPass}</span>` : "",
|
|
19744
|
+
catFail > 0 ? `<span class="category-stat-fail">✗ ${catFail}</span>` : "",
|
|
19745
|
+
catUnknown > 0 ? `<span class="category-stat-unknown">? ${catUnknown}</span>` : ""
|
|
19746
|
+
].filter(Boolean).join("");
|
|
19747
|
+
return `<details class="category-fold"${openAttr}>
|
|
19748
|
+
<summary>
|
|
19749
|
+
<span class="category-title">${esc2(sectionTitle)}</span>
|
|
19750
|
+
<span class="category-stats">${statsHtml}</span>
|
|
19751
|
+
</summary>
|
|
19752
|
+
<div class="category-body">${groups}</div>
|
|
19753
|
+
</details>`;
|
|
19754
|
+
}).filter(Boolean).join("\n");
|
|
19755
|
+
const failedResults = results.filter((r) => r.status === "fail");
|
|
19756
|
+
let remediationHtml = "";
|
|
19757
|
+
if (failedResults.length > 0) {
|
|
19758
|
+
const allFailedFindings = /* @__PURE__ */ new Map();
|
|
19759
|
+
for (const r of failedResults) {
|
|
19760
|
+
for (const f of r.relatedFindings) {
|
|
19761
|
+
const key = `${f.resourceId}:${f.title}`;
|
|
19762
|
+
if (!allFailedFindings.has(key)) {
|
|
19763
|
+
allFailedFindings.set(key, f);
|
|
19764
|
+
}
|
|
19765
|
+
}
|
|
19766
|
+
}
|
|
19767
|
+
const sorted = [...allFailedFindings.values()].sort(
|
|
19768
|
+
(a, b) => b.riskScore - a.riskScore
|
|
19769
|
+
);
|
|
19770
|
+
const items = sorted.map((f) => {
|
|
19771
|
+
const p = f.riskScore >= 9 ? "P0" : f.riskScore >= 7 ? "P1" : f.riskScore >= 4 ? "P2" : "P3";
|
|
19772
|
+
const rem = f.remediationSteps[0] ?? "Review and remediate.";
|
|
19773
|
+
return `<li><span class="priority-${p.toLowerCase()}">[${p}]</span> ${esc2(f.title)} — ${esc2(rem)}</li>`;
|
|
19774
|
+
}).join("\n");
|
|
19775
|
+
remediationHtml = `
|
|
19776
|
+
<section class="recommendations">
|
|
19777
|
+
<h2>\u5EFA\u8BAE\u6574\u6539\u9879\uFF08\u6309\u4F18\u5148\u7EA7\uFF09</h2>
|
|
19778
|
+
<ol>${items}</ol>
|
|
19779
|
+
</section>`;
|
|
19780
|
+
}
|
|
19781
|
+
const passRateColor = percent >= 80 ? "#22c55e" : percent >= 50 ? "#eab308" : "#ef4444";
|
|
19782
|
+
const unknownNote = unknownCount > 0 ? `<div style="color:#94a3b8;font-size:12px;margin-top:8px">\uFF08\u672A\u68C0\u67E5\u9879\u4E0D\u8BA1\u5165\u901A\u8FC7\u7387\uFF09</div>` : "";
|
|
19783
|
+
return `<!DOCTYPE html>
|
|
19784
|
+
<html lang="zh-CN">
|
|
19785
|
+
<head>
|
|
19786
|
+
<meta charset="UTF-8">
|
|
19787
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
19788
|
+
<title>\u7B49\u4FDD\u4E09\u7EA7\u9884\u68C0\u62A5\u544A — ${esc2(date5)}</title>
|
|
19789
|
+
<style>${sharedCss()}</style>
|
|
19790
|
+
</head>
|
|
19791
|
+
<body>
|
|
19792
|
+
<div class="container">
|
|
19793
|
+
|
|
19794
|
+
<header>
|
|
19795
|
+
<h1>🛡️ \u7B49\u4FDD\u4E09\u7EA7\u9884\u68C0\u62A5\u544A</h1>
|
|
19796
|
+
<div class="disclaimer">\u672C\u62A5\u544A\u4E3A\u7B49\u4FDD\u9884\u68C0\u53C2\u8003\uFF0C\u4EC5\u8986\u76D6 AWS \u4E91\u5E73\u53F0\u914D\u7F6E\u68C0\u67E5\u3002\u5B8C\u6574\u7B49\u4FDD\u6D4B\u8BC4\u9700\u7531\u6301\u8BC1\u6D4B\u8BC4\u673A\u6784\u6267\u884C\u3002</div>
|
|
19797
|
+
<div class="meta">\u8D26\u6237: ${esc2(accountId)} | \u533A\u57DF: ${esc2(region)} | \u626B\u63CF\u65F6\u95F4: ${esc2(scanTime)}</div>
|
|
19798
|
+
</header>
|
|
19799
|
+
|
|
19800
|
+
<section class="summary">
|
|
19801
|
+
<div class="score-card">
|
|
19802
|
+
<div class="score-value" style="color:${passRateColor}">${percent}%</div>
|
|
19803
|
+
<div class="score-label">\u901A\u8FC7\u7387</div>
|
|
19804
|
+
</div>
|
|
19805
|
+
<div class="severity-stats">
|
|
19806
|
+
<div class="stat-card" style="border-color:#22c55e30"><div class="stat-count" style="color:#22c55e">${passCount}</div><div class="stat-label">\u901A\u8FC7</div></div>
|
|
19807
|
+
<div class="stat-card" style="border-color:#ef444430"><div class="stat-count" style="color:#ef4444">${failCount}</div><div class="stat-label">\u4E0D\u901A\u8FC7</div></div>
|
|
19808
|
+
${unknownCount > 0 ? `<div class="stat-card" style="border-color:#94a3b830"><div class="stat-count" style="color:#94a3b8">${unknownCount}</div><div class="stat-label">\u672A\u68C0\u67E5</div></div>` : ""}
|
|
19809
|
+
</div>
|
|
19810
|
+
</section>
|
|
19811
|
+
${unknownNote}
|
|
19812
|
+
|
|
19813
|
+
${trendHtml}
|
|
19814
|
+
|
|
19815
|
+
${categorySections}
|
|
19816
|
+
|
|
19817
|
+
${remediationHtml}
|
|
19818
|
+
|
|
19819
|
+
<footer>
|
|
19820
|
+
<p>\u7531 AWS Security MCP Server v0.3.0 \u751F\u6210</p>
|
|
19821
|
+
<p>\u672C\u62A5\u544A\u4EC5\u4F9B\u53C2\u8003\u3002\u5B8C\u6574\u7B49\u4FDD\u6D4B\u8BC4\u9700\u7531\u6301\u8BC1\u6D4B\u8BC4\u673A\u6784\u6267\u884C\u3002</p>
|
|
19822
|
+
</footer>
|
|
19823
|
+
|
|
19824
|
+
</div>
|
|
19825
|
+
</body>
|
|
19826
|
+
</html>`;
|
|
19827
|
+
}
|
|
19828
|
+
|
|
19195
19829
|
// src/tools/save-results.ts
|
|
19196
19830
|
import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
|
|
19197
19831
|
import { join } from "path";
|
|
@@ -19699,6 +20333,42 @@ function createServer(defaultRegion) {
|
|
|
19699
20333
|
}
|
|
19700
20334
|
}
|
|
19701
20335
|
);
|
|
20336
|
+
server.tool(
|
|
20337
|
+
"generate_html_report",
|
|
20338
|
+
"Generate a professional HTML security report. Save the output as an .html file.",
|
|
20339
|
+
{
|
|
20340
|
+
scan_results: external_exports.string().describe("JSON string of FullScanResult from scan_all"),
|
|
20341
|
+
history: external_exports.string().optional().describe("JSON string of DashboardHistoryEntry[] from dashboard data.json for 30-day trend charts")
|
|
20342
|
+
},
|
|
20343
|
+
async ({ scan_results, history }) => {
|
|
20344
|
+
try {
|
|
20345
|
+
const parsed = JSON.parse(scan_results);
|
|
20346
|
+
const historyData = history ? JSON.parse(history) : void 0;
|
|
20347
|
+
const report = generateHtmlReport(parsed, historyData);
|
|
20348
|
+
return { content: [{ type: "text", text: report }] };
|
|
20349
|
+
} catch (err) {
|
|
20350
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
20351
|
+
}
|
|
20352
|
+
}
|
|
20353
|
+
);
|
|
20354
|
+
server.tool(
|
|
20355
|
+
"generate_mlps3_html_report",
|
|
20356
|
+
"Generate a professional HTML MLPS Level 3 compliance report (\u7B49\u4FDD\u4E09\u7EA7). Save as .html file.",
|
|
20357
|
+
{
|
|
20358
|
+
scan_results: external_exports.string().describe("JSON string of FullScanResult from scan_group mlps3_precheck or scan_all"),
|
|
20359
|
+
history: external_exports.string().optional().describe("JSON string of DashboardHistoryEntry[] from dashboard data.json for 30-day trend charts")
|
|
20360
|
+
},
|
|
20361
|
+
async ({ scan_results, history }) => {
|
|
20362
|
+
try {
|
|
20363
|
+
const parsed = JSON.parse(scan_results);
|
|
20364
|
+
const historyData = history ? JSON.parse(history) : void 0;
|
|
20365
|
+
const report = generateMlps3HtmlReport(parsed, historyData);
|
|
20366
|
+
return { content: [{ type: "text", text: report }] };
|
|
20367
|
+
} catch (err) {
|
|
20368
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
20369
|
+
}
|
|
20370
|
+
}
|
|
20371
|
+
);
|
|
19702
20372
|
server.tool(
|
|
19703
20373
|
"generate_maturity_report",
|
|
19704
20374
|
"Generate a security maturity assessment report from scan_all results. Requires service_detection module output. Read-only.",
|