infrawise 0.1.2 → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sidd27
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -142,16 +142,23 @@ Alternatively, let Claude Code manage the server lifecycle automatically:
142
142
 
143
143
  ### Step 3: What Claude can now do
144
144
 
145
- Claude gains four tools it calls silently while helping you:
145
+ Claude gains 13 tools it calls silently while helping you:
146
146
 
147
147
  | Tool | What it gives Claude |
148
148
  |---|---|
149
- | `get_graph_summary` | Full infrastructure graph tables, GSIs, function relationships, all findings |
150
- | `analyze_function` | Issues introduced by a specific function — scans, missing indexes, N+1 |
149
+ | `get_infra_overview` | Complete snapshotall services, counts, and high-severity findings |
150
+ | `get_graph_summary` | Full infrastructure graph all nodes, edges, and findings |
151
+ | `analyze_function` | Issues in a specific function — scans, missing indexes, N+1 |
151
152
  | `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
152
153
  | `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
153
154
  | `suggest_mongo_index` | Exact `createIndex` command for a MongoDB collection + field |
154
155
  | `mysql_index_suggestions` | Exact `ALTER TABLE ADD INDEX` SQL for your MySQL table |
156
+ | `get_queue_details` | SQS queues — DLQ status, encryption, message counts |
157
+ | `get_topic_details` | SNS topics — subscription counts and protocols |
158
+ | `get_secrets_overview` | Secrets Manager — names and rotation status (values never included) |
159
+ | `get_parameter_overview` | SSM Parameter Store — names, types, tiers (values never included) |
160
+ | `get_lambda_overview` | Lambda functions — runtime, memory, timeout, env var key names |
161
+ | `get_log_errors` | CloudWatch error patterns and counts (no raw log messages) |
155
162
 
156
163
  ### What changes in practice
157
164
 
@@ -187,7 +194,7 @@ Infrawise works with any editor or tool that supports MCP or can call an HTTP AP
187
194
 
188
195
  ## Configuration
189
196
 
190
- `infrawise.yaml` is generated by `infrawise init` and lives in your repo root:
197
+ `infrawise.yaml` is generated by `infrawise init` and lives in your repo root. Every service must be explicitly `enabled: true` — infrawise never connects to anything not listed in config.
191
198
 
192
199
  ```yaml
193
200
  project: payments-service
@@ -197,6 +204,7 @@ aws:
197
204
  region: ap-south-1
198
205
 
199
206
  dynamodb:
207
+ enabled: true
200
208
  includeTables: # omit to include all tables
201
209
  - Orders
202
210
  - Users
@@ -205,6 +213,38 @@ postgres:
205
213
  enabled: true
206
214
  connectionString: postgresql://infrawise_ro:password@host:5432/mydb
207
215
 
216
+ mysql:
217
+ enabled: false
218
+ connectionString: ""
219
+
220
+ mongodb:
221
+ enabled: false
222
+ connectionString: ""
223
+
224
+ sqs:
225
+ enabled: true
226
+
227
+ sns:
228
+ enabled: true
229
+
230
+ ssm:
231
+ enabled: true
232
+ paths: [] # filter by prefix e.g. ["/myapp/prod"]
233
+
234
+ secretsManager:
235
+ enabled: true
236
+
237
+ lambda:
238
+ enabled: true
239
+
240
+ rds:
241
+ enabled: false
242
+
243
+ cloudwatchLogs:
244
+ enabled: false
245
+ logGroupPrefixes: []
246
+ windowHours: 24
247
+
208
248
  analysis:
209
249
  sampleSize: 100
210
250
  ```
@@ -250,44 +290,52 @@ For Amazon RDS: allow inbound on port 5432 from your machine's IP in the securit
250
290
 
251
291
  ---
252
292
 
253
- ## What gets analyzed
293
+ ## Language support
254
294
 
255
- ### DynamoDB
295
+ Infrawise has two analysis layers with different language requirements:
256
296
 
