infrawise 0.13.2 → 0.15.0

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.
@@ -14,6 +14,7 @@ function emptySchema() {
14
14
  parameters: [],
15
15
  secrets: [],
16
16
  apiGateways: [],
17
+ outputs: [],
17
18
  };
18
19
  }
19
20
  // ─── File discovery ───────────────────────────────────────────────────────────
@@ -76,6 +77,25 @@ function tfGSINames(body) {
76
77
  }
77
78
  return names;
78
79
  }
80
+ function extractTerraformOutputBlocks(content) {
81
+ const results = [];
82
+ const pat = /output\s+"([^"]+)"\s*\{/g;
83
+ let match;
84
+ while ((match = pat.exec(content)) !== null) {
85
+ const startBrace = match.index + match[0].length - 1;
86
+ let depth = 1;
87
+ let i = startBrace + 1;
88
+ while (i < content.length && depth > 0) {
89
+ if (content[i] === '{')
90
+ depth++;
91
+ else if (content[i] === '}')
92
+ depth--;
93
+ i++;
94
+ }
95
+ results.push({ name: match[1] ?? '', body: content.slice(startBrace + 1, i - 1) });
96
+ }
97
+ return results;
98
+ }
79
99
  export async function extractTerraformSchema(repoPath) {
80
100
  const schema = emptySchema();
81
101
  const tfFiles = findFilesRecursively(repoPath, ['.tf']);
@@ -173,6 +193,16 @@ export async function extractTerraformSchema(repoPath) {
173
193
  break;
174
194
  }
175
195
  }
196
+ for (const { name, body } of extractTerraformOutputBlocks(content)) {
197
+ const valueMatch = body.match(/value\s*=\s*(.+)/);
198
+ schema.outputs.push({
199
+ name,
200
+ description: tfStr(body, 'description'),
201
+ value: valueMatch?.[1]?.trim(),
202
+ source: 'terraform',
203
+ filePath,
204
+ });
205
+ }
176
206
  }
177
207
  return schema;
178
208
  }
@@ -341,6 +371,30 @@ function processCFNResources(resources, schema, filePath, source) {
341
371
  }
342
372
  }
343
373
  }
374
+ function processCFNOutputs(template, schema, filePath, source) {
375
+ const outputs = template['Outputs'];
376
+ if (typeof outputs !== 'object' || outputs === null)
377
+ return;
378
+ for (const [name, raw] of Object.entries(outputs)) {
379
+ if (typeof raw !== 'object' || raw === null)
380
+ continue;
381
+ const out = raw;
382
+ const exp = out['Export'];
383
+ const expName = exp?.['Name'];
384
+ schema.outputs.push({
385
+ name,
386
+ description: typeof out['Description'] === 'string' ? out['Description'] : undefined,
387
+ exportName: expName === undefined
388
+ ? undefined
389
+ : typeof expName === 'string'
390
+ ? expName
391
+ : JSON.stringify(expName),
392
+ value: typeof out['Value'] === 'string' ? out['Value'] : JSON.stringify(out['Value']),
393
+ source,
394
+ filePath,
395
+ });
396
+ }
397
+ }
344
398
  export async function extractCloudFormationSchema(repoPath) {
345
399
  const schema = emptySchema();
346
400
  const cfnFiles = findFilesRecursively(repoPath, ['.yaml', '.yml', '.json']);
@@ -353,6 +407,7 @@ export async function extractCloudFormationSchema(repoPath) {
353
407
  if (!resources)
354
408
  continue;
355
409
  processCFNResources(resources, schema, filePath, 'cloudformation');
410
+ processCFNOutputs(parsed, schema, filePath, 'cloudformation');
356
411
  }
357
412
  return schema;
358
413
  }
@@ -375,6 +430,7 @@ export async function extractCDKSchema(repoPath) {
375
430
  if (!resources)
376
431
  continue;
377
432
  processCFNResources(resources, schema, filePath, 'cdk');
433
+ processCFNOutputs(parsed, schema, filePath, 'cdk');
378
434
  }
