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.
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.1";
7
+ var VERSION = "0.7.3";
8
8
 
9
9
  // src/utils/aws-client.ts
10
10
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -85,6 +85,25 @@ async function listOrgAccounts(region) {
85
85
  }
86
86
 
87
87
  // src/scanners/runner.ts
88
+ var DEFAULT_CONCURRENCY = 5;
89
+ async function runWithConcurrency(tasks, limit) {
90
+ const results = [];
91
+ const executing = /* @__PURE__ */ new Set();
92
+ for (let i = 0; i < tasks.length; i++) {
93
+ const idx = i;
94
+ const p = tasks[idx]().then((value) => {
95
+ results[idx] = { status: "fulfilled", value };
96
+ }).catch((reason) => {
97
+ results[idx] = { status: "rejected", reason };
98
+ }).finally(() => {
99
+ executing.delete(p);
100
+ });
101
+ executing.add(p);
102
+ if (executing.size >= limit) await Promise.race(executing);
103
+ }
104
+ await Promise.all(executing);
105
+ return results;
106
+ }
88
107
  var AGGREGATION_MODULES = /* @__PURE__ */ new Set([
89
108
  "security_hub_findings",
90
109
  "guardduty_findings",
@@ -132,8 +151,8 @@ function buildSummary(modules) {
132
151
  modulesError
133
152
  };
134
153
  }
135
- async function runScannersWithContext(scanners, ctx) {
136
- const settled = await Promise.allSettled(scanners.map((s) => s.scan(ctx)));
154
+ async function runScannersWithContext(scanners, ctx, concurrency = DEFAULT_CONCURRENCY) {
155
+ const settled = await runWithConcurrency(scanners.map((s) => () => s.scan(ctx)), concurrency);
137
156
  return settled.map((result, i) => {
138
157
  if (result.status === "fulfilled") {
139
158
  for (const f of result.value.findings) {
@@ -654,6 +673,25 @@ import {
654
673
  DescribeInstancesCommand,
655
674
  DescribeInstanceAttributeCommand
656
675
  } from "@aws-sdk/client-ec2";
676
+ 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
+ }
657
695
  var SECRET_PATTERNS = [
658
696
  { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/, matchType: "value" },
659
697
  { name: "Private Key", pattern: /-----BEGIN.*PRIVATE KEY-----/, matchType: "value" },
@@ -753,25 +791,28 @@ var SecretExposureScanner = class {
753
791
  nextToken = resp.NextToken;
754
792
  } while (nextToken);
755
793
  resourcesScanned += instances.length;
756
- for (const inst of instances) {
794
+ const userDataTasks = instances.map((inst) => async () => {
795
+ const instId = inst.InstanceId ?? "unknown";
796
+ const attrResp = await ec2.send(
797
+ new DescribeInstanceAttributeCommand({
798
+ InstanceId: instId,
799
+ Attribute: "userData"
800
+ })
801
+ );
802
+ const raw = attrResp.UserData?.Value;
803
+ return { instId, userData: raw ? Buffer.from(raw, "base64").toString("utf-8") : void 0 };
804
+ });
805
+ const settled = await runWithConcurrency2(userDataTasks, USERDATA_CONCURRENCY);
806
+ for (let i = 0; i < instances.length; i++) {
807
+ const result = settled[i];
808
+ const inst = instances[i];
757
809
  const instId = inst.InstanceId ?? "unknown";
758
810
  const instArn = `arn:${partition}:ec2:${region}:${accountId}:instance/${instId}`;
759
- let userData;
760
- try {
761
- const attrResp = await ec2.send(
762
- new DescribeInstanceAttributeCommand({
763
- InstanceId: instId,
764
- Attribute: "userData"
765
- })
766
- );
767
- const raw = attrResp.UserData?.Value;
768
- if (raw) {
769
- userData = Buffer.from(raw, "base64").toString("utf-8");
770
- }
771
- } catch (e) {
772
- warnings.push(`Could not read userData for ${instId}: ${e instanceof Error ? e.message : String(e)}`);
811
+ if (result.status === "rejected") {
812
+ warnings.push(`Could not read userData for ${instId}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
773
813
  continue;
774
814
  }
815
+ const userData = result.value.userData;
775
816
  if (!userData) continue;
776
817
  for (const sp of SECRET_PATTERNS) {
777
818
  if (sp.matchType === "name") continue;
@@ -1962,6 +2003,7 @@ import {
1962
2003
  import {
1963
2004
  S3Client as S3Client3,
1964
2005
  ListBucketsCommand as ListBucketsCommand2,
2006
+ GetBucketLocationCommand as GetBucketLocationCommand2,
1965
2007
  GetBucketTaggingCommand
1966
2008
  } from "@aws-sdk/client-s3";
1967
2009
  var DEFAULT_REQUIRED_TAGS = ["Environment", "Project", "Owner"];
@@ -2078,8 +2120,19 @@ var TagComplianceScanner = class {
2078
2120
  for (const bucket of buckets) {
2079
2121
  const name = bucket.Name ?? "unknown";
2080
2122
  const arn = `arn:${partition}:s3:::${name}`;
2123
+ let bucketClient;
2124
+ try {
2125
+ const locResp = await s3Client.send(new GetBucketLocationCommand2({ Bucket: name }));
2126
+ const rawLoc = locResp.LocationConstraint || "us-east-1";
2127
+ const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
2128
+ bucketClient = bucketRegion === region ? s3Client : createClient(S3Client3, bucketRegion, ctx.credentials);
2129
+ } catch (e) {
2130
+ const msg = e instanceof Error ? e.message : String(e);
2131
+ warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
2132
+ continue;
2133
+ }
2081
2134
  try {
2082
- const taggingResp = await s3Client.send(
2135
+ const taggingResp = await bucketClient.send(
2083
2136
  new GetBucketTaggingCommand({ Bucket: name })
2084
2137
  );
2085
2138
  const tags = (taggingResp.TagSet ?? []).map((t) => ({
@@ -2381,6 +2434,7 @@ import {
2381
2434
  import {
2382
2435
  S3Client as S3Client4,
2383
2436
  ListBucketsCommand as ListBucketsCommand3,
2437
+ GetBucketLocationCommand as GetBucketLocationCommand3,
2384
2438
  GetBucketVersioningCommand,
2385
2439
  GetBucketReplicationCommand
2386
2440
  } from "@aws-sdk/client-s3";
@@ -2555,8 +2609,19 @@ var DisasterRecoveryScanner = class {
2555
2609
  resourcesScanned += bucketNames.length;
2556
2610
  for (const name of bucketNames) {
2557
2611
  const arn = `arn:${partition}:s3:::${name}`;
2612
+ let bucketClient;
2613
+ try {
2614
+ const locResp = await s3Client.send(new GetBucketLocationCommand3({ Bucket: name }));
2615
+ const rawLoc = locResp.LocationConstraint || "us-east-1";
2616
+ const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
2617
+ bucketClient = bucketRegion === region ? s3Client : createClient(S3Client4, bucketRegion, ctx.credentials);
2618
+ } catch (e) {
2619
+ const msg = e instanceof Error ? e.message : String(e);
2620
+ warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
2621
+ continue;
2622
+ }
2558
2623
  try {
2559
- const ver = await s3Client.send(
2624
+ const ver = await bucketClient.send(
2560
2625
  new GetBucketVersioningCommand({ Bucket: name })
2561
2626
  );
2562
2627
  if (ver.Status !== "Enabled") {
@@ -2582,7 +2647,7 @@ var DisasterRecoveryScanner = class {
2582
2647
  warnings.push(`Bucket ${name} versioning check failed: ${msg}`);
2583
2648
  }
2584
2649
  try {
2585
- await s3Client.send(
2650
+ await bucketClient.send(
2586
2651
  new GetBucketReplicationCommand({ Bucket: name })
2587
2652
  );
2588
2653
  } catch (e) {
@@ -3784,6 +3849,39 @@ var zhI18n = {
3784
3849
  action: "\u5B89\u88C5 SSM Agent \u5E76\u914D\u7F6E Patch Manager"
3785
3850
  }
3786
3851
  },
3852
+ // HW Defense HTML Report
3853
+ hwReportTitle: "\u62A4\u7F51\u84DD\u961F\u5B89\u5168\u8BC4\u4F30\u62A5\u544A",
3854
+ hwAutoCheck: "\u81EA\u52A8\u5316\u68C0\u67E5",
3855
+ hwManualCheck: "\u4EBA\u5DE5\u786E\u8BA4\u4E8B\u9879",
3856
+ hwNoAutoCheck: "\u6B64\u9879\u65E0\u81EA\u52A8\u5316\u68C0\u67E5",
3857
+ hwClean: "\u672A\u53D1\u73B0\u95EE\u9898",
3858
+ hwTotalFindings: "\u53D1\u73B0\u603B\u6570",
3859
+ hwSectionsChecked: "\u68C0\u67E5\u5206\u7C7B",
3860
+ hwAutoVerified: "\u81EA\u52A8\u9A8C\u8BC1",
3861
+ hwManualPending: "\u4EBA\u5DE5\u5F85\u786E\u8BA4",
3862
+ hwManualCount: (n) => `${n} \u9879\u4EBA\u5DE5\u786E\u8BA4`,
3863
+ hwAffectedResources: (n) => `\u67E5\u770B\u53D7\u5F71\u54CD\u8D44\u6E90 (${n})`,
3864
+ hwRemediation: "\u4FEE\u590D\u5EFA\u8BAE",
3865
+ hwSectionNames: {
3866
+ attack_surface: { name: "\u653B\u51FB\u9762\u6536\u655B", icon: "\u{1F3AF}" },
3867
+ vulnerability_patch: { name: "\u6F0F\u6D1E\u4E0E\u8865\u4E01\u7BA1\u7406", icon: "\u{1FA79}" },
3868
+ identity_credential: { name: "\u8EAB\u4EFD\u4E0E\u51ED\u8BC1\u5B89\u5168", icon: "\u{1F511}" },
3869
+ transport_security: { name: "\u4F20\u8F93\u4E0E\u5B9E\u4F8B\u5B89\u5168", icon: "\u{1F512}" },
3870
+ security_services: { name: "\u5B89\u5168\u670D\u52A1\u72B6\u6001", icon: "\u{1F6E1}\uFE0F" },
3871
+ emergency_response: { name: "\u5E94\u6025\u54CD\u5E94\u51C6\u5907", icon: "\u{1F6A8}" },
3872
+ environment_control: { name: "\u73AF\u5883\u5904\u7F6E", icon: "\u{1F3D7}\uFE0F" },
3873
+ post_review: { name: "\u62A4\u7F51\u540E\u4F18\u5316", icon: "\u{1F4CA}" }
3874
+ },
3875
+ hwManualItems: {
3876
+ 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"],
3877
+ 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"],
3878
+ 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"],
3879
+ 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"],
3880
+ 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"],
3881
+ 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"],
3882
+ 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"],
3883
+ 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"]
3884
+ },
3787
3885
  // HW Checklist (full composite)
3788
3886
  hwChecklist: `
3789
3887
  \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
@@ -4060,6 +4158,39 @@ var enI18n = {
4060
4158
  action: "Install SSM Agent and configure Patch Manager"
4061
4159
  }
4062
4160
  },
4161
+ // HW Defense HTML Report
4162
+ hwReportTitle: "HW Defense Security Assessment Report",
4163
+ hwAutoCheck: "Automated Checks",
4164
+ hwManualCheck: "Manual Verification Items",
4165
+ hwNoAutoCheck: "No automated checks for this section",
4166
+ hwClean: "No issues found",
4167
+ hwTotalFindings: "Total Findings",
4168
+ hwSectionsChecked: "Sections Checked",
4169
+ hwAutoVerified: "Auto-Verified",
4170
+ hwManualPending: "Manual Pending",
4171
+ hwManualCount: (n) => `${n} manual item${n === 1 ? "" : "s"}`,
4172
+ hwAffectedResources: (n) => `View affected resources (${n})`,
4173
+ hwRemediation: "Remediation",
4174
+ hwSectionNames: {
4175
+ attack_surface: { name: "Attack Surface Reduction", icon: "\u{1F3AF}" },
4176
+ vulnerability_patch: { name: "Vulnerability & Patch Management", icon: "\u{1FA79}" },
4177
+ identity_credential: { name: "Identity & Credential Security", icon: "\u{1F511}" },
4178
+ transport_security: { name: "Transport & Instance Security", icon: "\u{1F512}" },
4179
+ security_services: { name: "Security Service Status", icon: "\u{1F6E1}\uFE0F" },
4180
+ emergency_response: { name: "Emergency Response Readiness", icon: "\u{1F6A8}" },
4181
+ environment_control: { name: "Environment Control", icon: "\u{1F3D7}\uFE0F" },
4182
+ post_review: { name: "Post-Drill Optimization", icon: "\u{1F4CA}" }
4183
+ },
4184
+ hwManualItems: {
4185
+ attack_surface: ["Draw ingress/egress path architecture diagram, mark all Internet/DX dedicated line paths"],
4186
+ vulnerability_patch: ["Contact security vendors for simulated attack drills (penetration testing)", "Monitor AWS security advisories (known vulnerabilities and patches)"],
4187
+ 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"],
4188
+ transport_security: ["Verify all external-facing services use TLS 1.2+", "Check internal service-to-service communication encryption"],
4189
+ 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"],
4190
+ 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)"],
4191
+ 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"],
4192
+ 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"]
4193
+ },
4063
4194
  // HW Checklist (full composite)
4064
4195
  hwChecklist: `
4065
4196
  \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
@@ -8234,6 +8365,374 @@ ${naNote}
8234
8365
  </html>`;
8235
8366
  }
8236
8367
 
8368
+ // src/tools/hw-report.ts
8369
+ function esc2(s) {
8370
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8371
+ }
8372
+ function safeUrl2(url) {
8373
+ try {
8374
+ const u = new URL(url);
8375
+ if (u.protocol === "https:" || u.protocol === "http:") return url;
8376
+ return null;
8377
+ } catch {
8378
+ return null;
8379
+ }
8380
+ }
8381
+ function escWithLinks2(s) {
8382
+ const parts = s.split(/(https?:\/\/\S+)/);
8383
+ return parts.map((part, i) => {
8384
+ if (i % 2 === 1) {
8385
+ const safe = safeUrl2(part);
8386
+ if (safe) {
8387
+ return `<a href="${esc2(safe)}" style="color:#60a5fa" target="_blank" rel="noopener">${esc2(part)}</a>`;
8388
+ }
8389
+ return esc2(part);
8390
+ }
8391
+ return esc2(part);
8392
+ }).join("");
8393
+ }
8394
+ var SEVERITY_ORDER3 = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
8395
+ var HW_SECTIONS = [
8396
+ {
8397
+ id: "attack_surface",
8398
+ autoModules: ["network_reachability", "dns_dangling", "public_access_verify", "waf_coverage"],
8399
+ shKeywords: ["network", "public", "exposure", "port", "waf", "firewall", "vpc", "securitygroup"]
8400
+ },
8401
+ {
8402
+ id: "vulnerability_patch",
8403
+ autoModules: ["patch_compliance_findings"],
8404
+ shKeywords: ["vulnerability", "patch", "cve", "inspector", "software"]
8405
+ },
8406
+ {
8407
+ id: "identity_credential",
8408
+ autoModules: ["iam_privilege_escalation", "secret_exposure"],
8409
+ shKeywords: ["iam", "access", "privilege", "credential", "password", "mfa", "key rotation"]
8410
+ },
8411
+ {
8412
+ id: "transport_security",
8413
+ autoModules: ["ssl_certificate", "imdsv2_enforcement"],
8414
+ shKeywords: ["ssl", "tls", "certificate", "imds", "metadata"]
8415
+ },
8416
+ {
8417
+ id: "security_services",
8418
+ autoModules: ["service_detection"],
8419
+ shKeywords: []
8420
+ },
8421
+ {
8422
+ id: "emergency_response",
8423
+ autoModules: [],
8424
+ shKeywords: []
8425
+ },
8426
+ {
8427
+ id: "environment_control",
8428
+ autoModules: [],
8429
+ shKeywords: []
8430
+ },
8431
+ {
8432
+ id: "post_review",
8433
+ autoModules: [],
8434
+ shKeywords: []
8435
+ }
8436
+ ];
8437
+ function hwCss() {
8438
+ return `
8439
+ *{margin:0;padding:0;box-sizing:border-box}
8440
+ body{background:#0f172a;color:#f8fafc;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.6;font-size:14px}
8441
+ .container{max-width:900px;margin:0 auto;padding:40px 24px}
8442
+ header{text-align:center;margin-bottom:40px;border-bottom:1px solid #334155;padding-bottom:24px}
8443
+ header h1{font-size:28px;font-weight:700;margin-bottom:8px;letter-spacing:-0.5px}
8444
+ .meta{color:#94a3b8;font-size:13px}
8445
+ h2{font-size:20px;font-weight:600;margin:32px 0 16px;padding-bottom:8px;border-bottom:1px solid #334155}
8446
+ h3{font-size:16px;font-weight:600;margin:16px 0 8px}
8447
+ h4{font-size:14px;font-weight:600;margin:12px 0 4px}
8448
+ .summary-cards{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:32px;justify-content:center}
8449
+ .summary-card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:16px 20px;text-align:center;min-width:100px;flex:1}
8450
+ .summary-card .stat-count{font-size:28px;font-weight:700}
8451
+ .summary-card .stat-label{font-size:12px;color:#94a3b8;margin-top:2px}
8452
+ .badge{display:inline-block;padding:2px 10px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:0.5px;color:#fff}
8453
+ .badge-critical{background:#ef4444}
8454
+ .badge-high{background:#f97316}
8455
+ .badge-medium{background:#eab308;color:#1e293b}
8456
+ .badge-low{background:#22c55e;color:#1e293b}
8457
+ .hw-section{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:16px;overflow:hidden}
8458
+ .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}
8459
+ .hw-section>summary::-webkit-details-marker{display:none}
8460
+ .hw-section>summary::marker{content:""}
8461
+ .hw-section>summary::after{content:"\\25B6";font-size:12px;color:#64748b;flex-shrink:0;transition:transform 0.2s;margin-left:auto}
8462
+ .hw-section[open]>summary::after{transform:rotate(90deg)}
8463
+ .hw-section[open]>summary{border-bottom:1px solid #334155}
8464
+ .hw-section-body{padding:16px 20px}
8465
+ .hw-section-icon{font-size:20px}
8466
+ .hw-section-title{flex:1}
8467
+ .hw-section-stats{display:inline-flex;gap:8px;font-size:12px;flex-wrap:wrap}
8468
+ .hw-section-stats .badge{font-size:10px;padding:1px 8px}
8469
+ .hw-auto-section{margin-bottom:16px}
8470
+ .hw-auto-section h4{color:#60a5fa;margin-bottom:8px}
8471
+ .hw-manual-section h4{color:#fbbf24;margin-bottom:8px}
8472
+ .hw-clean{color:#22c55e;font-size:14px;padding:8px 12px;background:rgba(34,197,94,0.1);border-radius:6px}
8473
+ .hw-no-auto{color:#94a3b8;font-size:14px;padding:8px 12px;background:rgba(148,163,184,0.08);border-radius:6px}
8474
+ .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)}
8475
+ .hw-manual-checkbox{color:#fbbf24;font-size:16px;flex-shrink:0}
8476
+ .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}
8477
+ .sev-critical{border-left-color:#ef4444}
8478
+ .sev-high{border-left-color:#f97316}
8479
+ .sev-medium{border-left-color:#eab308}
8480
+ .sev-low{border-left-color:#22c55e}
8481
+ .finding-title-text{font-weight:600;font-size:13px;flex:1;min-width:200px}
8482
+ .finding-resource{color:#94a3b8;font-size:12px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
8483
+ .finding-card>details{width:100%;margin-top:4px}
8484
+ .finding-card>details>summary{cursor:pointer;font-size:12px;color:#60a5fa;user-select:none}
8485
+ .finding-card-body{padding:8px 0}
8486
+ .finding-card-body p{color:#cbd5e1;font-size:13px;margin-bottom:4px}
8487
+ .finding-card-body ol{padding-left:20px}
8488
+ .finding-card-body li{color:#cbd5e1;font-size:13px;margin-bottom:2px}
8489
+ .hw-finding-group{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:12px 16px;margin-bottom:8px}
8490
+ .hw-finding-header{display:flex;align-items:center;gap:8px}
8491
+ .hw-finding-title{color:#e2e8f0;font-size:14px;flex:1}
8492
+ .hw-finding-count{color:#94a3b8;font-size:13px;font-weight:600;background:#334155;padding:2px 8px;border-radius:4px}
8493
+ .hw-finding-resources{padding:8px 0}
8494
+ .hw-resource-item{font-size:12px;color:#94a3b8;padding:2px 0;font-family:monospace}
8495
+ .hw-finding-remediation{border-top:1px solid #334155;margin-top:8px;padding-top:8px}
8496
+ .hw-finding-remediation ol{margin:4px 0;padding-left:20px;font-size:13px;color:#cbd5e1}
8497
+ footer{margin-top:48px;padding-top:24px;border-top:1px solid #334155;text-align:center}
8498
+ footer p{color:#64748b;font-size:12px;margin-bottom:4px}
8499
+ @media print{
8500
+ body{background:#fff;color:#1e293b;-webkit-print-color-adjust:exact;print-color-adjust:exact}
8501
+ .container{max-width:100%;padding:20px}
8502
+ .hw-section,.summary-card,.finding-card{background:#fff;border:1px solid #e2e8f0}
8503
+ .badge{border:1px solid}
8504
+ header{border-bottom-color:#e2e8f0}
8505
+ h2{border-bottom-color:#e2e8f0}
8506
+ footer{border-top-color:#e2e8f0}
8507
+ .summary-card .stat-label{color:#64748b}
8508
+ .finding-title-text{color:#1e293b}
8509
+ .finding-resource{color:#64748b}
8510
+ .finding-card-body p,.finding-card-body li{color:#475569}
8511
+ .hw-section[open]>summary{border-bottom-color:#e2e8f0}
8512
+ .hw-manual-item{color:#475569}
8513
+ details{display:block}
8514
+ details>summary{display:block}
8515
+ details>:not(summary){display:block !important}
8516
+ }
8517
+ `;
8518
+ }
8519
+ function generateHwDefenseHtmlReport(scanResults, lang) {
8520
+ const t = getI18n(lang ?? "zh");
8521
+ const htmlLang = (lang ?? "zh") === "zh" ? "zh-CN" : "en";
8522
+ const { accountId, region, scanStart } = scanResults;
8523
+ const date = scanStart.split("T")[0];
8524
+ const scanTime = scanStart.replace("T", " ").replace(/\.\d+Z$/, " UTC");
8525
+ const moduleMap = /* @__PURE__ */ new Map();
8526
+ for (const mod of scanResults.modules) {
8527
+ const findings = mod.findings.map((f) => ({ ...f, module: f.module ?? mod.module }));
8528
+ moduleMap.set(mod.module, findings);
8529
+ }
8530
+ const assignedShFindings = /* @__PURE__ */ new Set();
8531
+ function shFindingKey(f) {
8532
+ return `${f.title}|${f.resourceId}|${f.resourceArn}`;
8533
+ }
8534
+ const sectionResults = [];
8535
+ for (const section of HW_SECTIONS) {
8536
+ const findings = [];
8537
+ for (const mod of section.autoModules) {
8538
+ if (mod === "security_hub_findings") continue;
8539
+ const modFindings = moduleMap.get(mod) ?? [];
8540
+ findings.push(...modFindings);
8541
+ }
8542
+ if (section.shKeywords.length > 0) {
8543
+ const shFindings = moduleMap.get("security_hub_findings") ?? [];
8544
+ for (const f of shFindings) {
8545
+ const key = shFindingKey(f);
8546
+ if (assignedShFindings.has(key)) continue;
8547
+ const searchText = `${f.title} ${f.description} ${f.impact}`.toLowerCase();
8548
+ if (section.shKeywords.some((kw) => searchText.includes(kw))) {
8549
+ findings.push(f);
8550
+ assignedShFindings.add(key);
8551
+ }
8552
+ }
8553
+ }
8554
+ const hasAutoModules = section.autoModules.length > 0 || section.shKeywords.length > 0;
8555
+ const hasAutoResults = hasAutoModules && (section.autoModules.some((m) => moduleMap.has(m)) || section.shKeywords.length > 0 && moduleMap.has("security_hub_findings"));
8556
+ const manualItems = t.hwManualItems[section.id] ?? [];
8557
+ sectionResults.push({
8558
+ id: section.id,
8559
+ findings,
8560
+ manualItems,
8561
+ hasAutoModules,
8562
+ hasAutoResults
8563
+ });
8564
+ }
8565
+ const totalFindings = sectionResults.reduce((sum, s) => sum + s.findings.length, 0);
8566
+ const sectionsChecked = sectionResults.filter((s) => s.hasAutoResults || s.manualItems.length > 0).length;
8567
+ const autoVerified = sectionResults.filter((s) => s.hasAutoResults).length;
8568
+ const totalManualItems = sectionResults.reduce((sum, s) => sum + s.manualItems.length, 0);
8569
+ function groupFindings(findings) {
8570
+ const groups = /* @__PURE__ */ new Map();
8571
+ const groupTitles = /* @__PURE__ */ new Map();
8572
+ for (const f of findings) {
8573
+ const cveMatch = f.title.match(/CVE-\d{4}-\d+/i);
8574
+ if (cveMatch) {
8575
+ const key2 = `cve:${cveMatch[0].toUpperCase()}`;
8576
+ if (!groups.has(key2)) {
8577
+ groups.set(key2, []);
8578
+ groupTitles.set(key2, f.title);
8579
+ }
8580
+ groups.get(key2).push(f);
8581
+ continue;
8582
+ }
8583
+ const controlMatch = f.title.match(/[A-Z]+\.\d+/);
8584
+ if (controlMatch) {
8585
+ const key2 = `ctrl:${controlMatch[0]}`;
8586
+ if (!groups.has(key2)) {
8587
+ groups.set(key2, []);
8588
+ groupTitles.set(key2, f.title);
8589
+ }
8590
+ groups.get(key2).push(f);
8591
+ continue;
8592
+ }
8593
+ const key = `title:${f.title}`;
8594
+ if (!groups.has(key)) {
8595
+ groups.set(key, []);
8596
+ groupTitles.set(key, f.title);
8597
+ }
8598
+ groups.get(key).push(f);
8599
+ }
8600
+ const result = [];
8601
+ for (const [key, gFindings] of groups) {
8602
+ let highestSeverity = "LOW";
8603
+ for (const f of gFindings) {
8604
+ if (SEVERITY_ORDER3.indexOf(f.severity) < SEVERITY_ORDER3.indexOf(highestSeverity)) {
8605
+ highestSeverity = f.severity;
8606
+ }
8607
+ }
8608
+ result.push({ title: groupTitles.get(key), findings: gFindings, highestSeverity });
8609
+ }
8610
+ return result;
8611
+ }
8612
+ const renderGroup = (group) => {
8613
+ const sev = group.highestSeverity.toLowerCase();
8614
+ const count = group.findings.length;
8615
+ const first = group.findings[0];
8616
+ const resourceItems = group.findings.map((f) => `<div class="hw-resource-item">${esc2(f.resourceId)} &mdash; ${esc2(f.resourceArn)}</div>`).join("\n");
8617
+ const remediationSteps = first.remediationSteps.map((s) => `<li>${escWithLinks2(s)}</li>`).join("");
8618
+ return `<div class="hw-finding-group">
8619
+ <div class="hw-finding-header">
8620
+ <span class="badge badge-${esc2(sev)}">${esc2(group.highestSeverity)}</span>
8621
+ <span class="hw-finding-title">${esc2(group.title)}</span>
8622
+ <span class="hw-finding-count">&times;${count}</span>
8623
+ </div>
8624
+ <details>
8625
+ <summary>${t.hwAffectedResources(count)}</summary>
8626
+ <div class="hw-finding-resources">
8627
+ ${resourceItems}
8628
+ </div>
8629
+ <div class="hw-finding-remediation">
8630
+ <strong>${esc2(t.hwRemediation)}:</strong>
8631
+ <ol>${remediationSteps}</ol>
8632
+ </div>
8633
+ </details>
8634
+ </div>`;
8635
+ };
8636
+ const sectionsHtml = sectionResults.map((section) => {
8637
+ const meta = t.hwSectionNames[section.id];
8638
+ if (!meta) return "";
8639
+ const sectionName = meta.name;
8640
+ const sectionIcon = meta.icon ?? "";
8641
+ const statBadges = [];
8642
+ if (section.findings.length > 0) {
8643
+ const sevCounts = {};
8644
+ for (const f of section.findings) {
8645
+ sevCounts[f.severity] = (sevCounts[f.severity] ?? 0) + 1;
8646
+ }
8647
+ for (const sev of SEVERITY_ORDER3) {
8648
+ if (sevCounts[sev]) {
8649
+ statBadges.push(
8650
+ `<span class="badge badge-${sev.toLowerCase()}">${sevCounts[sev]} ${sev}</span>`
8651
+ );
8652
+ }
8653
+ }
8654
+ } else if (section.hasAutoResults) {
8655
+ statBadges.push(`<span style="color:#22c55e;font-size:12px">&#10003; ${esc2(t.hwClean)}</span>`);
8656
+ }
8657
+ if (section.manualItems.length > 0) {
8658
+ statBadges.push(`<span style="color:#fbbf24;font-size:12px">&#9744; ${esc2(t.hwManualCount(section.manualItems.length))}</span>`);
8659
+ }
8660
+ let autoHtml;
8661
+ if (!section.hasAutoModules) {
8662
+ autoHtml = `<div class="hw-no-auto">&#9898; ${esc2(t.hwNoAutoCheck)}</div>`;
8663
+ } else if (!section.hasAutoResults) {
8664
+ autoHtml = `<div class="hw-no-auto">&#9898; ${esc2(t.hwNoAutoCheck)}</div>`;
8665
+ } else if (section.findings.length === 0) {
8666
+ autoHtml = `<div class="hw-clean">&#10004; ${esc2(t.hwClean)}</div>`;
8667
+ } else {
8668
+ const sorted = [...section.findings].sort((a, b) => {
8669
+ const sevDiff = SEVERITY_ORDER3.indexOf(a.severity) - SEVERITY_ORDER3.indexOf(b.severity);
8670
+ if (sevDiff !== 0) return sevDiff;
8671
+ return b.riskScore - a.riskScore;
8672
+ });
8673
+ const groups = groupFindings(sorted);
8674
+ autoHtml = groups.map(renderGroup).join("\n");
8675
+ }
8676
+ let manualHtml = "";
8677
+ if (section.manualItems.length > 0) {
8678
+ const items = section.manualItems.map((item) => `<div class="hw-manual-item"><span class="hw-manual-checkbox">&#9633;</span>${esc2(item)}</div>`).join("\n");
8679
+ manualHtml = `
8680
+ <div class="hw-manual-section">
8681
+ <h4>&#128203; ${esc2(t.hwManualCheck)}</h4>
8682
+ ${items}
8683
+ </div>`;
8684
+ }
8685
+ return `<details class="hw-section">
8686
+ <summary>
8687
+ <span class="hw-section-icon">${esc2(sectionIcon)}</span>
8688
+ <span class="hw-section-title">${esc2(sectionName)}</span>
8689
+ <span class="hw-section-stats">${statBadges.join(" ")}</span>
8690
+ </summary>
8691
+ <div class="hw-section-body">
8692
+ <div class="hw-auto-section">
8693
+ <h4>&#129302; ${esc2(t.hwAutoCheck)}</h4>
8694
+ ${autoHtml}
8695
+ </div>
8696
+ ${manualHtml}
8697
+ </div>
8698
+ </details>`;
8699
+ }).filter(Boolean).join("\n");
8700
+ const findingsColor = totalFindings === 0 ? "#22c55e" : totalFindings <= 5 ? "#eab308" : "#ef4444";
8701
+ return `<!DOCTYPE html>
8702
+ <html lang="${htmlLang}">
8703
+ <head>
8704
+ <meta charset="UTF-8">
8705
+ <meta name="viewport" content="width=device-width,initial-scale=1">
8706
+ <title>${esc2(t.hwReportTitle)} &mdash; ${esc2(date)}</title>
8707
+ <style>${hwCss()}</style>
8708
+ </head>
8709
+ <body>
8710
+ <div class="container">
8711
+
8712
+ <header>
8713
+ <h1>&#128737;&#65039; ${esc2(t.hwReportTitle)}</h1>
8714
+ <div class="meta">${esc2(t.account)}: ${esc2(accountId)} | ${esc2(t.region)}: ${esc2(region)} | ${esc2(t.scanTime)}: ${esc2(scanTime)}</div>
8715
+ </header>
8716
+
8717
+ <section class="summary-cards">
8718
+ <div class="summary-card"><div class="stat-count" style="color:${findingsColor}">${totalFindings}</div><div class="stat-label">${esc2(t.hwTotalFindings)}</div></div>
8719
+ <div class="summary-card"><div class="stat-count" style="color:#60a5fa">${sectionsChecked}</div><div class="stat-label">${esc2(t.hwSectionsChecked)}</div></div>
8720
+ <div class="summary-card"><div class="stat-count" style="color:#22c55e">${autoVerified}</div><div class="stat-label">${esc2(t.hwAutoVerified)}</div></div>
8721
+ <div class="summary-card"><div class="stat-count" style="color:#fbbf24">${totalManualItems}</div><div class="stat-label">${esc2(t.hwManualPending)}</div></div>
8722
+ </section>
8723
+
8724
+ ${sectionsHtml}
8725
+
8726
+ <footer>
8727
+ <p>${esc2(t.generatedBy)} v${VERSION}</p>
8728
+
8729
+ </footer>
8730
+
8731
+ </div>
8732
+ </body>
8733
+ </html>`;
8734
+ }
8735
+
8237
8736
  // src/tools/save-results.ts
8238
8737
  import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
8239
8738
  import { join } from "path";
@@ -8304,7 +8803,7 @@ function saveResults(scanResults, outputDir) {
8304
8803
  }
8305
8804
 
8306
8805
  // src/tools/scan-groups.ts
8307
- var SEVERITY_ORDER3 = {
8806
+ var SEVERITY_ORDER4 = {
8308
8807
  LOW: 0,
8309
8808
  MEDIUM: 1,
8310
8809
  HIGH: 2,
@@ -8313,8 +8812,8 @@ var SEVERITY_ORDER3 = {
8313
8812
  function applyFindingsFilter(moduleName, findings, filter) {
8314
8813
  let result = findings;
8315
8814
  if (filter.minSeverity) {
8316
- const minLevel = SEVERITY_ORDER3[filter.minSeverity.toUpperCase()] ?? 0;
8317
- result = result.filter((f) => (SEVERITY_ORDER3[f.severity] ?? 0) >= minLevel);
8815
+ const minLevel = SEVERITY_ORDER4[filter.minSeverity.toUpperCase()] ?? 0;
8816
+ result = result.filter((f) => (SEVERITY_ORDER4[f.severity] ?? 0) >= minLevel);
8318
8817
  }
8319
8818
  if (moduleName === "security_hub_findings" && filter.securityHubCategories?.length) {
8320
8819
  const keywords = filter.securityHubCategories;
@@ -8348,10 +8847,10 @@ var SCAN_GROUPS = {
8348
8847
  },
8349
8848
  hw_defense: {
8350
8849
  name: "\u62A4\u7F51\u84DD\u961F\u52A0\u56FA",
8351
- description: "\u62A4\u7F51\u524D\u5B89\u5168\u81EA\u67E5 \u2014 \u653B\u51FB\u9762+\u5F31\u70B9\u8BC4\u4F30",
8352
- 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"],
8850
+ description: "\u62A4\u7F51\u524D\u5B89\u5168\u81EA\u67E5 \u2014 \u653B\u51FB\u8005\u89C6\u89D2\u7684\u653B\u51FB\u9762+\u5F31\u70B9\u8BC4\u4F30",
8851
+ 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"],
8353
8852
  findingsFilter: {
8354
- guardDutyTypes: ["Backdoor", "Trojan", "PenTest", "CryptoCurrency"],
8853
+ 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"],
8355
8854
  minSeverity: "MEDIUM"
8356
8855
  }
8357
8856
  },
@@ -8873,6 +9372,7 @@ function createServer(defaultRegion) {
8873
9372
  const summaryContent = content[0];
8874
9373
  if (summaryContent && summaryContent.type === "text") {
8875
9374
  summaryContent.text += "\n\n" + getHwDefenseChecklist(lang ?? "zh");
9375
+ 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.";
8876
9376
  }
8877
9377
  }
8878
9378
  return { content };
@@ -8971,6 +9471,23 @@ function createServer(defaultRegion) {
8971
9471
  }
8972
9472
  }
8973
9473
  );
9474
+ server.tool(
9475
+ "generate_hw_defense_report",
9476
+ "Generate an HTML report organized by HW Defense (\u62A4\u7F51) SOP checklist categories. Save as .html file.",
9477
+ {
9478
+ 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)")
9480
+ },
9481
+ async ({ scan_results, lang }) => {
9482
+ try {
9483
+ const parsed = JSON.parse(scan_results);
9484
+ const report = generateHwDefenseHtmlReport(parsed, lang ?? "zh");
9485
+ return { content: [{ type: "text", text: report }] };
9486
+ } catch (err) {
9487
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
9488
+ }
9489
+ }
9490
+ );
8974
9491
  server.tool(
8975
9492
  "generate_maturity_report",
8976
9493
  "Generate a security maturity assessment report from scan_all results. Requires service_detection module output. Read-only.",
@@ -9267,6 +9784,7 @@ export {
9267
9784
  calculateScore,
9268
9785
  createServer,
9269
9786
  generateHtmlReport,
9787
+ generateHwDefenseHtmlReport,
9270
9788
  generateMarkdownReport,
9271
9789
  generateMlps3HtmlReport,
9272
9790
  getCurrentAccountId,