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/README.md +12 -2
- package/dashboard/dist/assets/index-BXloWmhE.css +2 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/bin/aws-security-mcp.js +554 -69
- package/dist/bin/aws-security-mcp.js.map +1 -1
- package/dist/src/commands/deploy-dashboard.js +9 -41
- package/dist/src/commands/deploy-dashboard.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +545 -27
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-UN8P_PO6.css +0 -2
- /package/dashboard/dist/assets/{index-AKJ_-GfD.js → index-DsWFAp9v.js} +0 -0
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
760
|
-
|
|
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
|
|
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
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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)} — ${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">×${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">✓ ${esc2(t.hwClean)}</span>`);
|
|
8656
|
+
}
|
|
8657
|
+
if (section.manualItems.length > 0) {
|
|
8658
|
+
statBadges.push(`<span style="color:#fbbf24;font-size:12px">☐ ${esc2(t.hwManualCount(section.manualItems.length))}</span>`);
|
|
8659
|
+
}
|
|
8660
|
+
let autoHtml;
|
|
8661
|
+
if (!section.hasAutoModules) {
|
|
8662
|
+
autoHtml = `<div class="hw-no-auto">⚪ ${esc2(t.hwNoAutoCheck)}</div>`;
|
|
8663
|
+
} else if (!section.hasAutoResults) {
|
|
8664
|
+
autoHtml = `<div class="hw-no-auto">⚪ ${esc2(t.hwNoAutoCheck)}</div>`;
|
|
8665
|
+
} else if (section.findings.length === 0) {
|
|
8666
|
+
autoHtml = `<div class="hw-clean">✔ ${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">□</span>${esc2(item)}</div>`).join("\n");
|
|
8679
|
+
manualHtml = `
|
|
8680
|
+
<div class="hw-manual-section">
|
|
8681
|
+
<h4>📋 ${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>🤖 ${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)} — ${esc2(date)}</title>
|
|
8707
|
+
<style>${hwCss()}</style>
|
|
8708
|
+
</head>
|
|
8709
|
+
<body>
|
|
8710
|
+
<div class="container">
|
|
8711
|
+
|
|
8712
|
+
<header>
|
|
8713
|
+
<h1>🛡️ ${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
|
|
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 =
|
|
8317
|
-
result = result.filter((f) => (
|
|
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", "
|
|
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
|
-
|
|
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,
|