infrawise 0.5.0 → 0.7.0
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 +26 -14
- package/dist/adapters/aws.js +152 -72
- package/dist/adapters/dynamodb.js +17 -27
- package/dist/adapters/logs.js +12 -21
- package/dist/adapters/mongodb.js +10 -16
- package/dist/adapters/mysql.js +8 -17
- package/dist/adapters/postgres.js +9 -13
- package/dist/adapters/terraform.js +14 -56
- package/dist/analyzers/aws-services.js +34 -17
- package/dist/analyzers/dynamodb.js +6 -12
- package/dist/analyzers/index.js +13 -41
- package/dist/analyzers/mongodb.js +4 -9
- package/dist/analyzers/mysql.js +4 -9
- package/dist/analyzers/postgres.js +3 -9
- package/dist/analyzers/rds.js +5 -13
- package/dist/analyzers/terraform.js +1 -5
- package/dist/cli/commands/analyze.js +131 -158
- package/dist/cli/commands/auth.js +24 -30
- package/dist/cli/commands/dev.js +46 -84
- package/dist/cli/commands/doctor.js +51 -74
- package/dist/cli/commands/init.js +48 -73
- package/dist/cli/index.js +21 -23
- package/dist/cli/utils.js +40 -86
- package/dist/context/index.js +49 -85
- package/dist/core/cache.js +5 -43
- package/dist/core/config.js +50 -83
- package/dist/core/errors.js +7 -17
- package/dist/core/index.js +4 -22
- package/dist/core/logger.js +4 -10
- package/dist/graph/index.js +73 -33
- package/dist/server/index.js +116 -93
- package/dist/types.js +1 -2
- package/package.json +32 -30
package/dist/core/index.js
CHANGED
|
@@ -1,22 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_1.loadConfig; } });
|
|
6
|
-
Object.defineProperty(exports, "generateDefaultConfig", { enumerable: true, get: function () { return config_1.generateDefaultConfig; } });
|
|
7
|
-
Object.defineProperty(exports, "InfrawiseConfigSchema", { enumerable: true, get: function () { return config_1.InfrawiseConfigSchema; } });
|
|
8
|
-
Object.defineProperty(exports, "ConfigValidationError", { enumerable: true, get: function () { return config_1.ConfigError; } });
|
|
9
|
-
var logger_1 = require("./logger");
|
|
10
|
-
Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return logger_1.logger; } });
|
|
11
|
-
var errors_1 = require("./errors");
|
|
12
|
-
Object.defineProperty(exports, "InfrawiseError", { enumerable: true, get: function () { return errors_1.InfrawiseError; } });
|
|
13
|
-
Object.defineProperty(exports, "AWSConnectionError", { enumerable: true, get: function () { return errors_1.AWSConnectionError; } });
|
|
14
|
-
Object.defineProperty(exports, "DynamoDBError", { enumerable: true, get: function () { return errors_1.DynamoDBError; } });
|
|
15
|
-
Object.defineProperty(exports, "PostgresConnectionError", { enumerable: true, get: function () { return errors_1.PostgresConnectionError; } });
|
|
16
|
-
Object.defineProperty(exports, "RepositoryScanError", { enumerable: true, get: function () { return errors_1.RepositoryScanError; } });
|
|
17
|
-
Object.defineProperty(exports, "ConfigError", { enumerable: true, get: function () { return errors_1.ConfigError; } });
|
|
18
|
-
Object.defineProperty(exports, "formatError", { enumerable: true, get: function () { return errors_1.formatError; } });
|
|
19
|
-
var cache_1 = require("./cache");
|
|
20
|
-
Object.defineProperty(exports, "writeCache", { enumerable: true, get: function () { return cache_1.writeCache; } });
|
|
21
|
-
Object.defineProperty(exports, "readCache", { enumerable: true, get: function () { return cache_1.readCache; } });
|
|
22
|
-
Object.defineProperty(exports, "clearCache", { enumerable: true, get: function () { return cache_1.clearCache; } });
|
|
1
|
+
export { loadConfig, generateDefaultConfig, InfrawiseConfigSchema, ConfigError as ConfigValidationError } from './config.js';
|
|
2
|
+
export { logger } from './logger.js';
|
|
3
|
+
export { InfrawiseError, AWSConnectionError, DynamoDBError, PostgresConnectionError, RepositoryScanError, ConfigError, formatError, } from './errors.js';
|
|
4
|
+
export { writeCache, readCache, clearCache } from './cache.js';
|
package/dist/core/logger.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.logger = void 0;
|
|
7
|
-
const pino_1 = __importDefault(require("pino"));
|
|
1
|
+
import pino from 'pino';
|
|
8
2
|
function createLogger() {
|
|
9
3
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
10
4
|
if (isDevelopment) {
|
|
11
|
-
return (
|
|
5
|
+
return pino({
|
|
12
6
|
level: process.env.LOG_LEVEL ?? 'info',
|
|
13
7
|
transport: {
|
|
14
8
|
target: 'pino-pretty',
|
|
@@ -21,8 +15,8 @@ function createLogger() {
|
|
|
21
15
|
},
|
|
22
16
|
});
|
|
23
17
|
}
|
|
24
|
-
return (
|
|
18
|
+
return pino({
|
|
25
19
|
level: process.env.LOG_LEVEL ?? 'info',
|
|
26
20
|
});
|
|
27
21
|
}
|
|
28
|
-
|
|
22
|
+
export const logger = createLogger();
|
package/dist/graph/index.js
CHANGED
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildGraph = buildGraph;
|
|
4
|
-
exports.getTableNodes = getTableNodes;
|
|
5
|
-
exports.getFunctionNodes = getFunctionNodes;
|
|
6
|
-
exports.getIndexNodes = getIndexNodes;
|
|
7
|
-
exports.getQueueNodes = getQueueNodes;
|
|
8
|
-
exports.getTopicNodes = getTopicNodes;
|
|
9
|
-
exports.getSecretNodes = getSecretNodes;
|
|
10
|
-
exports.getParameterNodes = getParameterNodes;
|
|
11
|
-
exports.getLogGroupNodes = getLogGroupNodes;
|
|
12
|
-
exports.getLambdaNodes = getLambdaNodes;
|
|
13
|
-
exports.getEdgesForNode = getEdgesForNode;
|
|
14
|
-
exports.getOutgoingEdges = getOutgoingEdges;
|
|
15
|
-
exports.getIncomingEdges = getIncomingEdges;
|
|
16
|
-
exports.getScanEdges = getScanEdges;
|
|
17
|
-
exports.getEdgeFrequency = getEdgeFrequency;
|
|
18
|
-
function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [], mongoMeta = [], servicesMeta = {}) {
|
|
1
|
+
export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [], mongoMeta = [], servicesMeta = {}) {
|
|
19
2
|
const nodes = [];
|
|
20
3
|
const edges = [];
|
|
21
4
|
const nodeIds = new Set();
|
|
@@ -119,15 +102,69 @@ function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [], mongoM
|
|
|
119
102
|
});
|
|
120
103
|
}
|
|
121
104
|
for (const fn of servicesMeta.lambda ?? []) {
|
|
105
|
+
const lambdaId = `lambda:aws:${fn.name}`;
|
|
122
106
|
addNode({
|
|
123
|
-
id:
|
|
107
|
+
id: lambdaId,
|
|
124
108
|
type: 'lambda',
|
|
125
109
|
name: fn.name,
|
|
126
110
|
runtime: fn.runtime,
|
|
127
111
|
memoryMB: fn.memoryMB,
|
|
128
112
|
timeoutSec: fn.timeoutSec,
|
|
129
113
|
envVarKeys: fn.envVarKeys,
|
|
114
|
+
triggers: fn.triggers,
|
|
130
115
|
});
|
|
116
|
+
// Add trigger edges from source → lambda (only for enabled/active mappings)
|
|
117
|
+
for (const trigger of fn.triggers ?? []) {
|
|
118
|
+
if (trigger.state && trigger.state !== 'Enabled')
|
|
119
|
+
continue;
|
|
120
|
+
let sourceId;
|
|
121
|
+
if (trigger.type === 'sqs') {
|
|
122
|
+
sourceId = `queue:aws:${trigger.sourceName}`;
|
|
123
|
+
addNode({ id: sourceId, type: 'queue', name: trigger.sourceName, provider: 'aws', hasDLQ: false, encrypted: false });
|
|
124
|
+
}
|
|
125
|
+
else if (trigger.type === 'dynamodb') {
|
|
126
|
+
sourceId = `table:dynamo:${trigger.sourceName}`;
|
|
127
|
+
addNode({ id: sourceId, type: 'table', name: trigger.sourceName, databaseType: 'dynamodb' });
|
|
128
|
+
}
|
|
129
|
+
else if (trigger.type === 'kinesis') {
|
|
130
|
+
sourceId = `queue:aws:${trigger.sourceName}`;
|
|
131
|
+
addNode({ id: sourceId, type: 'queue', name: trigger.sourceName, provider: 'aws', hasDLQ: false, encrypted: false });
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
edges.push({ from: sourceId, to: lambdaId, type: 'triggers' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const rule of servicesMeta.eventbridge ?? []) {
|
|
140
|
+
const ruleId = `eventbridge_rule:aws:${rule.name}`;
|
|
141
|
+
addNode({
|
|
142
|
+
id: ruleId,
|
|
143
|
+
type: 'eventbridge_rule',
|
|
144
|
+
name: rule.name,
|
|
145
|
+
state: rule.state,
|
|
146
|
+
scheduleExpression: rule.scheduleExpression,
|
|
147
|
+
eventPattern: rule.eventPattern,
|
|
148
|
+
});
|
|
149
|
+
for (const targetArn of rule.targetArns) {
|
|
150
|
+
const fnName = targetArn.split(':').pop() ?? '';
|
|
151
|
+
const lambdaId = `lambda:aws:${fnName}`;
|
|
152
|
+
if (!nodeIds.has(lambdaId))
|
|
153
|
+
continue;
|
|
154
|
+
edges.push({ from: ruleId, to: lambdaId, type: 'triggers' });
|
|
155
|
+
const lambdaNodeRef = nodes.find((n) => n.id === lambdaId);
|
|
156
|
+
if (lambdaNodeRef && lambdaNodeRef.type === 'lambda') {
|
|
157
|
+
const trigger = {
|
|
158
|
+
type: 'eventbridge',
|
|
159
|
+
sourceArn: rule.arn,
|
|
160
|
+
sourceName: rule.name,
|
|
161
|
+
eventShape: 'event.detail',
|
|
162
|
+
ruleName: rule.name,
|
|
163
|
+
eventPattern: rule.scheduleExpression ?? rule.eventPattern ?? '',
|
|
164
|
+
};
|
|
165
|
+
lambdaNodeRef.triggers = [...(lambdaNodeRef.triggers ?? []), trigger];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
131
168
|
}
|
|
132
169
|
for (const db of servicesMeta.rds ?? []) {
|
|
133
170
|
addNode({
|
|
@@ -226,46 +263,49 @@ function resolveEdgeType(operationType) {
|
|
|
226
263
|
return 'query';
|
|
227
264
|
}
|
|
228
265
|
// ── Typed node selectors ─────────────────────────────────────────────────────
|
|
229
|
-
function getTableNodes(graph) {
|
|
266
|
+
export function getTableNodes(graph) {
|
|
230
267
|
return graph.nodes.filter((n) => n.type === 'table');
|
|
231
268
|
}
|
|
232
|
-
function getFunctionNodes(graph) {
|
|
269
|
+
export function getFunctionNodes(graph) {
|
|
233
270
|
return graph.nodes.filter((n) => n.type === 'function');
|
|
234
271
|
}
|
|
235
|
-
function getIndexNodes(graph) {
|
|
272
|
+
export function getIndexNodes(graph) {
|
|
236
273
|
return graph.nodes.filter((n) => n.type === 'index');
|
|
237
274
|
}
|
|
238
|
-
function getQueueNodes(graph) {
|
|
275
|
+
export function getQueueNodes(graph) {
|
|
239
276
|
return graph.nodes.filter((n) => n.type === 'queue');
|
|
240
277
|
}
|
|
241
|
-
function getTopicNodes(graph) {
|
|
278
|
+
export function getTopicNodes(graph) {
|
|
242
279
|
return graph.nodes.filter((n) => n.type === 'topic');
|
|
243
280
|
}
|
|
244
|
-
function getSecretNodes(graph) {
|
|
281
|
+
export function getSecretNodes(graph) {
|
|
245
282
|
return graph.nodes.filter((n) => n.type === 'secret');
|
|
246
283
|
}
|
|
247
|
-
function getParameterNodes(graph) {
|
|
284
|
+
export function getParameterNodes(graph) {
|
|
248
285
|
return graph.nodes.filter((n) => n.type === 'parameter');
|
|
249
286
|
}
|
|
250
|
-
function getLogGroupNodes(graph) {
|
|
287
|
+
export function getLogGroupNodes(graph) {
|
|
251
288
|
return graph.nodes.filter((n) => n.type === 'log_group');
|
|
252
289
|
}
|
|
253
|
-
function getLambdaNodes(graph) {
|
|
290
|
+
export function getLambdaNodes(graph) {
|
|
254
291
|
return graph.nodes.filter((n) => n.type === 'lambda');
|
|
255
292
|
}
|
|
256
|
-
function
|
|
293
|
+
export function getEventBridgeRuleNodes(graph) {
|
|
294
|
+
return graph.nodes.filter((n) => n.type === 'eventbridge_rule');
|
|
295
|
+
}
|
|
296
|
+
export function getEdgesForNode(graph, nodeId) {
|
|
257
297
|
return graph.edges.filter((e) => e.from === nodeId || e.to === nodeId);
|
|
258
298
|
}
|
|
259
|
-
function getOutgoingEdges(graph, nodeId) {
|
|
299
|
+
export function getOutgoingEdges(graph, nodeId) {
|
|
260
300
|
return graph.edges.filter((e) => e.from === nodeId);
|
|
261
301
|
}
|
|
262
|
-
function getIncomingEdges(graph, nodeId) {
|
|
302
|
+
export function getIncomingEdges(graph, nodeId) {
|
|
263
303
|
return graph.edges.filter((e) => e.to === nodeId);
|
|
264
304
|
}
|
|
265
|
-
function getScanEdges(graph) {
|
|
305
|
+
export function getScanEdges(graph) {
|
|
266
306
|
return graph.edges.filter((e) => e.type === 'scan');
|
|
267
307
|
}
|
|
268
|
-
function getEdgeFrequency(graph) {
|
|
308
|
+
export function getEdgeFrequency(graph) {
|
|
269
309
|
const freq = new Map();
|
|
270
310
|
for (const edge of graph.edges) {
|
|
271
311
|
const key = `${edge.from}->${edge.to}`;
|
package/dist/server/index.js
CHANGED
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const fastify_1 = __importDefault(require("fastify"));
|
|
13
|
-
const cors_1 = __importDefault(require("@fastify/cors"));
|
|
14
|
-
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
15
|
-
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
16
|
-
const zod_1 = require("zod");
|
|
17
|
-
const core_1 = require("../core");
|
|
18
|
-
const { version } = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../package.json'), 'utf8'));
|
|
19
|
-
const analyzers_1 = require("../analyzers");
|
|
20
|
-
const graph_1 = require("../graph");
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import Fastify from 'fastify';
|
|
4
|
+
import cors from '@fastify/cors';
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { logger } from '../core/index.js';
|
|
9
|
+
const { version } = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
|
|
10
|
+
import { summarizeFindings } from '../analyzers/index.js';
|
|
11
|
+
import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
|
|
21
12
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
22
13
|
let currentGraph = { nodes: [], edges: [] };
|
|
23
|
-
exports.currentGraph = currentGraph;
|
|
24
14
|
let currentFindings = [];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
exports.currentFindings = currentFindings = findings;
|
|
15
|
+
export function setGraphState(graph, findings, _config) {
|
|
16
|
+
currentGraph = graph;
|
|
17
|
+
currentFindings = findings;
|
|
29
18
|
}
|
|
30
19
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
31
20
|
function toText(data) {
|
|
@@ -34,25 +23,25 @@ function toText(data) {
|
|
|
34
23
|
function logged(name, fn) {
|
|
35
24
|
return async (args) => {
|
|
36
25
|
const hasArgs = Object.keys(args).length > 0;
|
|
37
|
-
|
|
26
|
+
logger.info(`→ ${name}${hasArgs ? ` ${JSON.stringify(args)}` : ''}`);
|
|
38
27
|
return fn(args);
|
|
39
28
|
};
|
|
40
29
|
}
|
|
41
30
|
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
42
|
-
function createMcpServer() {
|
|
43
|
-
const mcp = new
|
|
31
|
+
export function createMcpServer() {
|
|
32
|
+
const mcp = new McpServer({ name: 'infrawise', version });
|
|
44
33
|
mcp.registerTool('get_infra_overview', {
|
|
45
34
|
description: 'Returns a complete snapshot of all infrastructure: databases, queues, topics, secrets, parameters, log groups, lambdas, and all findings. Start here for a full picture.',
|
|
46
|
-
inputSchema:
|
|
35
|
+
inputSchema: z.object({}),
|
|
47
36
|
}, logged('get_infra_overview', async () => {
|
|
48
|
-
const tables =
|
|
49
|
-
const queues =
|
|
50
|
-
const topics =
|
|
51
|
-
const secrets =
|
|
52
|
-
const parameters =
|
|
53
|
-
const logGroups =
|
|
54
|
-
const lambdas =
|
|
55
|
-
const functions =
|
|
37
|
+
const tables = getTableNodes(currentGraph);
|
|
38
|
+
const queues = getQueueNodes(currentGraph);
|
|
39
|
+
const topics = getTopicNodes(currentGraph);
|
|
40
|
+
const secrets = getSecretNodes(currentGraph);
|
|
41
|
+
const parameters = getParameterNodes(currentGraph);
|
|
42
|
+
const logGroups = getLogGroupNodes(currentGraph);
|
|
43
|
+
const lambdas = getLambdaNodes(currentGraph);
|
|
44
|
+
const functions = getFunctionNodes(currentGraph);
|
|
56
45
|
return toText({
|
|
57
46
|
summary: {
|
|
58
47
|
tables: tables.length, functions: functions.length,
|
|
@@ -60,7 +49,7 @@ function createMcpServer() {
|
|
|
60
49
|
secrets: secrets.length, parameters: parameters.length,
|
|
61
50
|
logGroups: logGroups.length, lambdas: lambdas.length,
|
|
62
51
|
totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
|
|
63
|
-
findings:
|
|
52
|
+
findings: summarizeFindings(currentFindings),
|
|
64
53
|
},
|
|
65
54
|
databases: tables.map((t) => ({ name: t.name, type: t.databaseType })),
|
|
66
55
|
queues: queues.map((q) => ({ name: q.name, hasDLQ: q.hasDLQ, encrypted: q.encrypted, approximateMessages: q.approximateMessages })),
|
|
@@ -74,34 +63,41 @@ function createMcpServer() {
|
|
|
74
63
|
}));
|
|
75
64
|
mcp.registerTool('get_graph_summary', {
|
|
76
65
|
description: 'Returns the full infrastructure graph (all nodes and edges) plus findings summary.',
|
|
77
|
-
inputSchema:
|
|
66
|
+
inputSchema: z.object({}),
|
|
78
67
|
}, logged('get_graph_summary', async () => toText({
|
|
79
68
|
nodes: currentGraph.nodes,
|
|
80
69
|
edges: currentGraph.edges,
|
|
81
70
|
findings: currentFindings,
|
|
82
71
|
summary: {
|
|
83
72
|
totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
|
|
84
|
-
tables:
|
|
85
|
-
queues:
|
|
86
|
-
...
|
|
73
|
+
tables: getTableNodes(currentGraph).length, functions: getFunctionNodes(currentGraph).length,
|
|
74
|
+
queues: getQueueNodes(currentGraph).length, scans: getScanEdges(currentGraph).length,
|
|
75
|
+
...summarizeFindings(currentFindings),
|
|
87
76
|
},
|
|
88
77
|
})));
|
|
89
78
|
mcp.registerTool('analyze_function', {
|
|
90
|
-
description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, etc.',
|
|
91
|
-
inputSchema:
|
|
79
|
+
description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, trigger event shapes, etc.',
|
|
80
|
+
inputSchema: z.object({ function: z.string().describe('Function name to analyze') }),
|
|
92
81
|
}, logged('analyze_function', async ({ function: functionName }) => {
|
|
93
82
|
const funcNode = currentGraph.nodes.find((n) => n.type === 'function' && n.name === functionName);
|
|
94
|
-
if (
|
|
83
|
+
// Also check if there's a Lambda node with this name (for AWS-deployed functions)
|
|
84
|
+
const lambdaNode = currentGraph.nodes.find((n) => n.type === 'lambda' && n.name === functionName);
|
|
85
|
+
if (!funcNode && !lambdaNode) {
|
|
95
86
|
return toText({ function: functionName, found: false, issues: [], recommendations: [`Function "${functionName}" not found in the analyzed codebase.`] });
|
|
96
87
|
}
|
|
97
|
-
const outEdges =
|
|
88
|
+
const outEdges = funcNode ? getOutgoingEdges(currentGraph, funcNode.id) : [];
|
|
98
89
|
const relatedFindings = currentFindings.filter((f) => {
|
|
99
90
|
const meta = f.metadata;
|
|
100
91
|
return meta?.functionName === functionName || String(meta?.callerFunctions ?? '').includes(functionName);
|
|
101
92
|
});
|
|
93
|
+
const allTriggers = lambdaNode?.type === 'lambda' ? (lambdaNode.triggers ?? []) : [];
|
|
102
94
|
return toText({
|
|
103
95
|
function: functionName, found: true,
|
|
104
|
-
file: funcNode
|
|
96
|
+
file: funcNode?.type === 'function' ? funcNode.file : undefined,
|
|
97
|
+
triggers: allTriggers.map((t) => ({
|
|
98
|
+
type: t.type, source: t.sourceName, eventShape: t.eventShape,
|
|
99
|
+
...(t.ruleName ? { ruleName: t.ruleName, eventPattern: t.eventPattern } : {}),
|
|
100
|
+
})),
|
|
105
101
|
accesses: outEdges.map((e) => {
|
|
106
102
|
const target = currentGraph.nodes.find((n) => n.id === e.to);
|
|
107
103
|
return { targetId: e.to, edgeType: e.type, targetName: target && 'name' in target ? target.name : e.to, targetType: target?.type };
|
|
@@ -112,13 +108,14 @@ function createMcpServer() {
|
|
|
112
108
|
}));
|
|
113
109
|
mcp.registerTool('suggest_gsi', {
|
|
114
110
|
description: 'Get GSI suggestions for a DynamoDB table and attribute',
|
|
115
|
-
inputSchema:
|
|
116
|
-
table:
|
|
117
|
-
attribute:
|
|
111
|
+
inputSchema: z.object({
|
|
112
|
+
table: z.string().describe('DynamoDB table name'),
|
|
113
|
+
attribute: z.string().describe('Attribute to create the GSI on'),
|
|
118
114
|
}),
|
|
119
115
|
}, logged('suggest_gsi', async ({ table: tableName, attribute }) => {
|
|
120
116
|
const sanitizedAttr = attribute.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
121
|
-
const
|
|
117
|
+
const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
118
|
+
const indexName = `${sanitizedTable}-${sanitizedAttr}-index`;
|
|
122
119
|
const tableNode = currentGraph.nodes.find((n) => n.type === 'table' && n.databaseType === 'dynamodb' && 'name' in n && n.name === tableName);
|
|
123
120
|
return toText({
|
|
124
121
|
table: tableName, attribute, found: !!tableNode,
|
|
@@ -129,9 +126,9 @@ function createMcpServer() {
|
|
|
129
126
|
}));
|
|
130
127
|
mcp.registerTool('postgres_index_suggestions', {
|
|
131
128
|
description: 'Get PostgreSQL index suggestions for a table column',
|
|
132
|
-
inputSchema:
|
|
133
|
-
table:
|
|
134
|
-
column:
|
|
129
|
+
inputSchema: z.object({
|
|
130
|
+
table: z.string().describe('PostgreSQL table name'),
|
|
131
|
+
column: z.string().describe('Column name to index'),
|
|
135
132
|
}),
|
|
136
133
|
}, logged('postgres_index_suggestions', async ({ table: tableName, column }) => {
|
|
137
134
|
const sanitizedCol = column.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
@@ -139,33 +136,37 @@ function createMcpServer() {
|
|
|
139
136
|
const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
|
|
140
137
|
return toText({
|
|
141
138
|
table: tableName, column,
|
|
142
|
-
recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${
|
|
139
|
+
recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${sanitizedTable} (${sanitizedCol});`,
|
|
143
140
|
rationale: `An index on "${column}" eliminates sequential scans when filtering on this column.`,
|
|
144
141
|
notes: ['Use CONCURRENTLY to avoid locking the table', 'Run ANALYZE after creation',
|
|
145
|
-
`Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${
|
|
142
|
+
`Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${sanitizedTable} (${sanitizedCol}) WHERE ${sanitizedCol} IS NOT NULL;`],
|
|
146
143
|
});
|
|
147
144
|
}));
|
|
148
145
|
mcp.registerTool('suggest_mongo_index', {
|
|
149
146
|
description: 'Get index suggestions for a MongoDB collection field',
|
|
150
|
-
inputSchema:
|
|
151
|
-
collection:
|
|
152
|
-
field:
|
|
147
|
+
inputSchema: z.object({
|
|
148
|
+
collection: z.string().describe('MongoDB collection name'),
|
|
149
|
+
field: z.string().describe('Field name to index'),
|
|
153
150
|
}),
|
|
154
|
-
}, logged('suggest_mongo_index', async ({ collection, field }) =>
|
|
155
|
-
collection,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
151
|
+
}, logged('suggest_mongo_index', async ({ collection, field }) => {
|
|
152
|
+
const sanitizedCollection = collection.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
153
|
+
const sanitizedField = field.replace(/[^a-zA-Z0-9_.]/g, '_');
|
|
154
|
+
return toText({
|
|
155
|
+
collection, field,
|
|
156
|
+
recommendation: `db.${sanitizedCollection}.createIndex({ ${sanitizedField}: 1 })`,
|
|
157
|
+
rationale: `An index on "${field}" eliminates full collection scans when filtering on this field.`,
|
|
158
|
+
notes: [
|
|
159
|
+
`Compound: db.${sanitizedCollection}.createIndex({ ${sanitizedField}: 1, otherField: 1 })`,
|
|
160
|
+
`Text: db.${sanitizedCollection}.createIndex({ ${sanitizedField}: "text" })`,
|
|
161
|
+
`Verify: db.${sanitizedCollection}.explain("executionStats").find({ ${sanitizedField}: value })`,
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
}));
|
|
164
165
|
mcp.registerTool('mysql_index_suggestions', {
|
|
165
166
|
description: 'Get MySQL index suggestions for a table column',
|
|
166
|
-
inputSchema:
|
|
167
|
-
table:
|
|
168
|
-
column:
|
|
167
|
+
inputSchema: z.object({
|
|
168
|
+
table: z.string().describe('MySQL table name'),
|
|
169
|
+
column: z.string().describe('Column name to index'),
|
|
169
170
|
}),
|
|
170
171
|
}, logged('mysql_index_suggestions', async ({ table: tableName, column }) => {
|
|
171
172
|
const sanitizedCol = column.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
@@ -173,17 +174,17 @@ function createMcpServer() {
|
|
|
173
174
|
const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
|
|
174
175
|
return toText({
|
|
175
176
|
table: tableName, column,
|
|
176
|
-
recommendation: `ALTER TABLE ${
|
|
177
|
+
recommendation: `ALTER TABLE ${sanitizedTable} ADD INDEX ${indexName} (${sanitizedCol});`,
|
|
177
178
|
rationale: `An index on "${column}" eliminates full table scans when filtering on this column.`,
|
|
178
179
|
notes: ['MySQL InnoDB adds indexes online (no full lock for 5.6+)', 'EXPLAIN SELECT ... to verify after adding',
|
|
179
|
-
`Composite: ALTER TABLE ${
|
|
180
|
+
`Composite: ALTER TABLE ${sanitizedTable} ADD INDEX idx_composite (${sanitizedCol}, other_column);`],
|
|
180
181
|
});
|
|
181
182
|
}));
|
|
182
183
|
mcp.registerTool('get_queue_details', {
|
|
183
184
|
description: 'Returns all SQS queues with DLQ status, encryption, message counts, and retention.',
|
|
184
|
-
inputSchema:
|
|
185
|
+
inputSchema: z.object({}),
|
|
185
186
|
}, logged('get_queue_details', async () => {
|
|
186
|
-
const queues =
|
|
187
|
+
const queues = getQueueNodes(currentGraph);
|
|
187
188
|
const queueFindings = currentFindings.filter((f) => f.metadata?.queueName);
|
|
188
189
|
return toText({
|
|
189
190
|
total: queues.length,
|
|
@@ -196,16 +197,16 @@ function createMcpServer() {
|
|
|
196
197
|
}));
|
|
197
198
|
mcp.registerTool('get_topic_details', {
|
|
198
199
|
description: 'Returns all SNS topics with subscription counts and protocols.',
|
|
199
|
-
inputSchema:
|
|
200
|
+
inputSchema: z.object({}),
|
|
200
201
|
}, logged('get_topic_details', async () => {
|
|
201
|
-
const topics =
|
|
202
|
+
const topics = getTopicNodes(currentGraph);
|
|
202
203
|
return toText({ total: topics.length, topics: topics.map((t) => ({ name: t.name, provider: t.provider, subscriptionCount: t.subscriptionCount, encrypted: t.encrypted })) });
|
|
203
204
|
}));
|
|
204
205
|
mcp.registerTool('get_secrets_overview', {
|
|
205
206
|
description: 'Returns all Secrets Manager secrets: names, rotation status. Secret VALUES are never included.',
|
|
206
|
-
inputSchema:
|
|
207
|
+
inputSchema: z.object({}),
|
|
207
208
|
}, logged('get_secrets_overview', async () => {
|
|
208
|
-
const secrets =
|
|
209
|
+
const secrets = getSecretNodes(currentGraph);
|
|
209
210
|
const secretFindings = currentFindings.filter((f) => f.metadata?.secretName);
|
|
210
211
|
return toText({
|
|
211
212
|
total: secrets.length, note: 'Secret values are never included in this response.',
|
|
@@ -217,34 +218,55 @@ function createMcpServer() {
|
|
|
217
218
|
}));
|
|
218
219
|
mcp.registerTool('get_parameter_overview', {
|
|
219
220
|
description: 'Returns all SSM Parameter Store parameters: names, types, tiers. Parameter VALUES are never included.',
|
|
220
|
-
inputSchema:
|
|
221
|
+
inputSchema: z.object({}),
|
|
221
222
|
}, logged('get_parameter_overview', async () => {
|
|
222
|
-
const parameters =
|
|
223
|
+
const parameters = getParameterNodes(currentGraph);
|
|
223
224
|
return toText({
|
|
224
225
|
total: parameters.length, note: 'Parameter values are never included in this response.',
|
|
225
226
|
parameters: parameters.map((p) => ({ name: p.name, provider: p.provider, type: p.paramType, tier: p.tier })),
|
|
226
227
|
});
|
|
227
228
|
}));
|
|
228
229
|
mcp.registerTool('get_lambda_overview', {
|
|
229
|
-
description: 'Returns all Lambda functions: runtime, memory, timeout, env var key names (values never included).',
|
|
230
|
-
inputSchema:
|
|
230
|
+
description: 'Returns all Lambda functions: runtime, memory, timeout, env var key names (values never included), and known event source triggers with correct handler event shapes.',
|
|
231
|
+
inputSchema: z.object({}),
|
|
231
232
|
}, logged('get_lambda_overview', async () => {
|
|
232
|
-
const lambdas =
|
|
233
|
+
const lambdas = getLambdaNodes(currentGraph);
|
|
233
234
|
const lambdaFindings = currentFindings.filter((f) => f.metadata?.functionName);
|
|
234
235
|
return toText({
|
|
235
236
|
total: lambdas.length, note: 'Environment variable values are never included.',
|
|
236
237
|
lambdas: lambdas.map((l) => ({
|
|
237
238
|
name: l.name, runtime: l.runtime, memoryMB: l.memoryMB, timeoutSec: l.timeoutSec,
|
|
238
239
|
envVarCount: l.envVarKeys?.length ?? 0, envVarKeys: l.envVarKeys,
|
|
240
|
+
triggers: (l.triggers ?? []).map((t) => ({ type: t.type, source: t.sourceName, eventShape: t.eventShape, state: t.state })),
|
|
239
241
|
findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue })),
|
|
240
242
|
})),
|
|
241
243
|
});
|
|
242
244
|
}));
|
|
245
|
+
mcp.registerTool('get_eventbridge_details', {
|
|
246
|
+
description: 'Returns all EventBridge rules: name, state, schedule expression or event pattern, and target Lambda functions.',
|
|
247
|
+
inputSchema: z.object({}),
|
|
248
|
+
}, logged('get_eventbridge_details', async () => {
|
|
249
|
+
const rules = getEventBridgeRuleNodes(currentGraph);
|
|
250
|
+
return toText({
|
|
251
|
+
total: rules.length,
|
|
252
|
+
rules: rules.map((r) => ({
|
|
253
|
+
name: r.name,
|
|
254
|
+
state: r.state,
|
|
255
|
+
scheduleExpression: r.scheduleExpression,
|
|
256
|
+
eventPattern: r.eventPattern,
|
|
257
|
+
targets: currentGraph.edges
|
|
258
|
+
.filter((e) => e.from === r.id && e.type === 'triggers')
|
|
259
|
+
.map((e) => currentGraph.nodes.find((n) => n.id === e.to))
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.map((n) => n && 'name' in n ? n.name : ''),
|
|
262
|
+
})),
|
|
263
|
+
});
|
|
264
|
+
}));
|
|
243
265
|
mcp.registerTool('get_log_errors', {
|
|
244
266
|
description: 'Returns recent error patterns from CloudWatch log groups. Returns pattern counts and frequencies — never raw log messages.',
|
|
245
|
-
inputSchema:
|
|
267
|
+
inputSchema: z.object({ logGroup: z.string().describe('Filter to a specific log group name (optional)').optional() }),
|
|
246
268
|
}, logged('get_log_errors', async ({ logGroup: filterName }) => {
|
|
247
|
-
const logGroups =
|
|
269
|
+
const logGroups = getLogGroupNodes(currentGraph).filter((lg) => !filterName || lg.name.includes(filterName));
|
|
248
270
|
return toText({
|
|
249
271
|
note: 'Only error patterns and counts are returned — no raw log messages.',
|
|
250
272
|
windowHours: 24,
|
|
@@ -254,9 +276,9 @@ function createMcpServer() {
|
|
|
254
276
|
return mcp;
|
|
255
277
|
}
|
|
256
278
|
// ── Fastify server ────────────────────────────────────────────────────────────
|
|
257
|
-
function createServer(port = 3000) {
|
|
258
|
-
const fastify = (
|
|
259
|
-
fastify.register(
|
|
279
|
+
export function createServer(port = 3000) {
|
|
280
|
+
const fastify = Fastify({ logger: false });
|
|
281
|
+
fastify.register(cors, { origin: true });
|
|
260
282
|
const mcp = createMcpServer();
|
|
261
283
|
fastify.get('/health', async () => ({
|
|
262
284
|
status: 'ok', version,
|
|
@@ -265,7 +287,7 @@ function createServer(port = 3000) {
|
|
|
265
287
|
findings: currentFindings.length,
|
|
266
288
|
}));
|
|
267
289
|
fastify.post('/mcp', async (request, reply) => {
|
|
268
|
-
const transport = new
|
|
290
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
269
291
|
reply.raw.on('close', () => transport.close());
|
|
270
292
|
await mcp.connect(transport);
|
|
271
293
|
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
@@ -276,12 +298,13 @@ function createServer(port = 3000) {
|
|
|
276
298
|
start: async () => {
|
|
277
299
|
try {
|
|
278
300
|
await fastify.listen({ port, host: '0.0.0.0' });
|
|
279
|
-
|
|
301
|
+
logger.info(`Infrawise MCP server running at http://localhost:${port}`);
|
|
280
302
|
}
|
|
281
303
|
catch (e) {
|
|
282
|
-
|
|
304
|
+
logger.error(`Failed to start server: ${e instanceof Error ? e.message : String(e)}`);
|
|
283
305
|
process.exit(1);
|
|
284
306
|
}
|
|
285
307
|
},
|
|
286
308
|
};
|
|
287
309
|
}
|
|
310
|
+
export { currentGraph, currentFindings };
|
package/dist/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|