257
- | Analyzer | Severity | What it detects |
258
- |---|---|---|
259
- | Full Table Scan | High | `.scan()` calls without filters |
260
- | Missing GSI | Medium | Tables queried without a matching GSI |
261
- | Hot Partition | Medium | 5+ distinct code paths hitting the same table |
262
-
263
- ### PostgreSQL
297
+ ### Infrastructure-level analysis any project, any language
264
298
 
265
- | Analyzer | Severity | What it detects |
266
- |---|---|---|
267
- | Missing Index | Medium/High | Columns filtered without indexes |
268
- | N+1 Query | Medium | Repeated query patterns from ORM inefficiencies |
269
- | Large SELECT | Low | `SELECT *` usage |
299
+ The following analyzers work purely from infrastructure metadata (AWS APIs, database schema introspection, IaC files). They have no dependency on your application code or language:
270
300
 
271
- ### MySQL
301
+ | Service | What it checks |
302
+ |---|---|
303
+ | DynamoDB schema | Tables, GSIs, partition keys |
304
+ | PostgreSQL / MySQL schema | Tables, indexes, column types |
305
+ | MongoDB schema | Collections, indexes |
306
+ | SQS | Missing DLQs, unencrypted queues, large backlogs |
307
+ | Secrets Manager | Missing secret rotation |
308
+ | Lambda | Default memory (128 MB), high timeouts |
309
+ | RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
310
+ | CloudWatch Logs | Log groups with no retention policy |
311
+ | Terraform / CloudFormation / CDK | IaC drift vs deployed state |
272
312
 
273
- | Analyzer | Severity | What it detects |
274
- |---|---|---|
275
- | Missing MySQL Index | Medium | Tables queried without indexes |
276
- | MySQL Full Table Scan | High | Scan operations on MySQL tables |
313
+ ### Code-level analysis TypeScript and JavaScript only
277
314
 