379
435
  }
380
436
  // Strategy 2: Detect CDK TypeScript construct patterns if no cdk.out exists
@@ -400,6 +456,7 @@ function mergeSchemas(...schemas) {
400
456
  param: new Set(),
401
457
  secret: new Set(),
402
458
  api: new Set(),
459
+ output: new Set(),
403
460
  };
404
461
  for (const schema of schemas) {
405
462
  for (const t of schema.dynamoTables) {
@@ -472,6 +529,13 @@ function mergeSchemas(...schemas) {
472
529
  merged.apiGateways.push(a);
473
530
  }
474
531
  }
532
+ for (const o of schema.outputs) {
533
+ const k = `${o.source}::${o.filePath}::${o.name}`;
534
+ if (!seen.output.has(k)) {
535
+ seen.output.add(k);
536
+ merged.outputs.push(o);
537
+ }
538
+ }
475
539
  }
476
540
  return merged;
477
541
  }
@@ -491,7 +555,8 @@ export async function extractIaCSchema(repoPath) {
491
555
  merged.buckets.length +
492
556
  merged.parameters.length +
493
557
  merged.secrets.length +
494
- merged.apiGateways.length;
558
+ merged.apiGateways.length +
559
+ merged.outputs.length;
495
560
  logger.debug(`IaC schema total: ${total} resource(s) across TF/CFN/CDK`);
496
561
  return merged;
497
562
  }
@@ -284,6 +284,93 @@ export class S3UnencryptedAnalyzer {
284
284
  return findings;
285
285
  }
286
286
  }
