infrawise 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -74
- package/dist/adapters/aws.js +255 -0
- package/dist/adapters/dynamodb.js +97 -0
- package/dist/adapters/logs.js +119 -0
- package/dist/adapters/mongodb.js +109 -0
- package/dist/adapters/mysql.js +135 -0
- package/dist/adapters/postgres.js +131 -0
- package/dist/adapters/terraform.js +510 -0
- package/dist/analyzers/aws-services.js +168 -0
- package/dist/analyzers/dynamodb.js +144 -0
- package/dist/analyzers/index.js +59 -0
- package/dist/analyzers/mongodb.js +82 -0
- package/dist/analyzers/mysql.js +82 -0
- package/dist/analyzers/postgres.js +148 -0
- package/dist/analyzers/rds.js +109 -0
- package/dist/analyzers/terraform.js +95 -0
- package/dist/cli/commands/analyze.js +319 -0
- package/dist/cli/commands/auth.js +57 -0
- package/dist/cli/commands/dev.js +127 -0
- package/dist/cli/commands/doctor.js +338 -0
- package/dist/cli/commands/init.js +234 -0
- package/dist/cli/index.js +82 -0
- package/dist/cli/utils.js +165 -0
- package/dist/context/index.js +597 -0
- package/dist/core/cache.js +89 -0
- package/dist/core/config.js +163 -0
- package/dist/core/errors.js +97 -0
- package/dist/core/index.js +22 -0
- package/dist/core/logger.js +28 -0
- package/dist/graph/index.js +268 -0
- package/dist/server/index.js +348 -0
- package/dist/types.js +2 -0
- package/package.json +20 -30
- package/dist/index.js +0 -4800
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.extractTerraformSchema = extractTerraformSchema;
|
|
40
|
+
exports.extractCloudFormationSchema = extractCloudFormationSchema;
|
|
41
|
+
exports.extractCDKSchema = extractCDKSchema;
|
|
42
|
+
exports.extractIaCSchema = extractIaCSchema;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
46
|
+
const core_1 = require("../core");
|
|
47
|
+
function emptySchema() {
|
|
48
|
+
return {
|
|
49
|
+
dynamoTables: [],
|
|
50
|
+
rdsInstances: [],
|
|
51
|
+
mongoClusters: [],
|
|
52
|
+
queues: [],
|
|
53
|
+
topics: [],
|
|
54
|
+
lambdas: [],
|
|
55
|
+
buckets: [],
|
|
56
|
+
parameters: [],
|
|
57
|
+
secrets: [],
|
|
58
|
+
apiGateways: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// ─── File discovery ───────────────────────────────────────────────────────────
|
|
62
|
+
function findFilesRecursively(dir, extensions, skipDirs = new Set(['node_modules', '.git', 'dist', '.infrawise'])) {
|
|
63
|
+
const results = [];
|
|
64
|
+
if (!fs.existsSync(dir))
|
|
65
|
+
return results;
|
|
66
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (skipDirs.has(entry.name))
|
|
69
|
+
continue;
|
|
70
|
+
const fullPath = path.join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
results.push(...findFilesRecursively(fullPath, extensions, skipDirs));
|
|
73
|
+
}
|
|
74
|
+
else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
75
|
+
results.push(fullPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
// ─── Terraform HCL parser ─────────────────────────────────────────────────────
|
|
81
|
+
function extractTerraformResourceBlocks(content) {
|
|
82
|
+
const results = [];
|
|
83
|
+
const resourcePattern = /resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/g;
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = resourcePattern.exec(content)) !== null) {
|
|
86
|
+
const resourceType = match[1] ?? '';
|
|
87
|
+
const resourceName = match[2] ?? '';
|
|
88
|
+
const startBrace = match.index + match[0].length - 1;
|
|
89
|
+
let depth = 1;
|
|
90
|
+
let i = startBrace + 1;
|
|
91
|
+
while (i < content.length && depth > 0) {
|
|
92
|
+
if (content[i] === '{')
|
|
93
|
+
depth++;
|
|
94
|
+
else if (content[i] === '}')
|
|
95
|
+
depth--;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
results.push({ resourceType, resourceName, body: content.slice(startBrace + 1, i - 1) });
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
function tfStr(body, attr) {
|
|
103
|
+
const m = body.match(new RegExp(`${attr}\\s*=\\s*"([^"]*)"`, 'i'));
|
|
104
|
+
return m?.[1];
|
|
105
|
+
}
|
|
106
|
+
function tfBool(body, attr) {
|
|
107
|
+
const m = body.match(new RegExp(`${attr}\\s*=\\s*(true|false)`, 'i'));
|
|
108
|
+
return m?.[1] === 'true';
|
|
109
|
+
}
|
|
110
|
+
function tfGSINames(body) {
|
|
111
|
+
const names = [];
|
|
112
|
+
const pat = /global_secondary_index\s*\{([^}]*)\}/g;
|
|
113
|
+
let m;
|
|
114
|
+
while ((m = pat.exec(body)) !== null) {
|
|
115
|
+
const nameMatch = m[1].match(/name\s*=\s*"([^"]*)"/);
|
|
116
|
+
if (nameMatch?.[1])
|
|
117
|
+
names.push(nameMatch[1]);
|
|
118
|
+
}
|
|
119
|
+
return names;
|
|
120
|
+
}
|
|
121
|
+
async function extractTerraformSchema(repoPath) {
|
|
122
|
+
const schema = emptySchema();
|
|
123
|
+
const tfFiles = findFilesRecursively(repoPath, ['.tf']);
|
|
124
|
+
core_1.logger.debug(`Found ${tfFiles.length} Terraform file(s)`);
|
|
125
|
+
for (const filePath of tfFiles) {
|
|
126
|
+
let content;
|
|
127
|
+
try {
|
|
128
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const { resourceType, resourceName, body } of extractTerraformResourceBlocks(content)) {
|
|
134
|
+
switch (resourceType) {
|
|
135
|
+
case 'aws_dynamodb_table':
|
|
136
|
+
schema.dynamoTables.push({
|
|
137
|
+
name: tfStr(body, 'name') ?? resourceName,
|
|
138
|
+
partitionKey: tfStr(body, 'hash_key'),
|
|
139
|
+
sortKey: tfStr(body, 'range_key'),
|
|
140
|
+
gsiNames: tfGSINames(body),
|
|
141
|
+
source: 'terraform', filePath,
|
|
142
|
+
});
|
|
143
|
+
break;
|
|
144
|
+
case 'aws_db_instance':
|
|
145
|
+
case 'aws_rds_cluster':
|
|
146
|
+
schema.rdsInstances.push({
|
|
147
|
+
identifier: tfStr(body, 'identifier') ?? tfStr(body, 'cluster_identifier') ?? resourceName,
|
|
148
|
+
engine: tfStr(body, 'engine') ?? 'unknown',
|
|
149
|
+
source: 'terraform', filePath,
|
|
150
|
+
});
|
|
151
|
+
break;
|
|
152
|
+
case 'aws_docdb_cluster':
|
|
153
|
+
schema.mongoClusters.push({
|
|
154
|
+
identifier: tfStr(body, 'cluster_identifier') ?? resourceName,
|
|
155
|
+
source: 'terraform', filePath,
|
|
156
|
+
});
|
|
157
|
+
break;
|
|
158
|
+
case 'aws_sqs_queue': {
|
|
159
|
+
const name = tfStr(body, 'name') ?? resourceName;
|
|
160
|
+
const hasDLQ = body.includes('redrive_policy');
|
|
161
|
+
const encrypted = body.includes('kms_master_key_id') || tfBool(body, 'sqs_managed_sse_enabled');
|
|
162
|
+
schema.queues.push({ name, hasDLQ, encrypted, source: 'terraform', filePath });
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'aws_sns_topic':
|
|
166
|
+
schema.topics.push({
|
|
167
|
+
name: tfStr(body, 'name') ?? resourceName,
|
|
168
|
+
encrypted: body.includes('kms_master_key_id'),
|
|
169
|
+
source: 'terraform', filePath,
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
case 'aws_lambda_function':
|
|
173
|
+
schema.lambdas.push({
|
|
174
|
+
name: tfStr(body, 'function_name') ?? resourceName,
|
|
175
|
+
runtime: tfStr(body, 'runtime'),
|
|
176
|
+
source: 'terraform', filePath,
|
|
177
|
+
});
|
|
178
|
+
break;
|
|
179
|
+
case 'aws_s3_bucket':
|
|
180
|
+
schema.buckets.push({
|
|
181
|
+
name: tfStr(body, 'bucket') ?? tfStr(body, 'bucket_prefix') ?? resourceName,
|
|
182
|
+
versioned: body.includes('versioning') && body.includes('enabled = true'),
|
|
183
|
+
source: 'terraform', filePath,
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
case 'aws_ssm_parameter':
|
|
187
|
+
schema.parameters.push({
|
|
188
|
+
name: tfStr(body, 'name') ?? resourceName,
|
|
189
|
+
type: tfStr(body, 'type') ?? 'String',
|
|
190
|
+
source: 'terraform', filePath,
|
|
191
|
+
});
|
|
192
|
+
break;
|
|
193
|
+
case 'aws_secretsmanager_secret':
|
|
194
|
+
schema.secrets.push({
|
|
195
|
+
name: tfStr(body, 'name') ?? resourceName,
|
|
196
|
+
source: 'terraform', filePath,
|
|
197
|
+
});
|
|
198
|
+
break;
|
|
199
|
+
case 'aws_api_gateway_rest_api':
|
|
200
|
+
case 'aws_apigatewayv2_api':
|
|
201
|
+
schema.apiGateways.push({
|
|
202
|
+
name: tfStr(body, 'name') ?? resourceName,
|
|
203
|
+
source: 'terraform', filePath,
|
|
204
|
+
});
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return schema;
|
|
210
|
+
}
|
|
211
|
+
// ─── CloudFormation / CDK parser ──────────────────────────────────────────────
|
|
212
|
+
function isCloudFormationTemplate(parsed) {
|
|
213
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
214
|
+
return false;
|
|
215
|
+
const obj = parsed;
|
|
216
|
+
return 'AWSTemplateFormatVersion' in obj || ('Resources' in obj && typeof obj['Resources'] === 'object');
|
|
217
|
+
}
|
|
218
|
+
function parseCFNFile(filePath) {
|
|
219
|
+
let content;
|
|
220
|
+
try {
|
|
221
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
if (!content.includes('AWSTemplateFormatVersion') && !content.includes('Resources'))
|
|
227
|
+
return null;
|
|
228
|
+
let parsed;
|
|
229
|
+
try {
|
|
230
|
+
parsed = filePath.endsWith('.json') ? JSON.parse(content) : js_yaml_1.default.load(content);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
if (!isCloudFormationTemplate(parsed))
|
|
236
|
+
return null;
|
|
237
|
+
return parsed;
|
|
238
|
+
}
|
|
239
|
+
function cfnStr(props, ...keys) {
|
|
240
|
+
for (const key of keys) {
|
|
241
|
+
if (typeof props[key] === 'string')
|
|
242
|
+
return props[key];
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
function cfnBool(props, key) {
|
|
247
|
+
return props[key] === true || props[key] === 'true' || props[key] === 'Enabled';
|
|
248
|
+
}
|
|
249
|
+
function processCFNResources(resources, schema, filePath, source) {
|
|
250
|
+
for (const [logicalId, rawResource] of Object.entries(resources)) {
|
|
251
|
+
if (typeof rawResource !== 'object' || rawResource === null)
|
|
252
|
+
continue;
|
|
253
|
+
const resource = rawResource;
|
|
254
|
+
const resourceType = resource['Type'];
|
|
255
|
+
const props = (resource['Properties'] ?? {});
|
|
256
|
+
switch (resourceType) {
|
|
257
|
+
case 'AWS::DynamoDB::Table': {
|
|
258
|
+
let pk, sk;
|
|
259
|
+
const keySchema = props['KeySchema'];
|
|
260
|
+
if (Array.isArray(keySchema)) {
|
|
261
|
+
for (const kd of keySchema) {
|
|
262
|
+
if (typeof kd !== 'object' || kd === null)
|
|
263
|
+
continue;
|
|
264
|
+
const k = kd;
|
|
265
|
+
if (k['KeyType'] === 'HASH')
|
|
266
|
+
pk = k['AttributeName'];
|
|
267
|
+
if (k['KeyType'] === 'RANGE')
|
|
268
|
+
sk = k['AttributeName'];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const gsiNames = [];
|
|
272
|
+
const gsis = props['GlobalSecondaryIndexes'];
|
|
273
|
+
if (Array.isArray(gsis)) {
|
|
274
|
+
for (const g of gsis) {
|
|
275
|
+
if (typeof g === 'object' && g !== null) {
|
|
276
|
+
const gi = g;
|
|
277
|
+
if (typeof gi['IndexName'] === 'string')
|
|
278
|
+
gsiNames.push(gi['IndexName']);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
schema.dynamoTables.push({
|
|
283
|
+
name: cfnStr(props, 'TableName') ?? logicalId,
|
|
284
|
+
partitionKey: pk,
|
|
285
|
+
sortKey: sk,
|
|
286
|
+
gsiNames,
|
|
287
|
+
source, filePath,
|
|
288
|
+
});
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case 'AWS::RDS::DBInstance':
|
|
292
|
+
schema.rdsInstances.push({
|
|
293
|
+
identifier: cfnStr(props, 'DBInstanceIdentifier') ?? logicalId,
|
|
294
|
+
engine: cfnStr(props, 'Engine') ?? 'unknown',
|
|
295
|
+
source, filePath,
|
|
296
|
+
});
|
|
297
|
+
break;
|
|
298
|
+
case 'AWS::RDS::DBCluster':
|
|
299
|
+
schema.rdsInstances.push({
|
|
300
|
+
identifier: cfnStr(props, 'DBClusterIdentifier') ?? logicalId,
|
|
301
|
+
engine: cfnStr(props, 'Engine') ?? 'aurora',
|
|
302
|
+
source, filePath,
|
|
303
|
+
});
|
|
304
|
+
break;
|
|
305
|
+
case 'AWS::DocDB::DBCluster':
|
|
306
|
+
schema.mongoClusters.push({
|
|
307
|
+
identifier: cfnStr(props, 'DBClusterIdentifier') ?? logicalId,
|
|
308
|
+
source, filePath,
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
311
|
+
case 'AWS::SQS::Queue': {
|
|
312
|
+
const name = cfnStr(props, 'QueueName') ?? logicalId;
|
|
313
|
+
const hasDLQ = !!props['RedrivePolicy'];
|
|
314
|
+
const encrypted = !!(props['KmsMasterKeyId'] || cfnBool(props, 'SqsManagedSseEnabled'));
|
|
315
|
+
schema.queues.push({ name, hasDLQ, encrypted, source, filePath });
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case 'AWS::SNS::Topic':
|
|
319
|
+
schema.topics.push({
|
|
320
|
+
name: cfnStr(props, 'TopicName') ?? logicalId,
|
|
321
|
+
encrypted: !!props['KmsMasterKeyId'],
|
|
322
|
+
source, filePath,
|
|
323
|
+
});
|
|
324
|
+
break;
|
|
325
|
+
case 'AWS::Lambda::Function':
|
|
326
|
+
schema.lambdas.push({
|
|
327
|
+
name: cfnStr(props, 'FunctionName') ?? logicalId,
|
|
328
|
+
runtime: cfnStr(props, 'Runtime'),
|
|
329
|
+
source, filePath,
|
|
330
|
+
});
|
|
331
|
+
break;
|
|
332
|
+
case 'AWS::S3::Bucket': {
|
|
333
|
+
const versioningConfig = props['VersioningConfiguration'];
|
|
334
|
+
schema.buckets.push({
|
|
335
|
+
name: cfnStr(props, 'BucketName') ?? logicalId,
|
|
336
|
+
versioned: versioningConfig?.['Status'] === 'Enabled',
|
|
337
|
+
source, filePath,
|
|
338
|
+
});
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'AWS::SSM::Parameter':
|
|
342
|
+
schema.parameters.push({
|
|
343
|
+
name: cfnStr(props, 'Name') ?? logicalId,
|
|
344
|
+
type: cfnStr(props, 'Type') ?? 'String',
|
|
345
|
+
source, filePath,
|
|
346
|
+
});
|
|
347
|
+
break;
|
|
348
|
+
case 'AWS::SecretsManager::Secret':
|
|
349
|
+
schema.secrets.push({
|
|
350
|
+
name: cfnStr(props, 'Name') ?? logicalId,
|
|
351
|
+
source, filePath,
|
|
352
|
+
});
|
|
353
|
+
break;
|
|
354
|
+
case 'AWS::ApiGateway::RestApi':
|
|
355
|
+
case 'AWS::ApiGatewayV2::Api':
|
|
356
|
+
schema.apiGateways.push({
|
|
357
|
+
name: cfnStr(props, 'Name') ?? logicalId,
|
|
358
|
+
source, filePath,
|
|
359
|
+
});
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function extractCloudFormationSchema(repoPath) {
|
|
365
|
+
const schema = emptySchema();
|
|
366
|
+
const cfnFiles = findFilesRecursively(repoPath, ['.yaml', '.yml', '.json']);
|
|
367
|
+
core_1.logger.debug(`Scanning ${cfnFiles.length} potential CloudFormation file(s)`);
|
|
368
|
+
for (const filePath of cfnFiles) {
|
|
369
|
+
const parsed = parseCFNFile(filePath);
|
|
370
|
+
if (!parsed)
|
|
371
|
+
continue;
|
|
372
|
+
const resources = parsed['Resources'];
|
|
373
|
+
if (!resources)
|
|
374
|
+
continue;
|
|
375
|
+
processCFNResources(resources, schema, filePath, 'cloudformation');
|
|
376
|
+
}
|
|
377
|
+
return schema;
|
|
378
|
+
}
|
|
379
|
+
// ─── CDK parser ───────────────────────────────────────────────────────────────
|
|
380
|
+
async function extractCDKSchema(repoPath) {
|
|
381
|
+
const schema = emptySchema();
|
|
382
|
+
// Strategy 1: Parse cdk.out/*.template.json (synthesized CloudFormation)
|
|
383
|
+
const cdkOutDir = path.join(repoPath, 'cdk.out');
|
|
384
|
+
if (fs.existsSync(cdkOutDir)) {
|
|
385
|
+
const templateFiles = fs.readdirSync(cdkOutDir)
|
|
386
|
+
.filter((f) => f.endsWith('.template.json'))
|
|
387
|
+
.map((f) => path.join(cdkOutDir, f));
|
|
388
|
+
core_1.logger.debug(`Found ${templateFiles.length} CDK synthesized template(s) in cdk.out/`);
|
|
389
|
+
for (const filePath of templateFiles) {
|
|
390
|
+
const parsed = parseCFNFile(filePath);
|
|
391
|
+
if (!parsed)
|
|
392
|
+
continue;
|
|
393
|
+
const resources = parsed['Resources'];
|
|
394
|
+
if (!resources)
|
|
395
|
+
continue;
|
|
396
|
+
processCFNResources(resources, schema, filePath, 'cdk');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Strategy 2: Detect CDK TypeScript construct patterns if no cdk.out exists
|
|
400
|
+
if (schema.queues.length === 0 && schema.topics.length === 0 && schema.lambdas.length === 0) {
|
|
401
|
+
const cdkJsonPath = path.join(repoPath, 'cdk.json');
|
|
402
|
+
if (fs.existsSync(cdkJsonPath)) {
|
|
403
|
+
core_1.logger.debug('CDK project detected (cdk.json found) — run `cdk synth` for full IaC analysis');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return schema;
|
|
407
|
+
}
|
|
408
|
+
// ─── Combined ─────────────────────────────────────────────────────────────────
|
|
409
|
+
function mergeSchemas(...schemas) {
|
|
410
|
+
const merged = emptySchema();
|
|
411
|
+
const seen = {
|
|
412
|
+
dynamo: new Set(),
|
|
413
|
+
rds: new Set(),
|
|
414
|
+
mongo: new Set(),
|
|
415
|
+
queue: new Set(),
|
|
416
|
+
topic: new Set(),
|
|
417
|
+
lambda: new Set(),
|
|
418
|
+
bucket: new Set(),
|
|
419
|
+
param: new Set(),
|
|
420
|
+
secret: new Set(),
|
|
421
|
+
api: new Set(),
|
|
422
|
+
};
|
|
423
|
+
for (const schema of schemas) {
|
|
424
|
+
for (const t of schema.dynamoTables) {
|
|
425
|
+
const k = `${t.source}::${t.name}`;
|
|
426
|
+
if (!seen.dynamo.has(k)) {
|
|
427
|
+
seen.dynamo.add(k);
|
|
428
|
+
merged.dynamoTables.push(t);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const r of schema.rdsInstances) {
|
|
432
|
+
const k = `${r.source}::${r.identifier}`;
|
|
433
|
+
if (!seen.rds.has(k)) {
|
|
434
|
+
seen.rds.add(k);
|
|
435
|
+
merged.rdsInstances.push(r);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const m of schema.mongoClusters) {
|
|
439
|
+
const k = `${m.source}::${m.identifier}`;
|
|
440
|
+
if (!seen.mongo.has(k)) {
|
|
441
|
+
seen.mongo.add(k);
|
|
442
|
+
merged.mongoClusters.push(m);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const q of schema.queues) {
|
|
446
|
+
const k = `${q.source}::${q.name}`;
|
|
447
|
+
if (!seen.queue.has(k)) {
|
|
448
|
+
seen.queue.add(k);
|
|
449
|
+
merged.queues.push(q);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const t of schema.topics) {
|
|
453
|
+
const k = `${t.source}::${t.name}`;
|
|
454
|
+
if (!seen.topic.has(k)) {
|
|
455
|
+
seen.topic.add(k);
|
|
456
|
+
merged.topics.push(t);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
for (const l of schema.lambdas) {
|
|
460
|
+
const k = `${l.source}::${l.name}`;
|
|
461
|
+
if (!seen.lambda.has(k)) {
|
|
462
|
+
seen.lambda.add(k);
|
|
463
|
+
merged.lambdas.push(l);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
for (const b of schema.buckets) {
|
|
467
|
+
const k = `${b.source}::${b.name}`;
|
|
468
|
+
if (!seen.bucket.has(k)) {
|
|
469
|
+
seen.bucket.add(k);
|
|
470
|
+
merged.buckets.push(b);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const p of schema.parameters) {
|
|
474
|
+
const k = `${p.source}::${p.name}`;
|
|
475
|
+
if (!seen.param.has(k)) {
|
|
476
|
+
seen.param.add(k);
|
|
477
|
+
merged.parameters.push(p);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const s of schema.secrets) {
|
|
481
|
+
const k = `${s.source}::${s.name}`;
|
|
482
|
+
if (!seen.secret.has(k)) {
|
|
483
|
+
seen.secret.add(k);
|
|
484
|
+
merged.secrets.push(s);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const a of schema.apiGateways) {
|
|
488
|
+
const k = `${a.source}::${a.name}`;
|
|
489
|
+
if (!seen.api.has(k)) {
|
|
490
|
+
seen.api.add(k);
|
|
491
|
+
merged.apiGateways.push(a);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return merged;
|
|
496
|
+
}
|
|
497
|
+
async function extractIaCSchema(repoPath) {
|
|
498
|
+
const [tfSchema, cfnSchema, cdkSchema] = await Promise.all([
|
|
499
|
+
extractTerraformSchema(repoPath),
|
|
500
|
+
extractCloudFormationSchema(repoPath),
|
|
501
|
+
extractCDKSchema(repoPath),
|
|
502
|
+
]);
|
|
503
|
+
const merged = mergeSchemas(tfSchema, cfnSchema, cdkSchema);
|
|
504
|
+
const total = merged.dynamoTables.length + merged.rdsInstances.length +
|
|
505
|
+
merged.mongoClusters.length + merged.queues.length + merged.topics.length +
|
|
506
|
+
merged.lambdas.length + merged.buckets.length + merged.parameters.length +
|
|
507
|
+
merged.secrets.length + merged.apiGateways.length;
|
|
508
|
+
core_1.logger.debug(`IaC schema total: ${total} resource(s) across TF/CFN/CDK`);
|
|
509
|
+
return merged;
|
|
510
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LambdaHighTimeoutAnalyzer = exports.LambdaDefaultMemoryAnalyzer = exports.MissingLogRetentionAnalyzer = exports.MissingSecretRotationAnalyzer = exports.LargeQueueBacklogAnalyzer = exports.UnencryptedQueueAnalyzer = exports.MissingDLQAnalyzer = void 0;
|
|
4
|
+
// ─── SQS ─────────────────────────────────────────────────────────────────────
|
|
5
|
+
class MissingDLQAnalyzer {
|
|
6
|
+
name = 'MissingDLQAnalyzer';
|
|
7
|
+
async analyze(graph) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
for (const node of graph.nodes) {
|
|
10
|
+
if (node.type !== 'queue')
|
|
11
|
+
continue;
|
|
12
|
+
if (!node.hasDLQ) {
|
|
13
|
+
findings.push({
|
|
14
|
+
severity: 'high',
|
|
15
|
+
issue: `Queue "${node.name}" has no Dead Letter Queue`,
|
|
16
|
+
description: `SQS queue "${node.name}" has no DLQ configured. Failed messages will be discarded after maxReceiveCount retries, causing silent data loss.`,
|
|
17
|
+
recommendation: `Add a Dead Letter Queue to "${node.name}". Set maxReceiveCount to 3–5 retries before routing to DLQ. Alert on DLQ depth.`,
|
|
18
|
+
metadata: { queueName: node.name, provider: node.provider },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return findings;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.MissingDLQAnalyzer = MissingDLQAnalyzer;
|
|
26
|
+
class UnencryptedQueueAnalyzer {
|
|
27
|
+
name = 'UnencryptedQueueAnalyzer';
|
|
28
|
+
async analyze(graph) {
|
|
29
|
+
const findings = [];
|
|
30
|
+
for (const node of graph.nodes) {
|
|
31
|
+
if (node.type !== 'queue')
|
|
32
|
+
continue;
|
|
33
|
+
if (!node.encrypted) {
|
|
34
|
+
findings.push({
|
|
35
|
+
severity: 'low',
|
|
36
|
+
issue: `Queue "${node.name}" is not encrypted`,
|
|
37
|
+
description: `SQS queue "${node.name}" does not have server-side encryption enabled. Messages at rest are unencrypted.`,
|
|
38
|
+
recommendation: `Enable SQS-managed SSE (SqsManagedSseEnabled=true) or bring your own KMS key for "${node.name}".`,
|
|
39
|
+
metadata: { queueName: node.name },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return findings;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.UnencryptedQueueAnalyzer = UnencryptedQueueAnalyzer;
|
|
47
|
+
class LargeQueueBacklogAnalyzer {
|
|
48
|
+
name = 'LargeQueueBacklogAnalyzer';
|
|
49
|
+
threshold;
|
|
50
|
+
constructor(threshold = 1000) {
|
|
51
|
+
this.threshold = threshold;
|
|
52
|
+
}
|
|
53
|
+
async analyze(graph) {
|
|
54
|
+
const findings = [];
|
|
55
|
+
for (const node of graph.nodes) {
|
|
56
|
+
if (node.type !== 'queue')
|
|
57
|
+
continue;
|
|
58
|
+
const count = node.approximateMessages ?? 0;
|
|
59
|
+
if (count > this.threshold) {
|
|
60
|
+
findings.push({
|
|
61
|
+
severity: 'medium',
|
|
62
|
+
issue: `Queue "${node.name}" has a large backlog (${count.toLocaleString()} messages)`,
|
|
63
|
+
description: `The approximate message count for "${node.name}" is ${count.toLocaleString()}, indicating consumers may be falling behind or stuck.`,
|
|
64
|
+
recommendation: `Check consumer health and scaling for "${node.name}". Consider auto-scaling consumers on queue depth. If messages are stale, investigate consumer errors in CloudWatch.`,
|
|
65
|
+
metadata: { queueName: node.name, messageCount: count },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return findings;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.LargeQueueBacklogAnalyzer = LargeQueueBacklogAnalyzer;
|
|
73
|
+
// ─── Secrets Manager ─────────────────────────────────────────────────────────
|
|
74
|
+
class MissingSecretRotationAnalyzer {
|
|
75
|
+
name = 'MissingSecretRotationAnalyzer';
|
|
76
|
+
async analyze(graph) {
|
|
77
|
+
const findings = [];
|
|
78
|
+
for (const node of graph.nodes) {
|
|
79
|
+
if (node.type !== 'secret')
|
|
80
|
+
continue;
|
|
81
|
+
if (!node.rotationEnabled) {
|
|
82
|
+
findings.push({
|
|
83
|
+
severity: 'medium',
|
|
84
|
+
issue: `Secret "${node.name}" has no automatic rotation`,
|
|
85
|
+
description: `Secrets Manager secret "${node.name}" does not have automatic rotation enabled. Long-lived credentials increase the blast radius of a compromise.`,
|
|
86
|
+
recommendation: `Enable automatic rotation for "${node.name}" using a Lambda rotation function. AWS provides pre-built rotators for RDS, Redshift, and custom secrets.`,
|
|
87
|
+
metadata: { secretName: node.name, provider: node.provider },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return findings;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.MissingSecretRotationAnalyzer = MissingSecretRotationAnalyzer;
|
|
95
|
+
// ─── CloudWatch Logs ─────────────────────────────────────────────────────────
|
|
96
|
+
class MissingLogRetentionAnalyzer {
|
|
97
|
+
name = 'MissingLogRetentionAnalyzer';
|
|
98
|
+
async analyze(graph) {
|
|
99
|
+
const findings = [];
|
|
100
|
+
for (const node of graph.nodes) {
|
|
101
|
+
if (node.type !== 'log_group')
|
|
102
|
+
continue;
|
|
103
|
+
if (node.retentionDays === undefined) {
|
|
104
|
+
findings.push({
|
|
105
|
+
severity: 'medium',
|
|
106
|
+
issue: `Log group "${node.name}" has no retention policy`,
|
|
107
|
+
description: `CloudWatch Log group "${node.name}" retains logs indefinitely. This increases storage costs and can expose sensitive data longer than necessary.`,
|
|
108
|
+
recommendation: `Set a retention policy on "${node.name}". 90 days is a common baseline; adjust based on compliance requirements (e.g., 365 days for SOC2/PCI).`,
|
|
109
|
+
metadata: { logGroupName: node.name },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (node.retentionDays > 365) {
|
|
113
|
+
findings.push({
|
|
114
|
+
severity: 'low',
|
|
115
|
+
issue: `Log group "${node.name}" retains logs for ${node.retentionDays} days`,
|
|
116
|
+
description: `Log group "${node.name}" has a ${node.retentionDays}-day retention period. Unless required by compliance, this may be longer than needed.`,
|
|
117
|
+
recommendation: `Review whether ${node.retentionDays} days of retention is required for "${node.name}". Consider archiving older logs to S3 Glacier for cost savings.`,
|
|
118
|
+
metadata: { logGroupName: node.name, retentionDays: node.retentionDays },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return findings;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.MissingLogRetentionAnalyzer = MissingLogRetentionAnalyzer;
|
|
126
|
+
// ─── Lambda ───────────────────────────────────────────────────────────────────
|
|
127
|
+
class LambdaDefaultMemoryAnalyzer {
|
|
128
|
+
name = 'LambdaDefaultMemoryAnalyzer';
|
|
129
|
+
async analyze(graph) {
|
|
130
|
+
const findings = [];
|
|
131
|
+
for (const node of graph.nodes) {
|
|
132
|
+
if (node.type !== 'lambda')
|
|
133
|
+
continue;
|
|
134
|
+
if (node.memoryMB === 128) {
|
|
135
|
+
findings.push({
|
|
136
|
+
severity: 'low',
|
|
137
|
+
issue: `Lambda "${node.name}" uses the default 128 MB memory`,
|
|
138
|
+
description: `"${node.name}" uses the default 128 MB. Undersized memory causes throttled CPU and higher durations. AWS Lambda pricing is duration × memory, so more memory often lowers cost by reducing duration.`,
|
|
139
|
+
recommendation: `Run Lambda Power Tuning on "${node.name}" to find the optimal memory/cost balance. Most workloads perform better at 512 MB–1 GB.`,
|
|
140
|
+
metadata: { functionName: node.name, memoryMB: node.memoryMB },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.LambdaDefaultMemoryAnalyzer = LambdaDefaultMemoryAnalyzer;
|
|
148
|
+
class LambdaHighTimeoutAnalyzer {
|
|
149
|
+
name = 'LambdaHighTimeoutAnalyzer';
|
|
150
|
+
async analyze(graph) {
|
|
151
|
+
const findings = [];
|
|
152
|
+
for (const node of graph.nodes) {
|
|
153
|
+
if (node.type !== 'lambda')
|
|
154
|
+
continue;
|
|
155
|
+
if ((node.timeoutSec ?? 0) >= 300) {
|
|
156
|
+
findings.push({
|
|
157
|
+
severity: 'low',
|
|
158
|
+
issue: `Lambda "${node.name}" has a very high timeout (${node.timeoutSec}s)`,
|
|
159
|
+
description: `"${node.name}" has a ${node.timeoutSec}-second timeout. High timeouts mask latency issues and increase worst-case cost when functions hang.`,
|
|
160
|
+
recommendation: `Review whether "${node.name}" truly needs ${node.timeoutSec}s. Add internal circuit-breakers or streaming patterns to avoid reaching the timeout. Set alarms on p99 duration.`,
|
|
161
|
+
metadata: { functionName: node.name, timeoutSec: node.timeoutSec },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return findings;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
exports.LambdaHighTimeoutAnalyzer = LambdaHighTimeoutAnalyzer;
|