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 CHANGED
@@ -1,11 +1,9 @@
1
1
  # Infrawise
2
2
 
3
- <table><tr>
4
- <td><a href="https://www.npmjs.com/package/infrawise"><img src="https://img.shields.io/npm/v/infrawise" alt="npm version"></a></td>
5
- <td><a href="https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml"><img src="https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml/badge.svg" alt="Publish to npm"></a></td>
6
- <td><a href="https://github.com/Sidd27/infrawise/actions/workflows/ci.yml"><img src="https://github.com/Sidd27/infrawise/actions/workflows/ci.yml/badge.svg" alt="CI"></a></td>
7
- <td><a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a></td>
8
- </tr></table>
3
+ [![npm version](https://img.shields.io/npm/v/infrawise)](https://www.npmjs.com/package/infrawise)
4
+ [![Publish to npm](https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml)
5
+ [![CI](https://github.com/Sidd27/infrawise/actions/workflows/ci.yml/badge.svg)](https://github.com/Sidd27/infrawise/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
7
 
10
8
  **Understand your infrastructure, not just your code.**
11
9
 
@@ -17,6 +15,8 @@ It statically analyzes your codebase, cloud infrastructure, and database schemas
17
15
 
18
16
  ## Why this exists
19
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
+
20
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.
21
21
 
22
22
  Infrawise replaces guessing with infrastructure-aware context.
@@ -161,7 +161,7 @@ To let Claude Code manage the server lifecycle automatically:
161
161
  |---|---|
162
162
  | `get_infra_overview` | Complete snapshot — all services, counts, and high-severity findings |
163
163
  | `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
164
- | `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 |
165
165
  | `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
166
166
  | `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
167
167
  | `suggest_mongo_index` | Exact `createIndex` command for a MongoDB collection + field |
@@ -170,7 +170,8 @@ To let Claude Code manage the server lifecycle automatically:
170
170
  | `get_topic_details` | SNS topics — subscription counts and protocols |
171
171
  | `get_secrets_overview` | Secrets Manager — names and rotation status (values never included) |
172
172
  | `get_parameter_overview` | SSM Parameter Store — names, types, tiers (values never included) |
173
- | `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 |
174
175
  | `get_log_errors` | CloudWatch error patterns and counts (no raw log messages) |
175
176
 
176
177
  ---
@@ -241,6 +242,12 @@ secretsManager:
241
242
 
242
243
  lambda:
243
244
  enabled: true
245
+ includeFunctions: # omit to include all functions
246
+ - processOrders
247
+ - generateReport
248
+
249
+ eventbridge:
250
+ enabled: true
244
251
 
245
252
  rds:
246
253
  enabled: false
@@ -314,7 +321,8 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
314
321
  | SQS | Missing DLQs, unencrypted queues, large backlogs |
315
322
  | Kafka (kafkajs) | Producer/consumer topic mapping from code |
316
323
  | Secrets Manager | Missing secret rotation |
317
- | 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 |
318
326
  | RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
319
327
  | CloudWatch Logs | Log groups with no retention policy |
320
328
  | Terraform / CloudFormation / CDK | IaC drift vs deployed state |
@@ -422,6 +430,8 @@ src/
422
430
 
423
431
  ## Roadmap
424
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
+
425
435
  ### Planned
426
436
  - Runtime tracing integration
427
437
  - Incremental analysis for large monorepos
@@ -437,6 +447,12 @@ src/
437
447
 
438
448
  ---
439
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
+
440
456
  ## Contributing
441
457
 
442
458
  ### Prerequisites
@@ -483,18 +499,14 @@ export class MyAnalyzer implements Analyzer {
483
499
 
484
500
  ### Releasing
485
501
 
486
- The git tag is the source of truth. The version in root `package.json` and the tag must always match.
487
-
488
502
  ```bash
489
503
  pnpm release patch # 0.1.2 → 0.1.3 (bug fixes)
490
504
  pnpm release minor # 0.1.2 → 0.2.0 (new features, backwards compatible)
491
505
  pnpm release major # 0.1.2 → 1.0.0 (breaking changes)
492
506
  pnpm release 1.5.0 # explicit version
493
-
494
- git push origin main v1.2.3
495
507
  ```
496
508
 
497
- `pnpm release` bumps the version, tags, and creates a draft GitHub release with notes generated from commit messages. Push the tag, then publish the draft on GitHub to trigger npm publish.
509
+ Bumps `package.json`, commits, tags, pushes, and creates a draft GitHub release with notes from commit messages. Then publish the draft on GitHub to trigger npm publish.
498
510
 
499
511
  ### PR checklist
500
512
 
@@ -1,54 +1,36 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractSQSMetadata = extractSQSMetadata;
4
- exports.validateSQSAccess = validateSQSAccess;
5
- exports.extractSNSMetadata = extractSNSMetadata;
6
- exports.validateSNSAccess = validateSNSAccess;
7
- exports.extractSSMMetadata = extractSSMMetadata;
8
- exports.validateSSMAccess = validateSSMAccess;
9
- exports.extractSecretsMetadata = extractSecretsMetadata;
10
- exports.validateSecretsAccess = validateSecretsAccess;
11
- exports.extractLambdaMetadata = extractLambdaMetadata;
12
- exports.validateLambdaAccess = validateLambdaAccess;
13
- exports.extractRDSMetadata = extractRDSMetadata;
14
- exports.validateRDSAccess = validateRDSAccess;
15
- const client_sqs_1 = require("@aws-sdk/client-sqs");
16
- const client_sns_1 = require("@aws-sdk/client-sns");
17
- const client_ssm_1 = require("@aws-sdk/client-ssm");
18
- const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
19
- const client_lambda_1 = require("@aws-sdk/client-lambda");
20
- const client_rds_1 = require("@aws-sdk/client-rds");
21
- const credential_providers_1 = require("@aws-sdk/credential-providers");
22
- const core_1 = require("../core");
1
+ import { SQSClient, ListQueuesCommand, GetQueueAttributesCommand, } from '@aws-sdk/client-sqs';
2
+ import { SNSClient, ListTopicsCommand, GetTopicAttributesCommand, ListSubscriptionsByTopicCommand, } from '@aws-sdk/client-sns';
3
+ import { SSMClient, DescribeParametersCommand, } from '@aws-sdk/client-ssm';
4
+ import { SecretsManagerClient, ListSecretsCommand, } from '@aws-sdk/client-secrets-manager';
5
+ import { LambdaClient, ListFunctionsCommand, ListEventSourceMappingsCommand, } from '@aws-sdk/client-lambda';
6
+ import { EventBridgeClient, ListRulesCommand, ListTargetsByRuleCommand, } from '@aws-sdk/client-eventbridge';
7
+ import { RDSClient, DescribeDBInstancesCommand, } from '@aws-sdk/client-rds';
8
+ import { fromIni } from '@aws-sdk/credential-providers';
9
+ import { logger } from '../core/index.js';
23
10
  function clientConfig(cfg) {
24
11
  const region = cfg.region ?? 'us-east-1';
25
12
  const base = { region };
26
13
  if (cfg.endpoint)
27
14
  base.endpoint = cfg.endpoint;
28
15
  if (cfg.profile)
29
- base.credentials = (0, credential_providers_1.fromIni)({ profile: cfg.profile });
30
- else if (cfg.endpoint)
31
- base.credentials = {
32
- accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
33
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
34
- };
16
+ base.credentials = fromIni({ profile: cfg.profile });
35
17
  return base;
36
18
  }
37
19
  // ─── SQS ─────────────────────────────────────────────────────────────────────
38
- async function extractSQSMetadata(cfg = {}) {
39
- const client = new client_sqs_1.SQSClient(clientConfig(cfg));
20
+ export async function extractSQSMetadata(cfg = {}) {
21
+ const client = new SQSClient(clientConfig(cfg));
40
22
  const queues = [];
41
23
  try {
42
24
  let nextToken;
43
25
  const queueUrls = [];
44
26
  do {
45
- const res = await client.send(new client_sqs_1.ListQueuesCommand({ NextToken: nextToken, MaxResults: 1000 }));
27
+ const res = await client.send(new ListQueuesCommand({ NextToken: nextToken, MaxResults: 1000 }));
46
28
  queueUrls.push(...(res.QueueUrls ?? []));
47
29
  nextToken = res.NextToken;
48
30
  } while (nextToken);
49
31
  for (const url of queueUrls) {
50
32
  try {
51
- const attrs = await client.send(new client_sqs_1.GetQueueAttributesCommand({
33
+ const attrs = await client.send(new GetQueueAttributesCommand({
52
34
  QueueUrl: url,
53
35
  AttributeNames: [
54
36
  'QueueArn', 'VisibilityTimeout', 'MessageRetentionPeriod',
@@ -79,35 +61,35 @@ async function extractSQSMetadata(cfg = {}) {
79
61
  });
80
62
  }
81
63
  catch (err) {
82
- core_1.logger.warn(`SQS attrs failed for ${url}: ${err instanceof Error ? err.message : String(err)}`);
64
+ logger.warn(`SQS attrs failed for ${url}: ${err instanceof Error ? err.message : String(err)}`);
83
65
  }
84
66
  }
85
67
  }
86
68
  catch (err) {
87
- core_1.logger.warn(`SQS list failed: ${err instanceof Error ? err.message : String(err)}`);
69
+ logger.warn(`SQS list failed: ${err instanceof Error ? err.message : String(err)}`);
88
70
  }
89
71
  return queues;
90
72
  }
91
- async function validateSQSAccess(cfg = {}) {
92
- await new client_sqs_1.SQSClient(clientConfig(cfg)).send(new client_sqs_1.ListQueuesCommand({ MaxResults: 1 }));
73
+ export async function validateSQSAccess(cfg = {}) {
74
+ await new SQSClient(clientConfig(cfg)).send(new ListQueuesCommand({ MaxResults: 1 }));
93
75
  }
94
76
  // ─── SNS ─────────────────────────────────────────────────────────────────────
95
- async function extractSNSMetadata(cfg = {}) {
96
- const client = new client_sns_1.SNSClient(clientConfig(cfg));
77
+ export async function extractSNSMetadata(cfg = {}) {
78
+ const client = new SNSClient(clientConfig(cfg));
97
79
  const topics = [];
98
80
  try {
99
81
  let nextToken;
100
82
  const topicArns = [];
101
83
  do {
102
- const res = await client.send(new client_sns_1.ListTopicsCommand({ NextToken: nextToken }));
84
+ const res = await client.send(new ListTopicsCommand({ NextToken: nextToken }));
103
85
  topicArns.push(...(res.Topics ?? []).map((t) => t.TopicArn ?? '').filter(Boolean));
104
86
  nextToken = res.NextToken;
105
87
  } while (nextToken);
106
88
  for (const arn of topicArns) {
107
89
  try {
108
90
  const [attrsRes, subsRes] = await Promise.all([
109
- client.send(new client_sns_1.GetTopicAttributesCommand({ TopicArn: arn })),
110
- client.send(new client_sns_1.ListSubscriptionsByTopicCommand({ TopicArn: arn })),
91
+ client.send(new GetTopicAttributesCommand({ TopicArn: arn })),
92
+ client.send(new ListSubscriptionsByTopicCommand({ TopicArn: arn })),
111
93
  ]);
112
94
  const attrs = attrsRes.Attributes ?? {};
113
95
  const subs = subsRes.Subscriptions ?? [];
@@ -115,34 +97,36 @@ async function extractSNSMetadata(cfg = {}) {
115
97
  name: arn.split(':').pop() ?? arn,
116
98
  arn,
117
99
  encrypted: !!attrs['KmsMasterKeyId'],
118
- subscriptionCount: subs.length,
100
+ subscriptionCount: parseInt(attrs['SubscriptionsConfirmed'] ?? '0', 10),
119
101
  subscriptionProtocols: [...new Set(subs.map((s) => s.Protocol ?? 'unknown'))],
120
102
  });
121
103
  }
122
104
  catch (err) {
123
- core_1.logger.warn(`SNS attrs failed for ${arn}: ${err instanceof Error ? err.message : String(err)}`);
105
+ logger.warn(`SNS attrs failed for ${arn}: ${err instanceof Error ? err.message : String(err)}`);
124
106
  }
125
107
  }
126
108
  }
127
109
  catch (err) {
128
- core_1.logger.warn(`SNS list failed: ${err instanceof Error ? err.message : String(err)}`);
110
+ logger.warn(`SNS list failed: ${err instanceof Error ? err.message : String(err)}`);
129
111
  }
130
112
  return topics;
131
113
  }
132
- async function validateSNSAccess(cfg = {}) {
133
- await new client_sns_1.SNSClient(clientConfig(cfg)).send(new client_sns_1.ListTopicsCommand({}));
114
+ export async function validateSNSAccess(cfg = {}) {
115
+ await new SNSClient(clientConfig(cfg)).send(new ListTopicsCommand({}));
134
116
  }
135
117
  // ─── SSM Parameter Store ──────────────────────────────────────────────────────
136
- async function extractSSMMetadata(cfg = {}) {
137
- const client = new client_ssm_1.SSMClient(clientConfig(cfg));
118
+ export async function extractSSMMetadata(cfg = {}) {
119
+ const client = new SSMClient(clientConfig(cfg));
138
120
  const parameters = [];
139
121
  try {
140
122
  let nextToken;
141
123
  do {
142
- const res = await client.send(new client_ssm_1.DescribeParametersCommand({
124
+ const res = await client.send(new DescribeParametersCommand({
143
125
  NextToken: nextToken,
144
126
  MaxResults: 50,
145
- // Only metadata — GetParameter/GetParameters would return values
127
+ ParameterFilters: cfg.paths?.length
128
+ ? [{ Key: 'Path', Values: cfg.paths, Option: 'Recursive' }]
129
+ : undefined,
146
130
  }));
147
131
  for (const p of res.Parameters ?? []) {
148
132
  parameters.push({
@@ -158,22 +142,22 @@ async function extractSSMMetadata(cfg = {}) {
158
142
  } while (nextToken && parameters.length < 500);
159
143
  }
160
144
  catch (err) {
161
- core_1.logger.warn(`SSM list failed: ${err instanceof Error ? err.message : String(err)}`);
145
+ logger.warn(`SSM list failed: ${err instanceof Error ? err.message : String(err)}`);
162
146
  }
163
147
  return parameters;
164
148
  }
165
- async function validateSSMAccess(cfg = {}) {
166
- await new client_ssm_1.SSMClient(clientConfig(cfg)).send(new client_ssm_1.DescribeParametersCommand({ MaxResults: 1 }));
149
+ export async function validateSSMAccess(cfg = {}) {
150
+ await new SSMClient(clientConfig(cfg)).send(new DescribeParametersCommand({ MaxResults: 1 }));
167
151
  }
168
152
  // ─── Secrets Manager ──────────────────────────────────────────────────────────
169
- async function extractSecretsMetadata(cfg = {}) {
170
- const client = new client_secrets_manager_1.SecretsManagerClient(clientConfig(cfg));
153
+ export async function extractSecretsMetadata(cfg = {}) {
154
+ const client = new SecretsManagerClient(clientConfig(cfg));
171
155
  const secrets = [];
172
156
  try {
173
157
  let nextToken;
174
158
  do {
175
159
  // ListSecrets never returns secret values
176
- const res = await client.send(new client_secrets_manager_1.ListSecretsCommand({ NextToken: nextToken, MaxResults: 100 }));
160
+ const res = await client.send(new ListSecretsCommand({ NextToken: nextToken, MaxResults: 100 }));
177
161
  for (const s of res.SecretList ?? []) {
178
162
  secrets.push({
179
163
  name: s.Name ?? '',
@@ -189,24 +173,114 @@ async function extractSecretsMetadata(cfg = {}) {
189
173
  } while (nextToken && secrets.length < 200);
190
174
  }
191
175
  catch (err) {
192
- core_1.logger.warn(`Secrets Manager list failed: ${err instanceof Error ? err.message : String(err)}`);
176
+ logger.warn(`Secrets Manager list failed: ${err instanceof Error ? err.message : String(err)}`);
193
177
  }
194
178
  return secrets;
195
179
  }
196
- async function validateSecretsAccess(cfg = {}) {
197
- await new client_secrets_manager_1.SecretsManagerClient(clientConfig(cfg)).send(new client_secrets_manager_1.ListSecretsCommand({ MaxResults: 1 }));
180
+ export async function validateSecretsAccess(cfg = {}) {
181
+ await new SecretsManagerClient(clientConfig(cfg)).send(new ListSecretsCommand({ MaxResults: 1 }));
198
182
  }
199
183
  // ─── Lambda ───────────────────────────────────────────────────────────────────
200
- async function extractLambdaMetadata(cfg = {}) {
201
- const client = new client_lambda_1.LambdaClient(clientConfig(cfg));
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) {
272
+ const client = new LambdaClient(clientConfig(cfg));
202
273
  const functions = [];
203
274
  try {
204
275
  let marker;
205
276
  do {
206
- const res = await client.send(new client_lambda_1.ListFunctionsCommand({ Marker: marker, MaxItems: 50 }));
277
+ const res = await client.send(new ListFunctionsCommand({ Marker: marker, MaxItems: 50 }));
207
278
  for (const fn of res.Functions ?? []) {
279
+ const name = fn.FunctionName ?? '';
280
+ if (includeFunctions?.length && !includeFunctions.includes(name))
281
+ continue;
208
282
  functions.push({
209
- name: fn.FunctionName ?? '',
283
+ name,
210
284
  arn: fn.FunctionArn ?? '',
211
285
  runtime: fn.Runtime,
212
286
  handler: fn.Handler,
@@ -215,27 +289,33 @@ async function extractLambdaMetadata(cfg = {}) {
215
289
  lastModified: fn.LastModified,
216
290
  envVarKeys: Object.keys(fn.Environment?.Variables ?? {}),
217
291
  layers: (fn.Layers ?? []).map((l) => l.Arn ?? '').filter(Boolean),
292
+ triggers: [],
218
293
  });
219
294
  }
220
295
  marker = res.NextMarker;
221
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
+ }
222
302
  }
223
303
  catch (err) {
224
- core_1.logger.warn(`Lambda list failed: ${err instanceof Error ? err.message : String(err)}`);
304
+ logger.warn(`Lambda list failed: ${err instanceof Error ? err.message : String(err)}`);
225
305
  }
226
306
  return functions;
227
307
  }
228
- async function validateLambdaAccess(cfg = {}) {
229
- await new client_lambda_1.LambdaClient(clientConfig(cfg)).send(new client_lambda_1.ListFunctionsCommand({ MaxItems: 1 }));
308
+ export async function validateLambdaAccess(cfg = {}) {
309
+ await new LambdaClient(clientConfig(cfg)).send(new ListFunctionsCommand({ MaxItems: 1 }));
230
310
  }
231
311
  // ─── RDS ─────────────────────────────────────────────────────────────────────
232
- async function extractRDSMetadata(cfg = {}) {
233
- const client = new client_rds_1.RDSClient(clientConfig(cfg));
312
+ export async function extractRDSMetadata(cfg = {}) {
313
+ const client = new RDSClient(clientConfig(cfg));
234
314
  const instances = [];
235
315
  try {
236
316
  let marker;
237
317
  do {
238
- const res = await client.send(new client_rds_1.DescribeDBInstancesCommand({ Marker: marker, MaxRecords: 100 }));
318
+ const res = await client.send(new DescribeDBInstancesCommand({ Marker: marker, MaxRecords: 100 }));
239
319
  for (const db of res.DBInstances ?? []) {
240
320
  instances.push({
241
321
  dbInstanceIdentifier: db.DBInstanceIdentifier ?? '',
@@ -254,10 +334,10 @@ async function extractRDSMetadata(cfg = {}) {
254
334
  } while (marker && instances.length < 200);
255
335
  }
256
336
  catch (err) {
257
- core_1.logger.warn(`RDS list failed: ${err instanceof Error ? err.message : String(err)}`);
337
+ logger.warn(`RDS list failed: ${err instanceof Error ? err.message : String(err)}`);
258
338
  }
259
339
  return instances;
260
340
  }
261
- async function validateRDSAccess(cfg = {}) {
262
- await new client_rds_1.RDSClient(clientConfig(cfg)).send(new client_rds_1.DescribeDBInstancesCommand({ MaxRecords: 20 }));
341
+ export async function validateRDSAccess(cfg = {}) {
342
+ await new RDSClient(clientConfig(cfg)).send(new DescribeDBInstancesCommand({ MaxRecords: 20 }));
263
343
  }
@@ -1,11 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractDynamoMetadata = extractDynamoMetadata;
4
- exports.validateDynamoAccess = validateDynamoAccess;
5
- exports.probeDynamoAccess = probeDynamoAccess;
6
- const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
7
- const credential_providers_1 = require("@aws-sdk/credential-providers");
8
- const core_1 = require("../core");
1
+ import { DynamoDBClient, ListTablesCommand, DescribeTableCommand, } from '@aws-sdk/client-dynamodb';
2
+ import { fromIni } from '@aws-sdk/credential-providers';
3
+ import { DynamoDBError, logger } from '../core/index.js';
9
4
  function createDynamoClient(config) {
10
5
  const region = config.aws?.region ?? 'us-east-1';
11
6
  const profile = config.aws?.profile;
@@ -14,13 +9,8 @@ function createDynamoClient(config) {
14
9
  if (endpoint)
15
10
  clientConfig.endpoint = endpoint;
16
11
  if (profile)
17
- clientConfig.credentials = (0, credential_providers_1.fromIni)({ profile });
18
- else if (endpoint)
19
- clientConfig.credentials = {
20
- accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
21
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
22
- };
23
- return new client_dynamodb_1.DynamoDBClient(clientConfig);
12
+ clientConfig.credentials = fromIni({ profile });
13
+ return new DynamoDBClient(clientConfig);
24
14
  }
25
15
  function parseTableDescription(desc) {
26
16
  const tableName = desc.TableName ?? 'unknown';
@@ -47,7 +37,7 @@ async function listAllTables(client) {
47
37
  const tableNames = [];
48
38
  let lastEvaluatedTableName;
49
39
  do {
50
- const command = new client_dynamodb_1.ListTablesCommand({
40
+ const command = new ListTablesCommand({
51
41
  ExclusiveStartTableName: lastEvaluatedTableName,
52
42
  Limit: 100,
53
43
  });
@@ -59,7 +49,7 @@ async function listAllTables(client) {
59
49
  } while (lastEvaluatedTableName);
60
50
  return tableNames;
61
51
  }
62
- async function extractDynamoMetadata(config) {
52
+ export async function extractDynamoMetadata(config) {
63
53
  const client = createDynamoClient(config);
64
54
  const includeTables = config.dynamodb?.includeTables;
65
55
  let tableNames;
@@ -67,43 +57,43 @@ async function extractDynamoMetadata(config) {
67
57
  const allTables = await listAllTables(client);
68
58
  if (includeTables && includeTables.length > 0) {
69
59
  tableNames = allTables.filter((name) => includeTables.includes(name));
70
- core_1.logger.debug(`Filtered to ${tableNames.length} tables from config`);
60
+ logger.debug(`Filtered to ${tableNames.length} tables from config`);
71
61
  }
72
62
  else {
73
63
  tableNames = allTables;
74
64
  }
75
- core_1.logger.debug(`Found ${tableNames.length} DynamoDB table(s)`);
65
+ logger.debug(`Found ${tableNames.length} DynamoDB table(s)`);
76
66
  }
77
67
  catch (err) {
78
- throw new core_1.DynamoDBError(err instanceof Error ? err.message : 'Failed to list DynamoDB tables');
68
+ throw new DynamoDBError(err instanceof Error ? err.message : 'Failed to list DynamoDB tables');
79
69
  }
80
70
  const results = [];
81
71
  for (const tableName of tableNames) {
82
72
  try {
83
- const command = new client_dynamodb_1.DescribeTableCommand({ TableName: tableName });
73
+ const command = new DescribeTableCommand({ TableName: tableName });
84
74
  const response = await client.send(command);
85
75
  if (response.Table) {
86
76
  results.push(parseTableDescription(response.Table));
87
- core_1.logger.debug(`Described table: ${tableName}`);
77
+ logger.debug(`Described table: ${tableName}`);
88
78
  }
89
79
  }
90
80
  catch (err) {
91
- core_1.logger.warn(`Failed to describe table ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
81
+ logger.warn(`Failed to describe table ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
92
82
  }
93
83
  }
94
84
  return results;
95
85
  }
96
- async function validateDynamoAccess(config) {
86
+ export async function validateDynamoAccess(config) {
97
87
  const client = createDynamoClient(config);
98
88
  try {
99
- await client.send(new client_dynamodb_1.ListTablesCommand({ Limit: 1 }));
89
+ await client.send(new ListTablesCommand({ Limit: 1 }));
100
90
  return true;
101
91
  }
102
92
  catch {
103
93
  return false;
104
94
  }
105
95
  }
106
- async function probeDynamoAccess(config) {
96
+ export async function probeDynamoAccess(config) {
107
97
  const client = createDynamoClient(config);
108
- await client.send(new client_dynamodb_1.ListTablesCommand({ Limit: 1 }));
98
+ await client.send(new ListTablesCommand({ Limit: 1 }));
109
99
  }
@@ -1,10 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractLogsMetadata = extractLogsMetadata;
4
- exports.validateLogsAccess = validateLogsAccess;
5
- const client_cloudwatch_logs_1 = require("@aws-sdk/client-cloudwatch-logs");
6
- const credential_providers_1 = require("@aws-sdk/credential-providers");
7
- const core_1 = require("../core");
1
+ import { CloudWatchLogsClient, DescribeLogGroupsCommand, FilterLogEventsCommand, } from '@aws-sdk/client-cloudwatch-logs';
2
+ import { fromIni } from '@aws-sdk/credential-providers';
3
+ import { logger } from '../core/index.js';
8
4
  // Hard caps to prevent context bloat
9
5
  const MAX_LOG_GROUPS = 50;
10
6
  const MAX_EVENTS_PER_GROUP = 50;
@@ -14,12 +10,7 @@ function clientConfig(cfg) {
14
10
  if (cfg.endpoint)
15
11
  base.endpoint = cfg.endpoint;
16
12
  if (cfg.profile)
17
- base.credentials = (0, credential_providers_1.fromIni)({ profile: cfg.profile });
18
- else if (cfg.endpoint)
19
- base.credentials = {
20
- accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'test',
21
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'test',
22
- };
13
+ base.credentials = fromIni({ profile: cfg.profile });
23
14
  return base;
24
15
  }
25
16
  // Strip UUIDs, timestamps, numbers → group similar messages by pattern
@@ -44,8 +35,8 @@ function topPatterns(messages, limit = 5) {
44
35
  .slice(0, limit)
45
36
  .map(([pattern, count]) => ({ pattern, count }));
46
37
  }
47
- async function extractLogsMetadata(cfg = {}) {
48
- const client = new client_cloudwatch_logs_1.CloudWatchLogsClient(clientConfig(cfg));
38
+ export async function extractLogsMetadata(cfg = {}) {
39
+ const client = new CloudWatchLogsClient(clientConfig(cfg));
49
40
  const windowMs = (cfg.windowHours ?? 24) * 60 * 60 * 1000;
50
41
  const startTime = Date.now() - windowMs;
51
42
  const summaries = [];
@@ -56,7 +47,7 @@ async function extractLogsMetadata(cfg = {}) {
56
47
  for (const prefix of prefixes) {
57
48
  let nextToken;
58
49
  do {
59
- const res = await client.send(new client_cloudwatch_logs_1.DescribeLogGroupsCommand({
50
+ const res = await client.send(new DescribeLogGroupsCommand({
60
51
  nextToken,
61
52
  limit: Math.min(50, MAX_LOG_GROUPS - logGroups.length),
62
53
  ...(prefix ? { logGroupNamePrefix: prefix } : {}),
@@ -73,7 +64,7 @@ async function extractLogsMetadata(cfg = {}) {
73
64
  }
74
65
  }
75
66
  catch (err) {
76
- core_1.logger.warn(`CloudWatch Logs discovery failed: ${err instanceof Error ? err.message : String(err)}`);
67
+ logger.warn(`CloudWatch Logs discovery failed: ${err instanceof Error ? err.message : String(err)}`);
77
68
  return summaries;
78
69
  }
79
70
  // Sample errors from each group — patterns only, never raw messages for unrelated groups
@@ -85,7 +76,7 @@ async function extractLogsMetadata(cfg = {}) {
85
76
  if (errorMessages.length + warnMessages.length >= MAX_EVENTS_PER_GROUP)
86
77
  break;
87
78
  try {
88
- const res = await client.send(new client_cloudwatch_logs_1.FilterLogEventsCommand({
79
+ const res = await client.send(new FilterLogEventsCommand({
89
80
  logGroupName: lg.name,
90
81
  filterPattern,
91
82
  startTime,
@@ -119,9 +110,9 @@ async function extractLogsMetadata(cfg = {}) {
119
110
  lastErrorTime,
120
111
  });
121
112
  }
122
- core_1.logger.debug(`CloudWatch Logs: sampled ${summaries.length} log group(s)`);
113
+ logger.debug(`CloudWatch Logs: sampled ${summaries.length} log group(s)`);
123
114
  return summaries;
124
115
  }
125
- async function validateLogsAccess(cfg = {}) {
126
- await new client_cloudwatch_logs_1.CloudWatchLogsClient(clientConfig(cfg)).send(new client_cloudwatch_logs_1.DescribeLogGroupsCommand({ limit: 1 }));
116
+ export async function validateLogsAccess(cfg = {}) {
117
+ await new CloudWatchLogsClient(clientConfig(cfg)).send(new DescribeLogGroupsCommand({ limit: 1 }));
127
118
  }