infrawise 0.8.2 → 0.8.7
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 +1 -47
- package/dist/adapters/aws/logs.js +3 -1
- package/dist/adapters/aws/s3.js +37 -7
- package/dist/adapters/aws/services.js +28 -9
- package/dist/adapters/db/mongodb.js +5 -4
- package/dist/adapters/db/mysql.js +4 -1
- package/dist/adapters/iac/terraform.js +52 -25
- package/dist/analyzers/aws-services.js +5 -1
- package/dist/analyzers/rds.js +10 -2
- package/dist/analyzers/terraform.js +18 -3
- package/dist/cli/commands/analyze.js +161 -107
- package/dist/cli/commands/auth.js +4 -1
- package/dist/cli/commands/dev.js +6 -3
- package/dist/cli/commands/doctor.js +64 -24
- package/dist/cli/commands/init.js +25 -7
- package/dist/cli/utils.js +23 -9
- package/dist/context/index.js +36 -9
- package/dist/core/cache.js +12 -9
- package/dist/core/config.js +38 -16
- package/dist/core/index.js +2 -2
- package/dist/graph/index.js +63 -9
- package/dist/server/index.js +170 -51
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -404,53 +404,7 @@ Infrawise does not use an LLM to analyze your infrastructure. All extraction and
|
|
|
404
404
|
|
|
405
405
|
## Architecture overview
|
|
406
406
|
|
|
407
|
-
|
|
408
|
-
flowchart LR
|
|
409
|
-
subgraph IN["Your Infrastructure & Code"]
|
|
410
|
-
direction TB
|
|
411
|
-
D["DynamoDB"]
|
|
412
|
-
L["Lambda · SQS · SNS\nEventBridge · RDS"]
|
|
413
|
-
S["Secrets Manager · SSM\nCloudWatch"]
|
|
414
|
-
P["PostgreSQL · MySQL"]
|
|
415
|
-
M["MongoDB"]
|
|
416
|
-
T["Terraform · CDK\nCloudFormation"]
|
|
417
|
-
C["TypeScript / JS"]
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
A["Adapters"]
|
|
421
|
-
G["Graph Engine"]
|
|
422
|
-
AN["23 Analyzers"]
|
|
423
|
-
CA["Cache"]
|
|
424
|
-
|
|
425
|
-
subgraph SV["infrawise dev"]
|
|
426
|
-
MCP["MCP Server\nlocalhost:3000/mcp"]
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
subgraph AI["AI Coding Assistants"]
|
|
430
|
-
direction TB
|
|
431
|
-
CC["Claude Code"]
|
|
432
|
-
CU["Cursor"]
|
|
433
|
-
WS["Windsurf"]
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
D & L & S & P & M & T & C --> A
|
|
437
|
-
A --> G --> AN --> CA --> MCP
|
|
438
|
-
MCP --> CC & CU & WS
|
|
439
|
-
|
|
440
|
-
classDef aws fill:#FF9900,stroke:#232F3E,color:#000
|
|
441
|
-
classDef db fill:#336791,stroke:#1a3a5c,color:#fff
|
|
442
|
-
classDef iac fill:#7B42BC,stroke:#4a2080,color:#fff
|
|
443
|
-
classDef code fill:#3178C6,stroke:#1a4a80,color:#fff
|
|
444
|
-
classDef iw fill:#1a1a2e,stroke:#e94560,color:#fff
|
|
445
|
-
classDef ai fill:#10a37f,stroke:#0a6b54,color:#fff
|
|
446
|
-
|
|
447
|
-
class D,L,S aws
|
|
448
|
-
class P,M db
|
|
449
|
-
class T iac
|
|
450
|
-
class C code
|
|
451
|
-
class A,G,AN,CA,MCP iw
|
|
452
|
-
class CC,CU,WS ai
|
|
453
|
-
```
|
|
407
|
+

|
|
454
408
|
|
|
455
409
|
### Source layout
|
|
456
410
|
|
|
@@ -43,7 +43,9 @@ export async function extractLogsMetadata(cfg = {}) {
|
|
|
43
43
|
// Discover log groups
|
|
44
44
|
const logGroups = [];
|
|
45
45
|
try {
|
|
46
|
-
const prefixes = cfg.logGroupPrefixes?.length
|
|
46
|
+
const prefixes = cfg.logGroupPrefixes?.length
|
|
47
|
+
? cfg.logGroupPrefixes
|
|
48
|
+
: [undefined];
|
|
47
49
|
for (const prefix of prefixes) {
|
|
48
50
|
let nextToken;
|
|
49
51
|
do {
|
package/dist/adapters/aws/s3.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { S3Client, ListBucketsCommand, GetBucketNotificationConfigurationCommand, GetBucketVersioningCommand, GetBucketEncryptionCommand, GetPublicAccessBlockCommand, } from '@aws-sdk/client-s3';
|
|
2
2
|
import { fromIni } from '@aws-sdk/credential-providers';
|
|
3
3
|
import { logger } from '../../core/index.js';
|
|
4
|
+
function validateEndpoint(endpoint) {
|
|
5
|
+
let url;
|
|
6
|
+
try {
|
|
7
|
+
url = new URL(endpoint);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error(`Invalid aws.endpoint URL: "${endpoint}"`);
|
|
11
|
+
}
|
|
12
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
13
|
+
throw new Error(`aws.endpoint must use http:// or https://, got "${url.protocol}//"`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
4
16
|
function clientConfig(cfg) {
|
|
5
17
|
const region = cfg.region ?? 'us-east-1';
|
|
6
18
|
const base = { region };
|
|
7
|
-
if (cfg.endpoint)
|
|
19
|
+
if (cfg.endpoint) {
|
|
20
|
+
validateEndpoint(cfg.endpoint);
|
|
8
21
|
base.endpoint = cfg.endpoint;
|
|
22
|
+
}
|
|
9
23
|
if (cfg.profile)
|
|
10
24
|
base.credentials = fromIni({ profile: cfg.profile });
|
|
11
25
|
return base;
|
|
@@ -48,18 +62,34 @@ export async function extractS3Metadata(cfg = {}) {
|
|
|
48
62
|
notifications.push(notification);
|
|
49
63
|
}
|
|
50
64
|
}
|
|
51
|
-
const versioned = versionResult.status === 'fulfilled'
|
|
52
|
-
? versionResult.value.Status === 'Enabled'
|
|
53
|
-
: false;
|
|
65
|
+
const versioned = versionResult.status === 'fulfilled' ? versionResult.value.Status === 'Enabled' : false;
|
|
54
66
|
const encrypted = encryptResult.status === 'fulfilled'
|
|
55
67
|
? (encryptResult.value.ServerSideEncryptionConfiguration?.Rules?.length ?? 0) > 0
|
|
56
68
|
: false;
|
|
57
|
-
let publicAccessBlocked =
|
|
69
|
+
let publicAccessBlocked = null;
|
|
58
70
|
if (pabResult.status === 'fulfilled') {
|
|
59
71
|
const pab = pabResult.value.PublicAccessBlockConfiguration ?? {};
|
|
60
|
-
publicAccessBlocked = !!(pab.BlockPublicAcls &&
|
|
72
|
+
publicAccessBlocked = !!(pab.BlockPublicAcls &&
|
|
73
|
+
pab.IgnorePublicAcls &&
|
|
74
|
+
pab.BlockPublicPolicy &&
|
|
75
|
+
pab.RestrictPublicBuckets);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const httpStatus = pabResult.reason
|
|
79
|
+
?.$metadata?.httpStatusCode;
|
|
80
|
+
if (httpStatus !== 403)
|
|
81
|
+
publicAccessBlocked = false;
|
|
82
|
+
// 403 AccessDenied: leave as null — insufficient permissions, not a public access finding
|
|
61
83
|
}
|
|
62
|
-
buckets.push({
|
|
84
|
+
buckets.push({
|
|
85
|
+
name,
|
|
86
|
+
arn,
|
|
87
|
+
createdAt,
|
|
88
|
+
versioned,
|
|
89
|
+
encrypted,
|
|
90
|
+
publicAccessBlocked,
|
|
91
|
+
notifications,
|
|
92
|
+
});
|
|
63
93
|
}
|
|
64
94
|
}
|
|
65
95
|
catch (err) {
|
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
import { SQSClient, ListQueuesCommand, GetQueueAttributesCommand
|
|
1
|
+
import { SQSClient, ListQueuesCommand, GetQueueAttributesCommand } from '@aws-sdk/client-sqs';
|
|
2
2
|
import { SNSClient, ListTopicsCommand, GetTopicAttributesCommand, ListSubscriptionsByTopicCommand, } from '@aws-sdk/client-sns';
|
|
3
|
-
import { SSMClient, DescribeParametersCommand
|
|
4
|
-
import { SecretsManagerClient, ListSecretsCommand
|
|
3
|
+
import { SSMClient, DescribeParametersCommand } from '@aws-sdk/client-ssm';
|
|
4
|
+
import { SecretsManagerClient, ListSecretsCommand } from '@aws-sdk/client-secrets-manager';
|
|
5
5
|
import { LambdaClient, ListFunctionsCommand, ListEventSourceMappingsCommand, } from '@aws-sdk/client-lambda';
|
|
6
6
|
import { EventBridgeClient, ListRulesCommand, ListTargetsByRuleCommand, } from '@aws-sdk/client-eventbridge';
|
|
7
|
-
import { RDSClient, DescribeDBInstancesCommand
|
|
7
|
+
import { RDSClient, DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
|
|
8
8
|
import { fromIni } from '@aws-sdk/credential-providers';
|
|
9
9
|
import { logger } from '../../core/index.js';
|
|
10
|
+
function validateEndpoint(endpoint) {
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(endpoint);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error(`Invalid aws.endpoint URL: "${endpoint}"`);
|
|
17
|
+
}
|
|
18
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
19
|
+
throw new Error(`aws.endpoint must use http:// or https://, got "${url.protocol}//"`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
function clientConfig(cfg) {
|
|
11
23
|
const region = cfg.region ?? 'us-east-1';
|
|
12
24
|
const base = { region };
|
|
13
|
-
if (cfg.endpoint)
|
|
25
|
+
if (cfg.endpoint) {
|
|
26
|
+
validateEndpoint(cfg.endpoint);
|
|
14
27
|
base.endpoint = cfg.endpoint;
|
|
28
|
+
}
|
|
15
29
|
if (cfg.profile)
|
|
16
30
|
base.credentials = fromIni({ profile: cfg.profile });
|
|
17
31
|
return base;
|
|
@@ -33,9 +47,14 @@ export async function extractSQSMetadata(cfg = {}) {
|
|
|
33
47
|
const attrs = await client.send(new GetQueueAttributesCommand({
|
|
34
48
|
QueueUrl: url,
|
|
35
49
|
AttributeNames: [
|
|
36
|
-
'QueueArn',
|
|
37
|
-
'
|
|
38
|
-
'
|
|
50
|
+
'QueueArn',
|
|
51
|
+
'VisibilityTimeout',
|
|
52
|
+
'MessageRetentionPeriod',
|
|
53
|
+
'RedrivePolicy',
|
|
54
|
+
'KmsMasterKeyId',
|
|
55
|
+
'SqsManagedSseEnabled',
|
|
56
|
+
'ApproximateNumberOfMessages',
|
|
57
|
+
'ApproximateNumberOfMessagesNotVisible',
|
|
39
58
|
],
|
|
40
59
|
}));
|
|
41
60
|
const a = attrs.Attributes ?? {};
|
|
@@ -293,7 +312,7 @@ export async function extractLambdaMetadata(cfg = {}, includeFunctions) {
|
|
|
293
312
|
});
|
|
294
313
|
}
|
|
295
314
|
marker = res.NextMarker;
|
|
296
|
-
} while (marker
|
|
315
|
+
} while (marker);
|
|
297
316
|
// Fetch all event source mappings in one paginated call and attach to functions
|
|
298
317
|
const triggerMap = await fetchAllEventSourceMappings(cfg);
|
|
299
318
|
for (const fn of functions) {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { MongoClient } from 'mongodb';
|
|
2
2
|
import { InfrawiseError, logger } from '../../core/index.js';
|
|
3
3
|
const SYSTEM_DATABASES = new Set(['admin', 'local', 'config']);
|
|
4
|
+
function sanitizeConnectionDetail(s) {
|
|
5
|
+
return s.replace(/\/\/[^:/@]+:[^@]+@/g, '//***:***@');
|
|
6
|
+
}
|
|
4
7
|
export class MongoConnectionError extends InfrawiseError {
|
|
5
8
|
constructor(details) {
|
|
6
9
|
super('Unable to connect to MongoDB.\n\nPossible reasons:\n- invalid connection string\n- port 27017 not accessible\n- wrong credentials\n\nRun: infrawise doctor', undefined, undefined);
|
|
7
10
|
this.name = 'MongoConnectionError';
|
|
8
11
|
if (details) {
|
|
9
|
-
this.message = `Unable to connect to MongoDB.\n\nPossible reasons:\n- invalid connection string\n- port 27017 not accessible\n- wrong credentials\n\nRun: infrawise doctor\n\nDetail: ${details}`;
|
|
12
|
+
this.message = `Unable to connect to MongoDB.\n\nPossible reasons:\n- invalid connection string\n- port 27017 not accessible\n- wrong credentials\n\nRun: infrawise doctor\n\nDetail: ${sanitizeConnectionDetail(details)}`;
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
}
|
|
@@ -25,9 +28,7 @@ export async function extractMongoMetadata(connectionString, databases) {
|
|
|
25
28
|
else {
|
|
26
29
|
const adminDb = client.db('admin');
|
|
27
30
|
const dbList = await adminDb.admin().listDatabases();
|
|
28
|
-
dbNames = dbList.databases
|
|
29
|
-
.map((d) => d.name)
|
|
30
|
-
.filter((name) => !SYSTEM_DATABASES.has(name));
|
|
31
|
+
dbNames = dbList.databases.map((d) => d.name).filter((name) => !SYSTEM_DATABASES.has(name));
|
|
31
32
|
}
|
|
32
33
|
logger.debug(`Introspecting ${dbNames.length} MongoDB database(s)`);
|
|
33
34
|
const results = [];
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import mysql from 'mysql2/promise';
|
|
2
2
|
import { InfrawiseError, logger } from '../../core/index.js';
|
|
3
3
|
const SYSTEM_SCHEMAS = new Set(['information_schema', 'performance_schema', 'mysql', 'sys']);
|
|
4
|
+
function sanitizeConnectionDetail(s) {
|
|
5
|
+
return s.replace(/\/\/[^:/@]+:[^@]+@/g, '//***:***@');
|
|
6
|
+
}
|
|
4
7
|
export class MySQLConnectionError extends InfrawiseError {
|
|
5
8
|
constructor(details) {
|
|
6
9
|
super('Unable to connect to MySQL.\n\nPossible reasons:\n- invalid connection string\n- port 3306 not accessible\n- wrong credentials\n\nRun: infrawise doctor', undefined, undefined);
|
|
7
10
|
this.name = 'MySQLConnectionError';
|
|
8
11
|
if (details) {
|
|
9
|
-
this.message = `Unable to connect to MySQL.\n\nPossible reasons:\n- invalid connection string\n- port 3306 not accessible\n- wrong credentials\n\nRun: infrawise doctor\n\nDetail: ${details}`;
|
|
12
|
+
this.message = `Unable to connect to MySQL.\n\nPossible reasons:\n- invalid connection string\n- port 3306 not accessible\n- wrong credentials\n\nRun: infrawise doctor\n\nDetail: ${sanitizeConnectionDetail(details)}`;
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
}
|
|
@@ -96,7 +96,8 @@ export async function extractTerraformSchema(repoPath) {
|
|
|
96
96
|
partitionKey: tfStr(body, 'hash_key'),
|
|
97
97
|
sortKey: tfStr(body, 'range_key'),
|
|
98
98
|
gsiNames: tfGSINames(body),
|
|
99
|
-
source: 'terraform',
|
|
99
|
+
source: 'terraform',
|
|
100
|
+
filePath,
|
|
100
101
|
});
|
|
101
102
|
break;
|
|
102
103
|
case 'aws_db_instance':
|
|
@@ -104,13 +105,15 @@ export async function extractTerraformSchema(repoPath) {
|
|
|
104
105
|
schema.rdsInstances.push({
|
|
105
106
|
identifier: tfStr(body, 'identifier') ?? tfStr(body, 'cluster_identifier') ?? resourceName,
|
|
106
107
|
engine: tfStr(body, 'engine') ?? 'unknown',
|
|
107
|
-
source: 'terraform',
|
|
108
|
+
source: 'terraform',
|
|
109
|
+
filePath,
|
|
108
110
|
});
|
|
109
111
|
break;
|
|
110
112
|
case 'aws_docdb_cluster':
|
|
111
113
|
schema.mongoClusters.push({
|
|
112
114
|
identifier: tfStr(body, 'cluster_identifier') ?? resourceName,
|
|
113
|
-
source: 'terraform',
|
|
115
|
+
source: 'terraform',
|
|
116
|
+
filePath,
|
|
114
117
|
});
|
|
115
118
|
break;
|
|
116
119
|
case 'aws_sqs_queue': {
|
|
@@ -124,41 +127,47 @@ export async function extractTerraformSchema(repoPath) {
|
|
|
124
127
|
schema.topics.push({
|
|
125
128
|
name: tfStr(body, 'name') ?? resourceName,
|
|
126
129
|
encrypted: body.includes('kms_master_key_id'),
|
|
127
|
-
source: 'terraform',
|
|
130
|
+
source: 'terraform',
|
|
131
|
+
filePath,
|
|
128
132
|
});
|
|
129
133
|
break;
|
|
130
134
|
case 'aws_lambda_function':
|
|
131
135
|
schema.lambdas.push({
|
|
132
136
|
name: tfStr(body, 'function_name') ?? resourceName,
|
|
133
137
|
runtime: tfStr(body, 'runtime'),
|
|
134
|
-
source: 'terraform',
|
|
138
|
+
source: 'terraform',
|
|
139
|
+
filePath,
|
|
135
140
|
});
|
|
136
141
|
break;
|
|
137
142
|
case 'aws_s3_bucket':
|
|
138
143
|
schema.buckets.push({
|
|
139
144
|
name: tfStr(body, 'bucket') ?? tfStr(body, 'bucket_prefix') ?? resourceName,
|
|
140
145
|
versioned: body.includes('versioning') && body.includes('enabled = true'),
|
|
141
|
-
source: 'terraform',
|
|
146
|
+
source: 'terraform',
|
|
147
|
+
filePath,
|
|
142
148
|
});
|
|
143
149
|
break;
|
|
144
150
|
case 'aws_ssm_parameter':
|
|
145
151
|
schema.parameters.push({
|
|
146
152
|
name: tfStr(body, 'name') ?? resourceName,
|
|
147
153
|
type: tfStr(body, 'type') ?? 'String',
|
|
148
|
-
source: 'terraform',
|
|
154
|
+
source: 'terraform',
|
|
155
|
+
filePath,
|
|
149
156
|
});
|
|
150
157
|
break;
|
|
151
158
|
case 'aws_secretsmanager_secret':
|
|
152
159
|
schema.secrets.push({
|
|
153
160
|
name: tfStr(body, 'name') ?? resourceName,
|
|
154
|
-
source: 'terraform',
|
|
161
|
+
source: 'terraform',
|
|
162
|
+
filePath,
|
|
155
163
|
});
|
|
156
164
|
break;
|
|
157
165
|
case 'aws_api_gateway_rest_api':
|
|
158
166
|
case 'aws_apigatewayv2_api':
|
|
159
167
|
schema.apiGateways.push({
|
|
160
168
|
name: tfStr(body, 'name') ?? resourceName,
|
|
161
|
-
source: 'terraform',
|
|
169
|
+
source: 'terraform',
|
|
170
|
+
filePath,
|
|
162
171
|
});
|
|
163
172
|
break;
|
|
164
173
|
}
|
|
@@ -171,7 +180,8 @@ function isCloudFormationTemplate(parsed) {
|
|
|
171
180
|
if (typeof parsed !== 'object' || parsed === null)
|
|
172
181
|
return false;
|
|
173
182
|
const obj = parsed;
|
|
174
|
-
return 'AWSTemplateFormatVersion' in obj ||
|
|
183
|
+
return ('AWSTemplateFormatVersion' in obj ||
|
|
184
|
+
('Resources' in obj && typeof obj['Resources'] === 'object'));
|
|
175
185
|
}
|
|
176
186
|
function parseCFNFile(filePath) {
|
|
177
187
|
let content;
|
|
@@ -242,7 +252,8 @@ function processCFNResources(resources, schema, filePath, source) {
|
|
|
242
252
|
partitionKey: pk,
|
|
243
253
|
sortKey: sk,
|
|
244
254
|
gsiNames,
|
|
245
|
-
source,
|
|
255
|
+
source,
|
|
256
|
+
filePath,
|
|
246
257
|
});
|
|
247
258
|
break;
|
|
248
259
|
}
|
|
@@ -250,20 +261,23 @@ function processCFNResources(resources, schema, filePath, source) {
|
|
|
250
261
|
schema.rdsInstances.push({
|
|
251
262
|
identifier: cfnStr(props, 'DBInstanceIdentifier') ?? logicalId,
|
|
252
263
|
engine: cfnStr(props, 'Engine') ?? 'unknown',
|
|
253
|
-
source,
|
|
264
|
+
source,
|
|
265
|
+
filePath,
|
|
254
266
|
});
|
|
255
267
|
break;
|
|
256
268
|
case 'AWS::RDS::DBCluster':
|
|
257
269
|
schema.rdsInstances.push({
|
|
258
270
|
identifier: cfnStr(props, 'DBClusterIdentifier') ?? logicalId,
|
|
259
271
|
engine: cfnStr(props, 'Engine') ?? 'aurora',
|
|
260
|
-
source,
|
|
272
|
+
source,
|
|
273
|
+
filePath,
|
|
261
274
|
});
|
|
262
275
|
break;
|
|
263
276
|
case 'AWS::DocDB::DBCluster':
|
|
264
277
|
schema.mongoClusters.push({
|
|
265
278
|
identifier: cfnStr(props, 'DBClusterIdentifier') ?? logicalId,
|
|
266
|
-
source,
|
|
279
|
+
source,
|
|
280
|
+
filePath,
|
|
267
281
|
});
|
|
268
282
|
break;
|
|
269
283
|
case 'AWS::SQS::Queue': {
|
|
@@ -277,14 +291,16 @@ function processCFNResources(resources, schema, filePath, source) {
|
|
|
277
291
|
schema.topics.push({
|
|
278
292
|
name: cfnStr(props, 'TopicName') ?? logicalId,
|
|
279
293
|
encrypted: !!props['KmsMasterKeyId'],
|
|
280
|
-
source,
|
|
294
|
+
source,
|
|
295
|
+
filePath,
|
|
281
296
|
});
|
|
282
297
|
break;
|
|
283
298
|
case 'AWS::Lambda::Function':
|
|
284
299
|
schema.lambdas.push({
|
|
285
300
|
name: cfnStr(props, 'FunctionName') ?? logicalId,
|
|
286
301
|
runtime: cfnStr(props, 'Runtime'),
|
|
287
|
-
source,
|
|
302
|
+
source,
|
|
303
|
+
filePath,
|
|
288
304
|
});
|
|
289
305
|
break;
|
|
290
306
|
case 'AWS::S3::Bucket': {
|
|
@@ -292,7 +308,8 @@ function processCFNResources(resources, schema, filePath, source) {
|
|
|
292
308
|
schema.buckets.push({
|
|
293
309
|
name: cfnStr(props, 'BucketName') ?? logicalId,
|
|
294
310
|
versioned: versioningConfig?.['Status'] === 'Enabled',
|
|
295
|
-
source,
|
|
311
|
+
source,
|
|
312
|
+
filePath,
|
|
296
313
|
});
|
|
297
314
|
break;
|
|
298
315
|
}
|
|
@@ -300,20 +317,23 @@ function processCFNResources(resources, schema, filePath, source) {
|
|
|
300
317
|
schema.parameters.push({
|
|
301
318
|
name: cfnStr(props, 'Name') ?? logicalId,
|
|
302
319
|
type: cfnStr(props, 'Type') ?? 'String',
|
|
303
|
-
source,
|
|
320
|
+
source,
|
|
321
|
+
filePath,
|
|
304
322
|
});
|
|
305
323
|
break;
|
|
306
324
|
case 'AWS::SecretsManager::Secret':
|
|
307
325
|
schema.secrets.push({
|
|
308
326
|
name: cfnStr(props, 'Name') ?? logicalId,
|
|
309
|
-
source,
|
|
327
|
+
source,
|
|
328
|
+
filePath,
|
|
310
329
|
});
|
|
311
330
|
break;
|
|
312
331
|
case 'AWS::ApiGateway::RestApi':
|
|
313
332
|
case 'AWS::ApiGatewayV2::Api':
|
|
314
333
|
schema.apiGateways.push({
|
|
315
334
|
name: cfnStr(props, 'Name') ?? logicalId,
|
|
316
|
-
source,
|
|
335
|
+
source,
|
|
336
|
+
filePath,
|
|
317
337
|
});
|
|
318
338
|
break;
|
|
319
339
|
}
|
|
@@ -340,7 +360,8 @@ export async function extractCDKSchema(repoPath) {
|
|
|
340
360
|
// Strategy 1: Parse cdk.out/*.template.json (synthesized CloudFormation)
|
|
341
361
|
const cdkOutDir = path.join(repoPath, 'cdk.out');
|
|
342
362
|
if (fs.existsSync(cdkOutDir)) {
|
|
343
|
-
const templateFiles = fs
|
|
363
|
+
const templateFiles = fs
|
|
364
|
+
.readdirSync(cdkOutDir)
|
|
344
365
|
.filter((f) => f.endsWith('.template.json'))
|
|
345
366
|
.map((f) => path.join(cdkOutDir, f));
|
|
346
367
|
logger.debug(`Found ${templateFiles.length} CDK synthesized template(s) in cdk.out/`);
|
|
@@ -459,10 +480,16 @@ export async function extractIaCSchema(repoPath) {
|
|
|
459
480
|
extractCDKSchema(repoPath),
|
|
460
481
|
]);
|
|
461
482
|
const merged = mergeSchemas(tfSchema, cfnSchema, cdkSchema);
|
|
462
|
-
const total = merged.dynamoTables.length +
|
|
463
|
-
merged.
|
|
464
|
-
merged.
|
|
465
|
-
merged.
|
|
483
|
+
const total = merged.dynamoTables.length +
|
|
484
|
+
merged.rdsInstances.length +
|
|
485
|
+
merged.mongoClusters.length +
|
|
486
|
+
merged.queues.length +
|
|
487
|
+
merged.topics.length +
|
|
488
|
+
merged.lambdas.length +
|
|
489
|
+
merged.buckets.length +
|
|
490
|
+
merged.parameters.length +
|
|
491
|
+
merged.secrets.length +
|
|
492
|
+
merged.apiGateways.length;
|
|
466
493
|
logger.debug(`IaC schema total: ${total} resource(s) across TF/CFN/CDK`);
|
|
467
494
|
return merged;
|
|
468
495
|
}
|
|
@@ -155,7 +155,11 @@ export class LambdaMissingTriggerDLQAnalyzer {
|
|
|
155
155
|
issue: `Lambda "${node.name}" is triggered by "${trigger.sourceName}" which has no DLQ`,
|
|
156
156
|
description: `"${node.name}" receives events from "${trigger.sourceName}" (${trigger.type.toUpperCase()}). If the Lambda handler fails, messages will be retried and eventually discarded with no failure record.`,
|
|
157
157
|
recommendation: `Add a DLQ to "${trigger.sourceName}" and set a destination config on the event source mapping so failed batches are captured and inspectable.`,
|
|
158
|
-
metadata: {
|
|
158
|
+
metadata: {
|
|
159
|
+
functionName: node.name,
|
|
160
|
+
triggerSource: trigger.sourceName,
|
|
161
|
+
triggerType: trigger.type,
|
|
162
|
+
},
|
|
159
163
|
});
|
|
160
164
|
}
|
|
161
165
|
}
|
package/dist/analyzers/rds.js
CHANGED
|
@@ -12,7 +12,11 @@ export class RDSPubliclyAccessibleAnalyzer {
|
|
|
12
12
|
issue: `RDS instance "${node.name}" is publicly accessible`,
|
|
13
13
|
description: `"${node.name}" (${node.engine}) has PubliclyAccessible=true, meaning it is reachable from the internet. This exposes the database to brute-force and credential-stuffing attacks.`,
|
|
14
14
|
recommendation: `Set PubliclyAccessible=false on "${node.name}" and use a bastion host, VPN, or VPC peering for private access. If public access is required, enforce strong passwords, IP allowlisting, and TLS.`,
|
|
15
|
-
metadata: {
|
|
15
|
+
metadata: {
|
|
16
|
+
dbInstanceIdentifier: node.name,
|
|
17
|
+
engine: node.engine,
|
|
18
|
+
instanceClass: node.instanceClass,
|
|
19
|
+
},
|
|
16
20
|
});
|
|
17
21
|
}
|
|
18
22
|
}
|
|
@@ -32,7 +36,11 @@ export class RDSNoBackupAnalyzer {
|
|
|
32
36
|
issue: `RDS instance "${node.name}" has automated backups disabled`,
|
|
33
37
|
description: `"${node.name}" has a backup retention period of 0, meaning automated backups are off. Any accidental deletion or corruption is unrecoverable without a manual snapshot.`,
|
|
34
38
|
recommendation: `Enable automated backups on "${node.name}" with at least 7 days retention (35 days for production workloads). Enable point-in-time recovery.`,
|
|
35
|
-
metadata: {
|
|
39
|
+
metadata: {
|
|
40
|
+
dbInstanceIdentifier: node.name,
|
|
41
|
+
engine: node.engine,
|
|
42
|
+
backupRetentionDays: node.backupRetentionDays,
|
|
43
|
+
},
|
|
36
44
|
});
|
|
37
45
|
}
|
|
38
46
|
}
|
|
@@ -21,7 +21,12 @@ export class IaCDriftAnalyzer {
|
|
|
21
21
|
issue: `IaC drift: DynamoDB table "${name}" defined in IaC but not deployed`,
|
|
22
22
|
description: `"${name}" is in ${fp} but not found in AWS. It may be undeployed or deleted manually.`,
|
|
23
23
|
recommendation: 'Run `terraform apply` / deploy your stack, or remove the definition from IaC.',
|
|
24
|
-
metadata: {
|
|
24
|
+
metadata: {
|
|
25
|
+
resourceType: 'dynamodb_table',
|
|
26
|
+
name,
|
|
27
|
+
filePath: fp,
|
|
28
|
+
driftType: 'defined_not_deployed',
|
|
29
|
+
},
|
|
25
30
|
});
|
|
26
31
|
}
|
|
27
32
|
}
|
|
@@ -46,7 +51,12 @@ export class IaCDriftAnalyzer {
|
|
|
46
51
|
issue: `IaC drift: SQS queue "${name}" defined in IaC but not deployed`,
|
|
47
52
|
description: `SQS queue "${name}" is defined in ${fp} but not found in the live account.`,
|
|
48
53
|
recommendation: 'Deploy the queue via `terraform apply` or your CFN/CDK stack.',
|
|
49
|
-
metadata: {
|
|
54
|
+
metadata: {
|
|
55
|
+
resourceType: 'sqs_queue',
|
|
56
|
+
name,
|
|
57
|
+
filePath: fp,
|
|
58
|
+
driftType: 'defined_not_deployed',
|
|
59
|
+
},
|
|
50
60
|
});
|
|
51
61
|
}
|
|
52
62
|
}
|
|
@@ -71,7 +81,12 @@ export class IaCDriftAnalyzer {
|
|
|
71
81
|
issue: `IaC drift: Lambda "${name}" defined in IaC but not deployed`,
|
|
72
82
|
description: `Lambda function "${name}" is defined in ${fp} but not found in the live account.`,
|
|
73
83
|
recommendation: 'Deploy the function via `terraform apply` or your CFN/CDK stack.',
|
|
74
|
-
metadata: {
|
|
84
|
+
metadata: {
|
|
85
|
+
resourceType: 'lambda_function',
|
|
86
|
+
name,
|
|
87
|
+
filePath: fp,
|
|
88
|
+
driftType: 'defined_not_deployed',
|
|
89
|
+
},
|
|
75
90
|
});
|
|
76
91
|
}
|
|
77
92
|
}
|