infrawise 0.19.1 → 0.19.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.
@@ -47,20 +47,20 @@ function buildMarkdownReport(findings, projectName) {
47
47
  function mkSpinner(text) {
48
48
  return ora({ text: chalk.dim(text), color: 'cyan' }).start();
49
49
  }
50
- // Runs one extractor behind a spinner: succeeds with a count summary, or warns and
51
- // returns undefined on failure so a single service never aborts the whole analysis.
52
- async function extract(enabled, spinnerText, label, fn, summarize) {
50
+ // Runs one extractor, printing a static completion line (never a spinner, so many
51
+ // can run concurrently without corrupting each other's output). Warns and returns
52
+ // undefined on failure so a single service never aborts the whole analysis, and
53
+ // never rejects — safe to pass straight into Promise.all.
54
+ async function extract(enabled, label, fn, summarize) {
53
55
  if (!enabled)
54
56
  return undefined;
55
- const s = mkSpinner(spinnerText);
56
57
  try {
57
58
  const result = await fn();
58
- s.succeed(chalk.green(label) + chalk.dim(` ${summarize(result)}`));
59
+ log.success(label, summarize(result));
59
60
  return result;
60
61
  }
61
62
  catch (err) {
62
- s.warn(chalk.yellow(`${label} skipped`) +
63
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
63
+ log.warn(`${label} skipped`, err instanceof Error ? err.message : String(err));
64
64
  return undefined;
65
65
  }
66
66
  }
@@ -153,32 +153,106 @@ export async function runAnalyze(options = {}) {
153
153
  profile: config.aws?.profile,
154
154
  };
155
155
  const servicesMeta = {};
156
- const dynamoMeta = (await extract(config.dynamodb?.enabled === true, 'Extracting DynamoDB tables...', 'DynamoDB', () => extractDynamoMetadata(config), (r) => `${r.length} table(s)`)) ?? [];
157
156
  const pgConn = config.postgres?.enabled ? config.postgres.connectionString : undefined;
158
- const postgresMeta = (await extract(!!pgConn, 'Extracting PostgreSQL schema...', 'PostgreSQL', () => extractPostgresMetadata(pgConn ?? ''), (r) => `${r.length} table(s)`)) ?? [];
159
157
  const mysqlConn = config.mysql?.enabled ? config.mysql.connectionString : undefined;
160
- const mysqlMeta = (await extract(!!mysqlConn, 'Extracting MySQL schema...', 'MySQL', () => extractMySQLMetadata(mysqlConn ?? ''), (r) => `${r.length} table(s)`)) ?? [];
161
158
  const mongoConn = config.mongodb?.enabled ? config.mongodb.connectionString : undefined;
162
- const mongoMeta = (await extract(!!mongoConn, 'Extracting MongoDB schema...', 'MongoDB', () => extractMongoMetadata(mongoConn ?? '', config.mongodb?.databases), (r) => `${r.length} collection(s)`)) ?? [];
163
- servicesMeta.sqs = await extract(config.sqs?.enabled === true, 'Extracting SQS queues...', 'SQS', () => extractSQSMetadata(awsCfg), (r) => `${r.length} queue(s)`);
164
- servicesMeta.sns = await extract(config.sns?.enabled === true, 'Extracting SNS topics...', 'SNS', () => extractSNSMetadata(awsCfg), (r) => `${r.length} topic(s)`);
165
- servicesMeta.ssm = await extract(config.ssm?.enabled === true, 'Extracting SSM parameters...', 'SSM', () => extractSSMMetadata({ ...awsCfg, paths: config.ssm?.paths }), (r) => `${r.length} parameter(s) (metadata only, no values)`);
166
- servicesMeta.secrets = await extract(config.secretsManager?.enabled === true, 'Extracting Secrets Manager metadata...', 'Secrets Manager', () => extractSecretsMetadata(awsCfg), (r) => `${r.length} secret(s) (names/rotation only, no values)`);
167
- servicesMeta.lambda = await extract(config.lambda?.enabled === true, 'Extracting Lambda functions...', 'Lambda', () => extractLambdaMetadata(awsCfg, config.lambda?.includeFunctions), (r) => `${r.length} function(s)`);
168
- servicesMeta.eventbridge = await extract(config.eventbridge?.enabled === true, 'Extracting EventBridge rules...', 'EventBridge', () => extractEventBridgeMetadata(awsCfg), (r) => `${r.length} rule(s)`);
169
- servicesMeta.rds = await extract(config.rds?.enabled === true, 'Extracting RDS instances...', 'RDS', () => extractRDSMetadata(awsCfg), (r) => `${r.length} instance(s)`);
170
- servicesMeta.s3 = await extract(config.s3?.enabled === true, 'Extracting S3 buckets...', 'S3', () => extractS3Metadata(awsCfg), (r) => `${r.length} bucket(s)`);
171
- 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)`);
172
- servicesMeta.cognito = await extract(config.cognito?.enabled === true, 'Extracting Cognito user pools...', 'Cognito', () => extractCognitoMetadata(awsCfg), (r) => `${r.length} user pool(s)`);
173
- servicesMeta.kinesis = await extract(config.kinesis?.enabled === true, 'Extracting Kinesis streams...', 'Kinesis', () => extractKinesisMetadata(awsCfg), (r) => `${r.length} stream(s)`);
174
- servicesMeta.msk = await extract(config.msk?.enabled === true, 'Extracting MSK clusters...', 'MSK', () => extractMSKMetadata(awsCfg), (r) => `${r.length} cluster(s)`);
175
- servicesMeta.elasticache = await extract(config.elasticache?.enabled === true, 'Extracting ElastiCache clusters...', 'ElastiCache', () => extractElastiCacheMetadata(awsCfg), (r) => `${r.length} cluster(s)`);
176
159
  const cwLogs = config.cloudwatchLogs;
177
- servicesMeta.logs = await extract(cwLogs?.enabled, 'Sampling CloudWatch Logs (errors only, max 50 groups)...', 'CloudWatch Logs', () => extractLogsMetadata({
178
- ...awsCfg,
179
- logGroupPrefixes: cwLogs?.logGroupPrefixes,
180
- windowHours: cwLogs?.windowHours,
181
- }), (r) => `${r.length} group(s), ${r.filter((lg) => lg.errorCount > 0).length} with errors`);
160
+ // IaC schema and the repo scan are local work (no AWS calls) run them
161
+ // concurrently with the AWS/DB extractors so wall-clock is bounded by the
162
+ // slowest single task, not the sum of all of them. Each task catches its own
163
+ // errors and never rejects, so Promise.all below can't be aborted by one.
164
+ const iacTask = (async () => {
165
+ try {
166
+ const iacSchema = await extractIaCSchema(repoPath);
167
+ const total = iacSchema.dynamoTables.length +
168
+ iacSchema.rdsInstances.length +
169
+ iacSchema.mongoClusters.length +
170
+ iacSchema.queues.length +
171
+ iacSchema.topics.length +
172
+ iacSchema.lambdas.length +
173
+ iacSchema.buckets.length +
174
+ iacSchema.parameters.length +
175
+ iacSchema.secrets.length +
176
+ iacSchema.apiGateways.length +
177
+ iacSchema.outputs.length;
178
+ const drift = new IaCDriftAnalyzer();
179
+ drift.setIaCSchema(iacSchema);
180
+ log.success('IaC schema', `${total} resource(s) across TF/CFN/CDK`);
181
+ return {
182
+ drift: drift,
183
+ lambdas: iacSchema.lambdas,
184
+ outputs: iacSchema.outputs,
185
+ };
186
+ }
187
+ catch (err) {
188
+ log.warn('IaC scan skipped', err instanceof Error ? err.message : String(err));
189
+ return {
190
+ drift: undefined,
191
+ lambdas: [],
192
+ outputs: [],
193
+ };
194
+ }
195
+ })();
196
+ const repoTask = (async () => {
197
+ try {
198
+ const ops = await scanRepository(repoPath);
199
+ log.success('Repository scanned', `${ops.length} service operation(s) found`);
200
+ return ops;
201
+ }
202
+ catch (err) {
203
+ log.warn('Repository scan failed', err instanceof Error ? err.message : String(err));
204
+ return [];
205
+ }
206
+ })();
207
+ if (!options.silent)
208
+ log.info('Extracting infrastructure in parallel...');
209
+ const [dynamoRes, postgresRes, mysqlRes, mongoRes, sqsRes, snsRes, ssmRes, secretsRes, lambdaRes, eventbridgeRes, rdsRes, s3Res, apiGatewayRes, cognitoRes, kinesisRes, mskRes, elasticacheRes, logsRes, iac, operations,] = await Promise.all([
210
+ extract(config.dynamodb?.enabled === true, 'DynamoDB', () => extractDynamoMetadata(config), (r) => `${r.length} table(s)`),
211
+ extract(!!pgConn, 'PostgreSQL', () => extractPostgresMetadata(pgConn ?? ''), (r) => `${r.length} table(s)`),
212
+ extract(!!mysqlConn, 'MySQL', () => extractMySQLMetadata(mysqlConn ?? ''), (r) => `${r.length} table(s)`),
213
+ extract(!!mongoConn, 'MongoDB', () => extractMongoMetadata(mongoConn ?? '', config.mongodb?.databases), (r) => `${r.length} collection(s)`),
214
+ extract(config.sqs?.enabled === true, 'SQS', () => extractSQSMetadata(awsCfg), (r) => `${r.length} queue(s)`),
215
+ extract(config.sns?.enabled === true, 'SNS', () => extractSNSMetadata(awsCfg), (r) => `${r.length} topic(s)`),
216
+ extract(config.ssm?.enabled === true, 'SSM', () => extractSSMMetadata({ ...awsCfg, paths: config.ssm?.paths }), (r) => `${r.length} parameter(s) (metadata only, no values)`),
217
+ extract(config.secretsManager?.enabled === true, 'Secrets Manager', () => extractSecretsMetadata(awsCfg), (r) => `${r.length} secret(s) (names/rotation only, no values)`),
218
+ extract(config.lambda?.enabled === true, 'Lambda', () => extractLambdaMetadata(awsCfg, config.lambda?.includeFunctions), (r) => `${r.length} function(s)`),
219
+ extract(config.eventbridge?.enabled === true, 'EventBridge', () => extractEventBridgeMetadata(awsCfg), (r) => `${r.length} rule(s)`),
220
+ extract(config.rds?.enabled === true, 'RDS', () => extractRDSMetadata(awsCfg), (r) => `${r.length} instance(s)`),
221
+ extract(config.s3?.enabled === true, 'S3', () => extractS3Metadata(awsCfg), (r) => `${r.length} bucket(s)`),
222
+ extract(config.apiGateway?.enabled === true, 'API Gateway', () => extractAPIGatewayMetadata(awsCfg), (r) => `${r.length} API(s), ${r.reduce((sum, api) => sum + api.routes.length, 0)} route(s)`),
223
+ extract(config.cognito?.enabled === true, 'Cognito', () => extractCognitoMetadata(awsCfg), (r) => `${r.length} user pool(s)`),
224
+ extract(config.kinesis?.enabled === true, 'Kinesis', () => extractKinesisMetadata(awsCfg), (r) => `${r.length} stream(s)`),
225
+ extract(config.msk?.enabled === true, 'MSK', () => extractMSKMetadata(awsCfg), (r) => `${r.length} cluster(s)`),
226
+ extract(config.elasticache?.enabled === true, 'ElastiCache', () => extractElastiCacheMetadata(awsCfg), (r) => `${r.length} cluster(s)`),
227
+ extract(cwLogs?.enabled, 'CloudWatch Logs', () => extractLogsMetadata({
228
+ ...awsCfg,
229
+ logGroupPrefixes: cwLogs?.logGroupPrefixes,
230
+ windowHours: cwLogs?.windowHours,
231
+ }), (r) => `${r.length} group(s), ${r.filter((lg) => lg.errorCount > 0).length} with errors`),
232
+ iacTask,
233
+ repoTask,
234
+ ]);
235
+ const dynamoMeta = dynamoRes ?? [];
236
+ const postgresMeta = postgresRes ?? [];
237
+ const mysqlMeta = mysqlRes ?? [];
238
+ const mongoMeta = mongoRes ?? [];
239
+ servicesMeta.sqs = sqsRes;
240
+ servicesMeta.sns = snsRes;
241
+ servicesMeta.ssm = ssmRes;
242
+ servicesMeta.secrets = secretsRes;
243
+ servicesMeta.lambda = lambdaRes;
244
+ servicesMeta.eventbridge = eventbridgeRes;
245
+ servicesMeta.rds = rdsRes;
246
+ servicesMeta.s3 = s3Res;
247
+ servicesMeta.apiGateway = apiGatewayRes;
248
+ servicesMeta.cognito = cognitoRes;
249
+ servicesMeta.kinesis = kinesisRes;
250
+ servicesMeta.msk = mskRes;
251
+ servicesMeta.elasticache = elasticacheRes;
252
+ servicesMeta.logs = logsRes;
253
+ const iacDriftAnalyzer = iac.drift;
254
+ const iacLambdas = iac.lambdas;
255
+ const iacOutputs = iac.outputs;
182
256
  if (config.runtimeSignals?.enabled && (servicesMeta.lambda?.length || servicesMeta.sqs?.length)) {
183
257
  const s = mkSpinner('Fetching runtime signals (CloudWatch metrics)...');
184
258
  try {
@@ -203,51 +277,6 @@ export async function runAnalyze(options = {}) {
203
277
  chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
204
278
  }
205
279
  }
206
- // ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
207
- let iacDriftAnalyzer;
208
- let iacLambdas = [];
209
- let iacOutputs = [];
210
- {
211
- const s = mkSpinner('Extracting IaC schema (Terraform / CloudFormation / CDK)...');
212
- try {
213
- const iacSchema = await extractIaCSchema(repoPath);
214
- const total = iacSchema.dynamoTables.length +
215
- iacSchema.rdsInstances.length +
216
- iacSchema.mongoClusters.length +
217
- iacSchema.queues.length +
218
- iacSchema.topics.length +
219
- iacSchema.lambdas.length +
220
- iacSchema.buckets.length +
221
- iacSchema.parameters.length +
222
- iacSchema.secrets.length +
223
- iacSchema.apiGateways.length +
224
- iacSchema.outputs.length;
225
- iacDriftAnalyzer = new IaCDriftAnalyzer();
226
- iacDriftAnalyzer.setIaCSchema(iacSchema);
227
- iacLambdas = iacSchema.lambdas;
228
- iacOutputs = iacSchema.outputs;
229
- s.succeed(chalk.green('IaC schema') + chalk.dim(` ${total} resource(s) across TF/CFN/CDK`));
230
- }
231
- catch (err) {
232
- s.warn(chalk.yellow('IaC scan skipped') +
233
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
234
- }
235
- }
236
- // ── Repository scan ──────────────────────────────────────────────────────────
237
- let operations;
238
- {
239
- const s = mkSpinner(`Scanning ${path.basename(repoPath)} for service usage...`);
240
- try {
241
- operations = await scanRepository(repoPath);
242
- s.succeed(chalk.green('Repository scanned') +
243
- chalk.dim(` ${operations.length} service operation(s) found`));
244
- }
245
- catch (err) {
246
- s.warn(chalk.yellow('Repository scan failed') +
247
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
248
- operations = [];
249
- }
250
- }
251
280
  // ── Build graph ──────────────────────────────────────────────────────────────
252
281
  let graph;
253
282
  {
@@ -18,7 +18,7 @@ export function writeCache(key, data) {
18
18
  version: CACHE_VERSION,
19
19
  };
20
20
  const filePath = path.join(cacheDir, `${key}.json`);
21
- fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), 'utf-8');
21
+ fs.writeFileSync(filePath, JSON.stringify(entry), 'utf-8');
22
22
  }
23
23
  export function readCache(key, maxAgeMs = 3600000) {
24
24
  const filePath = path.join(cacheDir, `${key}.json`);
@@ -151,6 +151,7 @@ export function createMcpServer() {
151
151
  });
152
152
  }
153
153
  const outEdges = funcNode ? getOutgoingEdges(currentGraph, funcNode.id) : [];
154
+ const nodeMap = new Map(currentGraph.nodes.map((n) => [n.id, n]));
154
155
  const relatedFindings = currentFindings.filter((f) => {
155
156
  const meta = f.metadata;
156
157
  return (meta?.functionName === functionName ||
@@ -161,7 +162,6 @@ export function createMcpServer() {
161
162
  const allowedServices = lambdaNode?.type === 'lambda' ? lambdaNode.allowedServices : undefined;
162
163
  let missingPermissions;
163
164
  if (allowedServices && !allowedServices.includes('*') && funcNode) {
164
- const nodeMap = new Map(currentGraph.nodes.map((n) => [n.id, n]));
165
165
  const needed = new Set();
166
166
  for (const edge of outEdges) {
167
167
  const target = nodeMap.get(edge.to);
@@ -193,7 +193,7 @@ export function createMcpServer() {
193
193
  ...(t.ruleName ? { ruleName: t.ruleName, eventPattern: t.eventPattern } : {}),
194
194
  })),
195
195
  accesses: outEdges.map((e) => {
196
- const target = currentGraph.nodes.find((n) => n.id === e.to);
196
+ const target = nodeMap.get(e.to);
197
197
  return {
198
198
  targetId: e.to,
199
199
  edgeType: e.type,
@@ -420,6 +420,7 @@ export function createMcpServer() {
420
420
  inputSchema: z.object({}),
421
421
  }, logged('get_eventbridge_details', async () => {
422
422
  const rules = getEventBridgeRuleNodes(currentGraph);
423
+ const nodeMap = new Map(currentGraph.nodes.map((n) => [n.id, n]));
423
424
  return toText({
424
425
  total: rules.length,
425
426
  rules: rules.map((r) => ({
@@ -429,7 +430,7 @@ export function createMcpServer() {
429
430
  eventPattern: r.eventPattern,
430
431
  targets: currentGraph.edges
431
432
  .filter((e) => e.from === r.id && e.type === 'triggers')
432
- .map((e) => currentGraph.nodes.find((n) => n.id === e.to))
433
+ .map((e) => nodeMap.get(e.to))
433
434
  .filter(Boolean)
434
435
  .map((n) => (n && 'name' in n ? n.name : '')),
435
436
  })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "mcpName": "io.github.Sidd27/infrawise",
5
5
  "description": "CLI-first infrastructure intelligence platform — analyzes AWS (DynamoDB, Lambda, SQS, SNS, SSM, Secrets Manager, EventBridge, RDS, API Gateway, S3, CloudWatch, Cognito, Kinesis, MSK, ElastiCache), databases (PostgreSQL, MySQL, MongoDB), Apache Kafka, and IaC (Terraform, CDK, CloudFormation), and exposes findings as an MCP server for Claude Code and other AI coding assistants",
6
6
  "keywords": [