infrawise 0.10.3 → 0.12.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.
@@ -0,0 +1,166 @@
1
+ import { HeuristicLinker, IaCHandlerLinker, CompositeLinker, } from './linkers.js';
2
+ const TRANSPORT_EDGES = new Set(['publishes_to', 'triggers']);
3
+ export class PipelineAnalyzer {
4
+ name = 'PipelineAnalyzer';
5
+ iacLambdas = [];
6
+ setIaCLambdas(lambdas) {
7
+ this.iacLambdas = lambdas;
8
+ }
9
+ async analyze(graph) {
10
+ const links = new CompositeLinker(new IaCHandlerLinker(this.iacLambdas), new HeuristicLinker()).link(graph);
11
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
12
+ return [
13
+ ...detectMissingDlqHop(graph),
14
+ ...detectScanInPipeline(graph, links, nodeById),
15
+ ...detectRepeatedTableAccess(graph, links, nodeById),
16
+ ];
17
+ }
18
+ }
19
+ class UnionFind {
20
+ parent = new Map();
21
+ find(x) {
22
+ const p = this.parent.get(x);
23
+ if (p === undefined) {
24
+ this.parent.set(x, x);
25
+ return x;
26
+ }
27
+ if (p === x)
28
+ return x;
29
+ const root = this.find(p);
30
+ this.parent.set(x, root);
31
+ return root;
32
+ }
33
+ union(a, b) {
34
+ const ra = this.find(a);
35
+ const rb = this.find(b);
36
+ if (ra !== rb)
37
+ this.parent.set(ra, rb);
38
+ }
39
+ }
40
+ function buildComponents(graph, links) {
41
+ const uf = new UnionFind();
42
+ for (const e of graph.edges) {
43
+ if (TRANSPORT_EDGES.has(e.type))
44
+ uf.union(e.from, e.to);
45
+ }
46
+ for (const l of links)
47
+ uf.union(l.lambdaId, l.functionId);
48
+ const inferredRoots = new Set();
49
+ for (const l of links) {
50
+ if (l.confidence === 'inferred')
51
+ inferredRoots.add(uf.find(l.lambdaId));
52
+ }
53
+ return { uf, inferredRoots };
54
+ }
55
+ function detectMissingDlqHop(graph) {
56
+ const hasProducer = new Set();
57
+ const hasConsumer = new Set();
58
+ for (const e of graph.edges) {
59
+ if (e.type === 'publishes_to')
60
+ hasProducer.add(e.to);
61
+ if (e.type === 'triggers')
62
+ hasConsumer.add(e.from);
63
+ }
64
+ const findings = [];
65
+ for (const node of graph.nodes) {
66
+ if (node.type !== 'queue' || node.hasDLQ)
67
+ continue;
68
+ if (!hasProducer.has(node.id) || !hasConsumer.has(node.id))
69
+ continue;
70
+ findings.push({
71
+ severity: 'medium',
72
+ issue: `Queue "${node.name}" sits mid-pipeline without a Dead Letter Queue`,
73
+ description: `Queue "${node.name}" has both a producer and a downstream consumer but no DLQ. A failure in the consumer drops the message silently, breaking the pipeline with no recovery path.`,
74
+ recommendation: `Add a Dead Letter Queue to "${node.name}" with maxReceiveCount 3-5, and alert on DLQ depth so mid-pipeline failures are visible.`,
75
+ metadata: { queueName: node.name, pipelineHop: true },
76
+ });
77
+ }
78
+ return findings;
79
+ }
80
+ function detectScanInPipeline(graph, links, nodeById) {
81
+ const triggeredLambdas = new Set();
82
+ for (const e of graph.edges) {
83
+ if (e.type === 'triggers')
84
+ triggeredLambdas.add(e.to);
85
+ }
86
+ const triggeredFunctionLinks = new Map();
87
+ for (const l of links) {
88
+ if (triggeredLambdas.has(l.lambdaId))
89
+ triggeredFunctionLinks.set(l.functionId, l);
90
+ }
91
+ const findings = [];
92
+ const seen = new Set();
93
+ for (const e of graph.edges) {
94
+ if (e.type !== 'scan')
95
+ continue;
96
+ const link = triggeredFunctionLinks.get(e.from);
97
+ if (!link)
98
+ continue;
99
+ const fn = nodeById.get(e.from);
100
+ const table = nodeById.get(e.to);
101
+ const lambda = nodeById.get(link.lambdaId);
102
+ if (!fn || fn.type !== 'function' || !table || table.type !== 'table')
103
+ continue;
104
+ const dedupe = `${e.from}\0${e.to}`;
105
+ if (seen.has(dedupe))
106
+ continue;
107
+ seen.add(dedupe);
108
+ const inferred = link.confidence === 'inferred';
109
+ const lambdaName = lambda && lambda.type === 'lambda' ? lambda.name : link.lambdaId;
110
+ findings.push({
111
+ severity: inferred ? 'verify' : 'high',
112
+ issue: `Full scan runs inside event-triggered Lambda "${lambdaName}"`,
113
+ description: `Function "${fn.name}" (${fn.file}) performs a full scan on table "${table.name}" and runs as the handler for event-triggered Lambda "${lambdaName}". A scan inside an event-driven consumer executes on every invocation, multiplying read cost with traffic.` +
114
+ (inferred
115
+ ? ` The Lambda-to-code link is inferred by name match (not proven from IaC), so verify it before acting.`
116
+ : ` The Lambda-to-code link is proven from IaC handler configuration.`),
117
+ recommendation: `Replace the scan with a Query using a partition key, or add a GSI for the access pattern. Scans in per-event handlers are the highest-leverage place to fix.`,
118
+ metadata: { function: fn.name, table: table.name, lambda: lambdaName, inferred },
119
+ });
120
+ }
121
+ return findings;
122
+ }
123
+ function detectRepeatedTableAccess(graph, links, nodeById) {
124
+ const { uf, inferredRoots } = buildComponents(graph, links);
125
+ const access = new Map();
126
+ for (const e of graph.edges) {
127
+ if (e.type !== 'query' && e.type !== 'scan')
128
+ continue;
129
+ const fromNode = nodeById.get(e.from);
130
+ const toNode = nodeById.get(e.to);
131
+ if (!fromNode || fromNode.type !== 'function')
132
+ continue;
133
+ if (!toNode || toNode.type !== 'table')
134
+ continue;
135
+ const key = `${uf.find(e.from)}\0${e.to}`;
136
+ const set = access.get(key) ?? new Set();
137
+ set.add(e.from);
138
+ access.set(key, set);
139
+ }
140
+ const findings = [];
141
+ for (const [key, fns] of access) {
142
+ if (fns.size < 2)
143
+ continue;
144
+ const [root, tableId] = key.split('\0');
145
+ const table = nodeById.get(tableId);
146
+ if (!table || table.type !== 'table')
147
+ continue;
148
+ const fnNames = [...fns]
149
+ .map((id) => nodeById.get(id))
150
+ .filter((n) => !!n && n.type === 'function')
151
+ .map((n) => `${n.name} (${n.file})`)
152
+ .sort();
153
+ const inferred = inferredRoots.has(root);
154
+ findings.push({
155
+ severity: inferred ? 'verify' : 'medium',
156
+ issue: `Table "${table.name}" is accessed at multiple stages of one pipeline`,
157
+ description: `Table "${table.name}" is read by ${fns.size} functions linked in the same service pipeline: ${fnNames.join(', ')}. Re-reading the same table across pipeline stages often signals redundant work that could be passed forward in the message payload.` +
158
+ (inferred
159
+ ? ` One or more Lambda-to-code links on this pipeline are inferred by name match (not proven from IaC), so verify the chain before acting.`
160
+ : ''),
161
+ recommendation: `Confirm each access is necessary. Consider carrying the needed fields in the event payload, caching the first read, or consolidating the reads. If the duplication is intentional, no action needed.`,
162
+ metadata: { tableName: table.name, functions: fnNames, inferred },
163
+ });
164
+ }
165
+ return findings;
166
+ }
@@ -8,12 +8,12 @@ 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, } from '../../adapters/aws/services.js';
11
+ import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractEventBridgeMetadata, extractRDSMetadata, extractAPIGatewayMetadata, } from '../../adapters/aws/services.js';
12
12
  import { extractLogsMetadata } from '../../adapters/aws/logs.js';
