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 +21 -0
- package/README.md +146 -74
- 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 +348 -0
- package/dist/types.js +2 -0
- package/package.json +20 -30
- package/dist/index.js +0 -4800
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
|
|
145
|
+
Claude gains 13 tools it calls silently while helping you:
|
|
146
146
|
|
|
147
147
|
| Tool | What it gives Claude |
|
|
148
148
|
|---|---|
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
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 |
|
|
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
|
-
##
|
|
293
|
+
## Language support
|
|
254
294
|
|
|
255
|
-
|
|
295
|
+
Infrawise has two analysis layers with different language requirements:
|
|
256
296
|
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
328
|
+
| Collection Scan | High | `find()` calls without filter predicates |
|
|
284
329
|
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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
|
-
│
|
|
312
|
-
│
|
|
313
|
-
│
|
|
314
|
-
│
|
|
315
|
-
│
|
|
316
|
-
│
|
|
317
|
-
│
|
|
318
|
-
│
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
###
|
|
380
|
+
### Source layout
|
|
331
381
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
- `
|
|
396
|
-
- `
|
|
397
|
-
- `
|
|
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 `
|
|
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.
|
|
410
|
-
4. Add tests in `
|
|
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
|
|
464
|
+
1. Create your extractor as `src/adapters/yourdb.ts`
|
|
415
465
|
2. Export a function returning `Promise<YourTableMetadata[]>`
|
|
416
|
-
3.
|
|
417
|
-
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.
|
|
418
489
|
|
|
419
490
|
### PR checklist
|
|
420
491
|
|
|
421
|
-
- `pnpm
|
|
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
|
+
}
|