infrawise 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/dist/adapters/aws/index.js +1 -0
- package/dist/adapters/aws/metrics.js +94 -0
- package/dist/adapters/aws/services.js +197 -1
- package/dist/adapters/iac/terraform.js +66 -1
- package/dist/analyzers/aws-services.js +87 -0
- package/dist/analyzers/index.js +1 -1
- package/dist/cli/commands/analyze.js +46 -4
- package/dist/cli/commands/discover.js +5 -0
- package/dist/cli/commands/serve.js +4 -0
- package/dist/core/config.js +18 -0
- package/dist/graph/index.js +81 -9
- package/dist/server/index.js +98 -3
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ Infrawise gives AI coding assistants deterministic infrastructure awareness.
|
|
|
12
12
|
|
|
13
13
|
It statically analyzes your codebase, cloud infrastructure, and database schemas, then exposes that context through MCP so tools like Claude Code can understand your actual tables, indexes, query patterns, and service relationships instead of guessing from source files alone.
|
|
14
14
|
|
|
15
|
+

|
|
16
|
+
|
|
15
17
|
---
|
|
16
18
|
|
|
17
19
|
## Why this exists
|
|
@@ -72,7 +74,7 @@ That's it. Infrawise will:
|
|
|
72
74
|
1. Probe your environment and generate `infrawise.yaml` (first time only — asks which AWS profile to use only if you have several)
|
|
73
75
|
2. Scan your AWS services, databases, and codebase
|
|
74
76
|
3. Write `.mcp.json` so your editor auto-connects on every future launch
|
|
75
|
-
4. Open Claude Code with all
|
|
77
|
+
4. Open Claude Code with all 20 MCP tools ready
|
|
76
78
|
|
|
77
79
|
**Every time after:**
|
|
78
80
|
|
|
@@ -115,7 +117,7 @@ Writes `.mcp.json` to your project root and opens Claude Code. Claude Code reads
|
|
|
115
117
|
infrawise start --cursor
|
|
116
118
|
```
|
|
117
119
|
|
|
118
|
-
Writes `.cursor/mcp.json` and opens Cursor. All
|
|
120
|
+
Writes `.cursor/mcp.json` and opens Cursor. All 20 infrawise tools are available in Cursor's MCP panel.
|
|
119
121
|
|
|
120
122
|
### Any editor (no flag)
|
|
121
123
|
|
|
@@ -165,6 +167,10 @@ Add to your editor's MCP config:
|
|
|
165
167
|
| `get_eventbridge_details` | EventBridge rules — name, state, schedule/event pattern, target functions |
|
|
166
168
|
| `get_s3_overview` | S3 buckets — versioning, encryption, public access, event notifications |
|
|
167
169
|
| `get_log_errors` | CloudWatch error patterns and counts (no raw log messages) |
|
|
170
|
+
| `get_stack_outputs` | Stack outputs and cross-stack exports parsed from local IaC files (Terraform outputs, CFN/CDK Outputs) |
|
|
171
|
+
| `get_cognito_overview` | Cognito user pools — MFA config, app client auth flows, OAuth settings, token validity (secrets never included) |
|
|
172
|
+
| `get_stream_details` | Kinesis streams (shards, retention, capacity mode) and MSK clusters (state, Kafka version, brokers) |
|
|
173
|
+
| `get_cache_overview` | ElastiCache clusters — engine, encryption in transit/at rest, replication group, failover (data never read) |
|
|
168
174
|
|
|
169
175
|
---
|
|
170
176
|
|
|
@@ -301,6 +307,22 @@ s3:
|
|
|
301
307
|
apiGateway:
|
|
302
308
|
enabled: false
|
|
303
309
|
|
|
310
|
+
cognito:
|
|
311
|
+
enabled: false
|
|
312
|
+
|
|
313
|
+
kinesis:
|
|
314
|
+
enabled: false
|
|
315
|
+
|
|
316
|
+
msk:
|
|
317
|
+
enabled: false
|
|
318
|
+
|
|
319
|
+
elasticache:
|
|
320
|
+
enabled: false
|
|
321
|
+
|
|
322
|
+
runtimeSignals:
|
|
323
|
+
enabled: false # Lambda throttles/errors + queue age via CloudWatch metrics
|
|
324
|
+
windowHours: 24
|
|
325
|
+
|
|
304
326
|
cloudwatchLogs:
|
|
305
327
|
enabled: false
|
|
306
328
|
logGroupPrefixes: []
|
|
@@ -374,7 +396,11 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
|
|
|
374
396
|
| API Gateway | REST, HTTP, and WebSocket APIs — routes, methods, Lambda integrations |
|
|
375
397
|
| RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
|
|
376
398
|
| CloudWatch Logs | Log groups with no retention policy |
|
|
377
|
-
|
|
|
399
|
+
| Cognito | User pools and app client config — auth flows, OAuth settings, token validity, client secret presence |
|
|
400
|
+
| Kinesis / MSK | Streams (shards, retention, capacity mode) and MSK clusters (state, Kafka version, brokers) |
|
|
401
|
+
| ElastiCache | Missing in-transit encryption, single-node clusters with no replication |
|
|
402
|
+
| Runtime signals (opt-in) | Lambda throttling/errors and stale queue messages from CloudWatch metrics |
|
|
403
|
+
| Terraform / CloudFormation / CDK | IaC drift vs deployed state; stack outputs and cross-stack exports |
|
|
378
404
|
|
|
379
405
|
### Code correlation analysis (TypeScript / JavaScript)
|
|
380
406
|
|
|
@@ -446,7 +472,7 @@ src/
|
|
|
446
472
|
aws/ DynamoDB, S3, Lambda, SQS/SNS/SSM/Secrets/EventBridge/RDS/APIGateway, CloudWatch
|
|
447
473
|
db/ PostgreSQL, MySQL, MongoDB
|
|
448
474
|
iac/ Terraform, CDK, CloudFormation (local file parsing)
|
|
449
|
-
analyzers/
|
|
475
|
+
analyzers/ 34 rule-based analyzers
|
|
450
476
|
context/ Repository scanner (ts-morph AST)
|
|
451
477
|
server/ Fastify MCP server (@modelcontextprotocol/sdk, Streamable HTTP)
|
|
452
478
|
cli/ CLI commands (Commander.js)
|
|
@@ -473,6 +499,8 @@ Feature roadmap is tracked in the [GitHub Project](https://github.com/users/Sidd
|
|
|
473
499
|
|
|
474
500
|
The `demo/localstack/` directory runs infrawise against real AWS APIs emulated locally via [LocalStack](https://localstack.cloud) — an open-source tool that spins up a full AWS environment in Docker so you can test AWS integrations at zero cost, with no real AWS account needed. See [`demo/localstack/README.md`](demo/localstack/README.md) for setup instructions.
|
|
475
501
|
|
|
502
|
+

|
|
503
|
+
|
|
476
504
|
---
|
|
477
505
|
|
|
478
506
|
## Contributing
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { CloudWatchClient, GetMetricDataCommand } from '@aws-sdk/client-cloudwatch';
|
|
2
|
+
import { clientConfig } from './services.js';
|
|
3
|
+
import { logger } from '../../core/index.js';
|
|
4
|
+
export async function extractRuntimeSignals(cfg, lambdaNames, queueNames, windowHours = 24) {
|
|
5
|
+
const lambdas = new Map();
|
|
6
|
+
const queues = new Map();
|
|
7
|
+
if (lambdaNames.length === 0 && queueNames.length === 0)
|
|
8
|
+
return { lambdas, queues };
|
|
9
|
+
const client = new CloudWatchClient(clientConfig(cfg));
|
|
10
|
+
const end = new Date();
|
|
11
|
+
const start = new Date(end.getTime() - windowHours * 3600 * 1000);
|
|
12
|
+
const period = windowHours * 3600;
|
|
13
|
+
// Metric names live in Dimensions; query Ids are positional because function/queue
|
|
14
|
+
// names may contain characters GetMetricData Ids do not allow.
|
|
15
|
+
const queries = [];
|
|
16
|
+
lambdaNames.forEach((name, i) => {
|
|
17
|
+
queries.push({
|
|
18
|
+
Id: `lt${i}`,
|
|
19
|
+
MetricStat: {
|
|
20
|
+
Metric: {
|
|
21
|
+
Namespace: 'AWS/Lambda',
|
|
22
|
+
MetricName: 'Throttles',
|
|
23
|
+
Dimensions: [{ Name: 'FunctionName', Value: name }],
|
|
24
|
+
},
|
|
25
|
+
Period: period,
|
|
26
|
+
Stat: 'Sum',
|
|
27
|
+
},
|
|
28
|
+
}, {
|
|
29
|
+
Id: `le${i}`,
|
|
30
|
+
MetricStat: {
|
|
31
|
+
Metric: {
|
|
32
|
+
Namespace: 'AWS/Lambda',
|
|
33
|
+
MetricName: 'Errors',
|
|
34
|
+
Dimensions: [{ Name: 'FunctionName', Value: name }],
|
|
35
|
+
},
|
|
36
|
+
Period: period,
|
|
37
|
+
Stat: 'Sum',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
queueNames.forEach((name, i) => {
|
|
42
|
+
queries.push({
|
|
43
|
+
Id: `qa${i}`,
|
|
44
|
+
MetricStat: {
|
|
45
|
+
Metric: {
|
|
46
|
+
Namespace: 'AWS/SQS',
|
|
47
|
+
MetricName: 'ApproximateAgeOfOldestMessage',
|
|
48
|
+
Dimensions: [{ Name: 'QueueName', Value: name }],
|
|
49
|
+
},
|
|
50
|
+
Period: period,
|
|
51
|
+
Stat: 'Maximum',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
for (let i = 0; i < queries.length; i += 500) {
|
|
57
|
+
const chunk = queries.slice(i, i + 500);
|
|
58
|
+
let nextToken;
|
|
59
|
+
do {
|
|
60
|
+
const res = await client.send(new GetMetricDataCommand({
|
|
61
|
+
StartTime: start,
|
|
62
|
+
EndTime: end,
|
|
63
|
+
MetricDataQueries: chunk,
|
|
64
|
+
NextToken: nextToken,
|
|
65
|
+
}));
|
|
66
|
+
for (const r of res.MetricDataResults ?? []) {
|
|
67
|
+
const id = r.Id ?? '';
|
|
68
|
+
const value = r.Values?.[0] ?? 0;
|
|
69
|
+
const idx = parseInt(id.slice(2), 10);
|
|
70
|
+
if (Number.isNaN(idx))
|
|
71
|
+
continue;
|
|
72
|
+
if (id.startsWith('lt')) {
|
|
73
|
+
const e = lambdas.get(lambdaNames[idx]) ?? { throttles: 0, errors: 0 };
|
|
74
|
+
e.throttles = value;
|
|
75
|
+
lambdas.set(lambdaNames[idx], e);
|
|
76
|
+
}
|
|
77
|
+
else if (id.startsWith('le')) {
|
|
78
|
+
const e = lambdas.get(lambdaNames[idx]) ?? { throttles: 0, errors: 0 };
|
|
79
|
+
e.errors = value;
|
|
80
|
+
lambdas.set(lambdaNames[idx], e);
|
|
81
|
+
}
|
|
82
|
+
else if (id.startsWith('qa')) {
|
|
83
|
+
queues.set(queueNames[idx], { oldestMessageAgeSec: value });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
nextToken = res.NextToken;
|
|
87
|
+
} while (nextToken);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
logger.warn(`Runtime signals fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
92
|
+
}
|
|
93
|
+
return { lambdas, queues };
|
|
94
|
+
}
|
|
@@ -7,10 +7,14 @@ import { SecretsManagerClient, ListSecretsCommand } from '@aws-sdk/client-secret
|
|
|
7
7
|
import { LambdaClient, ListFunctionsCommand, ListEventSourceMappingsCommand, } from '@aws-sdk/client-lambda';
|
|
8
8
|
import { EventBridgeClient, ListRulesCommand, ListTargetsByRuleCommand, } from '@aws-sdk/client-eventbridge';
|
|
9
9
|
import { RDSClient, DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
|
|
10
|
+
import { KinesisClient, ListStreamsCommand, DescribeStreamSummaryCommand, } from '@aws-sdk/client-kinesis';
|
|
11
|
+
import { KafkaClient, ListClustersV2Command } from '@aws-sdk/client-kafka';
|
|
12
|
+
import { ElastiCacheClient, DescribeCacheClustersCommand, DescribeReplicationGroupsCommand, } from '@aws-sdk/client-elasticache';
|
|
13
|
+
import { CognitoIdentityProviderClient, ListUserPoolsCommand, ListUserPoolClientsCommand, DescribeUserPoolClientCommand, DescribeUserPoolCommand, } from '@aws-sdk/client-cognito-identity-provider';
|
|
10
14
|
import { IAMClient, ListAttachedRolePoliciesCommand, GetPolicyCommand, GetPolicyVersionCommand, ListRolePoliciesCommand, GetRolePolicyCommand, } from '@aws-sdk/client-iam';
|
|
11
15
|
import { fromIni } from '@aws-sdk/credential-providers';
|
|
12
16
|
import { logger } from '../../core/index.js';
|
|
13
|
-
function clientConfig(cfg) {
|
|
17
|
+
export function clientConfig(cfg) {
|
|
14
18
|
const region = cfg.region ?? 'us-east-1';
|
|
15
19
|
const base = { region };
|
|
16
20
|
if (cfg.profile)
|
|
@@ -420,6 +424,198 @@ export async function extractLambdaMetadata(cfg = {}, includeFunctions) {
|
|
|
420
424
|
export async function validateLambdaAccess(cfg = {}) {
|
|
421
425
|
await new LambdaClient(clientConfig(cfg)).send(new ListFunctionsCommand({ MaxItems: 1 }));
|
|
422
426
|
}
|
|
427
|
+
// ─── Kinesis ─────────────────────────────────────────────────────────────────
|
|
428
|
+
export async function extractKinesisMetadata(cfg = {}) {
|
|
429
|
+
const client = new KinesisClient(clientConfig(cfg));
|
|
430
|
+
const streams = [];
|
|
431
|
+
try {
|
|
432
|
+
let nextToken;
|
|
433
|
+
const names = [];
|
|
434
|
+
do {
|
|
435
|
+
const res = await client.send(new ListStreamsCommand({ NextToken: nextToken }));
|
|
436
|
+
names.push(...(res.StreamNames ?? []));
|
|
437
|
+
nextToken = res.NextToken;
|
|
438
|
+
} while (nextToken);
|
|
439
|
+
for (const name of names) {
|
|
440
|
+
try {
|
|
441
|
+
const res = await client.send(new DescribeStreamSummaryCommand({ StreamName: name }));
|
|
442
|
+
const d = res.StreamDescriptionSummary;
|
|
443
|
+
if (!d)
|
|
444
|
+
continue;
|
|
445
|
+
streams.push({
|
|
446
|
+
name,
|
|
447
|
+
arn: d.StreamARN ?? '',
|
|
448
|
+
status: d.StreamStatus ?? 'UNKNOWN',
|
|
449
|
+
shardCount: d.OpenShardCount,
|
|
450
|
+
retentionHours: d.RetentionPeriodHours,
|
|
451
|
+
encrypted: d.EncryptionType === 'KMS',
|
|
452
|
+
mode: d.StreamModeDetails?.StreamMode ?? 'PROVISIONED',
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
logger.warn(`Kinesis describe failed for ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
logger.warn(`Kinesis list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
462
|
+
}
|
|
463
|
+
return streams;
|
|
464
|
+
}
|
|
465
|
+
// ─── MSK ─────────────────────────────────────────────────────────────────────
|
|
466
|
+
export async function extractMSKMetadata(cfg = {}) {
|
|
467
|
+
const client = new KafkaClient(clientConfig(cfg));
|
|
468
|
+
const clusters = [];
|
|
469
|
+
try {
|
|
470
|
+
let nextToken;
|
|
471
|
+
do {
|
|
472
|
+
const res = await client.send(new ListClustersV2Command({ NextToken: nextToken }));
|
|
473
|
+
for (const c of res.ClusterInfoList ?? []) {
|
|
474
|
+
if (!c.ClusterName)
|
|
475
|
+
continue;
|
|
476
|
+
clusters.push({
|
|
477
|
+
name: c.ClusterName,
|
|
478
|
+
arn: c.ClusterArn ?? '',
|
|
479
|
+
state: c.State ?? 'UNKNOWN',
|
|
480
|
+
clusterType: c.ClusterType ?? 'PROVISIONED',
|
|
481
|
+
kafkaVersion: c.Provisioned?.CurrentBrokerSoftwareInfo?.KafkaVersion,
|
|
482
|
+
brokerNodes: c.Provisioned?.NumberOfBrokerNodes,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
nextToken = res.NextToken;
|
|
486
|
+
} while (nextToken);
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
logger.warn(`MSK list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
490
|
+
}
|
|
491
|
+
return clusters;
|
|
492
|
+
}
|
|
493
|
+
// ─── ElastiCache ─────────────────────────────────────────────────────────────
|
|
494
|
+
export async function extractElastiCacheMetadata(cfg = {}) {
|
|
495
|
+
const client = new ElastiCacheClient(clientConfig(cfg));
|
|
496
|
+
const clusters = [];
|
|
497
|
+
try {
|
|
498
|
+
const failoverByGroup = new Map();
|
|
499
|
+
try {
|
|
500
|
+
let marker;
|
|
501
|
+
do {
|
|
502
|
+
const res = await client.send(new DescribeReplicationGroupsCommand({ Marker: marker }));
|
|
503
|
+
for (const g of res.ReplicationGroups ?? []) {
|
|
504
|
+
if (g.ReplicationGroupId) {
|
|
505
|
+
failoverByGroup.set(g.ReplicationGroupId, g.AutomaticFailover ?? 'disabled');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
marker = res.Marker;
|
|
509
|
+
} while (marker);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
/* replication groups are optional context */
|
|
513
|
+
}
|
|
514
|
+
let marker;
|
|
515
|
+
do {
|
|
516
|
+
const res = await client.send(new DescribeCacheClustersCommand({ Marker: marker }));
|
|
517
|
+
for (const c of res.CacheClusters ?? []) {
|
|
518
|
+
if (!c.CacheClusterId)
|
|
519
|
+
continue;
|
|
520
|
+
clusters.push({
|
|
521
|
+
id: c.CacheClusterId,
|
|
522
|
+
engine: c.Engine ?? 'unknown',
|
|
523
|
+
engineVersion: c.EngineVersion ?? '',
|
|
524
|
+
nodeType: c.CacheNodeType ?? '',
|
|
525
|
+
numNodes: c.NumCacheNodes ?? 0,
|
|
526
|
+
transitEncryption: c.TransitEncryptionEnabled ?? false,
|
|
527
|
+
atRestEncryption: c.AtRestEncryptionEnabled ?? false,
|
|
528
|
+
replicationGroupId: c.ReplicationGroupId,
|
|
529
|
+
automaticFailover: c.ReplicationGroupId
|
|
530
|
+
? failoverByGroup.get(c.ReplicationGroupId)
|
|
531
|
+
: undefined,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
marker = res.Marker;
|
|
535
|
+
} while (marker);
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
logger.warn(`ElastiCache list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
539
|
+
}
|
|
540
|
+
return clusters;
|
|
541
|
+
}
|
|
542
|
+
// ─── Cognito ─────────────────────────────────────────────────────────────────
|
|
543
|
+
export async function extractCognitoMetadata(cfg = {}) {
|
|
544
|
+
const client = new CognitoIdentityProviderClient(clientConfig(cfg));
|
|
545
|
+
const pools = [];
|
|
546
|
+
try {
|
|
547
|
+
let nextToken;
|
|
548
|
+
const poolRefs = [];
|
|
549
|
+
do {
|
|
550
|
+
const res = await client.send(new ListUserPoolsCommand({ MaxResults: 60, NextToken: nextToken }));
|
|
551
|
+
for (const p of res.UserPools ?? []) {
|
|
552
|
+
if (p.Id && p.Name)
|
|
553
|
+
poolRefs.push({ id: p.Id, name: p.Name });
|
|
554
|
+
}
|
|
555
|
+
nextToken = res.NextToken;
|
|
556
|
+
} while (nextToken);
|
|
557
|
+
for (const ref of poolRefs) {
|
|
558
|
+
try {
|
|
559
|
+
const poolRes = await client.send(new DescribeUserPoolCommand({ UserPoolId: ref.id }));
|
|
560
|
+
const clients = [];
|
|
561
|
+
let clientToken;
|
|
562
|
+
do {
|
|
563
|
+
const clientsRes = await client.send(new ListUserPoolClientsCommand({
|
|
564
|
+
UserPoolId: ref.id,
|
|
565
|
+
MaxResults: 60,
|
|
566
|
+
NextToken: clientToken,
|
|
567
|
+
}));
|
|
568
|
+
for (const c of clientsRes.UserPoolClients ?? []) {
|
|
569
|
+
if (!c.ClientId)
|
|
570
|
+
continue;
|
|
571
|
+
try {
|
|
572
|
+
const detail = await client.send(new DescribeUserPoolClientCommand({ UserPoolId: ref.id, ClientId: c.ClientId }));
|
|
573
|
+
const d = detail.UserPoolClient;
|
|
574
|
+
if (!d)
|
|
575
|
+
continue;
|
|
576
|
+
clients.push({
|
|
577
|
+
clientName: d.ClientName ?? c.ClientName ?? '',
|
|
578
|
+
clientId: c.ClientId,
|
|
579
|
+
authFlows: d.ExplicitAuthFlows ?? [],
|
|
580
|
+
oauthFlows: d.AllowedOAuthFlows ?? [],
|
|
581
|
+
oauthScopes: d.AllowedOAuthScopes ?? [],
|
|
582
|
+
callbackUrls: d.CallbackURLs ?? [],
|
|
583
|
+
generatesSecret: !!d.ClientSecret,
|
|
584
|
+
accessTokenValidity: d.AccessTokenValidity,
|
|
585
|
+
idTokenValidity: d.IdTokenValidity,
|
|
586
|
+
refreshTokenValidity: d.RefreshTokenValidity,
|
|
587
|
+
tokenValidityUnits: d.TokenValidityUnits
|
|
588
|
+
? {
|
|
589
|
+
accessToken: d.TokenValidityUnits.AccessToken,
|
|
590
|
+
idToken: d.TokenValidityUnits.IdToken,
|
|
591
|
+
refreshToken: d.TokenValidityUnits.RefreshToken,
|
|
592
|
+
}
|
|
593
|
+
: undefined,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
/* skip client on describe failure */
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
clientToken = clientsRes.NextToken;
|
|
601
|
+
} while (clientToken);
|
|
602
|
+
pools.push({
|
|
603
|
+
name: ref.name,
|
|
604
|
+
id: ref.id,
|
|
605
|
+
mfaConfiguration: poolRes.UserPool?.MfaConfiguration,
|
|
606
|
+
clients,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
logger.warn(`Cognito describe failed for ${ref.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
logger.warn(`Cognito list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
616
|
+
}
|
|
617
|
+
return pools;
|
|
618
|
+
}
|
|
423
619
|
// ─── RDS ─────────────────────────────────────────────────────────────────────
|
|
424
620
|
export async function extractRDSMetadata(cfg = {}) {
|
|
425
621
|
const client = new RDSClient(clientConfig(cfg));
|
|
@@ -14,6 +14,7 @@ function emptySchema() {
|
|
|
14
14
|
parameters: [],
|
|
15
15
|
secrets: [],
|
|
16
16
|
apiGateways: [],
|
|
17
|
+
outputs: [],
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
// ─── File discovery ───────────────────────────────────────────────────────────
|
|
@@ -76,6 +77,25 @@ function tfGSINames(body) {
|
|
|
76
77
|
}
|
|
77
78
|
return names;
|
|
78
79
|
}
|
|
80
|
+
function extractTerraformOutputBlocks(content) {
|
|
81
|
+
const results = [];
|
|
82
|
+
const pat = /output\s+"([^"]+)"\s*\{/g;
|
|
83
|
+
let match;
|
|
84
|
+
while ((match = pat.exec(content)) !== null) {
|
|
85
|
+
const startBrace = match.index + match[0].length - 1;
|
|
86
|
+
let depth = 1;
|
|
87
|
+
let i = startBrace + 1;
|
|
88
|
+
while (i < content.length && depth > 0) {
|
|
89
|
+
if (content[i] === '{')
|
|
90
|
+
depth++;
|
|
91
|
+
else if (content[i] === '}')
|
|
92
|
+
depth--;
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
results.push({ name: match[1] ?? '', body: content.slice(startBrace + 1, i - 1) });
|
|
96
|
+
}
|
|
97
|
+
return results;
|
|
98
|
+
}
|
|
79
99
|
export async function extractTerraformSchema(repoPath) {
|
|
80
100
|
const schema = emptySchema();
|
|
81
101
|
const tfFiles = findFilesRecursively(repoPath, ['.tf']);
|
|
@@ -173,6 +193,16 @@ export async function extractTerraformSchema(repoPath) {
|
|
|
173
193
|
break;
|
|
174
194
|
}
|
|
175
195
|
}
|
|
196
|
+
for (const { name, body } of extractTerraformOutputBlocks(content)) {
|
|
197
|
+
const valueMatch = body.match(/value\s*=\s*(.+)/);
|
|
198
|
+
schema.outputs.push({
|
|
199
|
+
name,
|
|
200
|
+
description: tfStr(body, 'description'),
|
|
201
|
+
value: valueMatch?.[1]?.trim(),
|
|
202
|
+
source: 'terraform',
|
|
203
|
+
filePath,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
176
206
|
}
|
|
177
207
|
return schema;
|
|
178
208
|
}
|
|
@@ -341,6 +371,30 @@ function processCFNResources(resources, schema, filePath, source) {
|
|
|
341
371
|
}
|
|
342
372
|
}
|
|
343
373
|
}
|
|
374
|
+
function processCFNOutputs(template, schema, filePath, source) {
|
|
375
|
+
const outputs = template['Outputs'];
|
|
376
|
+
if (typeof outputs !== 'object' || outputs === null)
|
|
377
|
+
return;
|
|
378
|
+
for (const [name, raw] of Object.entries(outputs)) {
|
|
379
|
+
if (typeof raw !== 'object' || raw === null)
|
|
380
|
+
continue;
|
|
381
|
+
const out = raw;
|
|
382
|
+
const exp = out['Export'];
|
|
383
|
+
const expName = exp?.['Name'];
|
|
384
|
+
schema.outputs.push({
|
|
385
|
+
name,
|
|
386
|
+
description: typeof out['Description'] === 'string' ? out['Description'] : undefined,
|
|
387
|
+
exportName: expName === undefined
|
|
388
|
+
? undefined
|
|
389
|
+
: typeof expName === 'string'
|
|
390
|
+
? expName
|
|
391
|
+
: JSON.stringify(expName),
|
|
392
|
+
value: typeof out['Value'] === 'string' ? out['Value'] : JSON.stringify(out['Value']),
|
|
393
|
+
source,
|
|
394
|
+
filePath,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
344
398
|
export async function extractCloudFormationSchema(repoPath) {
|
|
345
399
|
const schema = emptySchema();
|
|
346
400
|
const cfnFiles = findFilesRecursively(repoPath, ['.yaml', '.yml', '.json']);
|
|
@@ -353,6 +407,7 @@ export async function extractCloudFormationSchema(repoPath) {
|
|
|
353
407
|
if (!resources)
|
|
354
408
|
continue;
|
|
355
409
|
processCFNResources(resources, schema, filePath, 'cloudformation');
|
|
410
|
+
processCFNOutputs(parsed, schema, filePath, 'cloudformation');
|
|
356
411
|
}
|
|
357
412
|
return schema;
|
|
358
413
|
}
|
|
@@ -375,6 +430,7 @@ export async function extractCDKSchema(repoPath) {
|
|
|
375
430
|
if (!resources)
|
|
376
431
|
continue;
|
|
377
432
|
processCFNResources(resources, schema, filePath, 'cdk');
|
|
433
|
+
processCFNOutputs(parsed, schema, filePath, 'cdk');
|
|
378
434
|
}
|
|
379
435
|
}
|
|
380
436
|
// Strategy 2: Detect CDK TypeScript construct patterns if no cdk.out exists
|
|
@@ -400,6 +456,7 @@ function mergeSchemas(...schemas) {
|
|
|
400
456
|
param: new Set(),
|
|
401
457
|
secret: new Set(),
|
|
402
458
|
api: new Set(),
|
|
459
|
+
output: new Set(),
|
|
403
460
|
};
|
|
404
461
|
for (const schema of schemas) {
|
|
405
462
|
for (const t of schema.dynamoTables) {
|
|
@@ -472,6 +529,13 @@ function mergeSchemas(...schemas) {
|
|
|
472
529
|
merged.apiGateways.push(a);
|
|
473
530
|
}
|
|
474
531
|
}
|
|
532
|
+
for (const o of schema.outputs) {
|
|
533
|
+
const k = `${o.source}::${o.filePath}::${o.name}`;
|
|
534
|
+
if (!seen.output.has(k)) {
|
|
535
|
+
seen.output.add(k);
|
|
536
|
+
merged.outputs.push(o);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
475
539
|
}
|
|
476
540
|
return merged;
|
|
477
541
|
}
|
|
@@ -491,7 +555,8 @@ export async function extractIaCSchema(repoPath) {
|
|
|
491
555
|
merged.buckets.length +
|
|
492
556
|
merged.parameters.length +
|
|
493
557
|
merged.secrets.length +
|
|
494
|
-
merged.apiGateways.length
|
|
558
|
+
merged.apiGateways.length +
|
|
559
|
+
merged.outputs.length;
|
|
495
560
|
logger.debug(`IaC schema total: ${total} resource(s) across TF/CFN/CDK`);
|
|
496
561
|
return merged;
|
|
497
562
|
}
|
|
@@ -284,6 +284,93 @@ export class S3UnencryptedAnalyzer {
|
|
|
284
284
|
return findings;
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
// ─── ElastiCache ─────────────────────────────────────────────────────────────
|
|
288
|
+
export class CacheTransitEncryptionAnalyzer {
|
|
289
|
+
name = 'CacheTransitEncryptionAnalyzer';
|
|
290
|
+
async analyze(graph) {
|
|
291
|
+
const findings = [];
|
|
292
|
+
for (const node of graph.nodes) {
|
|
293
|
+
if (node.type !== 'cache_cluster')
|
|
294
|
+
continue;
|
|
295
|
+
if (node.transitEncryption === false) {
|
|
296
|
+
findings.push({
|
|
297
|
+
severity: 'medium',
|
|
298
|
+
issue: `Cache cluster "${node.name}" has no in-transit encryption`,
|
|
299
|
+
description: `ElastiCache cluster "${node.name}" (${node.engine}) does not have TLS in-transit encryption enabled. Credentials and cached data cross the network in plaintext.`,
|
|
300
|
+
recommendation: `Enable transit encryption on "${node.name}". Note this requires clients to connect with TLS (rediss:// for Redis).`,
|
|
301
|
+
metadata: { cacheClusterId: node.name, provider: node.provider },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return findings;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export class CacheSingleNodeAnalyzer {
|
|
309
|
+
name = 'CacheSingleNodeAnalyzer';
|
|
310
|
+
async analyze(graph) {
|
|
311
|
+
const findings = [];
|
|
312
|
+
for (const node of graph.nodes) {
|
|
313
|
+
if (node.type !== 'cache_cluster')
|
|
314
|
+
continue;
|
|
315
|
+
if (node.numNodes === 1 && !node.replicationGroupId) {
|
|
316
|
+
findings.push({
|
|
317
|
+
severity: 'low',
|
|
318
|
+
issue: `Cache cluster "${node.name}" is a single node with no replication`,
|
|
319
|
+
description: `"${node.name}" runs one cache node outside a replication group. A node failure loses all cached data and takes the cache offline until replaced.`,
|
|
320
|
+
recommendation: `Move "${node.name}" into a replication group with at least one replica and automatic failover if cache availability matters for this workload.`,
|
|
321
|
+
metadata: { cacheClusterId: node.name },
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return findings;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ─── Runtime signals ─────────────────────────────────────────────────────────
|
|
329
|
+
export class LambdaThrottlingAnalyzer {
|
|
330
|
+
name = 'LambdaThrottlingAnalyzer';
|
|
331
|
+
async analyze(graph) {
|
|
332
|
+
const findings = [];
|
|
333
|
+
for (const node of graph.nodes) {
|
|
334
|
+
if (node.type !== 'lambda')
|
|
335
|
+
continue;
|
|
336
|
+
if ((node.recentThrottles ?? 0) > 0) {
|
|
337
|
+
findings.push({
|
|
338
|
+
severity: 'high',
|
|
339
|
+
issue: `Lambda "${node.name}" was throttled ${node.recentThrottles} time(s) recently`,
|
|
340
|
+
description: `"${node.name}" hit concurrency throttling in the analysis window. Throttled invocations are rejected or delayed; for sync callers this surfaces as errors, for event sources as retries and growing backlogs.`,
|
|
341
|
+
recommendation: `Check reserved/account concurrency for "${node.name}". Raise reserved concurrency, request an account limit increase, or smooth the invoke rate (SQS between producer and Lambda).`,
|
|
342
|
+
metadata: { functionName: node.name, recentThrottles: node.recentThrottles },
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return findings;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
export class StaleQueueMessagesAnalyzer {
|
|
350
|
+
name = 'StaleQueueMessagesAnalyzer';
|
|
351
|
+
thresholdSec;
|
|
352
|
+
constructor(thresholdSec = 3600) {
|
|
353
|
+
this.thresholdSec = thresholdSec;
|
|
354
|
+
}
|
|
355
|
+
async analyze(graph) {
|
|
356
|
+
const findings = [];
|
|
357
|
+
for (const node of graph.nodes) {
|
|
358
|
+
if (node.type !== 'queue')
|
|
359
|
+
continue;
|
|
360
|
+
const age = node.oldestMessageAgeSec ?? 0;
|
|
361
|
+
if (age > this.thresholdSec) {
|
|
362
|
+
findings.push({
|
|
363
|
+
severity: 'medium',
|
|
364
|
+
issue: `Queue "${node.name}" has messages older than ${Math.round(age / 3600)} hour(s)`,
|
|
365
|
+
description: `The oldest message in "${node.name}" is ${Math.round(age / 60)} minutes old. Consumers are not keeping up, are failing, or are not running.`,
|
|
366
|
+
recommendation: `Check consumer health for "${node.name}". If a Lambda consumes it, look at its error and throttle counts; if messages are near the retention limit they will be silently dropped.`,
|
|
367
|
+
metadata: { queueName: node.name, oldestMessageAgeSec: age },
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return findings;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
287
374
|
// ─── IAM ─────────────────────────────────────────────────────────────────────
|
|
288
375
|
const MINIMAL_ACTIONS = {
|
|
289
376
|
dynamodb: 'dynamodb:GetItem, dynamodb:PutItem, dynamodb:Query, dynamodb:UpdateItem, dynamodb:DeleteItem',
|
package/dist/analyzers/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer } from './mysql.j
|
|
|
5
5
|
export { MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer } from './mongodb.js';
|
|
6
6
|
export { IaCDriftAnalyzer } from './terraform.js';
|
|
7
7
|
export { PipelineAnalyzer } from './pipeline.js';
|
|
8
|
-
export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from './aws-services.js';
|
|
8
|
+
export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, CacheTransitEncryptionAnalyzer, CacheSingleNodeAnalyzer, LambdaThrottlingAnalyzer, StaleQueueMessagesAnalyzer, } from './aws-services.js';
|
|
9
9
|
export { RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, } from './rds.js';
|
|
10
10
|
export async function runAllAnalyzers(graph, analyzers) {
|
|
11
11
|
const allFindings = [];
|
|
@@ -8,12 +8,13 @@ import { extractPostgresMetadata } from '../../adapters/db/postgres.js';
|
|
|
8
8
|
import { extractMySQLMetadata } from '../../adapters/db/mysql.js';
|
|
9
9
|
import { extractMongoMetadata } from '../../adapters/db/mongodb.js';
|
|
10
10
|
import { extractIaCSchema } from '../../adapters/iac/terraform.js';
|
|
11
|
-
import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractEventBridgeMetadata, extractRDSMetadata, extractAPIGatewayMetadata, } from '../../adapters/aws/services.js';
|
|
11
|
+
import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractEventBridgeMetadata, extractRDSMetadata, extractAPIGatewayMetadata, extractCognitoMetadata, extractKinesisMetadata, extractMSKMetadata, extractElastiCacheMetadata, } from '../../adapters/aws/services.js';
|
|
12
12
|
import { extractLogsMetadata } from '../../adapters/aws/logs.js';
|
|
13
|
+
import { extractRuntimeSignals } from '../../adapters/aws/metrics.js';
|
|
13
14
|
import { extractS3Metadata } from '../../adapters/aws/s3.js';
|
|
14
15
|
import { scanRepository } from '../../context/index.js';
|
|
15
|
-
import { buildGraph } from '../../graph/index.js';
|
|
16
|
-
import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from '../../analyzers/index.js';
|
|
16
|
+
import { buildGraph, addStackOutputNodes } from '../../graph/index.js';
|
|
17
|
+
import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, CacheTransitEncryptionAnalyzer, CacheSingleNodeAnalyzer, LambdaThrottlingAnalyzer, StaleQueueMessagesAnalyzer, } from '../../analyzers/index.js';
|
|
17
18
|
import { printFinding, printSummaryBox, log, printHeader } from '../utils.js';
|
|
18
19
|
const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, verify: 0 };
|
|
19
20
|
function buildMarkdownReport(findings, projectName) {
|
|
@@ -119,6 +120,12 @@ function buildAnalyzers(config, iacDriftAnalyzer, iacLambdas) {
|
|
|
119
120
|
new S3UnencryptedAnalyzer(),
|
|
120
121
|
]
|
|
121
122
|
: []),
|
|
123
|
+
...(config.elasticache?.enabled === true
|
|
124
|
+
? [new CacheTransitEncryptionAnalyzer(), new CacheSingleNodeAnalyzer()]
|
|
125
|
+
: []),
|
|
126
|
+
...(config.runtimeSignals?.enabled === true
|
|
127
|
+
? [new LambdaThrottlingAnalyzer(), new StaleQueueMessagesAnalyzer()]
|
|
128
|
+
: []),
|
|
122
129
|
...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
|
|
123
130
|
pipelineAnalyzer,
|
|
124
131
|
];
|
|
@@ -158,15 +165,44 @@ export async function runAnalyze(options = {}) {
|
|
|
158
165
|
servicesMeta.rds = await extract(config.rds?.enabled === true, 'Extracting RDS instances...', 'RDS', () => extractRDSMetadata(awsCfg), (r) => `${r.length} instance(s)`);
|
|
159
166
|
servicesMeta.s3 = await extract(config.s3?.enabled === true, 'Extracting S3 buckets...', 'S3', () => extractS3Metadata(awsCfg), (r) => `${r.length} bucket(s)`);
|
|
160
167
|
servicesMeta.apiGateway = await extract(config.apiGateway?.enabled === true, 'Extracting API Gateway routes...', 'API Gateway', () => extractAPIGatewayMetadata(awsCfg), (r) => `${r.length} API(s), ${r.reduce((sum, api) => sum + api.routes.length, 0)} route(s)`);
|
|
168
|
+
servicesMeta.cognito = await extract(config.cognito?.enabled === true, 'Extracting Cognito user pools...', 'Cognito', () => extractCognitoMetadata(awsCfg), (r) => `${r.length} user pool(s)`);
|
|
169
|
+
servicesMeta.kinesis = await extract(config.kinesis?.enabled === true, 'Extracting Kinesis streams...', 'Kinesis', () => extractKinesisMetadata(awsCfg), (r) => `${r.length} stream(s)`);
|
|
170
|
+
servicesMeta.msk = await extract(config.msk?.enabled === true, 'Extracting MSK clusters...', 'MSK', () => extractMSKMetadata(awsCfg), (r) => `${r.length} cluster(s)`);
|
|
171
|
+
servicesMeta.elasticache = await extract(config.elasticache?.enabled === true, 'Extracting ElastiCache clusters...', 'ElastiCache', () => extractElastiCacheMetadata(awsCfg), (r) => `${r.length} cluster(s)`);
|
|
161
172
|
const cwLogs = config.cloudwatchLogs;
|
|
162
173
|
servicesMeta.logs = await extract(cwLogs?.enabled, 'Sampling CloudWatch Logs (errors only, max 50 groups)...', 'CloudWatch Logs', () => extractLogsMetadata({
|
|
163
174
|
...awsCfg,
|
|
164
175
|
logGroupPrefixes: cwLogs?.logGroupPrefixes,
|
|
165
176
|
windowHours: cwLogs?.windowHours,
|
|
166
177
|
}), (r) => `${r.length} group(s), ${r.filter((lg) => lg.errorCount > 0).length} with errors`);
|
|
178
|
+
if (config.runtimeSignals?.enabled && (servicesMeta.lambda?.length || servicesMeta.sqs?.length)) {
|
|
179
|
+
const s = mkSpinner('Fetching runtime signals (CloudWatch metrics)...');
|
|
180
|
+
try {
|
|
181
|
+
const signals = await extractRuntimeSignals(awsCfg, (servicesMeta.lambda ?? []).map((f) => f.name), (servicesMeta.sqs ?? []).map((q) => q.name), config.runtimeSignals.windowHours);
|
|
182
|
+
for (const fn of servicesMeta.lambda ?? []) {
|
|
183
|
+
const sig = signals.lambdas.get(fn.name);
|
|
184
|
+
if (sig) {
|
|
185
|
+
fn.recentThrottles = sig.throttles;
|
|
186
|
+
fn.recentErrors = sig.errors;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const q of servicesMeta.sqs ?? []) {
|
|
190
|
+
const sig = signals.queues.get(q.name);
|
|
191
|
+
if (sig)
|
|
192
|
+
q.oldestMessageAgeSec = sig.oldestMessageAgeSec;
|
|
193
|
+
}
|
|
194
|
+
s.succeed(chalk.green('Runtime signals') +
|
|
195
|
+
chalk.dim(` ${signals.lambdas.size} function(s), ${signals.queues.size} queue(s)`));
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
s.warn(chalk.yellow('Runtime signals skipped') +
|
|
199
|
+
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
167
202
|
// ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
|
|
168
203
|
let iacDriftAnalyzer;
|
|
169
204
|
let iacLambdas = [];
|
|
205
|
+
let iacOutputs = [];
|
|
170
206
|
{
|
|
171
207
|
const s = mkSpinner('Extracting IaC schema (Terraform / CloudFormation / CDK)...');
|
|
172
208
|
try {
|
|
@@ -180,10 +216,12 @@ export async function runAnalyze(options = {}) {
|
|
|
180
216
|
iacSchema.buckets.length +
|
|
181
217
|
iacSchema.parameters.length +
|
|
182
218
|
iacSchema.secrets.length +
|
|
183
|
-
iacSchema.apiGateways.length
|
|
219
|
+
iacSchema.apiGateways.length +
|
|
220
|
+
iacSchema.outputs.length;
|
|
184
221
|
iacDriftAnalyzer = new IaCDriftAnalyzer();
|
|
185
222
|
iacDriftAnalyzer.setIaCSchema(iacSchema);
|
|
186
223
|
iacLambdas = iacSchema.lambdas;
|
|
224
|
+
iacOutputs = iacSchema.outputs;
|
|
187
225
|
s.succeed(chalk.green('IaC schema') + chalk.dim(` ${total} resource(s) across TF/CFN/CDK`));
|
|
188
226
|
}
|
|
189
227
|
catch (err) {
|
|
@@ -211,6 +249,7 @@ export async function runAnalyze(options = {}) {
|
|
|
211
249
|
{
|
|
212
250
|
const s = mkSpinner('Building infrastructure graph...');
|
|
213
251
|
graph = buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
|
|
252
|
+
addStackOutputNodes(graph, iacOutputs);
|
|
214
253
|
s.succeed(chalk.green('Graph built') +
|
|
215
254
|
chalk.dim(` ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
|
|
216
255
|
}
|
|
@@ -278,11 +317,13 @@ export async function runCodeRefresh(repoPath, config) {
|
|
|
278
317
|
// Re-run IaC schema (pure file scan, no AWS calls)
|
|
279
318
|
let iacDriftAnalyzer;
|
|
280
319
|
let iacLambdas = [];
|
|
320
|
+
let iacOutputs = [];
|
|
281
321
|
try {
|
|
282
322
|
const iacSchema = await extractIaCSchema(repoPath);
|
|
283
323
|
iacDriftAnalyzer = new IaCDriftAnalyzer();
|
|
284
324
|
iacDriftAnalyzer.setIaCSchema(iacSchema);
|
|
285
325
|
iacLambdas = iacSchema.lambdas;
|
|
326
|
+
iacOutputs = iacSchema.outputs;
|
|
286
327
|
}
|
|
287
328
|
catch {
|
|
288
329
|
// IaC scan is best-effort
|
|
@@ -296,6 +337,7 @@ export async function runCodeRefresh(repoPath, config) {
|
|
|
296
337
|
operations = [];
|
|
297
338
|
}
|
|
298
339
|
const graph = buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
|
|
340
|
+
addStackOutputNodes(graph, iacOutputs);
|
|
299
341
|
const analyzers = buildAnalyzers(config, iacDriftAnalyzer, iacLambdas);
|
|
300
342
|
const findings = await runAllAnalyzers(graph, analyzers);
|
|
301
343
|
writeCache('graph', graph);
|
|
@@ -186,6 +186,11 @@ function writeYaml(cwd, opts) {
|
|
|
186
186
|
rds: { enabled: false },
|
|
187
187
|
s3: { enabled: true },
|
|
188
188
|
apiGateway: { enabled: true },
|
|
189
|
+
cognito: { enabled: false },
|
|
190
|
+
kinesis: { enabled: false },
|
|
191
|
+
msk: { enabled: false },
|
|
192
|
+
elasticache: { enabled: false },
|
|
193
|
+
runtimeSignals: { enabled: false, windowHours: 24 },
|
|
189
194
|
cloudwatchLogs: { enabled: false, logGroupPrefixes: [], windowHours: 24 },
|
|
190
195
|
analysis: { sampleSize: 100 },
|
|
191
196
|
};
|
|
@@ -25,6 +25,10 @@ const TOOL_MAP = [
|
|
|
25
25
|
{ name: 'get_s3_overview', service: 's3' },
|
|
26
26
|
{ name: 'get_api_routes', service: 'apiGateway' },
|
|
27
27
|
{ name: 'get_log_errors', service: 'cloudwatchLogs' },
|
|
28
|
+
{ name: 'get_stack_outputs', service: 'terraform' },
|
|
29
|
+
{ name: 'get_cognito_overview', service: 'cognito' },
|
|
30
|
+
{ name: 'get_stream_details', service: 'kinesis' },
|
|
31
|
+
{ name: 'get_cache_overview', service: 'elasticache' },
|
|
28
32
|
];
|
|
29
33
|
// With no config every registered tool is shown as active (the server exposes
|
|
30
34
|
// all of them); a config narrows the list to its enabled services.
|
package/dist/core/config.js
CHANGED
|
@@ -57,6 +57,16 @@ export const InfrawiseConfigSchema = z.object({
|
|
|
57
57
|
rds: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
58
58
|
s3: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
59
59
|
apiGateway: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
60
|
+
cognito: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
61
|
+
kinesis: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
62
|
+
msk: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
63
|
+
elasticache: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
64
|
+
runtimeSignals: z
|
|
65
|
+
.object({
|
|
66
|
+
enabled: z.boolean().optional().default(false),
|
|
67
|
+
windowHours: z.number().int().positive().optional().default(24),
|
|
68
|
+
})
|
|
69
|
+
.optional(),
|
|
60
70
|
cloudwatchLogs: z
|
|
61
71
|
.object({
|
|
62
72
|
enabled: z.boolean().optional().default(false),
|
|
@@ -185,6 +195,14 @@ export function generateDefaultConfig(projectName, options) {
|
|
|
185
195
|
rds: { enabled: options?.rds?.enabled ?? false },
|
|
186
196
|
s3: { enabled: options?.s3?.enabled ?? false },
|
|
187
197
|
apiGateway: { enabled: options?.apiGateway?.enabled ?? false },
|
|
198
|
+
cognito: { enabled: options?.cognito?.enabled ?? false },
|
|
199
|
+
kinesis: { enabled: options?.kinesis?.enabled ?? false },
|
|
200
|
+
msk: { enabled: options?.msk?.enabled ?? false },
|
|
201
|
+
elasticache: { enabled: options?.elasticache?.enabled ?? false },
|
|
202
|
+
runtimeSignals: {
|
|
203
|
+
enabled: options?.runtimeSignals?.enabled ?? false,
|
|
204
|
+
windowHours: options?.runtimeSignals?.windowHours ?? 24,
|
|
205
|
+
},
|
|
188
206
|
cloudwatchLogs: {
|
|
189
207
|
enabled: options?.cloudwatchLogs?.enabled ?? false,
|
|
190
208
|
logGroupPrefixes: options?.cloudwatchLogs?.logGroupPrefixes ?? [],
|
package/dist/graph/index.js
CHANGED
|
@@ -77,6 +77,32 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
77
77
|
visibilityTimeoutSec: q.visibilityTimeoutSec,
|
|
78
78
|
approximateMessages: q.approximateMessages,
|
|
79
79
|
retentionDays: q.retentionDays,
|
|
80
|
+
oldestMessageAgeSec: q.oldestMessageAgeSec,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
for (const s of servicesMeta.kinesis ?? []) {
|
|
84
|
+
addNode({
|
|
85
|
+
id: `stream:aws:${s.name}`,
|
|
86
|
+
type: 'stream',
|
|
87
|
+
name: s.name,
|
|
88
|
+
provider: 'aws',
|
|
89
|
+
status: s.status,
|
|
90
|
+
shardCount: s.shardCount,
|
|
91
|
+
retentionHours: s.retentionHours,
|
|
92
|
+
encrypted: s.encrypted,
|
|
93
|
+
mode: s.mode,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const c of servicesMeta.msk ?? []) {
|
|
97
|
+
addNode({
|
|
98
|
+
id: `kafka_cluster:aws:${c.name}`,
|
|
99
|
+
type: 'kafka_cluster',
|
|
100
|
+
name: c.name,
|
|
101
|
+
provider: 'aws',
|
|
102
|
+
state: c.state,
|
|
103
|
+
clusterType: c.clusterType,
|
|
104
|
+
kafkaVersion: c.kafkaVersion,
|
|
105
|
+
brokerNodes: c.brokerNodes,
|
|
80
106
|
});
|
|
81
107
|
}
|
|
82
108
|
for (const api of servicesMeta.apiGateway ?? []) {
|
|
@@ -150,6 +176,8 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
150
176
|
triggers: fn.triggers,
|
|
151
177
|
roleArn: fn.roleArn,
|
|
152
178
|
allowedServices: fn.allowedServices,
|
|
179
|
+
recentThrottles: fn.recentThrottles,
|
|
180
|
+
recentErrors: fn.recentErrors,
|
|
153
181
|
});
|
|
154
182
|
// Add trigger edges from source → lambda (only for enabled/active mappings)
|
|
155
183
|
for (const trigger of fn.triggers ?? []) {
|
|
@@ -177,15 +205,8 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
177
205
|
});
|
|
178
206
|
}
|
|
179
207
|
else if (trigger.type === 'kinesis') {
|
|
180
|
-
sourceId = `
|
|
181
|
-
addNode({
|
|
182
|
-
id: sourceId,
|
|
183
|
-
type: 'queue',
|
|
184
|
-
name: trigger.sourceName,
|
|
185
|
-
provider: 'aws',
|
|
186
|
-
hasDLQ: false,
|
|
187
|
-
encrypted: false,
|
|
188
|
-
});
|
|
208
|
+
sourceId = `stream:aws:${trigger.sourceName}`;
|
|
209
|
+
addNode({ id: sourceId, type: 'stream', name: trigger.sourceName, provider: 'aws' });
|
|
189
210
|
}
|
|
190
211
|
else {
|
|
191
212
|
continue;
|
|
@@ -271,6 +292,33 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
271
292
|
}
|
|
272
293
|
}
|
|
273
294
|
}
|
|
295
|
+
for (const c of servicesMeta.elasticache ?? []) {
|
|
296
|
+
addNode({
|
|
297
|
+
id: `cache_cluster:aws:${c.id}`,
|
|
298
|
+
type: 'cache_cluster',
|
|
299
|
+
name: c.id,
|
|
300
|
+
provider: 'aws',
|
|
301
|
+
engine: c.engine,
|
|
302
|
+
engineVersion: c.engineVersion,
|
|
303
|
+
nodeType: c.nodeType,
|
|
304
|
+
numNodes: c.numNodes,
|
|
305
|
+
transitEncryption: c.transitEncryption,
|
|
306
|
+
atRestEncryption: c.atRestEncryption,
|
|
307
|
+
replicationGroupId: c.replicationGroupId,
|
|
308
|
+
automaticFailover: c.automaticFailover,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
for (const pool of servicesMeta.cognito ?? []) {
|
|
312
|
+
addNode({
|
|
313
|
+
id: `user_pool:aws:${pool.id}`,
|
|
314
|
+
type: 'user_pool',
|
|
315
|
+
name: pool.name,
|
|
316
|
+
provider: 'aws',
|
|
317
|
+
poolId: pool.id,
|
|
318
|
+
mfaConfiguration: pool.mfaConfiguration,
|
|
319
|
+
clients: pool.clients,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
274
322
|
// ── Code operations (functions + edges) ───────────────────────────────────
|
|
275
323
|
for (const op of operations) {
|
|
276
324
|
const funcNodeId = `function:${op.filePath}:${op.functionName}`;
|
|
@@ -360,6 +408,25 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
360
408
|
}
|
|
361
409
|
return { nodes, edges };
|
|
362
410
|
}
|
|
411
|
+
export function addStackOutputNodes(graph, outputs) {
|
|
412
|
+
const ids = new Set(graph.nodes.map((n) => n.id));
|
|
413
|
+
for (const o of outputs) {
|
|
414
|
+
const id = `stack_output:${o.source}:${o.filePath}:${o.name}`;
|
|
415
|
+
if (ids.has(id))
|
|
416
|
+
continue;
|
|
417
|
+
ids.add(id);
|
|
418
|
+
graph.nodes.push({
|
|
419
|
+
id,
|
|
420
|
+
type: 'stack_output',
|
|
421
|
+
name: o.name,
|
|
422
|
+
description: o.description,
|
|
423
|
+
exportName: o.exportName,
|
|
424
|
+
value: o.value,
|
|
425
|
+
iacSource: o.source,
|
|
426
|
+
file: o.filePath,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
363
430
|
function resolveEdgeType(operationType) {
|
|
364
431
|
const op = operationType.toLowerCase();
|
|
365
432
|
if (op === 'scan' || op === 'scancommand')
|
|
@@ -385,6 +452,11 @@ export const getLambdaNodes = (g) => getNodes(g, 'lambda');
|
|
|
385
452
|
export const getEventBridgeRuleNodes = (g) => getNodes(g, 'eventbridge_rule');
|
|
386
453
|
export const getBucketNodes = (g) => getNodes(g, 'bucket');
|
|
387
454
|
export const getAPINodes = (g) => getNodes(g, 'api');
|
|
455
|
+
export const getStackOutputNodes = (g) => getNodes(g, 'stack_output');
|
|
456
|
+
export const getUserPoolNodes = (g) => getNodes(g, 'user_pool');
|
|
457
|
+
export const getStreamNodes = (g) => getNodes(g, 'stream');
|
|
458
|
+
export const getKafkaClusterNodes = (g) => getNodes(g, 'kafka_cluster');
|
|
459
|
+
export const getCacheClusterNodes = (g) => getNodes(g, 'cache_cluster');
|
|
388
460
|
export function getEdgesForNode(graph, nodeId) {
|
|
389
461
|
return graph.edges.filter((e) => e.from === nodeId || e.to === nodeId);
|
|
390
462
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { z } from 'zod';
|
|
|
8
8
|
import { logger } from '../core/index.js';
|
|
9
9
|
const { version } = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
|
|
10
10
|
import { summarizeFindings } from '../analyzers/index.js';
|
|
11
|
-
import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getBucketNodes, getAPINodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
|
|
11
|
+
import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getBucketNodes, getAPINodes, getStackOutputNodes, getUserPoolNodes, getStreamNodes, getKafkaClusterNodes, getCacheClusterNodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
|
|
12
12
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
13
13
|
let currentGraph = { nodes: [], edges: [] };
|
|
14
14
|
let currentFindings = [];
|
|
@@ -70,6 +70,7 @@ export function createMcpServer() {
|
|
|
70
70
|
const lambdas = getLambdaNodes(currentGraph);
|
|
71
71
|
const functions = getFunctionNodes(currentGraph);
|
|
72
72
|
const buckets = getBucketNodes(currentGraph);
|
|
73
|
+
const userPools = getUserPoolNodes(currentGraph);
|
|
73
74
|
return toText({
|
|
74
75
|
configured,
|
|
75
76
|
...(configured ? {} : { setupHint: NOT_CONFIGURED_HINT }),
|
|
@@ -84,6 +85,9 @@ export function createMcpServer() {
|
|
|
84
85
|
logGroups: logGroups.length,
|
|
85
86
|
lambdas: lambdas.length,
|
|
86
87
|
buckets: buckets.length,
|
|
88
|
+
userPools: userPools.length,
|
|
89
|
+
streams: getStreamNodes(currentGraph).length,
|
|
90
|
+
cacheClusters: getCacheClusterNodes(currentGraph).length,
|
|
87
91
|
totalNodes: currentGraph.nodes.length,
|
|
88
92
|
totalEdges: currentGraph.edges.length,
|
|
89
93
|
findings: summarizeFindings(currentFindings),
|
|
@@ -300,7 +304,7 @@ export function createMcpServer() {
|
|
|
300
304
|
});
|
|
301
305
|
}));
|
|
302
306
|
mcp.registerTool('get_queue_details', {
|
|
303
|
-
description: 'Returns all SQS queues with DLQ presence, encryption status, FIFO type (isFifo), visibility timeout, approximate message count, and retention days. When isFifo is true, all SendMessage calls must include a MessageGroupId. Call this when reviewing messaging architecture, investigating a message backlog, checking DLQ coverage, or verifying visibility timeout is set correctly relative to Lambda timeout (should be 6× the Lambda timeout). Use get_infra_overview for a quick queue count only.',
|
|
307
|
+
description: 'Returns all SQS queues with DLQ presence, encryption status, FIFO type (isFifo), visibility timeout, approximate message count, and retention days. When isFifo is true, all SendMessage calls must include a MessageGroupId. Call this when reviewing messaging architecture, investigating a message backlog, checking DLQ coverage, or verifying visibility timeout is set correctly relative to Lambda timeout (should be 6× the Lambda timeout). Use get_infra_overview for a quick queue count only. When runtime signals are enabled, oldestMessageAgeSec reports the age of the oldest message from CloudWatch.',
|
|
304
308
|
inputSchema: z.object({}),
|
|
305
309
|
}, logged('get_queue_details', async () => {
|
|
306
310
|
const queues = getQueueNodes(currentGraph);
|
|
@@ -316,6 +320,7 @@ export function createMcpServer() {
|
|
|
316
320
|
visibilityTimeoutSec: q.visibilityTimeoutSec,
|
|
317
321
|
approximateMessages: q.approximateMessages,
|
|
318
322
|
retentionDays: q.retentionDays,
|
|
323
|
+
oldestMessageAgeSec: q.oldestMessageAgeSec,
|
|
319
324
|
findings: queueFindings
|
|
320
325
|
.filter((f) => f.metadata.queueName === q.name)
|
|
321
326
|
.map((f) => ({ severity: f.severity, issue: f.issue })),
|
|
@@ -375,7 +380,7 @@ export function createMcpServer() {
|
|
|
375
380
|
});
|
|
376
381
|
}));
|
|
377
382
|
mcp.registerTool('get_lambda_overview', {
|
|
378
|
-
description: 'Returns all Lambda functions with runtime, memory (MB), timeout (sec), environment variable key names (values never returned), and event source triggers with the correct handler event shape for each. Call this when auditing Lambda configuration for default memory (128 MB) or high timeouts, or when you need the trigger event shape for a specific function without running analyze_function.',
|
|
383
|
+
description: 'Returns all Lambda functions with runtime, memory (MB), timeout (sec), environment variable key names (values never returned), and event source triggers with the correct handler event shape for each. Call this when auditing Lambda configuration for default memory (128 MB) or high timeouts, or when you need the trigger event shape for a specific function without running analyze_function. When runtime signals are enabled, recentThrottles and recentErrors report CloudWatch counts for the analysis window.',
|
|
379
384
|
inputSchema: z.object({}),
|
|
380
385
|
}, logged('get_lambda_overview', async () => {
|
|
381
386
|
const lambdas = getLambdaNodes(currentGraph);
|
|
@@ -391,6 +396,8 @@ export function createMcpServer() {
|
|
|
391
396
|
envVarCount: l.envVarKeys?.length ?? 0,
|
|
392
397
|
envVarKeys: l.envVarKeys,
|
|
393
398
|
roleArn: l.roleArn,
|
|
399
|
+
recentThrottles: l.recentThrottles,
|
|
400
|
+
recentErrors: l.recentErrors,
|
|
394
401
|
triggers: (l.triggers ?? []).map((t) => ({
|
|
395
402
|
type: t.type,
|
|
396
403
|
source: t.sourceName,
|
|
@@ -480,6 +487,90 @@ export function createMcpServer() {
|
|
|
480
487
|
})),
|
|
481
488
|
});
|
|
482
489
|
}));
|
|
490
|
+
mcp.registerTool('get_cache_overview', {
|
|
491
|
+
description: 'Returns all ElastiCache clusters with engine, version, node type, node count, in-transit and at-rest encryption status, replication group, and automatic failover state. Call this before writing cache client code (TLS is required when transit encryption is on — rediss:// for Redis) or when reviewing cache availability and security posture. Cached data is never read or included.',
|
|
492
|
+
inputSchema: z.object({}),
|
|
493
|
+
}, logged('get_cache_overview', async () => {
|
|
494
|
+
const caches = getCacheClusterNodes(currentGraph);
|
|
495
|
+
const cacheFindings = currentFindings.filter((f) => f.metadata?.cacheClusterId);
|
|
496
|
+
return toText({
|
|
497
|
+
total: caches.length,
|
|
498
|
+
note: 'Cached data is never included.',
|
|
499
|
+
caches: caches.map((c) => ({
|
|
500
|
+
id: c.name,
|
|
501
|
+
engine: c.engine,
|
|
502
|
+
engineVersion: c.engineVersion,
|
|
503
|
+
nodeType: c.nodeType,
|
|
504
|
+
numNodes: c.numNodes,
|
|
505
|
+
transitEncryption: c.transitEncryption,
|
|
506
|
+
atRestEncryption: c.atRestEncryption,
|
|
507
|
+
replicationGroupId: c.replicationGroupId,
|
|
508
|
+
automaticFailover: c.automaticFailover,
|
|
509
|
+
findings: cacheFindings
|
|
510
|
+
.filter((f) => f.metadata.cacheClusterId === c.name)
|
|
511
|
+
.map((f) => ({ severity: f.severity, issue: f.issue })),
|
|
512
|
+
})),
|
|
513
|
+
});
|
|
514
|
+
}));
|
|
515
|
+
mcp.registerTool('get_stream_details', {
|
|
516
|
+
description: 'Returns all Kinesis data streams (status, shard count, retention hours, encryption, capacity mode) and Amazon MSK clusters (state, cluster type, Kafka version, broker count). Call this when writing Kinesis producer or consumer code, checking whether a stream is PROVISIONED or ON_DEMAND before writing PutRecord calls, or reviewing streaming architecture. For Kafka topic-level producer/consumer mappings extracted from application code, use get_topic_details instead.',
|
|
517
|
+
inputSchema: z.object({}),
|
|
518
|
+
}, logged('get_stream_details', async () => {
|
|
519
|
+
const streams = getStreamNodes(currentGraph);
|
|
520
|
+
const clusters = getKafkaClusterNodes(currentGraph);
|
|
521
|
+
return toText({
|
|
522
|
+
totalStreams: streams.length,
|
|
523
|
+
totalKafkaClusters: clusters.length,
|
|
524
|
+
streams: streams.map((s) => ({
|
|
525
|
+
name: s.name,
|
|
526
|
+
status: s.status,
|
|
527
|
+
shardCount: s.shardCount,
|
|
528
|
+
retentionHours: s.retentionHours,
|
|
529
|
+
encrypted: s.encrypted,
|
|
530
|
+
mode: s.mode,
|
|
531
|
+
})),
|
|
532
|
+
kafkaClusters: clusters.map((c) => ({
|
|
533
|
+
name: c.name,
|
|
534
|
+
state: c.state,
|
|
535
|
+
clusterType: c.clusterType,
|
|
536
|
+
kafkaVersion: c.kafkaVersion,
|
|
537
|
+
brokerNodes: c.brokerNodes,
|
|
538
|
+
})),
|
|
539
|
+
});
|
|
540
|
+
}));
|
|
541
|
+
mcp.registerTool('get_cognito_overview', {
|
|
542
|
+
description: 'Returns all Cognito user pools with MFA configuration and every app client config: allowed auth flows, OAuth flows/scopes, callback URLs, token validity, and whether the client has a secret (SDK auth calls must send SECRET_HASH when true). Client secret values are never returned. Call this before writing any Cognito sign-in, sign-up, or token-refresh code to use the correct auth flow and client settings. Do NOT call to look up users or tokens — infrawise never reads user data.',
|
|
543
|
+
inputSchema: z.object({}),
|
|
544
|
+
}, logged('get_cognito_overview', async () => {
|
|
545
|
+
const pools = getUserPoolNodes(currentGraph);
|
|
546
|
+
return toText({
|
|
547
|
+
total: pools.length,
|
|
548
|
+
note: 'Client secret values and user data are never included.',
|
|
549
|
+
userPools: pools.map((p) => ({
|
|
550
|
+
name: p.name,
|
|
551
|
+
id: p.poolId,
|
|
552
|
+
mfaConfiguration: p.mfaConfiguration,
|
|
553
|
+
clients: p.clients ?? [],
|
|
554
|
+
})),
|
|
555
|
+
});
|
|
556
|
+
}));
|
|
557
|
+
mcp.registerTool('get_stack_outputs', {
|
|
558
|
+
description: 'Returns all stack outputs and cross-stack exports parsed from local IaC files: Terraform output blocks and CloudFormation/CDK Outputs sections, with name, description, export name, and the raw value expression. Call this when wiring cross-stack references (Fn::ImportValue, terraform_remote_state) or when you need the exported name of a resource defined in another stack. Do NOT call for live resource attributes — outputs come from local IaC files, not the deployed stack.',
|
|
559
|
+
inputSchema: z.object({}),
|
|
560
|
+
}, logged('get_stack_outputs', async () => {
|
|
561
|
+
const outputs = getStackOutputNodes(currentGraph);
|
|
562
|
+
return toText({
|
|
563
|
+
total: outputs.length,
|
|
564
|
+
outputs: outputs.map((o) => ({
|
|
565
|
+
name: o.name,
|
|
566
|
+
description: o.description,
|
|
567
|
+
exportName: o.exportName,
|
|
568
|
+
value: o.value,
|
|
569
|
+
source: o.iacSource,
|
|
570
|
+
file: o.file,
|
|
571
|
+
})),
|
|
572
|
+
});
|
|
573
|
+
}));
|
|
483
574
|
return mcp;
|
|
484
575
|
}
|
|
485
576
|
// ── Fastify server ────────────────────────────────────────────────────────────
|
|
@@ -519,6 +610,10 @@ export function createServer(port = 3000) {
|
|
|
519
610
|
'get_s3_overview',
|
|
520
611
|
'get_api_routes',
|
|
521
612
|
'get_log_errors',
|
|
613
|
+
'get_stack_outputs',
|
|
614
|
+
'get_cognito_overview',
|
|
615
|
+
'get_stream_details',
|
|
616
|
+
'get_cache_overview',
|
|
522
617
|
],
|
|
523
618
|
}));
|
|
524
619
|
fastify.post('/mcp', async (request, reply) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"mcpName": "io.github.Sidd27/infrawise",
|
|
5
5
|
"description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, API Gateway, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
|
|
6
6
|
"keywords": [
|
|
@@ -76,10 +76,15 @@
|
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@aws-sdk/client-api-gateway": "^3.1068.0",
|
|
78
78
|
"@aws-sdk/client-apigatewayv2": "^3.1068.0",
|
|
79
|
+
"@aws-sdk/client-cloudwatch": "^3.1079.0",
|
|
79
80
|
"@aws-sdk/client-cloudwatch-logs": "^3.1048.0",
|
|
81
|
+
"@aws-sdk/client-cognito-identity-provider": "^3.1079.0",
|
|
80
82
|
"@aws-sdk/client-dynamodb": "^3.1048.0",
|
|
83
|
+
"@aws-sdk/client-elasticache": "^3.1079.0",
|
|
81
84
|
"@aws-sdk/client-eventbridge": "^3.1051.0",
|
|
82
85
|
"@aws-sdk/client-iam": "^3.1073.0",
|
|
86
|
+
"@aws-sdk/client-kafka": "^3.1079.0",
|
|
87
|
+
"@aws-sdk/client-kinesis": "^3.1079.0",
|
|
83
88
|
"@aws-sdk/client-lambda": "^3.1048.0",
|
|
84
89
|
"@aws-sdk/client-rds": "^3.1048.0",
|
|
85
90
|
"@aws-sdk/client-s3": "^3.1048.0",
|