infrawise 0.12.4 → 0.12.6
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 +1 -1
- package/dist/cli/commands/analyze.js +100 -311
- package/dist/cli/commands/serve.js +185 -2
- package/dist/cli/commands/stdio.js +57 -48
- 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 +12 -2
- package/package.json +1 -1
- package/dist/cli/commands/dev.js +0 -172
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ Add to your editor's MCP config:
|
|
|
149
149
|
|
|
150
150
|
| Tool | What it provides |
|
|
151
151
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
152
|
-
| `get_infra_overview` | Complete snapshot — all services, counts,
|
|
152
|
+
| `get_infra_overview` | Complete snapshot — all services, counts, high-severity findings, and a `configured` flag |
|
|
153
153
|
| `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
|
|
154
154
|
| `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes |
|
|
155
155
|
| `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
|
|
@@ -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,192 @@
|
|
|
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, readCache, setCacheDir } from '../../core/index.js';
|
|
6
|
+
import { createServer, setGraphState, setConfigured } 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
|
+
// With no config every registered tool is shown as active (the server exposes
|
|
30
|
+
// all of them); a config narrows the list to its enabled services.
|
|
31
|
+
function isEnabled(cfg, service) {
|
|
32
|
+
if (!service || !cfg)
|
|
33
|
+
return true;
|
|
34
|
+
const svc = cfg[service];
|
|
35
|
+
return svc?.enabled === true;
|
|
36
|
+
}
|
|
37
|
+
function boxLine(visibleContent, coloredContent) {
|
|
38
|
+
const padding = ' '.repeat(Math.max(0, BOX_W - visibleContent.length));
|
|
39
|
+
console.log(chalk.dim(' │') + coloredContent + padding + chalk.dim('│'));
|
|
40
|
+
}
|
|
41
|
+
function boxDivider() {
|
|
42
|
+
console.log(chalk.dim(' ├────────────────────────────────────────────────────┤'));
|
|
43
|
+
}
|
|
44
|
+
function groupTools(tools) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < tools.length) {
|
|
48
|
+
const a = tools[i];
|
|
49
|
+
const b = tools[i + 1];
|
|
50
|
+
if (b && ` ${a} · ${b}`.length <= BOX_W) {
|
|
51
|
+
lines.push(`${a} · ${b}`);
|
|
52
|
+
i += 2;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push(a);
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
3
61
|
export async function runServe(options = {}) {
|
|
4
62
|
if (options.stdio) {
|
|
5
63
|
await runStdio(options.config);
|
|
6
64
|
return;
|
|
7
65
|
}
|
|
8
|
-
|
|
66
|
+
const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000);
|
|
67
|
+
printHeader('MCP Server');
|
|
68
|
+
setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
|
|
69
|
+
// A hosted MCP runtime may launch the server with no infrawise.yaml. Start
|
|
70
|
+
// anyway with an empty graph so the host can connect and list tools.
|
|
71
|
+
let config;
|
|
72
|
+
try {
|
|
73
|
+
config = loadConfig(options.config);
|
|
74
|
+
log.success('Config loaded', options.config ?? 'infrawise.yaml');
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
log.warn(`Starting with empty graph (no config loaded): ${err instanceof Error ? err.message : String(err)}`);
|
|
78
|
+
}
|
|
79
|
+
setConfigured(config !== undefined);
|
|
80
|
+
const repoPath = process.cwd();
|
|
81
|
+
// Auto-analyze if no cache
|
|
82
|
+
const cachedGraph = readCache('graph');
|
|
83
|
+
const cachedFindings = readCache('findings');
|
|
84
|
+
if (cachedGraph && cachedFindings) {
|
|
85
|
+
log.success('Cached analysis loaded', `${cachedGraph.nodes.length} nodes · ${cachedGraph.edges.length} edges · ${cachedFindings.length} finding(s)`);
|
|
86
|
+
setGraphState(cachedGraph, cachedFindings);
|
|
87
|
+
}
|
|
88
|
+
else if (config) {
|
|
89
|
+
log.warn('No cache found — running analysis now...');
|
|
90
|
+
console.log('');
|
|
91
|
+
await runAnalyze({ repo: repoPath, config: options.config });
|
|
92
|
+
const freshGraph = readCache('graph') ?? { nodes: [], edges: [] };
|
|
93
|
+
const freshFindings = readCache('findings') ?? [];
|
|
94
|
+
setGraphState(freshGraph, freshFindings);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
setGraphState({ nodes: [], edges: [] }, []);
|
|
98
|
+
}
|
|
99
|
+
console.log('');
|
|
100
|
+
// Start server
|
|
101
|
+
const spin = ora({ text: chalk.dim('Starting server...'), color: 'cyan' }).start();
|
|
102
|
+
const { start } = createServer(port);
|
|
103
|
+
await start();
|
|
104
|
+
spin.succeed(chalk.green('Server running'));
|
|
105
|
+
// Compute active/inactive tools from config
|
|
106
|
+
const activeTools = TOOL_MAP.filter((t) => isEnabled(config, t.service)).map((t) => t.name);
|
|
107
|
+
const inactiveTools = TOOL_MAP.filter((t) => !isEnabled(config, t.service)).map((t) => t.name);
|
|
108
|
+
// URL rows
|
|
109
|
+
const mcpUrl = `http://localhost:${port}/mcp`;
|
|
110
|
+
const healthUrl = `http://localhost:${port}/health`;
|
|
111
|
+
// Print box
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(chalk.dim(' ┌────────────────────────────────────────────────────┐'));
|
|
114
|
+
boxLine(' MCP Server', chalk.bold(' MCP Server'));
|
|
115
|
+
boxDivider();
|
|
116
|
+
boxLine(` POST ${mcpUrl}`, ` ${chalk.dim('POST')} ${chalk.cyan(mcpUrl)}`);
|
|
117
|
+
boxLine(` GET ${healthUrl}`, ` ${chalk.dim('GET')} ${chalk.cyan(healthUrl)}`);
|
|
118
|
+
boxDivider();
|
|
119
|
+
const activeLabel = ` Tools (${activeTools.length} active${inactiveTools.length > 0 ? ` · ${inactiveTools.length} off` : ''})`;
|
|
120
|
+
boxLine(activeLabel, chalk.dim(activeLabel));
|
|
121
|
+
for (const line of groupTools(activeTools)) {
|
|
122
|
+
boxLine(` ${line}`, ` ${line}`);
|
|
123
|
+
}
|
|
124
|
+
if (inactiveTools.length > 0) {
|
|
125
|
+
boxDivider();
|
|
126
|
+
boxLine(' Off (enable in infrawise.yaml):', chalk.dim(' Off (enable in infrawise.yaml):'));
|
|
127
|
+
for (const line of groupTools(inactiveTools)) {
|
|
128
|
+
boxLine(` ${line}`, chalk.dim(` ${line}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
console.log(chalk.dim(' └────────────────────────────────────────────────────┘'));
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(chalk.dim(' Add via CLI:'));
|
|
134
|
+
console.log(chalk.dim(` claude mcp add --transport http infrawise ${mcpUrl}`));
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(chalk.dim(' Watching for file changes... Press Ctrl+C to stop\n'));
|
|
137
|
+
// File watch — re-run code analysis on save (needs a config to drive analyzers)
|
|
138
|
+
if (config) {
|
|
139
|
+
const cfg = config;
|
|
140
|
+
let debounceTimer = null;
|
|
141
|
+
let refreshing = false;
|
|
142
|
+
const configFile = path.resolve(options.config ?? 'infrawise.yaml');
|
|
143
|
+
try {
|
|
144
|
+
fs.watch(repoPath, { recursive: true }, (_, filename) => {
|
|
145
|
+
if (!filename)
|
|
146
|
+
return;
|
|
147
|
+
const abs = path.join(repoPath, filename);
|
|
148
|
+
if (abs === configFile) {
|
|
149
|
+
console.log(chalk.dim('\n infrawise.yaml changed — restart to apply config changes\n'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const ext = path.extname(filename);
|
|
153
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
154
|
+
return;
|
|
155
|
+
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
156
|
+
return;
|
|
157
|
+
if (debounceTimer)
|
|
158
|
+
clearTimeout(debounceTimer);
|
|
159
|
+
debounceTimer = setTimeout(async () => {
|
|
160
|
+
if (refreshing)
|
|
161
|
+
return;
|
|
162
|
+
refreshing = true;
|
|
163
|
+
const spin = ora({
|
|
164
|
+
text: chalk.dim('Refreshing code analysis...'),
|
|
165
|
+
color: 'cyan',
|
|
166
|
+
}).start();
|
|
167
|
+
try {
|
|
168
|
+
const { graph, findings } = await runCodeRefresh(repoPath, cfg);
|
|
169
|
+
setGraphState(graph, findings);
|
|
170
|
+
spin.succeed(chalk.green('Analysis refreshed') +
|
|
171
|
+
chalk.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
spin.warn(chalk.yellow('Refresh failed') +
|
|
175
|
+
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
refreshing = false;
|
|
179
|
+
}
|
|
180
|
+
}, 2000);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// fs.watch may not support recursive on all platforms — silently skip
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
process.on('SIGINT', () => {
|
|
188
|
+
console.log(chalk.dim('\n Shutting down...\n'));
|
|
189
|
+
process.exit(0);
|
|
190
|
+
});
|
|
191
|
+
await new Promise(() => { });
|
|
9
192
|
}
|
|
@@ -1,72 +1,81 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { loadConfig,
|
|
5
|
-
import { createMcpServer, setGraphState } from '../../server/index.js';
|
|
4
|
+
import { loadConfig, readCache, setCacheDir } from '../../core/index.js';
|
|
5
|
+
import { createMcpServer, setGraphState, setConfigured } from '../../server/index.js';
|
|
6
6
|
import { runAnalyze, runCodeRefresh } from './analyze.js';
|
|
7
7
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
8
|
export async function runStdio(configPath) {
|
|
9
|
+
const resolvedConfigPath = configPath ?? 'infrawise.yaml';
|
|
10
|
+
setCacheDir(path.dirname(path.resolve(resolvedConfigPath)));
|
|
11
|
+
// A hosted MCP runtime (e.g. Glama) launches the server with no infrawise.yaml.
|
|
12
|
+
// Start anyway with an empty graph so the host can connect and list tools;
|
|
13
|
+
// analysis populates once a config is present.
|
|
9
14
|
let config;
|
|
10
15
|
try {
|
|
11
16
|
config = loadConfig(configPath);
|
|
12
17
|
}
|
|
13
18
|
catch (err) {
|
|
14
|
-
process.stderr.write(
|
|
15
|
-
process.exit(1);
|
|
19
|
+
process.stderr.write(`infrawise: starting with empty graph (no config loaded: ${err instanceof Error ? err.message : String(err)})\n`);
|
|
16
20
|
}
|
|
17
|
-
|
|
18
|
-
setCacheDir(path.dirname(path.resolve(resolvedConfigPath)));
|
|
21
|
+
setConfigured(config !== undefined);
|
|
19
22
|
const cachedGraph = readCache('graph', CACHE_TTL_MS);
|
|
20
23
|
const cachedFindings = readCache('findings', CACHE_TTL_MS);
|
|
21
24
|
if (cachedGraph && cachedFindings) {
|
|
22
|
-
setGraphState(cachedGraph, cachedFindings
|
|
25
|
+
setGraphState(cachedGraph, cachedFindings);
|
|
23
26
|
}
|
|
24
|
-
else {
|
|
27
|
+
else if (config) {
|
|
25
28
|
await runAnalyze({ config: configPath });
|
|
26
29
|
const graph = readCache('graph', CACHE_TTL_MS) ?? { nodes: [], edges: [] };
|
|
27
30
|
const findings = readCache('findings', CACHE_TTL_MS) ?? [];
|
|
28
|
-
setGraphState(graph, findings
|
|
31
|
+
setGraphState(graph, findings);
|
|
29
32
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const repoPath = process.cwd();
|
|
33
|
-
const configFile = path.resolve(resolvedConfigPath);
|
|
34
|
-
let debounceTimer = null;
|
|
35
|
-
let refreshing = false;
|
|
36
|
-
try {
|
|
37
|
-
fs.watch(repoPath, { recursive: true }, (_, filename) => {
|
|
38
|
-
if (!filename)
|
|
39
|
-
return;
|
|
40
|
-
const abs = path.join(repoPath, filename);
|
|
41
|
-
if (abs === configFile)
|
|
42
|
-
return;
|
|
43
|
-
const ext = path.extname(filename);
|
|
44
|
-
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
45
|
-
return;
|
|
46
|
-
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
47
|
-
return;
|
|
48
|
-
if (debounceTimer)
|
|
49
|
-
clearTimeout(debounceTimer);
|
|
50
|
-
debounceTimer = setTimeout(async () => {
|
|
51
|
-
if (refreshing)
|
|
52
|
-
return;
|
|
53
|
-
refreshing = true;
|
|
54
|
-
try {
|
|
55
|
-
const { graph, findings } = await runCodeRefresh(repoPath, config);
|
|
56
|
-
setGraphState(graph, findings, config);
|
|
57
|
-
process.stderr.write(`infrawise: code graph refreshed (${graph.nodes.length} nodes · ${findings.length} finding(s))\n`);
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// don't break the MCP connection on watcher errors
|
|
61
|
-
}
|
|
62
|
-
finally {
|
|
63
|
-
refreshing = false;
|
|
64
|
-
}
|
|
65
|
-
}, 2000);
|
|
66
|
-
});
|
|
33
|
+
else {
|
|
34
|
+
setGraphState({ nodes: [], edges: [] }, []);
|
|
67
35
|
}
|
|
68
|
-
|
|
69
|
-
|
|
36
|
+
// File watching drives runCodeRefresh, which needs a config — skip it without one.
|
|
37
|
+
// stderr is safe in stdio transport; stdout is reserved for MCP JSON-RPC.
|
|
38
|
+
if (config) {
|
|
39
|
+
const cfg = config;
|
|
40
|
+
const repoPath = process.cwd();
|
|
41
|
+
const configFile = path.resolve(resolvedConfigPath);
|
|
42
|
+
let debounceTimer = null;
|
|
43
|
+
let refreshing = false;
|
|
44
|
+
try {
|
|
45
|
+
fs.watch(repoPath, { recursive: true }, (_, filename) => {
|
|
46
|
+
if (!filename)
|
|
47
|
+
return;
|
|
48
|
+
const abs = path.join(repoPath, filename);
|
|
49
|
+
if (abs === configFile)
|
|
50
|
+
return;
|
|
51
|
+
const ext = path.extname(filename);
|
|
52
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
53
|
+
return;
|
|
54
|
+
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
55
|
+
return;
|
|
56
|
+
if (debounceTimer)
|
|
57
|
+
clearTimeout(debounceTimer);
|
|
58
|
+
debounceTimer = setTimeout(async () => {
|
|
59
|
+
if (refreshing)
|
|
60
|
+
return;
|
|
61
|
+
refreshing = true;
|
|
62
|
+
try {
|
|
63
|
+
const { graph, findings } = await runCodeRefresh(repoPath, cfg);
|
|
64
|
+
setGraphState(graph, findings);
|
|
65
|
+
process.stderr.write(`infrawise: code graph refreshed (${graph.nodes.length} nodes · ${findings.length} finding(s))\n`);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// don't break the MCP connection on watcher errors
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
refreshing = false;
|
|
72
|
+
}
|
|
73
|
+
}, 2000);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// fs.watch recursive not supported on all platforms — silently skip
|
|
78
|
+
}
|
|
70
79
|
}
|
|
71
80
|
const mcp = createMcpServer();
|
|
72
81
|
const transport = new StdioServerTransport();
|
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,10 +12,17 @@ import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecre
|
|
|
12
12
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
13
13
|
let currentGraph = { nodes: [], edges: [] };
|
|
14
14
|
let currentFindings = [];
|
|
15
|
-
|
|
15
|
+
// False when the server booted without an infrawise.yaml (e.g. a hosted MCP
|
|
16
|
+
// runtime). Used to return a "run locally" hint instead of a bare empty graph.
|
|
17
|
+
let configured = true;
|
|
18
|
+
export function setGraphState(graph, findings) {
|
|
16
19
|
currentGraph = graph;
|
|
17
20
|
currentFindings = findings;
|
|
18
21
|
}
|
|
22
|
+
export function setConfigured(value) {
|
|
23
|
+
configured = value;
|
|
24
|
+
}
|
|
25
|
+
const NOT_CONFIGURED_HINT = 'No infrastructure loaded. infrawise reads your live infra locally — run `npx infrawise start` in your project (with AWS credentials and an infrawise.yaml) so these tools return your real DynamoDB/RDS/SQS/Lambda/etc. context. A remotely hosted instance has no access to your cloud account or code, so it returns empty results by design.';
|
|
19
26
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
20
27
|
function toText(data) {
|
|
21
28
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
@@ -31,7 +38,7 @@ function logged(name, fn) {
|
|
|
31
38
|
export function createMcpServer() {
|
|
32
39
|
const mcp = new McpServer({ name: 'infrawise', version });
|
|
33
40
|
mcp.registerTool('get_infra_overview', {
|
|
34
|
-
description: 'Returns a compact infrastructure snapshot: service counts, all databases, queues, topics, secrets, lambdas, and high-severity findings. Call this first at the start of any database or infrastructure task to understand what services are in scope. Prefer this over get_graph_summary for quick orientation; use get_graph_summary only when you need every node, edge, and finding in full.',
|
|
41
|
+
description: 'Returns a compact infrastructure snapshot: service counts, all databases, queues, topics, secrets, lambdas, and high-severity findings. Call this first at the start of any database or infrastructure task to understand what services are in scope. Prefer this over get_graph_summary for quick orientation; use get_graph_summary only when you need every node, edge, and finding in full. Also returns a `configured` flag — when false, the server has no infrawise.yaml loaded (e.g. a remotely hosted instance) and all tools return empty results; a `setupHint` explains how to run infrawise locally.',
|
|
35
42
|
inputSchema: z.object({}),
|
|
36
43
|
}, logged('get_infra_overview', async () => {
|
|
37
44
|
const tables = getTableNodes(currentGraph);
|
|
@@ -44,6 +51,8 @@ export function createMcpServer() {
|
|
|
44
51
|
const functions = getFunctionNodes(currentGraph);
|
|
45
52
|
const buckets = getBucketNodes(currentGraph);
|
|
46
53
|
return toText({
|
|
54
|
+
configured,
|
|
55
|
+
...(configured ? {} : { setupHint: NOT_CONFIGURED_HINT }),
|
|
47
56
|
summary: {
|
|
48
57
|
tables: tables.length,
|
|
49
58
|
functions: functions.length,
|
|
@@ -460,6 +469,7 @@ export function createServer(port = 3000) {
|
|
|
460
469
|
'get_lambda_overview',
|
|
461
470
|
'get_eventbridge_details',
|
|
462
471
|
'get_s3_overview',
|
|
472
|
+
'get_api_routes',
|
|
463
473
|
'get_log_errors',
|
|
464
474
|
],
|
|
465
475
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
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
|
-
}
|