287
+ // ─── ElastiCache ─────────────────────────────────────────────────────────────
288
+ export class CacheTransitEncryptionAnalyzer {
289
+ name = 'CacheTransitEncryptionAnalyzer';
290
+ async analyze(graph) {
291
+ const findings = [];
292
+ for (const node of graph.nodes) {
293
+ if (node.type !== 'cache_cluster')
294
+ continue;
295
+ if (node.transitEncryption === false) {
296
+ findings.push({
297
+ severity: 'medium',
298
+ issue: `Cache cluster "${node.name}" has no in-transit encryption`,
299
+ description: `ElastiCache cluster "${node.name}" (${node.engine}) does not have TLS in-transit encryption enabled. Credentials and cached data cross the network in plaintext.`,
300
+ recommendation: `Enable transit encryption on "${node.name}". Note this requires clients to connect with TLS (rediss:// for Redis).`,
301
+ metadata: { cacheClusterId: node.name, provider: node.provider },
302
+ });
303
+ }
304
+ }
305
+ return findings;
306
+ }
307
+ }
308
+ export class CacheSingleNodeAnalyzer {
309
+ name = 'CacheSingleNodeAnalyzer';
310
+ async analyze(graph) {
311
+ const findings = [];
312
+ for (const node of graph.nodes) {
313
+ if (node.type !== 'cache_cluster')
314
+ continue;
315
+ if (node.numNodes === 1 && !node.replicationGroupId) {
316
+ findings.push({
317
+ severity: 'low',
318
+ issue: `Cache cluster "${node.name}" is a single node with no replication`,
319
+ description: `"${node.name}" runs one cache node outside a replication group. A node failure loses all cached data and takes the cache offline until replaced.`,
320
+ recommendation: `Move "${node.name}" into a replication group with at least one replica and automatic failover if cache availability matters for this workload.`,
321
+ metadata: { cacheClusterId: node.name },
322
+ });
323
+ }
324
+ }
325
+ return findings;
326
+ }
327
+ }
328
+ // ─── Runtime signals ─────────────────────────────────────────────────────────
329
+ export class LambdaThrottlingAnalyzer {
330
+ name = 'LambdaThrottlingAnalyzer';
331
+ async analyze(graph) {
332
+ const findings = [];
333
+ for (const node of graph.nodes) {
334
+ if (node.type !== 'lambda')
335
+ continue;
336
+ if ((node.recentThrottles ?? 0) > 0) {
337
+ findings.push({
338
+ severity: 'high',
339
+ issue: `Lambda "${node.name}" was throttled ${node.recentThrottles} time(s) recently`,
340
+ description: `"${node.name}" hit concurrency throttling in the analysis window. Throttled invocations are rejected or delayed; for sync callers this surfaces as errors, for event sources as retries and growing backlogs.`,
341
+ recommendation: `Check reserved/account concurrency for "${node.name}". Raise reserved concurrency, request an account limit increase, or smooth the invoke rate (SQS between producer and Lambda).`,
342
+ metadata: { functionName: node.name, recentThrottles: node.recentThrottles },
343
+ });
344
+ }
345
+ }
346
+ return findings;
347
+ }
348
+ }
349
+ export class StaleQueueMessagesAnalyzer {
350
+ name = 'StaleQueueMessagesAnalyzer';
351
+ thresholdSec;
352
+ constructor(thresholdSec = 3600) {
353
+ this.thresholdSec = thresholdSec;
354
+ }
355
+ async analyze(graph) {
356
+ const findings = [];
357
+ for (const node of graph.nodes) {
358
+ if (node.type !== 'queue')
359
+ continue;
360
+ const age = node.oldestMessageAgeSec ?? 0;
361
+ if (age > this.thresholdSec) {
362
+ findings.push({
363
+ severity: 'medium',
364
+ issue: `Queue "${node.name}" has messages older than ${Math.round(age / 3600)} hour(s)`,
365
+ description: `The oldest message in "${node.name}" is ${Math.round(age / 60)} minutes old. Consumers are not keeping up, are failing, or are not running.`,
366
+ recommendation: `Check consumer health for "${node.name}". If a Lambda consumes it, look at its error and throttle counts; if messages are near the retention limit they will be silently dropped.`,
367
+ metadata: { queueName: node.name, oldestMessageAgeSec: age },
368
+ });
369
+ }
370
+ }
371
+ return findings;
372
+ }
373
+ }
287
374
  // ─── IAM ─────────────────────────────────────────────────────────────────────
288
375
  const MINIMAL_ACTIONS = {
289
376
  dynamodb: 'dynamodb:GetItem, dynamodb:PutItem, dynamodb:Query, dynamodb:UpdateItem, dynamodb:DeleteItem',
@@ -5,7 +5,7 @@ export { MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer } from './mysql.j
5
5
  export { MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer } from './mongodb.js';
6
6
  export { IaCDriftAnalyzer } from './terraform.js';
7
7
  export { PipelineAnalyzer } from './pipeline.js';
8
- export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from './aws-services.js';
8
+ export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, CacheTransitEncryptionAnalyzer, CacheSingleNodeAnalyzer, LambdaThrottlingAnalyzer, StaleQueueMessagesAnalyzer, } from './aws-services.js';
9
9
  export { RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, } from './rds.js';
10
10
  export async function runAllAnalyzers(graph, analyzers) {
11
11
  const allFindings = [];
@@ -8,12 +8,13 @@ import { extractPostgresMetadata } from '../../adapters/db/postgres.js';
8
8
  import { extractMySQLMetadata } from '../../adapters/db/mysql.js';
9
9
  import { extractMongoMetadata } from '../../adapters/db/mongodb.js';
10
10
  import { extractIaCSchema } from '../../adapters/iac/terraform.js';
11
- import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractEventBridgeMetadata, extractRDSMetadata, extractAPIGatewayMetadata, } from '../../adapters/aws/services.js';
11
+ import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractEventBridgeMetadata, extractRDSMetadata, extractAPIGatewayMetadata, extractCognitoMetadata, extractKinesisMetadata, extractMSKMetadata, extractElastiCacheMetadata, } from '../../adapters/aws/services.js';
12
12
  import { extractLogsMetadata } from '../../adapters/aws/logs.js';
