mcp-aws-manager 0.3.1 → 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.
@@ -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-only AWS EC2 inventory/runtime collector plus MCP client setup helper.",
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 Codex/Claude",
83
+ " setup Register/re-register MCP server for supported clients",
82
84
  " doctor Check install and registration health",
83
- " discover Run EC2+SSM inventory workflow",
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
- " --clients <codex,claude> (default: codex,claude)",
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("--clients must include at least one of: codex, claude");
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: codex, claude`);
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 removeRegistration(cliBin, serverName) {
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
- runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "user"], { stdio: "ignore" });
444
- runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "local"], { stdio: "ignore" });
445
- return;
517
+ return [
518
+ ["mcp", "get", serverName],
519
+ ["mcp", "get", serverName, "-s", "user"],
520
+ ["mcp", "get", serverName, "-s", "local"]
521
+ ];
446
522
  }
447
- runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
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 isRegistered(cliBin, serverName) {
451
- const args = cliBin === "claude"
452
- ? ["mcp", "get", serverName]
453
- : ["mcp", "get", serverName, "--json"];
454
- const run = runCLICommand(cliBin, args, { stdio: "ignore" });
455
- return run && run.status === 0;
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 registrationAttempts(cliBin, serverName, mcpCommand) {
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) => commandExists(cli, ["mcp", "--help"]));
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("No codex/claude CLI found. Registration skipped.\n");
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, ["mcp", "--help"]);
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 Codex or Claude Code first.\n");
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
- try {
932
- let nextToken = undefined;
933
- do {
934
- const response = await ec2Client.send(new ec2.DescribeInstancesCommand({ NextToken: nextToken }));
935
- for (const reservation of response.Reservations || []) {
936
- for (const instance of reservation.Instances || []) {
937
- if (!instance || !instance.InstanceId) continue;
938
- stats.instancesScanned += 1;
939
- if (instanceFilter && !instanceFilter.has(instance.InstanceId)) continue;
940
- if (config.publicOnly && !instance.PublicIpAddress) continue;
941
-
942
- regionRecords.push({
943
- profile: plan.profileLabel,
944
- accountId: plan.accountId,
945
- region,
946
- instanceId: instance.InstanceId,
947
- name: getTagValue(instance.Tags, "Name"),
948
- state: instance.State && instance.State.Name ? instance.State.Name : null,
949
- privateIp: instance.PrivateIpAddress || null,
950
- publicIp: instance.PublicIpAddress || null,
951
- publicDns: instance.PublicDnsName || null,
952
- platform: instancePlatform(instance),
953
- iamInstanceProfileArn: instance.IamInstanceProfile && instance.IamInstanceProfile.Arn ? instance.IamInstanceProfile.Arn : null,
954
- ssmManaged: false,
955
- ssmOnline: false,
956
- ssmPingStatus: null,
957
- ssmLastPingDateTime: null,
958
- ssmAgentVersion: null,
959
- ssmPlatformName: null,
960
- ssmPlatformVersion: null,
961
- remediationStatus: null,
962
- runtimeSnapshot: null
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
- nextToken = response.NextToken;
967
- } while (nextToken);
1403
+ }
1404
+ }
968
1405
 
969
- const ssmInfo = await fetchSsmInfoMap(
970
- ssmClient,
971
- regionRecords.map((r) => r.instanceId),
972
- warnings,
973
- scopeLabel
974
- );
975
- for (const record of regionRecords) {
976
- applySsmInfo(record, ssmInfo.get(record.instanceId));
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
- if (config.autoRemediateSsm) {
980
- const unmanaged = regionRecords.filter((r) => !r.ssmManaged);
981
- let changedInRegion = 0;
982
-
983
- for (const record of unmanaged) {
984
- stats.remediationAttempts += 1;
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
- record.remediationStatus = remediation.status;
994
- if (remediation.changed) {
995
- stats.remediationChanged += 1;
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
- if (changedInRegion > 0 && config.remediationWaitSec > 0) {
1001
- warnings.push(`[${scopeLabel}] Waiting ${config.remediationWaitSec}s for SSM registration after remediation.`);
1002
- await sleep(config.remediationWaitSec * 1000);
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
- const recheck = await fetchSsmInfoMap(
1005
- ssmClient,
1006
- regionRecords.map((r) => r.instanceId),
1007
- warnings,
1008
- scopeLabel
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 record of regionRecords) {
1011
- const wasOnline = record.ssmOnline;
1012
- applySsmInfo(record, recheck.get(record.instanceId));
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
- warnings.push(`[${scopeLabel}] EC2/SSM collection failed: ${error.message || String(error)}`);
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
- ec2Client.destroy();
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) out = out.filter((r) => r.ssmManaged === true);
1248
- out.sort((a, b) => [a.profile || "", a.accountId || "", a.region || "", a.instanceId || ""].join("|").localeCompare([b.profile || "", b.accountId || "", b.region || "", b.instanceId || ""].join("|")));
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", "runtimeSnapshotStatus",
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 CLI/env parameters");
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, "ssm_readiness_planner: check remediation and snapshot options");
1325
- if (config.autoRemediateSsm) {
1326
- eprint(`Remediation target: ${config.ssmInstanceProfileArn || config.ssmInstanceProfileName} (replace=${config.allowReplaceProfile ? "on" : "off"})`);
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
- progress(config, 5, "ec2_inventory_collector: collect EC2 + SSM inventory");
1338
- const { records, stats } = await collectInventory(config, plans, warnings, requiredActions);
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, "ssm_connectivity_checker: evaluate managed/online status");
1342
- const unmanaged = records.filter((r) => !r.ssmManaged).length;
1343
- const offline = records.filter((r) => r.ssmManaged && !r.ssmOnline).length;
1344
- eprint(`SSM state: unmanaged=${unmanaged}, managed_offline=${offline}`);
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
- progress(config, 7, "runtime_snapshot_collector: collect runtime snapshots");
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, 8, "cli_output_formatter: render JSON/CSV");
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: summarize execution");
1362
- const ssmManaged = outputRecords.filter((r) => r.ssmManaged).length;
1363
- const ssmOnline = outputRecords.filter((r) => r.ssmOnline).length;
1364
- const publicCount = outputRecords.filter((r) => Boolean(r.publicIp)).length;
1365
- eprint(`Summary: profiles=${stats.profiles}, regions_scanned=${stats.regions}, region_errors=${stats.regionErrors}, instances_scanned=${stats.instancesScanned}, output_records=${outputRecords.length}, public_ip_records=${publicCount}, ssm_managed=${ssmManaged}, ssm_online=${ssmOnline}, remediation_attempts=${stats.remediationAttempts}, remediation_changed=${stats.remediationChanged}, runtime_snapshot_attempted=${snapshotStats.attempted}, runtime_snapshot_succeeded=${snapshotStats.succeeded}, warnings=${warnings.length}, required_actions=${requiredActions.length}`);
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) {