aws-security-mcp 0.7.4 → 0.7.6

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.
@@ -49,6 +49,8 @@ interface FullScanResult {
49
49
  region: string;
50
50
  accountId: string;
51
51
  modules: ScanResult[];
52
+ /** Optional pre-generated AI executive summary (client AI supplies; server never calls an LLM). */
53
+ aiSummary?: string;
52
54
  summary: {
53
55
  totalFindings: number;
54
56
  critical: number;
@@ -81,6 +83,8 @@ interface DashboardData {
81
83
  status: string;
82
84
  }>;
83
85
  findings: Finding[];
86
+ /** Optional pre-generated AI executive summary. */
87
+ aiSummary?: string;
84
88
  };
85
89
  history: DashboardHistoryEntry[];
86
90
  meta: {
@@ -124,15 +128,25 @@ declare function listOrgAccounts(region: string): Promise<OrgAccount[]>;
124
128
 
125
129
  declare function generateMarkdownReport(scanResults: FullScanResult, lang?: Lang): string;
126
130
 
131
+ declare function generateMlps3Report(scanResults: FullScanResult, lang?: Lang): string;
132
+
127
133
  declare function generateHtmlReport(scanResults: FullScanResult, history?: DashboardHistoryEntry[], lang?: Lang): string;
128
134
  declare function generateMlps3HtmlReport(scanResults: FullScanResult, history?: DashboardHistoryEntry[], lang?: Lang): string;
129
135
 
130
136
  declare function generateHwDefenseHtmlReport(scanResults: FullScanResult, lang?: Lang): string;
131
137
 
138
+ type ReportType = "dashboard" | "html" | "hw_defense" | "mlps3";
139
+ /**
140
+ * Build a ready-to-run prompt for the calling client AI to produce a
141
+ * report-type-tailored AI summary. The server never calls an LLM itself —
142
+ * it only assembles this prompt + a grounded findings digest.
143
+ */
144
+ declare function buildAiSummaryPrompt(type: ReportType, scan: FullScanResult, lang: Lang): string;
145
+
132
146
  declare function calculateScore(summary: FullScanResult["summary"]): number;
133
147
  declare function saveResults(scanResults: FullScanResult, outputDir?: string): string;
134
148
 
135
149
  declare function createServer(defaultRegion: string): McpServer;
136
150
  declare function startServer(defaultRegion: string): Promise<void>;
137
151
 
138
- export { type DashboardData, type DashboardHistoryEntry, type Finding, type FullScanResult, type Lang, type OrgAccount, type Priority, type ScanContext, type ScanResult, type Scanner, type Severity, assumeRole, buildRoleArn, calculateScore, createServer, generateHtmlReport, generateHwDefenseHtmlReport, generateMarkdownReport, generateMlps3HtmlReport, getCurrentAccountId, listOrgAccounts, runAllScanners, runMultiAccountScanners, saveResults, startServer };
152
+ export { type DashboardData, type DashboardHistoryEntry, type Finding, type FullScanResult, type Lang, type OrgAccount, type Priority, type ReportType, type ScanContext, type ScanResult, type Scanner, type Severity, assumeRole, buildAiSummaryPrompt, buildRoleArn, calculateScore, createServer, generateHtmlReport, generateHwDefenseHtmlReport, generateMarkdownReport, generateMlps3HtmlReport, generateMlps3Report, getCurrentAccountId, listOrgAccounts, runAllScanners, runMultiAccountScanners, saveResults, startServer };
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.7.4";
7
+ var VERSION = "0.7.6";
8
8
 
9
9
  // src/utils/aws-client.ts
10
10
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -84,8 +84,7 @@ async function listOrgAccounts(region) {
84
84
  return accounts;
85
85
  }
86
86
 
87
- // src/scanners/runner.ts
88
- var DEFAULT_CONCURRENCY = 5;
87
+ // src/utils/concurrency.ts
89
88
  async function runWithConcurrency(tasks, limit) {
90
89
  const results = [];
91
90
  const executing = /* @__PURE__ */ new Set();
@@ -104,6 +103,9 @@ async function runWithConcurrency(tasks, limit) {
104
103
  await Promise.all(executing);
105
104
  return results;
106
105
  }
106
+
107
+ // src/scanners/runner.ts
108
+ var DEFAULT_CONCURRENCY = 5;
107
109
  var AGGREGATION_MODULES = /* @__PURE__ */ new Set([
108
110
  "security_hub_findings",
109
111
  "guardduty_findings",
@@ -674,24 +676,6 @@ import {
674
676
  DescribeInstanceAttributeCommand
675
677
  } from "@aws-sdk/client-ec2";
676
678
  var USERDATA_CONCURRENCY = 5;
677
- async function runWithConcurrency2(tasks, limit) {
678
- const results = [];
679
- const executing = /* @__PURE__ */ new Set();
680
- for (let i = 0; i < tasks.length; i++) {
681
- const idx = i;
682
- const p = tasks[idx]().then((value) => {
683
- results[idx] = { status: "fulfilled", value };
684
- }).catch((reason) => {
685
- results[idx] = { status: "rejected", reason };
686
- }).finally(() => {
687
- executing.delete(p);
688
- });
689
- executing.add(p);
690
- if (executing.size >= limit) await Promise.race(executing);
691
- }
692
- await Promise.all(executing);
693
- return results;
694
- }
695
679
  var SECRET_PATTERNS = [
696
680
  { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/, matchType: "value" },
697
681
  { name: "Private Key", pattern: /-----BEGIN.*PRIVATE KEY-----/, matchType: "value" },
@@ -802,7 +786,7 @@ var SecretExposureScanner = class {
802
786
  const raw = attrResp.UserData?.Value;
803
787
  return { instId, userData: raw ? Buffer.from(raw, "base64").toString("utf-8") : void 0 };
804
788
  });
805
- const settled = await runWithConcurrency2(userDataTasks, USERDATA_CONCURRENCY);
789
+ const settled = await runWithConcurrency(userDataTasks, USERDATA_CONCURRENCY);
806
790
  for (let i = 0; i < instances.length; i++) {
807
791
  const result = settled[i];
808
792
  const inst = instances[i];
@@ -1782,7 +1766,8 @@ async function getBucketRegion(client, bucketName, defaultRegion, warnings) {
1782
1766
  const resp = await client.send(
1783
1767
  new GetBucketLocationCommand({ Bucket: bucketName })
1784
1768
  );
1785
- const loc = String(resp.LocationConstraint ?? "") || "us-east-1";
1769
+ const rawLoc = String(resp.LocationConstraint ?? "") || "us-east-1";
1770
+ const loc = rawLoc === "EU" ? "eu-west-1" : rawLoc;
1786
1771
  return loc;
1787
1772
  } catch (e) {
1788
1773
  const msg = e instanceof Error ? e.message : String(e);
@@ -3738,6 +3723,7 @@ var zhI18n = {
3738
3723
  // Extended — MLPS extras
3739
3724
  // Markdown report
3740
3725
  executiveSummary: "\u6267\u884C\u6458\u8981",
3726
+ aiSummaryTitle: "AI \u5B89\u5168\u6001\u52BF\u603B\u7ED3",
3741
3727
  totalFindingsLabel: "\u53D1\u73B0\u603B\u6570",
3742
3728
  description: "\u63CF\u8FF0",
3743
3729
  priority: "\u4F18\u5148\u7EA7",
@@ -4047,6 +4033,7 @@ var enI18n = {
4047
4033
  // Extended \u2014 MLPS extras
4048
4034
  // Markdown report
4049
4035
  executiveSummary: "Executive Summary",
4036
+ aiSummaryTitle: "AI Security Summary",
4050
4037
  totalFindingsLabel: "Total Findings",
4051
4038
  description: "Description",
4052
4039
  priority: "Priority",
@@ -4308,6 +4295,14 @@ function generateMarkdownReport(scanResults, lang) {
4308
4295
  `- **${t.totalFindingsLabel}:** ${summary.totalFindings} (${SEVERITY_ICON.CRITICAL} ${summary.critical} ${t.critical} | ${SEVERITY_ICON.HIGH} ${summary.high} ${t.high} | ${SEVERITY_ICON.MEDIUM} ${summary.medium} ${t.medium} | ${SEVERITY_ICON.LOW} ${summary.low} ${t.low})`
4309
4296
  );
4310
4297
  lines.push("");
4298
+ if (scanResults.aiSummary && scanResults.aiSummary.trim()) {
4299
+ lines.push(`> \u2728 **${t.aiSummaryTitle}**`);
4300
+ lines.push(">");
4301
+ for (const ln of scanResults.aiSummary.trim().split(/\r?\n/)) {
4302
+ lines.push(`> ${ln}`);
4303
+ }
4304
+ lines.push("");
4305
+ }
4311
4306
  if (summary.totalFindings === 0) {
4312
4307
  lines.push(`## ${t.findingsBySeverity}`);
4313
4308
  lines.push("");
@@ -7085,6 +7080,14 @@ function generateMlps3Report(scanResults, lang) {
7085
7080
  lines.push(`## ${t.accountInfo}`);
7086
7081
  lines.push(`- ${t.account}: ${accountId} | ${t.region}: ${region} | ${t.scanTime}: ${scanTime}`);
7087
7082
  lines.push("");
7083
+ if (scanResults.aiSummary && scanResults.aiSummary.trim()) {
7084
+ lines.push(`> \u2728 **${t.aiSummaryTitle}**`);
7085
+ lines.push(">");
7086
+ for (const ln of scanResults.aiSummary.trim().split(/\r?\n/)) {
7087
+ lines.push(`> ${ln}`);
7088
+ }
7089
+ lines.push("");
7090
+ }
7088
7091
  lines.push(`## ${t.preCheckOverview}`);
7089
7092
  lines.push(`- ${t.checkedCount(checkedTotal, autoClean, autoIssues)}`);
7090
7093
  if (autoUnknown > 0) {
@@ -7187,6 +7190,14 @@ function escWithLinks(s) {
7187
7190
  return esc(part);
7188
7191
  }).join("");
7189
7192
  }
7193
+ function aiSummaryBlock(summary, title) {
7194
+ const text = (summary ?? "").trim();
7195
+ if (!text) return "";
7196
+ return `<div class="ai-summary">
7197
+ <div class="ai-summary-title">\u2728 ${esc(title)}</div>
7198
+ <div class="ai-summary-body">${esc(text)}</div>
7199
+ </div>`;
7200
+ }
7190
7201
  function calcScore(summary) {
7191
7202
  const raw = 100 - summary.critical * 15 - summary.high * 5 - summary.medium * 2 - summary.low * 0.5;
7192
7203
  return Math.max(0, Math.min(100, Math.round(raw)));
@@ -7431,7 +7442,13 @@ function sharedCss() {
7431
7442
  details{display:block}
7432
7443
  details>summary{display:block}
7433
7444
  details>:not(summary){display:block !important}
7445
+ .ai-summary{background:#f5f3ff !important;border-color:#c7d2fe !important}
7446
+ .ai-summary-title{color:#4338ca !important}
7447
+ .ai-summary-body{color:#1e293b !important}
7434
7448
  }
7449
+ .ai-summary{background:linear-gradient(135deg,#1e293b,#1e1b4b);border:1px solid #4338ca;border-radius:10px;padding:16px 20px;margin:0 0 28px}
7450
+ .ai-summary-title{font-size:14px;font-weight:700;color:#a5b4fc;margin-bottom:8px}
7451
+ .ai-summary-body{font-size:14px;line-height:1.7;color:#cbd5e1;white-space:pre-wrap}
7435
7452
  `;
7436
7453
  }
7437
7454
  function donutChart(summary) {
@@ -7983,6 +8000,8 @@ ${remaining.map(renderRec).join("\n")}
7983
8000
  <div class="meta">${esc(t.account)}: ${esc(accountId)} | ${esc(t.region)}: ${esc(region)} | ${esc(date)} | ${esc(t.duration)}: ${esc(duration)}</div>
7984
8001
  </header>
7985
8002
 
8003
+ ${aiSummaryBlock(scanResults.aiSummary, t.aiSummaryTitle)}
8004
+
7986
8005
  <section class="summary">
7987
8006
  <div class="score-card">
7988
8007
  <div class="score-value" style="color:${scoreColor(score)}">${score}</div>
@@ -8330,6 +8349,8 @@ ${mlpsRemaining.map(renderMlpsRec).join("\n")}
8330
8349
  <div class="meta">${esc(t.account)}: ${esc(accountId)} | ${esc(t.region)}: ${esc(region)} | ${esc(t.scanTime)}: ${esc(scanTime)}</div>
8331
8350
  </header>
8332
8351
 
8352
+ ${aiSummaryBlock(scanResults.aiSummary, t.aiSummaryTitle)}
8353
+
8333
8354
  <section class="summary" style="display:block;text-align:center">
8334
8355
  <div style="font-size:36px;font-weight:700;margin-bottom:12px">
8335
8356
  <span style="color:#22c55e">${autoClean}</span> <span style="color:#94a3b8;font-size:18px">${esc(t.noIssues)}</span>
@@ -8378,6 +8399,14 @@ function safeUrl2(url) {
8378
8399
  return null;
8379
8400
  }
8380
8401
  }
8402
+ function aiSummaryBlock2(summary, title) {
8403
+ const text = (summary ?? "").trim();
8404
+ if (!text) return "";
8405
+ return `<div class="ai-summary">
8406
+ <div class="ai-summary-title">\u2728 ${esc2(title)}</div>
8407
+ <div class="ai-summary-body">${esc2(text)}</div>
8408
+ </div>`;
8409
+ }
8381
8410
  function escWithLinks2(s) {
8382
8411
  const parts = s.split(/(https?:\/\/\S+)/);
8383
8412
  return parts.map((part, i) => {
@@ -8513,7 +8542,13 @@ function hwCss() {
8513
8542
  details{display:block}
8514
8543
  details>summary{display:block}
8515
8544
  details>:not(summary){display:block !important}
8545
+ .ai-summary{background:#f5f3ff !important;border-color:#c7d2fe !important}
8546
+ .ai-summary-title{color:#4338ca !important}
8547
+ .ai-summary-body{color:#1e293b !important}
8516
8548
  }
8549
+ .ai-summary{background:linear-gradient(135deg,#1e293b,#1e1b4b);border:1px solid #4338ca;border-radius:10px;padding:16px 20px;margin:0 0 28px}
8550
+ .ai-summary-title{font-size:14px;font-weight:700;color:#a5b4fc;margin-bottom:8px}
8551
+ .ai-summary-body{font-size:14px;line-height:1.7;color:#cbd5e1;white-space:pre-wrap}
8517
8552
  `;
8518
8553
  }
8519
8554
  function generateHwDefenseHtmlReport(scanResults, lang) {
@@ -8714,6 +8749,8 @@ function generateHwDefenseHtmlReport(scanResults, lang) {
8714
8749
  <div class="meta">${esc2(t.account)}: ${esc2(accountId)} | ${esc2(t.region)}: ${esc2(region)} | ${esc2(t.scanTime)}: ${esc2(scanTime)}</div>
8715
8750
  </header>
8716
8751
 
8752
+ ${aiSummaryBlock2(scanResults.aiSummary, t.aiSummaryTitle)}
8753
+
8717
8754
  <section class="summary-cards">
8718
8755
  <div class="summary-card"><div class="stat-count" style="color:${findingsColor}">${totalFindings}</div><div class="stat-label">${esc2(t.hwTotalFindings)}</div></div>
8719
8756
  <div class="summary-card"><div class="stat-count" style="color:#60a5fa">${sectionsChecked}</div><div class="stat-label">${esc2(t.hwSectionsChecked)}</div></div>
@@ -8733,6 +8770,137 @@ ${sectionsHtml}
8733
8770
  </html>`;
8734
8771
  }
8735
8772
 
8773
+ // src/tools/ai-summary-prompt.ts
8774
+ function buildFindingsDigest(scan, lang, topN = 12) {
8775
+ const zh = lang === "zh";
8776
+ const all = scan.modules.flatMap(
8777
+ (m) => m.findings.map((f) => ({ ...f, module: f.module ?? m.module }))
8778
+ );
8779
+ const s = scan.summary;
8780
+ const byModule = [...scan.modules].filter((m) => m.findingsCount > 0).sort((a, b) => b.findingsCount - a.findingsCount).slice(0, 19).map((m) => `${m.module}=${m.findingsCount}`).join(", ");
8781
+ const top = [...all].sort((a, b) => b.riskScore - a.riskScore).slice(0, topN);
8782
+ const topLines = top.map(
8783
+ (f, i) => `${i + 1}. [${f.severity}/${f.priority}] ${f.title} \u2014 ${f.resourceType} ${f.resourceId} (${f.region}) risk=${f.riskScore}`
8784
+ ).join("\n");
8785
+ const head = zh ? `\u8D26\u53F7: ${scan.accountId} | \u533A\u57DF: ${scan.region}
8786
+ \u98CE\u9669\u603B\u6570: ${s.totalFindings} (CRITICAL ${s.critical} / HIGH ${s.high} / MEDIUM ${s.medium} / LOW ${s.low})
8787
+ \u6A21\u5757\u5206\u5E03: ${byModule || "\u65E0"}
8788
+
8789
+ \u9AD8\u98CE\u9669 Top ${top.length}:
8790
+ ${topLines || "\u65E0"}` : `Account: ${scan.accountId} | Region: ${scan.region}
8791
+ Total findings: ${s.totalFindings} (CRITICAL ${s.critical} / HIGH ${s.high} / MEDIUM ${s.medium} / LOW ${s.low})
8792
+ Module breakdown: ${byModule || "none"}
8793
+
8794
+ Top ${top.length} by risk:
8795
+ ${topLines || "none"}`;
8796
+ return head;
8797
+ }
8798
+ function getProfile(type, lang) {
8799
+ const zh = lang === "zh";
8800
+ switch (type) {
8801
+ case "dashboard":
8802
+ return zh ? {
8803
+ persona: "\u4F60\u5728\u4E3A\u5B89\u5168\u8FD0\u8425\u4EEA\u8868\u76D8\u5199\u4E00\u6BB5\u9AD8\u7BA1\u901F\u89C8\u3002\u8BFB\u8005\u662F CISO / \u7BA1\u7406\u5C42\uFF0C\u65F6\u95F4\u5F88\u5C11\u3002",
8804
+ focus: "\u6574\u4F53\u5B89\u5168\u6001\u52BF\u4E00\u53E5\u8BDD\u5B9A\u8C03 + \u5B89\u5168\u8BC4\u5206\u89E3\u8BFB + \u6B64\u523B\u6700\u8BE5\u5173\u6CE8\u7684 3 \u4EF6\u4E8B\uFF08\u6309\u4E1A\u52A1\u5F71\u54CD\u6392\u5E8F\uFF09\u3002\u4E0D\u8981\u7F57\u5217\u6240\u6709 finding\u3002",
8805
+ format: "3-5 \u53E5\u8BDD\uFF0C\u5F00\u5934\u4E00\u53E5\u7ED3\u8BBA\u5148\u884C\u3002\u4E2D\u6587\u3002\u4E0D\u7528 Markdown \u6807\u9898\uFF0C\u53EF\u7528\u7B80\u77ED\u5206\u53F7\u6216\u6362\u884C\u3002\u8BED\u6C14\u4E13\u4E1A\u3001\u514B\u5236\u3001\u53EF\u6267\u884C\u3002"
8806
+ } : {
8807
+ persona: "You are writing an executive at-a-glance summary for a security operations dashboard. Readers are CISO/leadership with little time.",
8808
+ focus: "One-line overall posture verdict + interpretation of the security score + the top 3 things to focus on right now (ordered by business impact). Do NOT list every finding.",
8809
+ format: "3-5 sentences, conclusion first. English. No Markdown headings; short line breaks ok. Professional, restrained, actionable tone."
8810
+ };
8811
+ case "html":
8812
+ return zh ? {
8813
+ persona: "\u4F60\u5728\u4E3A\u4E00\u4EFD\u5B8C\u6574\u7684 AWS \u5B89\u5168\u626B\u63CF\u62A5\u544A\u5199\u6267\u884C\u6458\u8981\u3002\u8BFB\u8005\u65E2\u6709\u7BA1\u7406\u5C42\u4E5F\u6709\u6280\u672F\u8D1F\u8D23\u4EBA\u3002",
8814
+ focus: "\u6574\u4F53\u98CE\u9669\u5168\u666F + Top \u98CE\u9669\u7684\u5171\u6027\u6839\u56E0\uFF08\u5982\u516C\u7F51\u66B4\u9732\u9762\u3001IAM \u8FC7\u6743\u3001\u52A0\u5BC6/\u65E5\u5FD7\u7F3A\u5931\u7B49\uFF09+ \u5206\u4F18\u5148\u7EA7(P0\u2192P2)\u7684\u4FEE\u590D\u8DEF\u7EBF\u5EFA\u8BAE\u3002\u70B9\u51FA\u7CFB\u7EDF\u6027\u95EE\u9898\u800C\u975E\u9010\u6761\u590D\u8FF0\u3002",
8815
+ format: "1 \u6BB5\u6982\u8FF0 + 3-5 \u6761\u8981\u70B9\u3002\u4E2D\u6587\u3002\u53EF\u7528\u8981\u70B9\u5217\u8868\u3002\u6280\u672F\u4E0E\u7BA1\u7406\u53CC\u89C6\u89D2\uFF0C\u7ED9\u51FA\u53EF\u843D\u5730\u7684\u4E0B\u4E00\u6B65\u3002"
8816
+ } : {
8817
+ persona: "You are writing the executive summary for a full AWS security scan report. Readers include both management and technical leads.",
8818
+ focus: "Overall risk landscape + common root causes behind the top risks (public exposure, over-privileged IAM, missing encryption/logging, etc.) + a prioritized (P0\u2192P2) remediation roadmap. Surface systemic issues rather than restating each finding.",
8819
+ format: "1 overview paragraph + 3-5 bullet points. English. Bullet list ok. Both technical and management lens, with concrete next steps."
8820
+ };
8821
+ case "hw_defense":
8822
+ return zh ? {
8823
+ persona: "\u4F60\u662F\u62A4\u7F51\u884C\u52A8(\u7F51\u7EDC\u5B89\u5168\u653B\u9632\u6F14\u7EC3)\u7684\u84DD\u961F\u6307\u6325\u3002\u8FD9\u662F\u4E00\u4EFD\u62A4\u7F51\u5907\u6218\u8BC4\u4F30\u62A5\u544A\u3002\u6CE8\u610F\uFF1AHW=\u62A4\u7F51\uFF0C\u4E0E\u534E\u4E3A\u65E0\u5173\u3002",
8824
+ focus: "\u4ECE\u653B\u51FB\u8005\u89C6\u89D2\u4E32\u8054 findings \u6210\u653B\u51FB\u94FE(kill-chain)\uFF1A\u54EA\u4E9B\u662F\u5916\u90E8\u53EF\u8FBE\u7684\u521D\u59CB\u7A81\u7834\u53E3\u3001\u6A2A\u5411\u79FB\u52A8/\u63D0\u6743\u8DEF\u5F84\u3001\u53EF\u80FD\u7684\u5F71\u54CD\u9762\uFF1B\u7136\u540E\u7ED9\u51FA\u5907\u6218\u6536\u655B\u6E05\u5355(\u5148\u5173\u4EC0\u4E48\u3001\u5148\u76EF\u4EC0\u4E48)\u3002\u5F3A\u8C03\u66B4\u9732\u9762\u6536\u655B\u4E0E\u76D1\u63A7\u52A0\u56FA\u3002",
8825
+ format: "\u5148\u4E00\u53E5\u7814\u5224\u7ED3\u8BBA(\u5F53\u524D\u66B4\u9732\u9762/\u88AB\u6253\u7A7F\u98CE\u9669)\uFF0C\u518D\u7ED9\u300E\u653B\u51FB\u94FE\u89C6\u89D2\u300F\u548C\u300E\u5907\u6218\u6574\u6539\u4F18\u5148\u7EA7\u300F\u4E24\u5757\u3002\u4E2D\u6587\u3002\u8BED\u6C14\u8D34\u62A4\u7F51\u5B9E\u6218\u8BED\u5883\uFF0C\u7D27\u8FEB\u4F46\u4E0D\u5938\u5F20\u3002"
8826
+ } : {
8827
+ persona: "You are the blue-team lead for an HW Defense exercise (cybersecurity attack-defense drill). This is a pre-exercise readiness report. Note: HW = HuWang/\u62A4\u7F51, unrelated to Huawei.",
8828
+ focus: "From an attacker's perspective, chain findings into a kill-chain: external initial footholds, lateral movement/privilege-escalation paths, likely blast radius; then a hardening checklist (what to close/watch first). Emphasize attack-surface reduction and monitoring.",
8829
+ format: "Lead with a one-line verdict (current exposure / breach risk), then two blocks: 'Attack-chain view' and 'Readiness remediation priorities'. English. Combat-readiness tone, urgent but not alarmist."
8830
+ };
8831
+ case "mlps3":
8832
+ return zh ? {
8833
+ persona: "\u4F60\u662F\u7B49\u4FDD\u6D4B\u8BC4\u987E\u95EE\u3002\u8FD9\u662F\u4E00\u4EFD\u7B49\u4FDD\u4E09\u7EA7(GB/T 22239-2019)\u5408\u89C4\u9884\u68C0\u62A5\u544A\u3002",
8834
+ focus: "\u4ECE\u5408\u89C4\u89C6\u89D2\u603B\u7ED3\uFF1A\u5F53\u524D\u5BF9\u7167\u7B49\u4FDD\u4E09\u7EA7\u63A7\u5236\u9879\u7684\u603B\u4F53\u8FBE\u6807\u60C5\u51B5\u3001\u54EA\u4E9B\u63A7\u5236\u57DF\u5B58\u5728\u660E\u663E\u4E0D\u8FBE\u6807\u9879(\u5982\u8BBF\u95EE\u63A7\u5236\u3001\u5B89\u5168\u5BA1\u8BA1\u3001\u5165\u4FB5\u9632\u8303\u3001\u6570\u636E\u5B8C\u6574\u6027/\u4FDD\u5BC6\u6027\u7B49)\u3001\u6574\u6539\u7D27\u8FEB\u5EA6\u4E0E\u8FC7\u4FDD\u5EFA\u8BAE\u3002\u628A\u6280\u672F finding \u6620\u5C04\u5230\u5408\u89C4\u8BED\u8A00\u3002",
8835
+ format: "1 \u6BB5\u5408\u89C4\u7ED3\u8BBA + \u6309\u63A7\u5236\u57DF\u5206\u7EC4\u7684\u4E0D\u8FBE\u6807\u8981\u70B9 + \u6574\u6539\u4F18\u5148\u7EA7\u3002\u4E2D\u6587\u3002\u5F15\u7528\u63A7\u5236\u57DF\u540D\u79F0\u3002\u8BED\u6C14\u4E25\u8C28\u3001\u9762\u5411\u6D4B\u8BC4\u3002"
8836
+ } : {
8837
+ persona: "You are an MLPS assessment advisor. This is an MLPS Level 3 (GB/T 22239-2019) compliance pre-check report.",
8838
+ focus: "Summarize from a compliance lens: overall conformance against MLPS L3 controls, which control domains have clear gaps (access control, security audit, intrusion prevention, data integrity/confidentiality, etc.), remediation urgency, and certification-readiness advice. Map technical findings to compliance language.",
8839
+ format: "1 compliance verdict paragraph + non-conformance points grouped by control domain + remediation priority. English. Reference control-domain names. Rigorous, assessment-oriented tone."
8840
+ };
8841
+ }
8842
+ }
8843
+ var TYPE_LABEL = {
8844
+ dashboard: { zh: "\u5B89\u5168\u8FD0\u8425\u4EEA\u8868\u76D8", en: "Security Operations Dashboard" },
8845
+ html: { zh: "AWS \u5B89\u5168\u626B\u63CF\u62A5\u544A", en: "AWS Security Scan Report" },
8846
+ hw_defense: { zh: "\u62A4\u7F51\u884C\u52A8\u8BC4\u4F30\u62A5\u544A", en: "HW Defense (\u62A4\u7F51) Readiness Report" },
8847
+ mlps3: { zh: "\u7B49\u4FDD\u4E09\u7EA7\u5408\u89C4\u9884\u68C0\u62A5\u544A", en: "MLPS Level 3 Compliance Pre-check" }
8848
+ };
8849
+ function buildAiSummaryPrompt(type, scan, lang) {
8850
+ const zh = lang === "zh";
8851
+ const p = getProfile(type, lang);
8852
+ const label = TYPE_LABEL[type][zh ? "zh" : "en"];
8853
+ const digest = buildFindingsDigest(scan, lang);
8854
+ if (zh) {
8855
+ return [
8856
+ `# \u4EFB\u52A1\uFF1A\u4E3A\u300C${label}\u300D\u751F\u6210\u9488\u5BF9\u6027\u7684 AI \u5B89\u5168\u603B\u7ED3`,
8857
+ ``,
8858
+ `## \u89D2\u8272`,
8859
+ p.persona,
8860
+ ``,
8861
+ `## \u603B\u7ED3\u91CD\u70B9`,
8862
+ p.focus,
8863
+ ``,
8864
+ `## \u8F93\u51FA\u683C\u5F0F`,
8865
+ p.format,
8866
+ ``,
8867
+ `## \u7EA6\u675F`,
8868
+ `- \u53EA\u4F9D\u636E\u4E0B\u9762\u7684\u626B\u63CF\u6570\u636E\uFF0C\u4E0D\u8981\u7F16\u9020\u4E0D\u5B58\u5728\u7684\u8D44\u6E90\u6216 finding\u3002`,
8869
+ `- \u4E0D\u8981\u9010\u6761\u590D\u8FF0\u6240\u6709 finding\uFF1B\u63D0\u70BC\u6A21\u5F0F\u4E0E\u4F18\u5148\u7EA7\u3002`,
8870
+ `- \u76F4\u63A5\u8F93\u51FA\u603B\u7ED3\u6B63\u6587\u672C\u8EAB\uFF08\u4E0D\u8981\u5E26\u300C\u4EE5\u4E0B\u662F\u603B\u7ED3\u300D\u4E4B\u7C7B\u7684\u524D\u8A00\uFF0C\u4E0D\u8981 code fence\uFF09\u3002`,
8871
+ `- \u8BED\u8A00\uFF1A\u4E2D\u6587\u3002`,
8872
+ ``,
8873
+ `## \u626B\u63CF\u6570\u636E\u6458\u8981`,
8874
+ digest,
8875
+ ``,
8876
+ `\u8BF7\u73B0\u5728\u8F93\u51FA\u603B\u7ED3\u6B63\u6587\u3002\u751F\u6210\u540E\uFF0C\u628A\u8FD9\u6BB5\u6587\u672C\u901A\u8FC7\u5BF9\u5E94\u62A5\u544A\u5DE5\u5177\u7684 ai_summary \u53C2\u6570\u56DE\u4F20\u5373\u53EF\u3002`
8877
+ ].join("\n");
8878
+ }
8879
+ return [
8880
+ `# Task: Produce a tailored AI security summary for the "${label}"`,
8881
+ ``,
8882
+ `## Role`,
8883
+ p.persona,
8884
+ ``,
8885
+ `## Summary focus`,
8886
+ p.focus,
8887
+ ``,
8888
+ `## Output format`,
8889
+ p.format,
8890
+ ``,
8891
+ `## Constraints`,
8892
+ `- Base it ONLY on the scan data below; do not invent resources or findings.`,
8893
+ `- Do not restate every finding; distill patterns and priorities.`,
8894
+ `- Output the summary body itself (no preamble like "Here is the summary", no code fences).`,
8895
+ `- Language: English.`,
8896
+ ``,
8897
+ `## Scan data digest`,
8898
+ digest,
8899
+ ``,
8900
+ `Now output the summary body. After generating it, pass the text back via the ai_summary parameter of the corresponding report tool.`
8901
+ ].join("\n");
8902
+ }
8903
+
8736
8904
  // src/tools/save-results.ts
8737
8905
  import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
8738
8906
  import { join } from "path";
@@ -8789,7 +8957,8 @@ function saveResults(scanResults, outputDir) {
8789
8957
  })),
8790
8958
  findings: scanResults.modules.flatMap(
8791
8959
  (m) => m.findings.map((f) => ({ ...f, module: m.module }))
8792
- )
8960
+ ),
8961
+ aiSummary: scanResults.aiSummary
8793
8962
  },
8794
8963
  history,
8795
8964
  meta: {
@@ -9060,8 +9229,9 @@ LOW \u2192 P3 (Low)
9060
9229
  `;
9061
9230
 
9062
9231
  // src/index.ts
9063
- import { readFileSync as readFileSync2 } from "fs";
9232
+ import { readFileSync as readFileSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
9064
9233
  import { join as join2, dirname } from "path";
9234
+ import { homedir as homedir2 } from "os";
9065
9235
  import { fileURLToPath } from "url";
9066
9236
  var MODULE_DESCRIPTIONS = {
9067
9237
  service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config) are enabled and assesses security maturity.",
@@ -9404,11 +9574,13 @@ function createServer(defaultRegion) {
9404
9574
  "Generate a Markdown security report from scan results. Read-only. Does not modify any AWS resources.",
9405
9575
  {
9406
9576
  scan_results: z.string().describe("JSON string of FullScanResult from scan_all"),
9407
- lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)")
9577
+ lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)"),
9578
+ ai_summary: z.string().optional().describe("Optional pre-generated AI executive summary (Markdown/plain text). Rendered if present; omit to hide.")
9408
9579
  },
9409
- async ({ scan_results, lang }) => {
9580
+ async ({ scan_results, lang, ai_summary }) => {
9410
9581
  try {
9411
9582
  const parsed = JSON.parse(scan_results);
9583
+ if (ai_summary) parsed.aiSummary = ai_summary;
9412
9584
  const report = generateMarkdownReport(parsed, lang ?? "zh");
9413
9585
  return { content: [{ type: "text", text: report }] };
9414
9586
  } catch (err) {
@@ -9421,11 +9593,13 @@ function createServer(defaultRegion) {
9421
9593
  "Generate a GB/T 22239-2019 \u7B49\u4FDD\u4E09\u7EA7 compliance pre-check report from scan results. Best used with scan_group mlps3_precheck results. Read-only.",
9422
9594
  {
9423
9595
  scan_results: z.string().describe("JSON string of FullScanResult from scan_group mlps3_precheck or scan_all"),
9424
- lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)")
9596
+ lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)"),
9597
+ ai_summary: z.string().optional().describe("Optional pre-generated AI executive summary (Markdown/plain text). Rendered if present; omit to hide.")
9425
9598
  },
9426
- async ({ scan_results, lang }) => {
9599
+ async ({ scan_results, lang, ai_summary }) => {
9427
9600
  try {
9428
9601
  const parsed = JSON.parse(scan_results);
9602
+ if (ai_summary) parsed.aiSummary = ai_summary;
9429
9603
  const report = generateMlps3Report(parsed, lang ?? "zh");
9430
9604
  return { content: [{ type: "text", text: report }] };
9431
9605
  } catch (err) {
@@ -9439,11 +9613,13 @@ function createServer(defaultRegion) {
9439
9613
  {
9440
9614
  scan_results: z.string().describe("JSON string of FullScanResult from scan_all"),
9441
9615
  history: z.string().optional().describe("JSON string of DashboardHistoryEntry[] from dashboard data.json for 30-day trend charts"),
9442
- lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)")
9616
+ lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)"),
9617
+ ai_summary: z.string().optional().describe("Optional pre-generated AI executive summary (Markdown/plain text). Rendered if present; omit to hide.")
9443
9618
  },
9444
- async ({ scan_results, history, lang }) => {
9619
+ async ({ scan_results, history, lang, ai_summary }) => {
9445
9620
  try {
9446
9621
  const parsed = JSON.parse(scan_results);
9622
+ if (ai_summary) parsed.aiSummary = ai_summary;
9447
9623
  const historyData = history ? JSON.parse(history) : void 0;
9448
9624
  const report = generateHtmlReport(parsed, historyData, lang ?? "zh");
9449
9625
  return { content: [{ type: "text", text: report }] };
@@ -9458,11 +9634,13 @@ function createServer(defaultRegion) {
9458
9634
  {
9459
9635
  scan_results: z.string().describe("JSON string of FullScanResult from scan_group mlps3_precheck or scan_all"),
9460
9636
  history: z.string().optional().describe("JSON string of DashboardHistoryEntry[] from dashboard data.json for 30-day trend charts"),
9461
- lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)")
9637
+ lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)"),
9638
+ ai_summary: z.string().optional().describe("Optional pre-generated AI executive summary (Markdown/plain text). Rendered if present; omit to hide.")
9462
9639
  },
9463
- async ({ scan_results, history, lang }) => {
9640
+ async ({ scan_results, history, lang, ai_summary }) => {
9464
9641
  try {
9465
9642
  const parsed = JSON.parse(scan_results);
9643
+ if (ai_summary) parsed.aiSummary = ai_summary;
9466
9644
  const historyData = history ? JSON.parse(history) : void 0;
9467
9645
  const report = generateMlps3HtmlReport(parsed, historyData, lang ?? "zh");
9468
9646
  return { content: [{ type: "text", text: report }] };
@@ -9471,16 +9649,36 @@ function createServer(defaultRegion) {
9471
9649
  }
9472
9650
  }
9473
9651
  );
9652
+ server.tool(
9653
+ "get_ai_summary_prompt",
9654
+ "Return a report-type-tailored prompt (with a grounded findings digest) that the CALLING AI should run to produce an AI security summary. Then pass the generated text back via the `ai_summary` parameter of the matching report tool (or scan_and_report). The server performs no LLM calls. Use this to make each summary specific to the report type (dashboard / security scan / HW Defense \u62A4\u7F51 / MLPS3 \u7B49\u4FDD).",
9655
+ {
9656
+ report_type: z.enum(["dashboard", "html", "hw_defense", "mlps3"]).describe("Target report type the summary is for: dashboard (overview), html (AWS security scan report), hw_defense (\u62A4\u7F51 attack-defense drill), mlps3 (\u7B49\u4FDD\u4E09\u7EA7 compliance)"),
9657
+ scan_results: z.string().describe("JSON string of FullScanResult from scan_all / scan_group"),
9658
+ lang: z.enum(["zh", "en"]).optional().describe("Summary language (default: zh)")
9659
+ },
9660
+ async ({ report_type, scan_results, lang }) => {
9661
+ try {
9662
+ const parsed = JSON.parse(scan_results);
9663
+ const prompt = buildAiSummaryPrompt(report_type, parsed, lang ?? "zh");
9664
+ return { content: [{ type: "text", text: prompt }] };
9665
+ } catch (err) {
9666
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
9667
+ }
9668
+ }
9669
+ );
9474
9670
  server.tool(
9475
9671
  "generate_hw_defense_report",
9476
9672
  "Generate an HTML report organized by HW Defense (\u62A4\u7F51) SOP checklist categories. Save as .html file.",
9477
9673
  {
9478
9674
  scan_results: z.string().describe("JSON string of FullScanResult from scan_group hw_defense or scan_all"),
9479
- lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)")
9675
+ lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)"),
9676
+ ai_summary: z.string().optional().describe("Optional pre-generated AI executive summary (Markdown/plain text). Rendered if present; omit to hide.")
9480
9677
  },
9481
- async ({ scan_results, lang }) => {
9678
+ async ({ scan_results, lang, ai_summary }) => {
9482
9679
  try {
9483
9680
  const parsed = JSON.parse(scan_results);
9681
+ if (ai_summary) parsed.aiSummary = ai_summary;
9484
9682
  const report = generateHwDefenseHtmlReport(parsed, lang ?? "zh");
9485
9683
  return { content: [{ type: "text", text: report }] };
9486
9684
  } catch (err) {
@@ -9609,11 +9807,13 @@ function createServer(defaultRegion) {
9609
9807
  "Saves scan results to local disk or S3 for dashboard display. Does not modify any AWS resources.",
9610
9808
  {
9611
9809
  scan_results: z.string().describe("JSON string of FullScanResult from scan_all"),
9612
- output_dir: z.string().optional().describe("Output directory (default: ~/.aws-security)")
9810
+ output_dir: z.string().optional().describe("Output directory (default: ~/.aws-security)"),
9811
+ ai_summary: z.string().optional().describe("Optional pre-generated AI executive summary (from get_ai_summary_prompt with report_type=dashboard). Persisted into dashboard data and rendered on the Overview; omit to hide.")
9613
9812
  },
9614
- async ({ scan_results, output_dir }) => {
9813
+ async ({ scan_results, output_dir, ai_summary }) => {
9615
9814
  try {
9616
9815
  const parsed = JSON.parse(scan_results);
9816
+ if (ai_summary) parsed.aiSummary = ai_summary;
9617
9817
  const dataPath = saveResults(parsed, output_dir);
9618
9818
  return {
9619
9819
  content: [
@@ -9702,6 +9902,81 @@ Deploy this as a StackSet from your Management Account to all member accounts.`
9702
9902
  }
9703
9903
  }
9704
9904
  );
9905
+ server.tool(
9906
+ "scan_and_report",
9907
+ "Run a full security scan AND generate reports in one step. Avoids large data transfer between tools. Reports are saved to ~/.aws-security/reports/",
9908
+ {
9909
+ region: z.string().optional().describe("AWS region (default: server region)"),
9910
+ org_mode: z.boolean().optional().describe("Enable multi-account org scanning"),
9911
+ role_name: z.string().optional().describe("IAM role name for cross-account scanning"),
9912
+ account_ids: z.array(z.string()).optional().describe("Filter to specific account IDs"),
9913
+ reports: z.array(z.enum(["html", "hw_defense", "mlps3", "markdown", "all"])).optional().describe("Report types to generate (default: all)"),
9914
+ lang: z.enum(["zh", "en"]).optional().describe("Language: zh or en (default: zh)"),
9915
+ ai_summary: z.string().optional().describe("Optional pre-generated AI executive summary (Markdown/plain text). Rendered in reports + dashboard if present; omit to hide.")
9916
+ },
9917
+ async ({ region, org_mode, role_name, account_ids, reports, lang, ai_summary }) => {
9918
+ try {
9919
+ const r = region ?? defaultRegion;
9920
+ const l = lang ?? "zh";
9921
+ const reportTypes = reports ?? ["all"];
9922
+ const wantAll = reportTypes.includes("all");
9923
+ let result;
9924
+ if (org_mode) {
9925
+ result = await runMultiAccountScanners(allScanners, r, {
9926
+ orgMode: true,
9927
+ roleName: role_name ?? "AWSSecurityMCPAudit",
9928
+ accountIds: account_ids
9929
+ });
9930
+ } else {
9931
+ result = await runAllScanners(allScanners, r);
9932
+ }
9933
+ if (ai_summary) result.aiSummary = ai_summary;
9934
+ const baseDir = join2(homedir2(), ".aws-security", "reports", (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
9935
+ mkdirSync2(baseDir, { recursive: true });
9936
+ const savedFiles = [];
9937
+ if (wantAll || reportTypes.includes("html")) {
9938
+ const html = generateHtmlReport(result, void 0, l);
9939
+ const p = join2(baseDir, "security-report.html");
9940
+ writeFileSync2(p, html);
9941
+ savedFiles.push(p);
9942
+ }
9943
+ if (wantAll || reportTypes.includes("hw_defense")) {
9944
+ const html = generateHwDefenseHtmlReport(result, l);
9945
+ const p = join2(baseDir, "hw-defense-report.html");
9946
+ writeFileSync2(p, html);
9947
+ savedFiles.push(p);
9948
+ }
9949
+ if (wantAll || reportTypes.includes("mlps3")) {
9950
+ const html = generateMlps3HtmlReport(result, void 0, l);
9951
+ const p = join2(baseDir, "mlps3-report.html");
9952
+ writeFileSync2(p, html);
9953
+ savedFiles.push(p);
9954
+ }
9955
+ if (wantAll || reportTypes.includes("markdown")) {
9956
+ const md = generateMarkdownReport(result, l);
9957
+ const p = join2(baseDir, "security-report.md");
9958
+ writeFileSync2(p, md);
9959
+ savedFiles.push(p);
9960
+ }
9961
+ saveResults(result);
9962
+ const summary = summarizeResult(result, l);
9963
+ const fileList = savedFiles.map((f) => ` ${f}`).join("\n");
9964
+ return {
9965
+ content: [{
9966
+ type: "text",
9967
+ text: `${summary}
9968
+
9969
+ Reports saved:
9970
+ ${fileList}
9971
+
9972
+ Dashboard data updated.`
9973
+ }]
9974
+ };
9975
+ } catch (err) {
9976
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
9977
+ }
9978
+ }
9979
+ );
9705
9980
  server.resource(
9706
9981
  "security-rules",
9707
9982
  "security://rules",
@@ -9780,6 +10055,7 @@ async function startServer(defaultRegion) {
9780
10055
  }
9781
10056
  export {
9782
10057
  assumeRole,
10058
+ buildAiSummaryPrompt,
9783
10059
  buildRoleArn,
9784
10060
  calculateScore,
9785
10061
  createServer,
@@ -9787,6 +10063,7 @@ export {
9787
10063
  generateHwDefenseHtmlReport,
9788
10064
  generateMarkdownReport,
9789
10065
  generateMlps3HtmlReport,
10066
+ generateMlps3Report,
9790
10067
  getCurrentAccountId,
9791
10068
  listOrgAccounts,
9792
10069
  runAllScanners,