aws-security-mcp 0.7.2 → 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 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, and MLPS Level 3 compliance reports
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` | 护网蓝队加固 | 14 modules |
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
@@ -116,9 +116,7 @@ import { join as join4, extname as extname2, relative } from "path";
116
116
  import { fileURLToPath as fileURLToPath3 } from "url";
117
117
  import {
118
118
  S3Client as S3Client5,
119
- PutObjectCommand,
120
- PutBucketWebsiteCommand,
121
- PutBucketPolicyCommand
119
+ PutObjectCommand
122
120
  } from "@aws-sdk/client-s3";
123
121
  function collectFiles(dir) {
124
122
  const files = [];
@@ -155,38 +153,8 @@ Expected: ${dashboardDir}`
155
153
  );
156
154
  }
157
155
  const s3 = new S3Client5({ region });
158
- console.log(`Configuring s3://${bucket} for static website hosting...`);
159
- await s3.send(
160
- new PutBucketWebsiteCommand({
161
- Bucket: bucket,
162
- WebsiteConfiguration: {
163
- IndexDocument: { Suffix: "index.html" },
164
- ErrorDocument: { Key: "index.html" }
165
- // SPA fallback
166
- }
167
- })
168
- );
169
- const partition = region.startsWith("cn-") ? "aws-cn" : "aws";
170
- console.log(`Setting public read bucket policy on s3://${bucket}...`);
171
- await s3.send(
172
- new PutBucketPolicyCommand({
173
- Bucket: bucket,
174
- Policy: JSON.stringify({
175
- Version: "2012-10-17",
176
- Statement: [
177
- {
178
- Sid: "PublicReadGetObject",
179
- Effect: "Allow",
180
- Principal: "*",
181
- Action: "s3:GetObject",
182
- Resource: `arn:${partition}:s3:::${bucket}/*`
183
- }
184
- ]
185
- })
186
- })
187
- );
188
156
  const files = collectFiles(dashboardDir);
189
- console.log(`Uploading ${files.length} files to s3://${bucket}...`);
157
+ console.log(`Uploading ${files.length} files to s3://${bucket}/ ...`);
190
158
  for (const filePath of files) {
191
159
  const key = relative(dashboardDir, filePath);
192
160
  const ext = extname2(filePath);
@@ -202,14 +170,14 @@ Expected: ${dashboardDir}`
202
170
  );
203
171
  console.log(` ${key}`);
204
172
  }
205
- const domain = region.startsWith("cn-") ? "amazonaws.com.cn" : "amazonaws.com";
206
- const websiteUrl = `http://${bucket}.s3-website.${region}.${domain}`;
207
173
  console.log(`
208
- Dashboard deployed successfully!`);
209
- console.log(`Website URL: ${websiteUrl}`);
210
- console.log(
211
- "\nNote: Ensure S3 Block Public Access is disabled on this bucket for the website to be accessible.\n"
212
- );
174
+ \u2705 Dashboard uploaded to s3://${bucket}/`);
175
+ console.log(`
176
+ S3 bucket remains private (Block Public Access enabled).`);
177
+ console.log(`Access control is managed via IAM permissions.`);
178
+ console.log(`
179
+ To view the dashboard locally:`);
180
+ console.log(` aws-security-mcp dashboard --port 3000`);
213
181
  }
214
182
  var __filename2, __dirname2, CONTENT_TYPES;
