infrawise 0.1.0 → 0.2.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/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
@@ -1,5 +1,10 @@
1
1
  # Infrawise
2
2
 
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)
7
+
3
8
  **Understand your infrastructure, not just your code.**
4
9
 
5
10
  Infrawise is a CLI tool that scans your TypeScript codebase, maps every function-to-database relationship into a graph, detects anti-patterns (full table scans, missing indexes, hot partitions, N+1 queries), and exposes all of it as an MCP server — so AI coding assistants like Claude Code have live, deterministic knowledge of your infrastructure when helping you write database code.
@@ -137,16 +142,23 @@ Alternatively, let Claude Code manage the server lifecycle automatically:
137
142
 
138
143
  ### Step 3: What Claude can now do
139
144
 
140
- Claude gains four tools it calls silently while helping you:
145
+ Claude gains 13 tools it calls silently while helping you:
141
146
 
142
147
  | Tool | What it gives Claude |
143
148
  |---|---|
144
- | `get_graph_summary` | Full infrastructure graph tables, GSIs, function relationships, all findings |
145
- | `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 |
146
152
  | `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
147
153
  | `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
148
154
  | `suggest_mongo_index` | Exact `createIndex` command for a MongoDB collection + field |
149
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) |
150
162
 
151
163
  ### What changes in practice
152
164
 
@@ -182,7 +194,7 @@ Infrawise works with any editor or tool that supports MCP or can call an HTTP AP
182
194
 
183
195
  ## Configuration
184
196
 
185
- `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.
186
198
 
187
199
  ```yaml
188
200
  project: payments-service
@@ -192,6 +204,7 @@ aws:
192
204
  region: ap-south-1
193
205
 
194
206
  dynamodb:
207
+ enabled: true
195
208
  includeTables: # omit to include all tables
196
209
  - Orders
197
210
  - Users
@@ -200,6 +213,38 @@ postgres:
200
213
  enabled: true
201
214
  connectionString: postgresql://infrawise_ro:password@host:5432/mydb
202
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
+
203
248
  analysis:
204
249
  sampleSize: 100
205
250
  ```
@@ -245,44 +290,52 @@ For Amazon RDS: allow inbound on port 5432 from your machine's IP in the securit
245
290
 
246
291
  ---
247
292
 
248
- ## What gets analyzed
293
+ ## Language support
249
294
 
250
- ### DynamoDB
295
+ Infrawise has two analysis layers with different language requirements:
251
296
 
252
- | Analyzer | Severity | What it detects |
253
- |---|---|---|
254
- | Full Table Scan | High | `.scan()` calls without filters |
255
- | Missing GSI | Medium | Tables queried without a matching GSI |
256
- | Hot Partition | Medium | 5+ distinct code paths hitting the same table |
257
-
258
- ### PostgreSQL
297
+ ### Infrastructure-level analysis any project, any language
259
298
 
260
- | Analyzer | Severity | What it detects |
261
- |---|---|---|
262
- | Missing Index | Medium/High | Columns filtered without indexes |
263
- | N+1 Query | Medium | Repeated query patterns from ORM inefficiencies |
264
- | 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:
265
300
 
266
- ### 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 |
267
312
 
268
- | Analyzer | Severity | What it detects |
269
- |---|---|---|
270
- | Missing MySQL Index | Medium | Tables queried without indexes |
271
- | MySQL Full Table Scan | High | Scan operations on MySQL tables |
313
+ ### Code-level analysis TypeScript and JavaScript only
272
314
 
273
- ### 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:
274
316
 
275
317
  | Analyzer | Severity | What it detects |
276
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 |
277
327
  | Missing Mongo Index | Medium | Collections queried without secondary indexes |
278
- | Collection Scan | High | Full collection scan operations |
328
+ | Collection Scan | High | `find()` calls without filter predicates |
279
329
 
280
- ### 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.
281
331
 
282
- | Analyzer | Severity | What it detects |
283
- |---|---|---|
284
- | IaC Drift | Medium | DynamoDB tables defined in IaC but not deployed in AWS |
285
- | 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
286
339
 
287
340
  ---
288
341
 
@@ -298,46 +351,46 @@ For Amazon RDS: allow inbound on port 5432 from your machine's IP in the securit
298
351
  ## Architecture
299
352
 
