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 +6 -1
- package/dist/adapters/aws.js +12 -4
- package/dist/adapters/dynamodb.js +14 -2
- package/dist/adapters/logs.js +13 -5
- package/dist/cli/commands/analyze.js +2 -2
- package/dist/cli/commands/doctor.js +13 -9
- package/dist/cli/commands/init.js +38 -20
- package/dist/context/index.js +59 -20
- package/dist/core/config.js +2 -0
- package/dist/graph/index.js +16 -9
- package/package.json +1 -1
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,
|
|
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
|
package/dist/adapters/aws.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
+
}
|
package/dist/adapters/logs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
200
|
-
?
|
|
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:
|
|
210
|
-
dynamodb: { enabled:
|
|
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 ?? '' },
|
package/dist/context/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/core/config.js
CHANGED
|
@@ -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: {
|
package/dist/graph/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|