aws-security-mcp 0.6.1 → 0.6.3

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/src/index.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
 
6
6
  // src/version.ts
7
- var VERSION = "0.6.1";
7
+ var VERSION = "0.6.3";
8
8
 
9
9
  // src/utils/aws-client.ts
10
10
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -274,10 +274,6 @@ import {
274
274
  ConfigServiceClient,
275
275
  DescribeConfigurationRecordersCommand
276
276
  } from "@aws-sdk/client-config-service";
277
- import {
278
- Macie2Client,
279
- GetMacieSessionCommand
280
- } from "@aws-sdk/client-macie2";
281
277
  import {
282
278
  CloudTrailClient,
283
279
  DescribeTrailsCommand
@@ -321,7 +317,7 @@ function isNotEnabled(err) {
321
317
  err.name === "DisabledException" || err.message.includes("not enabled") || err.message.includes("not subscribed");
322
318
  }
323
319
  function computeMaturityLevel(enabledCount) {
324
- if (enabledCount >= 6) return "comprehensive";
320
+ if (enabledCount >= 5) return "comprehensive";
325
321
  if (enabledCount >= 4) return "advanced";
326
322
  if (enabledCount >= 2) return "intermediate";
327
323
  return "basic";
@@ -625,53 +621,6 @@ var ServiceDetectionScanner = class {
625
621
  services.push({ name: "AWS Config", enabled: null, details: "Detection error" });
626
622
  }
627
623
  }
628
- if (region.startsWith("cn-")) {
629
- services.push({ name: "Macie", enabled: null, details: "Not available in China regions" });
630
- warnings.push("Macie is not available in AWS China regions.");
631
- } else {
632
- try {
633
- const mc = createClient(Macie2Client, region, ctx.credentials);
634
- await mc.send(new GetMacieSessionCommand({}));
635
- services.push({
636
- name: "Macie",
637
- enabled: true,
638
- details: "Sensitive data detection active"
639
- });
640
- } catch (err) {
641
- if (isAccessDenied(err)) {
642
- warnings.push("Macie: insufficient permissions to check status");
643
- services.push({ name: "Macie", enabled: null, details: "Access denied" });
644
- } else if (isNotEnabled(err)) {
645
- services.push({
646
- name: "Macie",
647
- enabled: false,
648
- recommendation: "Enable Macie to detect sensitive data in S3",
649
- freeTrialAvailable: true
650
- });
651
- findings.push(
652
- makeFinding({
653
- riskScore: 5,
654
- title: "Amazon Macie is not enabled",
655
- resourceType: "AWS::Macie::Session",
656
- resourceId: "macie",
657
- resourceArn: `arn:${partition}:macie2:${region}:${accountId}:session`,
658
- region,
659
- description: "Amazon Macie is not enabled in this region. Macie uses machine learning to discover and protect sensitive data stored in S3.",
660
- impact: "Detects sensitive data (PII, credentials, financial data) in S3 buckets. Without it, sensitive data exposure may go unnoticed.",
661
- remediationSteps: [
662
- "Open the Amazon Macie console.",
663
- "Click 'Get Started' and enable Macie.",
664
- "Macie offers a 30-day free trial for sensitive data discovery.",
665
- "Configure automated sensitive data discovery jobs."
666
- ]
667
- })
668
- );
669
- } else {
670
- warnings.push(`Macie detection failed: ${err instanceof Error ? err.message : String(err)}`);
671
- services.push({ name: "Macie", enabled: null, details: "Detection error" });
672
- }
673
- }
674
- }
675
624
  const knownServices = services.filter((s) => s.enabled !== null);
676
625
  const enabledCount = services.filter((s) => s.enabled === true).length;
677
626
  const coveragePercent = knownServices.length > 0 ? Math.round(enabledCount / knownServices.length * 100) : 0;
@@ -3697,6 +3646,12 @@ var zhI18n = {
3697
3646
  trendTitle: "30\u65E5\u8D8B\u52BF",
3698
3647
  findingsBySeverity: "\u6309\u4E25\u91CD\u6027\u5206\u7C7B\u7684\u53D1\u73B0",
3699
3648
  showMoreCount: (n) => `\u663E\u793A\u5269\u4F59 ${n} \u9879\u2026`,
3649
+ // Filter toolbar
3650
+ filterSeverity: "\u4E25\u91CD\u6027\uFF1A",
3651
+ filterModule: "\u6A21\u5757\uFF1A",
3652
+ filterAll: "\u5168\u90E8",
3653
+ filterAllModules: "\u5168\u90E8\u6A21\u5757",
3654
+ filterCountTpl: "\u663E\u793A {shown} / {total} \u4E2A\u53D1\u73B0",
3700
3655
  // Extended — MLPS extras
3701
3656
  // Markdown report
3702
3657
  executiveSummary: "\u6267\u884C\u6458\u8981",
@@ -3727,6 +3682,44 @@ var zhI18n = {
3727
3682
  "\u5B89\u5168\u8BA1\u7B97\u73AF\u5883": "\u56DB\u3001\u5B89\u5168\u8BA1\u7B97\u73AF\u5883",
3728
3683
  "\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3": "\u4E94\u3001\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3"
3729
3684
  },
3685
+ // Module display names
3686
+ moduleNames: {
3687
+ service_detection: "\u5B89\u5168\u670D\u52A1\u68C0\u6D4B",
3688
+ secret_exposure: "\u5BC6\u94A5\u66B4\u9732",
3689
+ ssl_certificate: "SSL \u8BC1\u4E66",
3690
+ dns_dangling: "\u60AC\u6302 DNS",
3691
+ network_reachability: "\u7F51\u7EDC\u53EF\u8FBE\u6027",
3692
+ iam_privilege_escalation: "IAM \u63D0\u6743\u5206\u6790",
3693
+ public_access_verify: "\u516C\u7F51\u8BBF\u95EE\u9A8C\u8BC1",
3694
+ tag_compliance: "\u6807\u7B7E\u5408\u89C4",
3695
+ idle_resources: "\u95F2\u7F6E\u8D44\u6E90",
3696
+ disaster_recovery: "\u707E\u5907\u8BC4\u4F30",
3697
+ security_hub_findings: "Security Hub",
3698
+ guardduty_findings: "GuardDuty",
3699
+ inspector_findings: "Inspector",
3700
+ trusted_advisor_findings: "Trusted Advisor",
3701
+ config_rules_findings: "Config Rules",
3702
+ access_analyzer_findings: "Access Analyzer",
3703
+ patch_compliance_findings: "\u8865\u4E01\u5408\u89C4",
3704
+ imdsv2_enforcement: "IMDSv2 \u5F3A\u5236",
3705
+ waf_coverage: "WAF \u8986\u76D6",
3706
+ // Security Hub sub-categories
3707
+ "sh:FSBP": "\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5",
3708
+ "sh:Inspector": "\u8F6F\u4EF6\u6F0F\u6D1E",
3709
+ "sh:GuardDuty": "\u5A01\u80C1\u68C0\u6D4B",
3710
+ "sh:Config": "\u914D\u7F6E\u5408\u89C4",
3711
+ "sh:Access Analyzer": "\u5916\u90E8\u8BBF\u95EE",
3712
+ "sh:Other": "\u5176\u4ED6\u5B89\u5168\u53D1\u73B0"
3713
+ },
3714
+ // Security Hub sub-categories
3715
+ securityHubSubCategories: {
3716
+ FSBP: { label: "\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5" },
3717
+ Inspector: { label: "\u8F6F\u4EF6\u6F0F\u6D1E" },
3718
+ GuardDuty: { label: "\u5A01\u80C1\u68C0\u6D4B" },
3719
+ Config: { label: "\u914D\u7F6E\u5408\u89C4" },
3720
+ "Access Analyzer": { label: "\u5916\u90E8\u8BBF\u95EE" },
3721
+ Other: { label: "\u5176\u4ED6\u5B89\u5168\u53D1\u73B0" }
3722
+ },
3730
3723
  // Service Recommendations
3731
3724
  notEnabled: "\u672A\u542F\u7528",
3732
3725
  serviceRecommendations: {
@@ -3929,6 +3922,12 @@ var enI18n = {
3929
3922
  trendTitle: "30-Day Trends",
3930
3923
  findingsBySeverity: "Findings by Severity",
3931
3924
  showMoreCount: (n) => `Show ${n} more\u2026`,
3925
+ // Filter toolbar
3926
+ filterSeverity: "Severity:",
3927
+ filterModule: "Module:",
3928
+ filterAll: "All",
3929
+ filterAllModules: "All Modules",
3930
+ filterCountTpl: "Showing {shown} / {total} findings",
3932
3931
  // Extended \u2014 MLPS extras
3933
3932
  // Markdown report
3934
3933
  executiveSummary: "Executive Summary",
@@ -3959,6 +3958,44 @@ var enI18n = {
3959
3958
  "\u5B89\u5168\u8BA1\u7B97\u73AF\u5883": "IV. Computing Environment Security",
3960
3959
  "\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3": "V. Security Management Center"
3961
3960
  },
3961
+ // Module display names
3962
+ moduleNames: {
3963
+ service_detection: "Security Service Detection",
3964
+ secret_exposure: "Secret Exposure",
3965
+ ssl_certificate: "SSL Certificate",
3966
+ dns_dangling: "Dangling DNS",
3967
+ network_reachability: "Network Reachability",
3968
+ iam_privilege_escalation: "IAM Privilege Escalation",
3969
+ public_access_verify: "Public Access Verification",
3970
+ tag_compliance: "Tag Compliance",
3971
+ idle_resources: "Idle Resources",
3972
+ disaster_recovery: "Disaster Recovery",
3973
+ security_hub_findings: "Security Hub",
3974
+ guardduty_findings: "GuardDuty",
3975
+ inspector_findings: "Inspector",
3976
+ trusted_advisor_findings: "Trusted Advisor",
3977
+ config_rules_findings: "Config Rules",
3978
+ access_analyzer_findings: "Access Analyzer",
3979
+ patch_compliance_findings: "Patch Compliance",
3980
+ imdsv2_enforcement: "IMDSv2 Enforcement",
3981
+ waf_coverage: "WAF Coverage",
3982
+ // Security Hub sub-categories
3983
+ "sh:FSBP": "Security Best Practices",
3984
+ "sh:Inspector": "Software Vulnerabilities",
3985
+ "sh:GuardDuty": "Threat Detection",
3986
+ "sh:Config": "Configuration Compliance",
3987
+ "sh:Access Analyzer": "External Access",
3988
+ "sh:Other": "Other Security Findings"
3989
+ },
3990
+ // Security Hub sub-categories
3991
+ securityHubSubCategories: {
3992
+ FSBP: { label: "Security Best Practices" },
3993
+ Inspector: { label: "Software Vulnerabilities" },
3994
+ GuardDuty: { label: "Threat Detection" },
3995
+ Config: { label: "Configuration Compliance" },
3996
+ "Access Analyzer": { label: "External Access" },
3997
+ Other: { label: "Other Security Findings" }
3998
+ },
3962
3999
  // Service Recommendations
3963
4000
  notEnabled: "Not Enabled",
3964
4001
  serviceRecommendations: {
@@ -4163,7 +4200,7 @@ function generateMarkdownReport(scanResults, lang) {
4163
4200
  for (const m of modules) {
4164
4201
  const status = m.status === "success" ? "\u2705" : "\u274C";
4165
4202
  lines.push(
4166
- `| ${m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
4203
+ `| ${t.moduleNames[m.module] ?? m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
4167
4204
  );
4168
4205
  }
4169
4206
  lines.push("");
@@ -7006,6 +7043,22 @@ var SEV_COLOR = {
7006
7043
  LOW: "#22c55e"
7007
7044
  };
7008
7045
  var SEVERITY_ORDER2 = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
7046
+ function getRecommendationTemplate(rem) {
7047
+ return rem.replace(/\b(i-[0-9a-f]+)\b/g, "{instance}").replace(/\b(vol-[0-9a-f]+)\b/g, "{volume}").replace(/\b(sg-[0-9a-f]+)\b/g, "{sg}").replace(/\b(eipalloc-[0-9a-f]+)\b/g, "{eip}").replace(/\b(arn:aws[-\w]*:[^"\s]+)\b/g, "{arn}").replace(/"[^"]+"/g, "{name}").replace(/bucket \S+/g, "bucket {name}").replace(/instance \S+/g, "instance {id}").replace(/volume \S+/g, "volume {id}").replace(/rule \S+/g, "rule {name}");
7048
+ }
7049
+ function getSecurityHubSource(finding) {
7050
+ const impact = finding.impact ?? "";
7051
+ const match = impact.match(/^Source:\s*([^(]+)/);
7052
+ if (!match) return "Other";
7053
+ const product = match[1].trim();
7054
+ if (product === "Security Hub" || product.includes("Foundational")) return "FSBP";
7055
+ if (product === "Inspector" || product.includes("Inspector")) return "Inspector";
7056
+ if (product === "GuardDuty" || product.includes("GuardDuty")) return "GuardDuty";
7057
+ if (product === "Config" || product.includes("Config")) return "Config";
7058
+ if (product === "IAM Access Analyzer" || product.includes("Access Analyzer")) return "Access Analyzer";
7059
+ return "Other";
7060
+ }
7061
+ var SECURITY_HUB_SUB_CAT_ORDER = ["FSBP", "Inspector", "GuardDuty", "Config", "Access Analyzer", "Other"];
7009
7062
  function scoreColor(score) {
7010
7063
  if (score >= 80) return "#22c55e";
7011
7064
  if (score >= 50) return "#eab308";
@@ -7190,7 +7243,16 @@ function sharedCss() {
7190
7243
  .rec-body ol{padding-left:24px}
7191
7244
  .rec-body li{margin-bottom:8px;color:#cbd5e1;font-size:13px}
7192
7245
  .rec-body .badge{margin-right:6px;vertical-align:middle}
7246
+ .filter-toolbar{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin-bottom:20px;padding:12px 16px;background:#1e293b;border:1px solid #334155;border-radius:8px}
7247
+ .filter-group{display:flex;align-items:center;gap:8px}
7248
+ .filter-label{color:#94a3b8;font-size:13px}
7249
+ .filter-btn{padding:4px 12px;border-radius:4px;border:1px solid #475569;background:transparent;color:#cbd5e1;cursor:pointer;font-size:13px}
7250
+ .filter-btn:hover{background:#334155}
7251
+ .filter-btn.active{background:#3b82f6;border-color:#3b82f6;color:#fff}
7252
+ .filter-select{padding:4px 8px;border-radius:4px;border:1px solid #475569;background:#0f172a;color:#cbd5e1;font-size:13px}
7253
+ .filter-count{color:#64748b;font-size:13px;margin-left:auto}
7193
7254
  @media print{
7255
+ .filter-toolbar{display:none !important}
7194
7256
  body{background:#fff;color:#1e293b;-webkit-print-color-adjust:exact;print-color-adjust:exact}
7195
7257
  .container{max-width:100%;padding:20px}
7196
7258
  .card,.score-card,.stat-card,.chart-box,.finding-fold,.top5-card,.trend-chart,.category-fold,.module-fold,.finding-card,.rec-fold{background:#fff;border:1px solid #e2e8f0}
@@ -7383,6 +7445,47 @@ function generateHtmlReport(scanResults, history, lang) {
7383
7445
  const allFindings = modules.flatMap(
7384
7446
  (m) => m.findings.map((f) => ({ ...f, module: f.module ?? m.module }))
7385
7447
  );
7448
+ const shModule = modules.find((m) => m.module === "security_hub_findings");
7449
+ const shSubCats = [];
7450
+ if (shModule && shModule.findingsCount > 0) {
7451
+ const catMap = {};
7452
+ for (const f of shModule.findings) {
7453
+ const cat = getSecurityHubSource(f);
7454
+ if (!catMap[cat]) catMap[cat] = [];
7455
+ catMap[cat].push(f);
7456
+ }
7457
+ for (const cat of SECURITY_HUB_SUB_CAT_ORDER) {
7458
+ const catFindings = catMap[cat];
7459
+ if (catFindings && catFindings.length > 0) {
7460
+ const meta = t.securityHubSubCategories[cat];
7461
+ const shLabel = t.moduleNames[`sh:${cat}`] ?? meta?.label ?? cat;
7462
+ shSubCats.push({
7463
+ key: cat,
7464
+ label: shLabel,
7465
+ count: catFindings.length,
7466
+ findings: catFindings
7467
+ });
7468
+ }
7469
+ }
7470
+ }
7471
+ const DETECTION_ONLY_MODULES = /* @__PURE__ */ new Set([
7472
+ "guardduty_findings",
7473
+ "inspector_findings",
7474
+ "config_rules_findings",
7475
+ "access_analyzer_findings"
7476
+ ]);
7477
+ const barChartModules = modules.flatMap((m) => {
7478
+ if (DETECTION_ONLY_MODULES.has(m.module)) return [];
7479
+ if (m.module === "security_hub_findings" && shSubCats.length > 0) {
7480
+ return shSubCats.map((sc) => ({
7481
+ ...m,
7482
+ module: t.moduleNames[`sh:${sc.key}`] ?? sc.key,
7483
+ findingsCount: sc.count,
7484
+ findings: sc.findings
7485
+ }));
7486
+ }
7487
+ return [{ ...m, module: t.moduleNames[m.module] ?? m.module }];
7488
+ });
7386
7489
  let top5Html = "";
7387
7490
  if (allFindings.length > 0) {
7388
7491
  const top5 = [...allFindings].sort((a, b) => b.riskScore - a.riskScore).slice(0, 5);
@@ -7408,13 +7511,14 @@ function generateHtmlReport(scanResults, history, lang) {
7408
7511
  </section>`;
7409
7512
  }
7410
7513
  let findingsHtml;
7514
+ let filterToolbarHtml = "";
7411
7515
  if (summary.totalFindings === 0) {
7412
7516
  findingsHtml = `<div class="no-findings">${esc(t.noIssuesFound)}</div>`;
7413
7517
  } else {
7414
7518
  const FOLD_THRESHOLD = 20;
7415
- const renderCard = (f) => {
7519
+ const renderCard = (f, moduleKey) => {
7416
7520
  const sev = f.severity.toLowerCase();
7417
- return `<div class="finding-card sev-${esc(sev)}">
7521
+ return `<div class="finding-card sev-${esc(sev)}" data-severity="${esc(f.severity)}" data-module="${esc(moduleKey)}">
7418
7522
  <span class="badge badge-${esc(sev)}">${esc(f.severity)}</span>
7419
7523
  <span class="finding-title-text">${esc(f.title)}</span>
7420
7524
  <span class="finding-resource">${esc(f.resourceArn || f.resourceId)}</span>
@@ -7425,12 +7529,12 @@ function generateHtmlReport(scanResults, history, lang) {
7425
7529
  </div></details>
7426
7530
  </div>`;
7427
7531
  };
7428
- const renderCards = (findings) => {
7532
+ const renderCards = (findings, moduleKey) => {
7429
7533
  if (findings.length <= FOLD_THRESHOLD) {
7430
- return findings.map(renderCard).join("\n");
7534
+ return findings.map((f) => renderCard(f, moduleKey)).join("\n");
7431
7535
  }
7432
- const first = findings.slice(0, FOLD_THRESHOLD).map(renderCard).join("\n");
7433
- const rest = findings.slice(FOLD_THRESHOLD).map(renderCard).join("\n");
7536
+ const first = findings.slice(0, FOLD_THRESHOLD).map((f) => renderCard(f, moduleKey)).join("\n");
7537
+ const rest = findings.slice(FOLD_THRESHOLD).map((f) => renderCard(f, moduleKey)).join("\n");
7434
7538
  return `${first}
7435
7539
  <details><summary>${t.showRemainingFindings(findings.length - FOLD_THRESHOLD)}</summary>
7436
7540
  ${rest}
@@ -7448,37 +7552,76 @@ ${rest}
7448
7552
  if (!moduleMap.has(mod)) moduleMap.set(mod, []);
7449
7553
  moduleMap.get(mod).push(f);
7450
7554
  }
7451
- const moduleEntries = [...moduleMap.entries()].sort((a, b) => {
7555
+ const expandedEntries = [];
7556
+ for (const [mod, findings] of moduleMap.entries()) {
7557
+ if (DETECTION_ONLY_MODULES.has(mod)) continue;
7558
+ if (mod === "security_hub_findings" && shSubCats.length > 0) {
7559
+ for (const sc of shSubCats) {
7560
+ expandedEntries.push([sc.key, sc.findings, sc.label]);
7561
+ }
7562
+ } else {
7563
+ expandedEntries.push([mod, findings, null]);
7564
+ }
7565
+ }
7566
+ const moduleEntries = expandedEntries.sort((a, b) => {
7452
7567
  const aHasCritHigh = a[1].some((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
7453
7568
  const bHasCritHigh = b[1].some((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
7454
7569
  if (aHasCritHigh !== bHasCritHigh) return aHasCritHigh ? -1 : 1;
7455
7570
  return b[1].length - a[1].length;
7456
7571
  });
7457
- findingsHtml = moduleEntries.map(([modName, modFindings]) => {
7458
- const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
7459
- for (const f of modFindings) sevCounts[f.severity]++;
7460
- const badges = SEVERITY_ORDER2.filter((sev) => sevCounts[sev] > 0).map((sev) => `<span class="badge badge-${sev.toLowerCase()}">${sevCounts[sev]} ${sev.charAt(0) + sev.slice(1).toLowerCase()}</span>`).join(" ");
7461
- const sevGroups = SEVERITY_ORDER2.map((sev) => {
7462
- const findings = modFindings.filter((f) => f.severity === sev);
7463
- if (findings.length === 0) return "";
7464
- findings.sort((a, b) => b.riskScore - a.riskScore);
7572
+ const renderSeverityGroups = (findings, moduleKey) => {
7573
+ return SEVERITY_ORDER2.map((sev) => {
7574
+ const sevFindings = findings.filter((f) => f.severity === sev);
7575
+ if (sevFindings.length === 0) return "";
7576
+ sevFindings.sort((a, b) => b.riskScore - a.riskScore);
7465
7577
  const emoji = SEV_EMOJI[sev] ?? "";
7466
7578
  const label = sev.charAt(0) + sev.slice(1).toLowerCase();
7467
7579
  return `<details class="severity-group-fold">
7468
- <summary><h4>${emoji} ${label} (${findings.length})</h4></summary>
7469
- ${renderCards(findings)}
7580
+ <summary><h4>${emoji} ${label} (${sevFindings.length})</h4></summary>
7581
+ ${renderCards(sevFindings, moduleKey)}
7470
7582
  </details>`;
7471
7583
  }).filter(Boolean).join("\n");
7472
- return `<details class="module-fold">
7584
+ };
7585
+ const renderModuleBadges = (findings) => {
7586
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
7587
+ for (const f of findings) sevCounts[f.severity]++;
7588
+ return SEVERITY_ORDER2.filter((sev) => sevCounts[sev] > 0).map((sev) => `<span class="badge badge-${sev.toLowerCase()}">${sevCounts[sev]} ${sev.charAt(0) + sev.slice(1).toLowerCase()}</span>`).join(" ");
7589
+ };
7590
+ findingsHtml = moduleEntries.map(([modName, modFindings, subCatLabel]) => {
7591
+ const badges = renderModuleBadges(modFindings);
7592
+ const displayName = subCatLabel ?? (t.moduleNames[modName] ?? modName);
7593
+ return `<details class="module-fold" data-module="${esc(modName)}">
7473
7594
  <summary>
7474
- <h3>&#128274; ${esc(modName)} (${modFindings.length})</h3>
7595
+ <h3>&#128274; ${esc(displayName)} (${modFindings.length})</h3>
7475
7596
  <span class="module-badges">${badges}</span>
7476
7597
  </summary>
7477
7598
  <div class="module-body">
7478
- ${sevGroups}
7599
+ ${renderSeverityGroups(modFindings, modName)}
7479
7600
  </div>
7480
7601
  </details>`;
7481
7602
  }).join("\n");
7603
+ const moduleOptions = moduleEntries.map(([modKey, , subCatLabel]) => {
7604
+ const label = subCatLabel ?? (t.moduleNames[modKey] ?? modKey);
7605
+ return `<option value="${esc(modKey)}">${esc(label)}</option>`;
7606
+ }).join("\n ");
7607
+ filterToolbarHtml = `<div class="filter-toolbar" id="filterBar">
7608
+ <div class="filter-group">
7609
+ <span class="filter-label">${esc(t.filterSeverity)}</span>
7610
+ <button class="filter-btn active" data-severity="ALL">${esc(t.filterAll)}</button>
7611
+ <button class="filter-btn" data-severity="CRITICAL">Critical</button>
7612
+ <button class="filter-btn" data-severity="HIGH">High</button>
7613
+ <button class="filter-btn" data-severity="MEDIUM">Medium</button>
7614
+ <button class="filter-btn" data-severity="LOW">Low</button>
7615
+ </div>
7616
+ <div class="filter-group">
7617
+ <span class="filter-label">${esc(t.filterModule)}</span>
7618
+ <select class="filter-select" id="moduleFilter">
7619
+ <option value="ALL">${esc(t.filterAllModules)}</option>
7620
+ ${moduleOptions}
7621
+ </select>
7622
+ </div>
7623
+ <div class="filter-count" id="filterCount" data-tpl="${esc(t.filterCountTpl)}"></div>
7624
+ </div>`;
7482
7625
  }
7483
7626
  let trendHtml = "";
7484
7627
  if (history && history.length >= 2) {
@@ -7495,8 +7638,29 @@ ${rest}
7495
7638
  </div>
7496
7639
  </section>`;
7497
7640
  }
7498
- const statsRows = modules.map(
7499
- (m) => `<tr><td>${esc(m.module)}</td><td>${m.resourcesScanned}</td><td>${m.findingsCount}</td><td>${m.status === "success" ? "&#10003;" : "&#10007;"}</td></tr>`
7641
+ const isModuleDisabled = (m) => {
7642
+ if (!m.warnings?.length) return void 0;
7643
+ const w = m.warnings.find(
7644
+ (w2) => SERVICE_NOT_ENABLED_PATTERNS.some((p) => w2.includes(p))
7645
+ );
7646
+ return w;
7647
+ };
7648
+ const statsRows = modules.flatMap(
7649
+ (m) => {
7650
+ if (DETECTION_ONLY_MODULES.has(m.module)) return [];
7651
+ if (m.module === "security_hub_findings" && shSubCats.length > 0) {
7652
+ return shSubCats.map(
7653
+ (sc) => `<tr><td>${esc(sc.label)}</td><td>${m.resourcesScanned}</td><td>${sc.count}</td><td>&#10003;</td></tr>`
7654
+ );
7655
+ }
7656
+ const disabledWarning = isModuleDisabled(m);
7657
+ if (disabledWarning) {
7658
+ const rec = t.serviceRecommendations[m.module];
7659
+ const reason = rec ? rec.action : disabledWarning;
7660
+ return [`<tr><td>${esc(t.moduleNames[m.module] ?? m.module)}</td><td>-</td><td>-</td><td style="color:#eab308">&#9888; ${esc(reason)}</td></tr>`];
7661
+ }
7662
+ return [`<tr><td>${esc(t.moduleNames[m.module] ?? m.module)}</td><td>${m.resourcesScanned}</td><td>${m.findingsCount}</td><td>${m.status === "success" ? "&#10003;" : "&#10007;"}</td></tr>`];
7663
+ }
7500
7664
  ).join("\n");
7501
7665
  let recsHtml = "";
7502
7666
  if (summary.totalFindings > 0) {
@@ -7504,6 +7668,9 @@ ${rest}
7504
7668
  const kbPatches = [];
7505
7669
  let kbSeverity = "LOW";
7506
7670
  let kbUrl;
7671
+ const cveList = [];
7672
+ let cveSeverity = "LOW";
7673
+ let cveUrl;
7507
7674
  const genericPatterns = ["See References", "None Provided", "Review the finding", "Review and remediate."];
7508
7675
  for (const f of allFindings) {
7509
7676
  const rem = f.remediationSteps[0] ?? "Review and remediate.";
@@ -7516,6 +7683,13 @@ ${rest}
7516
7683
  if (!kbUrl && url) kbUrl = url;
7517
7684
  continue;
7518
7685
  }
7686
+ const cveMatch = f.title.match(/CVE-[\d-]+/);
7687
+ if (cveMatch && (f.module === "security_hub_findings" || f.module === "inspector_findings")) {
7688
+ cveList.push(cveMatch[0]);
7689
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(cveSeverity)) cveSeverity = f.severity;
7690
+ if (!cveUrl && url) cveUrl = url;
7691
+ continue;
7692
+ }
7519
7693
  if (f.module === "security_hub_findings") {
7520
7694
  const controlMatch = f.title.match(/^([A-Z][A-Za-z0-9]*\.\d+)\s/);
7521
7695
  if (controlMatch) {
@@ -7532,6 +7706,23 @@ ${rest}
7532
7706
  continue;
7533
7707
  }
7534
7708
  }
7709
+ if (f.module !== "security_hub_findings" && f.module !== "inspector_findings") {
7710
+ const template = getRecommendationTemplate(rem);
7711
+ if (template !== rem) {
7712
+ const templateKey = `tmpl:${f.module}:${template}`;
7713
+ const existingTmpl = recMap.get(templateKey);
7714
+ if (existingTmpl) {
7715
+ existingTmpl.count++;
7716
+ if (!existingTmpl.url && url) existingTmpl.url = url;
7717
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(existingTmpl.severity)) {
7718
+ existingTmpl.severity = f.severity;
7719
+ }
7720
+ continue;
7721
+ }
7722
+ recMap.set(templateKey, { text: rem, severity: f.severity, count: 1, url });
7723
+ continue;
7724
+ }
7725
+ }
7535
7726
  const existing = recMap.get(rem);
7536
7727
  if (existing) {
7537
7728
  existing.count++;
@@ -7548,8 +7739,14 @@ ${rest}
7548
7739
  const kbList = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
7549
7740
  recMap.set("__kb__", { text: t.installWindowsPatches(unique.length, kbList), severity: kbSeverity, count: 1, url: kbUrl });
7550
7741
  }
7742
+ if (cveList.length > 0) {
7743
+ const unique = [...new Set(cveList)];
7744
+ const cveDisplay = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
7745
+ const cveText = (lang ?? "zh") === "zh" ? `\u4FEE\u590D ${unique.length} \u4E2A\u8F6F\u4EF6\u6F0F\u6D1E (${cveDisplay})\uFF0C\u66F4\u65B0\u53D7\u5F71\u54CD\u7684\u8F6F\u4EF6\u5305\u5230\u6700\u65B0\u7248\u672C` : `Fix ${unique.length} software vulnerabilities (${cveDisplay}) \u2014 update affected packages to latest patched versions`;
7746
+ recMap.set("__cve__", { text: cveText, severity: cveSeverity, count: 1, url: cveUrl });
7747
+ }
7551
7748
  for (const [key, rec] of recMap) {
7552
- if (key.startsWith("ctrl:") && rec.count > 1) {
7749
+ if ((key.startsWith("ctrl:") || key.startsWith("tmpl:")) && rec.count > 1) {
7553
7750
  rec.text += ` \u2014 ${t.affectedResources(rec.count)}`;
7554
7751
  rec.count = 1;
7555
7752
  }
@@ -7580,6 +7777,43 @@ ${remaining.map(renderRec).join("\n")}
7580
7777
  </div>
7581
7778
  </details>`;
7582
7779
  }
7780
+ const filterScript = summary.totalFindings > 0 ? `<script>
7781
+ (function(){
7782
+ var activeSev='ALL',activeMod='ALL';
7783
+ var countEl=document.getElementById('filterCount');
7784
+ var tpl=countEl?countEl.getAttribute('data-tpl'):'';
7785
+ function apply(){
7786
+ var cards=document.querySelectorAll('.finding-card[data-severity]');
7787
+ var shown=0,total=cards.length;
7788
+ cards.forEach(function(c){
7789
+ var sevOk=activeSev==='ALL'||c.getAttribute('data-severity')===activeSev;
7790
+ var modOk=activeMod==='ALL'||c.getAttribute('data-module')===activeMod;
7791
+ c.style.display=(sevOk&&modOk)?'':'none';
7792
+ if(sevOk&&modOk)shown++;
7793
+ });
7794
+ if(countEl)countEl.textContent=tpl.replace('{shown}',shown).replace('{total}',total);
7795
+ document.querySelectorAll('.module-fold').forEach(function(f){
7796
+ var mod=f.getAttribute('data-module');
7797
+ if(activeMod!=='ALL'&&mod!==activeMod){f.style.display='none';return;}
7798
+ f.style.display='';
7799
+ });
7800
+ document.querySelectorAll('.severity-group-fold').forEach(function(g){
7801
+ g.style.display=g.querySelectorAll('.finding-card:not([style*="display: none"])').length?'':'none';
7802
+ });
7803
+ }
7804
+ document.querySelectorAll('.filter-btn[data-severity]').forEach(function(b){
7805
+ b.addEventListener('click',function(){
7806
+ document.querySelectorAll('.filter-btn[data-severity]').forEach(function(x){x.classList.remove('active')});
7807
+ b.classList.add('active');
7808
+ activeSev=b.getAttribute('data-severity');
7809
+ apply();
7810
+ });
7811
+ });
7812
+ var sel=document.getElementById('moduleFilter');
7813
+ if(sel)sel.addEventListener('change',function(){activeMod=sel.value;apply();});
7814
+ apply();
7815
+ })();
7816
+ </script>` : "";
7583
7817
  return `<!DOCTYPE html>
7584
7818
  <html lang="${htmlLang}">
7585
7819
  <head>
@@ -7616,7 +7850,7 @@ ${remaining.map(renderRec).join("\n")}
7616
7850
  </div>
7617
7851
  <div class="chart-box">
7618
7852
  <div class="chart-title">${esc(t.findingsByModule)}</div>
7619
- ${barChart(modules, t.allModulesClean)}
7853
+ ${barChart(barChartModules, t.allModulesClean)}
7620
7854
  </div>
7621
7855
  </section>
7622
7856
 
@@ -7636,6 +7870,7 @@ ${buildServiceReminderHtml(modules, lang)}
7636
7870
 
7637
7871
  <section>
7638
7872
  <h2>${esc(t.allFindings)}</h2>
7873
+ ${filterToolbarHtml}
7639
7874
  ${findingsHtml}
7640
7875
  </section>
7641
7876
 
@@ -7647,6 +7882,7 @@ ${recsHtml}
7647
7882
  </footer>
7648
7883
 
7649
7884
  </div>
7885
+ ${filterScript}
7650
7886
  </body>
7651
7887
  </html>`;
7652
7888
  }
@@ -7789,6 +8025,9 @@ ${itemsHtml}
7789
8025
  const mlpsKbPatches = [];
7790
8026
  let mlpsKbSeverity = "LOW";
7791
8027
  let mlpsKbUrl;
8028
+ const mlpsCveList = [];
8029
+ let mlpsCveSeverity = "LOW";
8030
+ let mlpsCveUrl;
7792
8031
  const mlpsGenericPatterns = ["See References", "None Provided", "Review the finding", "Review and remediate."];
7793
8032
  for (const r of failedResults) {
7794
8033
  for (const f of r.relatedFindings) {
@@ -7802,6 +8041,13 @@ ${itemsHtml}
7802
8041
  if (!mlpsKbUrl && url) mlpsKbUrl = url;
7803
8042
  continue;
7804
8043
  }
8044
+ const cveMatch = f.title.match(/CVE-[\d-]+/);
8045
+ if (cveMatch) {
8046
+ mlpsCveList.push(cveMatch[0]);
8047
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(mlpsCveSeverity)) mlpsCveSeverity = f.severity;
8048
+ if (!mlpsCveUrl && url) mlpsCveUrl = url;
8049
+ continue;
8050
+ }
7805
8051
  if (f.module === "security_hub_findings") {
7806
8052
  const controlMatch = f.title.match(/^([A-Z][A-Za-z0-9]*\.\d+)\s/);
7807
8053
  if (controlMatch) {
@@ -7818,6 +8064,23 @@ ${itemsHtml}
7818
8064
  continue;
7819
8065
  }
7820
8066
  }
8067
+ if (f.module !== "security_hub_findings" && f.module !== "inspector_findings") {
8068
+ const template = getRecommendationTemplate(rem);
8069
+ if (template !== rem) {
8070
+ const templateKey = `tmpl:${f.module}:${template}`;
8071
+ const existingTmpl = mlpsRecMap.get(templateKey);
8072
+ if (existingTmpl) {
8073
+ existingTmpl.count++;
8074
+ if (!existingTmpl.url && url) existingTmpl.url = url;
8075
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(existingTmpl.severity)) {
8076
+ existingTmpl.severity = f.severity;
8077
+ }
8078
+ continue;
8079
+ }
8080
+ mlpsRecMap.set(templateKey, { text: rem, severity: f.severity, count: 1, url });
8081
+ continue;
8082
+ }
8083
+ }
7821
8084
  const existing = mlpsRecMap.get(rem);
7822
8085
  if (existing) {
7823
8086
  existing.count++;
@@ -7835,8 +8098,14 @@ ${itemsHtml}
7835
8098
  const kbList = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
7836
8099
  mlpsRecMap.set("__kb__", { text: t.installWindowsPatches(unique.length, kbList), severity: mlpsKbSeverity, count: 1, url: mlpsKbUrl });
7837
8100
  }
8101
+ if (mlpsCveList.length > 0) {
8102
+ const unique = [...new Set(mlpsCveList)];
8103
+ const cveDisplay = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
8104
+ const cveText = (lang ?? "zh") === "zh" ? `\u4FEE\u590D ${unique.length} \u4E2A\u8F6F\u4EF6\u6F0F\u6D1E (${cveDisplay})\uFF0C\u66F4\u65B0\u53D7\u5F71\u54CD\u7684\u8F6F\u4EF6\u5305\u5230\u6700\u65B0\u7248\u672C` : `Fix ${unique.length} software vulnerabilities (${cveDisplay}) \u2014 update affected packages to latest patched versions`;
8105
+ mlpsRecMap.set("__cve__", { text: cveText, severity: mlpsCveSeverity, count: 1, url: mlpsCveUrl });
8106
+ }
7838
8107
  for (const [key, rec] of mlpsRecMap) {
7839
- if (key.startsWith("ctrl:") && rec.count > 1) {
8108
+ if ((key.startsWith("ctrl:") || key.startsWith("tmpl:")) && rec.count > 1) {
7840
8109
  rec.text += ` \u2014 ${t.affectedResources(rec.count)}`;
7841
8110
  rec.count = 1;
7842
8111
  }
@@ -8132,7 +8401,6 @@ Detects which AWS security services are enabled and assesses overall security ma
8132
8401
  - **GuardDuty not enabled** \u2014 Risk 7.5: Provides continuous threat detection.
8133
8402
  - **Inspector not enabled** \u2014 Risk 6.0: Scans for software vulnerabilities.
8134
8403
  - **AWS Config not enabled** \u2014 Risk 6.0: Tracks configuration changes.
8135
- - **Macie not enabled** \u2014 Risk 5.0: Detects sensitive data in S3 (not available in China regions).
8136
8404
  - CloudTrail detection is included for coverage metrics.
8137
8405
 
8138
8406
  ### Maturity Levels
@@ -8140,8 +8408,8 @@ Detects which AWS security services are enabled and assesses overall security ma
8140
8408
  |------------------|-------|
8141
8409
  | 0\u20131 | Basic |
8142
8410
  | 2\u20133 | Intermediate |
8143
- | 4\u20135 | Advanced |
8144
- | 6 | Comprehensive |
8411
+ | 4 | Advanced |
8412
+ | 5 | Comprehensive |
8145
8413
 
8146
8414
  ## 2. Security Hub Findings (security_hub_findings)
8147
8415
  Aggregates active findings from AWS Security Hub. Replaces individual config scanners (SG, S3, IAM, CloudTrail, RDS, EBS, VPC, etc.) with centralized compliance checks from FSBP, CIS, and PCI DSS standards.
@@ -8275,7 +8543,7 @@ import { readFileSync as readFileSync2 } from "fs";
8275
8543
  import { join as join2, dirname } from "path";
8276
8544
  import { fileURLToPath } from "url";
8277
8545
  var MODULE_DESCRIPTIONS = {
8278
- service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config, Macie) are enabled and assesses security maturity.",
8546
+ service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config) are enabled and assesses security maturity.",
8279
8547
  secret_exposure: "Checks Lambda env vars and EC2 userData for exposed secrets (AWS keys, private keys, passwords).",
8280
8548
  ssl_certificate: "Checks ACM certificates for expiry, failed status, and upcoming renewals.",
8281
8549
  dns_dangling: "Checks Route53 CNAME records for dangling DNS (subdomain takeover risk).",
@@ -8710,14 +8978,12 @@ function createServer(defaultRegion) {
8710
8978
  "Security Hub": "+300 security checks",
8711
8979
  "GuardDuty": "Threat detection",
8712
8980
  "Inspector": "Vulnerability scanning",
8713
- "AWS Config": "Configuration tracking",
8714
- "Macie": "Sensitive data detection"
8981
+ "AWS Config": "Configuration tracking"
8715
8982
  };
8716
8983
  const serviceFreeTrials = {
8717
8984
  "Security Hub": true,
8718
8985
  "GuardDuty": true,
8719
- "Inspector": true,
8720
- "Macie": true
8986
+ "Inspector": true
8721
8987
  };
8722
8988
  const services = detection.services;
8723
8989
  const coveragePercent = detection.coveragePercent;
@@ -8752,7 +9018,7 @@ function createServer(defaultRegion) {
8752
9018
  lines.push("");
8753
9019
  lines.push("### Recommendations (Priority Order)");
8754
9020
  lines.push("");
8755
- const priorityOrder = ["Security Hub", "GuardDuty", "Inspector", "AWS Config", "Macie", "CloudTrail"];
9021
+ const priorityOrder = ["Security Hub", "GuardDuty", "Inspector", "AWS Config", "CloudTrail"];
8756
9022
  const sorted = disabled.sort(
8757
9023
  (a, b) => priorityOrder.indexOf(a.name) - priorityOrder.indexOf(b.name)
8758
9024
  );
@@ -8775,7 +9041,7 @@ function createServer(defaultRegion) {
8775
9041
  const nextMilestones = {
8776
9042
  basic: { level: "Intermediate", target: 2, suggestions: ["Security Hub", "GuardDuty"] },
8777
9043
  intermediate: { level: "Advanced", target: 4, suggestions: ["Inspector", "AWS Config"] },
8778
- advanced: { level: "Comprehensive", target: 6, suggestions: ["Macie"] }
9044
+ advanced: { level: "Comprehensive", target: 5, suggestions: ["CloudTrail"] }
8779
9045
  };
8780
9046
  const next = nextMilestones[maturityLevel];
8781
9047
  if (next) {