215
183
  var init_deploy_dashboard = __esm({
@@ -237,7 +205,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
237
205
  import { z } from "zod";
238
206
 
239
207
  // src/version.ts
240
- var VERSION = "0.7.2";
208
+ var VERSION = "0.7.3";
241
209
 
242
210
  // src/utils/aws-client.ts
243
211
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -313,6 +281,25 @@ async function listOrgAccounts(region) {
313
281
  }
314
282
 
315
283
  // src/scanners/runner.ts
284
+ var DEFAULT_CONCURRENCY = 5;
285
+ async function runWithConcurrency(tasks, limit) {
286
+ const results = [];
287
+ const executing = /* @__PURE__ */ new Set();
288
+ for (let i = 0; i < tasks.length; i++) {
289
+ const idx = i;
290
+ const p = tasks[idx]().then((value) => {
291
+ results[idx] = { status: "fulfilled", value };
292
+ }).catch((reason) => {
293
+ results[idx] = { status: "rejected", reason };
294
+ }).finally(() => {
295
+ executing.delete(p);
296
+ });
297
+ executing.add(p);
298
+ if (executing.size >= limit) await Promise.race(executing);
299
+ }
300
+ await Promise.all(executing);
301
+ return results;
302
+ }
316
303
  var AGGREGATION_MODULES = /* @__PURE__ */ new Set([
317
304
  "security_hub_findings",
318
305
  "guardduty_findings",
@@ -360,8 +347,8 @@ function buildSummary(modules) {
360
347
  modulesError
361
348
  };
362
349
  }
363
- async function runScannersWithContext(scanners, ctx) {
364
- const settled = await Promise.allSettled(scanners.map((s) => s.scan(ctx)));
350
+ async function runScannersWithContext(scanners, ctx, concurrency = DEFAULT_CONCURRENCY) {
351
+ const settled = await runWithConcurrency(scanners.map((s) => () => s.scan(ctx)), concurrency);
365
352
  return settled.map((result, i) => {
366
353
  if (result.status === "fulfilled") {
367
354
  for (const f of result.value.findings) {
@@ -882,6 +869,25 @@ import {
882
869
  DescribeInstancesCommand,
883
870
  DescribeInstanceAttributeCommand
884
871
  } from "@aws-sdk/client-ec2";
872
+ var USERDATA_CONCURRENCY = 5;
873
+ async function runWithConcurrency2(tasks, limit) {
874
+ const results = [];
875
+ const executing = /* @__PURE__ */ new Set();
876
+ for (let i = 0; i < tasks.length; i++) {
877
+ const idx = i;
878
+ const p = tasks[idx]().then((value) => {
879
+ results[idx] = { status: "fulfilled", value };
880
+ }).catch((reason) => {
881
+ results[idx] = { status: "rejected", reason };
882
+ }).finally(() => {
883
+ executing.delete(p);
884
+ });
885
+ executing.add(p);
886
+ if (executing.size >= limit) await Promise.race(executing);
887
+ }
888
+ await Promise.all(executing);
889
+ return results;
890
+ }
885
891
  var SECRET_PATTERNS = [
886
892
  { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/, matchType: "value" },
887
893
  { name: "Private Key", pattern: /-----BEGIN.*PRIVATE KEY-----/, matchType: "value" },
@@ -981,25 +987,28 @@ var SecretExposureScanner = class {
981
987
  nextToken = resp.NextToken;
982
988
  } while (nextToken);
983
989
  resourcesScanned += instances.length;
984
- for (const inst of instances) {
990
+ const userDataTasks = instances.map((inst) => async () => {
991
+ const instId = inst.InstanceId ?? "unknown";
992
+ const attrResp = await ec2.send(
993
+ new DescribeInstanceAttributeCommand({
994
+ InstanceId: instId,
995
+ Attribute: "userData"
996
+ })
997
+ );
998
+ const raw = attrResp.UserData?.Value;
999
+ return { instId, userData: raw ? Buffer.from(raw, "base64").toString("utf-8") : void 0 };
1000
+ });
1001
+ const settled = await runWithConcurrency2(userDataTasks, USERDATA_CONCURRENCY);
1002
+ for (let i = 0; i < instances.length; i++) {
1003
+ const result = settled[i];
1004
+ const inst = instances[i];
985
1005
  const instId = inst.InstanceId ?? "unknown";
986
1006
  const instArn = `arn:${partition}:ec2:${region}:${accountId}:instance/${instId}`;
987
- let userData;
988
- try {
989
- const attrResp = await ec2.send(
990
- new DescribeInstanceAttributeCommand({
991
- InstanceId: instId,
992
- Attribute: "userData"
993
- })
994
- );
995
- const raw = attrResp.UserData?.Value;
996
- if (raw) {
997
- userData = Buffer.from(raw, "base64").toString("utf-8");
998
- }
999
- } catch (e) {
1000
- warnings.push(`Could not read userData for ${instId}: ${e instanceof Error ? e.message : String(e)}`);
1007
+ if (result.status === "rejected") {
1008
+ warnings.push(`Could not read userData for ${instId}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
1001
1009
  continue;
1002
1010
  }
1011
+ const userData = result.value.userData;
1003
1012
  if (!userData) continue;
1004
1013
  for (const sp of SECRET_PATTERNS) {
1005
1014
  if (sp.matchType === "name") continue;
@@ -2190,6 +2199,7 @@ import {
2190
2199
  import {
2191
2200
  S3Client as S3Client3,
2192
2201
  ListBucketsCommand as ListBucketsCommand2,
2202
+ GetBucketLocationCommand as GetBucketLocationCommand2,
2193
2203
  GetBucketTaggingCommand
2194
2204
  } from "@aws-sdk/client-s3";
2195
2205
  var DEFAULT_REQUIRED_TAGS = ["Environment", "Project", "Owner"];
@@ -2306,8 +2316,19 @@ var TagComplianceScanner = class {
2306
2316
  for (const bucket of buckets) {
2307
2317
  const name = bucket.Name ?? "unknown";
2308
2318
  const arn = `arn:${partition}:s3:::${name}`;
2319
+ let bucketClient;
2309
2320
  try {
2310
- const taggingResp = await s3Client.send(
2321
+ const locResp = await s3Client.send(new GetBucketLocationCommand2({ Bucket: name }));
2322
+ const rawLoc = locResp.LocationConstraint || "us-east-1";
2323
+ const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
2324
+ bucketClient = bucketRegion === region ? s3Client : createClient(S3Client3, bucketRegion, ctx.credentials);
2325
+ } catch (e) {
2326
+ const msg = e instanceof Error ? e.message : String(e);
2327
+ warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
2328
+ continue;
2329
+ }
2330
+ try {
2331
+ const taggingResp = await bucketClient.send(
2311
2332
  new GetBucketTaggingCommand({ Bucket: name })
2312
2333
  );
2313
2334
  const tags = (taggingResp.TagSet ?? []).map((t) => ({
@@ -2609,6 +2630,7 @@ import {
2609
2630
  import {
2610
2631
  S3Client as S3Client4,
2611
2632
  ListBucketsCommand as ListBucketsCommand3,
2633
+ GetBucketLocationCommand as GetBucketLocationCommand3,
2612
2634
  GetBucketVersioningCommand,
2613
2635
  GetBucketReplicationCommand
2614
2636
  } from "@aws-sdk/client-s3";
@@ -2783,8 +2805,19 @@ var DisasterRecoveryScanner = class {
2783
2805
  resourcesScanned += bucketNames.length;
2784
2806
  for (const name of bucketNames) {
2785
2807
  const arn = `arn:${partition}:s3:::${name}`;
2808
+ let bucketClient;
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 s3Client.send(
2820
+ const ver = await bucketClient.send(
2788
2821
  new GetBucketVersioningCommand({ Bucket: name })
2789
2822
  );
2790
2823
  if (ver.Status !== "Enabled") {
@@ -2810,7 +2843,7 @@ var DisasterRecoveryScanner = class {
2810
2843
  warnings.push(`Bucket ${name} versioning check failed: ${msg}`);
2811
2844
  }
2812
2845
  try {
2813
- await s3Client.send(
2846
+ await bucketClient.send(
2814
2847
  new GetBucketReplicationCommand({ Bucket: name })
2815
2848
  );
2816
2849
  } catch (e) {
@@ -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 Deploy dashboard to an S3 bucket as a static website
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)