13
+ import { extractRuntimeSignals } from '../../adapters/aws/metrics.js';
13
14
  import { extractS3Metadata } from '../../adapters/aws/s3.js';
14
15
  import { scanRepository } from '../../context/index.js';
15
- import { buildGraph } from '../../graph/index.js';
16
- import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from '../../analyzers/index.js';
16
+ import { buildGraph, addStackOutputNodes } from '../../graph/index.js';
17
+ import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, CacheTransitEncryptionAnalyzer, CacheSingleNodeAnalyzer, LambdaThrottlingAnalyzer, StaleQueueMessagesAnalyzer, } from '../../analyzers/index.js';
17
18
  import { printFinding, printSummaryBox, log, printHeader } from '../utils.js';
18
19
  const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, verify: 0 };
19
20
  function buildMarkdownReport(findings, projectName) {
@@ -119,6 +120,12 @@ function buildAnalyzers(config, iacDriftAnalyzer, iacLambdas) {
119
120
  new S3UnencryptedAnalyzer(),
120
121
  ]
121
122
  : []),
123
+ ...(config.elasticache?.enabled === true
124
+ ? [new CacheTransitEncryptionAnalyzer(), new CacheSingleNodeAnalyzer()]
125
+ : []),
126
+ ...(config.runtimeSignals?.enabled === true
127
+ ? [new LambdaThrottlingAnalyzer(), new StaleQueueMessagesAnalyzer()]
128
+ : []),
122
129
  ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
123
130
  pipelineAnalyzer,
124
131
  ];