13
13
  import { extractS3Metadata } from '../../adapters/aws/s3.js';
14
14
  import { scanRepository } from '../../context/index.js';
15
15
  import { buildGraph } from '../../graph/index.js';
16
- import { runAllAnalyzers, IaCDriftAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from '../../analyzers/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, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from '../../analyzers/index.js';
17
17
  import { printFinding, printSummaryBox, log, printHeader } from '../utils.js';
18
18
  const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, verify: 0 };
19
19
  function buildMarkdownReport(findings, projectName) {
@@ -63,7 +63,6 @@ export async function runAnalyze(options = {}) {
63
63
  const awsCfg = {
64
64
  region: config.aws?.region,
65
65
  profile: config.aws?.profile,
66
- endpoint: config.aws?.endpoint,
67
66
  };
68
67
  const dynamoMeta = [];
69
68
  const postgresMeta = [];
@@ -230,6 +229,20 @@ export async function runAnalyze(options = {}) {
230
229
  chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
231
230
  }
232
231
  }
232
+ // ── API Gateway ───────────────────────────────────────────────────────────────
233
+ if (config.apiGateway?.enabled === true) {
234
+ const s = mkSpinner('Extracting API Gateway routes...');
235
+ try {
236
+ const result = await extractAPIGatewayMetadata(awsCfg);
237
+ servicesMeta.apiGateway = result;
238
+ const routeCount = result.reduce((sum, api) => sum + api.routes.length, 0);
239
+ s.succeed(chalk.green('API Gateway') + chalk.dim(` ${result.length} API(s), ${routeCount} route(s)`));
240
+ }
241
+ catch (err) {
242
+ s.warn(chalk.yellow('API Gateway skipped') +
243
+ chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
244
+ }
245
+ }
233
246
  // ── CloudWatch Logs ──────────────────────────────────────────────────────────
234
247
  if (config.cloudwatchLogs?.enabled) {
235
248
  const s = mkSpinner('Sampling CloudWatch Logs (errors only, max 50 groups)...');
@@ -251,6 +264,7 @@ export async function runAnalyze(options = {}) {
251
264
  }
252
265
  // ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
253
266
  let iacDriftAnalyzer;
267
+ let iacLambdas = [];
254
268
  {
255
269
  const s = mkSpinner('Extracting IaC schema (Terraform / CloudFormation / CDK)...');
256
270
  try {
@@ -267,6 +281,7 @@ export async function runAnalyze(options = {}) {
267
281
  iacSchema.apiGateways.length;
268
282
  iacDriftAnalyzer = new IaCDriftAnalyzer();
269
283
  iacDriftAnalyzer.setIaCSchema(iacSchema);
284
+ iacLambdas = iacSchema.lambdas;
270
285
  s.succeed(chalk.green('IaC schema') + chalk.dim(` ${total} resource(s) across TF/CFN/CDK`));
271
286
  }
272
287
  catch (err) {
@@ -301,9 +316,17 @@ export async function runAnalyze(options = {}) {
301
316
  let findings;
302
317
  {
303
318
  const s = mkSpinner('Running analyzers...');
319
+ const hotPartitionThreshold = config.analysis?.hotPartitionThreshold;
320
+ const hotPartitionThresholds = config.analysis?.hotPartitionThresholds;
321
+ const pipelineAnalyzer = new PipelineAnalyzer();
322
+ pipelineAnalyzer.setIaCLambdas(iacLambdas);
304
323
  const analyzers = [
305
324
  ...(config.dynamodb?.enabled === true
306
- ? [new FullTableScanAnalyzer(), new MissingGSIAnalyzer(), new HotPartitionAnalyzer()]
325
+ ? [
326
+ new FullTableScanAnalyzer(),
327
+ new MissingGSIAnalyzer(),
328
+ new HotPartitionAnalyzer(hotPartitionThreshold, hotPartitionThresholds),
329
+ ]
307
330
  : []),
308
331
  ...(config.postgres?.enabled
309
332
  ? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
@@ -319,6 +342,7 @@ export async function runAnalyze(options = {}) {
319
342
  new MissingDLQAnalyzer(),
320
343
  new UnencryptedQueueAnalyzer(),
321
344
  new LargeQueueBacklogAnalyzer(),
345
+ new VisibilityTimeoutMismatchAnalyzer(),
322
346
  ]
323
347
  : []),
324
348
  ...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
@@ -347,6 +371,7 @@ export async function runAnalyze(options = {}) {
347
371
  ]
348
372
  : []),
349
373
  ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
374
+ pipelineAnalyzer,
350
375
  ];
351
376
  findings = await runAllAnalyzers(graph, analyzers);
352
377
  s.succeed(chalk.green('Analysis complete') + chalk.dim(` ${findings.length} finding(s)`));
@@ -406,10 +431,12 @@ export async function runCodeRefresh(repoPath, config) {
406
431
  const servicesMeta = cached?.servicesMeta ?? {};
407
432
  // Re-run IaC schema (pure file scan, no AWS calls)
408
433
  let iacDriftAnalyzer;
434
+ let iacLambdas = [];
409
435
  try {
410
436
  const iacSchema = await extractIaCSchema(repoPath);
411
437
  iacDriftAnalyzer = new IaCDriftAnalyzer();
412
438
  iacDriftAnalyzer.setIaCSchema(iacSchema);
439
+ iacLambdas = iacSchema.lambdas;
413
440
  }
414
441
  catch {
415
442
  // IaC scan is best-effort
@@ -423,9 +450,17 @@ export async function runCodeRefresh(repoPath, config) {
423
450
  operations = [];
424
451
  }
425
452
  const graph = buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
453
+ const hotPartitionThreshold = config.analysis?.hotPartitionThreshold;
454
+ const hotPartitionThresholds = config.analysis?.hotPartitionThresholds;
455
+ const pipelineAnalyzer = new PipelineAnalyzer();
456
+ pipelineAnalyzer.setIaCLambdas(iacLambdas);
426
457
  const analyzers = [
427
458
  ...(config.dynamodb?.enabled === true
428
- ? [new FullTableScanAnalyzer(), new MissingGSIAnalyzer(), new HotPartitionAnalyzer()]
459
+ ? [
460
+ new FullTableScanAnalyzer(),
461
+ new MissingGSIAnalyzer(),
462
+ new HotPartitionAnalyzer(hotPartitionThreshold, hotPartitionThresholds),
463
+ ]
429
464
  : []),
430
465
  ...(config.postgres?.enabled
431
466
  ? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
@@ -437,7 +472,12 @@ export async function runCodeRefresh(repoPath, config) {
437
472
  ? [new MissingMongoIndexAnalyzer(), new MongoCollectionScanAnalyzer()]
438
473
  : []),
439
474
  ...(config.sqs?.enabled === true
440
- ? [new MissingDLQAnalyzer(), new UnencryptedQueueAnalyzer(), new LargeQueueBacklogAnalyzer()]
475
+ ? [
476
+ new MissingDLQAnalyzer(),
477
+ new UnencryptedQueueAnalyzer(),
478
+ new LargeQueueBacklogAnalyzer(),
479
+ new VisibilityTimeoutMismatchAnalyzer(),
480
+ ]
441
481
  : []),
442
482
  ...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
443
483
  ...(config.cloudwatchLogs?.enabled ? [new MissingLogRetentionAnalyzer()] : []),
@@ -465,6 +505,7 @@ export async function runCodeRefresh(repoPath, config) {
465
505
  ]
466
506
  : []),
467
507
  ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
508
+ pipelineAnalyzer,
468
509
  ];
469
510
  const findings = await runAllAnalyzers(graph, analyzers);
470
511
  writeCache('graph', graph);
@@ -0,0 +1,28 @@
1
+ import * as path from 'path';
2
+ import chalk from 'chalk';
3
+ import { readCache, setCacheDir } from '../../core/index.js';
4
+ import { printFinding, log, printHeader } from '../utils.js';
5
+ import { runAnalyze } from './analyze.js';
6
+ const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, verify: 0 };
7
+ export async function runCheck(options = {}) {
8
+ const failOn = options.failOn ?? 'high';
9
+ const threshold = SEVERITY_ORDER[failOn] ?? 3;
10
+ printHeader('Infrawise Check');
11
+ // Always extract fresh — CI must not gate on a stale graph.
12
+ await runAnalyze({ config: options.config, repo: options.repo, silent: true });
13
+ setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
14
+ const findings = readCache('findings') ?? [];
15
+ const violations = findings.filter((f) => (SEVERITY_ORDER[f.severity] ?? 0) >= threshold);
16
+ console.log('');
17
+ if (violations.length === 0) {
18
+ log.success('Check passed', `no ${failOn}+ findings (${findings.length} total below threshold)`);
19
+ console.log('');
20
+ return;
21
+ }
22
+ console.log(chalk.bold(' Blocking findings') + chalk.dim(` ${violations.length} at or above ${failOn}`));
23
+ violations.forEach((f, i) => printFinding(f, i));
24
+ console.log('');
25
+ log.fail(`Check failed`, `${violations.length} ${failOn}+ finding(s) must be resolved before deploy`);
26
+ console.log('');
27
+ process.exit(1);
28
+ }
@@ -0,0 +1,201 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import * as yaml from 'js-yaml';
5
+ import chalk from 'chalk';
6
+ import inquirer from 'inquirer';
7
+ import { probePort, scanDotEnv } from '../probe.js';
8
+ import { readAWSProfiles, detectAWSRegion, log } from '../utils.js';
9
+ import { runInit } from '../interactive-setup.js';
10
+ const DB_PORTS = { postgres: 5432, mysql: 3306, mongodb: 27017 };
11
+ const ENV_KEYS = {
12
+ postgres: ['DATABASE_URL', 'POSTGRES_URL', 'POSTGRESQL_URL', 'POSTGRES_CONNECTION_STRING'],
13
+ mysql: ['MYSQL_URL', 'MYSQL_CONNECTION_STRING'],
14
+ mongodb: ['MONGO_URI', 'MONGODB_URI', 'MONGO_URL', 'MONGODB_URL'],
15
+ };
16
+ export async function runDiscover(options = {}) {
17
+ if (options.interactive) {
18
+ await runInit();
19
+ return;
20
+ }
21
+ const cwd = process.cwd();
22
+ log.dim('Probing environment...');
23
+ const [pgDetected, mysqlDetected, mongoDetected] = await Promise.all([
24
+ probePort('localhost', DB_PORTS.postgres, 300),
25
+ probePort('localhost', DB_PORTS.mysql, 300),
26
+ probePort('localhost', DB_PORTS.mongodb, 300),
27
+ ]);
28
+ if (pgDetected)
29
+ log.success('Postgres detected', `port ${DB_PORTS.postgres}`);
30
+ if (mysqlDetected)
31
+ log.success('MySQL detected', `port ${DB_PORTS.mysql}`);
32
+ if (mongoDetected)
33
+ log.success('MongoDB detected', `port ${DB_PORTS.mongodb}`);
34
+ const envVars = scanDotEnv(cwd);
35
+ const secrets = extractDbSecrets(envVars);
36
+ const iacDetected = detectIaC(cwd);
37
+ const hasAWSConfig = process.env.AWS_ACCESS_KEY_ID !== undefined ||
38
+ fs.existsSync(path.join(os.homedir(), '.aws', 'credentials')) ||
39
+ fs.existsSync(path.join(os.homedir(), '.aws', 'config'));
40
+ if (!hasAWSConfig && !pgDetected && !mysqlDetected && !mongoDetected) {
41
+ console.error(chalk.red('\n Nothing detected.'));
42
+ console.error(chalk.dim(' Run `aws configure` to set up AWS credentials, or start a local database.\n'));
43
+ process.exit(1);
44
+ }
45
+ let selectedProfile = '';
46
+ if (process.env.AWS_PROFILE) {
47
+ selectedProfile = process.env.AWS_PROFILE;
48
+ log.success('AWS profile', `${selectedProfile} (from AWS_PROFILE)`);
49
+ }
50
+ else {
51
+ const profiles = readAWSProfiles();
52
+ if (profiles.length === 1) {
53
+ selectedProfile = profiles[0];
54
+ log.success('AWS profile', profiles[0]);
55
+ }
56
+ else if (profiles.length > 1) {
57
+ console.log('');
58
+ const answer = await inquirer.prompt([
59
+ {
60
+ type: 'select',
61
+ name: 'profile',
62
+ message: 'Select AWS profile:',
63
+ choices: profiles,
64
+ default: profiles[0],
65
+ },
66
+ ]);
67
+ selectedProfile = answer.profile;
68
+ }
69
+ }
70
+ const region = detectAWSRegion(selectedProfile);
71
+ ensureInfrawiseDir(cwd);
72
+ writeYaml(cwd, {
73
+ profile: selectedProfile,
74
+ region,
75
+ pgDetected,
76
+ mysqlDetected,
77
+ mongoDetected,
78
+ iacDetected,
79
+ });
80
+ const anyDbDetected = pgDetected || mysqlDetected || mongoDetected;
81
+ if (anyDbDetected) {
82
+ writeSecrets(cwd, secrets);
83
+ }
84
+ console.log('');
85
+ log.success('Generated infrawise.yaml');
86
+ if (pgDetected && !secrets.postgres) {
87
+ log.warn('Postgres detected — add connection string to .infrawise/secrets.yaml');
88
+ }
89
+ if (mysqlDetected && !secrets.mysql) {
90
+ log.warn('MySQL detected — add connection string to .infrawise/secrets.yaml');
91
+ }
92
+ if (mongoDetected && !secrets.mongodb) {
93
+ log.warn('MongoDB detected — add connection string to .infrawise/secrets.yaml');
94
+ }
95
+ console.log('');
96
+ }
97
+ function extractDbSecrets(env) {
98
+ const secrets = {};
99
+ for (const key of ENV_KEYS.postgres) {
100
+ if (env[key]) {
101
+ secrets.postgres = env[key];
102
+ break;
103
+ }
104
+ }
105
+ for (const key of ENV_KEYS.mysql) {
106
+ if (env[key]) {
107
+ secrets.mysql = env[key];
108
+ break;
109
+ }
110
+ }
111
+ for (const key of ENV_KEYS.mongodb) {
112
+ if (env[key]) {
113
+ secrets.mongodb = env[key];
114
+ break;
115
+ }
116
+ }
117
+ return secrets;
118
+ }
119
+ function detectIaC(cwd) {
120
+ return scanDirForIaC(cwd, 3);
121
+ }
122
+ function scanDirForIaC(dir, depth) {
123
+ if (depth < 0)
124
+ return false;
125
+ try {
126
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
127
+ for (const entry of entries) {
128
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
129
+ continue;
130
+ if (entry.isFile()) {
131
+ const n = entry.name;
132
+ if (n.endsWith('.tf') ||
133
+ n === 'cdk.json' ||
134
+ n === 'template.yaml' ||
135
+ n === 'template.yml') {
136
+ return true;
137
+ }
138
+ }
139
+ else if (entry.isDirectory() && depth > 0) {
140
+ if (scanDirForIaC(path.join(dir, entry.name), depth - 1))
141
+ return true;
142
+ }
143
+ }
144
+ }
145
+ catch {
146
+ // ignore permission errors
147
+ }
148
+ return false;
149
+ }
150
+ function ensureInfrawiseDir(cwd) {
151
+ const infrawiseDir = path.join(cwd, '.infrawise');
152
+ if (!fs.existsSync(infrawiseDir)) {
153
+ fs.mkdirSync(infrawiseDir, { recursive: true });
154
+ }
155
+ const gitignorePath = path.join(cwd, '.gitignore');
156
+ const entry = '.infrawise/';
157
+ if (fs.existsSync(gitignorePath)) {
158
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
159
+ if (!content.includes(entry)) {
160
+ const prefix = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
161
+ fs.appendFileSync(gitignorePath, `${prefix}${entry}\n`);
162
+ }
163
+ }
164
+ else {
165
+ fs.writeFileSync(gitignorePath, `${entry}\n`);
166
+ }
167
+ }
168
+ function writeYaml(cwd, opts) {
169
+ const config = {
170
+ project: path.basename(cwd),
171
+ aws: {
172
+ profile: opts.profile,
173
+ region: opts.region,
174
+ },
175
+ dynamodb: { enabled: true, includeTables: [] },
176
+ postgres: { enabled: opts.pgDetected, connectionString: '' },
177
+ mysql: { enabled: opts.mysqlDetected, connectionString: '' },
178
+ mongodb: { enabled: opts.mongoDetected, connectionString: '', databases: [] },
179
+ terraform: { enabled: opts.iacDetected },
180
+ sqs: { enabled: true },
181
+ sns: { enabled: true },
182
+ ssm: { enabled: true, paths: [] },
183
+ secretsManager: { enabled: true },
184
+ lambda: { enabled: true },
185
+ eventbridge: { enabled: true },
186
+ rds: { enabled: false },
187
+ s3: { enabled: true },
188
+ apiGateway: { enabled: true },
189
+ cloudwatchLogs: { enabled: false, logGroupPrefixes: [], windowHours: 24 },
190
+ analysis: { sampleSize: 100 },
191
+ };
192
+ fs.writeFileSync(path.join(cwd, 'infrawise.yaml'), yaml.dump(config, { lineWidth: 120 }), 'utf-8');
193
+ }
194
+ function writeSecrets(cwd, secrets) {
195
+ const data = {
196
+ postgres: { connectionString: secrets.postgres ?? '' },
197
+ mysql: { connectionString: secrets.mysql ?? '' },
198
+ mongodb: { connectionString: secrets.mongodb ?? '' },
199
+ };
200
+ fs.writeFileSync(path.join(cwd, '.infrawise', 'secrets.yaml'), yaml.dump(data, { lineWidth: 120 }), 'utf-8');
201
+ }
@@ -44,7 +44,7 @@ export async function runDoctor(options = {}) {
44
44
  name: 'Config file',
45
45
  status: exists ? 'pass' : 'fail',
46
46
  message: exists ? configPath : `Not found at ${configPath}`,
47
- detail: exists ? undefined : 'Run: infrawise init',
47
+ detail: exists ? undefined : 'Run: infrawise start',
48
48
  };
49
49
  }));
50
50
  // Config valid
@@ -80,7 +80,6 @@ export async function runDoctor(options = {}) {
80
80
  const awsCfg = {
81
81
  region: config?.aws?.region,
82
82
  profile: config?.aws?.profile,
83
- endpoint: config?.aws?.endpoint,
84
83
  };
85
84
  // DynamoDB
86
85
  results.push(await runCheck('Testing DynamoDB access...', async () => {
@@ -0,0 +1,9 @@
1
+ import { runDev } from './dev.js';
2
+ import { runStdio } from './stdio.js';
3
+ export async function runServe(options = {}) {
4
+ if (options.stdio) {
5
+ await runStdio(options.config);
6
+ return;
7
+ }
8
+ await runDev({ config: options.config, port: options.port });
9
+ }
@@ -4,7 +4,7 @@ import { spawn } from 'child_process';
4
4
  import chalk from 'chalk';
5
5
  import { readCache, setCacheDir, formatError, loadConfig } from '../../core/index.js';
6
6
  import { log, printHeader } from '../utils.js';
7
- import { runInit } from './init.js';
7
+ import { runDiscover } from './discover.js';
8
8
  import { runAnalyze } from './analyze.js';
9
9
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
10
10
  function writeMcpJson(configAbsPath) {
@@ -12,7 +12,7 @@ function writeMcpJson(configAbsPath) {
12
12
  mcpServers: {
13
13
  infrawise: {
14
14
  command: 'infrawise',
15
- args: ['stdio', '--config', configAbsPath],
15
+ args: ['serve', '--stdio', '--config', configAbsPath],
16
16
  },
17
17
  },
18
18
  };
@@ -27,7 +27,7 @@ function writeCursorMcp(configAbsPath) {
27
27
  mcpServers: {
28
28
  infrawise: {
29
29
  command: 'infrawise',
30
- args: ['stdio', '--config', configAbsPath],
30
+ args: ['serve', '--stdio', '--config', configAbsPath],
31
31
  },
32
32
  },
33
33
  };
@@ -54,7 +54,7 @@ function launchEditor(editor) {
54
54
  const cmd = editor;
55
55
  const args = ['.'];
56
56
  console.log('');
57
- console.log(chalk.dim(' Opening Cursor...'));
57
+ console.log(chalk.dim(` Opening ${cmd}...`));
58
58
  const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
59
59
  child.on('error', (err) => {
60
60
  if (err.code === 'ENOENT') {
@@ -71,23 +71,35 @@ export async function runStart(options = {}) {
71
71
  const cwd = process.cwd();
72
72
  const configPath = options.config ?? 'infrawise.yaml';
73
73
  const configAbsPath = path.resolve(cwd, configPath);
74
- // Step 1 create infrawise.yaml if missing
74
+ // --rediscover: wipe yaml + entire .infrawise dir, then re-run discovery and analysis
75
+ if (options.rediscover) {
76
+ if (fs.existsSync(configAbsPath)) {
77
+ fs.unlinkSync(configAbsPath);
78
+ }
79
+ const infrawiseDir = path.join(path.dirname(configAbsPath), '.infrawise');
80
+ if (fs.existsSync(infrawiseDir)) {
81
+ fs.rmSync(infrawiseDir, { recursive: true, force: true });
82
+ }
83
+ log.warn('Cleared config and .infrawise — rediscovering...');
84
+ console.log('');
85
+ }
86
+ // Generate config if missing
75
87
  if (!fs.existsSync(configAbsPath)) {
76
- log.warn('No infrawise.yaml found — running setup...');
88
+ log.warn('No infrawise.yaml found — probing environment...');
77
89
  console.log('');
78
- await runInit({ quiet: true });
90
+ await runDiscover({ interactive: options.interactive });
79
91
  console.log('');
80
92
  }
81
- // Validate config is loadable before proceeding
93
+ // Validate config
82
94
  try {
83
- loadConfig(configPath);
95
+ loadConfig(configAbsPath);
84
96
  }
85
97
  catch (err) {
86
98
  console.error(formatError(err));
87
99
  process.exit(1);
88
100
  }
89
101
  setCacheDir(path.dirname(configAbsPath));
90
- // Step 2 — check cache with 24h TTL, re-analyze if stale or missing
102
+ // Check cache, re-analyze if stale
91
103
  const cachedGraph = readCache('graph', CACHE_TTL_MS);
92
104
  const cachedFindings = readCache('findings', CACHE_TTL_MS);
93
105
  if (cachedGraph && cachedFindings) {
@@ -100,12 +112,12 @@ export async function runStart(options = {}) {
100
112
  await runAnalyze({ config: options.config, silent: true });
101
113
  console.log('');
102
114
  }
103
- // Step 3 — write editor MCP config files
115
+ // Write editor MCP config files
104
116
  console.log('');
105
117
  writeMcpJson(configAbsPath);
106
118
  if (options.cursor)
107
119
  writeCursorMcp(configAbsPath);
108
- // Step 4 — launch editor or print instructions
120
+ // Launch editor or print instructions
109
121
  const editor = options.claude ? 'claude' : options.cursor ? 'cursor' : null;
110
122
  if (!editor) {
111
123
  console.log('');