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
|
@@ -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 +
|
|
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 +
|
|
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
|
|
110
|
-
console.log(`
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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.
|
|
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
|
|
154
|
+
async function runScannersWithContext(scanners, ctx, concurrency = DEFAULT_CONCURRENCY) {
|
|
155
|
+
const settled = await runWithConcurrency(scanners.map((s) => () => s.scan(ctx)), concurrency);
|
|
137
156
|
return settled.map((result, i) => {
|
|
138
157
|
if (result.status === "fulfilled") {
|
|
139
158
|
for (const f of result.value.findings) {
|
|
@@ -654,6 +673,25 @@ import {
|
|
|
654
673
|
DescribeInstancesCommand,
|
|
655
674
|
DescribeInstanceAttributeCommand
|
|
656
675
|
} from "@aws-sdk/client-ec2";
|
|
676
|
+
var USERDATA_CONCURRENCY = 5;
|
|
677
|
+
async function runWithConcurrency2(tasks, limit) {
|
|
678
|
+
const results = [];
|
|
679
|
+
const executing = /* @__PURE__ */ new Set();
|
|
680
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
681
|
+
const idx = i;
|
|
682
|
+
const p = tasks[idx]().then((value) => {
|
|
683
|
+
results[idx] = { status: "fulfilled", value };
|
|
684
|
+
}).catch((reason) => {
|
|
685
|
+
results[idx] = { status: "rejected", reason };
|
|
686
|
+
}).finally(() => {
|
|
687
|
+
executing.delete(p);
|
|
688
|
+
});
|
|
689
|
+
executing.add(p);
|
|
690
|
+
if (executing.size >= limit) await Promise.race(executing);
|
|
691
|
+
}
|
|
692
|
+
await Promise.all(executing);
|
|
693
|
+
return results;
|
|
694
|
+
}
|
|
657
695
|
var SECRET_PATTERNS = [
|
|
658
696
|
{ name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/, matchType: "value" },
|
|
659
697
|
{ name: "Private Key", pattern: /-----BEGIN.*PRIVATE KEY-----/, matchType: "value" },
|
|
@@ -753,25 +791,28 @@ var SecretExposureScanner = class {
|
|
|
753
791
|
nextToken = resp.NextToken;
|
|
754
792
|
} while (nextToken);
|
|
755
793
|
resourcesScanned += instances.length;
|
|
756
|
-
|
|
794
|
+
const userDataTasks = instances.map((inst) => async () => {
|
|
795
|
+
const instId = inst.InstanceId ?? "unknown";
|
|
796
|
+
const attrResp = await ec2.send(
|
|
797
|
+
new DescribeInstanceAttributeCommand({
|
|
798
|
+
InstanceId: instId,
|
|
799
|
+
Attribute: "userData"
|
|
800
|
+
})
|
|
801
|
+
);
|
|
802
|
+
const raw = attrResp.UserData?.Value;
|
|
803
|
+
return { instId, userData: raw ? Buffer.from(raw, "base64").toString("utf-8") : void 0 };
|
|
804
|
+
});
|
|
805
|
+
const settled = await runWithConcurrency2(userDataTasks, USERDATA_CONCURRENCY);
|
|
806
|
+
for (let i = 0; i < instances.length; i++) {
|
|
807
|
+
const result = settled[i];
|
|
808
|
+
const inst = instances[i];
|
|
757
809
|
const instId = inst.InstanceId ?? "unknown";
|
|
758
810
|
const instArn = `arn:${partition}:ec2:${region}:${accountId}:instance/${instId}`;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const attrResp = await ec2.send(
|
|
762
|
-
new DescribeInstanceAttributeCommand({
|
|
763
|
-
InstanceId: instId,
|
|
764
|
-
Attribute: "userData"
|
|
765
|
-
})
|
|
766
|
-
);
|
|
767
|
-
const raw = attrResp.UserData?.Value;
|
|
768
|
-
if (raw) {
|
|
769
|
-
userData = Buffer.from(raw, "base64").toString("utf-8");
|
|
770
|
-
}
|
|
771
|
-
} catch (e) {
|
|
772
|
-
warnings.push(`Could not read userData for ${instId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
811
|
+
if (result.status === "rejected") {
|
|
812
|
+
warnings.push(`Could not read userData for ${instId}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
|
|
773
813
|
continue;
|
|
774
814
|
}
|
|
815
|
+
const userData = result.value.userData;
|
|
775
816
|
if (!userData) continue;
|
|
776
817
|
for (const sp of SECRET_PATTERNS) {
|
|
777
818
|
if (sp.matchType === "name") continue;
|
|
@@ -1962,6 +2003,7 @@ import {
|
|
|
1962
2003
|
import {
|
|
1963
2004
|
S3Client as S3Client3,
|
|
1964
2005
|
ListBucketsCommand as ListBucketsCommand2,
|
|
2006
|
+
GetBucketLocationCommand as GetBucketLocationCommand2,
|
|
1965
2007
|
GetBucketTaggingCommand
|
|
1966
2008
|
} from "@aws-sdk/client-s3";
|
|
1967
2009
|
var DEFAULT_REQUIRED_TAGS = ["Environment", "Project", "Owner"];
|
|
@@ -2078,8 +2120,19 @@ var TagComplianceScanner = class {
|
|
|
2078
2120
|
for (const bucket of buckets) {
|
|
2079
2121
|
const name = bucket.Name ?? "unknown";
|
|
2080
2122
|
const arn = `arn:${partition}:s3:::${name}`;
|
|
2123
|
+
let bucketClient;
|
|
2124
|
+
try {
|
|
2125
|
+
const locResp = await s3Client.send(new GetBucketLocationCommand2({ Bucket: name }));
|
|
2126
|
+
const rawLoc = locResp.LocationConstraint || "us-east-1";
|
|
2127
|
+
const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
|
|
2128
|
+
bucketClient = bucketRegion === region ? s3Client : createClient(S3Client3, bucketRegion, ctx.credentials);
|
|
2129
|
+
} catch (e) {
|
|
2130
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2131
|
+
warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2081
2134
|
try {
|
|
2082
|
-
const taggingResp = await
|
|
2135
|
+
const taggingResp = await bucketClient.send(
|
|
2083
2136
|
new GetBucketTaggingCommand({ Bucket: name })
|
|
2084
2137
|
);
|
|
2085
2138
|
const tags = (taggingResp.TagSet ?? []).map((t) => ({
|
|
@@ -2381,6 +2434,7 @@ import {
|
|
|
2381
2434
|
import {
|
|
2382
2435
|
S3Client as S3Client4,
|
|
2383
2436
|
ListBucketsCommand as ListBucketsCommand3,
|
|
2437
|
+
GetBucketLocationCommand as GetBucketLocationCommand3,
|
|
2384
2438
|
GetBucketVersioningCommand,
|
|
2385
2439
|
GetBucketReplicationCommand
|
|
2386
2440
|
} from "@aws-sdk/client-s3";
|
|
@@ -2555,8 +2609,19 @@ var DisasterRecoveryScanner = class {
|
|
|
2555
2609
|
resourcesScanned += bucketNames.length;
|
|
2556
2610
|
for (const name of bucketNames) {
|
|
2557
2611
|
const arn = `arn:${partition}:s3:::${name}`;
|
|
2612
|
+
let bucketClient;
|
|
2613
|
+
try {
|
|
2614
|
+
const locResp = await s3Client.send(new GetBucketLocationCommand3({ Bucket: name }));
|
|
2615
|
+
const rawLoc = locResp.LocationConstraint || "us-east-1";
|
|
2616
|
+
const bucketRegion = rawLoc === "EU" ? "eu-west-1" : rawLoc;
|
|
2617
|
+
bucketClient = bucketRegion === region ? s3Client : createClient(S3Client4, bucketRegion, ctx.credentials);
|
|
2618
|
+
} catch (e) {
|
|
2619
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2620
|
+
warnings.push(`Bucket ${name}: could not determine region, skipping: ${msg}`);
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2558
2623
|
try {
|
|
2559
|
-
const ver = await
|
|
2624
|
+
const ver = await bucketClient.send(
|
|
2560
2625
|
new GetBucketVersioningCommand({ Bucket: name })
|
|
2561
2626
|
);
|
|
2562
2627
|
if (ver.Status !== "Enabled") {
|
|
@@ -2582,7 +2647,7 @@ var DisasterRecoveryScanner = class {
|
|
|
2582
2647
|
warnings.push(`Bucket ${name} versioning check failed: ${msg}`);
|
|
2583
2648
|
}
|
|
2584
2649
|
try {
|
|
2585
|
-
await
|
|
2650
|
+
await bucketClient.send(
|
|
2586
2651
|
new GetBucketReplicationCommand({ Bucket: name })
|
|
2587
2652
|
);
|
|
2588
2653
|
} catch (e) {
|