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.
@@ -237,7 +237,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
237
237
  import { z } from "zod";
238
238
 
239
239
  // src/version.ts
240
- var VERSION = "0.6.1";
240
+ var VERSION = "0.6.3";
241
241
 
242
242
  // src/utils/aws-client.ts
243
243
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -502,10 +502,6 @@ import {
502
502
  ConfigServiceClient,
503
503
  DescribeConfigurationRecordersCommand
504
504
  } from "@aws-sdk/client-config-service";
505
- import {
506
- Macie2Client,
507
- GetMacieSessionCommand
508
- } from "@aws-sdk/client-macie2";
509
505
  import {
510
506
  CloudTrailClient,
511
507
  DescribeTrailsCommand
@@ -549,7 +545,7 @@ function isNotEnabled(err) {
549
545
  err.name === "DisabledException" || err.message.includes("not enabled") || err.message.includes("not subscribed");
550
546
  }
551
547
  function computeMaturityLevel(enabledCount) {
552
- if (enabledCount >= 6) return "comprehensive";
548
+ if (enabledCount >= 5) return "comprehensive";
553
549
  if (enabledCount >= 4) return "advanced";
554
550
  if (enabledCount >= 2) return "intermediate";
555
551
  return "basic";
@@ -853,53 +849,6 @@ var ServiceDetectionScanner = class {
853
849
  services.push({ name: "AWS Config", enabled: null, details: "Detection error" });
854
850
  }
855
851
  }
856
- if (region.startsWith("cn-")) {
857
- services.push({ name: "Macie", enabled: null, details: "Not available in China regions" });
858
- warnings.push("Macie is not available in AWS China regions.");
859
- } else {
860
- try {
861
- const mc = createClient(Macie2Client, region, ctx.credentials);
862
- await mc.send(new GetMacieSessionCommand({}));
863
- services.push({
864
- name: "Macie",
865
- enabled: true,
866
- details: "Sensitive data detection active"
867
- });
868
- } catch (err) {
869
- if (isAccessDenied(err)) {
870
- warnings.push("Macie: insufficient permissions to check status");
871
- services.push({ name: "Macie", enabled: null, details: "Access denied" });
872
- } else if (isNotEnabled(err)) {
873
- services.push({
874
- name: "Macie",
875
- enabled: false,
876
- recommendation: "Enable Macie to detect sensitive data in S3",
877
- freeTrialAvailable: true
878
- });
879
- findings.push(
880
- makeFinding({
881
- riskScore: 5,
882
- title: "Amazon Macie is not enabled",
883
- resourceType: "AWS::Macie::Session",
884
- resourceId: "macie",
885
- resourceArn: `arn:${partition}:macie2:${region}:${accountId}:session`,
886
- region,
887
- description: "Amazon Macie is not enabled in this region. Macie uses machine learning to discover and protect sensitive data stored in S3.",
888
- impact: "Detects sensitive data (PII, credentials, financial data) in S3 buckets. Without it, sensitive data exposure may go unnoticed.",
889
- remediationSteps: [
890
- "Open the Amazon Macie console.",
891
- "Click 'Get Started' and enable Macie.",
892
- "Macie offers a 30-day free trial for sensitive data discovery.",
893
- "Configure automated sensitive data discovery jobs."
894
- ]
895
- })
896
- );
897
- } else {
898
- warnings.push(`Macie detection failed: ${err instanceof Error ? err.message : String(err)}`);
899
- services.push({ name: "Macie", enabled: null, details: "Detection error" });
900
- }
901
- }
902
- }
903
852
  const knownServices = services.filter((s) => s.enabled !== null);
904
853
  const enabledCount = services.filter((s) => s.enabled === true).length;
905
854
  const coveragePercent = knownServices.length > 0 ? Math.round(enabledCount / knownServices.length * 100) : 0;
@@ -3925,6 +3874,12 @@ var zhI18n = {
3925
3874
  trendTitle: "30\u65E5\u8D8B\u52BF",
3926
3875
  findingsBySeverity: "\u6309\u4E25\u91CD\u6027\u5206\u7C7B\u7684\u53D1\u73B0",
3927
3876
  showMoreCount: (n) => `\u663E\u793A\u5269\u4F59 ${n} \u9879\u2026`,
3877
+ // Filter toolbar
3878
+ filterSeverity: "\u4E25\u91CD\u6027\uFF1A",
3879
+ filterModule: "\u6A21\u5757\uFF1A",
3880
+ filterAll: "\u5168\u90E8",
3881
+ filterAllModules: "\u5168\u90E8\u6A21\u5757",
3882
+ filterCountTpl: "\u663E\u793A {shown} / {total} \u4E2A\u53D1\u73B0",
3928
3883
  // Extended — MLPS extras
3929
3884
  // Markdown report
3930
3885
  executiveSummary: "\u6267\u884C\u6458\u8981",
@@ -3955,6 +3910,44 @@ var zhI18n = {
3955
3910
  "\u5B89\u5168\u8BA1\u7B97\u73AF\u5883": "\u56DB\u3001\u5B89\u5168\u8BA1\u7B97\u73AF\u5883",
3956
3911
  "\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3": "\u4E94\u3001\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3"
3957
3912
  },
3913
+ // Module display names
3914
+ moduleNames: {
3915
+ service_detection: "\u5B89\u5168\u670D\u52A1\u68C0\u6D4B",
3916
+ secret_exposure: "\u5BC6\u94A5\u66B4\u9732",
3917
+ ssl_certificate: "SSL \u8BC1\u4E66",
3918
+ dns_dangling: "\u60AC\u6302 DNS",
3919
+ network_reachability: "\u7F51\u7EDC\u53EF\u8FBE\u6027",
3920
+ iam_privilege_escalation: "IAM \u63D0\u6743\u5206\u6790",
3921
+ public_access_verify: "\u516C\u7F51\u8BBF\u95EE\u9A8C\u8BC1",
3922
+ tag_compliance: "\u6807\u7B7E\u5408\u89C4",
3923
+ idle_resources: "\u95F2\u7F6E\u8D44\u6E90",
3924
+ disaster_recovery: "\u707E\u5907\u8BC4\u4F30",
3925
+ security_hub_findings: "Security Hub",
3926
+ guardduty_findings: "GuardDuty",
3927
+ inspector_findings: "Inspector",
3928
+ trusted_advisor_findings: "Trusted Advisor",
3929
+ config_rules_findings: "Config Rules",
3930
+ access_analyzer_findings: "Access Analyzer",
3931
+ patch_compliance_findings: "\u8865\u4E01\u5408\u89C4",
3932
+ imdsv2_enforcement: "IMDSv2 \u5F3A\u5236",
3933
+ waf_coverage: "WAF \u8986\u76D6",
3934
+ // Security Hub sub-categories
3935
+ "sh:FSBP": "\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5",
3936
+ "sh:Inspector": "\u8F6F\u4EF6\u6F0F\u6D1E",
3937
+ "sh:GuardDuty": "\u5A01\u80C1\u68C0\u6D4B",
3938
+ "sh:Config": "\u914D\u7F6E\u5408\u89C4",
3939
+ "sh:Access Analyzer": "\u5916\u90E8\u8BBF\u95EE",
3940
+ "sh:Other": "\u5176\u4ED6\u5B89\u5168\u53D1\u73B0"
3941
+ },
3942
+ // Security Hub sub-categories
3943
+ securityHubSubCategories: {
3944
+ FSBP: { label: "\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5" },
3945
+ Inspector: { label: "\u8F6F\u4EF6\u6F0F\u6D1E" },
3946
+ GuardDuty: { label: "\u5A01\u80C1\u68C0\u6D4B" },
3947
+ Config: { label: "\u914D\u7F6E\u5408\u89C4" },
3948
+ "Access Analyzer": { label: "\u5916\u90E8\u8BBF\u95EE" },
3949
+ Other: { label: "\u5176\u4ED6\u5B89\u5168\u53D1\u73B0" }
3950
+ },
3958
3951
  // Service Recommendations
3959
3952
  notEnabled: "\u672A\u542F\u7528",
3960
3953
  serviceRecommendations: {
@@ -4157,6 +4150,12 @@ var enI18n = {
4157
4150
  trendTitle: "30-Day Trends",
4158
4151
  findingsBySeverity: "Findings by Severity",
4159
4152
  showMoreCount: (n) => `Show ${n} more\u2026`,
4153
+ // Filter toolbar
4154
+ filterSeverity: "Severity:",
4155
+ filterModule: "Module:",
4156
+ filterAll: "All",
4157
+ filterAllModules: "All Modules",
4158
+ filterCountTpl: "Showing {shown} / {total} findings",
4160
4159
  // Extended \u2014 MLPS extras
4161
4160
  // Markdown report
4162
4161
  executiveSummary: "Executive Summary",
@@ -4187,6 +4186,44 @@ var enI18n = {
4187
4186
  "\u5B89\u5168\u8BA1\u7B97\u73AF\u5883": "IV. Computing Environment Security",
4188
4187
  "\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3": "V. Security Management Center"
4189
4188
  },
4189
+ // Module display names
4190
+ moduleNames: {
4191
+ service_detection: "Security Service Detection",
4192
+ secret_exposure: "Secret Exposure",
4193
+ ssl_certificate: "SSL Certificate",
4194
+ dns_dangling: "Dangling DNS",
4195
+ network_reachability: "Network Reachability",
4196
+ iam_privilege_escalation: "IAM Privilege Escalation",
4197
+ public_access_verify: "Public Access Verification",
4198
+ tag_compliance: "Tag Compliance",
4199
+ idle_resources: "Idle Resources",
4200
+ disaster_recovery: "Disaster Recovery",
4201
+ security_hub_findings: "Security Hub",
4202
+ guardduty_findings: "GuardDuty",
4203
+ inspector_findings: "Inspector",
4204
+ trusted_advisor_findings: "Trusted Advisor",
4205
+ config_rules_findings: "Config Rules",
4206
+ access_analyzer_findings: "Access Analyzer",
4207
+ patch_compliance_findings: "Patch Compliance",
4208
+ imdsv2_enforcement: "IMDSv2 Enforcement",
4209
+ waf_coverage: "WAF Coverage",
4210
+ // Security Hub sub-categories
4211
+ "sh:FSBP": "Security Best Practices",
4212
+ "sh:Inspector": "Software Vulnerabilities",
4213
+ "sh:GuardDuty": "Threat Detection",
4214
+ "sh:Config": "Configuration Compliance",
4215
+ "sh:Access Analyzer": "External Access",
4216
+ "sh:Other": "Other Security Findings"
4217
+ },
4218
+ // Security Hub sub-categories
4219
+ securityHubSubCategories: {
4220
+ FSBP: { label: "Security Best Practices" },
4221
+ Inspector: { label: "Software Vulnerabilities" },
4222
+ GuardDuty: { label: "Threat Detection" },
4223
+ Config: { label: "Configuration Compliance" },
4224
+ "Access Analyzer": { label: "External Access" },
4225
+ Other: { label: "Other Security Findings" }
4226
+ },
4190
4227
  // Service Recommendations
4191
4228
  notEnabled: "Not Enabled",
4192
4229
  serviceRecommendations: {
@@ -4391,7 +4428,7 @@ function generateMarkdownReport(scanResults, lang) {
4391
4428
  for (const m of modules) {
4392
4429
  const status = m.status === "success" ? "\u2705" : "\u274C";
4393
4430
  lines.push(
4394
- `| ${m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
4431
+ `| ${t.moduleNames[m.module] ?? m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
4395
4432
  );
4396
4433
  }
4397
4434
  lines.push("");
@@ -7234,6 +7271,22 @@ var SEV_COLOR = {
7234
7271
  LOW: "#22c55e"
7235
7272
  };
7236
7273
  var SEVERITY_ORDER2 = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
7274
+ function getRecommendationTemplate(rem) {
7275
+ 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}");
7276
+ }
7277
+ function getSecurityHubSource(finding) {
7278
+ const impact = finding.impact ?? "";
7279
+ const match = impact.match(/^Source:\s*([^(]+)/);
7280
+ if (!match) return "Other";
7281
+ const product = match[1].trim();
7282
+ if (product === "Security Hub" || product.includes("Foundational")) return "FSBP";
7283
+ if (product === "Inspector" || product.includes("Inspector")) return "Inspector";
7284
+ if (product === "GuardDuty" || product.includes("GuardDuty")) return "GuardDuty";
7285
+ if (product === "Config" || product.includes("Config")) return "Config";
7286
+ if (product === "IAM Access Analyzer" || product.includes("Access Analyzer")) return "Access Analyzer";
7287
+ return "Other";
7288
+ }
7289
+ var SECURITY_HUB_SUB_CAT_ORDER = ["FSBP", "Inspector", "GuardDuty", "Config", "Access Analyzer", "Other"];
7237
7290
  function scoreColor(score) {
7238
7291
  if (score >= 80) return "#22c55e";
7239
7292
  if (score >= 50) return "#eab308";
@@ -7418,7 +7471,16 @@ function sharedCss() {
7418
7471
  .rec-body ol{padding-left:24px}
7419
7472
  .rec-body li{margin-bottom:8px;color:#cbd5e1;font-size:13px}
7420
7473
  .rec-body .badge{margin-right:6px;vertical-align:middle}
7474
+ .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}
7475
+ .filter-group{display:flex;align-items:center;gap:8px}
7476
+ .filter-label{color:#94a3b8;font-size:13px}
7477
+ .filter-btn{padding:4px 12px;border-radius:4px;border:1px solid #475569;background:transparent;color:#cbd5e1;cursor:pointer;font-size:13px}
7478
+ .filter-btn:hover{background:#334155}
7479
+ .filter-btn.active{background:#3b82f6;border-color:#3b82f6;color:#fff}
7480
+ .filter-select{padding:4px 8px;border-radius:4px;border:1px solid #475569;background:#0f172a;color:#cbd5e1;font-size:13px}
7481
+ .filter-count{color:#64748b;font-size:13px;margin-left:auto}
7421
7482
  @media print{
7483
+ .filter-toolbar{display:none !important}
7422
7484
  body{background:#fff;color:#1e293b;-webkit-print-color-adjust:exact;print-color-adjust:exact}
7423
7485
  .container{max-width:100%;padding:20px}
7424
7486
  .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}
@@ -7611,6 +7673,47 @@ function generateHtmlReport(scanResults, history, lang) {
7611
7673
  const allFindings = modules.flatMap(
7612
7674
  (m) => m.findings.map((f) => ({ ...f, module: f.module ?? m.module }))
7613
7675
  );
7676
+ const shModule = modules.find((m) => m.module === "security_hub_findings");
7677
+ const shSubCats = [];
7678
+ if (shModule && shModule.findingsCount > 0) {
7679
+ const catMap = {};
7680
+ for (const f of shModule.findings) {
7681
+ const cat = getSecurityHubSource(f);
7682
+ if (!catMap[cat]) catMap[cat] = [];
7683
+ catMap[cat].push(f);
7684
+ }
7685
+ for (const cat of SECURITY_HUB_SUB_CAT_ORDER) {
7686
+ const catFindings = catMap[cat];
7687
+ if (catFindings && catFindings.length > 0) {
7688
+ const meta = t.securityHubSubCategories[cat];
7689
+ const shLabel = t.moduleNames[`sh:${cat}`] ?? meta?.label ?? cat;
7690
+ shSubCats.push({
7691
+ key: cat,
7692
+ label: shLabel,
7693
+ count: catFindings.length,
7694
+ findings: catFindings
7695
+ });
7696
+ }
7697
+ }
7698
+ }
7699
+ const DETECTION_ONLY_MODULES = /* @__PURE__ */ new Set([
7700
+ "guardduty_findings",
7701
+ "inspector_findings",
7702
+ "config_rules_findings",
7703
+ "access_analyzer_findings"
7704
+ ]);
7705
+ const barChartModules = modules.flatMap((m) => {
7706
+ if (DETECTION_ONLY_MODULES.has(m.module)) return [];
7707
+ if (m.module === "security_hub_findings" && shSubCats.length > 0) {
7708
+ return shSubCats.map((sc) => ({
7709
+ ...m,
7710
+ module: t.moduleNames[`sh:${sc.key}`] ?? sc.key,
7711
+ findingsCount: sc.count,
7712
+ findings: sc.findings
7713
+ }));
7714
+ }
7715
+ return [{ ...m, module: t.moduleNames[m.module] ?? m.module }];
7716
+ });
7614
7717
  let top5Html = "";
7615
7718
  if (allFindings.length > 0) {
7616
7719
  const top5 = [...allFindings].sort((a, b) => b.riskScore - a.riskScore).slice(0, 5);
@@ -7636,13 +7739,14 @@ function generateHtmlReport(scanResults, history, lang) {
7636
7739
  </section>`;
7637
7740
  }
7638
7741
  let findingsHtml;
7742
+ let filterToolbarHtml = "";
7639
7743
  if (summary.totalFindings === 0) {
7640
7744
  findingsHtml = `<div class="no-findings">${esc(t.noIssuesFound)}</div>`;
7641
7745
  } else {
7642
7746
  const FOLD_THRESHOLD = 20;
7643
- const renderCard = (f) => {
7747
+ const renderCard = (f, moduleKey) => {
7644
7748
  const sev = f.severity.toLowerCase();
7645
- return `<div class="finding-card sev-${esc(sev)}">
7749
+ return `<div class="finding-card sev-${esc(sev)}" data-severity="${esc(f.severity)}" data-module="${esc(moduleKey)}">
7646
7750
  <span class="badge badge-${esc(sev)}">${esc(f.severity)}</span>
7647
7751
  <span class="finding-title-text">${esc(f.title)}</span>
7648
7752
  <span class="finding-resource">${esc(f.resourceArn || f.resourceId)}</span>
@@ -7653,12 +7757,12 @@ function generateHtmlReport(scanResults, history, lang) {
7653
7757
  </div></details>
7654
7758
  </div>`;
7655
7759
  };
7656
- const renderCards = (findings) => {
7760
+ const renderCards = (findings, moduleKey) => {
7657
7761
  if (findings.length <= FOLD_THRESHOLD) {
7658
- return findings.map(renderCard).join("\n");
7762
+ return findings.map((f) => renderCard(f, moduleKey)).join("\n");
7659
7763
  }
7660
- const first = findings.slice(0, FOLD_THRESHOLD).map(renderCard).join("\n");
7661
- const rest = findings.slice(FOLD_THRESHOLD).map(renderCard).join("\n");
7764
+ const first = findings.slice(0, FOLD_THRESHOLD).map((f) => renderCard(f, moduleKey)).join("\n");
7765
+ const rest = findings.slice(FOLD_THRESHOLD).map((f) => renderCard(f, moduleKey)).join("\n");
7662
7766
  return `${first}
7663
7767
  <details><summary>${t.showRemainingFindings(findings.length - FOLD_THRESHOLD)}</summary>
7664
7768
  ${rest}
@@ -7676,37 +7780,76 @@ ${rest}
7676
7780
  if (!moduleMap.has(mod)) moduleMap.set(mod, []);
7677
7781
  moduleMap.get(mod).push(f);
7678
7782
  }
7679
- const moduleEntries = [...moduleMap.entries()].sort((a, b) => {
7783
+ const expandedEntries = [];
7784
+ for (const [mod, findings] of moduleMap.entries()) {
7785
+ if (DETECTION_ONLY_MODULES.has(mod)) continue;
7786
+ if (mod === "security_hub_findings" && shSubCats.length > 0) {
7787
+ for (const sc of shSubCats) {
7788
+ expandedEntries.push([sc.key, sc.findings, sc.label]);
7789
+ }
7790
+ } else {
7791
+ expandedEntries.push([mod, findings, null]);
7792
+ }
7793
+ }
7794
+ const moduleEntries = expandedEntries.sort((a, b) => {
7680
7795
  const aHasCritHigh = a[1].some((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
7681
7796
  const bHasCritHigh = b[1].some((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
7682
7797
  if (aHasCritHigh !== bHasCritHigh) return aHasCritHigh ? -1 : 1;
7683
7798
  return b[1].length - a[1].length;
7684
7799
  });
7685
- findingsHtml = moduleEntries.map(([modName, modFindings]) => {
7686
- const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
7687
- for (const f of modFindings) sevCounts[f.severity]++;
7688
- 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(" ");
7689
- const sevGroups = SEVERITY_ORDER2.map((sev) => {
7690
- const findings = modFindings.filter((f) => f.severity === sev);
7691
- if (findings.length === 0) return "";
7692
- findings.sort((a, b) => b.riskScore - a.riskScore);
7800
+ const renderSeverityGroups = (findings, moduleKey) => {
7801
+ return SEVERITY_ORDER2.map((sev) => {
7802
+ const sevFindings = findings.filter((f) => f.severity === sev);
7803
+ if (sevFindings.length === 0) return "";
7804
+ sevFindings.sort((a, b) => b.riskScore - a.riskScore);
7693
7805
  const emoji = SEV_EMOJI[sev] ?? "";
7694
7806
  const label = sev.charAt(0) + sev.slice(1).toLowerCase();
7695
7807
  return `<details class="severity-group-fold">
7696
- <summary><h4>${emoji} ${label} (${findings.length})</h4></summary>
7697
- ${renderCards(findings)}
7808
+ <summary><h4>${emoji} ${label} (${sevFindings.length})</h4></summary>
7809
+ ${renderCards(sevFindings, moduleKey)}
7698
7810
  </details>`;
7699
7811
  }).filter(Boolean).join("\n");
7700
- return `<details class="module-fold">
7812
+ };
7813
+ const renderModuleBadges = (findings) => {
7814
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
7815
+ for (const f of findings) sevCounts[f.severity]++;
7816
+ 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(" ");
7817
+ };
7818
+ findingsHtml = moduleEntries.map(([modName, modFindings, subCatLabel]) => {
7819
+ const badges = renderModuleBadges(modFindings);
7820
+ const displayName = subCatLabel ?? (t.moduleNames[modName] ?? modName);
7821
+ return `<details class="module-fold" data-module="${esc(modName)}">
7701
7822
  <summary>
7702
- <h3>&#128274; ${esc(modName)} (${modFindings.length})</h3>
7823
+ <h3>&#128274; ${esc(displayName)} (${modFindings.length})</h3>
7703
7824
  <span class="module-badges">${badges}</span>
7704
7825
  </summary>
7705
7826
  <div class="module-body">
7706
- ${sevGroups}
7827
+ ${renderSeverityGroups(modFindings, modName)}
7707
7828
  </div>
7708
7829
  </details>`;
7709
7830
  }).join("\n");
7831
+ const moduleOptions = moduleEntries.map(([modKey, , subCatLabel]) => {
7832
+ const label = subCatLabel ?? (t.moduleNames[modKey] ?? modKey);
7833
+ return `<option value="${esc(modKey)}">${esc(label)}</option>`;
7834
+ }).join("\n ");
7835
+ filterToolbarHtml = `<div class="filter-toolbar" id="filterBar">
7836
+ <div class="filter-group">
7837
+ <span class="filter-label">${esc(t.filterSeverity)}</span>
7838
+ <button class="filter-btn active" data-severity="ALL">${esc(t.filterAll)}</button>
7839
+ <button class="filter-btn" data-severity="CRITICAL">Critical</button>
7840
+ <button class="filter-btn" data-severity="HIGH">High</button>
7841
+ <button class="filter-btn" data-severity="MEDIUM">Medium</button>
7842
+ <button class="filter-btn" data-severity="LOW">Low</button>
7843
+ </div>
7844
+ <div class="filter-group">
7845
+ <span class="filter-label">${esc(t.filterModule)}</span>
7846
+ <select class="filter-select" id="moduleFilter">
7847
+ <option value="ALL">${esc(t.filterAllModules)}</option>
7848
+ ${moduleOptions}
7849
+ </select>
7850
+ </div>
7851
+ <div class="filter-count" id="filterCount" data-tpl="${esc(t.filterCountTpl)}"></div>
7852
+ </div>`;
7710
7853
  }
7711
7854
  let trendHtml = "";
7712
7855
  if (history && history.length >= 2) {
@@ -7723,8 +7866,29 @@ ${rest}
7723
7866
  </div>
7724
7867
  </section>`;
7725
7868
  }
7726
- const statsRows = modules.map(
7727
- (m) => `<tr><td>${esc(m.module)}</td><td>${m.resourcesScanned}</td><td>${m.findingsCount}</td><td>${m.status === "success" ? "&#10003;" : "&#10007;"}</td></tr>`
7869
+ const isModuleDisabled = (m) => {
7870
+ if (!m.warnings?.length) return void 0;
7871
+ const w = m.warnings.find(
7872
+ (w2) => SERVICE_NOT_ENABLED_PATTERNS.some((p) => w2.includes(p))
7873
+ );
7874
+ return w;
7875
+ };
7876
+ const statsRows = modules.flatMap(
7877
+ (m) => {
7878
+ if (DETECTION_ONLY_MODULES.has(m.module)) return [];
7879
+ if (m.module === "security_hub_findings" && shSubCats.length > 0) {
7880
+ return shSubCats.map(
7881
+ (sc) => `<tr><td>${esc(sc.label)}</td><td>${m.resourcesScanned}</td><td>${sc.count}</td><td>&#10003;</td></tr>`
7882
+ );
7883
+ }
7884
+ const disabledWarning = isModuleDisabled(m);
7885
+ if (disabledWarning) {
7886
+ const rec = t.serviceRecommendations[m.module];
7887
+ const reason = rec ? rec.action : disabledWarning;
7888
+ return [`<tr><td>${esc(t.moduleNames[m.module] ?? m.module)}</td><td>-</td><td>-</td><td style="color:#eab308">&#9888; ${esc(reason)}</td></tr>`];
7889
+ }
7890
+ 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>`];
7891
+ }
7728
7892
  ).join("\n");
7729
7893
  let recsHtml = "";
7730
7894
  if (summary.totalFindings > 0) {
@@ -7732,6 +7896,9 @@ ${rest}
7732
7896
  const kbPatches = [];
7733
7897
  let kbSeverity = "LOW";
7734
7898
  let kbUrl;
7899
+ const cveList = [];
7900
+ let cveSeverity = "LOW";
7901
+ let cveUrl;
7735
7902
  const genericPatterns = ["See References", "None Provided", "Review the finding", "Review and remediate."];
7736
7903
  for (const f of allFindings) {
7737
7904
  const rem = f.remediationSteps[0] ?? "Review and remediate.";
@@ -7744,6 +7911,13 @@ ${rest}
7744
7911
  if (!kbUrl && url) kbUrl = url;
7745
7912
  continue;
7746
7913
  }
7914
+ const cveMatch = f.title.match(/CVE-[\d-]+/);
7915
+ if (cveMatch && (f.module === "security_hub_findings" || f.module === "inspector_findings")) {
7916
+ cveList.push(cveMatch[0]);
7917
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(cveSeverity)) cveSeverity = f.severity;
7918
+ if (!cveUrl && url) cveUrl = url;
7919
+ continue;
7920
+ }
7747
7921
  if (f.module === "security_hub_findings") {
7748
7922
  const controlMatch = f.title.match(/^([A-Z][A-Za-z0-9]*\.\d+)\s/);
7749
7923
  if (controlMatch) {
@@ -7760,6 +7934,23 @@ ${rest}
7760
7934
  continue;
7761
7935
  }
7762
7936
  }
7937
+ if (f.module !== "security_hub_findings" && f.module !== "inspector_findings") {
7938
+ const template = getRecommendationTemplate(rem);
7939
+ if (template !== rem) {
7940
+ const templateKey = `tmpl:${f.module}:${template}`;
7941
+ const existingTmpl = recMap.get(templateKey);
7942
+ if (existingTmpl) {
7943
+ existingTmpl.count++;
7944
+ if (!existingTmpl.url && url) existingTmpl.url = url;
7945
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(existingTmpl.severity)) {
7946
+ existingTmpl.severity = f.severity;
7947
+ }
7948
+ continue;
7949
+ }
7950
+ recMap.set(templateKey, { text: rem, severity: f.severity, count: 1, url });
7951
+ continue;
7952
+ }
7953
+ }
7763
7954
  const existing = recMap.get(rem);
7764
7955
  if (existing) {
7765
7956
  existing.count++;
@@ -7776,8 +7967,14 @@ ${rest}
7776
7967
  const kbList = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
7777
7968
  recMap.set("__kb__", { text: t.installWindowsPatches(unique.length, kbList), severity: kbSeverity, count: 1, url: kbUrl });
7778
7969
  }
7970
+ if (cveList.length > 0) {
7971
+ const unique = [...new Set(cveList)];
7972
+ const cveDisplay = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
7973
+ 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`;
7974
+ recMap.set("__cve__", { text: cveText, severity: cveSeverity, count: 1, url: cveUrl });
7975
+ }
7779
7976
  for (const [key, rec] of recMap) {
7780
- if (key.startsWith("ctrl:") && rec.count > 1) {
7977
+ if ((key.startsWith("ctrl:") || key.startsWith("tmpl:")) && rec.count > 1) {
7781
7978
  rec.text += ` \u2014 ${t.affectedResources(rec.count)}`;
7782
7979
  rec.count = 1;
7783
7980
  }
@@ -7808,6 +8005,43 @@ ${remaining.map(renderRec).join("\n")}
7808
8005
  </div>
7809
8006
  </details>`;
7810
8007
  }
8008
+ const filterScript = summary.totalFindings > 0 ? `<script>
8009
+ (function(){
8010
+ var activeSev='ALL',activeMod='ALL';
8011
+ var countEl=document.getElementById('filterCount');
8012
+ var tpl=countEl?countEl.getAttribute('data-tpl'):'';
8013
+ function apply(){
8014
+ var cards=document.querySelectorAll('.finding-card[data-severity]');
8015
+ var shown=0,total=cards.length;
8016
+ cards.forEach(function(c){
8017
+ var sevOk=activeSev==='ALL'||c.getAttribute('data-severity')===activeSev;
8018
+ var modOk=activeMod==='ALL'||c.getAttribute('data-module')===activeMod;
8019
+ c.style.display=(sevOk&&modOk)?'':'none';
8020
+ if(sevOk&&modOk)shown++;
8021
+ });
8022
+ if(countEl)countEl.textContent=tpl.replace('{shown}',shown).replace('{total}',total);
8023
+ document.querySelectorAll('.module-fold').forEach(function(f){
8024
+ var mod=f.getAttribute('data-module');
8025
+ if(activeMod!=='ALL'&&mod!==activeMod){f.style.display='none';return;}
8026
+ f.style.display='';
8027
+ });
8028
+ document.querySelectorAll('.severity-group-fold').forEach(function(g){
8029
+ g.style.display=g.querySelectorAll('.finding-card:not([style*="display: none"])').length?'':'none';
8030
+ });
8031
+ }
8032
+ document.querySelectorAll('.filter-btn[data-severity]').forEach(function(b){
8033
+ b.addEventListener('click',function(){
8034
+ document.querySelectorAll('.filter-btn[data-severity]').forEach(function(x){x.classList.remove('active')});
8035
+ b.classList.add('active');
8036
+ activeSev=b.getAttribute('data-severity');
8037
+ apply();
8038
+ });
8039
+ });
8040
+ var sel=document.getElementById('moduleFilter');
8041
+ if(sel)sel.addEventListener('change',function(){activeMod=sel.value;apply();});
8042
+ apply();
8043
+ })();
8044
+ </script>` : "";
7811
8045
  return `<!DOCTYPE html>
7812
8046
  <html lang="${htmlLang}">
7813
8047
  <head>
@@ -7844,7 +8078,7 @@ ${remaining.map(renderRec).join("\n")}
7844
8078
  </div>
7845
8079
  <div class="chart-box">
7846
8080
  <div class="chart-title">${esc(t.findingsByModule)}</div>
7847
- ${barChart(modules, t.allModulesClean)}
8081
+ ${barChart(barChartModules, t.allModulesClean)}
7848
8082
  </div>
7849
8083
  </section>
7850
8084
 
@@ -7864,6 +8098,7 @@ ${buildServiceReminderHtml(modules, lang)}
7864
8098
 
7865
8099
  <section>
7866
8100
  <h2>${esc(t.allFindings)}</h2>
8101
+ ${filterToolbarHtml}
7867
8102
  ${findingsHtml}
7868
8103
  </section>
7869
8104
 
@@ -7875,6 +8110,7 @@ ${recsHtml}
7875
8110
  </footer>
7876
8111
 
7877
8112
  </div>
8113
+ ${filterScript}
7878
8114
  </body>
7879
8115
  </html>`;
7880
8116
  }
@@ -8017,6 +8253,9 @@ ${itemsHtml}
8017
8253
  const mlpsKbPatches = [];
8018
8254
  let mlpsKbSeverity = "LOW";
8019
8255
  let mlpsKbUrl;
8256
+ const mlpsCveList = [];
8257
+ let mlpsCveSeverity = "LOW";
8258
+ let mlpsCveUrl;
8020
8259
  const mlpsGenericPatterns = ["See References", "None Provided", "Review the finding", "Review and remediate."];
8021
8260
  for (const r of failedResults) {
8022
8261
  for (const f of r.relatedFindings) {
@@ -8030,6 +8269,13 @@ ${itemsHtml}
8030
8269
  if (!mlpsKbUrl && url) mlpsKbUrl = url;
8031
8270
  continue;
8032
8271
  }
8272
+ const cveMatch = f.title.match(/CVE-[\d-]+/);
8273
+ if (cveMatch) {
8274
+ mlpsCveList.push(cveMatch[0]);
8275
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(mlpsCveSeverity)) mlpsCveSeverity = f.severity;
8276
+ if (!mlpsCveUrl && url) mlpsCveUrl = url;
8277
+ continue;
8278
+ }
8033
8279
  if (f.module === "security_hub_findings") {
8034
8280
  const controlMatch = f.title.match(/^([A-Z][A-Za-z0-9]*\.\d+)\s/);
8035
8281
  if (controlMatch) {
@@ -8046,6 +8292,23 @@ ${itemsHtml}
8046
8292
  continue;
8047
8293
  }
8048
8294
  }
8295
+ if (f.module !== "security_hub_findings" && f.module !== "inspector_findings") {
8296
+ const template = getRecommendationTemplate(rem);
8297
+ if (template !== rem) {
8298
+ const templateKey = `tmpl:${f.module}:${template}`;
8299
+ const existingTmpl = mlpsRecMap.get(templateKey);
8300
+ if (existingTmpl) {
8301
+ existingTmpl.count++;
8302
+ if (!existingTmpl.url && url) existingTmpl.url = url;
8303
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(existingTmpl.severity)) {
8304
+ existingTmpl.severity = f.severity;
8305
+ }
8306
+ continue;
8307
+ }
8308
+ mlpsRecMap.set(templateKey, { text: rem, severity: f.severity, count: 1, url });
8309
+ continue;
8310
+ }
8311
+ }
8049
8312
  const existing = mlpsRecMap.get(rem);
8050
8313
  if (existing) {
8051
8314
  existing.count++;
@@ -8063,8 +8326,14 @@ ${itemsHtml}
8063
8326
  const kbList = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
8064
8327
  mlpsRecMap.set("__kb__", { text: t.installWindowsPatches(unique.length, kbList), severity: mlpsKbSeverity, count: 1, url: mlpsKbUrl });
8065
8328
  }
8329
+ if (mlpsCveList.length > 0) {
8330
+ const unique = [...new Set(mlpsCveList)];
8331
+ const cveDisplay = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
8332
+ 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`;
8333
+ mlpsRecMap.set("__cve__", { text: cveText, severity: mlpsCveSeverity, count: 1, url: mlpsCveUrl });
8334
+ }
8066
8335
  for (const [key, rec] of mlpsRecMap) {
8067
- if (key.startsWith("ctrl:") && rec.count > 1) {
8336
+ if ((key.startsWith("ctrl:") || key.startsWith("tmpl:")) && rec.count > 1) {
8068
8337
  rec.text += ` \u2014 ${t.affectedResources(rec.count)}`;
8069
8338
  rec.count = 1;
8070
8339
  }
@@ -8360,7 +8629,6 @@ Detects which AWS security services are enabled and assesses overall security ma
8360
8629
  - **GuardDuty not enabled** \u2014 Risk 7.5: Provides continuous threat detection.
8361
8630
  - **Inspector not enabled** \u2014 Risk 6.0: Scans for software vulnerabilities.
8362
8631
  - **AWS Config not enabled** \u2014 Risk 6.0: Tracks configuration changes.
8363
- - **Macie not enabled** \u2014 Risk 5.0: Detects sensitive data in S3 (not available in China regions).
8364
8632
  - CloudTrail detection is included for coverage metrics.
8365
8633
 
8366
8634
  ### Maturity Levels
@@ -8368,8 +8636,8 @@ Detects which AWS security services are enabled and assesses overall security ma
8368
8636
  |------------------|-------|
8369
8637
  | 0\u20131 | Basic |
8370
8638
  | 2\u20133 | Intermediate |
8371
- | 4\u20135 | Advanced |
8372
- | 6 | Comprehensive |
8639
+ | 4 | Advanced |
8640
+ | 5 | Comprehensive |
8373
8641
 
8374
8642
  ## 2. Security Hub Findings (security_hub_findings)
8375
8643
  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.
@@ -8503,7 +8771,7 @@ import { readFileSync as readFileSync2 } from "fs";
8503
8771
  import { join as join2, dirname } from "path";
8504
8772
  import { fileURLToPath } from "url";
8505
8773
  var MODULE_DESCRIPTIONS = {
8506
- service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config, Macie) are enabled and assesses security maturity.",
8774
+ service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config) are enabled and assesses security maturity.",
8507
8775
  secret_exposure: "Checks Lambda env vars and EC2 userData for exposed secrets (AWS keys, private keys, passwords).",
8508
8776
  ssl_certificate: "Checks ACM certificates for expiry, failed status, and upcoming renewals.",
8509
8777
  dns_dangling: "Checks Route53 CNAME records for dangling DNS (subdomain takeover risk).",
@@ -8938,14 +9206,12 @@ function createServer(defaultRegion) {
8938
9206
  "Security Hub": "+300 security checks",
8939
9207
  "GuardDuty": "Threat detection",
8940
9208
  "Inspector": "Vulnerability scanning",
8941
- "AWS Config": "Configuration tracking",
8942
- "Macie": "Sensitive data detection"
9209
+ "AWS Config": "Configuration tracking"
8943
9210
  };
8944
9211
  const serviceFreeTrials = {
8945
9212
  "Security Hub": true,
8946
9213
  "GuardDuty": true,
8947
- "Inspector": true,
8948
- "Macie": true
9214
+ "Inspector": true
8949
9215
  };
8950
9216
  const services = detection.services;
8951
9217
  const coveragePercent = detection.coveragePercent;
@@ -8980,7 +9246,7 @@ function createServer(defaultRegion) {
8980
9246
  lines.push("");
8981
9247
  lines.push("### Recommendations (Priority Order)");
8982
9248
  lines.push("");
8983
- const priorityOrder = ["Security Hub", "GuardDuty", "Inspector", "AWS Config", "Macie", "CloudTrail"];
9249
+ const priorityOrder = ["Security Hub", "GuardDuty", "Inspector", "AWS Config", "CloudTrail"];
8984
9250
  const sorted = disabled.sort(
8985
9251
  (a, b) => priorityOrder.indexOf(a.name) - priorityOrder.indexOf(b.name)
8986
9252
  );
@@ -9003,7 +9269,7 @@ function createServer(defaultRegion) {
9003
9269
  const nextMilestones = {
9004
9270
  basic: { level: "Intermediate", target: 2, suggestions: ["Security Hub", "GuardDuty"] },
9005
9271
  intermediate: { level: "Advanced", target: 4, suggestions: ["Inspector", "AWS Config"] },
9006
- advanced: { level: "Comprehensive", target: 6, suggestions: ["Macie"] }
9272
+ advanced: { level: "Comprehensive", target: 5, suggestions: ["CloudTrail"] }
9007
9273
  };
9008
9274
  const next = nextMilestones[maturityLevel];
9009
9275
  if (next) {