aws-security-mcp 0.7.1 → 0.7.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.
@@ -116,9 +116,7 @@ import { join as join4, extname as extname2, relative } from "path";
116
116
  import { fileURLToPath as fileURLToPath3 } from "url";
117
117
  import {
118
118
  S3Client as S3Client5,
119
- PutObjectCommand,
120
- PutBucketWebsiteCommand,
121
- PutBucketPolicyCommand
119
+ PutObjectCommand
122
120
  } from "@aws-sdk/client-s3";
123
121
  function collectFiles(dir) {
124
122
  const files = [];
@@ -155,38 +153,8 @@ Expected: ${dashboardDir}`
155
153
  );
156
154
  }
157
155
  const s3 = new S3Client5({ region });
158
- console.log(`Configuring s3://${bucket} for static website hosting...`);
159
- await s3.send(
160
- new PutBucketWebsiteCommand({
161
- Bucket: bucket,
162
- WebsiteConfiguration: {
163
- IndexDocument: { Suffix: "index.html" },
164
- ErrorDocument: { Key: "index.html" }
165
- // SPA fallback
166
- }
167
- })
168
- );
169
- const partition = region.startsWith("cn-") ? "aws-cn" : "aws";
170
- console.log(`Setting public read bucket policy on s3://${bucket}...`);
171
- await s3.send(
172
- new PutBucketPolicyCommand({
173
- Bucket: bucket,
174
- Policy: JSON.stringify({
175
- Version: "2012-10-17",
176
- Statement: [
177
- {
178
- Sid: "PublicReadGetObject",
179
- Effect: "Allow",
180
- Principal: "*",
181
- Action: "s3:GetObject",
182
- Resource: `arn:${partition}:s3:::${bucket}/*`
183
- }
184
- ]
185
- })
186
- })
187
- );
188
156
  const files = collectFiles(dashboardDir);
189
- console.log(`Uploading ${files.length} files to s3://${bucket}...`);
157
+ console.log(`Uploading ${files.length} files to s3://${bucket}/ ...`);
190
158
  for (const filePath of files) {
191
159
  const key = relative(dashboardDir, filePath);
192
160
  const ext = extname2(filePath);
@@ -202,14 +170,14 @@ Expected: ${dashboardDir}`
202
170
  );
203
171
  console.log(` ${key}`);
204
172
  }
205
- const domain = region.startsWith("cn-") ? "amazonaws.com.cn" : "amazonaws.com";
206
- const websiteUrl = `http://${bucket}.s3-website.${region}.${domain}`;
207
173
  console.log(`
208
- Dashboard deployed successfully!`);
209
- console.log(`Website URL: ${websiteUrl}`);
210
- console.log(
211
- "\nNote: Ensure S3 Block Public Access is disabled on this bucket for the website to be accessible.\n"
212
- );
174
+ \u2705 Dashboard uploaded to s3://${bucket}/`);
175
+ console.log(`
176
+ S3 bucket remains private (Block Public Access enabled).`);
177
+ console.log(`Access control is managed via IAM permissions.`);
178
+ console.log(`
179
+ To view the dashboard locally:`);
180
+ console.log(` aws-security-mcp dashboard --port 3000`);
213
181
  }
214
182
  var __filename2, __dirname2, CONTENT_TYPES;
215
183
  var init_deploy_dashboard = __esm({
@@ -237,7 +205,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
237
205
  import { z } from "zod";
238
206
 
239
207
  // src/version.ts
240
- var VERSION = "0.7.1";
208
+ var VERSION = "0.7.3";
241
209
 
242
210
  // src/utils/aws-client.ts
243
211
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -313,6 +281,25 @@ async function listOrgAccounts(region) {
313
281
  }
314
282
 
315
283
  // src/scanners/runner.ts
284
+ var DEFAULT_CONCURRENCY = 5;
285
+ async function runWithConcurrency(tasks, limit) {
286
+ const results = [];
287
+ const executing = /* @__PURE__ */ new Set();
288
+ for (let i = 0; i < tasks.length; i++) {
289
+ const idx = i;
290
+ const p = tasks[idx]().then((value) => {
291
+ results[idx] = { status: "fulfilled", value };
292
+ }).catch((reason) => {
293
+ results[idx] = { status: "rejected", reason };
294
+ }).finally(() => {
295
+ executing.delete(p);
296
+ });
297
+ executing.add(p);
298
+ if (executing.size >= limit) await Promise.race(executing);
299
+ }
300
+ await Promise.all(executing);
301
+ return results;
302
+ }
316
303
  var AGGREGATION_MODULES = /* @__PURE__ */ new Set([
317
304
  "security_hub_findings",
318
305
  "guardduty_findings",
@@ -360,8 +347,8 @@ function buildSummary(modules) {
360
347
  modulesError
361
348
  };
362
349
  }
363
- async function runScannersWithContext(scanners, ctx) {
364
- const settled = await Promise.allSettled(scanners.map((s) => s.scan(ctx)));
350
+ async function runScannersWithContext(scanners, ctx, concurrency = DEFAULT_CONCURRENCY) {
351
+ const settled = await runWithConcurrency(scanners.map((s) => () => s.scan(ctx)), concurrency);
365
352
  return settled.map((result, i) => {
366
353
  if (result.status === "fulfilled") {
367
354
  for (const f of result.value.findings) {
@@ -882,6 +869,25 @@ import {
882
869
  DescribeInstancesCommand,
883
870
  DescribeInstanceAttributeCommand
884
871
  } from "@aws-sdk/client-ec2";
872
+ var USERDATA_CONCURRENCY = 5;
873
+ async function runWithConcurrency2(tasks, limit) {
874
+ const results = [];
875
+ const executing = /* @__PURE__ */ new Set();
876
+ for (let i = 0; i < tasks.length; i++) {
877
+ const idx = i;
878
+ const p = tasks[idx]().then((value) => {
879
+ results[idx] = { status: "fulfilled", value };
880
+ }).catch((reason) => {
881
+ results[idx] = { status: "rejected", reason };
882
+ }).finally(() => {
883
+ executing.delete(p);
884
+ });
885
+ executing.add(p);
886
+ if (executing.size >= limit) await Promise.race(executing);
887
+ }
888
+ await Promise.all(executing);
889
+ return results;
890
+ }
885
891
  var SECRET_PATTERNS = [
886
892
  { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/, matchType: "value" },
887
893
  { name: "Private Key", pattern: /-----BEGIN.*PRIVATE KEY-----/, matchType: "value" },
@@ -981,25 +987,28 @@ var SecretExposureScanner = class {
981
987
  nextToken = resp.NextToken;
982
988
  } while (nextToken);
983
989
  resourcesScanned += instances.length;
984
- for (const inst of instances) {
990
+ const userDataTasks = instances.map((inst) => async () => {
991
+ const instId = inst.InstanceId ?? "unknown";
992
+ const attrResp = await ec2.send(
993
+ new DescribeInstanceAttributeCommand({
994
+ InstanceId: instId,
995
+ Attribute: "userData"
996
+ })
997
+ );
998
+ const raw = attrResp.UserData?.Value;
999
+ return { instId, userData: raw ? Buffer.from(raw, "base64").toString("utf-8") : void 0 };
1000
+ });
1001
+ const settled = await runWithConcurrency2(userDataTasks, USERDATA_CONCURRENCY);
1002
+ for (let i = 0; i < instances.length; i++) {
1003
+ const result = settled[i];
1004
+ const inst = instances[i];
985
1005
  const instId = inst.InstanceId ?? "unknown";
986
1006
  const instArn = `arn:${partition}:ec2:${region}:${accountId}:instance/${instId}`;
987
- let userData;
988
- try {
989
- const attrResp = await ec2.send(
990
- new DescribeInstanceAttributeCommand({
991
- InstanceId: instId,
992
- Attribute: "userData"
993
- })
994
- );
995
- const raw = attrResp.UserData?.Value;
996
- if (raw) {
997
- userData = Buffer.from(raw, "base64").toString("utf-8");
998
- }
999
- } catch (e) {
1000
- warnings.push(`Could not read userData for ${instId}: ${e instanceof Error ? e.message : String(e)}`);
1007
+ if (result.status === "rejected") {
1008
+ warnings.push(`Could not read userData for ${instId}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
1001
1009
  continue;
1002
1010
  }
1011
+ const userData = result.value.userData;
1003
1012
  if (!userData) continue;
1004
1013
  for (const sp of SECRET_PATTERNS) {
1005
1014
  if (sp.matchType === "name") continue;
@@ -2190,6 +2199,7 @@ import {
2190
2199
  import {
2191
2200
  S3Client as S3Client3,
2192
2201
  ListBucketsCommand as ListBucketsCommand2,
2202
+ GetBucketLocationCommand as GetBucketLocationCommand2,
2193
2203
  GetBucketTaggingCommand
2194
2204
  } from "@aws-sdk/client-s3";
2195
2205
  var DEFAULT_REQUIRED_TAGS = ["Environment", "Project", "Owner"];
@@ -2306,8 +2316,19 @@ var TagComplianceScanner = class {
2306
2316
  for (const bucket of buckets) {
2307
2317
  const name = bucket.Name ?? "unknown";
2308
2318
  const arn = `arn:${partition}:s3:::${name}`;
2319
+ let bucketClient;
2309
2320
  try {
2310
- const taggingResp = await s3Client.send(
2321
+ const locResp = await s3Client.send(new GetBucketLocationCommand2({ Bucket: name }));
2322
+ const rawLoc = locResp.LocationConstraint || "us-east-1";
2323
+ const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
2324
+ bucketClient = bucketRegion === region ? s3Client : createClient(S3Client3, bucketRegion, ctx.credentials);
2325
+ } catch (e) {
2326
+ const msg = e instanceof Error ? e.message : String(e);
2327
+ warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
2328
+ continue;
2329
+ }
2330
+ try {
2331
+ const taggingResp = await bucketClient.send(
2311
2332
  new GetBucketTaggingCommand({ Bucket: name })
2312
2333
  );
2313
2334
  const tags = (taggingResp.TagSet ?? []).map((t) => ({
@@ -2609,6 +2630,7 @@ import {
2609
2630
  import {
2610
2631
  S3Client as S3Client4,
2611
2632
  ListBucketsCommand as ListBucketsCommand3,
2633
+ GetBucketLocationCommand as GetBucketLocationCommand3,
2612
2634
  GetBucketVersioningCommand,
2613
2635
  GetBucketReplicationCommand
2614
2636
  } from "@aws-sdk/client-s3";
@@ -2783,8 +2805,19 @@ var DisasterRecoveryScanner = class {
2783
2805
  resourcesScanned += bucketNames.length;
2784
2806
  for (const name of bucketNames) {
2785
2807
  const arn = `arn:${partition}:s3:::${name}`;
2808
+ let bucketClient;
2786
2809
  try {
2787
- const ver = await s3Client.send(
2810
+ const locResp = await s3Client.send(new GetBucketLocationCommand3({ Bucket: name }));
2811
+ const rawLoc = locResp.LocationConstraint || "us-east-1";
2812
+ const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
2813
+ bucketClient = bucketRegion === region ? s3Client : createClient(S3Client4, bucketRegion, ctx.credentials);
2814
+ } catch (e) {
2815
+ const msg = e instanceof Error ? e.message : String(e);
2816
+ warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
2817
+ continue;
2818
+ }
2819
+ try {
2820
+ const ver = await bucketClient.send(
2788
2821
  new GetBucketVersioningCommand({ Bucket: name })
2789
2822
  );
2790
2823
  if (ver.Status !== "Enabled") {
@@ -2810,7 +2843,7 @@ var DisasterRecoveryScanner = class {
2810
2843
  warnings.push(`Bucket ${name} versioning check failed: ${msg}`);
2811
2844
  }
2812
2845
  try {
2813
- await s3Client.send(
2846
+ await bucketClient.send(
2814
2847
  new GetBucketReplicationCommand({ Bucket: name })
2815
2848
  );
2816
2849
  } catch (e) {
@@ -4012,6 +4045,39 @@ var zhI18n = {
4012
4045
  action: "\u5B89\u88C5 SSM Agent \u5E76\u914D\u7F6E Patch Manager"
4013
4046
  }
4014
4047
  },
4048
+ // HW Defense HTML Report
4049
+ hwReportTitle: "\u62A4\u7F51\u84DD\u961F\u5B89\u5168\u8BC4\u4F30\u62A5\u544A",
4050
+ hwAutoCheck: "\u81EA\u52A8\u5316\u68C0\u67E5",
4051
+ hwManualCheck: "\u4EBA\u5DE5\u786E\u8BA4\u4E8B\u9879",
4052
+ hwNoAutoCheck: "\u6B64\u9879\u65E0\u81EA\u52A8\u5316\u68C0\u67E5",
4053
+ hwClean: "\u672A\u53D1\u73B0\u95EE\u9898",
4054
+ hwTotalFindings: "\u53D1\u73B0\u603B\u6570",
4055
+ hwSectionsChecked: "\u68C0\u67E5\u5206\u7C7B",
4056
+ hwAutoVerified: "\u81EA\u52A8\u9A8C\u8BC1",
4057
+ hwManualPending: "\u4EBA\u5DE5\u5F85\u786E\u8BA4",
4058
+ hwManualCount: (n) => `${n} \u9879\u4EBA\u5DE5\u786E\u8BA4`,
4059
+ hwAffectedResources: (n) => `\u67E5\u770B\u53D7\u5F71\u54CD\u8D44\u6E90 (${n})`,
4060
+ hwRemediation: "\u4FEE\u590D\u5EFA\u8BAE",
4061
+ hwSectionNames: {
4062
+ attack_surface: { name: "\u653B\u51FB\u9762\u6536\u655B", icon: "\u{1F3AF}" },
4063
+ vulnerability_patch: { name: "\u6F0F\u6D1E\u4E0E\u8865\u4E01\u7BA1\u7406", icon: "\u{1FA79}" },
4064
+ identity_credential: { name: "\u8EAB\u4EFD\u4E0E\u51ED\u8BC1\u5B89\u5168", icon: "\u{1F511}" },
4065
+ transport_security: { name: "\u4F20\u8F93\u4E0E\u5B9E\u4F8B\u5B89\u5168", icon: "\u{1F512}" },
4066
+ security_services: { name: "\u5B89\u5168\u670D\u52A1\u72B6\u6001", icon: "\u{1F6E1}\uFE0F" },
4067
+ emergency_response: { name: "\u5E94\u6025\u54CD\u5E94\u51C6\u5907", icon: "\u{1F6A8}" },
4068
+ environment_control: { name: "\u73AF\u5883\u5904\u7F6E", icon: "\u{1F3D7}\uFE0F" },
4069
+ post_review: { name: "\u62A4\u7F51\u540E\u4F18\u5316", icon: "\u{1F4CA}" }
4070
+ },
4071
+ hwManualItems: {
4072
+ attack_surface: ["\u7ED8\u5236\u51FA\u5165\u7AD9\u8DEF\u5F84\u67B6\u6784\u56FE\uFF0C\u6807\u6CE8\u6240\u6709\u4E92\u8054\u7F51/DX\u4E13\u7EBF\u51FA\u5165\u7AD9\u8DEF\u5F84"],
4073
+ vulnerability_patch: ["\u8054\u7CFB\u5B89\u5168\u5382\u5546\u8FDB\u884C\u6A21\u62DF\u653B\u51FB\u6F14\u7EC3\uFF08\u6E17\u900F\u6D4B\u8BD5\uFF09", "\u5173\u6CE8 AWS \u5B89\u5168\u516C\u544A\uFF08\u5DF2\u77E5\u6F0F\u6D1E\u4E0E\u8865\u4E01\uFF09"],
4074
+ identity_credential: ["\u6240\u6709 IAM \u7528\u6237\u7ED1\u5B9A MFA", "AKSK \u8F6E\u8F6C\u5468\u671F \u2264 90 \u5929", "\u907F\u514D\u5171\u4EAB\u8D26\u6237\u4F7F\u7528", "S3/Lambda/\u5E94\u7528\u4EE3\u7801\u4E2D\u65E0\u660E\u6587\u5BC6\u7801"],
4075
+ transport_security: ["\u786E\u8BA4\u6240\u6709\u5BF9\u5916\u670D\u52A1\u4F7F\u7528 TLS 1.2+", "\u68C0\u67E5\u5185\u90E8\u670D\u52A1\u95F4\u901A\u4FE1\u662F\u5426\u52A0\u5BC6"],
4076
+ security_services: ["\u786E\u8BA4 Security Hub \u5DF2\u5F00\u542F\u5E76\u914D\u7F6E\u6807\u51C6", "\u786E\u8BA4 GuardDuty \u5DF2\u5728\u6240\u6709\u533A\u57DF\u5F00\u542F", "\u786E\u8BA4 CloudTrail \u591A\u533A\u57DF\u65E5\u5FD7\u8BB0\u5F55\u5DF2\u5F00\u542F", "\u786E\u8BA4 Config Rules \u5DF2\u914D\u7F6E"],
4077
+ emergency_response: ["\u51C6\u5907\u4E13\u7528\u9694\u79BB\u5B89\u5168\u7EC4\uFF08\u65E0 Inbound/Outbound \u89C4\u5219\uFF09", "\u5236\u5B9A\u5B9E\u4F8B\u9694\u79BB SOP\uFF1A\u544A\u8B66 \u2192 \u6392\u67E5 \u2192 \u5C01\u9501\u653B\u51FBIP \u2192 \u7F51\u7EDC\u9694\u79BB \u2192 \u5B89\u5168\u5904\u7F6E \u2192 \u8BB0\u5F55\u653B\u51FB\u9879", "\u7EC4\u5EFA 7\xD724 \u76D1\u63A7\u5FEB\u901F\u54CD\u5E94\u56E2\u961F", "\u521B\u5EFA\u62A4\u7F51\u671F\u95F4\u4E13\u7528\u6C9F\u901A\u6E20\u9053\uFF08\u4F01\u5FAE/\u9489\u9489/\u98DE\u4E66/Chime\uFF09", "\u4E0E AWS TAM \u5EFA\u7ACB WAR-ROOM \u8054\u7CFB\uFF08\u4F01\u4E1A\u7EA7\u652F\u6301\u5BA2\u6237\uFF09"],
4078
+ environment_control: ["\u975E\u6838\u5FC3\u7CFB\u7EDF\u5728\u62A4\u7F51\u671F\u95F4\u5173\u95ED", "\u6D4B\u8BD5/\u5F00\u53D1\u73AF\u5883\u5173\u95ED\u6216\u4E0E\u751F\u4EA7\u4FDD\u6301\u540C\u7B49\u5B89\u5168\u57FA\u7EBF", "\u786E\u8BA4\u54EA\u4E9B\u73AF\u5883\u53EF\u4EE5\u7D27\u6025\u5173\u505C\uFF0C\u907F\u514D\u653B\u51FB\u6269\u6563"],
4079
+ post_review: ["\u9488\u5BF9\u653B\u51FB\u62A5\u544A\u9010\u9879\u5E94\u7B54\u4E0E\u4FEE\u590D", "\u4E0E\u5B89\u5168\u56E2\u961F\u5EFA\u7ACB\u5468\u671F\u6027\u5B89\u5168\u7EF4\u62A4\u6D41\u7A0B", "\u6301\u7EED\u8865\u5168\u5B89\u5168\u98CE\u9669"]
4080
+ },
4015
4081
  // HW Checklist (full composite)
4016
4082
  hwChecklist: `
4017
4083
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -4288,6 +4354,39 @@ var enI18n = {
4288
4354
  action: "Install SSM Agent and configure Patch Manager"
4289
4355
  }
4290
4356
  },
4357
+ // HW Defense HTML Report
4358
+ hwReportTitle: "HW Defense Security Assessment Report",
4359
+ hwAutoCheck: "Automated Checks",
4360
+ hwManualCheck: "Manual Verification Items",
4361
+ hwNoAutoCheck: "No automated checks for this section",
4362
+ hwClean: "No issues found",
4363
+ hwTotalFindings: "Total Findings",
4364
+ hwSectionsChecked: "Sections Checked",
4365
+ hwAutoVerified: "Auto-Verified",
4366
+ hwManualPending: "Manual Pending",
4367
+ hwManualCount: (n) => `${n} manual item${n === 1 ? "" : "s"}`,
4368
+ hwAffectedResources: (n) => `View affected resources (${n})`,
4369
+ hwRemediation: "Remediation",
4370
+ hwSectionNames: {
4371
+ attack_surface: { name: "Attack Surface Reduction", icon: "\u{1F3AF}" },
4372
+ vulnerability_patch: { name: "Vulnerability & Patch Management", icon: "\u{1FA79}" },
4373
+ identity_credential: { name: "Identity & Credential Security", icon: "\u{1F511}" },
4374
+ transport_security: { name: "Transport & Instance Security", icon: "\u{1F512}" },
4375
+ security_services: { name: "Security Service Status", icon: "\u{1F6E1}\uFE0F" },
4376
+ emergency_response: { name: "Emergency Response Readiness", icon: "\u{1F6A8}" },
4377
+ environment_control: { name: "Environment Control", icon: "\u{1F3D7}\uFE0F" },
4378
+ post_review: { name: "Post-Drill Optimization", icon: "\u{1F4CA}" }
4379
+ },
4380
+ hwManualItems: {
4381
+ attack_surface: ["Draw ingress/egress path architecture diagram, mark all Internet/DX dedicated line paths"],
4382
+ vulnerability_patch: ["Contact security vendors for simulated attack drills (penetration testing)", "Monitor AWS security advisories (known vulnerabilities and patches)"],
4383
+ identity_credential: ["All IAM users must have MFA enabled", "Access key rotation cycle \u2264 90 days", "Avoid shared account usage", "No plaintext passwords in S3/Lambda/application code"],
4384
+ transport_security: ["Verify all external-facing services use TLS 1.2+", "Check internal service-to-service communication encryption"],
4385
+ security_services: ["Verify Security Hub is enabled with standards configured", "Verify GuardDuty is enabled in all regions", "Verify CloudTrail multi-region logging is enabled", "Verify Config Rules are configured"],
4386
+ emergency_response: ["Prepare dedicated isolation security groups (no Inbound/Outbound rules)", "Establish instance isolation SOP: Alert \u2192 Investigate \u2192 Block attacker IP \u2192 Network isolation \u2192 Security response \u2192 Log attack details", "Form 7\xD724 monitoring rapid response team", "Create dedicated communication channels for the drill period (Teams/Slack/Chime)", "Establish WAR-ROOM connection with AWS TAM (Enterprise Support customers)"],
4387
+ environment_control: ["Shut down non-critical systems during the drill period", "Shut down test/dev environments or maintain same security baseline as production", "Confirm which environments can be emergency-stopped to prevent attack propagation"],
4388
+ post_review: ["Address and remediate each item from the attack report", "Establish periodic security maintenance processes with the security team", "Continuously fill security risk gaps"]
4389
+ },
4291
4390
  // HW Checklist (full composite)
4292
4391
  hwChecklist: `
4293
4392
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -8462,6 +8561,374 @@ ${naNote}
8462
8561
  </html>`;
8463
8562
  }
8464
8563
 
8564
+ // src/tools/hw-report.ts
8565
+ function esc2(s) {
8566
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8567
+ }
8568
+ function safeUrl2(url) {
8569
+ try {
8570
+ const u = new URL(url);
8571
+ if (u.protocol === "https:" || u.protocol === "http:") return url;
8572
+ return null;
8573
+ } catch {
8574
+ return null;
8575
+ }
8576
+ }
8577
+ function escWithLinks2(s) {
8578
+ const parts = s.split(/(https?:\/\/\S+)/);
8579
+ return parts.map((part, i) => {
8580
+ if (i % 2 === 1) {
8581
+ const safe = safeUrl2(part);
8582
+ if (safe) {
8583
+ return `<a href="${esc2(safe)}" style="color:#60a5fa" target="_blank" rel="noopener">${esc2(part)}</a>`;
8584
+ }
8585
+ return esc2(part);
8586
+ }
8587
+ return esc2(part);
8588
+ }).join("");
8589
+ }
8590
+ var SEVERITY_ORDER3 = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
8591
+ var HW_SECTIONS = [
8592
+ {
8593
+ id: "attack_surface",
8594
+ autoModules: ["network_reachability", "dns_dangling", "public_access_verify", "waf_coverage"],
8595
+ shKeywords: ["network", "public", "exposure", "port", "waf", "firewall", "vpc", "securitygroup"]
8596
+ },
8597
+ {
8598
+ id: "vulnerability_patch",
8599
+ autoModules: ["patch_compliance_findings"],
8600
+ shKeywords: ["vulnerability", "patch", "cve", "inspector", "software"]
8601
+ },
8602
+ {
8603
+ id: "identity_credential",
8604
+ autoModules: ["iam_privilege_escalation", "secret_exposure"],
8605
+ shKeywords: ["iam", "access", "privilege", "credential", "password", "mfa", "key rotation"]
8606
+ },
8607
+ {
8608
+ id: "transport_security",
8609
+ autoModules: ["ssl_certificate", "imdsv2_enforcement"],
8610
+ shKeywords: ["ssl", "tls", "certificate", "imds", "metadata"]
8611
+ },
8612
+ {
8613
+ id: "security_services",
8614
+ autoModules: ["service_detection"],
8615
+ shKeywords: []
8616
+ },
8617
+ {
8618
+ id: "emergency_response",
8619
+ autoModules: [],
8620
+ shKeywords: []
8621
+ },
8622
+ {
8623
+ id: "environment_control",
8624
+ autoModules: [],
8625
+ shKeywords: []
8626
+ },
8627
+ {
8628
+ id: "post_review",
8629
+ autoModules: [],
8630
+ shKeywords: []
8631
+ }
8632
+ ];
8633
+ function hwCss() {
8634
+ return `
8635
+ *{margin:0;padding:0;box-sizing:border-box}
8636
+ body{background:#0f172a;color:#f8fafc;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.6;font-size:14px}
8637
+ .container{max-width:900px;margin:0 auto;padding:40px 24px}
8638
+ header{text-align:center;margin-bottom:40px;border-bottom:1px solid #334155;padding-bottom:24px}
8639
+ header h1{font-size:28px;font-weight:700;margin-bottom:8px;letter-spacing:-0.5px}
8640
+ .meta{color:#94a3b8;font-size:13px}
8641
+ h2{font-size:20px;font-weight:600;margin:32px 0 16px;padding-bottom:8px;border-bottom:1px solid #334155}
8642
+ h3{font-size:16px;font-weight:600;margin:16px 0 8px}
8643
+ h4{font-size:14px;font-weight:600;margin:12px 0 4px}
8644
+ .summary-cards{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:32px;justify-content:center}
8645
+ .summary-card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:16px 20px;text-align:center;min-width:100px;flex:1}
8646
+ .summary-card .stat-count{font-size:28px;font-weight:700}
8647
+ .summary-card .stat-label{font-size:12px;color:#94a3b8;margin-top:2px}
8648
+ .badge{display:inline-block;padding:2px 10px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:0.5px;color:#fff}
8649
+ .badge-critical{background:#ef4444}
8650
+ .badge-high{background:#f97316}
8651
+ .badge-medium{background:#eab308;color:#1e293b}
8652
+ .badge-low{background:#22c55e;color:#1e293b}
8653
+ .hw-section{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:16px;overflow:hidden}
8654
+ .hw-section>summary{cursor:pointer;padding:16px 20px;display:flex;align-items:center;gap:12px;list-style:none;font-size:16px;font-weight:600;user-select:none;flex-wrap:wrap}
8655
+ .hw-section>summary::-webkit-details-marker{display:none}
8656
+ .hw-section>summary::marker{content:""}
8657
+ .hw-section>summary::after{content:"\\25B6";font-size:12px;color:#64748b;flex-shrink:0;transition:transform 0.2s;margin-left:auto}
8658
+ .hw-section[open]>summary::after{transform:rotate(90deg)}
8659
+ .hw-section[open]>summary{border-bottom:1px solid #334155}
8660
+ .hw-section-body{padding:16px 20px}
8661
+ .hw-section-icon{font-size:20px}
8662
+ .hw-section-title{flex:1}
8663
+ .hw-section-stats{display:inline-flex;gap:8px;font-size:12px;flex-wrap:wrap}
8664
+ .hw-section-stats .badge{font-size:10px;padding:1px 8px}
8665
+ .hw-auto-section{margin-bottom:16px}
8666
+ .hw-auto-section h4{color:#60a5fa;margin-bottom:8px}
8667
+ .hw-manual-section h4{color:#fbbf24;margin-bottom:8px}
8668
+ .hw-clean{color:#22c55e;font-size:14px;padding:8px 12px;background:rgba(34,197,94,0.1);border-radius:6px}
8669
+ .hw-no-auto{color:#94a3b8;font-size:14px;padding:8px 12px;background:rgba(148,163,184,0.08);border-radius:6px}
8670
+ .hw-manual-item{display:flex;align-items:flex-start;gap:8px;padding:6px 12px;margin-bottom:4px;font-size:14px;color:#cbd5e1;border-radius:4px;background:rgba(148,163,184,0.06)}
8671
+ .hw-manual-checkbox{color:#fbbf24;font-size:16px;flex-shrink:0}
8672
+ .finding-card{display:flex;align-items:center;gap:8px;padding:8px 12px;margin-bottom:4px;border-radius:6px;border-left:4px solid #334155;background:rgba(30,41,59,0.5);flex-wrap:wrap}
8673
+ .sev-critical{border-left-color:#ef4444}
8674
+ .sev-high{border-left-color:#f97316}
8675
+ .sev-medium{border-left-color:#eab308}
8676
+ .sev-low{border-left-color:#22c55e}
8677
+ .finding-title-text{font-weight:600;font-size:13px;flex:1;min-width:200px}
8678
+ .finding-resource{color:#94a3b8;font-size:12px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
8679
+ .finding-card>details{width:100%;margin-top:4px}
8680
+ .finding-card>details>summary{cursor:pointer;font-size:12px;color:#60a5fa;user-select:none}
8681
+ .finding-card-body{padding:8px 0}
8682
+ .finding-card-body p{color:#cbd5e1;font-size:13px;margin-bottom:4px}
8683
+ .finding-card-body ol{padding-left:20px}
8684
+ .finding-card-body li{color:#cbd5e1;font-size:13px;margin-bottom:2px}
8685
+ .hw-finding-group{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:12px 16px;margin-bottom:8px}
8686
+ .hw-finding-header{display:flex;align-items:center;gap:8px}
8687
+ .hw-finding-title{color:#e2e8f0;font-size:14px;flex:1}
8688
+ .hw-finding-count{color:#94a3b8;font-size:13px;font-weight:600;background:#334155;padding:2px 8px;border-radius:4px}
8689
+ .hw-finding-resources{padding:8px 0}
8690
+ .hw-resource-item{font-size:12px;color:#94a3b8;padding:2px 0;font-family:monospace}
8691
+ .hw-finding-remediation{border-top:1px solid #334155;margin-top:8px;padding-top:8px}
8692
+ .hw-finding-remediation ol{margin:4px 0;padding-left:20px;font-size:13px;color:#cbd5e1}
8693
+ footer{margin-top:48px;padding-top:24px;border-top:1px solid #334155;text-align:center}
8694
+ footer p{color:#64748b;font-size:12px;margin-bottom:4px}
8695
+ @media print{
8696
+ body{background:#fff;color:#1e293b;-webkit-print-color-adjust:exact;print-color-adjust:exact}
8697
+ .container{max-width:100%;padding:20px}
8698
+ .hw-section,.summary-card,.finding-card{background:#fff;border:1px solid #e2e8f0}
8699
+ .badge{border:1px solid}
8700
+ header{border-bottom-color:#e2e8f0}
8701
+ h2{border-bottom-color:#e2e8f0}
8702
+ footer{border-top-color:#e2e8f0}
8703
+ .summary-card .stat-label{color:#64748b}
8704
+ .finding-title-text{color:#1e293b}
8705
+ .finding-resource{color:#64748b}
8706
+ .finding-card-body p,.finding-card-body li{color:#475569}
8707
+ .hw-section[open]>summary{border-bottom-color:#e2e8f0}
8708
+ .hw-manual-item{color:#475569}
8709
+ details{display:block}
8710
+ details>summary{display:block}
8711
+ details>:not(summary){display:block !important}
8712
+ }
8713
+ `;
8714
+ }
8715
+ function generateHwDefenseHtmlReport(scanResults, lang) {
8716
+ const t = getI18n(lang ?? "zh");
8717
+ const htmlLang = (lang ?? "zh") === "zh" ? "zh-CN" : "en";
8718
+ const { accountId, region, scanStart } = scanResults;
8719
+ const date = scanStart.split("T")[0];
8720
+ const scanTime = scanStart.replace("T", " ").replace(/\.\d+Z$/, " UTC");
8721
+ const moduleMap = /* @__PURE__ */ new Map();
8722
+ for (const mod of scanResults.modules) {
8723
+ const findings = mod.findings.map((f) => ({ ...f, module: f.module ?? mod.module }));
8724
+ moduleMap.set(mod.module, findings);
8725
+ }
8726
+ const assignedShFindings = /* @__PURE__ */ new Set();
8727
+ function shFindingKey(f) {
8728
+ return `${f.title}|${f.resourceId}|${f.resourceArn}`;
8729
+ }
8730
+ const sectionResults = [];
8731
+ for (const section of HW_SECTIONS) {
8732
+ const findings = [];
8733
+ for (const mod of section.autoModules) {
8734
+ if (mod === "security_hub_findings") continue;
8735
+ const modFindings = moduleMap.get(mod) ?? [];
8736
+ findings.push(...modFindings);
8737
+ }
8738
+ if (section.shKeywords.length > 0) {
8739
+ const shFindings = moduleMap.get("security_hub_findings") ?? [];
8740
+ for (const f of shFindings) {
8741
+ const key = shFindingKey(f);
8742
+ if (assignedShFindings.has(key)) continue;
8743
+ const searchText = `${f.title} ${f.description} ${f.impact}`.toLowerCase();
8744
+ if (section.shKeywords.some((kw) => searchText.includes(kw))) {
8745
+ findings.push(f);
8746
+ assignedShFindings.add(key);
8747
+ }
8748
+ }
8749
+ }
8750
+ const hasAutoModules = section.autoModules.length > 0 || section.shKeywords.length > 0;
8751
+ const hasAutoResults = hasAutoModules && (section.autoModules.some((m) => moduleMap.has(m)) || section.shKeywords.length > 0 && moduleMap.has("security_hub_findings"));
8752
+ const manualItems = t.hwManualItems[section.id] ?? [];
8753
+ sectionResults.push({
8754
+ id: section.id,
8755
+ findings,
8756
+ manualItems,
8757
+ hasAutoModules,
8758
+ hasAutoResults
8759
+ });
8760
+ }
8761
+ const totalFindings = sectionResults.reduce((sum, s) => sum + s.findings.length, 0);
8762
+ const sectionsChecked = sectionResults.filter((s) => s.hasAutoResults || s.manualItems.length > 0).length;
8763
+ const autoVerified = sectionResults.filter((s) => s.hasAutoResults).length;
8764
+ const totalManualItems = sectionResults.reduce((sum, s) => sum + s.manualItems.length, 0);
8765
+ function groupFindings(findings) {
8766
+ const groups = /* @__PURE__ */ new Map();
8767
+ const groupTitles = /* @__PURE__ */ new Map();
8768
+ for (const f of findings) {
8769
+ const cveMatch = f.title.match(/CVE-\d{4}-\d+/i);
8770
+ if (cveMatch) {
8771
+ const key2 = `cve:${cveMatch[0].toUpperCase()}`;
8772
+ if (!groups.has(key2)) {
8773
+ groups.set(key2, []);
8774
+ groupTitles.set(key2, f.title);
8775
+ }
8776
+ groups.get(key2).push(f);
8777
+ continue;
8778
+ }
8779
+ const controlMatch = f.title.match(/[A-Z]+\.\d+/);
8780
+ if (controlMatch) {
8781
+ const key2 = `ctrl:${controlMatch[0]}`;
8782
+ if (!groups.has(key2)) {
8783
+ groups.set(key2, []);
8784
+ groupTitles.set(key2, f.title);
8785
+ }
8786
+ groups.get(key2).push(f);
8787
+ continue;
8788
+ }
8789
+ const key = `title:${f.title}`;
8790
+ if (!groups.has(key)) {
8791
+ groups.set(key, []);
8792
+ groupTitles.set(key, f.title);
8793
+ }
8794
+ groups.get(key).push(f);
8795
+ }
8796
+ const result = [];
8797
+ for (const [key, gFindings] of groups) {
8798
+ let highestSeverity = "LOW";
8799
+ for (const f of gFindings) {
8800
+ if (SEVERITY_ORDER3.indexOf(f.severity) < SEVERITY_ORDER3.indexOf(highestSeverity)) {
8801
+ highestSeverity = f.severity;
8802
+ }
8803
+ }
8804
+ result.push({ title: groupTitles.get(key), findings: gFindings, highestSeverity });
8805
+ }
8806
+ return result;
8807
+ }
8808
+ const renderGroup = (group) => {
8809
+ const sev = group.highestSeverity.toLowerCase();
8810
+ const count = group.findings.length;
8811
+ const first = group.findings[0];
8812
+ const resourceItems = group.findings.map((f) => `<div class="hw-resource-item">${esc2(f.resourceId)} &mdash; ${esc2(f.resourceArn)}</div>`).join("\n");
8813
+ const remediationSteps = first.remediationSteps.map((s) => `<li>${escWithLinks2(s)}</li>`).join("");
8814
+ return `<div class="hw-finding-group">
8815
+ <div class="hw-finding-header">
8816
+ <span class="badge badge-${esc2(sev)}">${esc2(group.highestSeverity)}</span>
8817
+ <span class="hw-finding-title">${esc2(group.title)}</span>
8818
+ <span class="hw-finding-count">&times;${count}</span>
8819
+ </div>
8820
+ <details>
8821
+ <summary>${t.hwAffectedResources(count)}</summary>
8822
+ <div class="hw-finding-resources">
8823
+ ${resourceItems}
8824
+ </div>
8825
+ <div class="hw-finding-remediation">
8826
+ <strong>${esc2(t.hwRemediation)}:</strong>
8827
+ <ol>${remediationSteps}</ol>
8828
+ </div>
8829
+ </details>
8830
+ </div>`;
8831
+ };
8832
+ const sectionsHtml = sectionResults.map((section) => {
8833
+ const meta = t.hwSectionNames[section.id];
8834
+ if (!meta) return "";
8835
+ const sectionName = meta.name;
8836
+ const sectionIcon = meta.icon ?? "";
8837
+ const statBadges = [];
8838
+ if (section.findings.length > 0) {
8839
+ const sevCounts = {};
8840
+ for (const f of section.findings) {
8841
+ sevCounts[f.severity] = (sevCounts[f.severity] ?? 0) + 1;
8842
+ }
8843
+ for (const sev of SEVERITY_ORDER3) {
8844
+ if (sevCounts[sev]) {
8845
+ statBadges.push(
8846
+ `<span class="badge badge-${sev.toLowerCase()}">${sevCounts[sev]} ${sev}</span>`
8847
+ );
8848
+ }
8849
+ }
8850
+ } else if (section.hasAutoResults) {
8851
+ statBadges.push(`<span style="color:#22c55e;font-size:12px">&#10003; ${esc2(t.hwClean)}</span>`);
8852
+ }
8853
+ if (section.manualItems.length > 0) {
8854
+ statBadges.push(`<span style="color:#fbbf24;font-size:12px">&#9744; ${esc2(t.hwManualCount(section.manualItems.length))}</span>`);
8855
+ }
8856
+ let autoHtml;
8857
+ if (!section.hasAutoModules) {
8858
+ autoHtml = `<div class="hw-no-auto">&#9898; ${esc2(t.hwNoAutoCheck)}</div>`;
8859
+ } else if (!section.hasAutoResults) {
8860
+ autoHtml = `<div class="hw-no-auto">&#9898; ${esc2(t.hwNoAutoCheck)}</div>`;
8861
+ } else if (section.findings.length === 0) {
8862
+ autoHtml = `<div class="hw-clean">&#10004; ${esc2(t.hwClean)}</div>`;
8863
+ } else {
8864
+ const sorted = [...section.findings].sort((a, b) => {
8865
+ const sevDiff = SEVERITY_ORDER3.indexOf(a.severity) - SEVERITY_ORDER3.indexOf(b.severity);
8866
+ if (sevDiff !== 0) return sevDiff;
8867
+ return b.riskScore - a.riskScore;
8868
+ });
8869
+ const groups = groupFindings(sorted);
8870
+ autoHtml = groups.map(renderGroup).join("\n");
8871
+ }
8872
+ let manualHtml = "";
8873
+ if (section.manualItems.length > 0) {
8874
+ const items = section.manualItems.map((item) => `<div class="hw-manual-item"><span class="hw-manual-checkbox">&#9633;</span>${esc2(item)}</div>`).join("\n");
8875
+ manualHtml = `
8876
+ <div class="hw-manual-section">
8877
+ <h4>&#128203; ${esc2(t.hwManualCheck)}</h4>
8878
+ ${items}
8879
+ </div>`;
8880
+ }
8881
+ return `<details class="hw-section">
8882
+ <summary>
8883
+ <span class="hw-section-icon">${esc2(sectionIcon)}</span>
8884
+ <span class="hw-section-title">${esc2(sectionName)}</span>
8885
+ <span class="hw-section-stats">${statBadges.join(" ")}</span>
8886
+ </summary>
8887
+ <div class="hw-section-body">
8888
+ <div class="hw-auto-section">
8889
+ <h4>&#129302; ${esc2(t.hwAutoCheck)}</h4>
8890
+ ${autoHtml}
8891
+ </div>
8892
+ ${manualHtml}
8893
+ </div>
8894
+ </details>`;
8895
+ }).filter(Boolean).join("\n");
8896
+ const findingsColor = totalFindings === 0 ? "#22c55e" : totalFindings <= 5 ? "#eab308" : "#ef4444";
8897
+ return `<!DOCTYPE html>
8898
+ <html lang="${htmlLang}">
8899
+ <head>
8900
+ <meta charset="UTF-8">
8901
+ <meta name="viewport" content="width=device-width,initial-scale=1">
8902
+ <title>${esc2(t.hwReportTitle)} &mdash; ${esc2(date)}</title>
8903
+ <style>${hwCss()}</style>
8904
+ </head>
8905
+ <body>
8906
+ <div class="container">
8907
+
8908
+ <header>
8909
+ <h1>&#128737;&#65039; ${esc2(t.hwReportTitle)}</h1>
8910
+ <div class="meta">${esc2(t.account)}: ${esc2(accountId)} | ${esc2(t.region)}: ${esc2(region)} | ${esc2(t.scanTime)}: ${esc2(scanTime)}</div>
8911
+ </header>
8912
+
8913
+ <section class="summary-cards">
8914
+ <div class="summary-card"><div class="stat-count" style="color:${findingsColor}">${totalFindings}</div><div class="stat-label">${esc2(t.hwTotalFindings)}</div></div>
8915
+ <div class="summary-card"><div class="stat-count" style="color:#60a5fa">${sectionsChecked}</div><div class="stat-label">${esc2(t.hwSectionsChecked)}</div></div>
8916
+ <div class="summary-card"><div class="stat-count" style="color:#22c55e">${autoVerified}</div><div class="stat-label">${esc2(t.hwAutoVerified)}</div></div>
8917
+ <div class="summary-card"><div class="stat-count" style="color:#fbbf24">${totalManualItems}</div><div class="stat-label">${esc2(t.hwManualPending)}</div></div>
8918
+ </section>
8919
+
8920
+ ${sectionsHtml}
8921
+
8922
+ <footer>
8923
+ <p>${esc2(t.generatedBy)} v${VERSION}</p>
8924
+
8925
+ </footer>
8926
+
8927
+ </div>
8928
+ </body>
8929
+ </html>`;
8930
+ }
8931
+
8465
8932
  // src/tools/save-results.ts
8466
8933
  import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
8467
8934
  import { join } from "path";
@@ -8532,7 +8999,7 @@ function saveResults(scanResults, outputDir) {
8532
8999
  }
8533
9000
 
8534
9001
  // src/tools/scan-groups.ts
8535
- var SEVERITY_ORDER3 = {
9002
+ var SEVERITY_ORDER4 = {
8536
9003
  LOW: 0,
8537
9004
  MEDIUM: 1,
8538
9005
  HIGH: 2,
@@ -8541,8 +9008,8 @@ var SEVERITY_ORDER3 = {
8541
9008
  function applyFindingsFilter(moduleName, findings, filter) {
8542
9009
  let result = findings;
8543
9010
  if (filter.minSeverity) {
8544
- const minLevel = SEVERITY_ORDER3[filter.minSeverity.toUpperCase()] ?? 0;
8545
- result = result.filter((f) => (SEVERITY_ORDER3[f.severity] ?? 0) >= minLevel);
9011
+ const minLevel = SEVERITY_ORDER4[filter.minSeverity.toUpperCase()] ?? 0;
9012
+ result = result.filter((f) => (SEVERITY_ORDER4[f.severity] ?? 0) >= minLevel);
8546
9013
  }
8547
9014
  if (moduleName === "security_hub_findings" && filter.securityHubCategories?.length) {
8548
9015
  const keywords = filter.securityHubCategories;
@@ -8576,10 +9043,10 @@ var SCAN_GROUPS = {
8576
9043
  },
8577
9044
  hw_defense: {
8578
9045
  name: "\u62A4\u7F51\u84DD\u961F\u52A0\u56FA",
8579
- description: "\u62A4\u7F51\u524D\u5B89\u5168\u81EA\u67E5 \u2014 \u653B\u51FB\u9762+\u5F31\u70B9\u8BC4\u4F30",
8580
- modules: ["service_detection", "secret_exposure", "network_reachability", "dns_dangling", "ssl_certificate", "iam_privilege_escalation", "security_hub_findings", "guardduty_findings", "inspector_findings", "config_rules_findings", "access_analyzer_findings", "patch_compliance_findings", "imdsv2_enforcement", "waf_coverage"],
9046
+ description: "\u62A4\u7F51\u524D\u5B89\u5168\u81EA\u67E5 \u2014 \u653B\u51FB\u8005\u89C6\u89D2\u7684\u653B\u51FB\u9762+\u5F31\u70B9\u8BC4\u4F30",
9047
+ modules: ["service_detection", "network_reachability", "dns_dangling", "public_access_verify", "ssl_certificate", "waf_coverage", "imdsv2_enforcement", "secret_exposure", "iam_privilege_escalation", "patch_compliance_findings", "security_hub_findings"],
8581
9048
  findingsFilter: {
8582
- guardDutyTypes: ["Backdoor", "Trojan", "PenTest", "CryptoCurrency"],
9049
+ securityHubCategories: ["network", "public", "exposure", "port", "WAF", "vulnerability", "patch", "IAM", "iam", "access", "privilege", "secret", "credential", "password", "IMDS", "firewall", "VPC", "vpc", "SecurityGroup", "securitygroup", "CVE", "cve", "Inspector", "inspector", "software", "MFA", "mfa", "key rotation", "SSL", "ssl", "TLS", "tls", "certificate", "metadata", "CloudTrail", "cloudtrail", "logging", "audit", "encryption"],
8583
9050
  minSeverity: "MEDIUM"
8584
9051
  }
8585
9052
  },
@@ -9101,6 +9568,7 @@ function createServer(defaultRegion) {
9101
9568
  const summaryContent = content[0];
9102
9569
  if (summaryContent && summaryContent.type === "text") {
9103
9570
  summaryContent.text += "\n\n" + getHwDefenseChecklist(lang ?? "zh");
9571
+ summaryContent.text += "\n\n\u{1F4A1} Tip: Call generate_hw_defense_report with these scan results to get a dedicated HTML report organized by HW Defense SOP checklist categories.";
9104
9572
  }
9105
9573
  }
9106
9574
  return { content };
@@ -9199,6 +9667,23 @@ function createServer(defaultRegion) {
9199
9667
  }
9200
9668
  }
9201
9669
  );
9670
+ server.tool(
9671
+ "generate_hw_defense_report",
9672
+ "Generate an HTML report organized by HW Defense (\u62A4\u7F51) SOP checklist categories. Save as .html file.",
9673
+ {
9674
+ scan_results: z.string().describe("JSON string of FullScanResult from scan_group hw_defense or scan_all"),
9675
+ lang: z.enum(["zh", "en"]).optional().describe("Report language (default: zh)")
9676
+ },
9677
+ async ({ scan_results, lang }) => {
9678
+ try {
9679
+ const parsed = JSON.parse(scan_results);
9680
+ const report = generateHwDefenseHtmlReport(parsed, lang ?? "zh");
9681
+ return { content: [{ type: "text", text: report }] };
9682
+ } catch (err) {
9683
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
9684
+ }
9685
+ }
9686
+ );
9202
9687
  server.tool(
9203
9688
  "generate_maturity_report",
9204
9689
  "Generate a security maturity assessment report from scan_all results. Requires service_detection module output. Read-only.",
@@ -9498,7 +9983,7 @@ var HELP = `Usage: aws-security-mcp [command] [options]
9498
9983
  Commands:
9499
9984
  (default) Start MCP server (stdio, for Kiro/Claude Code)
9500
9985
  dashboard Start local HTTP server serving the security dashboard
9501
- deploy-dashboard Deploy dashboard to an S3 bucket as a static website
9986
+ deploy-dashboard Upload dashboard files to a private S3 bucket
9502
9987
 
9503
9988
  Options:
9504
9989
  --region <region> AWS region (default: AWS_REGION env or us-east-1)