infrawise 0.12.4 → 0.12.5
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/dist/cli/commands/analyze.js +100 -311
- package/dist/cli/commands/serve.js +172 -2
- package/dist/cli/commands/stdio.js +3 -3
- package/dist/core/config.js +10 -6
- package/dist/core/errors.js +0 -21
- package/dist/core/index.js +2 -2
- package/dist/server/index.js +2 -1
- package/package.json +1 -1
- package/dist/cli/commands/dev.js +0 -172
|
@@ -46,6 +46,82 @@ function buildMarkdownReport(findings, projectName) {
|
|
|
46
46
|
function mkSpinner(text) {
|
|
47
47
|
return ora({ text: chalk.dim(text), color: 'cyan' }).start();
|
|
48
48
|
}
|
|
49
|
+
// Runs one extractor behind a spinner: succeeds with a count summary, or warns and
|
|
50
|
+
// returns undefined on failure so a single service never aborts the whole analysis.
|
|
51
|
+
async function extract(enabled, spinnerText, label, fn, summarize) {
|
|
52
|
+
if (!enabled)
|
|
53
|
+
return undefined;
|
|
54
|
+
const s = mkSpinner(spinnerText);
|
|
55
|
+
try {
|
|
56
|
+
const result = await fn();
|
|
57
|
+
s.succeed(chalk.green(label) + chalk.dim(` ${summarize(result)}`));
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
s.warn(chalk.yellow(`${label} skipped`) +
|
|
62
|
+
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Single source of truth for which analyzers run given the config. Used by both the
|
|
67
|
+
// full `runAnalyze` and the cache-backed `runCodeRefresh` so the two never drift.
|
|
68
|
+
function buildAnalyzers(config, iacDriftAnalyzer, iacLambdas) {
|
|
69
|
+
const pipelineAnalyzer = new PipelineAnalyzer();
|
|
70
|
+
pipelineAnalyzer.setIaCLambdas(iacLambdas);
|
|
71
|
+
return [
|
|
72
|
+
...(config.dynamodb?.enabled === true
|
|
73
|
+
? [
|
|
74
|
+
new FullTableScanAnalyzer(),
|
|
75
|
+
new MissingGSIAnalyzer(),
|
|
76
|
+
new HotPartitionAnalyzer(config.analysis?.hotPartitionThreshold, config.analysis?.hotPartitionThresholds),
|
|
77
|
+
]
|
|
78
|
+
: []),
|
|
79
|
+
...(config.postgres?.enabled
|
|
80
|
+
? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
|
|
81
|
+
: []),
|
|
82
|
+
...(config.mysql?.enabled
|
|
83
|
+
? [new MissingMySQLIndexAnalyzer(), new MySQLFullTableScanAnalyzer()]
|
|
84
|
+
: []),
|
|
85
|
+
...(config.mongodb?.enabled
|
|
86
|
+
? [new MissingMongoIndexAnalyzer(), new MongoCollectionScanAnalyzer()]
|
|
87
|
+
: []),
|
|
88
|
+
...(config.sqs?.enabled === true
|
|
89
|
+
? [
|
|
90
|
+
new MissingDLQAnalyzer(),
|
|
91
|
+
new UnencryptedQueueAnalyzer(),
|
|
92
|
+
new LargeQueueBacklogAnalyzer(),
|
|
93
|
+
new VisibilityTimeoutMismatchAnalyzer(),
|
|
94
|
+
]
|
|
95
|
+
: []),
|
|
96
|
+
...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
|
|
97
|
+
...(config.cloudwatchLogs?.enabled ? [new MissingLogRetentionAnalyzer()] : []),
|
|
98
|
+
...(config.lambda?.enabled === true
|
|
99
|
+
? [
|
|
100
|
+
new LambdaDefaultMemoryAnalyzer(),
|
|
101
|
+
new LambdaHighTimeoutAnalyzer(),
|
|
102
|
+
new LambdaMissingTriggerDLQAnalyzer(),
|
|
103
|
+
]
|
|
104
|
+
: []),
|
|
105
|
+
...(config.rds?.enabled === true
|
|
106
|
+
? [
|
|
107
|
+
new RDSPubliclyAccessibleAnalyzer(),
|
|
108
|
+
new RDSNoBackupAnalyzer(),
|
|
109
|
+
new RDSUnencryptedAnalyzer(),
|
|
110
|
+
new RDSNoDeletionProtectionAnalyzer(),
|
|
111
|
+
new RDSNoMultiAZAnalyzer(),
|
|
112
|
+
]
|
|
113
|
+
: []),
|
|
114
|
+
...(config.s3?.enabled === true
|
|
115
|
+
? [
|
|
116
|
+
new S3PublicAccessAnalyzer(),
|
|
117
|
+
new S3MissingVersioningAnalyzer(),
|
|
118
|
+
new S3UnencryptedAnalyzer(),
|
|
119
|
+
]
|
|
120
|
+
: []),
|
|
121
|
+
...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
|
|
122
|
+
pipelineAnalyzer,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
49
125
|
export async function runAnalyze(options = {}) {
|
|
50
126
|
printHeader('Running Analysis');
|
|
51
127
|
let config;
|
|
@@ -64,204 +140,29 @@ export async function runAnalyze(options = {}) {
|
|
|
64
140
|
region: config.aws?.region,
|
|
65
141
|
profile: config.aws?.profile,
|
|
66
142
|
};
|
|
67
|
-
const dynamoMeta = [];
|
|
68
|
-
const postgresMeta = [];
|
|
69
|
-
const mysqlMeta = [];
|
|
70
|
-
const mongoMeta = [];
|
|
71
143
|
const servicesMeta = {};
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
s.warn(chalk.yellow('PostgreSQL skipped') +
|
|
95
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// ── MySQL ───────────────────────────────────────────────────────────────────
|
|
99
|
-
if (config.mysql?.enabled && config.mysql.connectionString) {
|
|
100
|
-
const s = mkSpinner('Extracting MySQL schema...');
|
|
101
|
-
try {
|
|
102
|
-
const result = await extractMySQLMetadata(config.mysql.connectionString);
|
|
103
|
-
mysqlMeta.push(...result);
|
|
104
|
-
s.succeed(chalk.green('MySQL') + chalk.dim(` ${result.length} table(s)`));
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
s.warn(chalk.yellow('MySQL skipped') +
|
|
108
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// ── MongoDB ─────────────────────────────────────────────────────────────────
|
|
112
|
-
if (config.mongodb?.enabled && config.mongodb.connectionString) {
|
|
113
|
-
const s = mkSpinner('Extracting MongoDB schema...');
|
|
114
|
-
try {
|
|
115
|
-
const result = await extractMongoMetadata(config.mongodb.connectionString, config.mongodb.databases);
|
|
116
|
-
mongoMeta.push(...result);
|
|
117
|
-
s.succeed(chalk.green('MongoDB') + chalk.dim(` ${result.length} collection(s)`));
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
s.warn(chalk.yellow('MongoDB skipped') +
|
|
121
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// ── SQS ─────────────────────────────────────────────────────────────────────
|
|
125
|
-
if (config.sqs?.enabled === true) {
|
|
126
|
-
const s = mkSpinner('Extracting SQS queues...');
|
|
127
|
-
try {
|
|
128
|
-
const result = await extractSQSMetadata(awsCfg);
|
|
129
|
-
servicesMeta.sqs = result;
|
|
130
|
-
s.succeed(chalk.green('SQS') + chalk.dim(` ${result.length} queue(s)`));
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
s.warn(chalk.yellow('SQS skipped') +
|
|
134
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// ── SNS ─────────────────────────────────────────────────────────────────────
|
|
138
|
-
if (config.sns?.enabled === true) {
|
|
139
|
-
const s = mkSpinner('Extracting SNS topics...');
|
|
140
|
-
try {
|
|
141
|
-
const result = await extractSNSMetadata(awsCfg);
|
|
142
|
-
servicesMeta.sns = result;
|
|
143
|
-
s.succeed(chalk.green('SNS') + chalk.dim(` ${result.length} topic(s)`));
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
s.warn(chalk.yellow('SNS skipped') +
|
|
147
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// ── SSM Parameter Store ──────────────────────────────────────────────────────
|
|
151
|
-
if (config.ssm?.enabled === true) {
|
|
152
|
-
const s = mkSpinner('Extracting SSM parameters...');
|
|
153
|
-
try {
|
|
154
|
-
const result = await extractSSMMetadata({ ...awsCfg, paths: config.ssm?.paths });
|
|
155
|
-
servicesMeta.ssm = result;
|
|
156
|
-
s.succeed(chalk.green('SSM') +
|
|
157
|
-
chalk.dim(` ${result.length} parameter(s) `) +
|
|
158
|
-
chalk.dim('(metadata only, no values)'));
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
s.warn(chalk.yellow('SSM skipped') +
|
|
162
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// ── Secrets Manager ──────────────────────────────────────────────────────────
|
|
166
|
-
if (config.secretsManager?.enabled === true) {
|
|
167
|
-
const s = mkSpinner('Extracting Secrets Manager metadata...');
|
|
168
|
-
try {
|
|
169
|
-
const result = await extractSecretsMetadata(awsCfg);
|
|
170
|
-
servicesMeta.secrets = result;
|
|
171
|
-
s.succeed(chalk.green('Secrets Manager') +
|
|
172
|
-
chalk.dim(` ${result.length} secret(s) `) +
|
|
173
|
-
chalk.dim('(names/rotation only, no values)'));
|
|
174
|
-
}
|
|
175
|
-
catch (err) {
|
|
176
|
-
s.warn(chalk.yellow('Secrets Manager skipped') +
|
|
177
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
// ── Lambda ───────────────────────────────────────────────────────────────────
|
|
181
|
-
if (config.lambda?.enabled === true) {
|
|
182
|
-
const s = mkSpinner('Extracting Lambda functions...');
|
|
183
|
-
try {
|
|
184
|
-
const result = await extractLambdaMetadata(awsCfg, config.lambda?.includeFunctions);
|
|
185
|
-
servicesMeta.lambda = result;
|
|
186
|
-
s.succeed(chalk.green('Lambda') + chalk.dim(` ${result.length} function(s)`));
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
s.warn(chalk.yellow('Lambda skipped') +
|
|
190
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// ── EventBridge ──────────────────────────────────────────────────────────────
|
|
194
|
-
if (config.eventbridge?.enabled === true) {
|
|
195
|
-
const s = mkSpinner('Extracting EventBridge rules...');
|
|
196
|
-
try {
|
|
197
|
-
const result = await extractEventBridgeMetadata(awsCfg);
|
|
198
|
-
servicesMeta.eventbridge = result;
|
|
199
|
-
s.succeed(chalk.green('EventBridge') + chalk.dim(` ${result.length} rule(s)`));
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
s.warn(chalk.yellow('EventBridge skipped') +
|
|
203
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// ── RDS ──────────────────────────────────────────────────────────────────────
|
|
207
|
-
if (config.rds?.enabled === true) {
|
|
208
|
-
const s = mkSpinner('Extracting RDS instances...');
|
|
209
|
-
try {
|
|
210
|
-
const result = await extractRDSMetadata(awsCfg);
|
|
211
|
-
servicesMeta.rds = result;
|
|
212
|
-
s.succeed(chalk.green('RDS') + chalk.dim(` ${result.length} instance(s)`));
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
s.warn(chalk.yellow('RDS skipped') +
|
|
216
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
// ── S3 ────────────────────────────────────────────────────────────────────────
|
|
220
|
-
if (config.s3?.enabled === true) {
|
|
221
|
-
const s = mkSpinner('Extracting S3 buckets...');
|
|
222
|
-
try {
|
|
223
|
-
const result = await extractS3Metadata(awsCfg);
|
|
224
|
-
servicesMeta.s3 = result;
|
|
225
|
-
s.succeed(chalk.green('S3') + chalk.dim(` ${result.length} bucket(s)`));
|
|
226
|
-
}
|
|
227
|
-
catch (err) {
|
|
228
|
-
s.warn(chalk.yellow('S3 skipped') +
|
|
229
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// ── API Gateway ───────────────────────────────────────────────────────────────
|
|
233
|
-
if (config.apiGateway?.enabled === true) {
|
|
234
|
-
const s = mkSpinner('Extracting API Gateway routes...');
|
|
235
|
-
try {
|
|
236
|
-
const result = await extractAPIGatewayMetadata(awsCfg);
|
|
237
|
-
servicesMeta.apiGateway = result;
|
|
238
|
-
const routeCount = result.reduce((sum, api) => sum + api.routes.length, 0);
|
|
239
|
-
s.succeed(chalk.green('API Gateway') + chalk.dim(` ${result.length} API(s), ${routeCount} route(s)`));
|
|
240
|
-
}
|
|
241
|
-
catch (err) {
|
|
242
|
-
s.warn(chalk.yellow('API Gateway skipped') +
|
|
243
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
// ── CloudWatch Logs ──────────────────────────────────────────────────────────
|
|
247
|
-
if (config.cloudwatchLogs?.enabled) {
|
|
248
|
-
const s = mkSpinner('Sampling CloudWatch Logs (errors only, max 50 groups)...');
|
|
249
|
-
try {
|
|
250
|
-
const result = await extractLogsMetadata({
|
|
251
|
-
...awsCfg,
|
|
252
|
-
logGroupPrefixes: config.cloudwatchLogs.logGroupPrefixes,
|
|
253
|
-
windowHours: config.cloudwatchLogs.windowHours,
|
|
254
|
-
});
|
|
255
|
-
servicesMeta.logs = result;
|
|
256
|
-
const errorGroups = result.filter((lg) => lg.errorCount > 0).length;
|
|
257
|
-
s.succeed(chalk.green('CloudWatch Logs') +
|
|
258
|
-
chalk.dim(` ${result.length} group(s), ${errorGroups} with errors`));
|
|
259
|
-
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
s.warn(chalk.yellow('CloudWatch Logs skipped') +
|
|
262
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
263
|
-
}
|
|
264
|
-
}
|
|
144
|
+
const dynamoMeta = (await extract(config.dynamodb?.enabled === true, 'Extracting DynamoDB tables...', 'DynamoDB', () => extractDynamoMetadata(config), (r) => `${r.length} table(s)`)) ?? [];
|
|
145
|
+
const pgConn = config.postgres?.enabled ? config.postgres.connectionString : undefined;
|
|
146
|
+
const postgresMeta = (await extract(!!pgConn, 'Extracting PostgreSQL schema...', 'PostgreSQL', () => extractPostgresMetadata(pgConn ?? ''), (r) => `${r.length} table(s)`)) ?? [];
|
|
147
|
+
const mysqlConn = config.mysql?.enabled ? config.mysql.connectionString : undefined;
|
|
148
|
+
const mysqlMeta = (await extract(!!mysqlConn, 'Extracting MySQL schema...', 'MySQL', () => extractMySQLMetadata(mysqlConn ?? ''), (r) => `${r.length} table(s)`)) ?? [];
|
|
149
|
+
const mongoConn = config.mongodb?.enabled ? config.mongodb.connectionString : undefined;
|
|
150
|
+
const mongoMeta = (await extract(!!mongoConn, 'Extracting MongoDB schema...', 'MongoDB', () => extractMongoMetadata(mongoConn ?? '', config.mongodb?.databases), (r) => `${r.length} collection(s)`)) ?? [];
|
|
151
|
+
servicesMeta.sqs = await extract(config.sqs?.enabled === true, 'Extracting SQS queues...', 'SQS', () => extractSQSMetadata(awsCfg), (r) => `${r.length} queue(s)`);
|
|
152
|
+
servicesMeta.sns = await extract(config.sns?.enabled === true, 'Extracting SNS topics...', 'SNS', () => extractSNSMetadata(awsCfg), (r) => `${r.length} topic(s)`);
|
|
153
|
+
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)`);
|
|
154
|
+
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)`);
|
|
155
|
+
servicesMeta.lambda = await extract(config.lambda?.enabled === true, 'Extracting Lambda functions...', 'Lambda', () => extractLambdaMetadata(awsCfg, config.lambda?.includeFunctions), (r) => `${r.length} function(s)`);
|
|
156
|
+
servicesMeta.eventbridge = await extract(config.eventbridge?.enabled === true, 'Extracting EventBridge rules...', 'EventBridge', () => extractEventBridgeMetadata(awsCfg), (r) => `${r.length} rule(s)`);
|
|
157
|
+
servicesMeta.rds = await extract(config.rds?.enabled === true, 'Extracting RDS instances...', 'RDS', () => extractRDSMetadata(awsCfg), (r) => `${r.length} instance(s)`);
|
|
158
|
+
servicesMeta.s3 = await extract(config.s3?.enabled === true, 'Extracting S3 buckets...', 'S3', () => extractS3Metadata(awsCfg), (r) => `${r.length} bucket(s)`);
|
|
159
|
+
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)`);
|
|
160
|
+
const cwLogs = config.cloudwatchLogs;
|
|
161
|
+
servicesMeta.logs = await extract(cwLogs?.enabled, 'Sampling CloudWatch Logs (errors only, max 50 groups)...', 'CloudWatch Logs', () => extractLogsMetadata({
|
|
162
|
+
...awsCfg,
|
|
163
|
+
logGroupPrefixes: cwLogs?.logGroupPrefixes,
|
|
164
|
+
windowHours: cwLogs?.windowHours,
|
|
165
|
+
}), (r) => `${r.length} group(s), ${r.filter((lg) => lg.errorCount > 0).length} with errors`);
|
|
265
166
|
// ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
|
|
266
167
|
let iacDriftAnalyzer;
|
|
267
168
|
let iacLambdas = [];
|
|
@@ -316,63 +217,7 @@ export async function runAnalyze(options = {}) {
|
|
|
316
217
|
let findings;
|
|
317
218
|
{
|
|
318
219
|
const s = mkSpinner('Running analyzers...');
|
|
319
|
-
const
|
|
320
|
-
const hotPartitionThresholds = config.analysis?.hotPartitionThresholds;
|
|
321
|
-
const pipelineAnalyzer = new PipelineAnalyzer();
|
|
322
|
-
pipelineAnalyzer.setIaCLambdas(iacLambdas);
|
|
323
|
-
const analyzers = [
|
|
324
|
-
...(config.dynamodb?.enabled === true
|
|
325
|
-
? [
|
|
326
|
-
new FullTableScanAnalyzer(),
|
|
327
|
-
new MissingGSIAnalyzer(),
|
|
328
|
-
new HotPartitionAnalyzer(hotPartitionThreshold, hotPartitionThresholds),
|
|
329
|
-
]
|
|
330
|
-
: []),
|
|
331
|
-
...(config.postgres?.enabled
|
|
332
|
-
? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
|
|
333
|
-
: []),
|
|
334
|
-
...(config.mysql?.enabled
|
|
335
|
-
? [new MissingMySQLIndexAnalyzer(), new MySQLFullTableScanAnalyzer()]
|
|
336
|
-
: []),
|
|
337
|
-
...(config.mongodb?.enabled
|
|
338
|
-
? [new MissingMongoIndexAnalyzer(), new MongoCollectionScanAnalyzer()]
|
|
339
|
-
: []),
|
|
340
|
-
...(config.sqs?.enabled === true
|
|
341
|
-
? [
|
|
342
|
-
new MissingDLQAnalyzer(),
|
|
343
|
-
new UnencryptedQueueAnalyzer(),
|
|
344
|
-
new LargeQueueBacklogAnalyzer(),
|
|
345
|
-
new VisibilityTimeoutMismatchAnalyzer(),
|
|
346
|
-
]
|
|
347
|
-
: []),
|
|
348
|
-
...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
|
|
349
|
-
...(config.cloudwatchLogs?.enabled ? [new MissingLogRetentionAnalyzer()] : []),
|
|
350
|
-
...(config.lambda?.enabled === true
|
|
351
|
-
? [
|
|
352
|
-
new LambdaDefaultMemoryAnalyzer(),
|
|
353
|
-
new LambdaHighTimeoutAnalyzer(),
|
|
354
|
-
new LambdaMissingTriggerDLQAnalyzer(),
|
|
355
|
-
]
|
|
356
|
-
: []),
|
|
357
|
-
...(config.rds?.enabled === true
|
|
358
|
-
? [
|
|
359
|
-
new RDSPubliclyAccessibleAnalyzer(),
|
|
360
|
-
new RDSNoBackupAnalyzer(),
|
|
361
|
-
new RDSUnencryptedAnalyzer(),
|
|
362
|
-
new RDSNoDeletionProtectionAnalyzer(),
|
|
363
|
-
new RDSNoMultiAZAnalyzer(),
|
|
364
|
-
]
|
|
365
|
-
: []),
|
|
366
|
-
...(config.s3?.enabled === true
|
|
367
|
-
? [
|
|
368
|
-
new S3PublicAccessAnalyzer(),
|
|
369
|
-
new S3MissingVersioningAnalyzer(),
|
|
370
|
-
new S3UnencryptedAnalyzer(),
|
|
371
|
-
]
|
|
372
|
-
: []),
|
|
373
|
-
...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
|
|
374
|
-
pipelineAnalyzer,
|
|
375
|
-
];
|
|
220
|
+
const analyzers = buildAnalyzers(config, iacDriftAnalyzer, iacLambdas);
|
|
376
221
|
findings = await runAllAnalyzers(graph, analyzers);
|
|
377
222
|
s.succeed(chalk.green('Analysis complete') + chalk.dim(` ${findings.length} finding(s)`));
|
|
378
223
|
}
|
|
@@ -450,63 +295,7 @@ export async function runCodeRefresh(repoPath, config) {
|
|
|
450
295
|
operations = [];
|
|
451
296
|
}
|
|
452
297
|
const graph = buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
|
|
453
|
-
const
|
|
454
|
-
const hotPartitionThresholds = config.analysis?.hotPartitionThresholds;
|
|
455
|
-
const pipelineAnalyzer = new PipelineAnalyzer();
|
|
456
|
-
pipelineAnalyzer.setIaCLambdas(iacLambdas);
|
|
457
|
-
const analyzers = [
|
|
458
|
-
...(config.dynamodb?.enabled === true
|
|
459
|
-
? [
|
|
460
|
-
new FullTableScanAnalyzer(),
|
|
461
|
-
new MissingGSIAnalyzer(),
|
|
462
|
-
new HotPartitionAnalyzer(hotPartitionThreshold, hotPartitionThresholds),
|
|
463
|
-
]
|
|
464
|
-
: []),
|
|
465
|
-
...(config.postgres?.enabled
|
|
466
|
-
? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
|
|
467
|
-
: []),
|
|
468
|
-
...(config.mysql?.enabled
|
|
469
|
-
? [new MissingMySQLIndexAnalyzer(), new MySQLFullTableScanAnalyzer()]
|
|
470
|
-
: []),
|
|
471
|
-
...(config.mongodb?.enabled
|
|
472
|
-
? [new MissingMongoIndexAnalyzer(), new MongoCollectionScanAnalyzer()]
|
|
473
|
-
: []),
|
|
474
|
-
...(config.sqs?.enabled === true
|
|
475
|
-
? [
|
|
476
|
-
new MissingDLQAnalyzer(),
|
|
477
|
-
new UnencryptedQueueAnalyzer(),
|
|
478
|
-
new LargeQueueBacklogAnalyzer(),
|
|
479
|
-
new VisibilityTimeoutMismatchAnalyzer(),
|
|
480
|
-
]
|
|
481
|
-
: []),
|
|
482
|
-
...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
|
|
483
|
-
...(config.cloudwatchLogs?.enabled ? [new MissingLogRetentionAnalyzer()] : []),
|
|
484
|
-
...(config.lambda?.enabled === true
|
|
485
|
-
? [
|
|
486
|
-
new LambdaDefaultMemoryAnalyzer(),
|
|
487
|
-
new LambdaHighTimeoutAnalyzer(),
|
|
488
|
-
new LambdaMissingTriggerDLQAnalyzer(),
|
|
489
|
-
]
|
|
490
|
-
: []),
|
|
491
|
-
...(config.rds?.enabled === true
|
|
492
|
-
? [
|
|
493
|
-
new RDSPubliclyAccessibleAnalyzer(),
|
|
494
|
-
new RDSNoBackupAnalyzer(),
|
|
495
|
-
new RDSUnencryptedAnalyzer(),
|
|
496
|
-
new RDSNoDeletionProtectionAnalyzer(),
|
|
497
|
-
new RDSNoMultiAZAnalyzer(),
|
|
498
|
-
]
|
|
499
|
-
: []),
|
|
500
|
-
...(config.s3?.enabled === true
|
|
501
|
-
? [
|
|
502
|
-
new S3PublicAccessAnalyzer(),
|
|
503
|
-
new S3MissingVersioningAnalyzer(),
|
|
504
|
-
new S3UnencryptedAnalyzer(),
|
|
505
|
-
]
|
|
506
|
-
: []),
|
|
507
|
-
...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
|
|
508
|
-
pipelineAnalyzer,
|
|
509
|
-
];
|
|
298
|
+
const analyzers = buildAnalyzers(config, iacDriftAnalyzer, iacLambdas);
|
|
510
299
|
const findings = await runAllAnalyzers(graph, analyzers);
|
|
511
300
|
writeCache('graph', graph);
|
|
512
301
|
writeCache('findings', findings);
|
|
@@ -1,9 +1,179 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { loadConfig, formatError, readCache, setCacheDir } from '../../core/index.js';
|
|
6
|
+
import { createServer, setGraphState } from '../../server/index.js';
|
|
7
|
+
import { log, printHeader } from '../utils.js';
|
|
8
|
+
import { runAnalyze, runCodeRefresh } from './analyze.js';
|
|
2
9
|
import { runStdio } from './stdio.js';
|
|
10
|
+
const BOX_W = 52;
|
|
11
|
+
const TOOL_MAP = [
|
|
12
|
+
{ name: 'get_infra_overview' },
|
|
13
|
+
{ name: 'get_graph_summary' },
|
|
14
|
+
{ name: 'analyze_function' },
|
|
15
|
+
{ name: 'suggest_gsi', service: 'dynamodb' },
|
|
16
|
+
{ name: 'postgres_index_suggestions', service: 'postgres' },
|
|
17
|
+
{ name: 'suggest_mongo_index', service: 'mongodb' },
|
|
18
|
+
{ name: 'mysql_index_suggestions', service: 'mysql' },
|
|
19
|
+
{ name: 'get_queue_details', service: 'sqs' },
|
|
20
|
+
{ name: 'get_topic_details', service: 'sns' },
|
|
21
|
+
{ name: 'get_secrets_overview', service: 'secretsManager' },
|
|
22
|
+
{ name: 'get_parameter_overview', service: 'ssm' },
|
|
23
|
+
{ name: 'get_lambda_overview', service: 'lambda' },
|
|
24
|
+
{ name: 'get_eventbridge_details', service: 'eventbridge' },
|
|
25
|
+
{ name: 'get_s3_overview', service: 's3' },
|
|
26
|
+
{ name: 'get_api_routes', service: 'apiGateway' },
|
|
27
|
+
{ name: 'get_log_errors', service: 'cloudwatchLogs' },
|
|
28
|
+
];
|
|
29
|
+
function isEnabled(cfg, service) {
|
|
30
|
+
if (!service)
|
|
31
|
+
return true;
|
|
32
|
+
const svc = cfg[service];
|
|
33
|
+
return svc?.enabled === true;
|
|
34
|
+
}
|
|
35
|
+
function boxLine(visibleContent, coloredContent) {
|
|
36
|
+
const padding = ' '.repeat(Math.max(0, BOX_W - visibleContent.length));
|
|
37
|
+
console.log(chalk.dim(' │') + coloredContent + padding + chalk.dim('│'));
|
|
38
|
+
}
|
|
39
|
+
function boxDivider() {
|
|
40
|
+
console.log(chalk.dim(' ├────────────────────────────────────────────────────┤'));
|
|
41
|
+
}
|
|
42
|
+
function groupTools(tools) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < tools.length) {
|
|
46
|
+
const a = tools[i];
|
|
47
|
+
const b = tools[i + 1];
|
|
48
|
+
if (b && ` ${a} · ${b}`.length <= BOX_W) {
|
|
49
|
+
lines.push(`${a} · ${b}`);
|
|
50
|
+
i += 2;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
lines.push(a);
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
3
59
|
export async function runServe(options = {}) {
|
|
4
60
|
if (options.stdio) {
|
|
5
61
|
await runStdio(options.config);
|
|
6
62
|
return;
|
|
7
63
|
}
|
|
8
|
-
|
|
64
|
+
const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000);
|
|
65
|
+
printHeader('MCP Server');
|
|
66
|
+
let config;
|
|
67
|
+
try {
|
|
68
|
+
config = loadConfig(options.config);
|
|
69
|
+
setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
|
|
70
|
+
log.success('Config loaded', options.config ?? 'infrawise.yaml');
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(formatError(err));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const repoPath = process.cwd();
|
|
77
|
+
// Auto-analyze if no cache
|
|
78
|
+
const cachedGraph = readCache('graph');
|
|
79
|
+
const cachedFindings = readCache('findings');
|
|
80
|
+
if (cachedGraph && cachedFindings) {
|
|
81
|
+
log.success('Cached analysis loaded', `${cachedGraph.nodes.length} nodes · ${cachedGraph.edges.length} edges · ${cachedFindings.length} finding(s)`);
|
|
82
|
+
setGraphState(cachedGraph, cachedFindings);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
log.warn('No cache found — running analysis now...');
|
|
86
|
+
console.log('');
|
|
87
|
+
await runAnalyze({ repo: repoPath, config: options.config });
|
|
88
|
+
const freshGraph = readCache('graph') ?? { nodes: [], edges: [] };
|
|
89
|
+
const freshFindings = readCache('findings') ?? [];
|
|
90
|
+
setGraphState(freshGraph, freshFindings);
|
|
91
|
+
}
|
|
92
|
+
console.log('');
|
|
93
|
+
// Start server
|
|
94
|
+
const spin = ora({ text: chalk.dim('Starting server...'), color: 'cyan' }).start();
|
|
95
|
+
const { start } = createServer(port);
|
|
96
|
+
await start();
|
|
97
|
+
spin.succeed(chalk.green('Server running'));
|
|
98
|
+
// Compute active/inactive tools from config
|
|
99
|
+
const activeTools = TOOL_MAP.filter((t) => isEnabled(config, t.service)).map((t) => t.name);
|
|
100
|
+
const inactiveTools = TOOL_MAP.filter((t) => !isEnabled(config, t.service)).map((t) => t.name);
|
|
101
|
+
// URL rows
|
|
102
|
+
const mcpUrl = `http://localhost:${port}/mcp`;
|
|
103
|
+
const healthUrl = `http://localhost:${port}/health`;
|
|
104
|
+
// Print box
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(chalk.dim(' ┌────────────────────────────────────────────────────┐'));
|
|
107
|
+
boxLine(' MCP Server', chalk.bold(' MCP Server'));
|
|
108
|
+
boxDivider();
|
|
109
|
+
boxLine(` POST ${mcpUrl}`, ` ${chalk.dim('POST')} ${chalk.cyan(mcpUrl)}`);
|
|
110
|
+
boxLine(` GET ${healthUrl}`, ` ${chalk.dim('GET')} ${chalk.cyan(healthUrl)}`);
|
|
111
|
+
boxDivider();
|
|
112
|
+
const activeLabel = ` Tools (${activeTools.length} active${inactiveTools.length > 0 ? ` · ${inactiveTools.length} off` : ''})`;
|
|
113
|
+
boxLine(activeLabel, chalk.dim(activeLabel));
|
|
114
|
+
for (const line of groupTools(activeTools)) {
|
|
115
|
+
boxLine(` ${line}`, ` ${line}`);
|
|
116
|
+
}
|
|
117
|
+
if (inactiveTools.length > 0) {
|
|
118
|
+
boxDivider();
|
|
119
|
+
boxLine(' Off (enable in infrawise.yaml):', chalk.dim(' Off (enable in infrawise.yaml):'));
|
|
120
|
+
for (const line of groupTools(inactiveTools)) {
|
|
121
|
+
boxLine(` ${line}`, chalk.dim(` ${line}`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log(chalk.dim(' └────────────────────────────────────────────────────┘'));
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(chalk.dim(' Add via CLI:'));
|
|
127
|
+
console.log(chalk.dim(` claude mcp add --transport http infrawise ${mcpUrl}`));
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(chalk.dim(' Watching for file changes... Press Ctrl+C to stop\n'));
|
|
130
|
+
// File watch — re-run code analysis on save, skip slow AWS/DB extraction
|
|
131
|
+
let debounceTimer = null;
|
|
132
|
+
let refreshing = false;
|
|
133
|
+
const configFile = path.resolve(options.config ?? 'infrawise.yaml');
|
|
134
|
+
try {
|
|
135
|
+
fs.watch(repoPath, { recursive: true }, (_, filename) => {
|
|
136
|
+
if (!filename)
|
|
137
|
+
return;
|
|
138
|
+
const abs = path.join(repoPath, filename);
|
|
139
|
+
if (abs === configFile) {
|
|
140
|
+
console.log(chalk.dim('\n infrawise.yaml changed — restart to apply config changes\n'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const ext = path.extname(filename);
|
|
144
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
145
|
+
return;
|
|
146
|
+
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
147
|
+
return;
|
|
148
|
+
if (debounceTimer)
|
|
149
|
+
clearTimeout(debounceTimer);
|
|
150
|
+
debounceTimer = setTimeout(async () => {
|
|
151
|
+
if (refreshing)
|
|
152
|
+
return;
|
|
153
|
+
refreshing = true;
|
|
154
|
+
const spin = ora({ text: chalk.dim('Refreshing code analysis...'), color: 'cyan' }).start();
|
|
155
|
+
try {
|
|
156
|
+
const { graph, findings } = await runCodeRefresh(repoPath, config);
|
|
157
|
+
setGraphState(graph, findings);
|
|
158
|
+
spin.succeed(chalk.green('Analysis refreshed') +
|
|
159
|
+
chalk.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
spin.warn(chalk.yellow('Refresh failed') +
|
|
163
|
+
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
refreshing = false;
|
|
167
|
+
}
|
|
168
|
+
}, 2000);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// fs.watch may not support recursive on all platforms — silently skip
|
|
173
|
+
}
|
|
174
|
+
process.on('SIGINT', () => {
|
|
175
|
+
console.log(chalk.dim('\n Shutting down...\n'));
|
|
176
|
+
process.exit(0);
|
|
177
|
+
});
|
|
178
|
+
await new Promise(() => { });
|
|
9
179
|
}
|
|
@@ -19,13 +19,13 @@ export async function runStdio(configPath) {
|
|
|
19
19
|
const cachedGraph = readCache('graph', CACHE_TTL_MS);
|
|
20
20
|
const cachedFindings = readCache('findings', CACHE_TTL_MS);
|
|
21
21
|
if (cachedGraph && cachedFindings) {
|
|
22
|
-
setGraphState(cachedGraph, cachedFindings
|
|
22
|
+
setGraphState(cachedGraph, cachedFindings);
|
|
23
23
|
}
|
|
24
24
|
else {
|
|
25
25
|
await runAnalyze({ config: configPath });
|
|
26
26
|
const graph = readCache('graph', CACHE_TTL_MS) ?? { nodes: [], edges: [] };
|
|
27
27
|
const findings = readCache('findings', CACHE_TTL_MS) ?? [];
|
|
28
|
-
setGraphState(graph, findings
|
|
28
|
+
setGraphState(graph, findings);
|
|
29
29
|
}
|
|
30
30
|
// File watching — re-run code analysis on save (no AWS calls, instant)
|
|
31
31
|
// stderr is safe in stdio transport; stdout is reserved for MCP JSON-RPC
|
|
@@ -53,7 +53,7 @@ export async function runStdio(configPath) {
|
|
|
53
53
|
refreshing = true;
|
|
54
54
|
try {
|
|
55
55
|
const { graph, findings } = await runCodeRefresh(repoPath, config);
|
|
56
|
-
setGraphState(graph, findings
|
|
56
|
+
setGraphState(graph, findings);
|
|
57
57
|
process.stderr.write(`infrawise: code graph refreshed (${graph.nodes.length} nodes · ${findings.length} finding(s))\n`);
|
|
58
58
|
}
|
|
59
59
|
catch {
|
package/dist/core/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as yaml from 'js-yaml';
|
|
5
|
+
import { InfrawiseError } from './errors.js';
|
|
5
6
|
export const InfrawiseConfigSchema = z.object({
|
|
6
7
|
project: z.string().min(1, 'Project name is required'),
|
|
7
8
|
aws: z
|
|
@@ -74,11 +75,9 @@ export const InfrawiseConfigSchema = z.object({
|
|
|
74
75
|
})
|
|
75
76
|
.optional(),
|
|
76
77
|
});
|
|
77
|
-
export class ConfigError extends
|
|
78
|
-
details;
|
|
78
|
+
export class ConfigError extends InfrawiseError {
|
|
79
79
|
constructor(message, details) {
|
|
80
|
-
super(message);
|
|
81
|
-
this.details = details;
|
|
80
|
+
super(message, details, 'infrawise start');
|
|
82
81
|
this.name = 'ConfigError';
|
|
83
82
|
}
|
|
84
83
|
}
|
|
@@ -183,13 +182,18 @@ export function generateDefaultConfig(projectName, options) {
|
|
|
183
182
|
secretsManager: { enabled: options?.secretsManager?.enabled ?? true },
|
|
184
183
|
lambda: { enabled: options?.lambda?.enabled ?? true },
|
|
185
184
|
eventbridge: { enabled: options?.eventbridge?.enabled ?? true },
|
|
186
|
-
rds: { enabled: options?.rds?.enabled ??
|
|
185
|
+
rds: { enabled: options?.rds?.enabled ?? false },
|
|
186
|
+
s3: { enabled: options?.s3?.enabled ?? false },
|
|
187
|
+
apiGateway: { enabled: options?.apiGateway?.enabled ?? false },
|
|
187
188
|
cloudwatchLogs: {
|
|
188
189
|
enabled: options?.cloudwatchLogs?.enabled ?? false,
|
|
189
190
|
logGroupPrefixes: options?.cloudwatchLogs?.logGroupPrefixes ?? [],
|
|
190
191
|
windowHours: options?.cloudwatchLogs?.windowHours ?? 24,
|
|
191
192
|
},
|
|
192
|
-
analysis: {
|
|
193
|
+
analysis: {
|
|
194
|
+
sampleSize: options?.analysis?.sampleSize ?? 100,
|
|
195
|
+
hotPartitionThreshold: options?.analysis?.hotPartitionThreshold ?? 5,
|
|
196
|
+
},
|
|
193
197
|
};
|
|
194
198
|
return yaml.dump(config, { lineWidth: 120 });
|
|
195
199
|
}
|
package/dist/core/errors.js
CHANGED
|
@@ -22,17 +22,6 @@ export class InfrawiseError extends Error {
|
|
|
22
22
|
return lines.join('\n');
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
export class AWSConnectionError extends InfrawiseError {
|
|
26
|
-
constructor(details) {
|
|
27
|
-
super('Unable to connect to AWS.', [
|
|
28
|
-
'Invalid or missing AWS credentials',
|
|
29
|
-
'Incorrect AWS profile specified',
|
|
30
|
-
'Network connectivity issues',
|
|
31
|
-
details ?? 'Unexpected AWS error',
|
|
32
|
-
], 'infrawise doctor');
|
|
33
|
-
this.name = 'AWSConnectionError';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
25
|
export class DynamoDBError extends InfrawiseError {
|
|
37
26
|
constructor(details) {
|
|
38
27
|
super('Unable to access DynamoDB.', [
|
|
@@ -66,16 +55,6 @@ export class RepositoryScanError extends InfrawiseError {
|
|
|
66
55
|
this.name = 'RepositoryScanError';
|
|
67
56
|
}
|
|
68
57
|
}
|
|
69
|
-
export class ConfigError extends InfrawiseError {
|
|
70
|
-
constructor(details) {
|
|
71
|
-
super('Invalid or missing configuration.', [
|
|
72
|
-
'infrawise.yaml not found in current directory',
|
|
73
|
-
'Missing required fields in configuration',
|
|
74
|
-
details ?? 'Unexpected config error',
|
|
75
|
-
], 'infrawise start');
|
|
76
|
-
this.name = 'ConfigError';
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
58
|
export function formatError(err) {
|
|
80
59
|
if (err instanceof InfrawiseError) {
|
|
81
60
|
return err.format();
|
package/dist/core/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { loadConfig, generateDefaultConfig, InfrawiseConfigSchema, ConfigError
|
|
1
|
+
export { loadConfig, generateDefaultConfig, InfrawiseConfigSchema, ConfigError } from './config.js';
|
|
2
2
|
export { logger } from './logger.js';
|
|
3
|
-
export { InfrawiseError,
|
|
3
|
+
export { InfrawiseError, DynamoDBError, PostgresConnectionError, RepositoryScanError, formatError, } from './errors.js';
|
|
4
4
|
export { writeCache, readCache, clearCache, setCacheDir } from './cache.js';
|
package/dist/server/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecre
|
|
|
12
12
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
13
13
|
let currentGraph = { nodes: [], edges: [] };
|
|
14
14
|
let currentFindings = [];
|
|
15
|
-
export function setGraphState(graph, findings
|
|
15
|
+
export function setGraphState(graph, findings) {
|
|
16
16
|
currentGraph = graph;
|
|
17
17
|
currentFindings = findings;
|
|
18
18
|
}
|
|
@@ -460,6 +460,7 @@ export function createServer(port = 3000) {
|
|
|
460
460
|
'get_lambda_overview',
|
|
461
461
|
'get_eventbridge_details',
|
|
462
462
|
'get_s3_overview',
|
|
463
|
+
'get_api_routes',
|
|
463
464
|
'get_log_errors',
|
|
464
465
|
],
|
|
465
466
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"mcpName": "io.github.Sidd27/infrawise",
|
|
5
5
|
"description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, API Gateway, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
|
|
6
6
|
"keywords": [
|
package/dist/cli/commands/dev.js
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
-
import { loadConfig, formatError, readCache, setCacheDir } from '../../core/index.js';
|
|
6
|
-
import { createServer, setGraphState } from '../../server/index.js';
|
|
7
|
-
import { log, printHeader } from '../utils.js';
|
|
8
|
-
import { runAnalyze, runCodeRefresh } from './analyze.js';
|
|
9
|
-
const BOX_W = 52;
|
|
10
|
-
const TOOL_MAP = [
|
|
11
|
-
{ name: 'get_infra_overview' },
|
|
12
|
-
{ name: 'get_graph_summary' },
|
|
13
|
-
{ name: 'analyze_function' },
|
|
14
|
-
{ name: 'suggest_gsi', service: 'dynamodb' },
|
|
15
|
-
{ name: 'postgres_index_suggestions', service: 'postgres' },
|
|
16
|
-
{ name: 'suggest_mongo_index', service: 'mongodb' },
|
|
17
|
-
{ name: 'mysql_index_suggestions', service: 'mysql' },
|
|
18
|
-
{ name: 'get_queue_details', service: 'sqs' },
|
|
19
|
-
{ name: 'get_topic_details', service: 'sns' },
|
|
20
|
-
{ name: 'get_secrets_overview', service: 'secretsManager' },
|
|
21
|
-
{ name: 'get_parameter_overview', service: 'ssm' },
|
|
22
|
-
{ name: 'get_lambda_overview', service: 'lambda' },
|
|
23
|
-
{ name: 'get_eventbridge_details', service: 'eventbridge' },
|
|
24
|
-
{ name: 'get_log_errors', service: 'cloudwatchLogs' },
|
|
25
|
-
];
|
|
26
|
-
function isEnabled(cfg, service) {
|
|
27
|
-
if (!service)
|
|
28
|
-
return true;
|
|
29
|
-
const svc = cfg[service];
|
|
30
|
-
return svc?.enabled === true;
|
|
31
|
-
}
|
|
32
|
-
function boxLine(visibleContent, coloredContent) {
|
|
33
|
-
const padding = ' '.repeat(Math.max(0, BOX_W - visibleContent.length));
|
|
34
|
-
console.log(chalk.dim(' │') + coloredContent + padding + chalk.dim('│'));
|
|
35
|
-
}
|
|
36
|
-
function boxDivider() {
|
|
37
|
-
console.log(chalk.dim(' ├────────────────────────────────────────────────────┤'));
|
|
38
|
-
}
|
|
39
|
-
function groupTools(tools) {
|
|
40
|
-
const lines = [];
|
|
41
|
-
let i = 0;
|
|
42
|
-
while (i < tools.length) {
|
|
43
|
-
const a = tools[i];
|
|
44
|
-
const b = tools[i + 1];
|
|
45
|
-
if (b && ` ${a} · ${b}`.length <= BOX_W) {
|
|
46
|
-
lines.push(`${a} · ${b}`);
|
|
47
|
-
i += 2;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
lines.push(a);
|
|
51
|
-
i++;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return lines;
|
|
55
|
-
}
|
|
56
|
-
export async function runDev(options = {}) {
|
|
57
|
-
const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000);
|
|
58
|
-
printHeader('MCP Server');
|
|
59
|
-
let config;
|
|
60
|
-
try {
|
|
61
|
-
config = loadConfig(options.config);
|
|
62
|
-
setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
|
|
63
|
-
log.success('Config loaded', options.config ?? 'infrawise.yaml');
|
|
64
|
-
}
|
|
65
|
-
catch (err) {
|
|
66
|
-
console.error(formatError(err));
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
const repoPath = process.cwd();
|
|
70
|
-
// Auto-analyze if no cache
|
|
71
|
-
const cachedGraph = readCache('graph');
|
|
72
|
-
const cachedFindings = readCache('findings');
|
|
73
|
-
if (cachedGraph && cachedFindings) {
|
|
74
|
-
log.success('Cached analysis loaded', `${cachedGraph.nodes.length} nodes · ${cachedGraph.edges.length} edges · ${cachedFindings.length} finding(s)`);
|
|
75
|
-
setGraphState(cachedGraph, cachedFindings, config);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
log.warn('No cache found — running analysis now...');
|
|
79
|
-
console.log('');
|
|
80
|
-
await runAnalyze({ repo: repoPath, config: options.config });
|
|
81
|
-
const freshGraph = readCache('graph') ?? { nodes: [], edges: [] };
|
|
82
|
-
const freshFindings = readCache('findings') ?? [];
|
|
83
|
-
setGraphState(freshGraph, freshFindings, config);
|
|
84
|
-
}
|
|
85
|
-
console.log('');
|
|
86
|
-
// Start server
|
|
87
|
-
const spin = ora({ text: chalk.dim('Starting server...'), color: 'cyan' }).start();
|
|
88
|
-
const { start } = createServer(port);
|
|
89
|
-
await start();
|
|
90
|
-
spin.succeed(chalk.green('Server running'));
|
|
91
|
-
// Compute active/inactive tools from config
|
|
92
|
-
const activeTools = TOOL_MAP.filter((t) => isEnabled(config, t.service)).map((t) => t.name);
|
|
93
|
-
const inactiveTools = TOOL_MAP.filter((t) => !isEnabled(config, t.service)).map((t) => t.name);
|
|
94
|
-
// URL rows
|
|
95
|
-
const mcpUrl = `http://localhost:${port}/mcp`;
|
|
96
|
-
const healthUrl = `http://localhost:${port}/health`;
|
|
97
|
-
// Print box
|
|
98
|
-
console.log('');
|
|
99
|
-
console.log(chalk.dim(' ┌────────────────────────────────────────────────────┐'));
|
|
100
|
-
boxLine(' MCP Server', chalk.bold(' MCP Server'));
|
|
101
|
-
boxDivider();
|
|
102
|
-
boxLine(` POST ${mcpUrl}`, ` ${chalk.dim('POST')} ${chalk.cyan(mcpUrl)}`);
|
|
103
|
-
boxLine(` GET ${healthUrl}`, ` ${chalk.dim('GET')} ${chalk.cyan(healthUrl)}`);
|
|
104
|
-
boxDivider();
|
|
105
|
-
const activeLabel = ` Tools (${activeTools.length} active${inactiveTools.length > 0 ? ` · ${inactiveTools.length} off` : ''})`;
|
|
106
|
-
boxLine(activeLabel, chalk.dim(activeLabel));
|
|
107
|
-
for (const line of groupTools(activeTools)) {
|
|
108
|
-
boxLine(` ${line}`, ` ${line}`);
|
|
109
|
-
}
|
|
110
|
-
if (inactiveTools.length > 0) {
|
|
111
|
-
boxDivider();
|
|
112
|
-
boxLine(' Off (enable in infrawise.yaml):', chalk.dim(' Off (enable in infrawise.yaml):'));
|
|
113
|
-
for (const line of groupTools(inactiveTools)) {
|
|
114
|
-
boxLine(` ${line}`, chalk.dim(` ${line}`));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
console.log(chalk.dim(' └────────────────────────────────────────────────────┘'));
|
|
118
|
-
console.log('');
|
|
119
|
-
console.log(chalk.dim(' Add via CLI:'));
|
|
120
|
-
console.log(chalk.dim(` claude mcp add --transport http infrawise ${mcpUrl}`));
|
|
121
|
-
console.log('');
|
|
122
|
-
console.log(chalk.dim(' Watching for file changes... Press Ctrl+C to stop\n'));
|
|
123
|
-
// File watch — re-run code analysis on save, skip slow AWS/DB extraction
|
|
124
|
-
let debounceTimer = null;
|
|
125
|
-
let refreshing = false;
|
|
126
|
-
const configFile = path.resolve(options.config ?? 'infrawise.yaml');
|
|
127
|
-
try {
|
|
128
|
-
fs.watch(repoPath, { recursive: true }, (_, filename) => {
|
|
129
|
-
if (!filename)
|
|
130
|
-
return;
|
|
131
|
-
const abs = path.join(repoPath, filename);
|
|
132
|
-
if (abs === configFile) {
|
|
133
|
-
console.log(chalk.dim('\n infrawise.yaml changed — restart to apply config changes\n'));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const ext = path.extname(filename);
|
|
137
|
-
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
138
|
-
return;
|
|
139
|
-
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
140
|
-
return;
|
|
141
|
-
if (debounceTimer)
|
|
142
|
-
clearTimeout(debounceTimer);
|
|
143
|
-
debounceTimer = setTimeout(async () => {
|
|
144
|
-
if (refreshing)
|
|
145
|
-
return;
|
|
146
|
-
refreshing = true;
|
|
147
|
-
const spin = ora({ text: chalk.dim('Refreshing code analysis...'), color: 'cyan' }).start();
|
|
148
|
-
try {
|
|
149
|
-
const { graph, findings } = await runCodeRefresh(repoPath, config);
|
|
150
|
-
setGraphState(graph, findings, config);
|
|
151
|
-
spin.succeed(chalk.green('Analysis refreshed') +
|
|
152
|
-
chalk.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
spin.warn(chalk.yellow('Refresh failed') +
|
|
156
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
157
|
-
}
|
|
158
|
-
finally {
|
|
159
|
-
refreshing = false;
|
|
160
|
-
}
|
|
161
|
-
}, 2000);
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// fs.watch may not support recursive on all platforms — silently skip
|
|
166
|
-
}
|
|
167
|
-
process.on('SIGINT', () => {
|
|
168
|
-
console.log(chalk.dim('\n Shutting down...\n'));
|
|
169
|
-
process.exit(0);
|
|
170
|
-
});
|
|
171
|
-
await new Promise(() => { });
|
|
172
|
-
}
|