infrawise 0.13.2 → 0.15.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 +33 -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/db/mysql.js +37 -1
- package/dist/adapters/db/postgres.js +49 -2
- 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 +5 -0
- package/dist/core/config.js +18 -0
- package/dist/graph/index.js +96 -10
- package/dist/server/index.js +149 -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 21 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 21 infrawise tools are available in Cursor's MCP panel.
|
|
119
121
|
|
|
120
122
|
### Any editor (no flag)
|
|
121
123
|
|
|
@@ -151,6 +153,7 @@ Add to your editor's MCP config:
|
|
|
151
153
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
152
154
|
| `get_infra_overview` | Complete snapshot — services, counts, high-severity findings, analysis `freshness` (age + stale flag), `configured` flag |
|
|
153
155
|
| `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
|
|
156
|
+
| `get_table_schema` | Column-level schema for named tables/collections — types, PKs, FKs, indexes, DynamoDB keys (no row data) |
|
|
154
157
|
| `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes, missing IAM permissions |
|
|
155
158
|
| `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
|
|
156
159
|
| `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
|
|
@@ -165,6 +168,10 @@ Add to your editor's MCP config:
|
|
|
165
168
|
| `get_eventbridge_details` | EventBridge rules — name, state, schedule/event pattern, target functions |
|
|
166
169
|
| `get_s3_overview` | S3 buckets — versioning, encryption, public access, event notifications |
|
|
167
170
|
| `get_log_errors` | CloudWatch error patterns and counts (no raw log messages) |
|
|
171
|
+
| `get_stack_outputs` | Stack outputs and cross-stack exports parsed from local IaC files (Terraform outputs, CFN/CDK Outputs) |
|
|
172
|
+
| `get_cognito_overview` | Cognito user pools — MFA config, app client auth flows, OAuth settings, token validity (secrets never included) |
|
|
173
|
+
| `get_stream_details` | Kinesis streams (shards, retention, capacity mode) and MSK clusters (state, Kafka version, brokers) |
|
|
174
|
+
| `get_cache_overview` | ElastiCache clusters — engine, encryption in transit/at rest, replication group, failover (data never read) |
|
|
168
175
|
|
|
169
176
|
---
|
|
170
177
|
|
|
@@ -301,6 +308,22 @@ s3:
|
|
|
301
308
|
apiGateway:
|
|
302
309
|
enabled: false
|
|
303
310
|
|
|
311
|
+
cognito:
|
|
312
|
+
enabled: false
|
|
313
|
+
|
|
314
|
+
kinesis:
|
|
315
|
+
enabled: false
|
|
316
|
+
|
|
317
|
+
msk:
|
|
318
|
+
enabled: false
|
|
319
|
+
|
|
320
|
+
elasticache:
|
|
321
|
+
enabled: false
|
|
322
|
+
|
|
323
|
+
runtimeSignals:
|
|
324
|
+
enabled: false # Lambda throttles/errors + queue age via CloudWatch metrics
|
|
325
|
+
windowHours: 24
|
|
326
|
+
|
|
304
327
|
cloudwatchLogs:
|
|
305
328
|
enabled: false
|
|
306
329
|
logGroupPrefixes: []
|
|
@@ -374,7 +397,11 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
|
|
|
374
397
|
| API Gateway | REST, HTTP, and WebSocket APIs — routes, methods, Lambda integrations |
|
|
375
398
|
| RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
|
|
376
399
|
| CloudWatch Logs | Log groups with no retention policy |
|
|
377
|
-
|
|
|
400
|
+
| Cognito | User pools and app client config — auth flows, OAuth settings, token validity, client secret presence |
|
|
401
|
+
| Kinesis / MSK | Streams (shards, retention, capacity mode) and MSK clusters (state, Kafka version, brokers) |
|
|
402
|
+
| ElastiCache | Missing in-transit encryption, single-node clusters with no replication |
|
|
403
|
+
| Runtime signals (opt-in) | Lambda throttling/errors and stale queue messages from CloudWatch metrics |
|
|
404
|
+
| Terraform / CloudFormation / CDK | IaC drift vs deployed state; stack outputs and cross-stack exports |
|
|
378
405
|
|
|
379
406
|
### Code correlation analysis (TypeScript / JavaScript)
|
|
380
407
|
|
|
@@ -446,7 +473,7 @@ src/
|
|
|
446
473
|
aws/ DynamoDB, S3, Lambda, SQS/SNS/SSM/Secrets/EventBridge/RDS/APIGateway, CloudWatch
|
|
447
474
|
db/ PostgreSQL, MySQL, MongoDB
|
|
448
475
|
iac/ Terraform, CDK, CloudFormation (local file parsing)
|
|
449
|
-
analyzers/
|
|
476
|
+
analyzers/ 34 rule-based analyzers
|
|
450
477
|
context/ Repository scanner (ts-morph AST)
|
|
451
478
|
server/ Fastify MCP server (@modelcontextprotocol/sdk, Streamable HTTP)
|
|
452
479
|
cli/ CLI commands (Commander.js)
|
|
@@ -473,6 +500,8 @@ Feature roadmap is tracked in the [GitHub Project](https://github.com/users/Sidd
|
|
|
473
500
|
|
|
474
501
|
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
502
|
|
|
503
|
+

|
|
504
|
+
|
|
476
505
|
---
|
|
477
506
|
|
|
478
507
|
## 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));
|
|
@@ -28,10 +28,19 @@ export async function extractMySQLMetadata(connectionString) {
|
|
|
28
28
|
logger.debug(`Found ${tableRows.length} MySQL table(s)`);
|
|
29
29
|
// Get all columns
|
|
30
30
|
const [columnRows] = await connection.execute(`
|
|
31
|
-
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME
|
|
31
|
+
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE
|
|
32
32
|
FROM information_schema.columns
|
|
33
33
|
WHERE TABLE_SCHEMA NOT IN (${[...SYSTEM_SCHEMAS].map(() => '?').join(', ')})
|
|
34
34
|
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
|
|
35
|
+
`, [...SYSTEM_SCHEMAS]);
|
|
36
|
+
// Get foreign keys
|
|
37
|
+
const [fkRows] = await connection.execute(`
|
|
38
|
+
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME,
|
|
39
|
+
REFERENCED_TABLE_SCHEMA, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
|
40
|
+
FROM information_schema.key_column_usage
|
|
41
|
+
WHERE REFERENCED_TABLE_NAME IS NOT NULL
|
|
42
|
+
AND TABLE_SCHEMA NOT IN (${[...SYSTEM_SCHEMAS].map(() => '?').join(', ')})
|
|
43
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
|
|
35
44
|
`, [...SYSTEM_SCHEMAS]);
|
|
36
45
|
// Get all indexes
|
|
37
46
|
const [indexRows] = await connection.execute(`
|
|
@@ -51,6 +60,7 @@ export async function extractMySQLMetadata(connectionString) {
|
|
|
51
60
|
`, [...SYSTEM_SCHEMAS]);
|
|
52
61
|
// Build lookup maps
|
|
53
62
|
const columnMap = new Map();
|
|
63
|
+
const columnDetailMap = new Map();
|
|
54
64
|
for (const row of columnRows) {
|
|
55
65
|
const key = `${row['TABLE_SCHEMA']}.${row['TABLE_NAME']}`;
|
|
56
66
|
let cols = columnMap.get(key);
|
|
@@ -59,6 +69,30 @@ export async function extractMySQLMetadata(connectionString) {
|
|
|
59
69
|
columnMap.set(key, cols);
|
|
60
70
|
}
|
|
61
71
|
cols.push(row['COLUMN_NAME']);
|
|
72
|
+
let details = columnDetailMap.get(key);
|
|
73
|
+
if (!details) {
|
|
74
|
+
details = [];
|
|
75
|
+
columnDetailMap.set(key, details);
|
|
76
|
+
}
|
|
77
|
+
details.push({
|
|
78
|
+
name: row['COLUMN_NAME'],
|
|
79
|
+
dataType: row['DATA_TYPE'],
|
|
80
|
+
nullable: row['IS_NULLABLE'] === 'YES',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const fkMap = new Map();
|
|
84
|
+
for (const row of fkRows) {
|
|
85
|
+
const key = `${row['TABLE_SCHEMA']}.${row['TABLE_NAME']}`;
|
|
86
|
+
let fks = fkMap.get(key);
|
|
87
|
+
if (!fks) {
|
|
88
|
+
fks = [];
|
|
89
|
+
fkMap.set(key, fks);
|
|
90
|
+
}
|
|
91
|
+
fks.push({
|
|
92
|
+
column: row['COLUMN_NAME'],
|
|
93
|
+
referencesTable: `${row['REFERENCED_TABLE_SCHEMA']}.${row['REFERENCED_TABLE_NAME']}`,
|
|
94
|
+
referencesColumn: row['REFERENCED_COLUMN_NAME'],
|
|
95
|
+
});
|
|
62
96
|
}
|
|
63
97
|
const indexMap = new Map();
|
|
64
98
|
for (const row of indexRows) {
|
|
@@ -96,6 +130,8 @@ export async function extractMySQLMetadata(connectionString) {
|
|
|
96
130
|
indexes: indexMap.get(key) ?? [],
|
|
97
131
|
primaryKeys: pkMap.get(key) ?? [],
|
|
98
132
|
engine,
|
|
133
|
+
columnDetails: columnDetailMap.get(key) ?? [],
|
|
134
|
+
foreignKeys: fkMap.get(key) ?? [],
|
|
99
135
|
});
|
|
100
136
|
}
|
|
101
137
|
return results;
|
|
@@ -22,7 +22,7 @@ export async function extractPostgresMetadata(connectionString) {
|
|
|
22
22
|
logger.debug(`Found ${tablesResult.rows.length} PostgreSQL table(s)`);
|
|
23
23
|
// Get all columns
|
|
24
24
|
const columnsResult = await client.query(`
|
|
25
|
-
SELECT table_schema, table_name, column_name
|
|
25
|
+
SELECT table_schema, table_name, column_name, data_type, is_nullable
|
|
26
26
|
FROM information_schema.columns
|
|
27
27
|
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
28
28
|
ORDER BY table_schema, table_name, ordinal_position
|
|
@@ -48,8 +48,29 @@ export async function extractPostgresMetadata(connectionString) {
|
|
|
48
48
|
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
49
49
|
ORDER BY tc.table_schema, tc.table_name
|
|
50
50
|
`);
|
|
51
|
-
//
|
|
51
|
+
// Get foreign keys
|
|
52
|
+
const foreignKeysResult = await client.query(`
|
|
53
|
+
SELECT
|
|
54
|
+
tc.table_schema,
|
|
55
|
+
tc.table_name,
|
|
56
|
+
kcu.column_name,
|
|
57
|
+
ccu.table_schema AS ref_schema,
|
|
58
|
+
ccu.table_name AS ref_table,
|
|
59
|
+
ccu.column_name AS ref_column
|
|
60
|
+
FROM information_schema.table_constraints tc
|
|
61
|
+
JOIN information_schema.key_column_usage kcu
|
|
62
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
63
|
+
AND tc.table_schema = kcu.table_schema
|
|
64
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
65
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
66
|
+
AND tc.table_schema = ccu.table_schema
|
|
67
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
68
|
+
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
69
|
+
ORDER BY tc.table_schema, tc.table_name
|
|
70
|
+
`);
|
|
71
|
+
// Build maps for columns, indexes, primary keys, foreign keys
|
|
52
72
|
const columnMap = new Map();
|
|
73
|
+
const columnDetailMap = new Map();
|
|
53
74
|
for (const row of columnsResult.rows) {
|
|
54
75
|
const key = `${row.table_schema}.${row.table_name}`;
|
|
55
76
|
let cols = columnMap.get(key);
|
|
@@ -58,6 +79,30 @@ export async function extractPostgresMetadata(connectionString) {
|
|
|
58
79
|
columnMap.set(key, cols);
|
|
59
80
|
}
|
|
60
81
|
cols.push(row.column_name);
|
|
82
|
+
let details = columnDetailMap.get(key);
|
|
83
|
+
if (!details) {
|
|
84
|
+
details = [];
|
|
85
|
+
columnDetailMap.set(key, details);
|
|
86
|
+
}
|
|
87
|
+
details.push({
|
|
88
|
+
name: row.column_name,
|
|
89
|
+
dataType: row.data_type,
|
|
90
|
+
nullable: row.is_nullable === 'YES',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const fkMap = new Map();
|
|
94
|
+
for (const row of foreignKeysResult.rows) {
|
|
95
|
+
const key = `${row.table_schema}.${row.table_name}`;
|
|
96
|
+
let fks = fkMap.get(key);
|
|
97
|
+
if (!fks) {
|
|
98
|
+
fks = [];
|
|
99
|
+
fkMap.set(key, fks);
|
|
100
|
+
}
|
|
101
|
+
fks.push({
|
|
102
|
+
column: row.column_name,
|
|
103
|
+
referencesTable: `${row.ref_schema}.${row.ref_table}`,
|
|
104
|
+
referencesColumn: row.ref_column,
|
|
105
|
+
});
|
|
61
106
|
}
|
|
62
107
|
const indexMap = new Map();
|
|
63
108
|
for (const row of indexesResult.rows) {
|
|
@@ -89,6 +134,8 @@ export async function extractPostgresMetadata(connectionString) {
|
|
|
89
134
|
columns: columnMap.get(key) ?? [],
|
|
90
135
|
indexes: indexMap.get(key) ?? [],
|
|
91
136
|
primaryKeys: pkMap.get(key) ?? [],
|
|
137
|
+
columnDetails: columnDetailMap.get(key) ?? [],
|
|
138
|
+
foreignKeys: fkMap.get(key) ?? [],
|
|
92
139
|
});
|
|
93
140
|
}
|
|
94
141
|
return results;
|