infrawise 0.6.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 +21 -3
- package/dist/adapters/aws.js +104 -10
- package/dist/adapters/dynamodb.js +0 -5
- package/dist/adapters/logs.js +0 -5
- package/dist/analyzers/aws-services.js +27 -0
- package/dist/analyzers/index.js +1 -1
- package/dist/cli/commands/analyze.js +16 -3
- package/dist/cli/commands/dev.js +4 -3
- package/dist/cli/commands/doctor.js +17 -1
- package/dist/cli/commands/init.js +15 -1
- package/dist/core/config.js +9 -3
- package/dist/graph/index.js +58 -1
- package/dist/server/index.js +35 -7
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ It statically analyzes your codebase, cloud infrastructure, and database schemas
|
|
|
15
15
|
|
|
16
16
|
## Why this exists
|
|
17
17
|
|
|
18
|
+
New software developers don't write wrong code. Claude Code writes wrong code and they ship it. Infrawise is the only thing standing between Claude Code's generated output and a production incident.
|
|
19
|
+
|
|
18
20
|
AI coding assistants can read your source files but have no deterministic knowledge of your infrastructure. They do not know which GSIs exist, how tables are partitioned, which functions already trigger scans, or where indexes are missing. So they guess.
|
|
19
21
|
|
|
20
22
|
Infrawise replaces guessing with infrastructure-aware context.
|
|
@@ -159,7 +161,7 @@ To let Claude Code manage the server lifecycle automatically:
|
|
|
159
161
|
|---|---|
|
|
160
162
|
| `get_infra_overview` | Complete snapshot — all services, counts, and high-severity findings |
|
|
161
163
|
| `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
|
|
162
|
-
| `analyze_function` | Issues in a specific function — scans, missing indexes, N+1 |
|
|
164
|
+
| `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes |
|
|
163
165
|
| `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
|
|
164
166
|
| `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
|
|
165
167
|
| `suggest_mongo_index` | Exact `createIndex` command for a MongoDB collection + field |
|
|
@@ -168,7 +170,8 @@ To let Claude Code manage the server lifecycle automatically:
|
|
|
168
170
|
| `get_topic_details` | SNS topics — subscription counts and protocols |
|
|
169
171
|
| `get_secrets_overview` | Secrets Manager — names and rotation status (values never included) |
|
|
170
172
|
| `get_parameter_overview` | SSM Parameter Store — names, types, tiers (values never included) |
|
|
171
|
-
| `get_lambda_overview` | Lambda functions — runtime, memory, timeout, env var key names |
|
|
173
|
+
| `get_lambda_overview` | Lambda functions — runtime, memory, timeout, triggers (SQS/DynamoDB/Kinesis/EventBridge), env var key names |
|
|
174
|
+
| `get_eventbridge_details` | EventBridge rules — name, state, schedule/event pattern, target functions |
|
|
172
175
|
| `get_log_errors` | CloudWatch error patterns and counts (no raw log messages) |
|
|
173
176
|
|
|
174
177
|
---
|
|
@@ -239,6 +242,12 @@ secretsManager:
|
|
|
239
242
|
|
|
240
243
|
lambda:
|
|
241
244
|
enabled: true
|
|
245
|
+
includeFunctions: # omit to include all functions
|
|
246
|
+
- processOrders
|
|
247
|
+
- generateReport
|
|
248
|
+
|
|
249
|
+
eventbridge:
|
|
250
|
+
enabled: true
|
|
242
251
|
|
|
243
252
|
rds:
|
|
244
253
|
enabled: false
|
|
@@ -312,7 +321,8 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
|
|
|
312
321
|
| SQS | Missing DLQs, unencrypted queues, large backlogs |
|
|
313
322
|
| Kafka (kafkajs) | Producer/consumer topic mapping from code |
|
|
314
323
|
| Secrets Manager | Missing secret rotation |
|
|
315
|
-
| Lambda | Default memory (128 MB), high timeouts |
|
|
324
|
+
| Lambda | Default memory (128 MB), high timeouts, triggers (SQS/DynamoDB/Kinesis/EventBridge), missing DLQ on trigger source |
|
|
325
|
+
| EventBridge | Rules, schedules, event patterns, target Lambda functions |
|
|
316
326
|
| RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
|
|
317
327
|
| CloudWatch Logs | Log groups with no retention policy |
|
|
318
328
|
| Terraform / CloudFormation / CDK | IaC drift vs deployed state |
|
|
@@ -420,6 +430,8 @@ src/
|
|
|
420
430
|
|
|
421
431
|
## Roadmap
|
|
422
432
|
|
|
433
|
+
Feature roadmap is tracked in the [GitHub Project](https://github.com/users/Sidd27/projects/1). Priorities, complexity, and virality scores are visible there. Feature requests and upvotes welcome.
|
|
434
|
+
|
|
423
435
|
### Planned
|
|
424
436
|
- Runtime tracing integration
|
|
425
437
|
- Incremental analysis for large monorepos
|
|
@@ -435,6 +447,12 @@ src/
|
|
|
435
447
|
|
|
436
448
|
---
|
|
437
449
|
|
|
450
|
+
## Demo
|
|
451
|
+
|
|
452
|
+
The `demo/localstack/` directory runs infrawise against real AWS APIs emulated locally via [LocalStack](https://localstack.cloud) — an open-source tool that spins up a full AWS environment in Docker so you can test AWS integrations at zero cost, with no real AWS account needed. See [`demo/localstack/README.md`](demo/localstack/README.md) for setup instructions.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
438
456
|
## Contributing
|
|
439
457
|
|
|
440
458
|
### Prerequisites
|
package/dist/adapters/aws.js
CHANGED
|
@@ -2,7 +2,8 @@ import { SQSClient, ListQueuesCommand, GetQueueAttributesCommand, } from '@aws-s
|
|
|
2
2
|
import { SNSClient, ListTopicsCommand, GetTopicAttributesCommand, ListSubscriptionsByTopicCommand, } from '@aws-sdk/client-sns';
|
|
3
3
|
import { SSMClient, DescribeParametersCommand, } from '@aws-sdk/client-ssm';
|
|
4
4
|
import { SecretsManagerClient, ListSecretsCommand, } from '@aws-sdk/client-secrets-manager';
|
|
5
|
-
import { LambdaClient, ListFunctionsCommand, } from '@aws-sdk/client-lambda';
|
|
5
|
+
import { LambdaClient, ListFunctionsCommand, ListEventSourceMappingsCommand, } from '@aws-sdk/client-lambda';
|
|
6
|
+
import { EventBridgeClient, ListRulesCommand, ListTargetsByRuleCommand, } from '@aws-sdk/client-eventbridge';
|
|
6
7
|
import { RDSClient, DescribeDBInstancesCommand, } from '@aws-sdk/client-rds';
|
|
7
8
|
import { fromIni } from '@aws-sdk/credential-providers';
|
|
8
9
|
import { logger } from '../core/index.js';
|
|
@@ -13,11 +14,6 @@ function clientConfig(cfg) {
|
|
|
13
14
|
base.endpoint = cfg.endpoint;
|
|
14
15
|
if (cfg.profile)
|
|
15
16
|
base.credentials = fromIni({ profile: cfg.profile });
|
|
16
|
-
else if (cfg.endpoint)
|
|
17
|
-
base.credentials = {
|
|
18
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
|
|
19
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
|
|
20
|
-
};
|
|
21
17
|
return base;
|
|
22
18
|
}
|
|
23
19
|
// ─── SQS ─────────────────────────────────────────────────────────────────────
|
|
@@ -101,7 +97,7 @@ export async function extractSNSMetadata(cfg = {}) {
|
|
|
101
97
|
name: arn.split(':').pop() ?? arn,
|
|
102
98
|
arn,
|
|
103
99
|
encrypted: !!attrs['KmsMasterKeyId'],
|
|
104
|
-
subscriptionCount:
|
|
100
|
+
subscriptionCount: parseInt(attrs['SubscriptionsConfirmed'] ?? '0', 10),
|
|
105
101
|
subscriptionProtocols: [...new Set(subs.map((s) => s.Protocol ?? 'unknown'))],
|
|
106
102
|
});
|
|
107
103
|
}
|
|
@@ -128,7 +124,9 @@ export async function extractSSMMetadata(cfg = {}) {
|
|
|
128
124
|
const res = await client.send(new DescribeParametersCommand({
|
|
129
125
|
NextToken: nextToken,
|
|
130
126
|
MaxResults: 50,
|
|
131
|
-
|
|
127
|
+
ParameterFilters: cfg.paths?.length
|
|
128
|
+
? [{ Key: 'Path', Values: cfg.paths, Option: 'Recursive' }]
|
|
129
|
+
: undefined,
|
|
132
130
|
}));
|
|
133
131
|
for (const p of res.Parameters ?? []) {
|
|
134
132
|
parameters.push({
|
|
@@ -183,7 +181,94 @@ export async function validateSecretsAccess(cfg = {}) {
|
|
|
183
181
|
await new SecretsManagerClient(clientConfig(cfg)).send(new ListSecretsCommand({ MaxResults: 1 }));
|
|
184
182
|
}
|
|
185
183
|
// ─── Lambda ───────────────────────────────────────────────────────────────────
|
|
186
|
-
|
|
184
|
+
const EVENT_SHAPES = {
|
|
185
|
+
sqs: 'event.Records[0].body',
|
|
186
|
+
dynamodb: 'event.Records[0].dynamodb.NewImage',
|
|
187
|
+
kinesis: 'event.Records[0].kinesis.data // base64',
|
|
188
|
+
msk: 'event.records[topic][0].value // base64',
|
|
189
|
+
sns: 'event.Records[0].Sns.Message',
|
|
190
|
+
s3: 'event.Records[0].s3.object.key',
|
|
191
|
+
eventbridge: 'event.detail',
|
|
192
|
+
unknown: 'event // unknown trigger type',
|
|
193
|
+
};
|
|
194
|
+
function triggerFromArn(arn, batchSize, state) {
|
|
195
|
+
let type = 'unknown';
|
|
196
|
+
if (arn.includes(':sqs:'))
|
|
197
|
+
type = 'sqs';
|
|
198
|
+
else if (arn.includes(':dynamodb:'))
|
|
199
|
+
type = 'dynamodb';
|
|
200
|
+
else if (arn.includes(':kinesis:'))
|
|
201
|
+
type = 'kinesis';
|
|
202
|
+
else if (arn.includes(':kafka:') || arn.toLowerCase().includes('msk'))
|
|
203
|
+
type = 'msk';
|
|
204
|
+
else if (arn.includes(':sns:'))
|
|
205
|
+
type = 'sns';
|
|
206
|
+
else if (arn.includes(':s3:'))
|
|
207
|
+
type = 's3';
|
|
208
|
+
const sourceName = arn.split(':').pop() ?? arn;
|
|
209
|
+
return { type, sourceArn: arn, sourceName, eventShape: EVENT_SHAPES[type], batchSize, state };
|
|
210
|
+
}
|
|
211
|
+
async function fetchAllEventSourceMappings(cfg) {
|
|
212
|
+
const client = new LambdaClient(clientConfig(cfg));
|
|
213
|
+
const triggerMap = new Map();
|
|
214
|
+
try {
|
|
215
|
+
let marker;
|
|
216
|
+
do {
|
|
217
|
+
const res = await client.send(new ListEventSourceMappingsCommand({ Marker: marker, MaxItems: 100 }));
|
|
218
|
+
for (const m of res.EventSourceMappings ?? []) {
|
|
219
|
+
if (!m.FunctionArn || !m.EventSourceArn)
|
|
220
|
+
continue;
|
|
221
|
+
const trigger = triggerFromArn(m.EventSourceArn, m.BatchSize, m.State);
|
|
222
|
+
const existing = triggerMap.get(m.FunctionArn) ?? [];
|
|
223
|
+
existing.push(trigger);
|
|
224
|
+
triggerMap.set(m.FunctionArn, existing);
|
|
225
|
+
}
|
|
226
|
+
marker = res.NextMarker;
|
|
227
|
+
} while (marker);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
logger.warn(`Event source mappings fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
231
|
+
}
|
|
232
|
+
return triggerMap;
|
|
233
|
+
}
|
|
234
|
+
export async function extractEventBridgeMetadata(cfg = {}) {
|
|
235
|
+
const client = new EventBridgeClient(clientConfig(cfg));
|
|
236
|
+
const rules = [];
|
|
237
|
+
try {
|
|
238
|
+
let nextToken;
|
|
239
|
+
do {
|
|
240
|
+
const res = await client.send(new ListRulesCommand({ NextToken: nextToken, Limit: 100 }));
|
|
241
|
+
for (const rule of res.Rules ?? []) {
|
|
242
|
+
if (!rule.Name)
|
|
243
|
+
continue;
|
|
244
|
+
try {
|
|
245
|
+
const targetsRes = await client.send(new ListTargetsByRuleCommand({ Rule: rule.Name }));
|
|
246
|
+
const targetArns = (targetsRes.Targets ?? []).map((t) => t.Arn ?? '').filter(Boolean);
|
|
247
|
+
rules.push({
|
|
248
|
+
name: rule.Name,
|
|
249
|
+
arn: rule.Arn ?? '',
|
|
250
|
+
state: rule.State ?? 'UNKNOWN',
|
|
251
|
+
scheduleExpression: rule.ScheduleExpression,
|
|
252
|
+
eventPattern: rule.EventPattern,
|
|
253
|
+
targetArns,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
logger.warn(`EventBridge targets fetch failed for ${rule.Name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
nextToken = res.NextToken;
|
|
261
|
+
} while (nextToken && rules.length < 500);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
logger.warn(`EventBridge list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
265
|
+
}
|
|
266
|
+
return rules;
|
|
267
|
+
}
|
|
268
|
+
export async function validateEventBridgeAccess(cfg = {}) {
|
|
269
|
+
await new EventBridgeClient(clientConfig(cfg)).send(new ListRulesCommand({ Limit: 1 }));
|
|
270
|
+
}
|
|
271
|
+
export async function extractLambdaMetadata(cfg = {}, includeFunctions) {
|
|
187
272
|
const client = new LambdaClient(clientConfig(cfg));
|
|
188
273
|
const functions = [];
|
|
189
274
|
try {
|
|
@@ -191,8 +276,11 @@ export async function extractLambdaMetadata(cfg = {}) {
|
|
|
191
276
|
do {
|
|
192
277
|
const res = await client.send(new ListFunctionsCommand({ Marker: marker, MaxItems: 50 }));
|
|
193
278
|
for (const fn of res.Functions ?? []) {
|
|
279
|
+
const name = fn.FunctionName ?? '';
|
|
280
|
+
if (includeFunctions?.length && !includeFunctions.includes(name))
|
|
281
|
+
continue;
|
|
194
282
|
functions.push({
|
|
195
|
-
name
|
|
283
|
+
name,
|
|
196
284
|
arn: fn.FunctionArn ?? '',
|
|
197
285
|
runtime: fn.Runtime,
|
|
198
286
|
handler: fn.Handler,
|
|
@@ -201,10 +289,16 @@ export async function extractLambdaMetadata(cfg = {}) {
|
|
|
201
289
|
lastModified: fn.LastModified,
|
|
202
290
|
envVarKeys: Object.keys(fn.Environment?.Variables ?? {}),
|
|
203
291
|
layers: (fn.Layers ?? []).map((l) => l.Arn ?? '').filter(Boolean),
|
|
292
|
+
triggers: [],
|
|
204
293
|
});
|
|
205
294
|
}
|
|
206
295
|
marker = res.NextMarker;
|
|
207
296
|
} while (marker && functions.length < 200);
|
|
297
|
+
// Fetch all event source mappings in one paginated call and attach to functions
|
|
298
|
+
const triggerMap = await fetchAllEventSourceMappings(cfg);
|
|
299
|
+
for (const fn of functions) {
|
|
300
|
+
fn.triggers = triggerMap.get(fn.arn) ?? [];
|
|
301
|
+
}
|
|
208
302
|
}
|
|
209
303
|
catch (err) {
|
|
210
304
|
logger.warn(`Lambda list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -10,11 +10,6 @@ function createDynamoClient(config) {
|
|
|
10
10
|
clientConfig.endpoint = endpoint;
|
|
11
11
|
if (profile)
|
|
12
12
|
clientConfig.credentials = fromIni({ profile });
|
|
13
|
-
else if (endpoint)
|
|
14
|
-
clientConfig.credentials = {
|
|
15
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
|
|
16
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
|
|
17
|
-
};
|
|
18
13
|
return new DynamoDBClient(clientConfig);
|
|
19
14
|
}
|
|
20
15
|
function parseTableDescription(desc) {
|
package/dist/adapters/logs.js
CHANGED
|
@@ -11,11 +11,6 @@ function clientConfig(cfg) {
|
|
|
11
11
|
base.endpoint = cfg.endpoint;
|
|
12
12
|
if (cfg.profile)
|
|
13
13
|
base.credentials = fromIni({ profile: cfg.profile });
|
|
14
|
-
else if (cfg.endpoint)
|
|
15
|
-
base.credentials = {
|
|
16
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
|
|
17
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
|
|
18
|
-
};
|
|
19
14
|
return base;
|
|
20
15
|
}
|
|
21
16
|
// Strip UUIDs, timestamps, numbers → group similar messages by pattern
|
|
@@ -136,6 +136,33 @@ export class LambdaDefaultMemoryAnalyzer {
|
|
|
136
136
|
return findings;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
export class LambdaMissingTriggerDLQAnalyzer {
|
|
140
|
+
name = 'LambdaMissingTriggerDLQAnalyzer';
|
|
141
|
+
async analyze(graph) {
|
|
142
|
+
const findings = [];
|
|
143
|
+
for (const node of graph.nodes) {
|
|
144
|
+
if (node.type !== 'lambda')
|
|
145
|
+
continue;
|
|
146
|
+
for (const trigger of node.triggers ?? []) {
|
|
147
|
+
if (trigger.type !== 'sqs' && trigger.type !== 'kinesis' && trigger.type !== 'dynamodb')
|
|
148
|
+
continue;
|
|
149
|
+
// Check if there's a DLQ/destination on the trigger edge — we flag if the source queue itself has no DLQ
|
|
150
|
+
// and the trigger is active, since failures will be silently dropped
|
|
151
|
+
const sourceQueue = graph.nodes.find((n) => n.type === 'queue' && n.name === trigger.sourceName);
|
|
152
|
+
if (sourceQueue && sourceQueue.type === 'queue' && !sourceQueue.hasDLQ) {
|
|
153
|
+
findings.push({
|
|
154
|
+
severity: 'high',
|
|
155
|
+
issue: `Lambda "${node.name}" is triggered by "${trigger.sourceName}" which has no DLQ`,
|
|
156
|
+
description: `"${node.name}" receives events from "${trigger.sourceName}" (${trigger.type.toUpperCase()}). If the Lambda handler fails, messages will be retried and eventually discarded with no failure record.`,
|
|
157
|
+
recommendation: `Add a DLQ to "${trigger.sourceName}" and set a destination config on the event source mapping so failed batches are captured and inspectable.`,
|
|
158
|
+
metadata: { functionName: node.name, triggerSource: trigger.sourceName, triggerType: trigger.type },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return findings;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
139
166
|
export class LambdaHighTimeoutAnalyzer {
|
|
140
167
|
name = 'LambdaHighTimeoutAnalyzer';
|
|
141
168
|
async analyze(graph) {
|
package/dist/analyzers/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer } from './p
|
|
|
4
4
|
export { MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer } from './mysql.js';
|
|
5
5
|
export { MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer } from './mongodb.js';
|
|
6
6
|
export { IaCDriftAnalyzer } from './terraform.js';
|
|
7
|
-
export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, } from './aws-services.js';
|
|
7
|
+
export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, } from './aws-services.js';
|
|
8
8
|
export { RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, } from './rds.js';
|
|
9
9
|
export async function runAllAnalyzers(graph, analyzers) {
|
|
10
10
|
const allFindings = [];
|
|
@@ -7,11 +7,11 @@ import { extractPostgresMetadata } from '../../adapters/postgres.js';
|
|
|
7
7
|
import { extractMySQLMetadata } from '../../adapters/mysql.js';
|
|
8
8
|
import { extractMongoMetadata } from '../../adapters/mongodb.js';
|
|
9
9
|
import { extractIaCSchema } from '../../adapters/terraform.js';
|
|
10
|
-
import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractRDSMetadata, } from '../../adapters/aws.js';
|
|
10
|
+
import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractEventBridgeMetadata, extractRDSMetadata, } from '../../adapters/aws.js';
|
|
11
11
|
import { extractLogsMetadata } from '../../adapters/logs.js';
|
|
12
12
|
import { scanRepository } from '../../context/index.js';
|
|
13
13
|
import { buildGraph } from '../../graph/index.js';
|
|
14
|
-
import { runAllAnalyzers, IaCDriftAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, } from '../../analyzers/index.js';
|
|
14
|
+
import { runAllAnalyzers, IaCDriftAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, } from '../../analyzers/index.js';
|
|
15
15
|
import { printFinding, printSummaryBox, log, printHeader } from '../utils.js';
|
|
16
16
|
function mkSpinner(text) {
|
|
17
17
|
return ora({ text: chalk.dim(text), color: 'cyan' }).start();
|
|
@@ -134,7 +134,7 @@ export async function runAnalyze(options = {}) {
|
|
|
134
134
|
if (config.lambda?.enabled === true) {
|
|
135
135
|
const s = mkSpinner('Extracting Lambda functions...');
|
|
136
136
|
try {
|
|
137
|
-
const result = await extractLambdaMetadata(awsCfg);
|
|
137
|
+
const result = await extractLambdaMetadata(awsCfg, config.lambda?.includeFunctions);
|
|
138
138
|
servicesMeta.lambda = result;
|
|
139
139
|
s.succeed(chalk.green('Lambda') + chalk.dim(` ${result.length} function(s)`));
|
|
140
140
|
}
|
|
@@ -142,6 +142,18 @@ export async function runAnalyze(options = {}) {
|
|
|
142
142
|
s.warn(chalk.yellow('Lambda skipped') + chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
// ── EventBridge ──────────────────────────────────────────────────────────────
|
|
146
|
+
if (config.eventbridge?.enabled === true) {
|
|
147
|
+
const s = mkSpinner('Extracting EventBridge rules...');
|
|
148
|
+
try {
|
|
149
|
+
const result = await extractEventBridgeMetadata(awsCfg);
|
|
150
|
+
servicesMeta.eventbridge = result;
|
|
151
|
+
s.succeed(chalk.green('EventBridge') + chalk.dim(` ${result.length} rule(s)`));
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
s.warn(chalk.yellow('EventBridge skipped') + chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
145
157
|
// ── RDS ──────────────────────────────────────────────────────────────────────
|
|
146
158
|
if (config.rds?.enabled === true) {
|
|
147
159
|
const s = mkSpinner('Extracting RDS instances...');
|
|
@@ -246,6 +258,7 @@ export async function runAnalyze(options = {}) {
|
|
|
246
258
|
...(config.lambda?.enabled === true ? [
|
|
247
259
|
new LambdaDefaultMemoryAnalyzer(),
|
|
248
260
|
new LambdaHighTimeoutAnalyzer(),
|
|
261
|
+
new LambdaMissingTriggerDLQAnalyzer(),
|
|
249
262
|
] : []),
|
|
250
263
|
...(config.rds?.enabled === true ? [
|
|
251
264
|
new RDSPubliclyAccessibleAnalyzer(),
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -20,6 +20,7 @@ const TOOL_MAP = [
|
|
|
20
20
|
{ name: 'get_secrets_overview', service: 'secretsManager' },
|
|
21
21
|
{ name: 'get_parameter_overview', service: 'ssm' },
|
|
22
22
|
{ name: 'get_lambda_overview', service: 'lambda' },
|
|
23
|
+
{ name: 'get_eventbridge_details', service: 'eventbridge' },
|
|
23
24
|
{ name: 'get_log_errors', service: 'cloudwatchLogs' },
|
|
24
25
|
];
|
|
25
26
|
function isEnabled(cfg, service) {
|
|
@@ -70,7 +71,7 @@ export async function runDev(options = {}) {
|
|
|
70
71
|
const cachedFindings = readCache('findings');
|
|
71
72
|
if (cachedGraph && cachedFindings) {
|
|
72
73
|
log.success('Cached analysis loaded', `${cachedGraph.nodes.length} nodes · ${cachedGraph.edges.length} edges · ${cachedFindings.length} finding(s)`);
|
|
73
|
-
setGraphState(cachedGraph, cachedFindings);
|
|
74
|
+
setGraphState(cachedGraph, cachedFindings, config);
|
|
74
75
|
}
|
|
75
76
|
else {
|
|
76
77
|
log.warn('No cache found — running analysis now...');
|
|
@@ -78,7 +79,7 @@ export async function runDev(options = {}) {
|
|
|
78
79
|
await runAnalyze({ repo: repoPath, config: options.config });
|
|
79
80
|
const freshGraph = readCache('graph') ?? { nodes: [], edges: [] };
|
|
80
81
|
const freshFindings = readCache('findings') ?? [];
|
|
81
|
-
setGraphState(freshGraph, freshFindings);
|
|
82
|
+
setGraphState(freshGraph, freshFindings, config);
|
|
82
83
|
}
|
|
83
84
|
console.log('');
|
|
84
85
|
// Start server
|
|
@@ -145,7 +146,7 @@ export async function runDev(options = {}) {
|
|
|
145
146
|
const spin = ora({ text: chalk.dim('Refreshing code analysis...'), color: 'cyan' }).start();
|
|
146
147
|
try {
|
|
147
148
|
const { graph, findings } = await runCodeRefresh(repoPath, config);
|
|
148
|
-
setGraphState(graph, findings);
|
|
149
|
+
setGraphState(graph, findings, config);
|
|
149
150
|
spin.succeed(chalk.green('Analysis refreshed') + chalk.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
|
|
150
151
|
}
|
|
151
152
|
catch (err) {
|
|
@@ -8,7 +8,7 @@ import { probeDynamoAccess } from '../../adapters/dynamodb.js';
|
|
|
8
8
|
import { validatePostgresAccess } from '../../adapters/postgres.js';
|
|
9
9
|
import { validateMySQLAccess } from '../../adapters/mysql.js';
|
|
10
10
|
import { validateMongoAccess } from '../../adapters/mongodb.js';
|
|
11
|
-
import { validateSQSAccess, validateSNSAccess, validateSSMAccess, validateSecretsAccess, validateLambdaAccess, } from '../../adapters/aws.js';
|
|
11
|
+
import { validateSQSAccess, validateSNSAccess, validateSSMAccess, validateSecretsAccess, validateLambdaAccess, validateEventBridgeAccess, } from '../../adapters/aws.js';
|
|
12
12
|
import { validateLogsAccess } from '../../adapters/logs.js';
|
|
13
13
|
import { printHeader } from '../utils.js';
|
|
14
14
|
async function runCheck(label, fn) {
|
|
@@ -173,6 +173,22 @@ export async function runDoctor(options = {}) {
|
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
}));
|
|
176
|
+
// EventBridge
|
|
177
|
+
results.push(await runCheck('Testing EventBridge access...', async () => {
|
|
178
|
+
if (config?.eventbridge?.enabled !== true)
|
|
179
|
+
return { name: 'EventBridge', status: 'skip', message: 'Disabled in config' };
|
|
180
|
+
try {
|
|
181
|
+
await validateEventBridgeAccess(awsCfg);
|
|
182
|
+
return { name: 'EventBridge', status: 'pass', message: 'Connected' };
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return {
|
|
186
|
+
name: 'EventBridge', status: 'warn',
|
|
187
|
+
message: err instanceof Error ? err.message : String(err),
|
|
188
|
+
detail: 'Check IAM: events:ListRules, events:ListTargetsByRule',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}));
|
|
176
192
|
// CloudWatch Logs
|
|
177
193
|
results.push(await runCheck('Testing CloudWatch Logs access...', async () => {
|
|
178
194
|
if (!config?.cloudwatchLogs?.enabled)
|
|
@@ -29,7 +29,7 @@ export async function runInit(options = {}) {
|
|
|
29
29
|
default: repoName,
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
|
-
type: '
|
|
32
|
+
type: 'select',
|
|
33
33
|
name: 'awsProfile',
|
|
34
34
|
message: 'AWS profile:',
|
|
35
35
|
choices: [
|
|
@@ -155,6 +155,18 @@ export async function runInit(options = {}) {
|
|
|
155
155
|
message: 'Introspect Lambda functions?',
|
|
156
156
|
default: true,
|
|
157
157
|
},
|
|
158
|
+
{
|
|
159
|
+
type: 'confirm',
|
|
160
|
+
name: 'eventbridgeEnabled',
|
|
161
|
+
message: 'Introspect EventBridge rules?',
|
|
162
|
+
default: true,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'confirm',
|
|
166
|
+
name: 'rdsEnabled',
|
|
167
|
+
message: 'Introspect RDS instances?',
|
|
168
|
+
default: true,
|
|
169
|
+
},
|
|
158
170
|
{
|
|
159
171
|
type: 'confirm',
|
|
160
172
|
name: 'logsEnabled',
|
|
@@ -195,6 +207,8 @@ export async function runInit(options = {}) {
|
|
|
195
207
|
ssm: { enabled: services.ssmEnabled, paths: ssmPaths },
|
|
196
208
|
secretsManager: { enabled: services.secretsEnabled },
|
|
197
209
|
lambda: { enabled: services.lambdaEnabled },
|
|
210
|
+
eventbridge: { enabled: services.eventbridgeEnabled },
|
|
211
|
+
rds: { enabled: services.rdsEnabled },
|
|
198
212
|
cloudwatchLogs: {
|
|
199
213
|
enabled: services.logsEnabled,
|
|
200
214
|
logGroupPrefixes,
|
package/dist/core/config.js
CHANGED
|
@@ -6,7 +6,7 @@ export const InfrawiseConfigSchema = z.object({
|
|
|
6
6
|
project: z.string().min(1, 'Project name is required'),
|
|
7
7
|
aws: z
|
|
8
8
|
.object({
|
|
9
|
-
profile: z.string().optional().default('
|
|
9
|
+
profile: z.string().optional().default(''),
|
|
10
10
|
region: z.string().optional().default('us-east-1'),
|
|
11
11
|
endpoint: z.string().optional(),
|
|
12
12
|
})
|
|
@@ -34,7 +34,11 @@ export const InfrawiseConfigSchema = z.object({
|
|
|
34
34
|
paths: z.array(z.string()).optional(),
|
|
35
35
|
}).optional(),
|
|
36
36
|
secretsManager: z.object({ enabled: z.boolean().optional().default(true) }).optional(),
|
|
37
|
-
lambda: z.object({
|
|
37
|
+
lambda: z.object({
|
|
38
|
+
enabled: z.boolean().optional().default(true),
|
|
39
|
+
includeFunctions: z.array(z.string()).optional(),
|
|
40
|
+
}).optional(),
|
|
41
|
+
eventbridge: z.object({ enabled: z.boolean().optional().default(true) }).optional(),
|
|
38
42
|
rds: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
39
43
|
kafka: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
40
44
|
cloudwatchLogs: z.object({
|
|
@@ -94,7 +98,7 @@ export function generateDefaultConfig(projectName, options) {
|
|
|
94
98
|
const config = {
|
|
95
99
|
project: projectName,
|
|
96
100
|
aws: {
|
|
97
|
-
profile: options?.aws?.profile ?? '
|
|
101
|
+
profile: options?.aws?.profile ?? '',
|
|
98
102
|
region: options?.aws?.region ?? 'us-east-1',
|
|
99
103
|
...(options?.aws?.endpoint ? { endpoint: options.aws.endpoint } : {}),
|
|
100
104
|
},
|
|
@@ -121,6 +125,8 @@ export function generateDefaultConfig(projectName, options) {
|
|
|
121
125
|
},
|
|
122
126
|
secretsManager: { enabled: options?.secretsManager?.enabled ?? true },
|
|
123
127
|
lambda: { enabled: options?.lambda?.enabled ?? true },
|
|
128
|
+
eventbridge: { enabled: options?.eventbridge?.enabled ?? true },
|
|
129
|
+
rds: { enabled: options?.rds?.enabled ?? true },
|
|
124
130
|
cloudwatchLogs: {
|
|
125
131
|
enabled: options?.cloudwatchLogs?.enabled ?? false,
|
|
126
132
|
logGroupPrefixes: options?.cloudwatchLogs?.logGroupPrefixes ?? [],
|
package/dist/graph/index.js
CHANGED
|
@@ -102,15 +102,69 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
for (const fn of servicesMeta.lambda ?? []) {
|
|
105
|
+
const lambdaId = `lambda:aws:${fn.name}`;
|
|
105
106
|
addNode({
|
|
106
|
-
id:
|
|
107
|
+
id: lambdaId,
|
|
107
108
|
type: 'lambda',
|
|
108
109
|
name: fn.name,
|
|
109
110
|
runtime: fn.runtime,
|
|
110
111
|
memoryMB: fn.memoryMB,
|
|
111
112
|
timeoutSec: fn.timeoutSec,
|
|
112
113
|
envVarKeys: fn.envVarKeys,
|
|
114
|
+
triggers: fn.triggers,
|
|
113
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
|
+
}
|
|
114
168
|
}
|
|
115
169
|
for (const db of servicesMeta.rds ?? []) {
|
|
116
170
|
addNode({
|
|
@@ -236,6 +290,9 @@ export function getLogGroupNodes(graph) {
|
|
|
236
290
|
export function getLambdaNodes(graph) {
|
|
237
291
|
return graph.nodes.filter((n) => n.type === 'lambda');
|
|
238
292
|
}
|
|
293
|
+
export function getEventBridgeRuleNodes(graph) {
|
|
294
|
+
return graph.nodes.filter((n) => n.type === 'eventbridge_rule');
|
|
295
|
+
}
|
|
239
296
|
export function getEdgesForNode(graph, nodeId) {
|
|
240
297
|
return graph.edges.filter((e) => e.from === nodeId || e.to === nodeId);
|
|
241
298
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -8,11 +8,11 @@ import { z } from 'zod';
|
|
|
8
8
|
import { logger } from '../core/index.js';
|
|
9
9
|
const { version } = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
|
|
10
10
|
import { summarizeFindings } from '../analyzers/index.js';
|
|
11
|
-
import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
|
|
11
|
+
import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
|
|
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, _config) {
|
|
16
16
|
currentGraph = graph;
|
|
17
17
|
currentFindings = findings;
|
|
18
18
|
}
|
|
@@ -76,21 +76,28 @@ export function createMcpServer() {
|
|
|
76
76
|
},
|
|
77
77
|
})));
|
|
78
78
|
mcp.registerTool('analyze_function', {
|
|
79
|
-
description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, etc.',
|
|
79
|
+
description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, trigger event shapes, etc.',
|
|
80
80
|
inputSchema: z.object({ function: z.string().describe('Function name to analyze') }),
|
|
81
81
|
}, logged('analyze_function', async ({ function: functionName }) => {
|
|
82
82
|
const funcNode = currentGraph.nodes.find((n) => n.type === 'function' && n.name === functionName);
|
|
83
|
-
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) {
|
|
84
86
|
return toText({ function: functionName, found: false, issues: [], recommendations: [`Function "${functionName}" not found in the analyzed codebase.`] });
|
|
85
87
|
}
|
|
86
|
-
const outEdges = getOutgoingEdges(currentGraph, funcNode.id);
|
|
88
|
+
const outEdges = funcNode ? getOutgoingEdges(currentGraph, funcNode.id) : [];
|
|
87
89
|
const relatedFindings = currentFindings.filter((f) => {
|
|
88
90
|
const meta = f.metadata;
|
|
89
91
|
return meta?.functionName === functionName || String(meta?.callerFunctions ?? '').includes(functionName);
|
|
90
92
|
});
|
|
93
|
+
const allTriggers = lambdaNode?.type === 'lambda' ? (lambdaNode.triggers ?? []) : [];
|
|
91
94
|
return toText({
|
|
92
95
|
function: functionName, found: true,
|
|
93
|
-
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
|
+
})),
|
|
94
101
|
accesses: outEdges.map((e) => {
|
|
95
102
|
const target = currentGraph.nodes.find((n) => n.id === e.to);
|
|
96
103
|
return { targetId: e.to, edgeType: e.type, targetName: target && 'name' in target ? target.name : e.to, targetType: target?.type };
|
|
@@ -220,7 +227,7 @@ export function createMcpServer() {
|
|
|
220
227
|
});
|
|
221
228
|
}));
|
|
222
229
|
mcp.registerTool('get_lambda_overview', {
|
|
223
|
-
description: 'Returns all Lambda functions: runtime, memory, timeout, env var key names (values never included).',
|
|
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.',
|
|
224
231
|
inputSchema: z.object({}),
|
|
225
232
|
}, logged('get_lambda_overview', async () => {
|
|
226
233
|
const lambdas = getLambdaNodes(currentGraph);
|
|
@@ -230,10 +237,31 @@ export function createMcpServer() {
|
|
|
230
237
|
lambdas: lambdas.map((l) => ({
|
|
231
238
|
name: l.name, runtime: l.runtime, memoryMB: l.memoryMB, timeoutSec: l.timeoutSec,
|
|
232
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 })),
|
|
233
241
|
findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue })),
|
|
234
242
|
})),
|
|
235
243
|
});
|
|
236
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
|
+
}));
|
|
237
265
|
mcp.registerTool('get_log_errors', {
|
|
238
266
|
description: 'Returns recent error patterns from CloudWatch log groups. Returns pattern counts and frequencies — never raw log messages.',
|
|
239
267
|
inputSchema: z.object({ logGroup: z.string().describe('Filter to a specific log group name (optional)').optional() }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
|
|
6
6
|
"keywords": [
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@aws-sdk/client-cloudwatch-logs": "^3.1048.0",
|
|
56
56
|
"@aws-sdk/client-dynamodb": "^3.1048.0",
|
|
57
|
+
"@aws-sdk/client-eventbridge": "^3.1051.0",
|
|
57
58
|
"@aws-sdk/client-lambda": "^3.1048.0",
|
|
58
59
|
"@aws-sdk/client-rds": "^3.1048.0",
|
|
59
60
|
"@aws-sdk/client-secrets-manager": "^3.1048.0",
|