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.
@@ -1,7 +1,7 @@
1
1
  // src/commands/dashboard.ts
2
2
  import { createServer } from "http";
3
3
  import { readFile } from "fs/promises";
4
- import { join, extname, resolve } from "path";
4
+ import { join, extname, resolve, sep } from "path";
5
5
  import { existsSync, copyFileSync } from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
  import { exec } from "child_process";
@@ -46,7 +46,7 @@ Expected: ${dashboardDir}`
46
46
  let filePath = resolve(
47
47
  join(dashboardDir, url === "/" ? "index.html" : url)
48
48
  );
49
- if (!filePath.startsWith(resolvedBase + "/") && filePath !== resolvedBase) {
49
+ if (!filePath.startsWith(resolvedBase + sep) && filePath !== resolvedBase) {
50
50
  res.writeHead(403);
51
51
  res.end("Forbidden");
52
52
  return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/dashboard.ts"],"sourcesContent":["import { createServer } from \"node:http\";\nimport { readFile } from \"node:fs/promises\";\nimport { join, extname, resolve } from \"node:path\";\nimport { existsSync, copyFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { exec } from \"node:child_process\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = join(__filename, \"..\");\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".js\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\nexport function startDashboard(port = 3000): void {\n // dashboard/dist is two levels up from dist/src/commands/\n const dashboardDir = join(__dirname, \"../../dashboard/dist\");\n\n if (!existsSync(dashboardDir)) {\n console.error(\n `Dashboard not built. Run \"npm run build:dashboard\" first.\\n` +\n `Expected: ${dashboardDir}`,\n );\n process.exit(1);\n }\n\n // Copy real data.json if available\n const dataSource = join(\n process.env.HOME || process.env.USERPROFILE || \"~\",\n \".aws-security/dashboard/data.json\",\n );\n const dataDest = join(dashboardDir, \"data.json\");\n if (existsSync(dataSource)) {\n copyFileSync(dataSource, dataDest);\n console.log(`Loaded scan data from ${dataSource}`);\n } else {\n console.log(\n \"No scan data found at ~/.aws-security/dashboard/data.json — using bundled sample data\",\n );\n }\n\n const resolvedBase = resolve(dashboardDir);\n\n const server = createServer(async (req, res) => {\n const url = req.url?.split(\"?\")[0] ?? \"/\";\n let filePath = resolve(\n join(dashboardDir, url === \"/\" ? \"index.html\" : url),\n );\n\n // Ensure the resolved path is within dashboardDir\n if (!filePath.startsWith(resolvedBase + \"/\") && filePath !== resolvedBase) {\n res.writeHead(403);\n res.end(\"Forbidden\");\n return;\n }\n\n // SPA fallback: if file doesn't exist and isn't a static asset, serve index.html\n if (!existsSync(filePath)) {\n filePath = join(dashboardDir, \"index.html\");\n }\n\n try {\n const content = await readFile(filePath);\n const ext = extname(filePath);\n res.writeHead(200, {\n \"Content-Type\": MIME_TYPES[ext] || \"application/octet-stream\",\n });\n res.end(content);\n } catch {\n res.writeHead(404);\n res.end(\"Not found\");\n }\n });\n\n server.listen(port, () => {\n const url = `http://localhost:${port}`;\n console.log(`\\nAWS Security Dashboard: ${url}\\n`);\n console.log(\"Press Ctrl+C to stop.\\n\");\n\n // Try to open browser\n const cmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${cmd} ${url}`, () => {\n // ignore errors — browser open is best-effort\n });\n });\n\n server.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n console.error(`Port ${port} is already in use. Try --port <other>`);\n process.exit(1);\n }\n throw err;\n });\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB;AACzB,SAAS,MAAM,SAAS,eAAe;AACvC,SAAS,YAAY,oBAAoB;AACzC,SAAS,qBAAqB;AAC9B,SAAS,YAAY;AAErB,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,KAAK,YAAY,IAAI;AAEvC,IAAM,aAAqC;AAAA,EACzC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AACZ;AAEO,SAAS,eAAe,OAAO,KAAY;AAEhD,QAAM,eAAe,KAAK,WAAW,sBAAsB;AAE3D,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,YAAQ;AAAA,MACN;AAAA,YACe,YAAY;AAAA,IAC7B;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,aAAa;AAAA,IACjB,QAAQ,IAAI,QAAQ,QAAQ,IAAI,eAAe;AAAA,IAC/C;AAAA,EACF;AACA,QAAM,WAAW,KAAK,cAAc,WAAW;AAC/C,MAAI,WAAW,UAAU,GAAG;AAC1B,iBAAa,YAAY,QAAQ;AACjC,YAAQ,IAAI,yBAAyB,UAAU,EAAE;AAAA,EACnD,OAAO;AACL,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,QAAM,eAAe,QAAQ,YAAY;AAEzC,QAAM,SAAS,aAAa,OAAO,KAAK,QAAQ;AAC9C,UAAM,MAAM,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK;AACtC,QAAI,WAAW;AAAA,MACb,KAAK,cAAc,QAAQ,MAAM,eAAe,GAAG;AAAA,IACrD;AAGA,QAAI,CAAC,SAAS,WAAW,eAAe,GAAG,KAAK,aAAa,cAAc;AACzE,UAAI,UAAU,GAAG;AACjB,UAAI,IAAI,WAAW;AACnB;AAAA,IACF;AAGA,QAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,iBAAW,KAAK,cAAc,YAAY;AAAA,IAC5C;AAEA,QAAI;AACF,YAAM,UAAU,MAAM,SAAS,QAAQ;AACvC,YAAM,MAAM,QAAQ,QAAQ;AAC5B,UAAI,UAAU,KAAK;AAAA,QACjB,gBAAgB,WAAW,GAAG,KAAK;AAAA,MACrC,CAAC;AACD,UAAI,IAAI,OAAO;AAAA,IACjB,QAAQ;AACN,UAAI,UAAU,GAAG;AACjB,UAAI,IAAI,WAAW;AAAA,IACrB;AAAA,EACF,CAAC;AAED,SAAO,OAAO,MAAM,MAAM;AACxB,UAAM,MAAM,oBAAoB,IAAI;AACpC,YAAQ,IAAI;AAAA,0BAA6B,GAAG;AAAA,CAAI;AAChD,YAAQ,IAAI,yBAAyB;AAGrC,UAAM,MACJ,QAAQ,aAAa,WACjB,SACA,QAAQ,aAAa,UACnB,UACA;AACR,SAAK,GAAG,GAAG,IAAI,GAAG,IAAI,MAAM;AAAA,IAE5B,CAAC;AAAA,EACH,CAAC;AAED,SAAO,GAAG,SAAS,CAAC,QAA+B;AACjD,QAAI,IAAI,SAAS,cAAc;AAC7B,cAAQ,MAAM,QAAQ,IAAI,wCAAwC;AAClE,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM;AAAA,EACR,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../../src/commands/dashboard.ts"],"sourcesContent":["import { createServer } from \"node:http\";\nimport { readFile } from \"node:fs/promises\";\nimport { join, extname, resolve, sep } from \"node:path\";\nimport { existsSync, copyFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { exec } from \"node:child_process\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = join(__filename, \"..\");\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".js\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\nexport function startDashboard(port = 3000): void {\n // dashboard/dist is two levels up from dist/src/commands/\n const dashboardDir = join(__dirname, \"../../dashboard/dist\");\n\n if (!existsSync(dashboardDir)) {\n console.error(\n `Dashboard not built. Run \"npm run build:dashboard\" first.\\n` +\n `Expected: ${dashboardDir}`,\n );\n process.exit(1);\n }\n\n // Copy real data.json if available\n const dataSource = join(\n process.env.HOME || process.env.USERPROFILE || \"~\",\n \".aws-security/dashboard/data.json\",\n );\n const dataDest = join(dashboardDir, \"data.json\");\n if (existsSync(dataSource)) {\n copyFileSync(dataSource, dataDest);\n console.log(`Loaded scan data from ${dataSource}`);\n } else {\n console.log(\n \"No scan data found at ~/.aws-security/dashboard/data.json — using bundled sample data\",\n );\n }\n\n const resolvedBase = resolve(dashboardDir);\n\n const server = createServer(async (req, res) => {\n const url = req.url?.split(\"?\")[0] ?? \"/\";\n let filePath = resolve(\n join(dashboardDir, url === \"/\" ? \"index.html\" : url),\n );\n\n // Ensure the resolved path is within dashboardDir (handle both / and \\ separators)\n if (!filePath.startsWith(resolvedBase + sep) && filePath !== resolvedBase) {\n res.writeHead(403);\n res.end(\"Forbidden\");\n return;\n }\n\n // SPA fallback: if file doesn't exist and isn't a static asset, serve index.html\n if (!existsSync(filePath)) {\n filePath = join(dashboardDir, \"index.html\");\n }\n\n try {\n const content = await readFile(filePath);\n const ext = extname(filePath);\n res.writeHead(200, {\n \"Content-Type\": MIME_TYPES[ext] || \"application/octet-stream\",\n });\n res.end(content);\n } catch {\n res.writeHead(404);\n res.end(\"Not found\");\n }\n });\n\n server.listen(port, () => {\n const url = `http://localhost:${port}`;\n console.log(`\\nAWS Security Dashboard: ${url}\\n`);\n console.log(\"Press Ctrl+C to stop.\\n\");\n\n // Try to open browser\n const cmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${cmd} ${url}`, () => {\n // ignore errors — browser open is best-effort\n });\n });\n\n server.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n console.error(`Port ${port} is already in use. Try --port <other>`);\n process.exit(1);\n }\n throw err;\n });\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB;AACzB,SAAS,MAAM,SAAS,SAAS,WAAW;AAC5C,SAAS,YAAY,oBAAoB;AACzC,SAAS,qBAAqB;AAC9B,SAAS,YAAY;AAErB,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,KAAK,YAAY,IAAI;AAEvC,IAAM,aAAqC;AAAA,EACzC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AACZ;AAEO,SAAS,eAAe,OAAO,KAAY;AAEhD,QAAM,eAAe,KAAK,WAAW,sBAAsB;AAE3D,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,YAAQ;AAAA,MACN;AAAA,YACe,YAAY;AAAA,IAC7B;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,aAAa;AAAA,IACjB,QAAQ,IAAI,QAAQ,QAAQ,IAAI,eAAe;AAAA,IAC/C;AAAA,EACF;AACA,QAAM,WAAW,KAAK,cAAc,WAAW;AAC/C,MAAI,WAAW,UAAU,GAAG;AAC1B,iBAAa,YAAY,QAAQ;AACjC,YAAQ,IAAI,yBAAyB,UAAU,EAAE;AAAA,EACnD,OAAO;AACL,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,QAAM,eAAe,QAAQ,YAAY;AAEzC,QAAM,SAAS,aAAa,OAAO,KAAK,QAAQ;AAC9C,UAAM,MAAM,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK;AACtC,QAAI,WAAW;AAAA,MACb,KAAK,cAAc,QAAQ,MAAM,eAAe,GAAG;AAAA,IACrD;AAGA,QAAI,CAAC,SAAS,WAAW,eAAe,GAAG,KAAK,aAAa,cAAc;AACzE,UAAI,UAAU,GAAG;AACjB,UAAI,IAAI,WAAW;AACnB;AAAA,IACF;AAGA,QAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,iBAAW,KAAK,cAAc,YAAY;AAAA,IAC5C;AAEA,QAAI;AACF,YAAM,UAAU,MAAM,SAAS,QAAQ;AACvC,YAAM,MAAM,QAAQ,QAAQ;AAC5B,UAAI,UAAU,KAAK;AAAA,QACjB,gBAAgB,WAAW,GAAG,KAAK;AAAA,MACrC,CAAC;AACD,UAAI,IAAI,OAAO;AAAA,IACjB,QAAQ;AACN,UAAI,UAAU,GAAG;AACjB,UAAI,IAAI,WAAW;AAAA,IACrB;AAAA,EACF,CAAC;AAED,SAAO,OAAO,MAAM,MAAM;AACxB,UAAM,MAAM,oBAAoB,IAAI;AACpC,YAAQ,IAAI;AAAA,0BAA6B,GAAG;AAAA,CAAI;AAChD,YAAQ,IAAI,yBAAyB;AAGrC,UAAM,MACJ,QAAQ,aAAa,WACjB,SACA,QAAQ,aAAa,UACnB,UACA;AACR,SAAK,GAAG,GAAG,IAAI,GAAG,IAAI,MAAM;AAAA,IAE5B,CAAC;AAAA,EACH,CAAC;AAED,SAAO,GAAG,SAAS,CAAC,QAA+B;AACjD,QAAI,IAAI,SAAS,cAAc;AAC7B,cAAQ,MAAM,QAAQ,IAAI,wCAAwC;AAClE,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM;AAAA,EACR,CAAC;AACH;","names":[]}
@@ -4,9 +4,7 @@ import { join, extname, relative } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import {
6
6
  S3Client,
7
- PutObjectCommand,
8
- PutBucketWebsiteCommand,
9
- PutBucketPolicyCommand
7
+ PutObjectCommand
10
8
  } from "@aws-sdk/client-s3";
11
9
  var __filename = fileURLToPath(import.meta.url);
12
10
  var __dirname = join(__filename, "..");
@@ -56,38 +54,8 @@ Expected: ${dashboardDir}`
56
54
  );
57
55
  }
58
56
  const s3 = new S3Client({ region });
59
- console.log(`Configuring s3://${bucket} for static website hosting...`);
60
- await s3.send(
61
- new PutBucketWebsiteCommand({
62
- Bucket: bucket,
63
- WebsiteConfiguration: {
64
- IndexDocument: { Suffix: "index.html" },
65
- ErrorDocument: { Key: "index.html" }
66
- // SPA fallback
67
- }
68
- })
69
- );
70
- const partition = region.startsWith("cn-") ? "aws-cn" : "aws";
71
- console.log(`Setting public read bucket policy on s3://${bucket}...`);
72
- await s3.send(
73
- new PutBucketPolicyCommand({
74
- Bucket: bucket,
75
- Policy: JSON.stringify({
76
- Version: "2012-10-17",
77
- Statement: [
78
- {
79
- Sid: "PublicReadGetObject",
80
- Effect: "Allow",
81
- Principal: "*",
82
- Action: "s3:GetObject",
83
- Resource: `arn:${partition}:s3:::${bucket}/*`
84
- }
85
- ]
86
- })
87
- })
88
- );
89
57
  const files = collectFiles(dashboardDir);
90
- console.log(`Uploading ${files.length} files to s3://${bucket}...`);
58
+ console.log(`Uploading ${files.length} files to s3://${bucket}/ ...`);
91
59
  for (const filePath of files) {
92
60
  const key = relative(dashboardDir, filePath);
93
61
  const ext = extname(filePath);
@@ -103,14 +71,14 @@ Expected: ${dashboardDir}`
103
71
  );
104
72
  console.log(` ${key}`);
105
73
  }
106
- const domain = region.startsWith("cn-") ? "amazonaws.com.cn" : "amazonaws.com";
107
- const websiteUrl = `http://${bucket}.s3-website.${region}.${domain}`;
108
74
  console.log(`
109
- Dashboard deployed successfully!`);
110
- console.log(`Website URL: ${websiteUrl}`);
111
- console.log(
112
- "\nNote: Ensure S3 Block Public Access is disabled on this bucket for the website to be accessible.\n"
113
- );
75
+ \u2705 Dashboard uploaded to s3://${bucket}/`);
76
+ console.log(`
77
+ S3 bucket remains private (Block Public Access enabled).`);
78
+ console.log(`Access control is managed via IAM permissions.`);
79
+ console.log(`
80
+ To view the dashboard locally:`);
81
+ console.log(` aws-security-mcp dashboard --port 3000`);
114
82
  }
115
83
  export {
116
84
  deployDashboard
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/deploy-dashboard.ts"],"sourcesContent":["import { readdirSync, readFileSync, existsSync, copyFileSync } from \"node:fs\";\nimport { join, extname, relative } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n S3Client,\n PutObjectCommand,\n PutBucketWebsiteCommand,\n PutBucketPolicyCommand,\n} from \"@aws-sdk/client-s3\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = join(__filename, \"..\");\n\nconst CONTENT_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".js\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\nfunction collectFiles(dir: string): string[] {\n const files: string[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...collectFiles(full));\n } else {\n files.push(full);\n }\n }\n return files;\n}\n\nexport async function deployDashboard(\n bucket: string,\n region: string,\n): Promise<void> {\n const dashboardDir = join(__dirname, \"../../dashboard/dist\");\n\n if (!existsSync(dashboardDir)) {\n console.error(\n `Dashboard not built. Run \"npm run build:dashboard\" first.\\n` +\n `Expected: ${dashboardDir}`,\n );\n process.exit(1);\n }\n\n // Copy real data.json if available\n const dataSource = join(\n process.env.HOME || process.env.USERPROFILE || \"~\",\n \".aws-security/dashboard/data.json\",\n );\n const dataDest = join(dashboardDir, \"data.json\");\n if (existsSync(dataSource)) {\n copyFileSync(dataSource, dataDest);\n console.log(`Loaded scan data from ${dataSource}`);\n } else {\n console.log(\n \"No scan data at ~/.aws-security/dashboard/data.json — deploying with bundled sample data\",\n );\n }\n\n const s3 = new S3Client({ region });\n\n // Configure static website hosting\n console.log(`Configuring s3://${bucket} for static website hosting...`);\n await s3.send(\n new PutBucketWebsiteCommand({\n Bucket: bucket,\n WebsiteConfiguration: {\n IndexDocument: { Suffix: \"index.html\" },\n ErrorDocument: { Key: \"index.html\" }, // SPA fallback\n },\n }),\n );\n\n // Set bucket policy for public read access\n const partition = region.startsWith(\"cn-\") ? \"aws-cn\" : \"aws\";\n console.log(`Setting public read bucket policy on s3://${bucket}...`);\n await s3.send(\n new PutBucketPolicyCommand({\n Bucket: bucket,\n Policy: JSON.stringify({\n Version: \"2012-10-17\",\n Statement: [\n {\n Sid: \"PublicReadGetObject\",\n Effect: \"Allow\",\n Principal: \"*\",\n Action: \"s3:GetObject\",\n Resource: `arn:${partition}:s3:::${bucket}/*`,\n },\n ],\n }),\n }),\n );\n\n // Upload all files\n const files = collectFiles(dashboardDir);\n console.log(`Uploading ${files.length} files to s3://${bucket}...`);\n\n for (const filePath of files) {\n const key = relative(dashboardDir, filePath);\n const ext = extname(filePath);\n const contentType = CONTENT_TYPES[ext] || \"application/octet-stream\";\n const body = readFileSync(filePath);\n\n await s3.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: body,\n ContentType: contentType,\n }),\n );\n console.log(` ${key}`);\n }\n\n const domain = region.startsWith(\"cn-\") ? \"amazonaws.com.cn\" : \"amazonaws.com\";\n const websiteUrl = `http://${bucket}.s3-website.${region}.${domain}`;\n console.log(`\\nDashboard deployed successfully!`);\n console.log(`Website URL: ${websiteUrl}`);\n console.log(\n \"\\nNote: Ensure S3 Block Public Access is disabled on this bucket for the website to be accessible.\\n\",\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/commands/deploy-dashboard.ts"],"sourcesContent":["import { readdirSync, readFileSync, existsSync, copyFileSync } from \"node:fs\";\nimport { join, extname, relative } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n S3Client,\n PutObjectCommand,\n} from \"@aws-sdk/client-s3\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = join(__filename, \"..\");\n\nconst CONTENT_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".js\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\nfunction collectFiles(dir: string): string[] {\n const files: string[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...collectFiles(full));\n } else {\n files.push(full);\n }\n }\n return files;\n}\n\nexport async function deployDashboard(\n bucket: string,\n region: string,\n): Promise<void> {\n const dashboardDir = join(__dirname, \"../../dashboard/dist\");\n\n if (!existsSync(dashboardDir)) {\n console.error(\n `Dashboard not built. Run \"npm run build:dashboard\" first.\\n` +\n `Expected: ${dashboardDir}`,\n );\n process.exit(1);\n }\n\n // Copy real data.json if available\n const dataSource = join(\n process.env.HOME || process.env.USERPROFILE || \"~\",\n \".aws-security/dashboard/data.json\",\n );\n const dataDest = join(dashboardDir, \"data.json\");\n if (existsSync(dataSource)) {\n copyFileSync(dataSource, dataDest);\n console.log(`Loaded scan data from ${dataSource}`);\n } else {\n console.log(\n \"No scan data at ~/.aws-security/dashboard/data.json — deploying with bundled sample data\",\n );\n }\n\n const s3 = new S3Client({ region });\n\n // Upload all files (private no public policy, no website hosting)\n const files = collectFiles(dashboardDir);\n console.log(`Uploading ${files.length} files to s3://${bucket}/ ...`);\n\n for (const filePath of files) {\n const key = relative(dashboardDir, filePath);\n const ext = extname(filePath);\n const contentType = CONTENT_TYPES[ext] || \"application/octet-stream\";\n const body = readFileSync(filePath);\n\n await s3.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: body,\n ContentType: contentType,\n }),\n );\n console.log(` ${key}`);\n }\n\n console.log(`\\n✅ Dashboard uploaded to s3://${bucket}/`);\n console.log(`\\nS3 bucket remains private (Block Public Access enabled).`);\n console.log(`Access control is managed via IAM permissions.`);\n console.log(`\\nTo view the dashboard locally:`);\n console.log(` aws-security-mcp dashboard --port 3000`);\n}\n"],"mappings":";AAAA,SAAS,aAAa,cAAc,YAAY,oBAAoB;AACpE,SAAS,MAAM,SAAS,gBAAgB;AACxC,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,KAAK,YAAY,IAAI;AAEvC,IAAM,gBAAwC;AAAA,EAC5C,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AACZ;AAEA,SAAS,aAAa,KAAuB;AAC3C,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAC7D,UAAM,OAAO,KAAK,KAAK,MAAM,IAAI;AACjC,QAAI,MAAM,YAAY,GAAG;AACvB,YAAM,KAAK,GAAG,aAAa,IAAI,CAAC;AAAA,IAClC,OAAO;AACL,YAAM,KAAK,IAAI;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,gBACpB,QACA,QACe;AACf,QAAM,eAAe,KAAK,WAAW,sBAAsB;AAE3D,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,YAAQ;AAAA,MACN;AAAA,YACe,YAAY;AAAA,IAC7B;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,aAAa;AAAA,IACjB,QAAQ,IAAI,QAAQ,QAAQ,IAAI,eAAe;AAAA,IAC/C;AAAA,EACF;AACA,QAAM,WAAW,KAAK,cAAc,WAAW;AAC/C,MAAI,WAAW,UAAU,GAAG;AAC1B,iBAAa,YAAY,QAAQ;AACjC,YAAQ,IAAI,yBAAyB,UAAU,EAAE;AAAA,EACnD,OAAO;AACL,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,IAAI,SAAS,EAAE,OAAO,CAAC;AAGlC,QAAM,QAAQ,aAAa,YAAY;AACvC,UAAQ,IAAI,aAAa,MAAM,MAAM,kBAAkB,MAAM,OAAO;AAEpE,aAAW,YAAY,OAAO;AAC5B,UAAM,MAAM,SAAS,cAAc,QAAQ;AAC3C,UAAM,MAAM,QAAQ,QAAQ;AAC5B,UAAM,cAAc,cAAc,GAAG,KAAK;AAC1C,UAAM,OAAO,aAAa,QAAQ;AAElC,UAAM,GAAG;AAAA,MACP,IAAI,iBAAiB;AAAA,QACnB,QAAQ;AAAA,QACR,KAAK;AAAA,QACL,MAAM;AAAA,QACN,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AACA,YAAQ,IAAI,KAAK,GAAG,EAAE;AAAA,EACxB;AAEA,UAAQ,IAAI;AAAA,oCAAkC,MAAM,GAAG;AACvD,UAAQ,IAAI;AAAA,yDAA4D;AACxE,UAAQ,IAAI,gDAAgD;AAC5D,UAAQ,IAAI;AAAA,+BAAkC;AAC9C,UAAQ,IAAI,0CAA0C;AACxD;","names":[]}
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.2";
7
+ var VERSION = "0.7.4";
8
8
 
9
9
  // src/utils/aws-client.ts
10
10
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -85,6 +85,25 @@ async function listOrgAccounts(region) {
85
85
  }
86
86
 
87
87
  // src/scanners/runner.ts
88
+ var DEFAULT_CONCURRENCY = 5;
89
+ async function runWithConcurrency(tasks, limit) {
90
+ const results = [];
91
+ const executing = /* @__PURE__ */ new Set();
92
+ for (let i = 0; i < tasks.length; i++) {
93
+ const idx = i;
94
+ const p = tasks[idx]().then((value) => {
95
+ results[idx] = { status: "fulfilled", value };
96
+ }).catch((reason) => {
97
+ results[idx] = { status: "rejected", reason };
98
+ }).finally(() => {
99
+ executing.delete(p);
100
+ });
101
+ executing.add(p);
102
+ if (executing.size >= limit) await Promise.race(executing);
103
+ }
104
+ await Promise.all(executing);
105
+ return results;
106
+ }
88
107
  var AGGREGATION_MODULES = /* @__PURE__ */ new Set([
89
108
  "security_hub_findings",
90
109
  "guardduty_findings",
@@ -132,8 +151,8 @@ function buildSummary(modules) {
132
151
  modulesError
133
152
  };
134
153
  }
135
- async function runScannersWithContext(scanners, ctx) {
136
- const settled = await Promise.allSettled(scanners.map((s) => s.scan(ctx)));
154
+ async function runScannersWithContext(scanners, ctx, concurrency = DEFAULT_CONCURRENCY) {
155
+ const settled = await runWithConcurrency(scanners.map((s) => () => s.scan(ctx)), concurrency);
137
156
  return settled.map((result, i) => {
138
157
  if (result.status === "fulfilled") {
139
158
  for (const f of result.value.findings) {
@@ -654,6 +673,25 @@ import {
654
673
  DescribeInstancesCommand,
655
674
  DescribeInstanceAttributeCommand
656
675
  } from "@aws-sdk/client-ec2";
676
+ var USERDATA_CONCURRENCY = 5;
677
+ async function runWithConcurrency2(tasks, limit) {
678
+ const results = [];
679
+ const executing = /* @__PURE__ */ new Set();
680
+ for (let i = 0; i < tasks.length; i++) {
681
+ const idx = i;
682
+ const p = tasks[idx]().then((value) => {
683
+ results[idx] = { status: "fulfilled", value };
684
+ }).catch((reason) => {
685
+ results[idx] = { status: "rejected", reason };
686
+ }).finally(() => {
687
+ executing.delete(p);
688
+ });
689
+ executing.add(p);
690
+ if (executing.size >= limit) await Promise.race(executing);
691
+ }
692
+ await Promise.all(executing);
693
+ return results;
694
+ }
657
695
  var SECRET_PATTERNS = [
658
696
  { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/, matchType: "value" },
659
697
  { name: "Private Key", pattern: /-----BEGIN.*PRIVATE KEY-----/, matchType: "value" },
@@ -753,25 +791,28 @@ var SecretExposureScanner = class {
753
791
  nextToken = resp.NextToken;
754
792
  } while (nextToken);
755
793
  resourcesScanned += instances.length;
756
- for (const inst of instances) {
794
+ const userDataTasks = instances.map((inst) => async () => {
795
+ const instId = inst.InstanceId ?? "unknown";
796
+ const attrResp = await ec2.send(
797
+ new DescribeInstanceAttributeCommand({
798
+ InstanceId: instId,
799
+ Attribute: "userData"
800
+ })
801
+ );
802
+ const raw = attrResp.UserData?.Value;
803
+ return { instId, userData: raw ? Buffer.from(raw, "base64").toString("utf-8") : void 0 };
804
+ });
805
+ const settled = await runWithConcurrency2(userDataTasks, USERDATA_CONCURRENCY);
806
+ for (let i = 0; i < instances.length; i++) {
807
+ const result = settled[i];
808
+ const inst = instances[i];
757
809
  const instId = inst.InstanceId ?? "unknown";
758
810
  const instArn = `arn:${partition}:ec2:${region}:${accountId}:instance/${instId}`;
759
- let userData;
760
- try {
761
- const attrResp = await ec2.send(
762
- new DescribeInstanceAttributeCommand({
763
- InstanceId: instId,
764
- Attribute: "userData"
765
- })
766
- );
767
- const raw = attrResp.UserData?.Value;
768
- if (raw) {
769
- userData = Buffer.from(raw, "base64").toString("utf-8");
770
- }
771
- } catch (e) {
772
- warnings.push(`Could not read userData for ${instId}: ${e instanceof Error ? e.message : String(e)}`);
811
+ if (result.status === "rejected") {
812
+ warnings.push(`Could not read userData for ${instId}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
773
813
  continue;
774
814
  }
815
+ const userData = result.value.userData;
775
816
  if (!userData) continue;
776
817
  for (const sp of SECRET_PATTERNS) {
777
818
  if (sp.matchType === "name") continue;
@@ -1962,6 +2003,7 @@ import {
1962
2003
  import {
1963
2004
  S3Client as S3Client3,
1964
2005
  ListBucketsCommand as ListBucketsCommand2,
2006
+ GetBucketLocationCommand as GetBucketLocationCommand2,
1965
2007
  GetBucketTaggingCommand
1966
2008
  } from "@aws-sdk/client-s3";
1967
2009
  var DEFAULT_REQUIRED_TAGS = ["Environment", "Project", "Owner"];
@@ -2078,8 +2120,19 @@ var TagComplianceScanner = class {
2078
2120
  for (const bucket of buckets) {
2079
2121
  const name = bucket.Name ?? "unknown";
2080
2122
  const arn = `arn:${partition}:s3:::${name}`;
2123
+ let bucketClient;
2124
+ try {
2125
+ const locResp = await s3Client.send(new GetBucketLocationCommand2({ Bucket: name }));
2126
+ const rawLoc = locResp.LocationConstraint || "us-east-1";
2127
+ const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
2128
+ bucketClient = bucketRegion === region ? s3Client : createClient(S3Client3, bucketRegion, ctx.credentials);
2129
+ } catch (e) {
2130
+ const msg = e instanceof Error ? e.message : String(e);
2131
+ warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
2132
+ continue;
2133
+ }
2081
2134
  try {
2082
- const taggingResp = await s3Client.send(
2135
+ const taggingResp = await bucketClient.send(
2083
2136
  new GetBucketTaggingCommand({ Bucket: name })
2084
2137
  );
2085
2138
  const tags = (taggingResp.TagSet ?? []).map((t) => ({
@@ -2381,6 +2434,7 @@ import {
2381
2434
  import {
2382
2435
  S3Client as S3Client4,
2383
2436
  ListBucketsCommand as ListBucketsCommand3,
2437
+ GetBucketLocationCommand as GetBucketLocationCommand3,
2384
2438
  GetBucketVersioningCommand,
2385
2439
  GetBucketReplicationCommand
2386
2440
  } from "@aws-sdk/client-s3";
@@ -2555,8 +2609,19 @@ var DisasterRecoveryScanner = class {
2555
2609
  resourcesScanned += bucketNames.length;
2556
2610
  for (const name of bucketNames) {
2557
2611
  const arn = `arn:${partition}:s3:::${name}`;
2612
+ let bucketClient;
2613
+ try {
2614
+ const locResp = await s3Client.send(new GetBucketLocationCommand3({ Bucket: name }));
2615
+ const rawLoc = locResp.LocationConstraint || "us-east-1";
2616
+ const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
2617
+ bucketClient = bucketRegion === region ? s3Client : createClient(S3Client4, bucketRegion, ctx.credentials);
2618
+ } catch (e) {
2619
+ const msg = e instanceof Error ? e.message : String(e);
2620
+ warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
2621
+ continue;
2622
+ }
2558
2623
  try {
2559
- const ver = await s3Client.send(
2624
+ const ver = await bucketClient.send(
2560
2625
  new GetBucketVersioningCommand({ Bucket: name })
2561
2626
  );
2562
2627
  if (ver.Status !== "Enabled") {
@@ -2582,7 +2647,7 @@ var DisasterRecoveryScanner = class {
2582
2647
  warnings.push(`Bucket ${name} versioning check failed: ${msg}`);
2583
2648
  }
2584
2649
  try {
2585
- await s3Client.send(
2650
+ await bucketClient.send(
2586
2651
  new GetBucketReplicationCommand({ Bucket: name })
2587
2652
  );
2588
2653
  } catch (e) {