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/README.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# Infrawise
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/infrawise)
|
|
4
|
+
[](https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml)
|
|
5
|
+
[](https://github.com/Sidd27/infrawise/actions/workflows/ci.yml)
|
|
6
|
+
[](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
|
-
|
|
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
|
|
package/dist/adapters/aws.js
CHANGED
|
@@ -1,54 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
110
|
-
client.send(new
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
124
|
+
const res = await client.send(new DescribeParametersCommand({
|
|
143
125
|
NextToken: nextToken,
|
|
144
126
|
MaxResults: 50,
|
|
145
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
180
|
+
export async function validateSecretsAccess(cfg = {}) {
|
|
181
|
+
await new SecretsManagerClient(clientConfig(cfg)).send(new ListSecretsCommand({ MaxResults: 1 }));
|
|
198
182
|
}
|
|
199
183
|
// ─── Lambda ───────────────────────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
341
|
+
export async function validateRDSAccess(cfg = {}) {
|
|
342
|
+
await new RDSClient(clientConfig(cfg)).send(new DescribeDBInstancesCommand({ MaxRecords: 20 }));
|
|
263
343
|
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 =
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
60
|
+
logger.debug(`Filtered to ${tableNames.length} tables from config`);
|
|
71
61
|
}
|
|
72
62
|
else {
|
|
73
63
|
tableNames = allTables;
|
|
74
64
|
}
|
|
75
|
-
|
|
65
|
+
logger.debug(`Found ${tableNames.length} DynamoDB table(s)`);
|
|
76
66
|
}
|
|
77
67
|
catch (err) {
|
|
78
|
-
throw new
|
|
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
|
|
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
|
-
|
|
77
|
+
logger.debug(`Described table: ${tableName}`);
|
|
88
78
|
}
|
|
89
79
|
}
|
|
90
80
|
catch (err) {
|
|
91
|
-
|
|
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
|
|
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
|
|
98
|
+
await client.send(new ListTablesCommand({ Limit: 1 }));
|
|
109
99
|
}
|
package/dist/adapters/logs.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
116
|
+
export async function validateLogsAccess(cfg = {}) {
|
|
117
|
+
await new CloudWatchLogsClient(clientConfig(cfg)).send(new DescribeLogGroupsCommand({ limit: 1 }));
|
|
127
118
|
}
|