aws-security-mcp 0.7.2 → 0.7.4
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/dist/bin/aws-security-mcp.js +98 -65
- package/dist/bin/aws-security-mcp.js.map +1 -1
- package/dist/src/commands/dashboard.js +2 -2
- package/dist/src/commands/dashboard.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.js +86 -21
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ MCP server for automated AWS security scanning — 19 modules, risk scoring, zer
|
|
|
14
14
|
- **100% Read-Only** — uses only Describe/Get/List API calls; never modifies your AWS resources
|
|
15
15
|
- **Multi-Account Support** — scan all accounts in an AWS Organization via `org_mode` with cross-account role assumption
|
|
16
16
|
- **Parallel Execution** — all modules run concurrently via `Promise.allSettled`
|
|
17
|
-
- **Report Generation** — Markdown, professional HTML,
|
|
17
|
+
- **Report Generation** — Markdown, professional HTML, MLPS Level 3 compliance, and HW Defense reports
|
|
18
18
|
- **React Dashboard** — local or S3-hosted dashboard with 30-day trend charts
|
|
19
19
|
- **MCP Resources** — embedded security rules and risk scoring model documentation
|
|
20
20
|
- **MCP Prompts** — pre-built workflows for full scans and finding analysis
|
|
@@ -141,6 +141,7 @@ For multi-account scanning across an AWS Organization:
|
|
|
141
141
|
| `generate_html_report` | Generate a professional HTML report |
|
|
142
142
|
| `generate_mlps3_report` | Generate a MLPS Level 3 compliance report |
|
|
143
143
|
| `generate_mlps3_html_report` | Generate a MLPS Level 3 HTML compliance report |
|
|
144
|
+
| `generate_hw_defense_report` | Generate an HW Defense HTML report (SOP-organized, findings grouped by CVE/control-ID) |
|
|
144
145
|
| `generate_maturity_report` | Generate a security maturity assessment |
|
|
145
146
|
| `save_results` | Save scan results for the dashboard |
|
|
146
147
|
| `get_setup_template` | Get CloudFormation StackSet template for cross-account audit role |
|
|
@@ -282,7 +283,7 @@ Pre-defined scanner groupings for common scenarios:
|
|
|
282
283
|
| Group | Description | Modules |
|
|
283
284
|
|-------|-------------|---------|
|
|
284
285
|
| `mlps3_precheck` | GB/T 22239-2019 等保三级预检 | 17 modules |
|
|
285
|
-
| `hw_defense` | 护网蓝队加固 |
|
|
286
|
+
| `hw_defense` | 护网蓝队加固 — attacker-focused hardening | 11 modules |
|
|
286
287
|
| `exposure` | 公网暴露面评估 | 8 modules |
|
|
287
288
|
| `data_encryption` | 数据加密审计 | 2 modules |
|
|
288
289
|
| `least_privilege` | 最小权限审计 | 3 modules |
|
|
@@ -347,6 +348,15 @@ The `generate_report` tool produces a Markdown report with:
|
|
|
347
348
|
- **Scan Statistics** — per-module resource counts and status
|
|
348
349
|
- **Recommendations** — prioritized action items
|
|
349
350
|
|
|
351
|
+
## HW Defense Report
|
|
352
|
+
|
|
353
|
+
The `generate_hw_defense_report` tool produces a dedicated HTML report for 护网 (HW) blue-team hardening exercises. Key features:
|
|
354
|
+
|
|
355
|
+
- **SOP checklist organization** — findings are grouped by standard operating procedure categories rather than by scanner module
|
|
356
|
+
- **Grouped findings** — duplicate and related findings are collapsed by CVE ID, control ID, or title, reducing noise
|
|
357
|
+
- **Attacker-focused perspective** — the `hw_defense` scan group (11 modules) prioritizes checks that mirror real-world red-team attack chains: privilege escalation, network exposure, secret leakage, missing detection services, and patch gaps
|
|
358
|
+
- **Collapsible sections** — categories default to collapsed for quick executive overview, expandable for detailed review
|
|
359
|
+
|
|
350
360
|
## License
|
|
351
361
|
|
|
352
362
|
MIT
|
|
@@ -16,7 +16,7 @@ __export(dashboard_exports, {
|
|
|
16
16
|
});
|
|
17
17
|
import { createServer as createServer2 } from "http";
|
|
18
18
|
import { readFile } from "fs/promises";
|
|
19
|
-
import { join as join3, extname, resolve } from "path";
|
|
19
|
+
import { join as join3, extname, resolve, sep } from "path";
|
|
20
20
|
import { existsSync as existsSync2, copyFileSync } from "fs";
|
|
21
21
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
22
22
|
import { exec } from "child_process";
|
|
@@ -48,7 +48,7 @@ Expected: ${dashboardDir}`
|
|
|
48
48
|
let filePath = resolve(
|
|
49
49
|
join3(dashboardDir, url === "/" ? "index.html" : url)
|
|
50
50
|
);
|
|
51
|
-
if (!filePath.startsWith(resolvedBase +
|
|
51
|
+
if (!filePath.startsWith(resolvedBase + sep) && filePath !== resolvedBase) {
|
|
52
52
|
res.writeHead(403);
|
|
53
53
|
res.end("Forbidden");
|
|
54
54
|
return;
|
|
@@ -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.4";
|
|
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;
|
|
2809
|
+
try {
|
|
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
|
+
}
|
|
2786
2819
|
try {
|
|
2787
|
-
const ver = await
|
|
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) {
|
|
@@ -9950,7 +9983,7 @@ var HELP = `Usage: aws-security-mcp [command] [options]
|
|
|
9950
9983
|
Commands:
|
|
9951
9984
|
(default) Start MCP server (stdio, for Kiro/Claude Code)
|
|
9952
9985
|
dashboard Start local HTTP server serving the security dashboard
|
|
9953
|
-
deploy-dashboard
|
|
9986
|
+
deploy-dashboard Upload dashboard files to a private S3 bucket
|
|
9954
9987
|
|
|
9955
9988
|
Options:
|
|
9956
9989
|
--region <region> AWS region (default: AWS_REGION env or us-east-1)
|