infrawise 0.2.2 → 0.4.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.
package/README.md CHANGED
@@ -220,6 +220,9 @@ lambda:
220
220
  rds:
221
221
  enabled: false
222
222
 
223
+ kafka:
224
+ enabled: false
225
+
223
226
  cloudwatchLogs:
224
227
  enabled: false
225
228
  logGroupPrefixes: []
@@ -284,6 +287,7 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
284
287
  | PostgreSQL / MySQL schema | Tables, indexes, column types |
285
288
  | MongoDB schema | Collections, indexes |
286
289
  | SQS | Missing DLQs, unencrypted queues, large backlogs |
290
+ | Kafka (kafkajs) | Producer/consumer topic mapping from code |
287
291
  | Secrets Manager | Missing secret rotation |
288
292
  | Lambda | Default memory (128 MB), high timeouts |
289
293
  | RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
@@ -309,7 +313,7 @@ Uses [ts-morph](https://ts-morph.com/) AST analysis to detect which functions ca
309
313
 
310
314
  Non-TypeScript/JavaScript projects still get full value from infrastructure-level analyzers — code correlation (function-to-table mapping, N+1 patterns) is skipped.
311
315
 
312
- The scanner supports: AWS SDK v3/v2 for DynamoDB, `pg`/Prisma/Knex for PostgreSQL, `mysql2`/Knex for MySQL, driver/Mongoose for MongoDB, and AWS SDK v3 for SQS/SNS/SSM/Secrets/Lambda.
316
+ The scanner supports: AWS SDK v3/v2 for DynamoDB, `pg`/Prisma/Knex for PostgreSQL, `mysql2`/Knex for MySQL, driver/Mongoose for MongoDB, AWS SDK v3 for SQS/SNS/SSM/Secrets/Lambda, and `kafkajs` for Kafka topics (producer/consumer).
313
317
 
314
318
  ---
315
319
 
@@ -399,6 +403,7 @@ src/
399
403
  - Kubernetes workload graphing
400
404
  - VS Code extension
401
405
  - Infrastructure drift detection
406
+ - MSK (Amazon Managed Streaming for Apache Kafka) — cluster metadata + topic listing via MSK API and Kafka admin client
402
407
 
403
408
  ### Under consideration
404
409
  - OpenTelemetry integration
@@ -22,9 +22,17 @@ const credential_providers_1 = require("@aws-sdk/credential-providers");
22
22
  const core_1 = require("../core");
23
23
  function clientConfig(cfg) {
24
24
  const region = cfg.region ?? 'us-east-1';
25
- return cfg.profile
26
- ? { region, credentials: (0, credential_providers_1.fromIni)({ profile: cfg.profile }) }
27
- : { region };
25
+ const base = { region };
26
+ if (cfg.endpoint)
27
+ base.endpoint = cfg.endpoint;
28
+ if (cfg.profile)
29
+ base.credentials = (0, credential_providers_1.fromIni)({ profile: cfg.profile });
30
+ else if (cfg.endpoint)
31
+ base.credentials = {
32
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
33
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
34
+ };
35
+ return base;
28
36
  }
29
37
  // ─── SQS ─────────────────────────────────────────────────────────────────────
30
38
  async function extractSQSMetadata(cfg = {}) {
@@ -236,7 +244,7 @@ async function extractRDSMetadata(cfg = {}) {
236
244
  instanceClass: db.DBInstanceClass ?? '',
237
245
  publiclyAccessible: db.PubliclyAccessible ?? false,
238
246
  storageEncrypted: db.StorageEncrypted ?? false,
239
- backupRetentionPeriod: db.BackupRetentionPeriod ?? 0,
247
+ backupRetentionDays: db.BackupRetentionPeriod ?? 0,
240
248
  deletionProtection: db.DeletionProtection ?? false,
241
249
  multiAZ: db.MultiAZ ?? false,
242
250
  dbInstanceStatus: db.DBInstanceStatus ?? '',
@@ -2,16 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractDynamoMetadata = extractDynamoMetadata;
4
4
  exports.validateDynamoAccess = validateDynamoAccess;
5
+ exports.probeDynamoAccess = probeDynamoAccess;
5
6
  const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
6
7
  const credential_providers_1 = require("@aws-sdk/credential-providers");
7
8
  const core_1 = require("../core");
8
9
  function createDynamoClient(config) {
9
10
  const region = config.aws?.region ?? 'us-east-1';
10
11
  const profile = config.aws?.profile;
12
+ const endpoint = config.aws?.endpoint;
11
13
  const clientConfig = { region };
12
- if (profile) {
14
+ if (endpoint)
15
+ clientConfig.endpoint = endpoint;
16
+ if (profile)
13
17
  clientConfig.credentials = (0, credential_providers_1.fromIni)({ profile });
14
- }
18
+ else if (endpoint)
19
+ clientConfig.credentials = {
20
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
21
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
22
+ };
15
23
  return new client_dynamodb_1.DynamoDBClient(clientConfig);
16
24
  }
17
25
  function parseTableDescription(desc) {
@@ -95,3 +103,7 @@ async function validateDynamoAccess(config) {
95
103
  return false;
96
104
  }
97
105
  }
106
+ async function probeDynamoAccess(config) {
107
+ const client = createDynamoClient(config);
108
+ await client.send(new client_dynamodb_1.ListTablesCommand({ Limit: 1 }));
109
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractLogsSummary = extractLogsSummary;
3
+ exports.extractLogsMetadata = extractLogsMetadata;
4
4
  exports.validateLogsAccess = validateLogsAccess;
5
5
  const client_cloudwatch_logs_1 = require("@aws-sdk/client-cloudwatch-logs");
6
6
  const credential_providers_1 = require("@aws-sdk/credential-providers");
@@ -10,9 +10,17 @@ const MAX_LOG_GROUPS = 50;
10
10
  const MAX_EVENTS_PER_GROUP = 50;
11
11
  function clientConfig(cfg) {
12
12
  const region = cfg.region ?? 'us-east-1';
13
- return cfg.profile
14
- ? { region, credentials: (0, credential_providers_1.fromIni)({ profile: cfg.profile }) }
15
- : { region };
13
+ const base = { region };
14
+ if (cfg.endpoint)
15
+ base.endpoint = cfg.endpoint;
16
+ if (cfg.profile)
17
+ base.credentials = (0, credential_providers_1.fromIni)({ profile: cfg.profile });
18
+ else if (cfg.endpoint)
19
+ base.credentials = {
20
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
21
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
22
+ };
23
+ return base;
16
24
  }
17
25
  // Strip UUIDs, timestamps, numbers → group similar messages by pattern
18
26
  function toPattern(message) {
@@ -36,7 +44,7 @@ function topPatterns(messages, limit = 5) {
36
44
  .slice(0, limit)
37
45
  .map(([pattern, count]) => ({ pattern, count }));
38
46
  }
39
- async function extractLogsSummary(cfg = {}) {
47
+ async function extractLogsMetadata(cfg = {}) {
40
48
  const client = new client_cloudwatch_logs_1.CloudWatchLogsClient(clientConfig(cfg));
41
49
  const windowMs = (cfg.windowHours ?? 24) * 60 * 60 * 1000;
42
50
  const startTime = Date.now() - windowMs;
@@ -64,7 +64,7 @@ async function runAnalyze(options = {}) {
64
64
  process.exit(1);
65
65
  }
66
66
  const repoPath = options.repo ?? process.cwd();
67
- const awsCfg = { region: config.aws?.region, profile: config.aws?.profile };
67
+ const awsCfg = { region: config.aws?.region, profile: config.aws?.profile, endpoint: config.aws?.endpoint };
68
68
  const dynamoMeta = [];
69
69
  const postgresMeta = [];
70
70
  const mysqlMeta = [];
@@ -194,7 +194,7 @@ async function runAnalyze(options = {}) {
194
194
  if (config.cloudwatchLogs?.enabled) {
195
195
  const spin = (0, ora_1.default)({ text: chalk_1.default.dim('Sampling CloudWatch Logs (errors only, max 50 groups)...'), color: 'cyan' }).start();
196
196
  try {
197
- const result = await (0, logs_1.extractLogsSummary)({
197
+ const result = await (0, logs_1.extractLogsMetadata)({
198
198
  ...awsCfg,
199
199
  logGroupPrefixes: config.cloudwatchLogs.logGroupPrefixes,
200
200
  windowHours: config.cloudwatchLogs.windowHours,
@@ -113,7 +113,7 @@ async function runDoctor(options = {}) {
113
113
  detail: hasCreds ? undefined : 'Run: aws configure',
114
114
  };
115
115
  }));
116
- const awsCfg = { region: config?.aws?.region, profile: config?.aws?.profile };
116
+ const awsCfg = { region: config?.aws?.region, profile: config?.aws?.profile, endpoint: config?.aws?.endpoint };
117
117
  // DynamoDB
118
118
  results.push(await runCheck('Testing DynamoDB access...', async () => {
119
119
  if (!config)
@@ -121,15 +121,15 @@ async function runDoctor(options = {}) {
121
121
  if (config.dynamodb?.enabled !== true)
122
122
  return { name: 'DynamoDB', status: 'skip', message: 'Disabled in config' };
123
123
  try {
124
- const ok = await (0, dynamodb_1.validateDynamoAccess)(config);
125
- return {
126
- name: 'DynamoDB', status: ok ? 'pass' : 'warn',
127
- message: ok ? `Connected (profile: ${config.aws?.profile ?? 'default'})` : 'Cannot connect',
128
- detail: ok ? undefined : 'Check IAM: dynamodb:ListTables, dynamodb:DescribeTable',
129
- };
124
+ await (0, dynamodb_1.probeDynamoAccess)(config);
125
+ return { name: 'DynamoDB', status: 'pass', message: `Connected (profile: ${config.aws?.profile ?? 'default'})` };
130
126
  }
131
127
  catch (err) {
132
- return { name: 'DynamoDB', status: 'warn', message: err instanceof Error ? err.message : String(err) };
128
+ return {
129
+ name: 'DynamoDB', status: 'warn',
130
+ message: err instanceof Error ? err.message : String(err),
131
+ detail: 'Check IAM: dynamodb:ListTables, dynamodb:DescribeTable',
132
+ };
133
133
  }
134
134
  }));
135
135
  // SQS
@@ -293,7 +293,11 @@ async function runDoctor(options = {}) {
293
293
  // IaC detection
294
294
  results.push(await runCheck('Detecting IaC files...', async () => {
295
295
  const cwd = process.cwd();
296
- const hasTF = fs.existsSync(path.join(cwd, 'main.tf')) || fs.readdirSync(cwd).some((f) => f.endsWith('.tf'));
296
+ const hasTfFile = (dir) => fs.existsSync(dir) && fs.readdirSync(dir).some((f) => f.endsWith('.tf'));
297
+ const hasTF = hasTfFile(cwd) || fs.readdirSync(cwd).some((entry) => {
298
+ const full = path.join(cwd, entry);
299
+ return fs.statSync(full).isDirectory() && hasTfFile(full);
300
+ });
297
301
  const hasCFN = fs.existsSync(path.join(cwd, 'template.yaml')) || fs.existsSync(path.join(cwd, 'template.json'));
298
302
  const hasCDK = fs.existsSync(path.join(cwd, 'cdk.json'));
299
303
  const hasCDKOut = fs.existsSync(path.join(cwd, 'cdk.out'));
@@ -71,7 +71,13 @@ async function runInit(options = {}) {
71
71
  type: 'list',
72
72
  name: 'awsProfile',
73
73
  message: 'AWS profile:',
74
- choices: profiles,
74
+ choices: [
75
+ new inquirer_1.default.Separator('── no profile ──'),
76
+ { name: 'Environment variables (CI/CD, real AWS)', value: '__env__' },
77
+ { name: 'LocalStack (local development)', value: '__localstack__' },
78
+ new inquirer_1.default.Separator('── named profiles ──'),
79
+ ...profiles,
80
+ ],
75
81
  default: profiles[0],
76
82
  },
77
83
  {
@@ -80,24 +86,18 @@ async function runInit(options = {}) {
80
86
  message: 'AWS region:',
81
87
  default: detectedRegion,
82
88
  },
89
+ {
90
+ type: 'input',
91
+ name: 'endpoint',
92
+ message: 'LocalStack endpoint:',
93
+ default: 'http://localhost:4566',
94
+ when: (a) => a.awsProfile === '__localstack__',
95
+ },
83
96
  ]);
84
97
  // ── Databases ──────────────────────────────────────────────────────────────
85
98
  console.log('\n ' + chalk_1.default.bold('Databases'));
99
+ console.log(chalk_1.default.dim(' Self-hosted databases (PostgreSQL, MySQL, MongoDB).'));
86
100
  const databases = await inquirer_1.default.prompt([
87
- {
88
- type: 'confirm',
89
- name: 'dynamoEnabled',
90
- message: 'Enable DynamoDB analysis?',
91
- default: true,
92
- },
93
- {
94
- type: 'input',
95
- name: 'dynamoTables',
96
- message: 'DynamoDB tables to include:',
97
- default: '',
98
- suffix: chalk_1.default.dim(' (comma-separated, blank = all)'),
99
- when: (a) => a.dynamoEnabled,
100
- },
101
101
  {
102
102
  type: 'confirm',
103
103
  name: 'pgEnabled',
@@ -140,8 +140,22 @@ async function runInit(options = {}) {
140
140
  ]);
141
141
  // ── AWS services ───────────────────────────────────────────────────────────
142
142
  console.log('\n ' + chalk_1.default.bold('AWS Services'));
143
- console.log(chalk_1.default.dim(' Infrawise will introspect these services credentials from the AWS profile above.'));
143
+ console.log(chalk_1.default.dim(' Infrawise will introspect these services using the credentials configured above.'));
144
144
  const services = await inquirer_1.default.prompt([
145
+ {
146
+ type: 'confirm',
147
+ name: 'dynamoEnabled',
148
+ message: 'Introspect DynamoDB?',
149
+ default: true,
150
+ },
151
+ {
152
+ type: 'input',
153
+ name: 'dynamoTables',
154
+ message: 'DynamoDB tables to include:',
155
+ default: '',
156
+ suffix: chalk_1.default.dim(' (comma-separated, blank = all)'),
157
+ when: (a) => a.dynamoEnabled,
158
+ },
145
159
  {
146
160
  type: 'confirm',
147
161
  name: 'sqsEnabled',
@@ -196,8 +210,8 @@ async function runInit(options = {}) {
196
210
  },
197
211
  ]);
198
212
  // ── Build config ───────────────────────────────────────────────────────────
199
- const includeTables = databases.dynamoTables
200
- ? databases.dynamoTables.split(',').map((t) => t.trim()).filter(Boolean)
213
+ const includeTables = services.dynamoTables
214
+ ? services.dynamoTables.split(',').map((t) => t.trim()).filter(Boolean)
201
215
  : [];
202
216
  const ssmPaths = services.ssmPaths
203
217
  ? services.ssmPaths.split(',').map((p) => p.trim()).filter(Boolean)
@@ -205,9 +219,13 @@ async function runInit(options = {}) {
205
219
  const logGroupPrefixes = services.logGroupPrefixes
206
220
  ? services.logGroupPrefixes.split(',').map((p) => p.trim()).filter(Boolean)
207
221
  : [];
222
+ const isLocalStack = core.awsProfile === '__localstack__';
223
+ const isEnvVars = core.awsProfile === '__env__';
224
+ const resolvedProfile = isLocalStack || isEnvVars ? '' : core.awsProfile;
225
+ const resolvedEndpoint = isLocalStack ? (core.endpoint || 'http://localhost:4566') : undefined;
208
226
  const configContent = (0, core_1.generateDefaultConfig)(core.project, {
209
- aws: { profile: core.awsProfile, region: core.region },
210
- dynamodb: { enabled: databases.dynamoEnabled, includeTables },
227
+ aws: { profile: resolvedProfile, region: core.region, endpoint: resolvedEndpoint },
228
+ dynamodb: { enabled: services.dynamoEnabled, includeTables },
211
229
  postgres: { enabled: databases.pgEnabled, connectionString: databases.pgConnectionString ?? '' },
212
230
  mysql: { enabled: databases.mysqlEnabled, connectionString: databases.mysqlConnectionString ?? '' },
213
231
  mongodb: { enabled: databases.mongoEnabled, connectionString: databases.mongoConnectionString ?? '' },
@@ -82,6 +82,11 @@ const MONGO_COLLECTION_METHODS = new Set(['collection']);
82
82
  // ── AWS service patterns ──────────────────────────────────────────────────────
83
83
  const SQS_COMMANDS = new Set(['SendMessageCommand', 'SendMessageBatchCommand', 'ReceiveMessageCommand', 'DeleteMessageCommand', 'sendMessage', 'sendMessageBatch', 'receiveMessage']);
84
84
  const SNS_COMMANDS = new Set(['PublishCommand', 'PublishBatchCommand', 'publish', 'publishBatch']);
85
+ // kafkajs patterns — detection relies on variable naming (producer/consumer/kafka)
86
+ const KAFKA_PRODUCER_METHODS = new Set(['send', 'sendBatch']);
87
+ const KAFKA_CONSUMER_METHODS = new Set(['subscribe']);
88
+ const KAFKA_CLIENT_PATTERNS = ['kafka', 'producer', 'consumer'];
89
+ const KAFKA_TOPIC_KEYS = ['topic'];
85
90
  const SSM_COMMANDS = new Set(['GetParameterCommand', 'GetParametersCommand', 'GetParametersByPathCommand', 'getParameter', 'getParameters', 'getParametersByPath']);
86
91
  const SECRETS_COMMANDS = new Set(['GetSecretValueCommand', 'getSecretValue']);
87
92
  const LAMBDA_COMMANDS = new Set(['InvokeCommand', 'InvokeAsyncCommand', 'invoke', 'invokeAsync']);
@@ -158,7 +163,7 @@ function detectDynamoOperations(callExpr, filePath) {
158
163
  return {
159
164
  functionName: getEnclosingFunctionName(callExpr),
160
165
  operationType: className,
161
- databaseType: 'dynamodb',
166
+ serviceType: 'dynamodb',
162
167
  target: tableName,
163
168
  filePath,
164
169
  };
@@ -173,7 +178,7 @@ function detectDynamoOperations(callExpr, filePath) {
173
178
  return {
174
179
  functionName: getEnclosingFunctionName(callExpr),
175
180
  operationType: methodName,
176
- databaseType: 'dynamodb',
181
+ serviceType: 'dynamodb',
177
182
  target: tableName,
178
183
  filePath,
179
184
  };
@@ -229,7 +234,7 @@ function detectPostgresOperations(callExpr, filePath) {
229
234
  return {
230
235
  functionName: getEnclosingFunctionName(callExpr),
231
236
  operationType: 'query',
232
- databaseType: 'postgres',
237
+ serviceType: 'postgres',
233
238
  target: tableName,
234
239
  filePath,
235
240
  };
@@ -245,7 +250,7 @@ function detectPostgresOperations(callExpr, filePath) {
245
250
  return {
246
251
  functionName: getEnclosingFunctionName(callExpr),
247
252
  operationType: methodName,
248
- databaseType: 'postgres',
253
+ serviceType: 'postgres',
249
254
  target: modelName,
250
255
  filePath,
251
256
  };
@@ -262,7 +267,7 @@ function detectPostgresOperations(callExpr, filePath) {
262
267
  return {
263
268
  functionName: getEnclosingFunctionName(callExpr),
264
269
  operationType: methodName,
265
- databaseType: 'postgres',
270
+ serviceType: 'postgres',
266
271
  target: 'unknown',
267
272
  filePath,
268
273
  };
@@ -278,7 +283,7 @@ function detectPostgresOperations(callExpr, filePath) {
278
283
  return {
279
284
  functionName: getEnclosingFunctionName(callExpr),
280
285
  operationType: methodName,
281
- databaseType: 'postgres',
286
+ serviceType: 'postgres',
282
287
  target: tableName,
283
288
  filePath,
284
289
  };
@@ -315,7 +320,7 @@ function detectMySQLOperations(callExpr, filePath) {
315
320
  return {
316
321
  functionName: getEnclosingFunctionName(callExpr),
317
322
  operationType: 'query',
318
- databaseType: 'mysql',
323
+ serviceType: 'mysql',
319
324
  target: tableName,
320
325
  filePath,
321
326
  };
@@ -334,7 +339,7 @@ function detectMySQLOperations(callExpr, filePath) {
334
339
  return {
335
340
  functionName: getEnclosingFunctionName(callExpr),
336
341
  operationType: methodName,
337
- databaseType: 'mysql',
342
+ serviceType: 'mysql',
338
343
  target: tableName,
339
344
  filePath,
340
345
  };
@@ -382,7 +387,7 @@ function detectMongoOperations(callExpr, filePath) {
382
387
  return {
383
388
  functionName: getEnclosingFunctionName(callExpr),
384
389
  operationType: opType,
385
- databaseType: 'mongodb',
390
+ serviceType: 'mongodb',
386
391
  target: collectionName,
387
392
  filePath,
388
393
  };
@@ -399,7 +404,7 @@ function detectMongoOperations(callExpr, filePath) {
399
404
  return {
400
405
  functionName: getEnclosingFunctionName(callExpr),
401
406
  operationType: 'query',
402
- databaseType: 'mongodb',
407
+ serviceType: 'mongodb',
403
408
  target: collectionName,
404
409
  filePath,
405
410
  };
@@ -422,6 +427,35 @@ function extractArgValue(arg, ...keys) {
422
427
  }
423
428
  return 'unknown';
424
429
  }
430
+ function detectKafkaOperations(callExpr, filePath) {
431
+ const expr = callExpr.getExpression();
432
+ const args = callExpr.getArguments();
433
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
434
+ return null;
435
+ const methodName = expr.getName();
436
+ const objText = expr.getExpression().getText().toLowerCase();
437
+ if (!KAFKA_CLIENT_PATTERNS.some((p) => objText.includes(p)))
438
+ return null;
439
+ if (KAFKA_PRODUCER_METHODS.has(methodName)) {
440
+ return {
441
+ functionName: getEnclosingFunctionName(callExpr),
442
+ operationType: methodName,
443
+ serviceType: 'kafka',
444
+ target: args.length > 0 ? extractArgValue(args[0], ...KAFKA_TOPIC_KEYS) : 'unknown',
445
+ filePath,
446
+ };
447
+ }
448
+ if (KAFKA_CONSUMER_METHODS.has(methodName)) {
449
+ return {
450
+ functionName: getEnclosingFunctionName(callExpr),
451
+ operationType: methodName,
452
+ serviceType: 'kafka',
453
+ target: args.length > 0 ? extractArgValue(args[0], ...KAFKA_TOPIC_KEYS) : 'unknown',
454
+ filePath,
455
+ };
456
+ }
457
+ return null;
458
+ }
425
459
  function detectAWSServiceOperations(callExpr, filePath) {
426
460
  const expr = callExpr.getExpression();
427
461
  const args = callExpr.getArguments();
@@ -435,7 +469,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
435
469
  return {
436
470
  functionName: getEnclosingFunctionName(callExpr),
437
471
  operationType: className,
438
- databaseType: 'sqs',
472
+ serviceType: 'sqs',
439
473
  target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SQS_ARG_KEYS) : 'unknown',
440
474
  filePath,
441
475
  };
@@ -444,7 +478,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
444
478
  return {
445
479
  functionName: getEnclosingFunctionName(callExpr),
446
480
  operationType: className,
447
- databaseType: 'sns',
481
+ serviceType: 'sns',
448
482
  target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SNS_ARG_KEYS) : 'unknown',
449
483
  filePath,
450
484
  };
@@ -453,7 +487,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
453
487
  return {
454
488
  functionName: getEnclosingFunctionName(callExpr),
455
489
  operationType: className,
456
- databaseType: 'ssm',
490
+ serviceType: 'ssm',
457
491
  target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SSM_ARG_KEYS) : 'unknown',
458
492
  filePath,
459
493
  };
@@ -462,7 +496,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
462
496
  return {
463
497
  functionName: getEnclosingFunctionName(callExpr),
464
498
  operationType: className,
465
- databaseType: 'secretsmanager',
499
+ serviceType: 'secretsmanager',
466
500
  target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SECRETS_ARG_KEYS) : 'unknown',
467
501
  filePath,
468
502
  };
@@ -471,7 +505,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
471
505
  return {
472
506
  functionName: getEnclosingFunctionName(callExpr),
473
507
  operationType: className,
474
- databaseType: 'lambda',
508
+ serviceType: 'lambda',
475
509
  target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...LAMBDA_ARG_KEYS) : 'unknown',
476
510
  filePath,
477
511
  };
@@ -486,7 +520,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
486
520
  return {
487
521
  functionName: getEnclosingFunctionName(callExpr),
488
522
  operationType: methodName,
489
- databaseType: 'sqs',
523
+ serviceType: 'sqs',
490
524
  target: args.length > 0 ? extractArgValue(args[0], ...SQS_ARG_KEYS) : 'unknown',
491
525
  filePath,
492
526
  };
@@ -495,7 +529,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
495
529
  return {
496
530
  functionName: getEnclosingFunctionName(callExpr),
497
531
  operationType: methodName,
498
- databaseType: 'sns',
532
+ serviceType: 'sns',
499
533
  target: args.length > 0 ? extractArgValue(args[0], ...SNS_ARG_KEYS) : 'unknown',
500
534
  filePath,
501
535
  };
@@ -504,7 +538,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
504
538
  return {
505
539
  functionName: getEnclosingFunctionName(callExpr),
506
540
  operationType: methodName,
507
- databaseType: 'ssm',
541
+ serviceType: 'ssm',
508
542
  target: args.length > 0 ? extractArgValue(args[0], ...SSM_ARG_KEYS) : 'unknown',
509
543
  filePath,
510
544
  };
@@ -513,7 +547,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
513
547
  return {
514
548
  functionName: getEnclosingFunctionName(callExpr),
515
549
  operationType: methodName,
516
- databaseType: 'secretsmanager',
550
+ serviceType: 'secretsmanager',
517
551
  target: args.length > 0 ? extractArgValue(args[0], ...SECRETS_ARG_KEYS) : 'unknown',
518
552
  filePath,
519
553
  };
@@ -522,7 +556,7 @@ function detectAWSServiceOperations(callExpr, filePath) {
522
556
  return {
523
557
  functionName: getEnclosingFunctionName(callExpr),
524
558
  operationType: methodName,
525
- databaseType: 'lambda',
559
+ serviceType: 'lambda',
526
560
  target: args.length > 0 ? extractArgValue(args[0], ...LAMBDA_ARG_KEYS) : 'unknown',
527
561
  filePath,
528
562
  };
@@ -586,6 +620,11 @@ async function scanRepository(repoPath) {
586
620
  operations.push(mongoOp);
587
621
  continue;
588
622
  }
623
+ const kafkaOp = detectKafkaOperations(callExpr, filePath);
624
+ if (kafkaOp) {
625
+ operations.push(kafkaOp);
626
+ continue;
627
+ }
589
628
  const awsOp = detectAWSServiceOperations(callExpr, filePath);
590
629
  if (awsOp) {
591
630
  operations.push(awsOp);
@@ -46,6 +46,7 @@ exports.InfrawiseConfigSchema = zod_1.z.object({
46
46
  .object({
47
47
  profile: zod_1.z.string().optional().default('default'),
48
48
  region: zod_1.z.string().optional().default('us-east-1'),
49
+ endpoint: zod_1.z.string().optional(),
49
50
  })
50
51
  .optional()
51
52
  .default({}),
@@ -128,6 +129,7 @@ function generateDefaultConfig(projectName, options) {
128
129
  aws: {
129
130
  profile: options?.aws?.profile ?? 'default',
130
131
  region: options?.aws?.region ?? 'us-east-1',
132
+ ...(options?.aws?.endpoint ? { endpoint: options.aws.endpoint } : {}),
131
133
  },
132
134
  dynamodb: { enabled: options?.dynamodb?.enabled ?? true, includeTables: options?.dynamodb?.includeTables ?? [] },
133
135
  postgres: {
@@ -140,7 +140,7 @@ function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [], mongoM
140
140
  instanceClass: db.instanceClass,
141
141
  publiclyAccessible: db.publiclyAccessible,
142
142
  storageEncrypted: db.storageEncrypted,
143
- backupRetentionDays: db.backupRetentionPeriod,
143
+ backupRetentionDays: db.backupRetentionDays,
144
144
  deletionProtection: db.deletionProtection,
145
145
  multiAZ: db.multiAZ,
146
146
  });
@@ -153,31 +153,38 @@ function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [], mongoM
153
153
  nodeIds.add(funcNodeId);
154
154
  }
155
155
  // AWS service operations create edges to service nodes
156
- if (op.databaseType === 'sqs') {
156
+ if (op.serviceType === 'sqs') {
157
157
  const queueId = `queue:aws:${op.target}`;
158
158
  addNode({ id: queueId, type: 'queue', name: op.target, provider: 'aws', hasDLQ: false, encrypted: false });
159
159
  edges.push({ from: funcNodeId, to: queueId, type: 'publishes_to' });
160
160
  continue;
161
161
  }
162
- if (op.databaseType === 'sns') {
162
+ if (op.serviceType === 'sns') {
163
163
  const topicId = `topic:aws:${op.target}`;
164
164
  addNode({ id: topicId, type: 'topic', name: op.target, provider: 'aws', encrypted: false });
165
165
  edges.push({ from: funcNodeId, to: topicId, type: 'publishes_to' });
166
166
  continue;
167
167
  }
168
- if (op.databaseType === 'ssm') {
168
+ if (op.serviceType === 'kafka') {
169
+ const topicId = `topic:kafka:${op.target}`;
170
+ addNode({ id: topicId, type: 'topic', name: op.target, provider: 'kafka', encrypted: false });
171
+ const edgeType = op.operationType === 'subscribe' ? 'subscribes_to' : 'publishes_to';
172
+ edges.push({ from: funcNodeId, to: topicId, type: edgeType });
173
+ continue;
174
+ }
175
+ if (op.serviceType === 'ssm') {
169
176
  const paramId = `parameter:aws:${op.target}`;
170
177
  addNode({ id: paramId, type: 'parameter', name: op.target, provider: 'aws', paramType: 'String', tier: 'Standard' });
171
178
  edges.push({ from: funcNodeId, to: paramId, type: 'reads_parameter' });
172
179
  continue;
173
180
  }
174
- if (op.databaseType === 'secretsmanager') {
181
+ if (op.serviceType === 'secretsmanager') {
175
182
  const secretId = `secret:aws:${op.target}`;
176
183
  addNode({ id: secretId, type: 'secret', name: op.target, provider: 'aws', rotationEnabled: false });
177
184
  edges.push({ from: funcNodeId, to: secretId, type: 'reads_secret' });
178
185
  continue;
179
186
  }
180
- if (op.databaseType === 'lambda') {
187
+ if (op.serviceType === 'lambda') {
181
188
  const lambdaId = `lambda:aws:${op.target}`;
182
189
  addNode({ id: lambdaId, type: 'lambda', name: op.target });
183
190
  edges.push({ from: funcNodeId, to: lambdaId, type: 'triggers' });
@@ -185,16 +192,16 @@ function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [], mongoM
185
192
  }
186
193
  // Database operations
187
194
  let tableNodeId;
188
- if (op.databaseType === 'dynamodb') {
195
+ if (op.serviceType === 'dynamodb') {
189
196
  tableNodeId = `table:dynamo:${op.target}`;
190
197
  addNode({ id: tableNodeId, type: 'table', name: op.target, databaseType: 'dynamodb' });
191
198
  }
192
- else if (op.databaseType === 'mysql') {
199
+ else if (op.serviceType === 'mysql') {
193
200
  const q = op.target.includes('.') ? op.target : `default.${op.target}`;
194
201
  tableNodeId = `table:mysql:${q}`;
195
202
  addNode({ id: tableNodeId, type: 'table', name: q, databaseType: 'mysql' });
196
203
  }
197
- else if (op.databaseType === 'mongodb') {
204
+ else if (op.serviceType === 'mongodb') {
198
205
  const q = op.target.includes('.') ? op.target : `default.${op.target}`;
199
206
  tableNodeId = `table:mongodb:${q}`;
200
207
  addNode({ id: tableNodeId, type: 'table', name: q, databaseType: 'mongodb' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
5
5
  "keywords": [
6
6
  "infrastructure",