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
|
@@ -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
|
|
209
|
-
console.log(`
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
988
|
-
|
|
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
|
|
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
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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)} — ${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">×${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">✓ ${esc2(t.hwClean)}</span>`);
|
|
8852
|
+
}
|
|
8853
|
+
if (section.manualItems.length > 0) {
|
|
8854
|
+
statBadges.push(`<span style="color:#fbbf24;font-size:12px">☐ ${esc2(t.hwManualCount(section.manualItems.length))}</span>`);
|
|
8855
|
+
}
|
|
8856
|
+
let autoHtml;
|
|
8857
|
+
if (!section.hasAutoModules) {
|
|
8858
|
+
autoHtml = `<div class="hw-no-auto">⚪ ${esc2(t.hwNoAutoCheck)}</div>`;
|
|
8859
|
+
} else if (!section.hasAutoResults) {
|
|
8860
|
+
autoHtml = `<div class="hw-no-auto">⚪ ${esc2(t.hwNoAutoCheck)}</div>`;
|
|
8861
|
+
} else if (section.findings.length === 0) {
|
|
8862
|
+
autoHtml = `<div class="hw-clean">✔ ${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">□</span>${esc2(item)}</div>`).join("\n");
|
|
8875
|
+
manualHtml = `
|
|
8876
|
+
<div class="hw-manual-section">
|
|
8877
|
+
<h4>📋 ${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>🤖 ${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)} — ${esc2(date)}</title>
|
|
8903
|
+
<style>${hwCss()}</style>
|
|
8904
|
+
</head>
|
|
8905
|
+
<body>
|
|
8906
|
+
<div class="container">
|
|
8907
|
+
|
|
8908
|
+
<header>
|
|
8909
|
+
<h1>🛡️ ${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
|
|
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 =
|
|
8545
|
-
result = result.filter((f) => (
|
|
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", "
|
|
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
|
-
|
|
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
|
|
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)
|