@@ -158,15 +165,44 @@ export async function runAnalyze(options = {}) {
158
165
  servicesMeta.rds = await extract(config.rds?.enabled === true, 'Extracting RDS instances...', 'RDS', () => extractRDSMetadata(awsCfg), (r) => `${r.length} instance(s)`);
159
166
  servicesMeta.s3 = await extract(config.s3?.enabled === true, 'Extracting S3 buckets...', 'S3', () => extractS3Metadata(awsCfg), (r) => `${r.length} bucket(s)`);
160
167
  servicesMeta.apiGateway = await extract(config.apiGateway?.enabled === true, 'Extracting API Gateway routes...', 'API Gateway', () => extractAPIGatewayMetadata(awsCfg), (r) => `${r.length} API(s), ${r.reduce((sum, api) => sum + api.routes.length, 0)} route(s)`);
168
+ servicesMeta.cognito = await extract(config.cognito?.enabled === true, 'Extracting Cognito user pools...', 'Cognito', () => extractCognitoMetadata(awsCfg), (r) => `${r.length} user pool(s)`);
169
+ servicesMeta.kinesis = await extract(config.kinesis?.enabled === true, 'Extracting Kinesis streams...', 'Kinesis', () => extractKinesisMetadata(awsCfg), (r) => `${r.length} stream(s)`);
170
+ servicesMeta.msk = await extract(config.msk?.enabled === true, 'Extracting MSK clusters...', 'MSK', () => extractMSKMetadata(awsCfg), (r) => `${r.length} cluster(s)`);
171
+ servicesMeta.elasticache = await extract(config.elasticache?.enabled === true, 'Extracting ElastiCache clusters...', 'ElastiCache', () => extractElastiCacheMetadata(awsCfg), (r) => `${r.length} cluster(s)`);
161
172
  const cwLogs = config.cloudwatchLogs;
162
173
  servicesMeta.logs = await extract(cwLogs?.enabled, 'Sampling CloudWatch Logs (errors only, max 50 groups)...', 'CloudWatch Logs', () => extractLogsMetadata({
163
174
  ...awsCfg,
164
175
  logGroupPrefixes: cwLogs?.logGroupPrefixes,
165
176
  windowHours: cwLogs?.windowHours,
166
177
  }), (r) => `${r.length} group(s), ${r.filter((lg) => lg.errorCount > 0).length} with errors`);
178
+ if (config.runtimeSignals?.enabled && (servicesMeta.lambda?.length || servicesMeta.sqs?.length)) {
179
+ const s = mkSpinner('Fetching runtime signals (CloudWatch metrics)...');
180
+ try {
181
+ const signals = await extractRuntimeSignals(awsCfg, (servicesMeta.lambda ?? []).map((f) => f.name), (servicesMeta.sqs ?? []).map((q) => q.name), config.runtimeSignals.windowHours);
182
+ for (const fn of servicesMeta.lambda ?? []) {
183
+ const sig = signals.lambdas.get(fn.name);
184
+ if (sig) {
185
+ fn.recentThrottles = sig.throttles;
186
+ fn.recentErrors = sig.errors;
187
+ }
188
+ }
189
+ for (const q of servicesMeta.sqs ?? []) {
190
+ const sig = signals.queues.get(q.name);
191
+ if (sig)
192
+ q.oldestMessageAgeSec = sig.oldestMessageAgeSec;
193
+ }
194
+ s.succeed(chalk.green('Runtime signals') +
195
+ chalk.dim(` ${signals.lambdas.size} function(s), ${signals.queues.size} queue(s)`));
196
+ }
197
+ catch (err) {
198
+ s.warn(chalk.yellow('Runtime signals skipped') +
199
+ chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
200
+ }
201
+ }
167
202
  // ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
168
203
  let iacDriftAnalyzer;
169
204
  let iacLambdas = [];
205
+ let iacOutputs = [];
170
206
  {
171
207
  const s = mkSpinner('Extracting IaC schema (Terraform / CloudFormation / CDK)...');
172
208
  try {
@@ -180,10 +216,12 @@ export async function runAnalyze(options = {}) {
180
216
  iacSchema.buckets.length +
181
217
  iacSchema.parameters.length +
182
218
  iacSchema.secrets.length +
183
- iacSchema.apiGateways.length;
219
+ iacSchema.apiGateways.length +
220
+ iacSchema.outputs.length;
184
221
  iacDriftAnalyzer = new IaCDriftAnalyzer();
185
222
  iacDriftAnalyzer.setIaCSchema(iacSchema);
186
223
  iacLambdas = iacSchema.lambdas;
224
+ iacOutputs = iacSchema.outputs;
187
225
  s.succeed(chalk.green('IaC schema') + chalk.dim(` ${total} resource(s) across TF/CFN/CDK`));
188
226
  }
189
227
  catch (err) {
@@ -211,6 +249,7 @@ export async function runAnalyze(options = {}) {
211
249
  {
212
250
  const s = mkSpinner('Building infrastructure graph...');
213
251
  graph = buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
252
+ addStackOutputNodes(graph, iacOutputs);
214
253
  s.succeed(chalk.green('Graph built') +
215
254
  chalk.dim(` ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
216
255
  }
@@ -278,11 +317,13 @@ export async function runCodeRefresh(repoPath, config) {
278
317
  // Re-run IaC schema (pure file scan, no AWS calls)
279
318
  let iacDriftAnalyzer;
280
319
  let iacLambdas = [];
320
+ let iacOutputs = [];
281
321
  try {
282
322
  const iacSchema = await extractIaCSchema(repoPath);
283
323
  iacDriftAnalyzer = new IaCDriftAnalyzer();
284
324
  iacDriftAnalyzer.setIaCSchema(iacSchema);
285
325
  iacLambdas = iacSchema.lambdas;
326
+ iacOutputs = iacSchema.outputs;
286
327
  }
287
328
  catch {
288
329
  // IaC scan is best-effort
@@ -296,6 +337,7 @@ export async function runCodeRefresh(repoPath, config) {
296
337
  operations = [];
297
338
  }
298
339
  const graph = buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
340
+ addStackOutputNodes(graph, iacOutputs);
299
341
  const analyzers = buildAnalyzers(config, iacDriftAnalyzer, iacLambdas);
300
342
  const findings = await runAllAnalyzers(graph, analyzers);
301
343
  writeCache('graph', graph);
@@ -186,6 +186,11 @@ function writeYaml(cwd, opts) {
186
186
  rds: { enabled: false },
187
187
  s3: { enabled: true },
188
188
  apiGateway: { enabled: true },
189
+ cognito: { enabled: false },
190
+ kinesis: { enabled: false },
191
+ msk: { enabled: false },
192
+ elasticache: { enabled: false },
193
+ runtimeSignals: { enabled: false, windowHours: 24 },
189
194
  cloudwatchLogs: { enabled: false, logGroupPrefixes: [], windowHours: 24 },
190
195
  analysis: { sampleSize: 100 },
191
196
  };
@@ -11,6 +11,7 @@ const BOX_W = 52;
11
11
  const TOOL_MAP = [
12
12
  { name: 'get_infra_overview' },
13
13
  { name: 'get_graph_summary' },
14
+ { name: 'get_table_schema' },
14
15
  { name: 'analyze_function' },
15
16
  { name: 'suggest_gsi', service: 'dynamodb' },
16
17
  { name: 'postgres_index_suggestions', service: 'postgres' },
@@ -25,6 +26,10 @@ const TOOL_MAP = [
25
26
  { name: 'get_s3_overview', service: 's3' },
26
27
  { name: 'get_api_routes', service: 'apiGateway' },
27
28
  { name: 'get_log_errors', service: 'cloudwatchLogs' },
29
+ { name: 'get_stack_outputs', service: 'terraform' },
30
+ { name: 'get_cognito_overview', service: 'cognito' },
31
+ { name: 'get_stream_details', service: 'kinesis' },
32
+ { name: 'get_cache_overview', service: 'elasticache' },
28
33
  ];
29
34
  // With no config every registered tool is shown as active (the server exposes
30
35
  // all of them); a config narrows the list to its enabled services.
@@ -57,6 +57,16 @@ export const InfrawiseConfigSchema = z.object({
57
57
  rds: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
58
58
  s3: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
59
59
  apiGateway: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
60
+ cognito: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
61
+ kinesis: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
62
+ msk: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
63
+ elasticache: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
64
+ runtimeSignals: z
65
+ .object({
66
+ enabled: z.boolean().optional().default(false),
67
+ windowHours: z.number().int().positive().optional().default(24),
68
+ })
69
+ .optional(),
60
70
  cloudwatchLogs: z
61
71
  .object({
62
72
  enabled: z.boolean().optional().default(false),
@@ -185,6 +195,14 @@ export function generateDefaultConfig(projectName, options) {
185
195
  rds: { enabled: options?.rds?.enabled ?? false },
186
196
  s3: { enabled: options?.s3?.enabled ?? false },
187
197
  apiGateway: { enabled: options?.apiGateway?.enabled ?? false },
198
+ cognito: { enabled: options?.cognito?.enabled ?? false },
199
+ kinesis: { enabled: options?.kinesis?.enabled ?? false },
200
+ msk: { enabled: options?.msk?.enabled ?? false },
201
+ elasticache: { enabled: options?.elasticache?.enabled ?? false },
202
+ runtimeSignals: {
203
+ enabled: options?.runtimeSignals?.enabled ?? false,
204
+ windowHours: options?.runtimeSignals?.windowHours ?? 24,
205
+ },
188
206
  cloudwatchLogs: {
189
207
  enabled: options?.cloudwatchLogs?.enabled ?? false,
190
208
  logGroupPrefixes: options?.cloudwatchLogs?.logGroupPrefixes ?? [],
@@ -13,7 +13,14 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
13
13
  // ── Database tables ──────────────────────────────────────────────────────
14
14
  for (const table of dynamoMeta) {
15
15
  const nodeId = `table:dynamo:${table.tableName}`;
16
- addNode({ id: nodeId, type: 'table', name: table.tableName, databaseType: 'dynamodb' });
16
+ addNode({
17
+ id: nodeId,
18
+ type: 'table',
19
+ name: table.tableName,
20
+ databaseType: 'dynamodb',
21
+ partitionKey: table.partitionKey,
22
+ sortKey: table.sortKey,
23
+ });
17
24
  for (const indexName of table.indexes) {
18
25
  const indexNodeId = `index:${table.tableName}:${indexName}`;
19
26
  addNode({ id: indexNodeId, type: 'index', name: indexName });
@@ -27,6 +34,9 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
27
34
  type: 'table',
28
35
  name: `${table.schema}.${table.table}`,
29
36
  databaseType: 'postgres',
37
+ columns: table.columnDetails,
38
+ primaryKeys: table.primaryKeys,
39
+ foreignKeys: table.foreignKeys,
30
40
  });
31
41
  for (const indexName of table.indexes) {
32
42
  const indexNodeId = `index:${table.schema}.${table.table}:${indexName}`;
@@ -41,6 +51,9 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
41
51
  type: 'table',
42
52
  name: `${table.schema}.${table.table}`,
43
53
  databaseType: 'mysql',
54
+ columns: table.columnDetails,
55
+ primaryKeys: table.primaryKeys,
56
+ foreignKeys: table.foreignKeys,
44
57
  });
45
58
  for (const indexName of table.indexes) {
46
59
  const indexNodeId = `index:${table.schema}.${table.table}:${indexName}`;
@@ -55,6 +68,7 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
55
68
  type: 'table',
56
69
  name: `${coll.database}.${coll.collection}`,
57
70
  databaseType: 'mongodb',
71
+ estimatedCount: coll.estimatedCount,
58
72
  });
59
73
  for (const idx of coll.indexes) {
60
74
  if (idx.name === '_id_')
@@ -77,6 +91,32 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
77
91
  visibilityTimeoutSec: q.visibilityTimeoutSec,
78
92
  approximateMessages: q.approximateMessages,
79
93
  retentionDays: q.retentionDays,
94
+ oldestMessageAgeSec: q.oldestMessageAgeSec,
95
+ });
96
+ }
97
+ for (const s of servicesMeta.kinesis ?? []) {
98
+ addNode({
99
+ id: `stream:aws:${s.name}`,
100
+ type: 'stream',
101
+ name: s.name,
102
+ provider: 'aws',
103
+ status: s.status,
104
+ shardCount: s.shardCount,
105
+ retentionHours: s.retentionHours,
106
+ encrypted: s.encrypted,
107
+ mode: s.mode,
108
+ });
109
+ }
110
+ for (const c of servicesMeta.msk ?? []) {
111
+ addNode({
112
+ id: `kafka_cluster:aws:${c.name}`,
113
+ type: 'kafka_cluster',
114
+ name: c.name,
115
+ provider: 'aws',
116
+ state: c.state,
117
+ clusterType: c.clusterType,
118
+ kafkaVersion: c.kafkaVersion,
119
+ brokerNodes: c.brokerNodes,
80
120
  });
81
121
  }
82
122
  for (const api of servicesMeta.apiGateway ?? []) {
@@ -150,6 +190,8 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
150
190
  triggers: fn.triggers,
151
191
  roleArn: fn.roleArn,
152
192
  allowedServices: fn.allowedServices,
193
+ recentThrottles: fn.recentThrottles,
194
+ recentErrors: fn.recentErrors,
153
195
  });
154
196
  // Add trigger edges from source → lambda (only for enabled/active mappings)
155
197
  for (const trigger of fn.triggers ?? []) {
@@ -177,15 +219,8 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
177
219
  });
178
220
  }
179
221
  else if (trigger.type === 'kinesis') {
180
- sourceId = `queue:aws:${trigger.sourceName}`;
181
- addNode({
182
- id: sourceId,
183
- type: 'queue',
184
- name: trigger.sourceName,
185
- provider: 'aws',
186
- hasDLQ: false,
187
- encrypted: false,
188
- });
222
+ sourceId = `stream:aws:${trigger.sourceName}`;
223
+ addNode({ id: sourceId, type: 'stream', name: trigger.sourceName, provider: 'aws' });
189
224
  }
190
225
  else {
191
226
  continue;
@@ -271,6 +306,33 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
271
306
  }
272
307
  }
273
308
  }
309
+ for (const c of servicesMeta.elasticache ?? []) {
310
+ addNode({
311
+ id: `cache_cluster:aws:${c.id}`,
312
+ type: 'cache_cluster',
313
+ name: c.id,
314
+ provider: 'aws',
315
+ engine: c.engine,
316
+ engineVersion: c.engineVersion,
317
+ nodeType: c.nodeType,
318
+ numNodes: c.numNodes,
319
+ transitEncryption: c.transitEncryption,
320
+ atRestEncryption: c.atRestEncryption,
321
+ replicationGroupId: c.replicationGroupId,
322
+ automaticFailover: c.automaticFailover,
323
+ });
324
+ }
325
+ for (const pool of servicesMeta.cognito ?? []) {
326
+ addNode({
327
+ id: `user_pool:aws:${pool.id}`,
328
+ type: 'user_pool',
329
+ name: pool.name,
330
+ provider: 'aws',
331
+ poolId: pool.id,
332
+ mfaConfiguration: pool.mfaConfiguration,
333
+ clients: pool.clients,
334
+ });
335
+ }
274
336
  // ── Code operations (functions + edges) ───────────────────────────────────
275
337
  for (const op of operations) {
276
338
  const funcNodeId = `function:${op.filePath}:${op.functionName}`;
@@ -360,6 +422,25 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
360
422
  }
361
423
  return { nodes, edges };
362
424
  }
425
+ export function addStackOutputNodes(graph, outputs) {
426
+ const ids = new Set(graph.nodes.map((n) => n.id));
427
+ for (const o of outputs) {
428
+ const id = `stack_output:${o.source}:${o.filePath}:${o.name}`;
429
+ if (ids.has(id))
430
+ continue;
431
+ ids.add(id);
432
+ graph.nodes.push({
433
+ id,
434
+ type: 'stack_output',
435
+ name: o.name,
436
+ description: o.description,
437
+ exportName: o.exportName,
438
+ value: o.value,
439
+ iacSource: o.source,
440
+ file: o.filePath,
441
+ });
442
+ }
443
+ }
363
444
  function resolveEdgeType(operationType) {
364
445
  const op = operationType.toLowerCase();
365
446
  if (op === 'scan' || op === 'scancommand')
@@ -385,6 +466,11 @@ export const getLambdaNodes = (g) => getNodes(g, 'lambda');
385
466
  export const getEventBridgeRuleNodes = (g) => getNodes(g, 'eventbridge_rule');
386
467
  export const getBucketNodes = (g) => getNodes(g, 'bucket');
387
468
  export const getAPINodes = (g) => getNodes(g, 'api');
469
+ export const getStackOutputNodes = (g) => getNodes(g, 'stack_output');
470
+ export const getUserPoolNodes = (g) => getNodes(g, 'user_pool');
471
+ export const getStreamNodes = (g) => getNodes(g, 'stream');
472
+ export const getKafkaClusterNodes = (g) => getNodes(g, 'kafka_cluster');
473
+ export const getCacheClusterNodes = (g) => getNodes(g, 'cache_cluster');
388
474
  export function getEdgesForNode(graph, nodeId) {
389
475
  return graph.edges.filter((e) => e.from === nodeId || e.to === nodeId);
390
476
  }