infrawise 0.4.0 → 0.4.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 +48 -2
- package/dist/cli/commands/analyze.js +112 -51
- package/dist/cli/index.js +6 -1
- package/dist/core/config.js +5 -0
- 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@v0
|
|
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
|
|
@@ -466,7 +511,8 @@ pnpm release minor # 0.1.2 → 0.2.0 (new features, backwards compatible)
|
|
|
466
511
|
pnpm release major # 0.1.2 → 1.0.0 (breaking changes)
|
|
467
512
|
pnpm release 1.5.0 # explicit version
|
|
468
513
|
|
|
469
|
-
git push origin main
|
|
514
|
+
git push origin main v1.2.3
|
|
515
|
+
git push origin v1 --force # update the floating major tag
|
|
470
516
|
```
|
|
471
517
|
|
|
472
518
|
Pushing a `v*.*.*` tag creates a draft GitHub release. Publishing that release triggers npm publish automatically.
|
|
@@ -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,127 +129,127 @@ 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
254
|
const result = await (0, logs_1.extractLogsMetadata)({
|
|
198
255
|
...awsCfg,
|
|
@@ -201,16 +258,16 @@ async function runAnalyze(options = {}) {
|
|
|
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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|