infrawise 0.19.0 → 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.
- package/README.md +3 -1
- package/dist/cli/commands/analyze.js +103 -74
- package/dist/core/cache.js +1 -1
- package/dist/server/index.js +4 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -362,7 +362,7 @@ analysis:
|
|
|
362
362
|
|
|
363
363
|
### AWS setup
|
|
364
364
|
|
|
365
|
-
Infrawise is **read-only**. Minimum IAM policy
|
|
365
|
+
Infrawise is **read-only**. Minimum IAM policy for DynamoDB:
|
|
366
366
|
|
|
367
367
|
```json
|
|
368
368
|
{
|
|
@@ -377,6 +377,8 @@ Infrawise is **read-only**. Minimum IAM policy required:
|
|
|
377
377
|
}
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
For the full policy across all supported services, how to scope it to only the services you enable, and using a session policy for temporary scoped credentials, see the [AWS setup guide](https://sidd27.github.io/infrawise/getting-started/aws-setup/).
|
|
381
|
+
|
|
380
382
|
For SSO profiles, log in before running infrawise:
|
|
381
383
|
|
|
382
384
|
```bash
|
|
@@ -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
|
|
51
|
-
//
|
|
52
|
-
|
|
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
|
-
|
|
59
|
+
log.success(label, summarize(result));
|
|
59
60
|
return result;
|
|
60
61
|
}
|
|
61
62
|
catch (err) {
|
|
62
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
{
|
package/dist/core/cache.js
CHANGED
|
@@ -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
|
|
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`);
|
package/dist/server/index.js
CHANGED
|
@@ -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 =
|
|
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) =>
|
|
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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"mcpName": "io.github.Sidd27/infrawise",
|
|
5
|
-
"description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB,
|
|
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": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"mcp-server",
|