mcp-aws-manager 0.3.0 → 0.3.2
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/AGENT_GUIDANCE_LOOP_TEMPLATE_KO.md +68 -0
- package/IMPLEMENTATION_INTEGRATIONS.md +91 -0
- package/MCP_CLIENT_SETUP.md +30 -5
- package/MCP_DIFFERENTIATION.md +39 -0
- package/README.md +60 -7
- package/bin/mcp-aws-manager-mcp.js +264 -6
- package/bin/mcp-aws-manager.js +888 -132
- package/package.json +13 -3
package/bin/mcp-aws-manager.js
CHANGED
|
@@ -11,7 +11,8 @@ const DEFAULT_SNAPSHOT_CONCURRENCY = 3;
|
|
|
11
11
|
const MAX_SSM_FILTER_IDS = 50;
|
|
12
12
|
const DEFAULT_SERVER_NAME = "mcp-aws-manager";
|
|
13
13
|
const DEFAULT_MCP_COMMAND = "mcp-aws-manager-mcp";
|
|
14
|
-
const SUPPORTED_CLIENTS = new Set(["codex", "claude"]);
|
|
14
|
+
const SUPPORTED_CLIENTS = new Set(["codex", "claude", "cursor", "windsurf", "antigravity"]);
|
|
15
|
+
const INTERNAL_BACKEND_ID = "internal";
|
|
15
16
|
|
|
16
17
|
function eprint(msg) {
|
|
17
18
|
process.stderr.write(String(msg) + "\n");
|
|
@@ -65,6 +66,7 @@ function expandHome(input) {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
function usageText() {
|
|
69
|
+
const clientList = Array.from(SUPPORTED_CLIENTS).join(",");
|
|
68
70
|
return [
|
|
69
71
|
"Usage:",
|
|
70
72
|
" mcp-aws-manager",
|
|
@@ -74,18 +76,18 @@ function usageText() {
|
|
|
74
76
|
" mcp-aws-manager discover [discover-options]",
|
|
75
77
|
" mcp-aws-manager [discover-options]",
|
|
76
78
|
"",
|
|
77
|
-
"SSM-
|
|
79
|
+
"SSM-first AWS inventory/runtime collector (EC2/Lambda/ALB/ASG/RDS/ElastiCache/Route53) plus MCP client setup helper.",
|
|
78
80
|
"",
|
|
79
81
|
"Commands:",
|
|
80
82
|
" bootstrap Ensure mcp-aws-manager MCP server is registered (default command)",
|
|
81
|
-
" setup Register/re-register MCP server for
|
|
83
|
+
" setup Register/re-register MCP server for supported clients",
|
|
82
84
|
" doctor Check install and registration health",
|
|
83
|
-
" discover Run
|
|
85
|
+
" discover Run multi-service inventory workflow (EC2/Lambda/ALB/ASG/RDS/ElastiCache/Route53)",
|
|
84
86
|
"",
|
|
85
87
|
"Setup/Bootstrap/Doctor options:",
|
|
86
88
|
" --name <server-name> (default: mcp-aws-manager)",
|
|
87
89
|
" --mcp-command <command> (default: mcp-aws-manager-mcp)",
|
|
88
|
-
|
|
90
|
+
` --clients <${clientList}> (default: codex,claude)`,
|
|
89
91
|
" --force (setup/bootstrap only; always remove then add)",
|
|
90
92
|
" -h, --help",
|
|
91
93
|
"",
|
|
@@ -93,6 +95,20 @@ function usageText() {
|
|
|
93
95
|
" --profiles <a,b,c>",
|
|
94
96
|
" --regions <a,b,c>",
|
|
95
97
|
" --instance-ids <id1,id2>",
|
|
98
|
+
" --include-lambda",
|
|
99
|
+
" --no-include-lambda",
|
|
100
|
+
" --include-ec2",
|
|
101
|
+
" --no-ec2",
|
|
102
|
+
" --include-alb",
|
|
103
|
+
" --no-include-alb",
|
|
104
|
+
" --include-asg",
|
|
105
|
+
" --no-include-asg",
|
|
106
|
+
" --include-rds",
|
|
107
|
+
" --no-include-rds",
|
|
108
|
+
" --include-elasticache",
|
|
109
|
+
" --no-include-elasticache",
|
|
110
|
+
" --include-route53",
|
|
111
|
+
" --no-include-route53",
|
|
96
112
|
" --public-only",
|
|
97
113
|
" --managed-only",
|
|
98
114
|
" --auto-remediate-ssm",
|
|
@@ -112,6 +128,8 @@ function usageText() {
|
|
|
112
128
|
"",
|
|
113
129
|
"Env:",
|
|
114
130
|
" MCP_AWS_PROFILES, MCP_AWS_REGIONS, MCP_AWS_INSTANCE_IDS",
|
|
131
|
+
" MCP_AWS_INCLUDE_LAMBDA, MCP_AWS_INCLUDE_EC2, MCP_AWS_INCLUDE_ALB, MCP_AWS_INCLUDE_ASG",
|
|
132
|
+
" MCP_AWS_INCLUDE_RDS, MCP_AWS_INCLUDE_ELASTICACHE, MCP_AWS_INCLUDE_ROUTE53",
|
|
115
133
|
" MCP_AWS_PUBLIC_ONLY, MCP_AWS_MANAGED_ONLY, MCP_AWS_AUTO_REMEDIATE_SSM",
|
|
116
134
|
" MCP_AWS_SSM_INSTANCE_PROFILE_NAME, MCP_AWS_SSM_INSTANCE_PROFILE_ARN",
|
|
117
135
|
" MCP_AWS_ALLOW_REPLACE_PROFILE, MCP_AWS_REMEDIATION_WAIT",
|
|
@@ -125,14 +143,14 @@ function usageText() {
|
|
|
125
143
|
function parseClients(raw) {
|
|
126
144
|
const values = parseCsv(raw) || [];
|
|
127
145
|
if (!values.length) {
|
|
128
|
-
throw new Error(
|
|
146
|
+
throw new Error(`--clients must include at least one of: ${Array.from(SUPPORTED_CLIENTS).join(", ")}`);
|
|
129
147
|
}
|
|
130
148
|
const out = [];
|
|
131
149
|
const seen = new Set();
|
|
132
150
|
for (const value of values) {
|
|
133
151
|
const name = String(value).trim().toLowerCase();
|
|
134
152
|
if (!SUPPORTED_CLIENTS.has(name)) {
|
|
135
|
-
throw new Error(`Unsupported client '${value}'. Supported:
|
|
153
|
+
throw new Error(`Unsupported client '${value}'. Supported: ${Array.from(SUPPORTED_CLIENTS).join(", ")}`);
|
|
136
154
|
}
|
|
137
155
|
if (!seen.has(name)) {
|
|
138
156
|
seen.add(name);
|
|
@@ -251,6 +269,13 @@ function parseDiscoverArgs(argv) {
|
|
|
251
269
|
profiles: null,
|
|
252
270
|
regions: null,
|
|
253
271
|
instanceIds: null,
|
|
272
|
+
includeLambda: null,
|
|
273
|
+
includeEc2: null,
|
|
274
|
+
includeAlb: null,
|
|
275
|
+
includeAsg: null,
|
|
276
|
+
includeRds: null,
|
|
277
|
+
includeElastiCache: null,
|
|
278
|
+
includeRoute53: null,
|
|
254
279
|
publicOnly: false,
|
|
255
280
|
managedOnly: false,
|
|
256
281
|
autoRemediateSsm: false,
|
|
@@ -290,6 +315,20 @@ function parseDiscoverArgs(argv) {
|
|
|
290
315
|
for (let i = 0; i < args.length; i += 1) {
|
|
291
316
|
const arg = args[i];
|
|
292
317
|
if (arg === "-h" || arg === "--help") { options.help = true; continue; }
|
|
318
|
+
if (arg === "--include-lambda") { options.includeLambda = true; continue; }
|
|
319
|
+
if (arg === "--no-include-lambda") { options.includeLambda = false; continue; }
|
|
320
|
+
if (arg === "--include-ec2") { options.includeEc2 = true; continue; }
|
|
321
|
+
if (arg === "--no-ec2") { options.includeEc2 = false; continue; }
|
|
322
|
+
if (arg === "--include-alb") { options.includeAlb = true; continue; }
|
|
323
|
+
if (arg === "--no-include-alb") { options.includeAlb = false; continue; }
|
|
324
|
+
if (arg === "--include-asg") { options.includeAsg = true; continue; }
|
|
325
|
+
if (arg === "--no-include-asg") { options.includeAsg = false; continue; }
|
|
326
|
+
if (arg === "--include-rds") { options.includeRds = true; continue; }
|
|
327
|
+
if (arg === "--no-include-rds") { options.includeRds = false; continue; }
|
|
328
|
+
if (arg === "--include-elasticache") { options.includeElastiCache = true; continue; }
|
|
329
|
+
if (arg === "--no-include-elasticache") { options.includeElastiCache = false; continue; }
|
|
330
|
+
if (arg === "--include-route53") { options.includeRoute53 = true; continue; }
|
|
331
|
+
if (arg === "--no-include-route53") { options.includeRoute53 = false; continue; }
|
|
293
332
|
if (arg === "--public-only") { options.publicOnly = true; continue; }
|
|
294
333
|
if (arg === "--managed-only") { options.managedOnly = true; continue; }
|
|
295
334
|
if (arg === "--auto-remediate-ssm") { options.autoRemediateSsm = true; continue; }
|
|
@@ -330,6 +369,13 @@ function parseDiscoverArgs(argv) {
|
|
|
330
369
|
profiles: parseCsv(options.profiles) || parseCsv(envText("MCP_AWS_PROFILES")),
|
|
331
370
|
regions: parseCsv(options.regions) || parseCsv(envText("MCP_AWS_REGIONS")),
|
|
332
371
|
instanceIds: parseCsv(options.instanceIds) || parseCsv(envText("MCP_AWS_INSTANCE_IDS")),
|
|
372
|
+
includeLambda: options.includeLambda != null ? options.includeLambda : envBool("MCP_AWS_INCLUDE_LAMBDA"),
|
|
373
|
+
includeEc2: options.includeEc2 != null ? options.includeEc2 : (envText("MCP_AWS_INCLUDE_EC2") != null ? envBool("MCP_AWS_INCLUDE_EC2") : true),
|
|
374
|
+
includeAlb: options.includeAlb != null ? options.includeAlb : envBool("MCP_AWS_INCLUDE_ALB"),
|
|
375
|
+
includeAsg: options.includeAsg != null ? options.includeAsg : envBool("MCP_AWS_INCLUDE_ASG"),
|
|
376
|
+
includeRds: options.includeRds != null ? options.includeRds : envBool("MCP_AWS_INCLUDE_RDS"),
|
|
377
|
+
includeElastiCache: options.includeElastiCache != null ? options.includeElastiCache : envBool("MCP_AWS_INCLUDE_ELASTICACHE"),
|
|
378
|
+
includeRoute53: options.includeRoute53 != null ? options.includeRoute53 : envBool("MCP_AWS_INCLUDE_ROUTE53"),
|
|
333
379
|
publicOnly: Boolean(options.publicOnly || envBool("MCP_AWS_PUBLIC_ONLY")),
|
|
334
380
|
managedOnly: Boolean(options.managedOnly || envBool("MCP_AWS_MANAGED_ONLY")),
|
|
335
381
|
autoRemediateSsm: Boolean(options.autoRemediateSsm || envBool("MCP_AWS_AUTO_REMEDIATE_SSM")),
|
|
@@ -352,6 +398,22 @@ function validateConfig(config, warnings, requiredActions) {
|
|
|
352
398
|
if (config.outPath) {
|
|
353
399
|
fs.mkdirSync(path.dirname(config.outPath), { recursive: true });
|
|
354
400
|
}
|
|
401
|
+
const includeAny = config.includeEc2
|
|
402
|
+
|| config.includeLambda
|
|
403
|
+
|| config.includeAlb
|
|
404
|
+
|| config.includeAsg
|
|
405
|
+
|| config.includeRds
|
|
406
|
+
|| config.includeElastiCache
|
|
407
|
+
|| config.includeRoute53;
|
|
408
|
+
if (!includeAny) {
|
|
409
|
+
throw new Error("At least one resource type must be enabled. Use --include-ec2 and/or other --include-* options.");
|
|
410
|
+
}
|
|
411
|
+
if (config.managedOnly && !config.includeEc2) {
|
|
412
|
+
warnings.push("--managed-only applies only to EC2 SSM records.");
|
|
413
|
+
}
|
|
414
|
+
if (config.publicOnly && !config.includeEc2) {
|
|
415
|
+
warnings.push("--public-only applies only to EC2 public IPv4 records.");
|
|
416
|
+
}
|
|
355
417
|
if (config.autoRemediateSsm && !config.ssmInstanceProfileArn && !config.ssmInstanceProfileName) {
|
|
356
418
|
warnings.push("--auto-remediate-ssm is enabled but no instance profile target was provided; remediation is disabled.");
|
|
357
419
|
requiredActions.push({
|
|
@@ -406,7 +468,9 @@ function runCLICommand(cliBin, args, options = {}) {
|
|
|
406
468
|
env: process.env,
|
|
407
469
|
stdio: options.stdio || "pipe",
|
|
408
470
|
encoding: "utf8",
|
|
409
|
-
shell: false
|
|
471
|
+
shell: false,
|
|
472
|
+
timeout: options.timeoutMs != null ? Math.max(1000, Math.round(options.timeoutMs)) : 4000,
|
|
473
|
+
windowsHide: true
|
|
410
474
|
};
|
|
411
475
|
|
|
412
476
|
const direct = spawnSync(cliBin, args, execOptions);
|
|
@@ -438,28 +502,73 @@ function commandExists(bin, checkArgs) {
|
|
|
438
502
|
return run && run.status === 0;
|
|
439
503
|
}
|
|
440
504
|
|
|
441
|
-
function
|
|
505
|
+
function clientHelpAttempts(cliBin) {
|
|
506
|
+
if (cliBin === "cursor" || cliBin === "windsurf" || cliBin === "antigravity") {
|
|
507
|
+
return [
|
|
508
|
+
["mcp", "--help"],
|
|
509
|
+
["--help"]
|
|
510
|
+
];
|
|
511
|
+
}
|
|
512
|
+
return [["mcp", "--help"]];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function clientGetAttempts(cliBin, serverName) {
|
|
442
516
|
if (cliBin === "claude") {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
517
|
+
return [
|
|
518
|
+
["mcp", "get", serverName],
|
|
519
|
+
["mcp", "get", serverName, "-s", "user"],
|
|
520
|
+
["mcp", "get", serverName, "-s", "local"]
|
|
521
|
+
];
|
|
446
522
|
}
|
|
447
|
-
|
|
523
|
+
if (cliBin === "cursor" || cliBin === "windsurf" || cliBin === "antigravity") {
|
|
524
|
+
return [
|
|
525
|
+
["mcp", "get", serverName, "--json"],
|
|
526
|
+
["mcp", "get", serverName],
|
|
527
|
+
["mcp", "show", serverName]
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
return [
|
|
531
|
+
["mcp", "get", serverName, "--json"],
|
|
532
|
+
["mcp", "get", serverName]
|
|
533
|
+
];
|
|
448
534
|
}
|
|
449
535
|
|
|
450
|
-
function
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
536
|
+
function clientRemoveAttempts(cliBin, serverName) {
|
|
537
|
+
if (cliBin === "claude") {
|
|
538
|
+
return [
|
|
539
|
+
["mcp", "remove", serverName, "-s", "user"],
|
|
540
|
+
["mcp", "remove", serverName, "-s", "local"],
|
|
541
|
+
["mcp", "remove", serverName]
|
|
542
|
+
];
|
|
543
|
+
}
|
|
544
|
+
if (cliBin === "cursor" || cliBin === "windsurf" || cliBin === "antigravity") {
|
|
545
|
+
return [
|
|
546
|
+
["mcp", "remove", serverName],
|
|
547
|
+
["mcp", "rm", serverName],
|
|
548
|
+
["mcp", "remove", serverName, "-s", "user"],
|
|
549
|
+
["mcp", "remove", serverName, "-s", "local"]
|
|
550
|
+
];
|
|
551
|
+
}
|
|
552
|
+
return [["mcp", "remove", serverName]];
|
|
456
553
|
}
|
|
457
554
|
|
|
458
|
-
function
|
|
555
|
+
function clientAddAttempts(cliBin, serverName, mcpCommand) {
|
|
459
556
|
if (cliBin === "claude") {
|
|
460
557
|
return [
|
|
461
558
|
["mcp", "add", "--scope", "user", serverName, "--", mcpCommand],
|
|
462
|
-
["mcp", "add", "--scope", "user", serverName, mcpCommand]
|
|
559
|
+
["mcp", "add", "--scope", "user", serverName, mcpCommand],
|
|
560
|
+
["mcp", "add", serverName, "--", mcpCommand],
|
|
561
|
+
["mcp", "add", serverName, mcpCommand]
|
|
562
|
+
];
|
|
563
|
+
}
|
|
564
|
+
if (cliBin === "cursor" || cliBin === "windsurf" || cliBin === "antigravity") {
|
|
565
|
+
return [
|
|
566
|
+
["mcp", "add", serverName, "--", mcpCommand],
|
|
567
|
+
["mcp", "add", serverName, mcpCommand],
|
|
568
|
+
["mcp", "add", "--scope", "user", serverName, "--", mcpCommand],
|
|
569
|
+
["mcp", "add", "--scope", "user", serverName, mcpCommand],
|
|
570
|
+
["mcp", "add", "--scope", "local", serverName, "--", mcpCommand],
|
|
571
|
+
["mcp", "add", "--scope", "local", serverName, mcpCommand]
|
|
463
572
|
];
|
|
464
573
|
}
|
|
465
574
|
return [
|
|
@@ -468,6 +577,26 @@ function registrationAttempts(cliBin, serverName, mcpCommand) {
|
|
|
468
577
|
];
|
|
469
578
|
}
|
|
470
579
|
|
|
580
|
+
function removeRegistration(cliBin, serverName) {
|
|
581
|
+
const attempts = clientRemoveAttempts(cliBin, serverName);
|
|
582
|
+
for (const args of attempts) {
|
|
583
|
+
runCLICommand(cliBin, args, { stdio: "ignore" });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function isRegistered(cliBin, serverName) {
|
|
588
|
+
const attempts = clientGetAttempts(cliBin, serverName);
|
|
589
|
+
for (const args of attempts) {
|
|
590
|
+
const run = runCLICommand(cliBin, args, { stdio: "ignore" });
|
|
591
|
+
if (run && run.status === 0) return true;
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function registrationAttempts(cliBin, serverName, mcpCommand) {
|
|
597
|
+
return clientAddAttempts(cliBin, serverName, mcpCommand);
|
|
598
|
+
}
|
|
599
|
+
|
|
471
600
|
function tryRegister(cliBin, serverName, mcpCommand) {
|
|
472
601
|
const attempts = registrationAttempts(cliBin, serverName, mcpCommand);
|
|
473
602
|
let lastRun = null;
|
|
@@ -485,7 +614,9 @@ function tryRegister(cliBin, serverName, mcpCommand) {
|
|
|
485
614
|
|
|
486
615
|
function runSetupInternal(config, options = {}) {
|
|
487
616
|
const ensureOnly = options.ensureOnly === true;
|
|
488
|
-
const clients = config.clients.filter((cli) =>
|
|
617
|
+
const clients = config.clients.filter((cli) =>
|
|
618
|
+
clientHelpAttempts(cli).some((args) => commandExists(cli, args))
|
|
619
|
+
);
|
|
489
620
|
const results = [];
|
|
490
621
|
|
|
491
622
|
process.stdout.write(ensureOnly ? "Bootstrap start.\n" : "Setup start.\n");
|
|
@@ -494,7 +625,8 @@ function runSetupInternal(config, options = {}) {
|
|
|
494
625
|
process.stdout.write(`Clients: ${config.clients.join(",")}\n`);
|
|
495
626
|
|
|
496
627
|
if (!clients.length) {
|
|
497
|
-
process.stdout.write(
|
|
628
|
+
process.stdout.write(`No supported client CLI found. Supported: ${Array.from(SUPPORTED_CLIENTS).join(", ")}\n`);
|
|
629
|
+
process.stdout.write("Registration skipped.\n");
|
|
498
630
|
return 2;
|
|
499
631
|
}
|
|
500
632
|
|
|
@@ -547,7 +679,7 @@ function runDoctor(config) {
|
|
|
547
679
|
}
|
|
548
680
|
|
|
549
681
|
for (const cliBin of config.clients) {
|
|
550
|
-
const exists = commandExists(cliBin,
|
|
682
|
+
const exists = clientHelpAttempts(cliBin).some((args) => commandExists(cliBin, args));
|
|
551
683
|
if (!exists) {
|
|
552
684
|
hasIssue = true;
|
|
553
685
|
process.stdout.write(`${cliBin}: not installed or not available in PATH\n`);
|
|
@@ -566,7 +698,7 @@ function runDoctor(config) {
|
|
|
566
698
|
}
|
|
567
699
|
|
|
568
700
|
if (!foundClient) {
|
|
569
|
-
process.stdout.write("No requested clients detected. Install
|
|
701
|
+
process.stdout.write("No requested clients detected. Install at least one supported client first.\n");
|
|
570
702
|
}
|
|
571
703
|
|
|
572
704
|
process.stdout.write(hasIssue ? "Doctor result: issues found.\n" : "Doctor result: healthy.\n");
|
|
@@ -578,10 +710,16 @@ function loadAwsModules() {
|
|
|
578
710
|
if (awsModulesCache) return awsModulesCache;
|
|
579
711
|
try {
|
|
580
712
|
const ec2 = require("@aws-sdk/client-ec2");
|
|
713
|
+
const lambda = require("@aws-sdk/client-lambda");
|
|
714
|
+
const elbv2 = require("@aws-sdk/client-elastic-load-balancing-v2");
|
|
715
|
+
const autoScaling = require("@aws-sdk/client-auto-scaling");
|
|
716
|
+
const rds = require("@aws-sdk/client-rds");
|
|
717
|
+
const elasticache = require("@aws-sdk/client-elasticache");
|
|
718
|
+
const route53 = require("@aws-sdk/client-route-53");
|
|
581
719
|
const sts = require("@aws-sdk/client-sts");
|
|
582
720
|
const ssm = require("@aws-sdk/client-ssm");
|
|
583
721
|
const credentialProviders = require("@aws-sdk/credential-providers");
|
|
584
|
-
awsModulesCache = { ec2, sts, ssm, credentialProviders };
|
|
722
|
+
awsModulesCache = { ec2, lambda, elbv2, autoScaling, rds, elasticache, route53, sts, ssm, credentialProviders };
|
|
585
723
|
return awsModulesCache;
|
|
586
724
|
} catch {
|
|
587
725
|
throw new Error("Missing AWS SDK dependencies. Run: npm install");
|
|
@@ -733,6 +871,16 @@ async function discoverRegionsForProfile(profileName, profileLabel, warnings) {
|
|
|
733
871
|
}
|
|
734
872
|
|
|
735
873
|
async function buildDiscoveryPlan(config, warnings, requiredActions) {
|
|
874
|
+
const needsRegionalPlan = Boolean(
|
|
875
|
+
config.includeEc2
|
|
876
|
+
|| config.includeLambda
|
|
877
|
+
|| config.includeAlb
|
|
878
|
+
|| config.includeAsg
|
|
879
|
+
|| config.includeRds
|
|
880
|
+
|| config.includeElastiCache
|
|
881
|
+
|| config.runtimeSnapshot
|
|
882
|
+
|| config.autoRemediateSsm
|
|
883
|
+
);
|
|
736
884
|
const plans = [];
|
|
737
885
|
for (const profileName of requestedProfiles(config)) {
|
|
738
886
|
const profileLabel = profileName || "default";
|
|
@@ -741,7 +889,7 @@ async function buildDiscoveryPlan(config, warnings, requiredActions) {
|
|
|
741
889
|
|
|
742
890
|
const regions = config.regions && config.regions.length
|
|
743
891
|
? config.regions.slice()
|
|
744
|
-
: await discoverRegionsForProfile(profileName, profileLabel, warnings);
|
|
892
|
+
: (needsRegionalPlan ? await discoverRegionsForProfile(profileName, profileLabel, warnings) : [baseRegion()]);
|
|
745
893
|
|
|
746
894
|
if (!regions.length) {
|
|
747
895
|
warnings.push(`[${profileLabel}] No regions resolved, profile skipped.`);
|
|
@@ -751,7 +899,44 @@ async function buildDiscoveryPlan(config, warnings, requiredActions) {
|
|
|
751
899
|
plans.push({ profileName, profileLabel, accountId: auth.accountId, regions });
|
|
752
900
|
}
|
|
753
901
|
return plans;
|
|
754
|
-
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function buildOperationPlan(config) {
|
|
905
|
+
const inventoryOps = [];
|
|
906
|
+
if (config.includeEc2) {
|
|
907
|
+
inventoryOps.push("inventory.ec2");
|
|
908
|
+
inventoryOps.push("inventory.ssmStatus");
|
|
909
|
+
}
|
|
910
|
+
if (config.includeLambda) {
|
|
911
|
+
inventoryOps.push("inventory.lambda");
|
|
912
|
+
}
|
|
913
|
+
if (config.includeAlb) {
|
|
914
|
+
inventoryOps.push("inventory.alb");
|
|
915
|
+
inventoryOps.push("inventory.targetGroup");
|
|
916
|
+
}
|
|
917
|
+
if (config.includeAsg) {
|
|
918
|
+
inventoryOps.push("inventory.asg");
|
|
919
|
+
}
|
|
920
|
+
if (config.includeRds) {
|
|
921
|
+
inventoryOps.push("inventory.rds");
|
|
922
|
+
}
|
|
923
|
+
if (config.includeElastiCache) {
|
|
924
|
+
inventoryOps.push("inventory.elasticache");
|
|
925
|
+
}
|
|
926
|
+
if (config.includeRoute53) {
|
|
927
|
+
inventoryOps.push("inventory.route53");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const runtimeOps = [];
|
|
931
|
+
if (config.autoRemediateSsm) {
|
|
932
|
+
runtimeOps.push("remediation.attachSsmRole");
|
|
933
|
+
}
|
|
934
|
+
if (config.runtimeSnapshot) {
|
|
935
|
+
runtimeOps.push("runtime.snapshot");
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return { inventoryOps, runtimeOps };
|
|
939
|
+
}
|
|
755
940
|
|
|
756
941
|
function getTagValue(tags, key) {
|
|
757
942
|
if (!Array.isArray(tags)) return null;
|
|
@@ -905,13 +1090,236 @@ async function sleep(ms) {
|
|
|
905
1090
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
906
1091
|
}
|
|
907
1092
|
|
|
1093
|
+
function boolFromArray(values) {
|
|
1094
|
+
return Array.isArray(values) && values.length > 0;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function lambdaVpcConfigured(fnConfig) {
|
|
1098
|
+
if (!fnConfig || typeof fnConfig !== "object") return false;
|
|
1099
|
+
return boolFromArray(fnConfig.SubnetIds) || boolFromArray(fnConfig.SecurityGroupIds);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function baseInventoryRecord(plan, region, resourceType, service, resourceId, name, state, platform) {
|
|
1103
|
+
return {
|
|
1104
|
+
resourceType,
|
|
1105
|
+
service,
|
|
1106
|
+
resourceId: resourceId || null,
|
|
1107
|
+
profile: plan.profileLabel,
|
|
1108
|
+
accountId: plan.accountId,
|
|
1109
|
+
region: region || null,
|
|
1110
|
+
instanceId: null,
|
|
1111
|
+
name: name || null,
|
|
1112
|
+
state: state || null,
|
|
1113
|
+
privateIp: null,
|
|
1114
|
+
publicIp: null,
|
|
1115
|
+
publicDns: null,
|
|
1116
|
+
platform: platform || null,
|
|
1117
|
+
iamInstanceProfileArn: null,
|
|
1118
|
+
ssmManaged: null,
|
|
1119
|
+
ssmOnline: null,
|
|
1120
|
+
ssmPingStatus: null,
|
|
1121
|
+
ssmLastPingDateTime: null,
|
|
1122
|
+
ssmAgentVersion: null,
|
|
1123
|
+
ssmPlatformName: null,
|
|
1124
|
+
ssmPlatformVersion: null,
|
|
1125
|
+
remediationStatus: null,
|
|
1126
|
+
runtimeSnapshot: null,
|
|
1127
|
+
lambdaFunctionName: null,
|
|
1128
|
+
lambdaFunctionArn: null,
|
|
1129
|
+
lambdaRuntime: null,
|
|
1130
|
+
lambdaHandler: null,
|
|
1131
|
+
lambdaRole: null,
|
|
1132
|
+
lambdaLastModified: null,
|
|
1133
|
+
lambdaState: null,
|
|
1134
|
+
lambdaPackageType: null,
|
|
1135
|
+
lambdaTimeoutSec: null,
|
|
1136
|
+
lambdaMemoryMb: null,
|
|
1137
|
+
lambdaVpcConfigured: null,
|
|
1138
|
+
albArn: null,
|
|
1139
|
+
albType: null,
|
|
1140
|
+
albScheme: null,
|
|
1141
|
+
albDnsName: null,
|
|
1142
|
+
albVpcId: null,
|
|
1143
|
+
albListenerPorts: null,
|
|
1144
|
+
targetGroupArn: null,
|
|
1145
|
+
targetGroupType: null,
|
|
1146
|
+
targetGroupPort: null,
|
|
1147
|
+
targetGroupProtocol: null,
|
|
1148
|
+
targetGroupVpcId: null,
|
|
1149
|
+
targetGroupLoadBalancers: null,
|
|
1150
|
+
asgName: null,
|
|
1151
|
+
asgDesiredCapacity: null,
|
|
1152
|
+
asgMinSize: null,
|
|
1153
|
+
asgMaxSize: null,
|
|
1154
|
+
asgInstanceCount: null,
|
|
1155
|
+
asgLaunchTemplate: null,
|
|
1156
|
+
rdsIdentifier: null,
|
|
1157
|
+
rdsEngine: null,
|
|
1158
|
+
rdsEngineVersion: null,
|
|
1159
|
+
rdsStatus: null,
|
|
1160
|
+
rdsMultiAz: null,
|
|
1161
|
+
rdsEndpoint: null,
|
|
1162
|
+
rdsStorageGb: null,
|
|
1163
|
+
elasticacheClusterId: null,
|
|
1164
|
+
elasticacheEngine: null,
|
|
1165
|
+
elasticacheEngineVersion: null,
|
|
1166
|
+
elasticacheNodeType: null,
|
|
1167
|
+
elasticacheStatus: null,
|
|
1168
|
+
elasticacheNumNodes: null,
|
|
1169
|
+
route53ZoneId: null,
|
|
1170
|
+
route53ZoneName: null,
|
|
1171
|
+
route53PrivateZone: null,
|
|
1172
|
+
route53RecordSetCount: null,
|
|
1173
|
+
route53CallerReference: null
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function addEc2Record(regionRecords, plan, region, instance) {
|
|
1178
|
+
const record = baseInventoryRecord(
|
|
1179
|
+
plan,
|
|
1180
|
+
region,
|
|
1181
|
+
"ec2",
|
|
1182
|
+
"ec2",
|
|
1183
|
+
instance.InstanceId || null,
|
|
1184
|
+
getTagValue(instance.Tags, "Name"),
|
|
1185
|
+
instance.State && instance.State.Name ? instance.State.Name : null,
|
|
1186
|
+
instancePlatform(instance)
|
|
1187
|
+
);
|
|
1188
|
+
record.instanceId = instance.InstanceId || null;
|
|
1189
|
+
record.privateIp = instance.PrivateIpAddress || null;
|
|
1190
|
+
record.publicIp = instance.PublicIpAddress || null;
|
|
1191
|
+
record.publicDns = instance.PublicDnsName || null;
|
|
1192
|
+
record.iamInstanceProfileArn = instance.IamInstanceProfile && instance.IamInstanceProfile.Arn ? instance.IamInstanceProfile.Arn : null;
|
|
1193
|
+
record.ssmManaged = false;
|
|
1194
|
+
record.ssmOnline = false;
|
|
1195
|
+
regionRecords.push(record);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function addLambdaRecord(regionRecords, plan, region, fn) {
|
|
1199
|
+
const vpc = fn && fn.VpcConfig ? fn.VpcConfig : null;
|
|
1200
|
+
const record = baseInventoryRecord(
|
|
1201
|
+
plan,
|
|
1202
|
+
region,
|
|
1203
|
+
"lambda",
|
|
1204
|
+
"lambda",
|
|
1205
|
+
fn && fn.FunctionArn ? fn.FunctionArn : (fn && fn.FunctionName ? fn.FunctionName : null),
|
|
1206
|
+
fn && fn.FunctionName ? fn.FunctionName : null,
|
|
1207
|
+
fn && fn.State ? fn.State : null,
|
|
1208
|
+
"Lambda"
|
|
1209
|
+
);
|
|
1210
|
+
record.lambdaFunctionName = fn && fn.FunctionName ? fn.FunctionName : null;
|
|
1211
|
+
record.lambdaFunctionArn = fn && fn.FunctionArn ? fn.FunctionArn : null;
|
|
1212
|
+
record.lambdaRuntime = fn && fn.Runtime ? fn.Runtime : null;
|
|
1213
|
+
record.lambdaHandler = fn && fn.Handler ? fn.Handler : null;
|
|
1214
|
+
record.lambdaRole = fn && fn.Role ? fn.Role : null;
|
|
1215
|
+
record.lambdaLastModified = fn && fn.LastModified ? fn.LastModified : null;
|
|
1216
|
+
record.lambdaState = fn && fn.State ? fn.State : null;
|
|
1217
|
+
record.lambdaPackageType = fn && fn.PackageType ? fn.PackageType : null;
|
|
1218
|
+
record.lambdaTimeoutSec = fn && typeof fn.Timeout === "number" ? fn.Timeout : null;
|
|
1219
|
+
record.lambdaMemoryMb = fn && typeof fn.MemorySize === "number" ? fn.MemorySize : null;
|
|
1220
|
+
record.lambdaVpcConfigured = lambdaVpcConfigured(vpc);
|
|
1221
|
+
regionRecords.push(record);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function addAlbRecord(regionRecords, plan, region, lb) {
|
|
1225
|
+
const name = lb && lb.LoadBalancerName ? lb.LoadBalancerName : null;
|
|
1226
|
+
const arn = lb && lb.LoadBalancerArn ? lb.LoadBalancerArn : null;
|
|
1227
|
+
const state = lb && lb.State && lb.State.Code ? lb.State.Code : null;
|
|
1228
|
+
const record = baseInventoryRecord(plan, region, "alb", "elbv2", arn || name, name, state, "LoadBalancer");
|
|
1229
|
+
record.albArn = arn;
|
|
1230
|
+
record.albType = lb && lb.Type ? lb.Type : null;
|
|
1231
|
+
record.albScheme = lb && lb.Scheme ? lb.Scheme : null;
|
|
1232
|
+
record.albDnsName = lb && lb.DNSName ? lb.DNSName : null;
|
|
1233
|
+
record.albVpcId = lb && lb.VpcId ? lb.VpcId : null;
|
|
1234
|
+
record.albListenerPorts = null;
|
|
1235
|
+
regionRecords.push(record);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function addTargetGroupRecord(regionRecords, plan, region, tg) {
|
|
1239
|
+
const arn = tg && tg.TargetGroupArn ? tg.TargetGroupArn : null;
|
|
1240
|
+
const name = tg && tg.TargetGroupName ? tg.TargetGroupName : null;
|
|
1241
|
+
const record = baseInventoryRecord(plan, region, "target_group", "elbv2", arn || name, name, null, "TargetGroup");
|
|
1242
|
+
record.targetGroupArn = arn;
|
|
1243
|
+
record.targetGroupType = tg && tg.TargetType ? tg.TargetType : null;
|
|
1244
|
+
record.targetGroupPort = tg && typeof tg.Port === "number" ? tg.Port : null;
|
|
1245
|
+
record.targetGroupProtocol = tg && tg.Protocol ? tg.Protocol : null;
|
|
1246
|
+
record.targetGroupVpcId = tg && tg.VpcId ? tg.VpcId : null;
|
|
1247
|
+
const lbs = Array.isArray(tg && tg.LoadBalancerArns ? tg.LoadBalancerArns : null) ? tg.LoadBalancerArns : [];
|
|
1248
|
+
record.targetGroupLoadBalancers = lbs.length ? lbs.join(",") : null;
|
|
1249
|
+
regionRecords.push(record);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function addAsgRecord(regionRecords, plan, region, asg) {
|
|
1253
|
+
const name = asg && asg.AutoScalingGroupName ? asg.AutoScalingGroupName : null;
|
|
1254
|
+
const state = asg && asg.Status ? asg.Status : "InService";
|
|
1255
|
+
const record = baseInventoryRecord(plan, region, "asg", "autoscaling", name, name, state, "AutoScalingGroup");
|
|
1256
|
+
record.asgName = name;
|
|
1257
|
+
record.asgDesiredCapacity = asg && typeof asg.DesiredCapacity === "number" ? asg.DesiredCapacity : null;
|
|
1258
|
+
record.asgMinSize = asg && typeof asg.MinSize === "number" ? asg.MinSize : null;
|
|
1259
|
+
record.asgMaxSize = asg && typeof asg.MaxSize === "number" ? asg.MaxSize : null;
|
|
1260
|
+
record.asgInstanceCount = Array.isArray(asg && asg.Instances ? asg.Instances : null) ? asg.Instances.length : 0;
|
|
1261
|
+
record.asgLaunchTemplate = asg && asg.LaunchTemplate && asg.LaunchTemplate.LaunchTemplateName
|
|
1262
|
+
? asg.LaunchTemplate.LaunchTemplateName
|
|
1263
|
+
: null;
|
|
1264
|
+
regionRecords.push(record);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function addRdsRecord(regionRecords, plan, region, db) {
|
|
1268
|
+
const identifier = db && db.DBInstanceIdentifier ? db.DBInstanceIdentifier : null;
|
|
1269
|
+
const arn = db && db.DBInstanceArn ? db.DBInstanceArn : identifier;
|
|
1270
|
+
const status = db && db.DBInstanceStatus ? db.DBInstanceStatus : null;
|
|
1271
|
+
const record = baseInventoryRecord(plan, region, "rds", "rds", arn, identifier, status, "RDS");
|
|
1272
|
+
record.rdsIdentifier = identifier;
|
|
1273
|
+
record.rdsEngine = db && db.Engine ? db.Engine : null;
|
|
1274
|
+
record.rdsEngineVersion = db && db.EngineVersion ? db.EngineVersion : null;
|
|
1275
|
+
record.rdsStatus = status;
|
|
1276
|
+
record.rdsMultiAz = db && typeof db.MultiAZ === "boolean" ? db.MultiAZ : null;
|
|
1277
|
+
record.rdsEndpoint = db && db.Endpoint && db.Endpoint.Address ? db.Endpoint.Address : null;
|
|
1278
|
+
record.rdsStorageGb = db && typeof db.AllocatedStorage === "number" ? db.AllocatedStorage : null;
|
|
1279
|
+
regionRecords.push(record);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function addElastiCacheRecord(regionRecords, plan, region, cluster) {
|
|
1283
|
+
const id = cluster && cluster.CacheClusterId ? cluster.CacheClusterId : null;
|
|
1284
|
+
const arn = cluster && cluster.ARN ? cluster.ARN : id;
|
|
1285
|
+
const status = cluster && cluster.CacheClusterStatus ? cluster.CacheClusterStatus : null;
|
|
1286
|
+
const record = baseInventoryRecord(plan, region, "elasticache", "elasticache", arn, id, status, "ElastiCache");
|
|
1287
|
+
record.elasticacheClusterId = id;
|
|
1288
|
+
record.elasticacheEngine = cluster && cluster.Engine ? cluster.Engine : null;
|
|
1289
|
+
record.elasticacheEngineVersion = cluster && cluster.EngineVersion ? cluster.EngineVersion : null;
|
|
1290
|
+
record.elasticacheNodeType = cluster && cluster.CacheNodeType ? cluster.CacheNodeType : null;
|
|
1291
|
+
record.elasticacheStatus = status;
|
|
1292
|
+
record.elasticacheNumNodes = cluster && typeof cluster.NumCacheNodes === "number" ? cluster.NumCacheNodes : null;
|
|
1293
|
+
regionRecords.push(record);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function addRoute53ZoneRecord(records, plan, zone, recordSetCount) {
|
|
1297
|
+
const zoneId = zone && zone.Id ? String(zone.Id).replace("/hostedzone/", "") : null;
|
|
1298
|
+
const zoneName = zone && zone.Name ? zone.Name : null;
|
|
1299
|
+
const privateZone = Boolean(zone && zone.Config && zone.Config.PrivateZone);
|
|
1300
|
+
const record = baseInventoryRecord(plan, "global", "route53_zone", "route53", zoneId || zoneName, zoneName, "active", "Route53");
|
|
1301
|
+
record.route53ZoneId = zoneId;
|
|
1302
|
+
record.route53ZoneName = zoneName;
|
|
1303
|
+
record.route53PrivateZone = privateZone;
|
|
1304
|
+
record.route53RecordSetCount = typeof recordSetCount === "number" ? recordSetCount : null;
|
|
1305
|
+
record.route53CallerReference = zone && zone.CallerReference ? zone.CallerReference : null;
|
|
1306
|
+
records.push(record);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
908
1309
|
async function collectInventory(config, plans, warnings, requiredActions) {
|
|
909
|
-
const { ec2, ssm } = loadAwsModules();
|
|
1310
|
+
const { ec2, ssm, lambda, elbv2, autoScaling, rds, elasticache, route53 } = loadAwsModules();
|
|
910
1311
|
const stats = {
|
|
911
1312
|
profiles: 0,
|
|
912
1313
|
regions: 0,
|
|
913
1314
|
regionErrors: 0,
|
|
914
1315
|
instancesScanned: 0,
|
|
1316
|
+
lambdaFunctionsScanned: 0,
|
|
1317
|
+
loadBalancersScanned: 0,
|
|
1318
|
+
targetGroupsScanned: 0,
|
|
1319
|
+
autoScalingGroupsScanned: 0,
|
|
1320
|
+
rdsInstancesScanned: 0,
|
|
1321
|
+
elasticacheClustersScanned: 0,
|
|
1322
|
+
route53ZonesScanned: 0,
|
|
915
1323
|
remediationAttempts: 0,
|
|
916
1324
|
remediationChanged: 0
|
|
917
1325
|
};
|
|
@@ -924,106 +1332,278 @@ async function collectInventory(config, plans, warnings, requiredActions) {
|
|
|
924
1332
|
for (const region of plan.regions) {
|
|
925
1333
|
stats.regions += 1;
|
|
926
1334
|
const scopeLabel = `${plan.profileLabel}/${region}`;
|
|
927
|
-
const ec2Client = new ec2.EC2Client(awsClientConfig(plan.profileName, region));
|
|
928
|
-
const ssmClient = new ssm.SSMClient(awsClientConfig(plan.profileName, region));
|
|
1335
|
+
const ec2Client = config.includeEc2 ? new ec2.EC2Client(awsClientConfig(plan.profileName, region)) : null;
|
|
1336
|
+
const ssmClient = config.includeEc2 ? new ssm.SSMClient(awsClientConfig(plan.profileName, region)) : null;
|
|
1337
|
+
const lambdaClient = config.includeLambda ? new lambda.LambdaClient(awsClientConfig(plan.profileName, region)) : null;
|
|
1338
|
+
const elbv2Client = config.includeAlb ? new elbv2.ElasticLoadBalancingV2Client(awsClientConfig(plan.profileName, region)) : null;
|
|
1339
|
+
const asgClient = config.includeAsg ? new autoScaling.AutoScalingClient(awsClientConfig(plan.profileName, region)) : null;
|
|
1340
|
+
const rdsClient = config.includeRds ? new rds.RDSClient(awsClientConfig(plan.profileName, region)) : null;
|
|
1341
|
+
const elasticacheClient = config.includeElastiCache ? new elasticache.ElastiCacheClient(awsClientConfig(plan.profileName, region)) : null;
|
|
929
1342
|
|
|
930
1343
|
const regionRecords = [];
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
for (const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1344
|
+
if (config.includeEc2) {
|
|
1345
|
+
try {
|
|
1346
|
+
let nextToken = undefined;
|
|
1347
|
+
do {
|
|
1348
|
+
const response = await ec2Client.send(new ec2.DescribeInstancesCommand({ NextToken: nextToken }));
|
|
1349
|
+
for (const reservation of response.Reservations || []) {
|
|
1350
|
+
for (const instance of reservation.Instances || []) {
|
|
1351
|
+
if (!instance || !instance.InstanceId) continue;
|
|
1352
|
+
stats.instancesScanned += 1;
|
|
1353
|
+
if (instanceFilter && !instanceFilter.has(instance.InstanceId)) continue;
|
|
1354
|
+
if (config.publicOnly && !instance.PublicIpAddress) continue;
|
|
1355
|
+
|
|
1356
|
+
addEc2Record(regionRecords, plan, region, instance);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
nextToken = response.NextToken;
|
|
1360
|
+
} while (nextToken);
|
|
1361
|
+
|
|
1362
|
+
const ec2Records = regionRecords.filter((r) => r.resourceType === "ec2");
|
|
1363
|
+
const ssmInfo = await fetchSsmInfoMap(
|
|
1364
|
+
ssmClient,
|
|
1365
|
+
ec2Records.map((r) => r.instanceId),
|
|
1366
|
+
warnings,
|
|
1367
|
+
scopeLabel
|
|
1368
|
+
);
|
|
1369
|
+
for (const record of ec2Records) {
|
|
1370
|
+
applySsmInfo(record, ssmInfo.get(record.instanceId));
|
|
1371
|
+
}
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
stats.regionErrors += 1;
|
|
1374
|
+
warnings.push(`[${scopeLabel}] EC2/SSM collection failed: ${error.message || String(error)}`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (config.includeLambda) {
|
|
1379
|
+
try {
|
|
1380
|
+
let marker = undefined;
|
|
1381
|
+
do {
|
|
1382
|
+
const response = await lambdaClient.send(
|
|
1383
|
+
new lambda.ListFunctionsCommand({ Marker: marker, MaxItems: 50 })
|
|
1384
|
+
);
|
|
1385
|
+
for (const fn of response.Functions || []) {
|
|
1386
|
+
stats.lambdaFunctionsScanned += 1;
|
|
1387
|
+
addLambdaRecord(regionRecords, plan, region, fn);
|
|
964
1388
|
}
|
|
1389
|
+
marker = response.NextMarker;
|
|
1390
|
+
} while (marker);
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
stats.regionErrors += 1;
|
|
1393
|
+
const detail = `${error.name || "Error"}: ${error.message || String(error)}`;
|
|
1394
|
+
warnings.push(`[${scopeLabel}] Lambda collection failed: ${detail}`);
|
|
1395
|
+
const lower = detail.toLowerCase();
|
|
1396
|
+
if (lower.includes("accessdenied") || lower.includes("not authorized")) {
|
|
1397
|
+
pushRequiredAction(requiredActions, {
|
|
1398
|
+
code: "LAMBDA_LIST_PERMISSION_REQUIRED",
|
|
1399
|
+
message: `Missing Lambda list permission for ${scopeLabel}.`,
|
|
1400
|
+
hint: "Need lambda:ListFunctions permission."
|
|
1401
|
+
});
|
|
965
1402
|
}
|
|
966
|
-
|
|
967
|
-
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
968
1405
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1406
|
+
if (config.includeAlb) {
|
|
1407
|
+
try {
|
|
1408
|
+
let marker = undefined;
|
|
1409
|
+
do {
|
|
1410
|
+
const response = await elbv2Client.send(
|
|
1411
|
+
new elbv2.DescribeLoadBalancersCommand({ Marker: marker, PageSize: 100 })
|
|
1412
|
+
);
|
|
1413
|
+
for (const lb of response.LoadBalancers || []) {
|
|
1414
|
+
stats.loadBalancersScanned += 1;
|
|
1415
|
+
addAlbRecord(regionRecords, plan, region, lb);
|
|
1416
|
+
}
|
|
1417
|
+
marker = response.NextMarker;
|
|
1418
|
+
} while (marker);
|
|
1419
|
+
|
|
1420
|
+
let tgMarker = undefined;
|
|
1421
|
+
do {
|
|
1422
|
+
const response = await elbv2Client.send(
|
|
1423
|
+
new elbv2.DescribeTargetGroupsCommand({ Marker: tgMarker, PageSize: 100 })
|
|
1424
|
+
);
|
|
1425
|
+
for (const tg of response.TargetGroups || []) {
|
|
1426
|
+
stats.targetGroupsScanned += 1;
|
|
1427
|
+
addTargetGroupRecord(regionRecords, plan, region, tg);
|
|
1428
|
+
}
|
|
1429
|
+
tgMarker = response.NextMarker;
|
|
1430
|
+
} while (tgMarker);
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
stats.regionErrors += 1;
|
|
1433
|
+
const detail = `${error.name || "Error"}: ${error.message || String(error)}`;
|
|
1434
|
+
warnings.push(`[${scopeLabel}] ELBv2 collection failed: ${detail}`);
|
|
1435
|
+
const lower = detail.toLowerCase();
|
|
1436
|
+
if (lower.includes("accessdenied") || lower.includes("not authorized")) {
|
|
1437
|
+
pushRequiredAction(requiredActions, {
|
|
1438
|
+
code: "ELBV2_LIST_PERMISSION_REQUIRED",
|
|
1439
|
+
message: `Missing ELBv2 list permission for ${scopeLabel}.`,
|
|
1440
|
+
hint: "Need elasticloadbalancing:DescribeLoadBalancers + elasticloadbalancing:DescribeTargetGroups."
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
977
1443
|
}
|
|
1444
|
+
}
|
|
978
1445
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
let
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const remediation = await remediateInstanceProfile(
|
|
986
|
-
ec2Client,
|
|
987
|
-
record.instanceId,
|
|
988
|
-
config,
|
|
989
|
-
warnings,
|
|
990
|
-
requiredActions,
|
|
991
|
-
scopeLabel
|
|
1446
|
+
if (config.includeAsg) {
|
|
1447
|
+
try {
|
|
1448
|
+
let nextToken = undefined;
|
|
1449
|
+
do {
|
|
1450
|
+
const response = await asgClient.send(
|
|
1451
|
+
new autoScaling.DescribeAutoScalingGroupsCommand({ NextToken: nextToken, MaxRecords: 100 })
|
|
992
1452
|
);
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
changedInRegion += 1;
|
|
1453
|
+
for (const asg of response.AutoScalingGroups || []) {
|
|
1454
|
+
stats.autoScalingGroupsScanned += 1;
|
|
1455
|
+
addAsgRecord(regionRecords, plan, region, asg);
|
|
997
1456
|
}
|
|
1457
|
+
nextToken = response.NextToken;
|
|
1458
|
+
} while (nextToken);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
stats.regionErrors += 1;
|
|
1461
|
+
const detail = `${error.name || "Error"}: ${error.message || String(error)}`;
|
|
1462
|
+
warnings.push(`[${scopeLabel}] AutoScaling collection failed: ${detail}`);
|
|
1463
|
+
const lower = detail.toLowerCase();
|
|
1464
|
+
if (lower.includes("accessdenied") || lower.includes("not authorized")) {
|
|
1465
|
+
pushRequiredAction(requiredActions, {
|
|
1466
|
+
code: "ASG_LIST_PERMISSION_REQUIRED",
|
|
1467
|
+
message: `Missing Auto Scaling list permission for ${scopeLabel}.`,
|
|
1468
|
+
hint: "Need autoscaling:DescribeAutoScalingGroups."
|
|
1469
|
+
});
|
|
998
1470
|
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
999
1473
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1474
|
+
if (config.includeRds) {
|
|
1475
|
+
try {
|
|
1476
|
+
let marker = undefined;
|
|
1477
|
+
do {
|
|
1478
|
+
const response = await rdsClient.send(
|
|
1479
|
+
new rds.DescribeDBInstancesCommand({ Marker: marker, MaxRecords: 100 })
|
|
1480
|
+
);
|
|
1481
|
+
for (const db of response.DBInstances || []) {
|
|
1482
|
+
stats.rdsInstancesScanned += 1;
|
|
1483
|
+
addRdsRecord(regionRecords, plan, region, db);
|
|
1484
|
+
}
|
|
1485
|
+
marker = response.Marker;
|
|
1486
|
+
} while (marker);
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
stats.regionErrors += 1;
|
|
1489
|
+
const detail = `${error.name || "Error"}: ${error.message || String(error)}`;
|
|
1490
|
+
warnings.push(`[${scopeLabel}] RDS collection failed: ${detail}`);
|
|
1491
|
+
const lower = detail.toLowerCase();
|
|
1492
|
+
if (lower.includes("accessdenied") || lower.includes("not authorized")) {
|
|
1493
|
+
pushRequiredAction(requiredActions, {
|
|
1494
|
+
code: "RDS_LIST_PERMISSION_REQUIRED",
|
|
1495
|
+
message: `Missing RDS list permission for ${scopeLabel}.`,
|
|
1496
|
+
hint: "Need rds:DescribeDBInstances."
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1003
1501
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1502
|
+
if (config.includeElastiCache) {
|
|
1503
|
+
try {
|
|
1504
|
+
let marker = undefined;
|
|
1505
|
+
do {
|
|
1506
|
+
const response = await elasticacheClient.send(
|
|
1507
|
+
new elasticache.DescribeCacheClustersCommand({
|
|
1508
|
+
Marker: marker,
|
|
1509
|
+
MaxRecords: 100,
|
|
1510
|
+
ShowCacheNodeInfo: false
|
|
1511
|
+
})
|
|
1009
1512
|
);
|
|
1010
|
-
for (const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (record.remediationStatus && record.ssmOnline && !wasOnline) {
|
|
1014
|
-
record.remediationStatus = `${record.remediationStatus};ssm-online`;
|
|
1015
|
-
}
|
|
1513
|
+
for (const cluster of response.CacheClusters || []) {
|
|
1514
|
+
stats.elasticacheClustersScanned += 1;
|
|
1515
|
+
addElastiCacheRecord(regionRecords, plan, region, cluster);
|
|
1016
1516
|
}
|
|
1517
|
+
marker = response.Marker;
|
|
1518
|
+
} while (marker);
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
stats.regionErrors += 1;
|
|
1521
|
+
const detail = `${error.name || "Error"}: ${error.message || String(error)}`;
|
|
1522
|
+
warnings.push(`[${scopeLabel}] ElastiCache collection failed: ${detail}`);
|
|
1523
|
+
const lower = detail.toLowerCase();
|
|
1524
|
+
if (lower.includes("accessdenied") || lower.includes("not authorized")) {
|
|
1525
|
+
pushRequiredAction(requiredActions, {
|
|
1526
|
+
code: "ELASTICACHE_LIST_PERMISSION_REQUIRED",
|
|
1527
|
+
message: `Missing ElastiCache list permission for ${scopeLabel}.`,
|
|
1528
|
+
hint: "Need elasticache:DescribeCacheClusters."
|
|
1529
|
+
});
|
|
1017
1530
|
}
|
|
1018
1531
|
}
|
|
1532
|
+
}
|
|
1019
1533
|
|
|
1534
|
+
if (regionRecords.length) {
|
|
1020
1535
|
records.push(...regionRecords);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (ec2Client) ec2Client.destroy();
|
|
1539
|
+
if (ssmClient) ssmClient.destroy();
|
|
1540
|
+
if (lambdaClient) lambdaClient.destroy();
|
|
1541
|
+
if (elbv2Client) elbv2Client.destroy();
|
|
1542
|
+
if (asgClient) asgClient.destroy();
|
|
1543
|
+
if (rdsClient) rdsClient.destroy();
|
|
1544
|
+
if (elasticacheClient) elasticacheClient.destroy();
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
if (config.includeRoute53) {
|
|
1548
|
+
const scopeLabel = `${plan.profileLabel}/global`;
|
|
1549
|
+
const route53Client = new route53.Route53Client(awsClientConfig(plan.profileName, "us-east-1"));
|
|
1550
|
+
try {
|
|
1551
|
+
let marker = undefined;
|
|
1552
|
+
let isTruncated = false;
|
|
1553
|
+
do {
|
|
1554
|
+
const response = await route53Client.send(
|
|
1555
|
+
new route53.ListHostedZonesCommand({ Marker: marker, MaxItems: "100" })
|
|
1556
|
+
);
|
|
1557
|
+
for (const zone of response.HostedZones || []) {
|
|
1558
|
+
stats.route53ZonesScanned += 1;
|
|
1559
|
+
let recordSetCount = null;
|
|
1560
|
+
if (zone && zone.Id) {
|
|
1561
|
+
try {
|
|
1562
|
+
let rsCount = 0;
|
|
1563
|
+
let nextName = undefined;
|
|
1564
|
+
let nextType = undefined;
|
|
1565
|
+
let nextId = undefined;
|
|
1566
|
+
let truncated = false;
|
|
1567
|
+
do {
|
|
1568
|
+
const rsResp = await route53Client.send(
|
|
1569
|
+
new route53.ListResourceRecordSetsCommand({
|
|
1570
|
+
HostedZoneId: String(zone.Id).replace("/hostedzone/", ""),
|
|
1571
|
+
StartRecordName: nextName,
|
|
1572
|
+
StartRecordType: nextType,
|
|
1573
|
+
StartRecordIdentifier: nextId,
|
|
1574
|
+
MaxItems: "100"
|
|
1575
|
+
})
|
|
1576
|
+
);
|
|
1577
|
+
rsCount += Array.isArray(rsResp.ResourceRecordSets) ? rsResp.ResourceRecordSets.length : 0;
|
|
1578
|
+
truncated = Boolean(rsResp.IsTruncated);
|
|
1579
|
+
nextName = rsResp.NextRecordName;
|
|
1580
|
+
nextType = rsResp.NextRecordType;
|
|
1581
|
+
nextId = rsResp.NextRecordIdentifier;
|
|
1582
|
+
} while (truncated);
|
|
1583
|
+
recordSetCount = rsCount;
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
warnings.push(`[${scopeLabel}] ListResourceRecordSets failed for zone ${zone.Name || zone.Id}: ${err.message || String(err)}`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
addRoute53ZoneRecord(records, plan, zone, recordSetCount);
|
|
1589
|
+
}
|
|
1590
|
+
marker = response.NextMarker;
|
|
1591
|
+
isTruncated = Boolean(response.IsTruncated);
|
|
1592
|
+
} while (isTruncated && marker);
|
|
1021
1593
|
} catch (error) {
|
|
1594
|
+
const detail = `${error.name || "Error"}: ${error.message || String(error)}`;
|
|
1595
|
+
warnings.push(`[${scopeLabel}] Route53 collection failed: ${detail}`);
|
|
1022
1596
|
stats.regionErrors += 1;
|
|
1023
|
-
|
|
1597
|
+
const lower = detail.toLowerCase();
|
|
1598
|
+
if (lower.includes("accessdenied") || lower.includes("not authorized")) {
|
|
1599
|
+
pushRequiredAction(requiredActions, {
|
|
1600
|
+
code: "ROUTE53_LIST_PERMISSION_REQUIRED",
|
|
1601
|
+
message: `Missing Route53 list permission for ${scopeLabel}.`,
|
|
1602
|
+
hint: "Need route53:ListHostedZones (+ route53:ListResourceRecordSets for record counts)."
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1024
1605
|
} finally {
|
|
1025
|
-
|
|
1026
|
-
ssmClient.destroy();
|
|
1606
|
+
route53Client.destroy();
|
|
1027
1607
|
}
|
|
1028
1608
|
}
|
|
1029
1609
|
}
|
|
@@ -1224,6 +1804,91 @@ async function mapWithConcurrency(items, concurrency, fn) {
|
|
|
1224
1804
|
await Promise.all(workers);
|
|
1225
1805
|
}
|
|
1226
1806
|
|
|
1807
|
+
function groupEc2RecordsByScope(records) {
|
|
1808
|
+
const map = new Map();
|
|
1809
|
+
for (const record of records) {
|
|
1810
|
+
if (!record || record.resourceType !== "ec2" || !record.instanceId) continue;
|
|
1811
|
+
const key = [record.profile || "", record.accountId || "", record.region || ""].join("|");
|
|
1812
|
+
if (!map.has(key)) map.set(key, []);
|
|
1813
|
+
map.get(key).push(record);
|
|
1814
|
+
}
|
|
1815
|
+
return map;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
async function collectRemediation(config, records, plans, warnings, requiredActions) {
|
|
1819
|
+
if (!config.autoRemediateSsm) {
|
|
1820
|
+
return { attempted: 0, changed: 0 };
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const { ec2, ssm } = loadAwsModules();
|
|
1824
|
+
const planLookup = new Map(plans.map((p) => [`${p.profileLabel}::${p.accountId}`, p]));
|
|
1825
|
+
const scoped = groupEc2RecordsByScope(records);
|
|
1826
|
+
let attempted = 0;
|
|
1827
|
+
let changed = 0;
|
|
1828
|
+
|
|
1829
|
+
for (const [scopeKey, scopeRecords] of scoped.entries()) {
|
|
1830
|
+
const [profile, accountId, region] = scopeKey.split("|");
|
|
1831
|
+
const plan = planLookup.get(`${profile}::${accountId}`);
|
|
1832
|
+
if (!plan) {
|
|
1833
|
+
for (const record of scopeRecords) {
|
|
1834
|
+
if (!record.ssmManaged) {
|
|
1835
|
+
record.remediationStatus = "skipped-no-plan";
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
const scopeLabel = `${profile}/${region}`;
|
|
1842
|
+
const ec2Client = new ec2.EC2Client(awsClientConfig(plan.profileName, region));
|
|
1843
|
+
const ssmClient = new ssm.SSMClient(awsClientConfig(plan.profileName, region));
|
|
1844
|
+
const unmanaged = scopeRecords.filter((r) => !r.ssmManaged);
|
|
1845
|
+
let changedInScope = 0;
|
|
1846
|
+
|
|
1847
|
+
try {
|
|
1848
|
+
for (const record of unmanaged) {
|
|
1849
|
+
attempted += 1;
|
|
1850
|
+
const remediation = await remediateInstanceProfile(
|
|
1851
|
+
ec2Client,
|
|
1852
|
+
record.instanceId,
|
|
1853
|
+
config,
|
|
1854
|
+
warnings,
|
|
1855
|
+
requiredActions,
|
|
1856
|
+
scopeLabel
|
|
1857
|
+
);
|
|
1858
|
+
record.remediationStatus = remediation.status;
|
|
1859
|
+
if (remediation.changed) {
|
|
1860
|
+
changed += 1;
|
|
1861
|
+
changedInScope += 1;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (changedInScope > 0 && config.remediationWaitSec > 0) {
|
|
1866
|
+
warnings.push(`[${scopeLabel}] Waiting ${config.remediationWaitSec}s for SSM registration after remediation.`);
|
|
1867
|
+
await sleep(config.remediationWaitSec * 1000);
|
|
1868
|
+
|
|
1869
|
+
const recheck = await fetchSsmInfoMap(
|
|
1870
|
+
ssmClient,
|
|
1871
|
+
scopeRecords.map((r) => r.instanceId),
|
|
1872
|
+
warnings,
|
|
1873
|
+
scopeLabel
|
|
1874
|
+
);
|
|
1875
|
+
for (const record of scopeRecords) {
|
|
1876
|
+
const wasOnline = record.ssmOnline;
|
|
1877
|
+
applySsmInfo(record, recheck.get(record.instanceId));
|
|
1878
|
+
if (record.remediationStatus && record.ssmOnline && !wasOnline) {
|
|
1879
|
+
record.remediationStatus = `${record.remediationStatus};ssm-online`;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
} finally {
|
|
1884
|
+
ec2Client.destroy();
|
|
1885
|
+
ssmClient.destroy();
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
return { attempted, changed };
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1227
1892
|
async function collectSnapshots(config, records, plans, warnings, requiredActions) {
|
|
1228
1893
|
if (!config.runtimeSnapshot) return { attempted: 0, succeeded: 0 };
|
|
1229
1894
|
const targets = records.filter((r) => r.ssmOnline === true);
|
|
@@ -1242,10 +1907,23 @@ async function collectSnapshots(config, records, plans, warnings, requiredAction
|
|
|
1242
1907
|
return { attempted, succeeded };
|
|
1243
1908
|
}
|
|
1244
1909
|
|
|
1910
|
+
async function executeRuntimeOperations(config, records, plans, warnings, requiredActions) {
|
|
1911
|
+
const remediation = await collectRemediation(config, records, plans, warnings, requiredActions);
|
|
1912
|
+
const snapshots = await collectSnapshots(config, records, plans, warnings, requiredActions);
|
|
1913
|
+
return {
|
|
1914
|
+
remediationAttempts: remediation.attempted,
|
|
1915
|
+
remediationChanged: remediation.changed,
|
|
1916
|
+
snapshotAttempted: snapshots.attempted,
|
|
1917
|
+
snapshotSucceeded: snapshots.succeeded
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1245
1921
|
function aggregate(config, records) {
|
|
1246
1922
|
let out = records.slice();
|
|
1247
|
-
if (config.managedOnly)
|
|
1248
|
-
|
|
1923
|
+
if (config.managedOnly) {
|
|
1924
|
+
out = out.filter((r) => r.resourceType !== "ec2" || r.ssmManaged === true);
|
|
1925
|
+
}
|
|
1926
|
+
out.sort((a, b) => [a.profile || "", a.accountId || "", a.region || "", a.resourceType || "", a.resourceId || a.instanceId || ""].join("|").localeCompare([b.profile || "", b.accountId || "", b.region || "", b.resourceType || "", b.resourceId || b.instanceId || ""].join("|")));
|
|
1249
1927
|
return out;
|
|
1250
1928
|
}
|
|
1251
1929
|
|
|
@@ -1257,6 +1935,9 @@ function csvEscape(value) {
|
|
|
1257
1935
|
|
|
1258
1936
|
function toCsvRow(record) {
|
|
1259
1937
|
return {
|
|
1938
|
+
resourceType: record.resourceType,
|
|
1939
|
+
service: record.service,
|
|
1940
|
+
resourceId: record.resourceId,
|
|
1260
1941
|
profile: record.profile,
|
|
1261
1942
|
accountId: record.accountId,
|
|
1262
1943
|
region: record.region,
|
|
@@ -1276,6 +1957,53 @@ function toCsvRow(record) {
|
|
|
1276
1957
|
ssmPlatformName: record.ssmPlatformName,
|
|
1277
1958
|
ssmPlatformVersion: record.ssmPlatformVersion,
|
|
1278
1959
|
remediationStatus: record.remediationStatus,
|
|
1960
|
+
lambdaFunctionName: record.lambdaFunctionName,
|
|
1961
|
+
lambdaFunctionArn: record.lambdaFunctionArn,
|
|
1962
|
+
lambdaRuntime: record.lambdaRuntime,
|
|
1963
|
+
lambdaHandler: record.lambdaHandler,
|
|
1964
|
+
lambdaRole: record.lambdaRole,
|
|
1965
|
+
lambdaLastModified: record.lambdaLastModified,
|
|
1966
|
+
lambdaState: record.lambdaState,
|
|
1967
|
+
lambdaPackageType: record.lambdaPackageType,
|
|
1968
|
+
lambdaTimeoutSec: record.lambdaTimeoutSec,
|
|
1969
|
+
lambdaMemoryMb: record.lambdaMemoryMb,
|
|
1970
|
+
lambdaVpcConfigured: record.lambdaVpcConfigured,
|
|
1971
|
+
albArn: record.albArn,
|
|
1972
|
+
albType: record.albType,
|
|
1973
|
+
albScheme: record.albScheme,
|
|
1974
|
+
albDnsName: record.albDnsName,
|
|
1975
|
+
albVpcId: record.albVpcId,
|
|
1976
|
+
albListenerPorts: record.albListenerPorts,
|
|
1977
|
+
targetGroupArn: record.targetGroupArn,
|
|
1978
|
+
targetGroupType: record.targetGroupType,
|
|
1979
|
+
targetGroupPort: record.targetGroupPort,
|
|
1980
|
+
targetGroupProtocol: record.targetGroupProtocol,
|
|
1981
|
+
targetGroupVpcId: record.targetGroupVpcId,
|
|
1982
|
+
targetGroupLoadBalancers: record.targetGroupLoadBalancers,
|
|
1983
|
+
asgName: record.asgName,
|
|
1984
|
+
asgDesiredCapacity: record.asgDesiredCapacity,
|
|
1985
|
+
asgMinSize: record.asgMinSize,
|
|
1986
|
+
asgMaxSize: record.asgMaxSize,
|
|
1987
|
+
asgInstanceCount: record.asgInstanceCount,
|
|
1988
|
+
asgLaunchTemplate: record.asgLaunchTemplate,
|
|
1989
|
+
rdsIdentifier: record.rdsIdentifier,
|
|
1990
|
+
rdsEngine: record.rdsEngine,
|
|
1991
|
+
rdsEngineVersion: record.rdsEngineVersion,
|
|
1992
|
+
rdsStatus: record.rdsStatus,
|
|
1993
|
+
rdsMultiAz: record.rdsMultiAz,
|
|
1994
|
+
rdsEndpoint: record.rdsEndpoint,
|
|
1995
|
+
rdsStorageGb: record.rdsStorageGb,
|
|
1996
|
+
elasticacheClusterId: record.elasticacheClusterId,
|
|
1997
|
+
elasticacheEngine: record.elasticacheEngine,
|
|
1998
|
+
elasticacheEngineVersion: record.elasticacheEngineVersion,
|
|
1999
|
+
elasticacheNodeType: record.elasticacheNodeType,
|
|
2000
|
+
elasticacheStatus: record.elasticacheStatus,
|
|
2001
|
+
elasticacheNumNodes: record.elasticacheNumNodes,
|
|
2002
|
+
route53ZoneId: record.route53ZoneId,
|
|
2003
|
+
route53ZoneName: record.route53ZoneName,
|
|
2004
|
+
route53PrivateZone: record.route53PrivateZone,
|
|
2005
|
+
route53RecordSetCount: record.route53RecordSetCount,
|
|
2006
|
+
route53CallerReference: record.route53CallerReference,
|
|
1279
2007
|
runtimeSnapshotStatus: record.runtimeSnapshot ? record.runtimeSnapshot.status : null,
|
|
1280
2008
|
runtimeSnapshotCollectedAt: record.runtimeSnapshot ? record.runtimeSnapshot.collectedAt : null,
|
|
1281
2009
|
runtimeSnapshotOutput: record.runtimeSnapshot ? record.runtimeSnapshot.output : null,
|
|
@@ -1288,9 +2016,19 @@ function renderOutput(config, records) {
|
|
|
1288
2016
|
return JSON.stringify(records, null, 2);
|
|
1289
2017
|
}
|
|
1290
2018
|
const fields = [
|
|
2019
|
+
"resourceType", "service", "resourceId",
|
|
1291
2020
|
"profile", "accountId", "region", "instanceId", "name", "state", "privateIp", "publicIp", "publicDns",
|
|
1292
2021
|
"platform", "iamInstanceProfileArn", "ssmManaged", "ssmOnline", "ssmPingStatus", "ssmLastPingDateTime",
|
|
1293
|
-
"ssmAgentVersion", "ssmPlatformName", "ssmPlatformVersion", "remediationStatus",
|
|
2022
|
+
"ssmAgentVersion", "ssmPlatformName", "ssmPlatformVersion", "remediationStatus",
|
|
2023
|
+
"lambdaFunctionName", "lambdaFunctionArn", "lambdaRuntime", "lambdaHandler", "lambdaRole", "lambdaLastModified",
|
|
2024
|
+
"lambdaState", "lambdaPackageType", "lambdaTimeoutSec", "lambdaMemoryMb", "lambdaVpcConfigured",
|
|
2025
|
+
"albArn", "albType", "albScheme", "albDnsName", "albVpcId", "albListenerPorts",
|
|
2026
|
+
"targetGroupArn", "targetGroupType", "targetGroupPort", "targetGroupProtocol", "targetGroupVpcId", "targetGroupLoadBalancers",
|
|
2027
|
+
"asgName", "asgDesiredCapacity", "asgMinSize", "asgMaxSize", "asgInstanceCount", "asgLaunchTemplate",
|
|
2028
|
+
"rdsIdentifier", "rdsEngine", "rdsEngineVersion", "rdsStatus", "rdsMultiAz", "rdsEndpoint", "rdsStorageGb",
|
|
2029
|
+
"elasticacheClusterId", "elasticacheEngine", "elasticacheEngineVersion", "elasticacheNodeType", "elasticacheStatus", "elasticacheNumNodes",
|
|
2030
|
+
"route53ZoneId", "route53ZoneName", "route53PrivateZone", "route53RecordSetCount", "route53CallerReference",
|
|
2031
|
+
"runtimeSnapshotStatus",
|
|
1294
2032
|
"runtimeSnapshotCollectedAt", "runtimeSnapshotOutput", "runtimeSnapshotError"
|
|
1295
2033
|
];
|
|
1296
2034
|
const lines = [fields.join(",")];
|
|
@@ -1315,18 +2053,19 @@ async function runWorkflow(config) {
|
|
|
1315
2053
|
const warnings = [];
|
|
1316
2054
|
const requiredActions = [];
|
|
1317
2055
|
|
|
1318
|
-
progress(config, 1, "orchestrator: parse
|
|
1319
|
-
eprint(`Inputs: profiles=${config.profiles ? config.profiles.join(",") : "auto"}, regions=${config.regions ? config.regions.join(",") : "auto"}, public_only=${config.publicOnly ? "on" : "off"}, managed_only=${config.managedOnly ? "on" : "off"}, auto_remediate_ssm=${config.autoRemediateSsm ? "on" : "off"}, runtime_snapshot=${config.runtimeSnapshot ? "on" : "off"}, auto_sso_login=${config.autoSsoLogin ? "on" : "off"}`);
|
|
2056
|
+
progress(config, 1, "orchestrator: parse user request and operation scope");
|
|
2057
|
+
eprint(`Inputs: execution_mode=${INTERNAL_BACKEND_ID}, profiles=${config.profiles ? config.profiles.join(",") : "auto"}, regions=${config.regions ? config.regions.join(",") : "auto"}, include_ec2=${config.includeEc2 ? "on" : "off"}, include_lambda=${config.includeLambda ? "on" : "off"}, include_alb=${config.includeAlb ? "on" : "off"}, include_asg=${config.includeAsg ? "on" : "off"}, include_rds=${config.includeRds ? "on" : "off"}, include_elasticache=${config.includeElastiCache ? "on" : "off"}, include_route53=${config.includeRoute53 ? "on" : "off"}, public_only=${config.publicOnly ? "on" : "off"}, managed_only=${config.managedOnly ? "on" : "off"}, auto_remediate_ssm=${config.autoRemediateSsm ? "on" : "off"}, runtime_snapshot=${config.runtimeSnapshot ? "on" : "off"}, auto_sso_login=${config.autoSsoLogin ? "on" : "off"}`);
|
|
1320
2058
|
|
|
1321
2059
|
progress(config, 2, "config_validator: validate settings and output path");
|
|
1322
2060
|
validateConfig(config, warnings, requiredActions);
|
|
1323
2061
|
|
|
1324
|
-
progress(config, 3, "
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
2062
|
+
progress(config, 3, "execution_resolver: resolve internal execution modules");
|
|
2063
|
+
eprint(`Execution mode: ${INTERNAL_BACKEND_ID}`);
|
|
2064
|
+
|
|
2065
|
+
progress(config, 4, "aws_discovery_planner: build operation graph and discovery plan");
|
|
2066
|
+
const operationPlan = buildOperationPlan(config);
|
|
2067
|
+
eprint(`Operation plan: inventory=${operationPlan.inventoryOps.length ? operationPlan.inventoryOps.join(",") : "none"}; runtime=${operationPlan.runtimeOps.length ? operationPlan.runtimeOps.join(",") : "none"}`);
|
|
1328
2068
|
|
|
1329
|
-
progress(config, 4, "aws_discovery_planner: validate credentials and profile/region plan");
|
|
1330
2069
|
const plans = await buildDiscoveryPlan(config, warnings, requiredActions);
|
|
1331
2070
|
if (plans.length) {
|
|
1332
2071
|
eprint("Discovery plan: " + plans.map((p) => `${p.profileLabel}(${p.regions.length} regions)`).join(", "));
|
|
@@ -1334,14 +2073,21 @@ async function runWorkflow(config) {
|
|
|
1334
2073
|
warnings.push("No usable AWS profiles were found after validation.");
|
|
1335
2074
|
}
|
|
1336
2075
|
|
|
1337
|
-
|
|
1338
|
-
|
|
2076
|
+
if (config.autoRemediateSsm) {
|
|
2077
|
+
eprint(`Remediation target: ${config.ssmInstanceProfileArn || config.ssmInstanceProfileName} (replace=${config.allowReplaceProfile ? "on" : "off"})`);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
progress(config, 5, "resource_inventory_collector: collect multi-service inventory");
|
|
2081
|
+
const inventoryResult = await collectInventory(config, plans, warnings, requiredActions);
|
|
2082
|
+
const records = inventoryResult.records;
|
|
2083
|
+
const stats = inventoryResult.stats;
|
|
1339
2084
|
eprint(`Collected inventory records: ${records.length}`);
|
|
1340
2085
|
|
|
1341
|
-
progress(config, 6, "
|
|
1342
|
-
const
|
|
1343
|
-
const
|
|
1344
|
-
|
|
2086
|
+
progress(config, 6, "runtime_operation_executor: execute runtime snapshot/remediation tasks");
|
|
2087
|
+
const ec2Records = records.filter((r) => r.resourceType === "ec2");
|
|
2088
|
+
const unmanaged = ec2Records.filter((r) => !r.ssmManaged).length;
|
|
2089
|
+
const offline = ec2Records.filter((r) => r.ssmManaged && !r.ssmOnline).length;
|
|
2090
|
+
eprint(`Pre-runtime SSM state: unmanaged=${unmanaged}, managed_offline=${offline}`);
|
|
1345
2091
|
|
|
1346
2092
|
if (unmanaged > 0 && !config.autoRemediateSsm) {
|
|
1347
2093
|
pushRequiredAction(requiredActions, {
|
|
@@ -1351,18 +2097,28 @@ async function runWorkflow(config) {
|
|
|
1351
2097
|
});
|
|
1352
2098
|
}
|
|
1353
2099
|
|
|
1354
|
-
|
|
1355
|
-
const snapshotStats = await collectSnapshots(config, records, plans, warnings, requiredActions);
|
|
2100
|
+
const runtimeStats = await executeRuntimeOperations(config, records, plans, warnings, requiredActions);
|
|
1356
2101
|
|
|
1357
|
-
progress(config,
|
|
2102
|
+
progress(config, 7, "result_aggregator: normalize records/warnings/actions");
|
|
1358
2103
|
const outputRecords = aggregate(config, records);
|
|
2104
|
+
|
|
2105
|
+
progress(config, 8, "cli_output_formatter: render JSON/CSV and guidance payload");
|
|
1359
2106
|
writeOutput(config, renderOutput(config, outputRecords));
|
|
1360
2107
|
|
|
1361
|
-
progress(config, 9, "END:
|
|
1362
|
-
const
|
|
1363
|
-
const
|
|
1364
|
-
const
|
|
1365
|
-
|
|
2108
|
+
progress(config, 9, "END: emit execution summary and evidence metadata");
|
|
2109
|
+
const outputEc2 = outputRecords.filter((r) => r.resourceType === "ec2");
|
|
2110
|
+
const outputLambda = outputRecords.filter((r) => r.resourceType === "lambda");
|
|
2111
|
+
const outputAlb = outputRecords.filter((r) => r.resourceType === "alb");
|
|
2112
|
+
const outputTargetGroups = outputRecords.filter((r) => r.resourceType === "target_group");
|
|
2113
|
+
const outputAsg = outputRecords.filter((r) => r.resourceType === "asg");
|
|
2114
|
+
const outputRds = outputRecords.filter((r) => r.resourceType === "rds");
|
|
2115
|
+
const outputElastiCache = outputRecords.filter((r) => r.resourceType === "elasticache");
|
|
2116
|
+
const outputRoute53 = outputRecords.filter((r) => r.resourceType === "route53_zone");
|
|
2117
|
+
const ssmManaged = outputEc2.filter((r) => r.ssmManaged).length;
|
|
2118
|
+
const ssmOnline = outputEc2.filter((r) => r.ssmOnline).length;
|
|
2119
|
+
const publicCount = outputEc2.filter((r) => Boolean(r.publicIp)).length;
|
|
2120
|
+
eprint(`Summary: execution_mode=${INTERNAL_BACKEND_ID}, profiles=${stats.profiles}, regions_scanned=${stats.regions}, region_errors=${stats.regionErrors}, ec2_scanned=${stats.instancesScanned}, lambda_scanned=${stats.lambdaFunctionsScanned}, alb_scanned=${stats.loadBalancersScanned}, targetgroup_scanned=${stats.targetGroupsScanned}, asg_scanned=${stats.autoScalingGroupsScanned}, rds_scanned=${stats.rdsInstancesScanned}, elasticache_scanned=${stats.elasticacheClustersScanned}, route53_zone_scanned=${stats.route53ZonesScanned}, output_records=${outputRecords.length}, output_ec2=${outputEc2.length}, output_lambda=${outputLambda.length}, output_alb=${outputAlb.length}, output_target_group=${outputTargetGroups.length}, output_asg=${outputAsg.length}, output_rds=${outputRds.length}, output_elasticache=${outputElastiCache.length}, output_route53_zone=${outputRoute53.length}, ec2_public_ip_records=${publicCount}, ec2_ssm_managed=${ssmManaged}, ec2_ssm_online=${ssmOnline}, remediation_attempts=${runtimeStats.remediationAttempts}, remediation_changed=${runtimeStats.remediationChanged}, runtime_snapshot_attempted=${runtimeStats.snapshotAttempted}, runtime_snapshot_succeeded=${runtimeStats.snapshotSucceeded}, warnings=${warnings.length}, required_actions=${requiredActions.length}`);
|
|
2121
|
+
eprint(`EvidenceMeta: workflow_id=b60f0f18-cc45-4af9-a7c7-217284457759; schema=gesia.orflow.contract.v2; step_count=${TOTAL_STEPS}; execution_mode=${INTERNAL_BACKEND_ID}; inventory_ops=${operationPlan.inventoryOps.length}; runtime_ops=${operationPlan.runtimeOps.length}`);
|
|
1366
2122
|
|
|
1367
2123
|
for (const warning of warnings) eprint(`WARNING: ${warning}`);
|
|
1368
2124
|
for (const action of requiredActions) {
|