300
353
  ```
301
- Your TypeScript repo
302
-
303
- ┌───────────────────────────────────────────────────────┐
354
+ Your repo (any language) Your repo (TS/JS only)
355
+ │ │
356
+ │ Repository Scanner (ts-morph AST)
357
+ │ which functions → which tables
358
+ │ │
359
+ ┌───────┴──────────────────────────────────┴────────────┐
304
360
  │ infrawise analyze │
305
361
  │ │
306
- Repository Scanner AWS DynamoDB PostgreSQL
307
- (ts-morph AST) ↓ ↓
308
- ↓ └──────┬───────┘
309
- └────────────────────►
310
- Graph Engine
311
- (nodes + edges)
312
-
313
- Analyzer Engine
314
- │ (rule-based, deterministic) │
315
- └──────────────────────────────┬────────────────────────┘
316
-
317
- ┌──────────────────┐
318
- MCP Server │ ◄── Claude Code
319
- │ localhost:3000 │ ◄── Cursor
320
- └──────────────────┘ ◄── 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
321
376
  ```
322
377
 
323
378
  The analysis is entirely deterministic — no LLM is involved in extracting or analyzing your infrastructure. AI is only at the consumption layer.
324
379
 
325
- ### Package structure
380
+ ### Source layout
326
381
 
327
- | Package | Description |
328
- |---|---|
329
- | `@infrawise/shared` | Shared TypeScript types |
330
- | `@infrawise/core` | Config (Zod + YAML), logger (Pino), local cache |
331
- | `@infrawise/graph` | Graph engine — nodes, edges, builder |
332
- | `@infrawise/adapters-dynamodb` | DynamoDB extractor (AWS SDK v3) |
333
- | `@infrawise/adapters-postgres` | PostgreSQL extractor (pg) |
334
- | `@infrawise/adapters-mysql` | MySQL extractor (mysql2) |
335
- | `@infrawise/adapters-mongodb` | MongoDB extractor (mongodb driver) |
336
- | `@infrawise/adapters-terraform` | Terraform and CloudFormation IaC schema extractor |
337
- | `@infrawise/context` | Repository scanner (ts-morph AST) |
338
- | `@infrawise/analyzers` | 11 rule-based analyzers |
339
- | `@infrawise/server` | Fastify MCP HTTP server |
340
- | `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
+ ```
341
394
 
342
395
  ---
343
396
 
@@ -359,7 +412,7 @@ The analysis is entirely deterministic — no LLM is involved in extracting or a
359
412
 
360
413
  | Requirement | Version |
361
414
  |---|---|
362
- | Node.js | 22+ |
415
+ | Node.js | 24+ |
363
416
  | pnpm | 9+ |
364
417
  | AWS CLI | any (for integration testing) |
365
418
 
@@ -386,14 +439,16 @@ pnpm typecheck # TypeScript strict check
386
439
  pnpm lint # ESLint
387
440
  ```
388
441
 
389
- Tests live in:
390
- - `packages/core/src/__tests__/` — config validation
391
- - `packages/graph/src/__tests__/` — graph builder
392
- - `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
393
448
 
394
449
  ### Adding a new analyzer
395
450
 
396
- 1. Create your analyzer in `packages/analyzers/src/`
451
+ 1. Create your analyzer in `src/analyzers/`
397
452
  2. Implement the `Analyzer` interface:
398
453
  ```ts
399
454
  export class MyAnalyzer implements Analyzer {
@@ -401,20 +456,42 @@ export class MyAnalyzer implements Analyzer {
401
456
  async analyze(graph: SystemGraph): Promise<Finding[]> { ... }
402
457
  }
403
458
  ```
404
- 3. Register it in `packages/analyzers/src/index.ts`
405
- 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__/`
406
461
 
407
462
  ### Adding a new database adapter
408
463
 
409
- 1. Create a new package under `packages/adapters/yourdb/`
464
+ 1. Create your extractor as `src/adapters/yourdb.ts`
410
465
  2. Export a function returning `Promise<YourTableMetadata[]>`
411
- 3. Extend `SystemGraph` node types in `@infrawise/shared` if needed
412
- 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.
413
489
 
414
490
  ### PR checklist
415
491
 
416
- - `pnpm test` passes
492
+ - `pnpm lint` passes (no errors)
417
493
  - `pnpm typecheck` passes
494
+ - `pnpm test` passes
418
495
  - New analyzers have unit tests with mock graph data
419
496
  - No hardcoded AWS regions or credentials
420
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
+ }