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 +21 -0
- package/README.md +152 -75
- package/dist/adapters/aws.js +255 -0
- package/dist/adapters/dynamodb.js +97 -0
- package/dist/adapters/logs.js +119 -0
- package/dist/adapters/mongodb.js +109 -0
- package/dist/adapters/mysql.js +135 -0
- package/dist/adapters/postgres.js +131 -0
- package/dist/adapters/terraform.js +510 -0
- package/dist/analyzers/aws-services.js +168 -0
- package/dist/analyzers/dynamodb.js +144 -0
- package/dist/analyzers/index.js +59 -0
- package/dist/analyzers/mongodb.js +82 -0
- package/dist/analyzers/mysql.js +82 -0
- package/dist/analyzers/postgres.js +148 -0
- package/dist/analyzers/rds.js +109 -0
- package/dist/analyzers/terraform.js +95 -0
- package/dist/cli/commands/analyze.js +319 -0
- package/dist/cli/commands/auth.js +57 -0
- package/dist/cli/commands/dev.js +127 -0
- package/dist/cli/commands/doctor.js +338 -0
- package/dist/cli/commands/init.js +234 -0
- package/dist/cli/index.js +82 -0
- package/dist/cli/utils.js +165 -0
- package/dist/context/index.js +597 -0
- package/dist/core/cache.js +89 -0
- package/dist/core/config.js +163 -0
- package/dist/core/errors.js +97 -0
- package/dist/core/index.js +22 -0
- package/dist/core/logger.js +28 -0
- package/dist/graph/index.js +268 -0
- package/dist/server/index.js +347 -0
- package/dist/types.js +2 -0
- package/package.json +31 -26
- package/dist/index.js +0 -3462
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
|
+
[](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)
|
|
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
|
|
145
|
+
Claude gains 13 tools it calls silently while helping you:
|
|
141
146
|
|
|
142
147
|
| Tool | What it gives Claude |
|
|
143
148
|
|---|---|
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
149
|
+
| `get_infra_overview` | Complete snapshot — all 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
|
-
##
|
|
293
|
+
## Language support
|
|
249
294
|
|
|
250
|
-
|
|
295
|
+
Infrawise has two analysis layers with different language requirements:
|
|
251
296
|
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
328
|
+
| Collection Scan | High | `find()` calls without filter predicates |
|
|
279
329
|
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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
|
-
│
|
|
307
|
-
│
|
|
308
|
-
│
|
|
309
|
-
│
|
|
310
|
-
│
|
|
311
|
-
│
|
|
312
|
-
│
|
|
313
|
-
│
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
###
|
|
380
|
+
### Source layout
|
|
326
381
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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 |
|
|
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
|
-
- `
|
|
391
|
-
- `
|
|
392
|
-
- `
|
|
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 `
|
|
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.
|
|
405
|
-
4. Add tests in `
|
|
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
|
|
464
|
+
1. Create your extractor as `src/adapters/yourdb.ts`
|
|
410
465
|
2. Export a function returning `Promise<YourTableMetadata[]>`
|
|
411
|
-
3.
|
|
412
|
-
4. Wire it into `
|
|
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
|
|
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
|
+
}
|