infrawise 0.3.0 → 0.4.1
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 +52 -2
- package/dist/adapters/aws.js +1 -1
- package/dist/adapters/logs.js +2 -2
- package/dist/cli/commands/analyze.js +113 -52
- package/dist/cli/index.js +6 -1
- package/dist/context/index.js +59 -20
- package/dist/core/config.js +5 -0
- package/dist/graph/index.js +16 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,41 @@ To let Claude Code manage the server lifecycle automatically:
|
|
|
160
160
|
|
|
161
161
|
---
|
|
162
162
|
|
|
163
|
+
## GitHub Action
|
|
164
|
+
|
|
165
|
+
Add infrastructure analysis to any PR workflow. No `infrawise.yaml` required — the action auto-generates one from your AWS credentials.
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
# .github/workflows/infrawise.yml
|
|
169
|
+
name: Infrawise
|
|
170
|
+
on: [pull_request]
|
|
171
|
+
|
|
172
|
+
jobs:
|
|
173
|
+
analyze:
|
|
174
|
+
runs-on: ubuntu-latest
|
|
175
|
+
steps:
|
|
176
|
+
- uses: actions/checkout@v4
|
|
177
|
+
- uses: Sidd27/infrawise@v1
|
|
178
|
+
with:
|
|
179
|
+
fail-on: high # fail the PR on high severity findings (default)
|
|
180
|
+
env:
|
|
181
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
182
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
183
|
+
AWS_REGION: ${{ secrets.AWS_REGION }}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If `infrawise.yaml` is not present in the repo, the action generates a minimal config with DynamoDB, Lambda, SQS, SNS, SSM, Secrets Manager, and RDS enabled — using `AWS_REGION` from the environment. Add an `infrawise.yaml` to your repo to customize which services to include or to add database connections.
|
|
187
|
+
|
|
188
|
+
Findings appear as annotations on the PR diff and in the job summary.
|
|
189
|
+
|
|
190
|
+
| Input | Default | Description |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| `version` | `latest` | infrawise version to install |
|
|
193
|
+
| `fail-on` | `high` | Exit code 1 if findings at or above this severity (`high`, `medium`, `low`, `never`) |
|
|
194
|
+
| `config` | `infrawise.yaml` | Path to config file — auto-generated if not present |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
163
198
|
## CLI reference
|
|
164
199
|
|
|
165
200
|
| Command | What it does |
|
|
@@ -176,6 +211,16 @@ To let Claude Code manage the server lifecycle automatically:
|
|
|
176
211
|
|
|
177
212
|
`infrawise.yaml` is generated by `infrawise init` and lives in your repo root. Every service must be explicitly `enabled: true` — infrawise never connects to anything not listed in config.
|
|
178
213
|
|
|
214
|
+
Connection strings support `${ENV_VAR}` substitution so passwords never need to be committed:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
postgres:
|
|
218
|
+
enabled: true
|
|
219
|
+
connectionString: postgresql://infrawise_ro:${DB_PASSWORD}@host:5432/mydb
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Full example:
|
|
223
|
+
|
|
179
224
|
```yaml
|
|
180
225
|
project: payments-service
|
|
181
226
|
|
|
@@ -191,7 +236,7 @@ dynamodb:
|
|
|
191
236
|
|
|
192
237
|
postgres:
|
|
193
238
|
enabled: true
|
|
194
|
-
connectionString: postgresql://infrawise_ro
|
|
239
|
+
connectionString: postgresql://infrawise_ro:${DB_PASSWORD}@host:5432/mydb
|
|
195
240
|
|
|
196
241
|
mysql:
|
|
197
242
|
enabled: false
|
|
@@ -220,6 +265,9 @@ lambda:
|
|
|
220
265
|
rds:
|
|
221
266
|
enabled: false
|
|
222
267
|
|
|
268
|
+
kafka:
|
|
269
|
+
enabled: false
|
|
270
|
+
|
|
223
271
|
cloudwatchLogs:
|
|
224
272
|
enabled: false
|
|
225
273
|
logGroupPrefixes: []
|
|
@@ -284,6 +332,7 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
|
|
|
284
332
|
| PostgreSQL / MySQL schema | Tables, indexes, column types |
|
|
285
333
|
| MongoDB schema | Collections, indexes |
|
|
286
334
|
| SQS | Missing DLQs, unencrypted queues, large backlogs |
|
|
335
|
+
| Kafka (kafkajs) | Producer/consumer topic mapping from code |
|
|
287
336
|
| Secrets Manager | Missing secret rotation |
|
|
288
337
|
| Lambda | Default memory (128 MB), high timeouts |
|
|
289
338
|
| RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
|
|
@@ -309,7 +358,7 @@ Uses [ts-morph](https://ts-morph.com/) AST analysis to detect which functions ca
|
|
|
309
358
|
|
|
310
359
|
Non-TypeScript/JavaScript projects still get full value from infrastructure-level analyzers — code correlation (function-to-table mapping, N+1 patterns) is skipped.
|
|
311
360
|
|
|
312
|
-
The scanner supports: AWS SDK v3/v2 for DynamoDB, `pg`/Prisma/Knex for PostgreSQL, `mysql2`/Knex for MySQL, driver/Mongoose for MongoDB,
|
|
361
|
+
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
362
|
|
|
314
363
|
---
|
|
315
364
|
|
|
@@ -399,6 +448,7 @@ src/
|
|
|
399
448
|
- Kubernetes workload graphing
|
|
400
449
|
- VS Code extension
|
|
401
450
|
- Infrastructure drift detection
|
|
451
|
+
- MSK (Amazon Managed Streaming for Apache Kafka) — cluster metadata + topic listing via MSK API and Kafka admin client
|
|
402
452
|
|
|
403
453
|
### Under consideration
|
|
404
454
|
- OpenTelemetry integration
|
package/dist/adapters/aws.js
CHANGED
|
@@ -244,7 +244,7 @@ async function extractRDSMetadata(cfg = {}) {
|
|
|
244
244
|
instanceClass: db.DBInstanceClass ?? '',
|
|
245
245
|
publiclyAccessible: db.PubliclyAccessible ?? false,
|
|
246
246
|
storageEncrypted: db.StorageEncrypted ?? false,
|
|
247
|
-
|
|
247
|
+
backupRetentionDays: db.BackupRetentionPeriod ?? 0,
|
|
248
248
|
deletionProtection: db.DeletionProtection ?? false,
|
|
249
249
|
multiAZ: db.MultiAZ ?? false,
|
|
250
250
|
dbInstanceStatus: db.DBInstanceStatus ?? '',
|
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");
|
|
@@ -44,7 +44,7 @@ function topPatterns(messages, limit = 5) {
|
|
|
44
44
|
.slice(0, limit)
|
|
45
45
|
.map(([pattern, count]) => ({ pattern, count }));
|
|
46
46
|
}
|
|
47
|
-
async function
|
|
47
|
+
async function extractLogsMetadata(cfg = {}) {
|
|
48
48
|
const client = new client_cloudwatch_logs_1.CloudWatchLogsClient(clientConfig(cfg));
|
|
49
49
|
const windowMs = (cfg.windowHours ?? 24) * 60 * 60 * 1000;
|
|
50
50
|
const startTime = Date.now() - windowMs;
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.runAnalyze = runAnalyze;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
40
41
|
const path = __importStar(require("path"));
|
|
41
42
|
const chalk_1 = __importDefault(require("chalk"));
|
|
42
43
|
const ora_1 = __importDefault(require("ora"));
|
|
@@ -52,16 +53,72 @@ const context_1 = require("../../context");
|
|
|
52
53
|
const graph_1 = require("../../graph");
|
|
53
54
|
const analyzers_1 = require("../../analyzers");
|
|
54
55
|
const utils_1 = require("../utils");
|
|
56
|
+
const SEVERITY_RANK = { high: 3, medium: 2, low: 1, never: 0 };
|
|
57
|
+
function mkSpinner(text, ci) {
|
|
58
|
+
if (ci) {
|
|
59
|
+
return {
|
|
60
|
+
succeed: (msg) => console.log(` ✓ ${msg}`),
|
|
61
|
+
warn: (msg) => console.log(` ⚠ ${msg}`),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return (0, ora_1.default)({ text: chalk_1.default.dim(text), color: 'cyan' }).start();
|
|
65
|
+
}
|
|
66
|
+
function emitCIOutput(findings, failOn) {
|
|
67
|
+
for (const f of findings) {
|
|
68
|
+
const level = f.severity === 'high' ? 'error' : f.severity === 'medium' ? 'warning' : 'notice';
|
|
69
|
+
console.log(`::${level} title=Infrawise [${f.severity.toUpperCase()}]::${f.issue} — ${f.description}`);
|
|
70
|
+
}
|
|
71
|
+
const summaryPath = process.env['GITHUB_STEP_SUMMARY'];
|
|
72
|
+
if (summaryPath) {
|
|
73
|
+
const high = findings.filter((f) => f.severity === 'high').length;
|
|
74
|
+
const medium = findings.filter((f) => f.severity === 'medium').length;
|
|
75
|
+
const low = findings.filter((f) => f.severity === 'low').length;
|
|
76
|
+
const rows = findings.map((f) => {
|
|
77
|
+
const icon = f.severity === 'high' ? '🔴' : f.severity === 'medium' ? '🟡' : '🔵';
|
|
78
|
+
return `| ${icon} ${f.severity.toUpperCase()} | ${f.issue} | ${f.recommendation} |`;
|
|
79
|
+
}).join('\n');
|
|
80
|
+
const summary = findings.length === 0
|
|
81
|
+
? '## ✅ Infrawise — no issues found\n'
|
|
82
|
+
: [
|
|
83
|
+
'## Infrawise Analysis',
|
|
84
|
+
'',
|
|
85
|
+
`**${high} high · ${medium} medium · ${low} low**`,
|
|
86
|
+
'',
|
|
87
|
+
'| Severity | Issue | Recommendation |',
|
|
88
|
+
'|---|---|---|',
|
|
89
|
+
rows,
|
|
90
|
+
].join('\n');
|
|
91
|
+
fs.appendFileSync(summaryPath, summary + '\n');
|
|
92
|
+
}
|
|
93
|
+
const threshold = SEVERITY_RANK[failOn] ?? 3;
|
|
94
|
+
const maxFound = Math.max(0, ...findings.map((f) => SEVERITY_RANK[f.severity] ?? 0));
|
|
95
|
+
if (maxFound >= threshold && threshold > 0) {
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
55
99
|
async function runAnalyze(options = {}) {
|
|
56
|
-
|
|
100
|
+
const ci = options.ci ?? false;
|
|
101
|
+
const failOn = options.failOn ?? 'high';
|
|
102
|
+
if (!ci)
|
|
103
|
+
(0, utils_1.printHeader)('Running Analysis');
|
|
57
104
|
let config;
|
|
105
|
+
let isFallback = false;
|
|
58
106
|
try {
|
|
59
107
|
config = (0, core_1.loadConfig)(options.config);
|
|
60
|
-
|
|
108
|
+
if (!ci)
|
|
109
|
+
utils_1.log.success('Config loaded', options.config ?? 'infrawise.yaml');
|
|
61
110
|
}
|
|
62
111
|
catch (err) {
|
|
63
|
-
|
|
64
|
-
|
|
112
|
+
const isNotFound = err instanceof Error && err.message.includes('not found at');
|
|
113
|
+
if (ci && isNotFound) {
|
|
114
|
+
config = { project: path.basename(process.cwd()) };
|
|
115
|
+
isFallback = true;
|
|
116
|
+
console.log(' ✓ No infrawise.yaml — running code-only analysis (repo scan + IaC)');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.error((0, core_1.formatError)(err));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
65
122
|
}
|
|
66
123
|
const repoPath = options.repo ?? process.cwd();
|
|
67
124
|
const awsCfg = { region: config.aws?.region, profile: config.aws?.profile, endpoint: config.aws?.endpoint };
|
|
@@ -72,145 +129,145 @@ async function runAnalyze(options = {}) {
|
|
|
72
129
|
const servicesMeta = {};
|
|
73
130
|
// ── DynamoDB ────────────────────────────────────────────────────────────────
|
|
74
131
|
if (config.dynamodb?.enabled === true) {
|
|
75
|
-
const
|
|
132
|
+
const s = mkSpinner('Extracting DynamoDB tables...', ci);
|
|
76
133
|
try {
|
|
77
134
|
const result = await (0, dynamodb_1.extractDynamoMetadata)(config);
|
|
78
135
|
dynamoMeta.push(...result);
|
|
79
|
-
|
|
136
|
+
s.succeed(chalk_1.default.green('DynamoDB') + chalk_1.default.dim(` ${result.length} table(s)`));
|
|
80
137
|
}
|
|
81
138
|
catch (err) {
|
|
82
|
-
|
|
139
|
+
s.warn(chalk_1.default.yellow('DynamoDB skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
83
140
|
}
|
|
84
141
|
}
|
|
85
142
|
// ── PostgreSQL ──────────────────────────────────────────────────────────────
|
|
86
143
|
if (config.postgres?.enabled && config.postgres.connectionString) {
|
|
87
|
-
const
|
|
144
|
+
const s = mkSpinner('Extracting PostgreSQL schema...', ci);
|
|
88
145
|
try {
|
|
89
146
|
const result = await (0, postgres_1.extractPostgresMetadata)(config.postgres.connectionString);
|
|
90
147
|
postgresMeta.push(...result);
|
|
91
|
-
|
|
148
|
+
s.succeed(chalk_1.default.green('PostgreSQL') + chalk_1.default.dim(` ${result.length} table(s)`));
|
|
92
149
|
}
|
|
93
150
|
catch (err) {
|
|
94
|
-
|
|
151
|
+
s.warn(chalk_1.default.yellow('PostgreSQL skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
95
152
|
}
|
|
96
153
|
}
|
|
97
154
|
// ── MySQL ───────────────────────────────────────────────────────────────────
|
|
98
155
|
if (config.mysql?.enabled && config.mysql.connectionString) {
|
|
99
|
-
const
|
|
156
|
+
const s = mkSpinner('Extracting MySQL schema...', ci);
|
|
100
157
|
try {
|
|
101
158
|
const result = await (0, mysql_1.extractMySQLMetadata)(config.mysql.connectionString);
|
|
102
159
|
mysqlMeta.push(...result);
|
|
103
|
-
|
|
160
|
+
s.succeed(chalk_1.default.green('MySQL') + chalk_1.default.dim(` ${result.length} table(s)`));
|
|
104
161
|
}
|
|
105
162
|
catch (err) {
|
|
106
|
-
|
|
163
|
+
s.warn(chalk_1.default.yellow('MySQL skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
107
164
|
}
|
|
108
165
|
}
|
|
109
166
|
// ── MongoDB ─────────────────────────────────────────────────────────────────
|
|
110
167
|
if (config.mongodb?.enabled && config.mongodb.connectionString) {
|
|
111
|
-
const
|
|
168
|
+
const s = mkSpinner('Extracting MongoDB schema...', ci);
|
|
112
169
|
try {
|
|
113
170
|
const result = await (0, mongodb_1.extractMongoMetadata)(config.mongodb.connectionString, config.mongodb.databases);
|
|
114
171
|
mongoMeta.push(...result);
|
|
115
|
-
|
|
172
|
+
s.succeed(chalk_1.default.green('MongoDB') + chalk_1.default.dim(` ${result.length} collection(s)`));
|
|
116
173
|
}
|
|
117
174
|
catch (err) {
|
|
118
|
-
|
|
175
|
+
s.warn(chalk_1.default.yellow('MongoDB skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
119
176
|
}
|
|
120
177
|
}
|
|
121
178
|
// ── SQS ─────────────────────────────────────────────────────────────────────
|
|
122
179
|
if (config.sqs?.enabled === true) {
|
|
123
|
-
const
|
|
180
|
+
const s = mkSpinner('Extracting SQS queues...', ci);
|
|
124
181
|
try {
|
|
125
182
|
const result = await (0, aws_1.extractSQSMetadata)(awsCfg);
|
|
126
183
|
servicesMeta.sqs = result;
|
|
127
|
-
|
|
184
|
+
s.succeed(chalk_1.default.green('SQS') + chalk_1.default.dim(` ${result.length} queue(s)`));
|
|
128
185
|
}
|
|
129
186
|
catch (err) {
|
|
130
|
-
|
|
187
|
+
s.warn(chalk_1.default.yellow('SQS skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
131
188
|
}
|
|
132
189
|
}
|
|
133
190
|
// ── SNS ─────────────────────────────────────────────────────────────────────
|
|
134
191
|
if (config.sns?.enabled === true) {
|
|
135
|
-
const
|
|
192
|
+
const s = mkSpinner('Extracting SNS topics...', ci);
|
|
136
193
|
try {
|
|
137
194
|
const result = await (0, aws_1.extractSNSMetadata)(awsCfg);
|
|
138
195
|
servicesMeta.sns = result;
|
|
139
|
-
|
|
196
|
+
s.succeed(chalk_1.default.green('SNS') + chalk_1.default.dim(` ${result.length} topic(s)`));
|
|
140
197
|
}
|
|
141
198
|
catch (err) {
|
|
142
|
-
|
|
199
|
+
s.warn(chalk_1.default.yellow('SNS skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
143
200
|
}
|
|
144
201
|
}
|
|
145
202
|
// ── SSM Parameter Store ──────────────────────────────────────────────────────
|
|
146
203
|
if (config.ssm?.enabled === true) {
|
|
147
|
-
const
|
|
204
|
+
const s = mkSpinner('Extracting SSM parameters...', ci);
|
|
148
205
|
try {
|
|
149
206
|
const result = await (0, aws_1.extractSSMMetadata)({ ...awsCfg, paths: config.ssm?.paths });
|
|
150
207
|
servicesMeta.ssm = result;
|
|
151
|
-
|
|
208
|
+
s.succeed(chalk_1.default.green('SSM') + chalk_1.default.dim(` ${result.length} parameter(s) `) + chalk_1.default.dim('(metadata only, no values)'));
|
|
152
209
|
}
|
|
153
210
|
catch (err) {
|
|
154
|
-
|
|
211
|
+
s.warn(chalk_1.default.yellow('SSM skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
155
212
|
}
|
|
156
213
|
}
|
|
157
214
|
// ── Secrets Manager ──────────────────────────────────────────────────────────
|
|
158
215
|
if (config.secretsManager?.enabled === true) {
|
|
159
|
-
const
|
|
216
|
+
const s = mkSpinner('Extracting Secrets Manager metadata...', ci);
|
|
160
217
|
try {
|
|
161
218
|
const result = await (0, aws_1.extractSecretsMetadata)(awsCfg);
|
|
162
219
|
servicesMeta.secrets = result;
|
|
163
|
-
|
|
220
|
+
s.succeed(chalk_1.default.green('Secrets Manager') + chalk_1.default.dim(` ${result.length} secret(s) `) + chalk_1.default.dim('(names/rotation only, no values)'));
|
|
164
221
|
}
|
|
165
222
|
catch (err) {
|
|
166
|
-
|
|
223
|
+
s.warn(chalk_1.default.yellow('Secrets Manager skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
167
224
|
}
|
|
168
225
|
}
|
|
169
226
|
// ── Lambda ───────────────────────────────────────────────────────────────────
|
|
170
227
|
if (config.lambda?.enabled === true) {
|
|
171
|
-
const
|
|
228
|
+
const s = mkSpinner('Extracting Lambda functions...', ci);
|
|
172
229
|
try {
|
|
173
230
|
const result = await (0, aws_1.extractLambdaMetadata)(awsCfg);
|
|
174
231
|
servicesMeta.lambda = result;
|
|
175
|
-
|
|
232
|
+
s.succeed(chalk_1.default.green('Lambda') + chalk_1.default.dim(` ${result.length} function(s)`));
|
|
176
233
|
}
|
|
177
234
|
catch (err) {
|
|
178
|
-
|
|
235
|
+
s.warn(chalk_1.default.yellow('Lambda skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
179
236
|
}
|
|
180
237
|
}
|
|
181
238
|
// ── RDS ──────────────────────────────────────────────────────────────────────
|
|
182
239
|
if (config.rds?.enabled === true) {
|
|
183
|
-
const
|
|
240
|
+
const s = mkSpinner('Extracting RDS instances...', ci);
|
|
184
241
|
try {
|
|
185
242
|
const result = await (0, aws_1.extractRDSMetadata)(awsCfg);
|
|
186
243
|
servicesMeta.rds = result;
|
|
187
|
-
|
|
244
|
+
s.succeed(chalk_1.default.green('RDS') + chalk_1.default.dim(` ${result.length} instance(s)`));
|
|
188
245
|
}
|
|
189
246
|
catch (err) {
|
|
190
|
-
|
|
247
|
+
s.warn(chalk_1.default.yellow('RDS skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
191
248
|
}
|
|
192
249
|
}
|
|
193
250
|
// ── CloudWatch Logs ──────────────────────────────────────────────────────────
|
|
194
251
|
if (config.cloudwatchLogs?.enabled) {
|
|
195
|
-
const
|
|
252
|
+
const s = mkSpinner('Sampling CloudWatch Logs (errors only, max 50 groups)...', ci);
|
|
196
253
|
try {
|
|
197
|
-
const result = await (0, logs_1.
|
|
254
|
+
const result = await (0, logs_1.extractLogsMetadata)({
|
|
198
255
|
...awsCfg,
|
|
199
256
|
logGroupPrefixes: config.cloudwatchLogs.logGroupPrefixes,
|
|
200
257
|
windowHours: config.cloudwatchLogs.windowHours,
|
|
201
258
|
});
|
|
202
259
|
servicesMeta.logs = result;
|
|
203
260
|
const errorGroups = result.filter((lg) => lg.errorCount > 0).length;
|
|
204
|
-
|
|
261
|
+
s.succeed(chalk_1.default.green('CloudWatch Logs') + chalk_1.default.dim(` ${result.length} group(s), ${errorGroups} with errors`));
|
|
205
262
|
}
|
|
206
263
|
catch (err) {
|
|
207
|
-
|
|
264
|
+
s.warn(chalk_1.default.yellow('CloudWatch Logs skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
208
265
|
}
|
|
209
266
|
}
|
|
210
267
|
// ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
|
|
211
268
|
let iacDriftAnalyzer;
|
|
212
269
|
{
|
|
213
|
-
const
|
|
270
|
+
const s = mkSpinner('Extracting IaC schema (Terraform / CloudFormation / CDK)...', ci);
|
|
214
271
|
try {
|
|
215
272
|
const iacSchema = await (0, terraform_1.extractIaCSchema)(repoPath);
|
|
216
273
|
const total = iacSchema.dynamoTables.length + iacSchema.rdsInstances.length +
|
|
@@ -219,52 +276,52 @@ async function runAnalyze(options = {}) {
|
|
|
219
276
|
iacSchema.secrets.length + iacSchema.apiGateways.length;
|
|
220
277
|
iacDriftAnalyzer = new analyzers_1.IaCDriftAnalyzer();
|
|
221
278
|
iacDriftAnalyzer.setIaCSchema(iacSchema);
|
|
222
|
-
|
|
279
|
+
s.succeed(chalk_1.default.green('IaC schema') + chalk_1.default.dim(` ${total} resource(s) across TF/CFN/CDK`));
|
|
223
280
|
}
|
|
224
281
|
catch (err) {
|
|
225
|
-
|
|
282
|
+
s.warn(chalk_1.default.yellow('IaC scan skipped') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
226
283
|
}
|
|
227
284
|
}
|
|
228
285
|
// ── Repository scan ──────────────────────────────────────────────────────────
|
|
229
286
|
let operations;
|
|
230
287
|
{
|
|
231
|
-
const
|
|
288
|
+
const s = mkSpinner(`Scanning ${path.basename(repoPath)} for service usage...`, ci);
|
|
232
289
|
try {
|
|
233
290
|
operations = await (0, context_1.scanRepository)(repoPath);
|
|
234
|
-
|
|
291
|
+
s.succeed(chalk_1.default.green('Repository scanned') + chalk_1.default.dim(` ${operations.length} service operation(s) found`));
|
|
235
292
|
}
|
|
236
293
|
catch (err) {
|
|
237
|
-
|
|
294
|
+
s.warn(chalk_1.default.yellow('Repository scan failed') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
238
295
|
operations = [];
|
|
239
296
|
}
|
|
240
297
|
}
|
|
241
298
|
// ── Build graph ──────────────────────────────────────────────────────────────
|
|
242
299
|
let graph;
|
|
243
300
|
{
|
|
244
|
-
const
|
|
301
|
+
const s = mkSpinner('Building infrastructure graph...', ci);
|
|
245
302
|
graph = (0, graph_1.buildGraph)(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
|
|
246
|
-
|
|
303
|
+
s.succeed(chalk_1.default.green('Graph built') + chalk_1.default.dim(` ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
|
|
247
304
|
}
|
|
248
305
|
// ── Run analyzers ────────────────────────────────────────────────────────────
|
|
249
306
|
let findings;
|
|
250
307
|
{
|
|
251
|
-
const
|
|
308
|
+
const s = mkSpinner('Running analyzers...', ci);
|
|
252
309
|
const analyzers = [
|
|
253
|
-
...(config.dynamodb?.enabled === true ? [
|
|
310
|
+
...(config.dynamodb?.enabled === true || isFallback ? [
|
|
254
311
|
new analyzers_1.FullTableScanAnalyzer(),
|
|
255
312
|
new analyzers_1.MissingGSIAnalyzer(),
|
|
256
313
|
new analyzers_1.HotPartitionAnalyzer(),
|
|
257
314
|
] : []),
|
|
258
|
-
...(config.postgres?.enabled ? [
|
|
315
|
+
...(config.postgres?.enabled || isFallback ? [
|
|
259
316
|
new analyzers_1.MissingIndexAnalyzer(),
|
|
260
317
|
new analyzers_1.NplusOneAnalyzer(),
|
|
261
318
|
new analyzers_1.LargeSelectAnalyzer(),
|
|
262
319
|
] : []),
|
|
263
|
-
...(config.mysql?.enabled ? [
|
|
320
|
+
...(config.mysql?.enabled || isFallback ? [
|
|
264
321
|
new analyzers_1.MissingMySQLIndexAnalyzer(),
|
|
265
322
|
new analyzers_1.MySQLFullTableScanAnalyzer(),
|
|
266
323
|
] : []),
|
|
267
|
-
...(config.mongodb?.enabled ? [
|
|
324
|
+
...(config.mongodb?.enabled || isFallback ? [
|
|
268
325
|
new analyzers_1.MissingMongoIndexAnalyzer(),
|
|
269
326
|
new analyzers_1.MongoCollectionScanAnalyzer(),
|
|
270
327
|
] : []),
|
|
@@ -293,13 +350,17 @@ async function runAnalyze(options = {}) {
|
|
|
293
350
|
...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
|
|
294
351
|
];
|
|
295
352
|
findings = await (0, analyzers_1.runAllAnalyzers)(graph, analyzers);
|
|
296
|
-
|
|
353
|
+
s.succeed(chalk_1.default.green('Analysis complete') + chalk_1.default.dim(` ${findings.length} finding(s)`));
|
|
297
354
|
}
|
|
298
355
|
// ── Cache ─────────────────────────────────────────────────────────────────────
|
|
299
356
|
(0, core_1.writeCache)('graph', graph);
|
|
300
357
|
(0, core_1.writeCache)('findings', findings);
|
|
301
358
|
(0, core_1.writeCache)('operations', operations);
|
|
302
359
|
// ── Output ────────────────────────────────────────────────────────────────────
|
|
360
|
+
if (ci) {
|
|
361
|
+
emitCIOutput(findings, failOn);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
303
364
|
console.log('');
|
|
304
365
|
if (findings.length === 0) {
|
|
305
366
|
console.log(` ${chalk_1.default.green.bold('✓ No issues found!')} ${chalk_1.default.dim('Your infrastructure looks clean.')}`);
|
package/dist/cli/index.js
CHANGED
|
@@ -37,12 +37,17 @@ program
|
|
|
37
37
|
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
38
38
|
.option('-r, --repo <path>', 'Path to repository to scan', process.cwd())
|
|
39
39
|
.option('--no-cache', 'Skip reading/writing the cache')
|
|
40
|
+
.option('--ci', 'CI mode: GitHub annotations output, job summary, exit code on findings')
|
|
41
|
+
.option('--fail-on <severity>', 'Exit 1 if findings at or above this severity (high|medium|low|never)', 'high')
|
|
40
42
|
.action(async (options) => {
|
|
41
|
-
(
|
|
43
|
+
if (!options.ci)
|
|
44
|
+
(0, utils_1.printBanner)();
|
|
42
45
|
await (0, analyze_1.runAnalyze)({
|
|
43
46
|
config: options.config !== 'infrawise.yaml' ? options.config : undefined,
|
|
44
47
|
repo: options.repo,
|
|
45
48
|
noCache: !options.cache,
|
|
49
|
+
ci: options.ci ?? false,
|
|
50
|
+
failOn: options.failOn ?? 'high',
|
|
46
51
|
});
|
|
47
52
|
});
|
|
48
53
|
program
|
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
|
@@ -109,6 +109,11 @@ function loadConfig(configPath) {
|
|
|
109
109
|
catch (err) {
|
|
110
110
|
throw new ConfigError(`Unable to read configuration file: ${resolvedPath}`, [String(err)]);
|
|
111
111
|
}
|
|
112
|
+
// Expand ${ENV_VAR} references — lets users store secrets in env, not in the YAML file
|
|
113
|
+
rawContent = rawContent.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => {
|
|
114
|
+
const val = process.env[name];
|
|
115
|
+
return val !== undefined ? val : match;
|
|
116
|
+
});
|
|
112
117
|
let parsedYaml;
|
|
113
118
|
try {
|
|
114
119
|
parsedYaml = yaml.load(rawContent);
|
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.1",
|
|
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",
|