278
- ### MongoDB
315
+ The repository scanner uses [ts-morph](https://ts-morph.com/) for static AST analysis. It detects which functions call which tables and how, enabling pattern detection that requires correlating code with infrastructure:
279
316
 
280
317
  | Analyzer | Severity | What it detects |
281
318
  |---|---|---|
319
+ | Full Table Scan (DynamoDB) | High | `.scan()` calls without filters |
320
+ | Missing GSI | Medium | Queries on attributes without a matching GSI |
321
+ | Hot Partition | Medium | 5+ distinct code paths hitting the same table |
322
+ | Missing Index (PostgreSQL) | Medium | Tables queried without indexes |
323
+ | N+1 Query | Medium | Repeated query patterns from ORM loops |
324
+ | Large SELECT | Low | `SELECT *` usage |
325
+ | Missing MySQL Index | Medium | MySQL tables queried without indexes |
326
+ | MySQL Full Table Scan | High | Full table scan patterns in MySQL queries |
282
327
  | Missing Mongo Index | Medium | Collections queried without secondary indexes |
283
- | Collection Scan | High | Full collection scan operations |
328
+ | Collection Scan | High | `find()` calls without filter predicates |
284
329
 
285
- ### Terraform / CloudFormation (IaC Drift)
330
+ **Non-TypeScript/JavaScript projects** still get full value from all infrastructure-level analyzers. The code correlation layer (which functions hit which tables, N+1 patterns) is skipped — run `infrawise analyze` and it will report 0 code operations while still surfacing all schema, configuration, and IaC findings.
286
331
 
287
- | Analyzer | Severity | What it detects |
288
- |---|---|---|
289
- | IaC Drift | Medium | DynamoDB tables defined in IaC but not deployed in AWS |
290
- | IaC Drift | Medium | DynamoDB tables deployed in AWS but not defined in IaC |
332
+ The scanner detects these patterns in TypeScript and JavaScript files:
333
+
334
+ - **DynamoDB** AWS SDK v3 (`client.send(new QueryCommand(...))`) and v2-style (`dynamoDb.scan(...)`)
335
+ - **PostgreSQL** `pg` pool/client queries, Prisma, Knex
336
+ - **MySQL** — `mysql2` connection/pool queries, Knex with MySQL dialect
337
+ - **MongoDB** — driver `collection.find/findOne/aggregate`, Mongoose models
338
+ - **SQS / SNS / SSM / Secrets / Lambda** — AWS SDK v3 command pattern
291
339
 
292
340
  ---
293
341
 
@@ -303,46 +351,46 @@ For Amazon RDS: allow inbound on port 5432 from your machine's IP in the securit
303
351
  ## Architecture
304
352
 
305
353
  ```
306
- Your TypeScript repo
307
-
308
- ┌───────────────────────────────────────────────────────┐
354
+ Your repo (any language) Your repo (TS/JS only)
355
+ │ │
356
+ │ Repository Scanner (ts-morph AST)
357
+ │ which functions → which tables
358
+ │ │
359
+ ┌───────┴──────────────────────────────────┴────────────┐
309
360
  │ infrawise analyze │
310
361
  │ │
311
- Repository Scanner AWS DynamoDB PostgreSQL
312
- (ts-morph AST) ↓ ↓
313
- ↓ └──────┬───────┘
314
- └────────────────────►
315
- Graph Engine
316
- (nodes + edges)
317
-
318
- Analyzer Engine
319
- │ (rule-based, deterministic) │
320
- └──────────────────────────────┬────────────────────────┘
321
-
322
- ┌──────────────────┐
323
- MCP Server │ ◄── Claude Code
324
- │ localhost:3000 │ ◄── Cursor
325
- └──────────────────┘ ◄── Windsurf
362
+ │ AWS APIs / DB schema / IaC files + Code ops (opt)
363
+ (works for any project) (TS/JS only)
364
+
365
+ Graph Engine
366
+ (nodes + edges)
367
+
368
+ Analyzer Engine
369
+ (rule-based, deterministic)
370
+ └─────────────────────────┬─────────────────────────────┘
371
+
372
+ ┌──────────────────┐
373
+ │ MCP Server │ ◄── Claude Code
374
+ localhost:3000 │ ◄── Cursor
375
+ └──────────────────┘ ◄── Windsurf
326
376
  ```
327
377
 
328
378
  The analysis is entirely deterministic — no LLM is involved in extracting or analyzing your infrastructure. AI is only at the consumption layer.
329
379
 
330
- ### Package structure
380
+ ### Source layout
331
381
 
332
- | Package | Description |
333
- |---|---|
334
- | `@infrawise/shared` | Shared TypeScript types |
335
- | `@infrawise/core` | Config (Zod + YAML), logger (Pino), local cache |
336
- | `@infrawise/graph` | Graph engine — nodes, edges, builder |
337
- | `@infrawise/adapters-dynamodb` | DynamoDB extractor (AWS SDK v3) |
338
- | `@infrawise/adapters-postgres` | PostgreSQL extractor (pg) |
339
- | `@infrawise/adapters-mysql` | MySQL extractor (mysql2) |
340
- | `@infrawise/adapters-mongodb` | MongoDB extractor (mongodb driver) |
341
- | `@infrawise/adapters-terraform` | Terraform and CloudFormation IaC schema extractor |
342
- | `@infrawise/context` | Repository scanner (ts-morph AST) |
343
- | `@infrawise/analyzers` | 11 rule-based analyzers |
344
- | `@infrawise/server` | Fastify MCP HTTP server |
345
- | `infrawise` | CLI (Commander.js) |
382
+ ```
383
+ src/
384
+ types.ts Shared type definitions
385
+ core/ Config (Zod + YAML), logger (Pino), local cache
386
+ graph/ Graph engine — nodes, edges, builder
387
+ adapters/ Flat extractors: dynamodb.ts, postgres.ts, mysql.ts,
388
+ mongodb.ts, aws.ts, logs.ts, terraform.ts
389
+ analyzers/ 23 rule-based analyzers
390
+ context/ Repository scanner (ts-morph AST)
391
+ server/ Fastify MCP HTTP server (plain JSON-RPC, no SDK)
392
+ cli/ CLI commands (Commander.js)
393
+ ```
346
394
 
347
395
  ---
348
396
 
@@ -391,14 +439,16 @@ pnpm typecheck # TypeScript strict check
391
439
  pnpm lint # ESLint
392
440
  ```
393
441
 
394
- Tests live in:
395
- - `packages/core/src/__tests__/` — config validation
396
- - `packages/graph/src/__tests__/` — graph builder
397
- - `packages/analyzers/src/__tests__/` — all 6 analyzers
442
+ Tests live in `src/**/__tests__/`:
443
+ - `src/core/__tests__/` — config validation, cache
444
+ - `src/graph/__tests__/` — graph builder
445
+ - `src/analyzers/__tests__/` — all analyzers
446
+ - `src/server/__tests__/` — MCP server (Fastify inject, all 13 tools)
447
+ - `src/context/__tests__/` — repository scanner
398
448
 
399
449
  ### Adding a new analyzer
400
450
 
401
- 1. Create your analyzer in `packages/analyzers/src/`
451
+ 1. Create your analyzer in `src/analyzers/`
402
452
  2. Implement the `Analyzer` interface:
403
453
  ```ts
404
454
  export class MyAnalyzer implements Analyzer {
@@ -406,20 +456,42 @@ export class MyAnalyzer implements Analyzer {
406
456
  async analyze(graph: SystemGraph): Promise<Finding[]> { ... }
407
457
  }
408
458
  ```
409
- 3. Register it in `packages/analyzers/src/index.ts`
410
- 4. Add tests in `packages/analyzers/src/__tests__/`
459
+ 3. Export it from `src/analyzers/index.ts`
460
+ 4. Add tests in `src/analyzers/__tests__/`
411
461
 
412
462
  ### Adding a new database adapter
413
463
 
414
- 1. Create a new package under `packages/adapters/yourdb/`
464
+ 1. Create your extractor as `src/adapters/yourdb.ts`
415
465
  2. Export a function returning `Promise<YourTableMetadata[]>`
416
- 3. Extend `SystemGraph` node types in `@infrawise/shared` if needed
417
- 4. Wire it into `packages/cli/src/commands/analyze.ts`
466
+ 3. Add the metadata type to `src/types.ts` if needed
467
+ 4. Wire it into `src/cli/commands/analyze.ts`
468
+
469
+ ### Releasing
470
+
471
+ The git tag is the source of truth. The version in root `package.json` and the tag must always match — the release script does both atomically.
472
+
473
+ ```bash
474
+ pnpm release patch # 0.1.2 → 0.1.3 (bug fixes)
475
+ pnpm release minor # 0.1.2 → 0.2.0 (new features, backwards compatible)
476
+ pnpm release major # 0.1.2 → 1.0.0 (breaking changes)
477
+ pnpm release 1.5.0 # explicit version
478
+
479
+ git push origin main --tags
480
+ ```
481
+
482
+ **What happens after push:**
483
+
484
+ 1. `release.yml` fires on the `v*.*.*` tag → creates a **draft** GitHub release with auto-generated notes
485
+ 2. Review the draft on GitHub → click **Publish release**
486
+ 3. `npm-publish.yml` fires on publish → reads the version from the tag, stamps it onto `package.json`, builds, and publishes to npm
487
+
488
+ The CLI reads its version from root `package.json` at runtime, so `infrawise --version` always matches the installed package.
418
489
 
419
490
  ### PR checklist
420
491
 
421
- - `pnpm test` passes
492
+ - `pnpm lint` passes (no errors)
422
493
  - `pnpm typecheck` passes
494
+ - `pnpm test` passes
423
495
  - New analyzers have unit tests with mock graph data
424
496
  - No hardcoded AWS regions or credentials
425
497
 
@@ -0,0 +1,255 @@
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");
23
+ function clientConfig(cfg) {
24
+ const region = cfg.region ?? 'us-east-1';
25
+ return cfg.profile
26
+ ? { region, credentials: (0, credential_providers_1.fromIni)({ profile: cfg.profile }) }
27
+ : { region };
28
+ }
29
+ // ─── SQS ─────────────────────────────────────────────────────────────────────
30
+ async function extractSQSMetadata(cfg = {}) {
31
+ const client = new client_sqs_1.SQSClient(clientConfig(cfg));
32
+ const queues = [];
33
+ try {
34
+ let nextToken;
35
+ const queueUrls = [];
36
+ do {
37
+ const res = await client.send(new client_sqs_1.ListQueuesCommand({ NextToken: nextToken, MaxResults: 1000 }));
38
+ queueUrls.push(...(res.QueueUrls ?? []));
39
+ nextToken = res.NextToken;
40
+ } while (nextToken);
41
+ for (const url of queueUrls) {
42
+ try {
43
+ const attrs = await client.send(new client_sqs_1.GetQueueAttributesCommand({
44
+ QueueUrl: url,
45
+ AttributeNames: [
46
+ 'QueueArn', 'VisibilityTimeout', 'MessageRetentionPeriod',
47
+ 'RedrivePolicy', 'KmsMasterKeyId', 'SqsManagedSseEnabled',
48
+ 'ApproximateNumberOfMessages', 'ApproximateNumberOfMessagesNotVisible',
49
+ ],
50
+ }));
51
+ const a = attrs.Attributes ?? {};
52
+ const arn = a['QueueArn'] ?? '';
53
+ const name = arn.split(':').pop() ?? url.split('/').pop() ?? url;
54
+ const redrivePolicy = a['RedrivePolicy'];
55
+ const dlqArn = redrivePolicy
56
+ ? JSON.parse(redrivePolicy).deadLetterTargetArn
57
+ : undefined;
58
+ const encrypted = !!(a['KmsMasterKeyId'] || a['SqsManagedSseEnabled'] === 'true');
59
+ const retentionSeconds = parseInt(a['MessageRetentionPeriod'] ?? '345600', 10);
60
+ queues.push({
61
+ name,
62
+ url,
63
+ arn,
64
+ hasDLQ: !!dlqArn,
65
+ dlqArn,
66
+ encrypted,
67
+ visibilityTimeoutSec: parseInt(a['VisibilityTimeout'] ?? '30', 10),
68
+ retentionDays: Math.round(retentionSeconds / 86400),
69
+ approximateMessages: parseInt(a['ApproximateNumberOfMessages'] ?? '0', 10),
70
+ approximateInflight: parseInt(a['ApproximateNumberOfMessagesNotVisible'] ?? '0', 10),
71
+ });
72
+ }
73
+ catch (err) {
74
+ core_1.logger.warn(`SQS attrs failed for ${url}: ${err instanceof Error ? err.message : String(err)}`);
75
+ }
76
+ }
77
+ }
78
+ catch (err) {
79
+ core_1.logger.warn(`SQS list failed: ${err instanceof Error ? err.message : String(err)}`);
80
+ }
81
+ return queues;
82
+ }
83
+ async function validateSQSAccess(cfg = {}) {
84
+ await new client_sqs_1.SQSClient(clientConfig(cfg)).send(new client_sqs_1.ListQueuesCommand({ MaxResults: 1 }));
85
+ }
86
+ // ─── SNS ─────────────────────────────────────────────────────────────────────
87
+ async function extractSNSMetadata(cfg = {}) {
88
+ const client = new client_sns_1.SNSClient(clientConfig(cfg));
89
+ const topics = [];
90
+ try {
91
+ let nextToken;
92
+ const topicArns = [];
93
+ do {
94
+ const res = await client.send(new client_sns_1.ListTopicsCommand({ NextToken: nextToken }));
95
+ topicArns.push(...(res.Topics ?? []).map((t) => t.TopicArn ?? '').filter(Boolean));
96
+ nextToken = res.NextToken;
97
+ } while (nextToken);
98
+ for (const arn of topicArns) {
99
+ try {
100
+ const [attrsRes, subsRes] = await Promise.all([
101
+ client.send(new client_sns_1.GetTopicAttributesCommand({ TopicArn: arn })),
102
+ client.send(new client_sns_1.ListSubscriptionsByTopicCommand({ TopicArn: arn })),
103
+ ]);
104
+ const attrs = attrsRes.Attributes ?? {};
105
+ const subs = subsRes.Subscriptions ?? [];
106
+ topics.push({
107
+ name: arn.split(':').pop() ?? arn,
108
+ arn,
109
+ encrypted: !!attrs['KmsMasterKeyId'],
110
+ subscriptionCount: subs.length,
111
+ subscriptionProtocols: [...new Set(subs.map((s) => s.Protocol ?? 'unknown'))],
112
+ });
113
+ }
114
+ catch (err) {
115
+ core_1.logger.warn(`SNS attrs failed for ${arn}: ${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ }
118
+ }
119
+ catch (err) {
120
+ core_1.logger.warn(`SNS list failed: ${err instanceof Error ? err.message : String(err)}`);
121
+ }
122
+ return topics;
123
+ }
124
+ async function validateSNSAccess(cfg = {}) {
125
+ await new client_sns_1.SNSClient(clientConfig(cfg)).send(new client_sns_1.ListTopicsCommand({}));
126
+ }
127
+ // ─── SSM Parameter Store ──────────────────────────────────────────────────────
128
+ async function extractSSMMetadata(cfg = {}) {
129
+ const client = new client_ssm_1.SSMClient(clientConfig(cfg));
130
+ const parameters = [];
131
+ try {
132
+ let nextToken;
133
+ do {
134
+ const res = await client.send(new client_ssm_1.DescribeParametersCommand({
135
+ NextToken: nextToken,
136
+ MaxResults: 50,
137
+ // Only metadata — GetParameter/GetParameters would return values
138
+ }));
139
+ for (const p of res.Parameters ?? []) {
140
+ parameters.push({
141
+ name: p.Name ?? '',
142
+ type: p.Type ?? 'String',
143
+ tier: p.Tier ?? 'Standard',
144
+ lastModified: p.LastModifiedDate?.toISOString(),
145
+ description: p.Description,
146
+ keyId: p.KeyId,
147
+ });
148
+ }
149
+ nextToken = res.NextToken;
150
+ } while (nextToken && parameters.length < 500);
151
+ }
152
+ catch (err) {
153
+ core_1.logger.warn(`SSM list failed: ${err instanceof Error ? err.message : String(err)}`);
154
+ }
155
+ return parameters;
156
+ }
157
+ async function validateSSMAccess(cfg = {}) {
158
+ await new client_ssm_1.SSMClient(clientConfig(cfg)).send(new client_ssm_1.DescribeParametersCommand({ MaxResults: 1 }));
159
+ }
160
+ // ─── Secrets Manager ──────────────────────────────────────────────────────────
161
+ async function extractSecretsMetadata(cfg = {}) {
162
+ const client = new client_secrets_manager_1.SecretsManagerClient(clientConfig(cfg));
163
+ const secrets = [];
164
+ try {
165
+ let nextToken;
166
+ do {
167
+ // ListSecrets never returns secret values
168
+ const res = await client.send(new client_secrets_manager_1.ListSecretsCommand({ NextToken: nextToken, MaxResults: 100 }));
169
+ for (const s of res.SecretList ?? []) {
170
+ secrets.push({
171
+ name: s.Name ?? '',
172
+ arn: s.ARN ?? '',
173
+ rotationEnabled: s.RotationEnabled ?? false,
174
+ rotationDays: s.RotationRules?.AutomaticallyAfterDays,
175
+ lastRotated: s.LastRotatedDate?.toISOString(),
176
+ lastAccessed: s.LastAccessedDate?.toISOString(),
177
+ description: s.Description,
178
+ });
179
+ }
180
+ nextToken = res.NextToken;
181
+ } while (nextToken && secrets.length < 200);
182
+ }
183
+ catch (err) {
184
+ core_1.logger.warn(`Secrets Manager list failed: ${err instanceof Error ? err.message : String(err)}`);
185
+ }
186
+ return secrets;
187
+ }
188
+ async function validateSecretsAccess(cfg = {}) {
189
+ await new client_secrets_manager_1.SecretsManagerClient(clientConfig(cfg)).send(new client_secrets_manager_1.ListSecretsCommand({ MaxResults: 1 }));
190
+ }
191
+ // ─── Lambda ───────────────────────────────────────────────────────────────────
192
+ async function extractLambdaMetadata(cfg = {}) {
193
+ const client = new client_lambda_1.LambdaClient(clientConfig(cfg));
194
+ const functions = [];
195
+ try {
196
+ let marker;
197
+ do {
198
+ const res = await client.send(new client_lambda_1.ListFunctionsCommand({ Marker: marker, MaxItems: 50 }));
199
+ for (const fn of res.Functions ?? []) {
200
+ functions.push({
201
+ name: fn.FunctionName ?? '',
202
+ arn: fn.FunctionArn ?? '',
203
+ runtime: fn.Runtime,
204
+ handler: fn.Handler,
205
+ memoryMB: fn.MemorySize,
206
+ timeoutSec: fn.Timeout,
207
+ lastModified: fn.LastModified,
208
+ envVarKeys: Object.keys(fn.Environment?.Variables ?? {}),
209
+ layers: (fn.Layers ?? []).map((l) => l.Arn ?? '').filter(Boolean),
210
+ });
211
+ }
212
+ marker = res.NextMarker;
213
+ } while (marker && functions.length < 200);
214
+ }
215
+ catch (err) {
216
+ core_1.logger.warn(`Lambda list failed: ${err instanceof Error ? err.message : String(err)}`);
217
+ }
218
+ return functions;
219
+ }
220
+ async function validateLambdaAccess(cfg = {}) {
221
+ await new client_lambda_1.LambdaClient(clientConfig(cfg)).send(new client_lambda_1.ListFunctionsCommand({ MaxItems: 1 }));
222
+ }
223
+ // ─── RDS ─────────────────────────────────────────────────────────────────────
224
+ async function extractRDSMetadata(cfg = {}) {
225
+ const client = new client_rds_1.RDSClient(clientConfig(cfg));
226
+ const instances = [];
227
+ try {
228
+ let marker;
229
+ do {
230
+ const res = await client.send(new client_rds_1.DescribeDBInstancesCommand({ Marker: marker, MaxRecords: 100 }));
231
+ for (const db of res.DBInstances ?? []) {
232
+ instances.push({
233
+ dbInstanceIdentifier: db.DBInstanceIdentifier ?? '',
234
+ engine: db.Engine ?? '',
235
+ engineVersion: db.EngineVersion ?? '',
236
+ instanceClass: db.DBInstanceClass ?? '',
237
+ publiclyAccessible: db.PubliclyAccessible ?? false,
238
+ storageEncrypted: db.StorageEncrypted ?? false,
239
+ backupRetentionPeriod: db.BackupRetentionPeriod ?? 0,
240
+ deletionProtection: db.DeletionProtection ?? false,
241
+ multiAZ: db.MultiAZ ?? false,
242
+ dbInstanceStatus: db.DBInstanceStatus ?? '',
243
+ });
244
+ }
245
+ marker = res.Marker;
246
+ } while (marker && instances.length < 200);
247
+ }
248
+ catch (err) {
249
+ core_1.logger.warn(`RDS list failed: ${err instanceof Error ? err.message : String(err)}`);
250
+ }
251
+ return instances;
252
+ }
253
+ async function validateRDSAccess(cfg = {}) {
254
+ await new client_rds_1.RDSClient(clientConfig(cfg)).send(new client_rds_1.DescribeDBInstancesCommand({ MaxRecords: 20 }));
255
+ }