infrawise 0.1.0 → 0.1.2
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 +6 -1
- package/dist/index.js +1891 -553
- package/package.json +19 -4
package/dist/index.js
CHANGED
|
@@ -81,9 +81,7 @@ var require_config = __commonJS({
|
|
|
81
81
|
profile: zod_1.z.string().optional().default("default"),
|
|
82
82
|
region: zod_1.z.string().optional().default("us-east-1")
|
|
83
83
|
}).optional().default({}),
|
|
84
|
-
dynamodb: zod_1.z.object({
|
|
85
|
-
includeTables: zod_1.z.array(zod_1.z.string()).optional()
|
|
86
|
-
}).optional(),
|
|
84
|
+
dynamodb: zod_1.z.object({ includeTables: zod_1.z.array(zod_1.z.string()).optional() }).optional(),
|
|
87
85
|
postgres: zod_1.z.object({
|
|
88
86
|
enabled: zod_1.z.boolean().optional().default(false),
|
|
89
87
|
connectionString: zod_1.z.string().optional()
|
|
@@ -97,8 +95,19 @@ var require_config = __commonJS({
|
|
|
97
95
|
connectionString: zod_1.z.string().optional(),
|
|
98
96
|
databases: zod_1.z.array(zod_1.z.string()).optional()
|
|
99
97
|
}).optional(),
|
|
100
|
-
terraform: zod_1.z.object({
|
|
101
|
-
|
|
98
|
+
terraform: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(true) }).optional(),
|
|
99
|
+
sqs: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(true) }).optional(),
|
|
100
|
+
sns: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(true) }).optional(),
|
|
101
|
+
ssm: zod_1.z.object({
|
|
102
|
+
enabled: zod_1.z.boolean().optional().default(true),
|
|
103
|
+
paths: zod_1.z.array(zod_1.z.string()).optional()
|
|
104
|
+
}).optional(),
|
|
105
|
+
secretsManager: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(true) }).optional(),
|
|
106
|
+
lambda: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(true) }).optional(),
|
|
107
|
+
cloudwatchLogs: zod_1.z.object({
|
|
108
|
+
enabled: zod_1.z.boolean().optional().default(false),
|
|
109
|
+
logGroupPrefixes: zod_1.z.array(zod_1.z.string()).optional(),
|
|
110
|
+
windowHours: zod_1.z.number().int().positive().optional().default(24)
|
|
102
111
|
}).optional(),
|
|
103
112
|
analysis: zod_1.z.object({
|
|
104
113
|
sampleSize: zod_1.z.number().int().positive().optional().default(100)
|
|
@@ -125,19 +134,13 @@ var require_config = __commonJS({
|
|
|
125
134
|
try {
|
|
126
135
|
rawContent = fs4.readFileSync(resolvedPath, "utf-8");
|
|
127
136
|
} catch (err) {
|
|
128
|
-
throw new ConfigError(`Unable to read configuration file: ${resolvedPath}`, [
|
|
129
|
-
"Check file permissions",
|
|
130
|
-
String(err)
|
|
131
|
-
]);
|
|
137
|
+
throw new ConfigError(`Unable to read configuration file: ${resolvedPath}`, [String(err)]);
|
|
132
138
|
}
|
|
133
139
|
let parsedYaml;
|
|
134
140
|
try {
|
|
135
141
|
parsedYaml = yaml.load(rawContent);
|
|
136
142
|
} catch (err) {
|
|
137
|
-
throw new ConfigError(`Invalid YAML in configuration file: ${resolvedPath}`, [
|
|
138
|
-
"Check the YAML syntax",
|
|
139
|
-
String(err)
|
|
140
|
-
]);
|
|
143
|
+
throw new ConfigError(`Invalid YAML in configuration file: ${resolvedPath}`, [String(err)]);
|
|
141
144
|
}
|
|
142
145
|
const result = exports2.InfrawiseConfigSchema.safeParse(parsedYaml);
|
|
143
146
|
if (!result.success) {
|
|
@@ -153,9 +156,7 @@ var require_config = __commonJS({
|
|
|
153
156
|
profile: options?.aws?.profile ?? "default",
|
|
154
157
|
region: options?.aws?.region ?? "us-east-1"
|
|
155
158
|
},
|
|
156
|
-
dynamodb: {
|
|
157
|
-
includeTables: options?.dynamodb?.includeTables ?? []
|
|
158
|
-
},
|
|
159
|
+
dynamodb: { includeTables: options?.dynamodb?.includeTables ?? [] },
|
|
159
160
|
postgres: {
|
|
160
161
|
enabled: options?.postgres?.enabled ?? false,
|
|
161
162
|
connectionString: options?.postgres?.connectionString ?? ""
|
|
@@ -169,12 +170,21 @@ var require_config = __commonJS({
|
|
|
169
170
|
connectionString: options?.mongodb?.connectionString ?? "",
|
|
170
171
|
databases: options?.mongodb?.databases ?? []
|
|
171
172
|
},
|
|
172
|
-
terraform: {
|
|
173
|
-
|
|
173
|
+
terraform: { enabled: options?.terraform?.enabled ?? true },
|
|
174
|
+
sqs: { enabled: options?.sqs?.enabled ?? true },
|
|
175
|
+
sns: { enabled: options?.sns?.enabled ?? true },
|
|
176
|
+
ssm: {
|
|
177
|
+
enabled: options?.ssm?.enabled ?? true,
|
|
178
|
+
paths: options?.ssm?.paths ?? []
|
|
174
179
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
180
|
+
secretsManager: { enabled: options?.secretsManager?.enabled ?? true },
|
|
181
|
+
lambda: { enabled: options?.lambda?.enabled ?? true },
|
|
182
|
+
cloudwatchLogs: {
|
|
183
|
+
enabled: options?.cloudwatchLogs?.enabled ?? false,
|
|
184
|
+
logGroupPrefixes: options?.cloudwatchLogs?.logGroupPrefixes ?? [],
|
|
185
|
+
windowHours: options?.cloudwatchLogs?.windowHours ?? 24
|
|
186
|
+
},
|
|
187
|
+
analysis: { sampleSize: options?.analysis?.sampleSize ?? 100 }
|
|
178
188
|
};
|
|
179
189
|
return yaml.dump(config, { lineWidth: 120 });
|
|
180
190
|
}
|
|
@@ -984,22 +994,37 @@ var require_dist6 = __commonJS({
|
|
|
984
994
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
985
995
|
exports2.extractTerraformSchema = extractTerraformSchema;
|
|
986
996
|
exports2.extractCloudFormationSchema = extractCloudFormationSchema;
|
|
997
|
+
exports2.extractCDKSchema = extractCDKSchema;
|
|
987
998
|
exports2.extractIaCSchema = extractIaCSchema2;
|
|
988
999
|
var fs4 = __importStar(require("fs"));
|
|
989
1000
|
var path5 = __importStar(require("path"));
|
|
990
1001
|
var js_yaml_1 = __importDefault(require("js-yaml"));
|
|
991
1002
|
var core_1 = require_dist();
|
|
992
|
-
function
|
|
1003
|
+
function emptySchema() {
|
|
1004
|
+
return {
|
|
1005
|
+
dynamoTables: [],
|
|
1006
|
+
rdsInstances: [],
|
|
1007
|
+
mongoClusters: [],
|
|
1008
|
+
queues: [],
|
|
1009
|
+
topics: [],
|
|
1010
|
+
lambdas: [],
|
|
1011
|
+
buckets: [],
|
|
1012
|
+
parameters: [],
|
|
1013
|
+
secrets: [],
|
|
1014
|
+
apiGateways: []
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function findFilesRecursively(dir, extensions, skipDirs = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".infrawise"])) {
|
|
993
1018
|
const results = [];
|
|
994
1019
|
if (!fs4.existsSync(dir))
|
|
995
1020
|
return results;
|
|
996
1021
|
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
997
1022
|
for (const entry of entries) {
|
|
998
|
-
if (
|
|
1023
|
+
if (skipDirs.has(entry.name))
|
|
999
1024
|
continue;
|
|
1000
1025
|
const fullPath = path5.join(dir, entry.name);
|
|
1001
1026
|
if (entry.isDirectory()) {
|
|
1002
|
-
results.push(...findFilesRecursively(fullPath, extensions));
|
|
1027
|
+
results.push(...findFilesRecursively(fullPath, extensions, skipDirs));
|
|
1003
1028
|
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
1004
1029
|
results.push(fullPath);
|
|
1005
1030
|
}
|
|
@@ -1011,8 +1036,8 @@ var require_dist6 = __commonJS({
|
|
|
1011
1036
|
const resourcePattern = /resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/g;
|
|
1012
1037
|
let match;
|
|
1013
1038
|
while ((match = resourcePattern.exec(content)) !== null) {
|
|
1014
|
-
const resourceType = match[1];
|
|
1015
|
-
const resourceName = match[2];
|
|
1039
|
+
const resourceType = match[1] ?? "";
|
|
1040
|
+
const resourceName = match[2] ?? "";
|
|
1016
1041
|
const startBrace = match.index + match[0].length - 1;
|
|
1017
1042
|
let depth = 1;
|
|
1018
1043
|
let i = startBrace + 1;
|
|
@@ -1023,30 +1048,31 @@ var require_dist6 = __commonJS({
|
|
|
1023
1048
|
depth--;
|
|
1024
1049
|
i++;
|
|
1025
1050
|
}
|
|
1026
|
-
|
|
1027
|
-
results.push({ resourceType: resourceType ?? "", resourceName: resourceName ?? "", body });
|
|
1051
|
+
results.push({ resourceType, resourceName, body: content.slice(startBrace + 1, i - 1) });
|
|
1028
1052
|
}
|
|
1029
1053
|
return results;
|
|
1030
1054
|
}
|
|
1031
|
-
function
|
|
1032
|
-
const
|
|
1033
|
-
const m = body.match(pattern);
|
|
1055
|
+
function tfStr(body, attr) {
|
|
1056
|
+
const m = body.match(new RegExp(`${attr}\\s*=\\s*"([^"]*)"`, "i"));
|
|
1034
1057
|
return m?.[1];
|
|
1035
1058
|
}
|
|
1036
|
-
function
|
|
1059
|
+
function tfBool(body, attr) {
|
|
1060
|
+
const m = body.match(new RegExp(`${attr}\\s*=\\s*(true|false)`, "i"));
|
|
1061
|
+
return m?.[1] === "true";
|
|
1062
|
+
}
|
|
1063
|
+
function tfGSINames(body) {
|
|
1037
1064
|
const names = [];
|
|
1038
|
-
const
|
|
1065
|
+
const pat = /global_secondary_index\s*\{([^}]*)\}/g;
|
|
1039
1066
|
let m;
|
|
1040
|
-
while ((m =
|
|
1041
|
-
const
|
|
1042
|
-
const nameMatch = gsiBody.match(/name\s*=\s*"([^"]*)"/);
|
|
1067
|
+
while ((m = pat.exec(body)) !== null) {
|
|
1068
|
+
const nameMatch = m[1].match(/name\s*=\s*"([^"]*)"/);
|
|
1043
1069
|
if (nameMatch?.[1])
|
|
1044
1070
|
names.push(nameMatch[1]);
|
|
1045
1071
|
}
|
|
1046
1072
|
return names;
|
|
1047
1073
|
}
|
|
1048
1074
|
async function extractTerraformSchema(repoPath) {
|
|
1049
|
-
const schema =
|
|
1075
|
+
const schema = emptySchema();
|
|
1050
1076
|
const tfFiles = findFilesRecursively(repoPath, [".tf"]);
|
|
1051
1077
|
core_1.logger.info(`Found ${tfFiles.length} Terraform file(s)`);
|
|
1052
1078
|
for (const filePath of tfFiles) {
|
|
@@ -1056,38 +1082,88 @@ var require_dist6 = __commonJS({
|
|
|
1056
1082
|
} catch {
|
|
1057
1083
|
continue;
|
|
1058
1084
|
}
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1085
|
+
for (const { resourceType, resourceName, body } of extractTerraformResourceBlocks(content)) {
|
|
1086
|
+
switch (resourceType) {
|
|
1087
|
+
case "aws_dynamodb_table":
|
|
1088
|
+
schema.dynamoTables.push({
|
|
1089
|
+
name: tfStr(body, "name") ?? resourceName,
|
|
1090
|
+
partitionKey: tfStr(body, "hash_key"),
|
|
1091
|
+
sortKey: tfStr(body, "range_key"),
|
|
1092
|
+
gsiNames: tfGSINames(body),
|
|
1093
|
+
source: "terraform",
|
|
1094
|
+
filePath
|
|
1095
|
+
});
|
|
1096
|
+
break;
|
|
1097
|
+
case "aws_db_instance":
|
|
1098
|
+
case "aws_rds_cluster":
|
|
1099
|
+
schema.rdsInstances.push({
|
|
1100
|
+
identifier: tfStr(body, "identifier") ?? tfStr(body, "cluster_identifier") ?? resourceName,
|
|
1101
|
+
engine: tfStr(body, "engine") ?? "unknown",
|
|
1102
|
+
source: "terraform",
|
|
1103
|
+
filePath
|
|
1104
|
+
});
|
|
1105
|
+
break;
|
|
1106
|
+
case "aws_docdb_cluster":
|
|
1107
|
+
schema.mongoClusters.push({
|
|
1108
|
+
identifier: tfStr(body, "cluster_identifier") ?? resourceName,
|
|
1109
|
+
source: "terraform",
|
|
1110
|
+
filePath
|
|
1111
|
+
});
|
|
1112
|
+
break;
|
|
1113
|
+
case "aws_sqs_queue": {
|
|
1114
|
+
const name = tfStr(body, "name") ?? resourceName;
|
|
1115
|
+
const hasDLQ = body.includes("redrive_policy");
|
|
1116
|
+
const encrypted = body.includes("kms_master_key_id") || tfBool(body, "sqs_managed_sse_enabled");
|
|
1117
|
+
schema.queues.push({ name, hasDLQ, encrypted, source: "terraform", filePath });
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
case "aws_sns_topic":
|
|
1121
|
+
schema.topics.push({
|
|
1122
|
+
name: tfStr(body, "name") ?? resourceName,
|
|
1123
|
+
encrypted: body.includes("kms_master_key_id"),
|
|
1124
|
+
source: "terraform",
|
|
1125
|
+
filePath
|
|
1126
|
+
});
|
|
1127
|
+
break;
|
|
1128
|
+
case "aws_lambda_function":
|
|
1129
|
+
schema.lambdas.push({
|
|
1130
|
+
name: tfStr(body, "function_name") ?? resourceName,
|
|
1131
|
+
runtime: tfStr(body, "runtime"),
|
|
1132
|
+
source: "terraform",
|
|
1133
|
+
filePath
|
|
1134
|
+
});
|
|
1135
|
+
break;
|
|
1136
|
+
case "aws_s3_bucket":
|
|
1137
|
+
schema.buckets.push({
|
|
1138
|
+
name: tfStr(body, "bucket") ?? tfStr(body, "bucket_prefix") ?? resourceName,
|
|
1139
|
+
versioned: body.includes("versioning") && body.includes("enabled = true"),
|
|
1140
|
+
source: "terraform",
|
|
1141
|
+
filePath
|
|
1142
|
+
});
|
|
1143
|
+
break;
|
|
1144
|
+
case "aws_ssm_parameter":
|
|
1145
|
+
schema.parameters.push({
|
|
1146
|
+
name: tfStr(body, "name") ?? resourceName,
|
|
1147
|
+
type: tfStr(body, "type") ?? "String",
|
|
1148
|
+
source: "terraform",
|
|
1149
|
+
filePath
|
|
1150
|
+
});
|
|
1151
|
+
break;
|
|
1152
|
+
case "aws_secretsmanager_secret":
|
|
1153
|
+
schema.secrets.push({
|
|
1154
|
+
name: tfStr(body, "name") ?? resourceName,
|
|
1155
|
+
source: "terraform",
|
|
1156
|
+
filePath
|
|
1157
|
+
});
|
|
1158
|
+
break;
|
|
1159
|
+
case "aws_api_gateway_rest_api":
|
|
1160
|
+
case "aws_apigatewayv2_api":
|
|
1161
|
+
schema.apiGateways.push({
|
|
1162
|
+
name: tfStr(body, "name") ?? resourceName,
|
|
1163
|
+
source: "terraform",
|
|
1164
|
+
filePath
|
|
1165
|
+
});
|
|
1166
|
+
break;
|
|
1091
1167
|
}
|
|
1092
1168
|
}
|
|
1093
1169
|
}
|
|
@@ -1106,16 +1182,11 @@ var require_dist6 = __commonJS({
|
|
|
1106
1182
|
} catch {
|
|
1107
1183
|
return null;
|
|
1108
1184
|
}
|
|
1109
|
-
if (!content.includes("AWSTemplateFormatVersion") && !content.includes("Resources"))
|
|
1185
|
+
if (!content.includes("AWSTemplateFormatVersion") && !content.includes("Resources"))
|
|
1110
1186
|
return null;
|
|
1111
|
-
}
|
|
1112
1187
|
let parsed;
|
|
1113
1188
|
try {
|
|
1114
|
-
|
|
1115
|
-
parsed = JSON.parse(content);
|
|
1116
|
-
} else {
|
|
1117
|
-
parsed = js_yaml_1.default.load(content);
|
|
1118
|
-
}
|
|
1189
|
+
parsed = filePath.endsWith(".json") ? JSON.parse(content) : js_yaml_1.default.load(content);
|
|
1119
1190
|
} catch {
|
|
1120
1191
|
return null;
|
|
1121
1192
|
}
|
|
@@ -1123,128 +1194,602 @@ var require_dist6 = __commonJS({
|
|
|
1123
1194
|
return null;
|
|
1124
1195
|
return parsed;
|
|
1125
1196
|
}
|
|
1126
|
-
function
|
|
1197
|
+
function cfnStr(props, ...keys) {
|
|
1127
1198
|
for (const key of keys) {
|
|
1128
|
-
if (typeof
|
|
1129
|
-
return
|
|
1199
|
+
if (typeof props[key] === "string")
|
|
1200
|
+
return props[key];
|
|
1130
1201
|
}
|
|
1131
1202
|
return void 0;
|
|
1132
1203
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
for (const
|
|
1138
|
-
|
|
1139
|
-
if (!parsed)
|
|
1140
|
-
continue;
|
|
1141
|
-
const resources = parsed["Resources"];
|
|
1142
|
-
if (!resources || typeof resources !== "object")
|
|
1204
|
+
function cfnBool(props, key) {
|
|
1205
|
+
return props[key] === true || props[key] === "true" || props[key] === "Enabled";
|
|
1206
|
+
}
|
|
1207
|
+
function processCFNResources(resources, schema, filePath, source) {
|
|
1208
|
+
for (const [logicalId, rawResource] of Object.entries(resources)) {
|
|
1209
|
+
if (typeof rawResource !== "object" || rawResource === null)
|
|
1143
1210
|
continue;
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
if (resourceType === "AWS::DynamoDB::Table") {
|
|
1151
|
-
let partitionKey;
|
|
1152
|
-
let sortKey;
|
|
1211
|
+
const resource = rawResource;
|
|
1212
|
+
const resourceType = resource["Type"];
|
|
1213
|
+
const props = resource["Properties"] ?? {};
|
|
1214
|
+
switch (resourceType) {
|
|
1215
|
+
case "AWS::DynamoDB::Table": {
|
|
1216
|
+
let pk, sk;
|
|
1153
1217
|
const keySchema = props["KeySchema"];
|
|
1154
1218
|
if (Array.isArray(keySchema)) {
|
|
1155
|
-
for (const
|
|
1156
|
-
if (typeof
|
|
1219
|
+
for (const kd of keySchema) {
|
|
1220
|
+
if (typeof kd !== "object" || kd === null)
|
|
1157
1221
|
continue;
|
|
1158
|
-
const
|
|
1159
|
-
if (
|
|
1160
|
-
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
1222
|
+
const k = kd;
|
|
1223
|
+
if (k["KeyType"] === "HASH")
|
|
1224
|
+
pk = k["AttributeName"];
|
|
1225
|
+
if (k["KeyType"] === "RANGE")
|
|
1226
|
+
sk = k["AttributeName"];
|
|
1163
1227
|
}
|
|
1164
1228
|
}
|
|
1165
1229
|
const gsiNames = [];
|
|
1166
1230
|
const gsis = props["GlobalSecondaryIndexes"];
|
|
1167
1231
|
if (Array.isArray(gsis)) {
|
|
1168
|
-
for (const
|
|
1169
|
-
if (typeof
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1232
|
+
for (const g of gsis) {
|
|
1233
|
+
if (typeof g === "object" && g !== null) {
|
|
1234
|
+
const gi = g;
|
|
1235
|
+
if (typeof gi["IndexName"] === "string")
|
|
1236
|
+
gsiNames.push(gi["IndexName"]);
|
|
1237
|
+
}
|
|
1174
1238
|
}
|
|
1175
1239
|
}
|
|
1176
|
-
const tableName = getStringProp(props, "TableName") ?? logicalId;
|
|
1177
1240
|
schema.dynamoTables.push({
|
|
1178
|
-
name:
|
|
1179
|
-
partitionKey,
|
|
1180
|
-
sortKey,
|
|
1241
|
+
name: cfnStr(props, "TableName") ?? logicalId,
|
|
1242
|
+
partitionKey: pk,
|
|
1243
|
+
sortKey: sk,
|
|
1181
1244
|
gsiNames,
|
|
1182
|
-
source
|
|
1245
|
+
source,
|
|
1183
1246
|
filePath
|
|
1184
1247
|
});
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1248
|
+
break;
|
|
1249
|
+
}
|
|
1250
|
+
case "AWS::RDS::DBInstance":
|
|
1251
|
+
schema.rdsInstances.push({
|
|
1252
|
+
identifier: cfnStr(props, "DBInstanceIdentifier") ?? logicalId,
|
|
1253
|
+
engine: cfnStr(props, "Engine") ?? "unknown",
|
|
1254
|
+
source,
|
|
1255
|
+
filePath
|
|
1256
|
+
});
|
|
1257
|
+
break;
|
|
1258
|
+
case "AWS::RDS::DBCluster":
|
|
1188
1259
|
schema.rdsInstances.push({
|
|
1189
|
-
identifier,
|
|
1190
|
-
engine,
|
|
1191
|
-
source
|
|
1260
|
+
identifier: cfnStr(props, "DBClusterIdentifier") ?? logicalId,
|
|
1261
|
+
engine: cfnStr(props, "Engine") ?? "aurora",
|
|
1262
|
+
source,
|
|
1192
1263
|
filePath
|
|
1193
1264
|
});
|
|
1194
|
-
|
|
1195
|
-
|
|
1265
|
+
break;
|
|
1266
|
+
case "AWS::DocDB::DBCluster":
|
|
1196
1267
|
schema.mongoClusters.push({
|
|
1197
|
-
identifier,
|
|
1198
|
-
source
|
|
1268
|
+
identifier: cfnStr(props, "DBClusterIdentifier") ?? logicalId,
|
|
1269
|
+
source,
|
|
1270
|
+
filePath
|
|
1271
|
+
});
|
|
1272
|
+
break;
|
|
1273
|
+
case "AWS::SQS::Queue": {
|
|
1274
|
+
const name = cfnStr(props, "QueueName") ?? logicalId;
|
|
1275
|
+
const hasDLQ = !!props["RedrivePolicy"];
|
|
1276
|
+
const encrypted = !!(props["KmsMasterKeyId"] || cfnBool(props, "SqsManagedSseEnabled"));
|
|
1277
|
+
schema.queues.push({ name, hasDLQ, encrypted, source, filePath });
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
case "AWS::SNS::Topic":
|
|
1281
|
+
schema.topics.push({
|
|
1282
|
+
name: cfnStr(props, "TopicName") ?? logicalId,
|
|
1283
|
+
encrypted: !!props["KmsMasterKeyId"],
|
|
1284
|
+
source,
|
|
1285
|
+
filePath
|
|
1286
|
+
});
|
|
1287
|
+
break;
|
|
1288
|
+
case "AWS::Lambda::Function":
|
|
1289
|
+
schema.lambdas.push({
|
|
1290
|
+
name: cfnStr(props, "FunctionName") ?? logicalId,
|
|
1291
|
+
runtime: cfnStr(props, "Runtime"),
|
|
1292
|
+
source,
|
|
1293
|
+
filePath
|
|
1294
|
+
});
|
|
1295
|
+
break;
|
|
1296
|
+
case "AWS::S3::Bucket": {
|
|
1297
|
+
const versioningConfig = props["VersioningConfiguration"];
|
|
1298
|
+
schema.buckets.push({
|
|
1299
|
+
name: cfnStr(props, "BucketName") ?? logicalId,
|
|
1300
|
+
versioned: versioningConfig?.["Status"] === "Enabled",
|
|
1301
|
+
source,
|
|
1199
1302
|
filePath
|
|
1200
1303
|
});
|
|
1304
|
+
break;
|
|
1201
1305
|
}
|
|
1306
|
+
case "AWS::SSM::Parameter":
|
|
1307
|
+
schema.parameters.push({
|
|
1308
|
+
name: cfnStr(props, "Name") ?? logicalId,
|
|
1309
|
+
type: cfnStr(props, "Type") ?? "String",
|
|
1310
|
+
source,
|
|
1311
|
+
filePath
|
|
1312
|
+
});
|
|
1313
|
+
break;
|
|
1314
|
+
case "AWS::SecretsManager::Secret":
|
|
1315
|
+
schema.secrets.push({
|
|
1316
|
+
name: cfnStr(props, "Name") ?? logicalId,
|
|
1317
|
+
source,
|
|
1318
|
+
filePath
|
|
1319
|
+
});
|
|
1320
|
+
break;
|
|
1321
|
+
case "AWS::ApiGateway::RestApi":
|
|
1322
|
+
case "AWS::ApiGatewayV2::Api":
|
|
1323
|
+
schema.apiGateways.push({
|
|
1324
|
+
name: cfnStr(props, "Name") ?? logicalId,
|
|
1325
|
+
source,
|
|
1326
|
+
filePath
|
|
1327
|
+
});
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async function extractCloudFormationSchema(repoPath) {
|
|
1333
|
+
const schema = emptySchema();
|
|
1334
|
+
const cfnFiles = findFilesRecursively(repoPath, [".yaml", ".yml", ".json"]);
|
|
1335
|
+
core_1.logger.info(`Scanning ${cfnFiles.length} potential CloudFormation file(s)`);
|
|
1336
|
+
for (const filePath of cfnFiles) {
|
|
1337
|
+
const parsed = parseCFNFile(filePath);
|
|
1338
|
+
if (!parsed)
|
|
1339
|
+
continue;
|
|
1340
|
+
const resources = parsed["Resources"];
|
|
1341
|
+
if (!resources)
|
|
1342
|
+
continue;
|
|
1343
|
+
processCFNResources(resources, schema, filePath, "cloudformation");
|
|
1344
|
+
}
|
|
1345
|
+
return schema;
|
|
1346
|
+
}
|
|
1347
|
+
async function extractCDKSchema(repoPath) {
|
|
1348
|
+
const schema = emptySchema();
|
|
1349
|
+
const cdkOutDir = path5.join(repoPath, "cdk.out");
|
|
1350
|
+
if (fs4.existsSync(cdkOutDir)) {
|
|
1351
|
+
const templateFiles = fs4.readdirSync(cdkOutDir).filter((f) => f.endsWith(".template.json")).map((f) => path5.join(cdkOutDir, f));
|
|
1352
|
+
core_1.logger.info(`Found ${templateFiles.length} CDK synthesized template(s) in cdk.out/`);
|
|
1353
|
+
for (const filePath of templateFiles) {
|
|
1354
|
+
const parsed = parseCFNFile(filePath);
|
|
1355
|
+
if (!parsed)
|
|
1356
|
+
continue;
|
|
1357
|
+
const resources = parsed["Resources"];
|
|
1358
|
+
if (!resources)
|
|
1359
|
+
continue;
|
|
1360
|
+
processCFNResources(resources, schema, filePath, "cdk");
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (schema.queues.length === 0 && schema.topics.length === 0 && schema.lambdas.length === 0) {
|
|
1364
|
+
const cdkJsonPath = path5.join(repoPath, "cdk.json");
|
|
1365
|
+
if (fs4.existsSync(cdkJsonPath)) {
|
|
1366
|
+
core_1.logger.info("CDK project detected (cdk.json found) \u2014 run `cdk synth` for full IaC analysis");
|
|
1202
1367
|
}
|
|
1203
1368
|
}
|
|
1204
1369
|
return schema;
|
|
1205
1370
|
}
|
|
1371
|
+
function mergeSchemas(...schemas) {
|
|
1372
|
+
const merged = emptySchema();
|
|
1373
|
+
const seen = {
|
|
1374
|
+
dynamo: /* @__PURE__ */ new Set(),
|
|
1375
|
+
rds: /* @__PURE__ */ new Set(),
|
|
1376
|
+
mongo: /* @__PURE__ */ new Set(),
|
|
1377
|
+
queue: /* @__PURE__ */ new Set(),
|
|
1378
|
+
topic: /* @__PURE__ */ new Set(),
|
|
1379
|
+
lambda: /* @__PURE__ */ new Set(),
|
|
1380
|
+
bucket: /* @__PURE__ */ new Set(),
|
|
1381
|
+
param: /* @__PURE__ */ new Set(),
|
|
1382
|
+
secret: /* @__PURE__ */ new Set(),
|
|
1383
|
+
api: /* @__PURE__ */ new Set()
|
|
1384
|
+
};
|
|
1385
|
+
for (const schema of schemas) {
|
|
1386
|
+
for (const t of schema.dynamoTables) {
|
|
1387
|
+
const k = `${t.source}::${t.name}`;
|
|
1388
|
+
if (!seen.dynamo.has(k)) {
|
|
1389
|
+
seen.dynamo.add(k);
|
|
1390
|
+
merged.dynamoTables.push(t);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
for (const r of schema.rdsInstances) {
|
|
1394
|
+
const k = `${r.source}::${r.identifier}`;
|
|
1395
|
+
if (!seen.rds.has(k)) {
|
|
1396
|
+
seen.rds.add(k);
|
|
1397
|
+
merged.rdsInstances.push(r);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
for (const m of schema.mongoClusters) {
|
|
1401
|
+
const k = `${m.source}::${m.identifier}`;
|
|
1402
|
+
if (!seen.mongo.has(k)) {
|
|
1403
|
+
seen.mongo.add(k);
|
|
1404
|
+
merged.mongoClusters.push(m);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
for (const q of schema.queues) {
|
|
1408
|
+
const k = `${q.source}::${q.name}`;
|
|
1409
|
+
if (!seen.queue.has(k)) {
|
|
1410
|
+
seen.queue.add(k);
|
|
1411
|
+
merged.queues.push(q);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
for (const t of schema.topics) {
|
|
1415
|
+
const k = `${t.source}::${t.name}`;
|
|
1416
|
+
if (!seen.topic.has(k)) {
|
|
1417
|
+
seen.topic.add(k);
|
|
1418
|
+
merged.topics.push(t);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
for (const l of schema.lambdas) {
|
|
1422
|
+
const k = `${l.source}::${l.name}`;
|
|
1423
|
+
if (!seen.lambda.has(k)) {
|
|
1424
|
+
seen.lambda.add(k);
|
|
1425
|
+
merged.lambdas.push(l);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
for (const b of schema.buckets) {
|
|
1429
|
+
const k = `${b.source}::${b.name}`;
|
|
1430
|
+
if (!seen.bucket.has(k)) {
|
|
1431
|
+
seen.bucket.add(k);
|
|
1432
|
+
merged.buckets.push(b);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
for (const p of schema.parameters) {
|
|
1436
|
+
const k = `${p.source}::${p.name}`;
|
|
1437
|
+
if (!seen.param.has(k)) {
|
|
1438
|
+
seen.param.add(k);
|
|
1439
|
+
merged.parameters.push(p);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
for (const s of schema.secrets) {
|
|
1443
|
+
const k = `${s.source}::${s.name}`;
|
|
1444
|
+
if (!seen.secret.has(k)) {
|
|
1445
|
+
seen.secret.add(k);
|
|
1446
|
+
merged.secrets.push(s);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
for (const a of schema.apiGateways) {
|
|
1450
|
+
const k = `${a.source}::${a.name}`;
|
|
1451
|
+
if (!seen.api.has(k)) {
|
|
1452
|
+
seen.api.add(k);
|
|
1453
|
+
merged.apiGateways.push(a);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return merged;
|
|
1458
|
+
}
|
|
1206
1459
|
async function extractIaCSchema2(repoPath) {
|
|
1207
|
-
const [tfSchema, cfnSchema] = await Promise.all([
|
|
1460
|
+
const [tfSchema, cfnSchema, cdkSchema] = await Promise.all([
|
|
1208
1461
|
extractTerraformSchema(repoPath),
|
|
1209
|
-
extractCloudFormationSchema(repoPath)
|
|
1462
|
+
extractCloudFormationSchema(repoPath),
|
|
1463
|
+
extractCDKSchema(repoPath)
|
|
1210
1464
|
]);
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
const seenRds = /* @__PURE__ */ new Set();
|
|
1216
|
-
const seenMongo = /* @__PURE__ */ new Set();
|
|
1217
|
-
const dynamoTables = [];
|
|
1218
|
-
const rdsInstances = [];
|
|
1219
|
-
const mongoClusters = [];
|
|
1220
|
-
for (const t of [...tfSchema.dynamoTables, ...cfnSchema.dynamoTables]) {
|
|
1221
|
-
const k = dynamoKey(t);
|
|
1222
|
-
if (!seenDynamo.has(k)) {
|
|
1223
|
-
seenDynamo.add(k);
|
|
1224
|
-
dynamoTables.push(t);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
for (const r of [...tfSchema.rdsInstances, ...cfnSchema.rdsInstances]) {
|
|
1228
|
-
const k = rdsKey(r);
|
|
1229
|
-
if (!seenRds.has(k)) {
|
|
1230
|
-
seenRds.add(k);
|
|
1231
|
-
rdsInstances.push(r);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
for (const m of [...tfSchema.mongoClusters, ...cfnSchema.mongoClusters]) {
|
|
1235
|
-
const k = mongoKey(m);
|
|
1236
|
-
if (!seenMongo.has(k)) {
|
|
1237
|
-
seenMongo.add(k);
|
|
1238
|
-
mongoClusters.push(m);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
return { dynamoTables, rdsInstances, mongoClusters };
|
|
1465
|
+
const merged = mergeSchemas(tfSchema, cfnSchema, cdkSchema);
|
|
1466
|
+
const total = merged.dynamoTables.length + merged.rdsInstances.length + merged.mongoClusters.length + merged.queues.length + merged.topics.length + merged.lambdas.length + merged.buckets.length + merged.parameters.length + merged.secrets.length + merged.apiGateways.length;
|
|
1467
|
+
core_1.logger.info(`IaC schema total: ${total} resource(s) across TF/CFN/CDK`);
|
|
1468
|
+
return merged;
|
|
1242
1469
|
}
|
|
1243
1470
|
}
|
|
1244
1471
|
});
|
|
1245
1472
|
|
|
1246
|
-
// ../
|
|
1473
|
+
// ../adapters/aws-services/dist/index.js
|
|
1247
1474
|
var require_dist7 = __commonJS({
|
|
1475
|
+
"../adapters/aws-services/dist/index.js"(exports2) {
|
|
1476
|
+
"use strict";
|
|
1477
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1478
|
+
exports2.extractSQSMetadata = extractSQSMetadata2;
|
|
1479
|
+
exports2.validateSQSAccess = validateSQSAccess2;
|
|
1480
|
+
exports2.extractSNSMetadata = extractSNSMetadata2;
|
|
1481
|
+
exports2.validateSNSAccess = validateSNSAccess2;
|
|
1482
|
+
exports2.extractSSMMetadata = extractSSMMetadata2;
|
|
1483
|
+
exports2.validateSSMAccess = validateSSMAccess2;
|
|
1484
|
+
exports2.extractSecretsMetadata = extractSecretsMetadata2;
|
|
1485
|
+
exports2.validateSecretsAccess = validateSecretsAccess2;
|
|
1486
|
+
exports2.extractLambdaMetadata = extractLambdaMetadata2;
|
|
1487
|
+
exports2.validateLambdaAccess = validateLambdaAccess2;
|
|
1488
|
+
var client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
1489
|
+
var client_sns_1 = require("@aws-sdk/client-sns");
|
|
1490
|
+
var client_ssm_1 = require("@aws-sdk/client-ssm");
|
|
1491
|
+
var client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
1492
|
+
var client_lambda_1 = require("@aws-sdk/client-lambda");
|
|
1493
|
+
var credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
1494
|
+
var core_1 = require_dist();
|
|
1495
|
+
function clientConfig(cfg) {
|
|
1496
|
+
const region = cfg.region ?? "us-east-1";
|
|
1497
|
+
return cfg.profile ? { region, credentials: (0, credential_providers_1.fromIni)({ profile: cfg.profile }) } : { region };
|
|
1498
|
+
}
|
|
1499
|
+
async function extractSQSMetadata2(cfg = {}) {
|
|
1500
|
+
const client = new client_sqs_1.SQSClient(clientConfig(cfg));
|
|
1501
|
+
const queues = [];
|
|
1502
|
+
try {
|
|
1503
|
+
let nextToken;
|
|
1504
|
+
const queueUrls = [];
|
|
1505
|
+
do {
|
|
1506
|
+
const res = await client.send(new client_sqs_1.ListQueuesCommand({ NextToken: nextToken, MaxResults: 1e3 }));
|
|
1507
|
+
queueUrls.push(...res.QueueUrls ?? []);
|
|
1508
|
+
nextToken = res.NextToken;
|
|
1509
|
+
} while (nextToken);
|
|
1510
|
+
for (const url of queueUrls) {
|
|
1511
|
+
try {
|
|
1512
|
+
const attrs = await client.send(new client_sqs_1.GetQueueAttributesCommand({
|
|
1513
|
+
QueueUrl: url,
|
|
1514
|
+
AttributeNames: [
|
|
1515
|
+
"QueueArn",
|
|
1516
|
+
"VisibilityTimeout",
|
|
1517
|
+
"MessageRetentionPeriod",
|
|
1518
|
+
"RedrivePolicy",
|
|
1519
|
+
"KmsMasterKeyId",
|
|
1520
|
+
"SqsManagedSseEnabled",
|
|
1521
|
+
"ApproximateNumberOfMessages",
|
|
1522
|
+
"ApproximateNumberOfMessagesNotVisible"
|
|
1523
|
+
]
|
|
1524
|
+
}));
|
|
1525
|
+
const a = attrs.Attributes ?? {};
|
|
1526
|
+
const arn = a["QueueArn"] ?? "";
|
|
1527
|
+
const name = arn.split(":").pop() ?? url.split("/").pop() ?? url;
|
|
1528
|
+
const redrivePolicy = a["RedrivePolicy"];
|
|
1529
|
+
const dlqArn = redrivePolicy ? JSON.parse(redrivePolicy).deadLetterTargetArn : void 0;
|
|
1530
|
+
const encrypted = !!(a["KmsMasterKeyId"] || a["SqsManagedSseEnabled"] === "true");
|
|
1531
|
+
const retentionSeconds = parseInt(a["MessageRetentionPeriod"] ?? "345600", 10);
|
|
1532
|
+
queues.push({
|
|
1533
|
+
name,
|
|
1534
|
+
url,
|
|
1535
|
+
arn,
|
|
1536
|
+
hasDLQ: !!dlqArn,
|
|
1537
|
+
dlqArn,
|
|
1538
|
+
encrypted,
|
|
1539
|
+
visibilityTimeoutSec: parseInt(a["VisibilityTimeout"] ?? "30", 10),
|
|
1540
|
+
retentionDays: Math.round(retentionSeconds / 86400),
|
|
1541
|
+
approximateMessages: parseInt(a["ApproximateNumberOfMessages"] ?? "0", 10),
|
|
1542
|
+
approximateInflight: parseInt(a["ApproximateNumberOfMessagesNotVisible"] ?? "0", 10)
|
|
1543
|
+
});
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
core_1.logger.warn(`SQS attrs failed for ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
core_1.logger.warn(`SQS list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1550
|
+
}
|
|
1551
|
+
return queues;
|
|
1552
|
+
}
|
|
1553
|
+
async function validateSQSAccess2(cfg = {}) {
|
|
1554
|
+
await new client_sqs_1.SQSClient(clientConfig(cfg)).send(new client_sqs_1.ListQueuesCommand({ MaxResults: 1 }));
|
|
1555
|
+
}
|
|
1556
|
+
async function extractSNSMetadata2(cfg = {}) {
|
|
1557
|
+
const client = new client_sns_1.SNSClient(clientConfig(cfg));
|
|
1558
|
+
const topics = [];
|
|
1559
|
+
try {
|
|
1560
|
+
let nextToken;
|
|
1561
|
+
const topicArns = [];
|
|
1562
|
+
do {
|
|
1563
|
+
const res = await client.send(new client_sns_1.ListTopicsCommand({ NextToken: nextToken }));
|
|
1564
|
+
topicArns.push(...(res.Topics ?? []).map((t) => t.TopicArn ?? "").filter(Boolean));
|
|
1565
|
+
nextToken = res.NextToken;
|
|
1566
|
+
} while (nextToken);
|
|
1567
|
+
for (const arn of topicArns) {
|
|
1568
|
+
try {
|
|
1569
|
+
const [attrsRes, subsRes] = await Promise.all([
|
|
1570
|
+
client.send(new client_sns_1.GetTopicAttributesCommand({ TopicArn: arn })),
|
|
1571
|
+
client.send(new client_sns_1.ListSubscriptionsByTopicCommand({ TopicArn: arn }))
|
|
1572
|
+
]);
|
|
1573
|
+
const attrs = attrsRes.Attributes ?? {};
|
|
1574
|
+
const subs = subsRes.Subscriptions ?? [];
|
|
1575
|
+
topics.push({
|
|
1576
|
+
name: arn.split(":").pop() ?? arn,
|
|
1577
|
+
arn,
|
|
1578
|
+
encrypted: !!attrs["KmsMasterKeyId"],
|
|
1579
|
+
subscriptionCount: subs.length,
|
|
1580
|
+
subscriptionProtocols: [...new Set(subs.map((s) => s.Protocol ?? "unknown"))]
|
|
1581
|
+
});
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
core_1.logger.warn(`SNS attrs failed for ${arn}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
core_1.logger.warn(`SNS list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1588
|
+
}
|
|
1589
|
+
return topics;
|
|
1590
|
+
}
|
|
1591
|
+
async function validateSNSAccess2(cfg = {}) {
|
|
1592
|
+
await new client_sns_1.SNSClient(clientConfig(cfg)).send(new client_sns_1.ListTopicsCommand({}));
|
|
1593
|
+
}
|
|
1594
|
+
async function extractSSMMetadata2(cfg = {}) {
|
|
1595
|
+
const client = new client_ssm_1.SSMClient(clientConfig(cfg));
|
|
1596
|
+
const parameters = [];
|
|
1597
|
+
try {
|
|
1598
|
+
let nextToken;
|
|
1599
|
+
do {
|
|
1600
|
+
const res = await client.send(new client_ssm_1.DescribeParametersCommand({
|
|
1601
|
+
NextToken: nextToken,
|
|
1602
|
+
MaxResults: 50
|
|
1603
|
+
// Only metadata — GetParameter/GetParameters would return values
|
|
1604
|
+
}));
|
|
1605
|
+
for (const p of res.Parameters ?? []) {
|
|
1606
|
+
parameters.push({
|
|
1607
|
+
name: p.Name ?? "",
|
|
1608
|
+
type: p.Type ?? "String",
|
|
1609
|
+
tier: p.Tier ?? "Standard",
|
|
1610
|
+
lastModified: p.LastModifiedDate?.toISOString(),
|
|
1611
|
+
description: p.Description,
|
|
1612
|
+
keyId: p.KeyId
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
nextToken = res.NextToken;
|
|
1616
|
+
} while (nextToken && parameters.length < 500);
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
core_1.logger.warn(`SSM list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1619
|
+
}
|
|
1620
|
+
return parameters;
|
|
1621
|
+
}
|
|
1622
|
+
async function validateSSMAccess2(cfg = {}) {
|
|
1623
|
+
await new client_ssm_1.SSMClient(clientConfig(cfg)).send(new client_ssm_1.DescribeParametersCommand({ MaxResults: 1 }));
|
|
1624
|
+
}
|
|
1625
|
+
async function extractSecretsMetadata2(cfg = {}) {
|
|
1626
|
+
const client = new client_secrets_manager_1.SecretsManagerClient(clientConfig(cfg));
|
|
1627
|
+
const secrets = [];
|
|
1628
|
+
try {
|
|
1629
|
+
let nextToken;
|
|
1630
|
+
do {
|
|
1631
|
+
const res = await client.send(new client_secrets_manager_1.ListSecretsCommand({ NextToken: nextToken, MaxResults: 100 }));
|
|
1632
|
+
for (const s of res.SecretList ?? []) {
|
|
1633
|
+
secrets.push({
|
|
1634
|
+
name: s.Name ?? "",
|
|
1635
|
+
arn: s.ARN ?? "",
|
|
1636
|
+
rotationEnabled: s.RotationEnabled ?? false,
|
|
1637
|
+
rotationDays: s.RotationRules?.AutomaticallyAfterDays,
|
|
1638
|
+
lastRotated: s.LastRotatedDate?.toISOString(),
|
|
1639
|
+
lastAccessed: s.LastAccessedDate?.toISOString(),
|
|
1640
|
+
description: s.Description
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
nextToken = res.NextToken;
|
|
1644
|
+
} while (nextToken && secrets.length < 200);
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
core_1.logger.warn(`Secrets Manager list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1647
|
+
}
|
|
1648
|
+
return secrets;
|
|
1649
|
+
}
|
|
1650
|
+
async function validateSecretsAccess2(cfg = {}) {
|
|
1651
|
+
await new client_secrets_manager_1.SecretsManagerClient(clientConfig(cfg)).send(new client_secrets_manager_1.ListSecretsCommand({ MaxResults: 1 }));
|
|
1652
|
+
}
|
|
1653
|
+
async function extractLambdaMetadata2(cfg = {}) {
|
|
1654
|
+
const client = new client_lambda_1.LambdaClient(clientConfig(cfg));
|
|
1655
|
+
const functions = [];
|
|
1656
|
+
try {
|
|
1657
|
+
let marker;
|
|
1658
|
+
do {
|
|
1659
|
+
const res = await client.send(new client_lambda_1.ListFunctionsCommand({ Marker: marker, MaxItems: 50 }));
|
|
1660
|
+
for (const fn of res.Functions ?? []) {
|
|
1661
|
+
functions.push({
|
|
1662
|
+
name: fn.FunctionName ?? "",
|
|
1663
|
+
arn: fn.FunctionArn ?? "",
|
|
1664
|
+
runtime: fn.Runtime,
|
|
1665
|
+
handler: fn.Handler,
|
|
1666
|
+
memoryMB: fn.MemorySize,
|
|
1667
|
+
timeoutSec: fn.Timeout,
|
|
1668
|
+
lastModified: fn.LastModified,
|
|
1669
|
+
envVarKeys: Object.keys(fn.Environment?.Variables ?? {}),
|
|
1670
|
+
layers: (fn.Layers ?? []).map((l) => l.Arn ?? "").filter(Boolean)
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
marker = res.NextMarker;
|
|
1674
|
+
} while (marker && functions.length < 200);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
core_1.logger.warn(`Lambda list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1677
|
+
}
|
|
1678
|
+
return functions;
|
|
1679
|
+
}
|
|
1680
|
+
async function validateLambdaAccess2(cfg = {}) {
|
|
1681
|
+
await new client_lambda_1.LambdaClient(clientConfig(cfg)).send(new client_lambda_1.ListFunctionsCommand({ MaxItems: 1 }));
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
// ../adapters/logs/dist/index.js
|
|
1687
|
+
var require_dist8 = __commonJS({
|
|
1688
|
+
"../adapters/logs/dist/index.js"(exports2) {
|
|
1689
|
+
"use strict";
|
|
1690
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1691
|
+
exports2.extractLogsSummary = extractLogsSummary2;
|
|
1692
|
+
exports2.validateLogsAccess = validateLogsAccess2;
|
|
1693
|
+
var client_cloudwatch_logs_1 = require("@aws-sdk/client-cloudwatch-logs");
|
|
1694
|
+
var credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
1695
|
+
var core_1 = require_dist();
|
|
1696
|
+
var MAX_LOG_GROUPS = 50;
|
|
1697
|
+
var MAX_EVENTS_PER_GROUP = 50;
|
|
1698
|
+
function clientConfig(cfg) {
|
|
1699
|
+
const region = cfg.region ?? "us-east-1";
|
|
1700
|
+
return cfg.profile ? { region, credentials: (0, credential_providers_1.fromIni)({ profile: cfg.profile }) } : { region };
|
|
1701
|
+
}
|
|
1702
|
+
function toPattern(message) {
|
|
1703
|
+
return message.slice(0, 200).replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<UUID>").replace(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?Z?/g, "<TIMESTAMP>").replace(/\b\d{5,}\b/g, "<NUM>").replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, "<IP>").replace(/Bearer\s+\S+/gi, "Bearer <TOKEN>").trim();
|
|
1704
|
+
}
|
|
1705
|
+
function topPatterns(messages, limit = 5) {
|
|
1706
|
+
const counts = {};
|
|
1707
|
+
for (const msg of messages) {
|
|
1708
|
+
const p = toPattern(msg);
|
|
1709
|
+
counts[p] = (counts[p] ?? 0) + 1;
|
|
1710
|
+
}
|
|
1711
|
+
return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([pattern, count]) => ({ pattern, count }));
|
|
1712
|
+
}
|
|
1713
|
+
async function extractLogsSummary2(cfg = {}) {
|
|
1714
|
+
const client = new client_cloudwatch_logs_1.CloudWatchLogsClient(clientConfig(cfg));
|
|
1715
|
+
const windowMs = (cfg.windowHours ?? 24) * 60 * 60 * 1e3;
|
|
1716
|
+
const startTime = Date.now() - windowMs;
|
|
1717
|
+
const summaries = [];
|
|
1718
|
+
const logGroups = [];
|
|
1719
|
+
try {
|
|
1720
|
+
const prefixes = cfg.logGroupPrefixes?.length ? cfg.logGroupPrefixes : [void 0];
|
|
1721
|
+
for (const prefix of prefixes) {
|
|
1722
|
+
let nextToken;
|
|
1723
|
+
do {
|
|
1724
|
+
const res = await client.send(new client_cloudwatch_logs_1.DescribeLogGroupsCommand({
|
|
1725
|
+
nextToken,
|
|
1726
|
+
limit: Math.min(50, MAX_LOG_GROUPS - logGroups.length),
|
|
1727
|
+
...prefix ? { logGroupNamePrefix: prefix } : {}
|
|
1728
|
+
}));
|
|
1729
|
+
for (const lg of res.logGroups ?? []) {
|
|
1730
|
+
logGroups.push({ name: lg.logGroupName ?? "", retentionDays: lg.retentionInDays });
|
|
1731
|
+
}
|
|
1732
|
+
nextToken = res.nextToken;
|
|
1733
|
+
if (logGroups.length >= MAX_LOG_GROUPS)
|
|
1734
|
+
break;
|
|
1735
|
+
} while (nextToken);
|
|
1736
|
+
if (logGroups.length >= MAX_LOG_GROUPS)
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
core_1.logger.warn(`CloudWatch Logs discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1741
|
+
return summaries;
|
|
1742
|
+
}
|
|
1743
|
+
for (const lg of logGroups) {
|
|
1744
|
+
const errorMessages = [];
|
|
1745
|
+
const warnMessages = [];
|
|
1746
|
+
let lastErrorTime;
|
|
1747
|
+
for (const filterPattern of ["ERROR", "Exception", "WARN"]) {
|
|
1748
|
+
if (errorMessages.length + warnMessages.length >= MAX_EVENTS_PER_GROUP)
|
|
1749
|
+
break;
|
|
1750
|
+
try {
|
|
1751
|
+
const res = await client.send(new client_cloudwatch_logs_1.FilterLogEventsCommand({
|
|
1752
|
+
logGroupName: lg.name,
|
|
1753
|
+
filterPattern,
|
|
1754
|
+
startTime,
|
|
1755
|
+
limit: 25
|
|
1756
|
+
}));
|
|
1757
|
+
for (const event of res.events ?? []) {
|
|
1758
|
+
const msg = event.message ?? "";
|
|
1759
|
+
if (filterPattern === "WARN") {
|
|
1760
|
+
warnMessages.push(msg);
|
|
1761
|
+
} else {
|
|
1762
|
+
errorMessages.push(msg);
|
|
1763
|
+
if (event.timestamp) {
|
|
1764
|
+
const ts = new Date(event.timestamp).toISOString();
|
|
1765
|
+
if (!lastErrorTime || ts > lastErrorTime)
|
|
1766
|
+
lastErrorTime = ts;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
} catch {
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
summaries.push({
|
|
1774
|
+
logGroupName: lg.name,
|
|
1775
|
+
retentionDays: lg.retentionDays,
|
|
1776
|
+
errorCount: errorMessages.length,
|
|
1777
|
+
warnCount: warnMessages.length,
|
|
1778
|
+
topErrorPatterns: topPatterns(errorMessages),
|
|
1779
|
+
lastErrorTime
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
core_1.logger.info(`CloudWatch Logs: sampled ${summaries.length} log group(s)`);
|
|
1783
|
+
return summaries;
|
|
1784
|
+
}
|
|
1785
|
+
async function validateLogsAccess2(cfg = {}) {
|
|
1786
|
+
await new client_cloudwatch_logs_1.CloudWatchLogsClient(clientConfig(cfg)).send(new client_cloudwatch_logs_1.DescribeLogGroupsCommand({ limit: 1 }));
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// ../context/dist/index.js
|
|
1792
|
+
var require_dist9 = __commonJS({
|
|
1248
1793
|
"../context/dist/index.js"(exports2) {
|
|
1249
1794
|
"use strict";
|
|
1250
1795
|
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -1329,6 +1874,16 @@ var require_dist7 = __commonJS({
|
|
|
1329
1874
|
"estimatedDocumentCount"
|
|
1330
1875
|
]);
|
|
1331
1876
|
var MONGO_COLLECTION_METHODS = /* @__PURE__ */ new Set(["collection"]);
|
|
1877
|
+
var SQS_COMMANDS = /* @__PURE__ */ new Set(["SendMessageCommand", "SendMessageBatchCommand", "ReceiveMessageCommand", "DeleteMessageCommand", "sendMessage", "sendMessageBatch", "receiveMessage"]);
|
|
1878
|
+
var SNS_COMMANDS = /* @__PURE__ */ new Set(["PublishCommand", "PublishBatchCommand", "publish", "publishBatch"]);
|
|
1879
|
+
var SSM_COMMANDS = /* @__PURE__ */ new Set(["GetParameterCommand", "GetParametersCommand", "GetParametersByPathCommand", "getParameter", "getParameters", "getParametersByPath"]);
|
|
1880
|
+
var SECRETS_COMMANDS = /* @__PURE__ */ new Set(["GetSecretValueCommand", "getSecretValue"]);
|
|
1881
|
+
var LAMBDA_COMMANDS = /* @__PURE__ */ new Set(["InvokeCommand", "InvokeAsyncCommand", "invoke", "invokeAsync"]);
|
|
1882
|
+
var SQS_ARG_KEYS = ["QueueUrl", "QueueName"];
|
|
1883
|
+
var SNS_ARG_KEYS = ["TopicArn", "TargetArn"];
|
|
1884
|
+
var SSM_ARG_KEYS = ["Name", "Path"];
|
|
1885
|
+
var SECRETS_ARG_KEYS = ["SecretId", "SecretName"];
|
|
1886
|
+
var LAMBDA_ARG_KEYS = ["FunctionName"];
|
|
1332
1887
|
var PRISMA_METHODS = /* @__PURE__ */ new Set([
|
|
1333
1888
|
"findMany",
|
|
1334
1889
|
"findFirst",
|
|
@@ -1603,67 +2158,192 @@ var require_dist7 = __commonJS({
|
|
|
1603
2158
|
}
|
|
1604
2159
|
return null;
|
|
1605
2160
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
compilerOptions: hasTsConfig ? void 0 : {
|
|
1616
|
-
target: 99,
|
|
1617
|
-
allowJs: true
|
|
1618
|
-
},
|
|
1619
|
-
skipAddingFilesFromTsConfig: !hasTsConfig
|
|
1620
|
-
});
|
|
1621
|
-
if (!hasTsConfig) {
|
|
1622
|
-
project.addSourceFilesAtPaths([
|
|
1623
|
-
path5.join(resolvedPath, "**/*.ts"),
|
|
1624
|
-
path5.join(resolvedPath, "**/*.tsx"),
|
|
1625
|
-
`!${path5.join(resolvedPath, "**/node_modules/**")}`,
|
|
1626
|
-
`!${path5.join(resolvedPath, "**/dist/**")}`
|
|
1627
|
-
]);
|
|
1628
|
-
}
|
|
1629
|
-
const sourceFiles = project.getSourceFiles();
|
|
1630
|
-
core_1.logger.info(`Scanning ${sourceFiles.length} TypeScript file(s) in ${resolvedPath}`);
|
|
1631
|
-
const operations = [];
|
|
1632
|
-
for (const sourceFile of sourceFiles) {
|
|
1633
|
-
const filePath = sourceFile.getFilePath();
|
|
1634
|
-
if (filePath.includes("node_modules") || filePath.includes("/dist/"))
|
|
1635
|
-
continue;
|
|
1636
|
-
const callExpressions = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
|
|
1637
|
-
for (const callExpr of callExpressions) {
|
|
1638
|
-
const dynamoOp = detectDynamoOperations(callExpr, filePath);
|
|
1639
|
-
if (dynamoOp) {
|
|
1640
|
-
operations.push(dynamoOp);
|
|
1641
|
-
continue;
|
|
1642
|
-
}
|
|
1643
|
-
const postgresOp = detectPostgresOperations(callExpr, filePath);
|
|
1644
|
-
if (postgresOp) {
|
|
1645
|
-
operations.push(postgresOp);
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
const mysqlOp = detectMySQLOperations(callExpr, filePath);
|
|
1649
|
-
if (mysqlOp) {
|
|
1650
|
-
operations.push(mysqlOp);
|
|
1651
|
-
continue;
|
|
1652
|
-
}
|
|
1653
|
-
const mongoOp = detectMongoOperations(callExpr, filePath);
|
|
1654
|
-
if (mongoOp) {
|
|
1655
|
-
operations.push(mongoOp);
|
|
2161
|
+
function extractArgValue(arg, ...keys) {
|
|
2162
|
+
if (ts_morph_1.Node.isObjectLiteralExpression(arg)) {
|
|
2163
|
+
for (const prop of arg.getProperties()) {
|
|
2164
|
+
if (ts_morph_1.Node.isPropertyAssignment(prop) && keys.includes(prop.getName())) {
|
|
2165
|
+
const init = prop.getInitializer();
|
|
2166
|
+
if (init && ts_morph_1.Node.isStringLiteral(init)) {
|
|
2167
|
+
const val = init.getLiteralValue();
|
|
2168
|
+
return val.includes(":") ? val.split(":").pop() ?? val : val;
|
|
2169
|
+
}
|
|
1656
2170
|
}
|
|
1657
2171
|
}
|
|
1658
2172
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
2173
|
+
return "unknown";
|
|
2174
|
+
}
|
|
2175
|
+
function detectAWSServiceOperations(callExpr, filePath) {
|
|
2176
|
+
const expr = callExpr.getExpression();
|
|
2177
|
+
const args = callExpr.getArguments();
|
|
2178
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(expr) && expr.getName() === "send" && args.length > 0) {
|
|
2179
|
+
const firstArg = args[0];
|
|
2180
|
+
if (ts_morph_1.Node.isNewExpression(firstArg)) {
|
|
2181
|
+
const className = firstArg.getExpression().getText();
|
|
2182
|
+
const cmdArgs = firstArg.getArguments();
|
|
2183
|
+
if (SQS_COMMANDS.has(className)) {
|
|
2184
|
+
return {
|
|
2185
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2186
|
+
operationType: className,
|
|
2187
|
+
databaseType: "sqs",
|
|
2188
|
+
target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SQS_ARG_KEYS) : "unknown",
|
|
2189
|
+
filePath
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
if (SNS_COMMANDS.has(className)) {
|
|
2193
|
+
return {
|
|
2194
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2195
|
+
operationType: className,
|
|
2196
|
+
databaseType: "sns",
|
|
2197
|
+
target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SNS_ARG_KEYS) : "unknown",
|
|
2198
|
+
filePath
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
if (SSM_COMMANDS.has(className)) {
|
|
2202
|
+
return {
|
|
2203
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2204
|
+
operationType: className,
|
|
2205
|
+
databaseType: "ssm",
|
|
2206
|
+
target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SSM_ARG_KEYS) : "unknown",
|
|
2207
|
+
filePath
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
if (SECRETS_COMMANDS.has(className)) {
|
|
2211
|
+
return {
|
|
2212
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2213
|
+
operationType: className,
|
|
2214
|
+
databaseType: "secretsmanager",
|
|
2215
|
+
target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...SECRETS_ARG_KEYS) : "unknown",
|
|
2216
|
+
filePath
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
if (LAMBDA_COMMANDS.has(className)) {
|
|
2220
|
+
return {
|
|
2221
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2222
|
+
operationType: className,
|
|
2223
|
+
databaseType: "lambda",
|
|
2224
|
+
target: cmdArgs.length > 0 ? extractArgValue(cmdArgs[0], ...LAMBDA_ARG_KEYS) : "unknown",
|
|
2225
|
+
filePath
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
|
|
2231
|
+
const methodName = expr.getName();
|
|
2232
|
+
const objText = expr.getExpression().getText().toLowerCase();
|
|
2233
|
+
if (SQS_COMMANDS.has(methodName) && (objText.includes("sqs") || objText.includes("queue"))) {
|
|
2234
|
+
return {
|
|
2235
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2236
|
+
operationType: methodName,
|
|
2237
|
+
databaseType: "sqs",
|
|
2238
|
+
target: args.length > 0 ? extractArgValue(args[0], ...SQS_ARG_KEYS) : "unknown",
|
|
2239
|
+
filePath
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
if (SNS_COMMANDS.has(methodName) && (objText.includes("sns") || objText.includes("topic"))) {
|
|
2243
|
+
return {
|
|
2244
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2245
|
+
operationType: methodName,
|
|
2246
|
+
databaseType: "sns",
|
|
2247
|
+
target: args.length > 0 ? extractArgValue(args[0], ...SNS_ARG_KEYS) : "unknown",
|
|
2248
|
+
filePath
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
if (SSM_COMMANDS.has(methodName) && (objText.includes("ssm") || objText.includes("parameter"))) {
|
|
2252
|
+
return {
|
|
2253
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2254
|
+
operationType: methodName,
|
|
2255
|
+
databaseType: "ssm",
|
|
2256
|
+
target: args.length > 0 ? extractArgValue(args[0], ...SSM_ARG_KEYS) : "unknown",
|
|
2257
|
+
filePath
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
if (SECRETS_COMMANDS.has(methodName) && (objText.includes("secret") || objText.includes("secrets"))) {
|
|
2261
|
+
return {
|
|
2262
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2263
|
+
operationType: methodName,
|
|
2264
|
+
databaseType: "secretsmanager",
|
|
2265
|
+
target: args.length > 0 ? extractArgValue(args[0], ...SECRETS_ARG_KEYS) : "unknown",
|
|
2266
|
+
filePath
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
if (LAMBDA_COMMANDS.has(methodName) && (objText.includes("lambda") || objText.includes("fn"))) {
|
|
2270
|
+
return {
|
|
2271
|
+
functionName: getEnclosingFunctionName(callExpr),
|
|
2272
|
+
operationType: methodName,
|
|
2273
|
+
databaseType: "lambda",
|
|
2274
|
+
target: args.length > 0 ? extractArgValue(args[0], ...LAMBDA_ARG_KEYS) : "unknown",
|
|
2275
|
+
filePath
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return null;
|
|
2280
|
+
}
|
|
2281
|
+
async function scanRepository2(repoPath) {
|
|
2282
|
+
const resolvedPath = path5.resolve(repoPath);
|
|
2283
|
+
if (!fs4.existsSync(resolvedPath)) {
|
|
2284
|
+
throw new core_1.RepositoryScanError(`Path does not exist: ${resolvedPath}`);
|
|
2285
|
+
}
|
|
2286
|
+
const tsconfigPath = path5.join(resolvedPath, "tsconfig.json");
|
|
2287
|
+
const hasTsConfig = fs4.existsSync(tsconfigPath);
|
|
2288
|
+
const project = new ts_morph_1.Project({
|
|
2289
|
+
tsConfigFilePath: hasTsConfig ? tsconfigPath : void 0,
|
|
2290
|
+
compilerOptions: hasTsConfig ? void 0 : {
|
|
2291
|
+
target: 99,
|
|
2292
|
+
allowJs: true
|
|
2293
|
+
},
|
|
2294
|
+
skipAddingFilesFromTsConfig: !hasTsConfig
|
|
2295
|
+
});
|
|
2296
|
+
if (!hasTsConfig) {
|
|
2297
|
+
project.addSourceFilesAtPaths([
|
|
2298
|
+
path5.join(resolvedPath, "**/*.ts"),
|
|
2299
|
+
path5.join(resolvedPath, "**/*.tsx"),
|
|
2300
|
+
`!${path5.join(resolvedPath, "**/node_modules/**")}`,
|
|
2301
|
+
`!${path5.join(resolvedPath, "**/dist/**")}`
|
|
2302
|
+
]);
|
|
2303
|
+
}
|
|
2304
|
+
const sourceFiles = project.getSourceFiles();
|
|
2305
|
+
core_1.logger.info(`Scanning ${sourceFiles.length} TypeScript file(s) in ${resolvedPath}`);
|
|
2306
|
+
const operations = [];
|
|
2307
|
+
for (const sourceFile of sourceFiles) {
|
|
2308
|
+
const filePath = sourceFile.getFilePath();
|
|
2309
|
+
if (filePath.includes("node_modules") || filePath.includes("/dist/"))
|
|
2310
|
+
continue;
|
|
2311
|
+
const callExpressions = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
|
|
2312
|
+
for (const callExpr of callExpressions) {
|
|
2313
|
+
const dynamoOp = detectDynamoOperations(callExpr, filePath);
|
|
2314
|
+
if (dynamoOp) {
|
|
2315
|
+
operations.push(dynamoOp);
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
const postgresOp = detectPostgresOperations(callExpr, filePath);
|
|
2319
|
+
if (postgresOp) {
|
|
2320
|
+
operations.push(postgresOp);
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
const mysqlOp = detectMySQLOperations(callExpr, filePath);
|
|
2324
|
+
if (mysqlOp) {
|
|
2325
|
+
operations.push(mysqlOp);
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
const mongoOp = detectMongoOperations(callExpr, filePath);
|
|
2329
|
+
if (mongoOp) {
|
|
2330
|
+
operations.push(mongoOp);
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
const awsOp = detectAWSServiceOperations(callExpr, filePath);
|
|
2334
|
+
if (awsOp) {
|
|
2335
|
+
operations.push(awsOp);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
core_1.logger.info(`Extracted ${operations.length} database operation(s)`);
|
|
2340
|
+
return operations;
|
|
1661
2341
|
}
|
|
1662
2342
|
}
|
|
1663
2343
|
});
|
|
1664
2344
|
|
|
1665
2345
|
// ../graph/dist/index.js
|
|
1666
|
-
var
|
|
2346
|
+
var require_dist10 = __commonJS({
|
|
1667
2347
|
"../graph/dist/index.js"(exports2) {
|
|
1668
2348
|
"use strict";
|
|
1669
2349
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -1671,157 +2351,200 @@ var require_dist8 = __commonJS({
|
|
|
1671
2351
|
exports2.getTableNodes = getTableNodes;
|
|
1672
2352
|
exports2.getFunctionNodes = getFunctionNodes;
|
|
1673
2353
|
exports2.getIndexNodes = getIndexNodes;
|
|
2354
|
+
exports2.getQueueNodes = getQueueNodes;
|
|
2355
|
+
exports2.getTopicNodes = getTopicNodes;
|
|
2356
|
+
exports2.getSecretNodes = getSecretNodes;
|
|
2357
|
+
exports2.getParameterNodes = getParameterNodes;
|
|
2358
|
+
exports2.getLogGroupNodes = getLogGroupNodes;
|
|
2359
|
+
exports2.getLambdaNodes = getLambdaNodes;
|
|
1674
2360
|
exports2.getEdgesForNode = getEdgesForNode;
|
|
1675
2361
|
exports2.getOutgoingEdges = getOutgoingEdges;
|
|
1676
2362
|
exports2.getIncomingEdges = getIncomingEdges;
|
|
1677
2363
|
exports2.getScanEdges = getScanEdges;
|
|
1678
2364
|
exports2.getEdgeFrequency = getEdgeFrequency;
|
|
1679
|
-
function buildGraph2(operations, dynamoMeta, postgresMeta, mysqlMeta = [], mongoMeta = []) {
|
|
2365
|
+
function buildGraph2(operations, dynamoMeta, postgresMeta, mysqlMeta = [], mongoMeta = [], servicesMeta = {}) {
|
|
1680
2366
|
const nodes = [];
|
|
1681
2367
|
const edges = [];
|
|
1682
2368
|
const nodeIds = /* @__PURE__ */ new Set();
|
|
2369
|
+
function addNode(node) {
|
|
2370
|
+
if (!nodeIds.has(node.id)) {
|
|
2371
|
+
nodes.push(node);
|
|
2372
|
+
nodeIds.add(node.id);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
1683
2375
|
for (const table of dynamoMeta) {
|
|
1684
2376
|
const nodeId = `table:dynamo:${table.tableName}`;
|
|
1685
|
-
|
|
1686
|
-
nodes.push({
|
|
1687
|
-
id: nodeId,
|
|
1688
|
-
type: "table",
|
|
1689
|
-
name: table.tableName,
|
|
1690
|
-
databaseType: "dynamodb"
|
|
1691
|
-
});
|
|
1692
|
-
nodeIds.add(nodeId);
|
|
1693
|
-
}
|
|
2377
|
+
addNode({ id: nodeId, type: "table", name: table.tableName, databaseType: "dynamodb" });
|
|
1694
2378
|
for (const indexName of table.indexes) {
|
|
1695
2379
|
const indexNodeId = `index:${table.tableName}:${indexName}`;
|
|
1696
|
-
|
|
1697
|
-
nodes.push({ id: indexNodeId, type: "index", name: indexName });
|
|
1698
|
-
nodeIds.add(indexNodeId);
|
|
1699
|
-
}
|
|
2380
|
+
addNode({ id: indexNodeId, type: "index", name: indexName });
|
|
1700
2381
|
edges.push({ from: nodeId, to: indexNodeId, type: "uses_index" });
|
|
1701
2382
|
}
|
|
1702
2383
|
}
|
|
1703
2384
|
for (const table of postgresMeta) {
|
|
1704
2385
|
const nodeId = `table:postgres:${table.schema}.${table.table}`;
|
|
1705
|
-
|
|
1706
|
-
nodes.push({
|
|
1707
|
-
id: nodeId,
|
|
1708
|
-
type: "table",
|
|
1709
|
-
name: `${table.schema}.${table.table}`,
|
|
1710
|
-
databaseType: "postgres"
|
|
1711
|
-
});
|
|
1712
|
-
nodeIds.add(nodeId);
|
|
1713
|
-
}
|
|
2386
|
+
addNode({ id: nodeId, type: "table", name: `${table.schema}.${table.table}`, databaseType: "postgres" });
|
|
1714
2387
|
for (const indexName of table.indexes) {
|
|
1715
2388
|
const indexNodeId = `index:${table.schema}.${table.table}:${indexName}`;
|
|
1716
|
-
|
|
1717
|
-
nodes.push({ id: indexNodeId, type: "index", name: indexName });
|
|
1718
|
-
nodeIds.add(indexNodeId);
|
|
1719
|
-
}
|
|
2389
|
+
addNode({ id: indexNodeId, type: "index", name: indexName });
|
|
1720
2390
|
edges.push({ from: nodeId, to: indexNodeId, type: "uses_index" });
|
|
1721
2391
|
}
|
|
1722
2392
|
}
|
|
1723
2393
|
for (const table of mysqlMeta) {
|
|
1724
2394
|
const nodeId = `table:mysql:${table.schema}.${table.table}`;
|
|
1725
|
-
|
|
1726
|
-
nodes.push({
|
|
1727
|
-
id: nodeId,
|
|
1728
|
-
type: "table",
|
|
1729
|
-
name: `${table.schema}.${table.table}`,
|
|
1730
|
-
databaseType: "mysql"
|
|
1731
|
-
});
|
|
1732
|
-
nodeIds.add(nodeId);
|
|
1733
|
-
}
|
|
2395
|
+
addNode({ id: nodeId, type: "table", name: `${table.schema}.${table.table}`, databaseType: "mysql" });
|
|
1734
2396
|
for (const indexName of table.indexes) {
|
|
1735
2397
|
const indexNodeId = `index:${table.schema}.${table.table}:${indexName}`;
|
|
1736
|
-
|
|
1737
|
-
nodes.push({ id: indexNodeId, type: "index", name: indexName });
|
|
1738
|
-
nodeIds.add(indexNodeId);
|
|
1739
|
-
}
|
|
2398
|
+
addNode({ id: indexNodeId, type: "index", name: indexName });
|
|
1740
2399
|
edges.push({ from: nodeId, to: indexNodeId, type: "uses_index" });
|
|
1741
2400
|
}
|
|
1742
2401
|
}
|
|
1743
2402
|
for (const coll of mongoMeta) {
|
|
1744
2403
|
const nodeId = `table:mongodb:${coll.database}.${coll.collection}`;
|
|
1745
|
-
|
|
1746
|
-
nodes.push({
|
|
1747
|
-
id: nodeId,
|
|
1748
|
-
type: "table",
|
|
1749
|
-
name: `${coll.database}.${coll.collection}`,
|
|
1750
|
-
databaseType: "mongodb"
|
|
1751
|
-
});
|
|
1752
|
-
nodeIds.add(nodeId);
|
|
1753
|
-
}
|
|
2404
|
+
addNode({ id: nodeId, type: "table", name: `${coll.database}.${coll.collection}`, databaseType: "mongodb" });
|
|
1754
2405
|
for (const idx of coll.indexes) {
|
|
1755
2406
|
if (idx.name === "_id_")
|
|
1756
2407
|
continue;
|
|
1757
2408
|
const indexNodeId = `index:${coll.database}.${coll.collection}:${idx.name}`;
|
|
1758
|
-
|
|
1759
|
-
nodes.push({ id: indexNodeId, type: "index", name: idx.name });
|
|
1760
|
-
nodeIds.add(indexNodeId);
|
|
1761
|
-
}
|
|
2409
|
+
addNode({ id: indexNodeId, type: "index", name: idx.name });
|
|
1762
2410
|
edges.push({ from: nodeId, to: indexNodeId, type: "uses_index" });
|
|
1763
2411
|
}
|
|
1764
2412
|
}
|
|
2413
|
+
for (const q of servicesMeta.sqs ?? []) {
|
|
2414
|
+
addNode({
|
|
2415
|
+
id: `queue:aws:${q.name}`,
|
|
2416
|
+
type: "queue",
|
|
2417
|
+
name: q.name,
|
|
2418
|
+
provider: "aws",
|
|
2419
|
+
hasDLQ: q.hasDLQ,
|
|
2420
|
+
encrypted: q.encrypted,
|
|
2421
|
+
approximateMessages: q.approximateMessages,
|
|
2422
|
+
retentionDays: q.retentionDays
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
for (const t of servicesMeta.sns ?? []) {
|
|
2426
|
+
addNode({
|
|
2427
|
+
id: `topic:aws:${t.name}`,
|
|
2428
|
+
type: "topic",
|
|
2429
|
+
name: t.name,
|
|
2430
|
+
provider: "aws",
|
|
2431
|
+
subscriptionCount: t.subscriptionCount,
|
|
2432
|
+
encrypted: t.encrypted
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
for (const s of servicesMeta.secrets ?? []) {
|
|
2436
|
+
addNode({
|
|
2437
|
+
id: `secret:aws:${s.name}`,
|
|
2438
|
+
type: "secret",
|
|
2439
|
+
name: s.name,
|
|
2440
|
+
provider: "aws",
|
|
2441
|
+
rotationEnabled: s.rotationEnabled,
|
|
2442
|
+
rotationDays: s.rotationDays
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
for (const p of servicesMeta.ssm ?? []) {
|
|
2446
|
+
addNode({
|
|
2447
|
+
id: `parameter:aws:${p.name}`,
|
|
2448
|
+
type: "parameter",
|
|
2449
|
+
name: p.name,
|
|
2450
|
+
provider: "aws",
|
|
2451
|
+
paramType: p.type,
|
|
2452
|
+
tier: p.tier
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
for (const lg of servicesMeta.logs ?? []) {
|
|
2456
|
+
addNode({
|
|
2457
|
+
id: `log_group:aws:${lg.logGroupName}`,
|
|
2458
|
+
type: "log_group",
|
|
2459
|
+
name: lg.logGroupName,
|
|
2460
|
+
provider: "aws",
|
|
2461
|
+
retentionDays: lg.retentionDays,
|
|
2462
|
+
errorCount: lg.errorCount,
|
|
2463
|
+
topErrorPatterns: lg.topErrorPatterns
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
for (const fn of servicesMeta.lambda ?? []) {
|
|
2467
|
+
addNode({
|
|
2468
|
+
id: `lambda:aws:${fn.name}`,
|
|
2469
|
+
type: "lambda",
|
|
2470
|
+
name: fn.name,
|
|
2471
|
+
runtime: fn.runtime,
|
|
2472
|
+
memoryMB: fn.memoryMB,
|
|
2473
|
+
timeoutSec: fn.timeoutSec,
|
|
2474
|
+
envVarKeys: fn.envVarKeys
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
1765
2477
|
for (const op of operations) {
|
|
1766
2478
|
const funcNodeId = `function:${op.filePath}:${op.functionName}`;
|
|
1767
2479
|
if (!nodeIds.has(funcNodeId)) {
|
|
1768
|
-
nodes.push({
|
|
1769
|
-
id: funcNodeId,
|
|
1770
|
-
type: "function",
|
|
1771
|
-
name: op.functionName,
|
|
1772
|
-
file: op.filePath
|
|
1773
|
-
});
|
|
2480
|
+
nodes.push({ id: funcNodeId, type: "function", name: op.functionName, file: op.filePath });
|
|
1774
2481
|
nodeIds.add(funcNodeId);
|
|
1775
2482
|
}
|
|
2483
|
+
if (op.databaseType === "sqs") {
|
|
2484
|
+
const queueId = `queue:aws:${op.target}`;
|
|
2485
|
+
if (!nodeIds.has(queueId)) {
|
|
2486
|
+
addNode({ id: queueId, type: "queue", name: op.target, provider: "aws", hasDLQ: false, encrypted: false });
|
|
2487
|
+
}
|
|
2488
|
+
edges.push({ from: funcNodeId, to: queueId, type: "publishes_to" });
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
if (op.databaseType === "sns") {
|
|
2492
|
+
const topicId = `topic:aws:${op.target}`;
|
|
2493
|
+
if (!nodeIds.has(topicId)) {
|
|
2494
|
+
addNode({ id: topicId, type: "topic", name: op.target, provider: "aws", encrypted: false });
|
|
2495
|
+
}
|
|
2496
|
+
edges.push({ from: funcNodeId, to: topicId, type: "publishes_to" });
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
if (op.databaseType === "ssm") {
|
|
2500
|
+
const paramId = `parameter:aws:${op.target}`;
|
|
2501
|
+
if (!nodeIds.has(paramId)) {
|
|
2502
|
+
addNode({ id: paramId, type: "parameter", name: op.target, provider: "aws", paramType: "String", tier: "Standard" });
|
|
2503
|
+
}
|
|
2504
|
+
edges.push({ from: funcNodeId, to: paramId, type: "reads_parameter" });
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
if (op.databaseType === "secretsmanager") {
|
|
2508
|
+
const secretId = `secret:aws:${op.target}`;
|
|
2509
|
+
if (!nodeIds.has(secretId)) {
|
|
2510
|
+
addNode({ id: secretId, type: "secret", name: op.target, provider: "aws", rotationEnabled: false });
|
|
2511
|
+
}
|
|
2512
|
+
edges.push({ from: funcNodeId, to: secretId, type: "reads_secret" });
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2515
|
+
if (op.databaseType === "lambda") {
|
|
2516
|
+
const lambdaId = `lambda:aws:${op.target}`;
|
|
2517
|
+
if (!nodeIds.has(lambdaId)) {
|
|
2518
|
+
addNode({ id: lambdaId, type: "lambda", name: op.target });
|
|
2519
|
+
}
|
|
2520
|
+
edges.push({ from: funcNodeId, to: lambdaId, type: "triggers" });
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
1776
2523
|
let tableNodeId;
|
|
1777
2524
|
if (op.databaseType === "dynamodb") {
|
|
1778
2525
|
tableNodeId = `table:dynamo:${op.target}`;
|
|
1779
2526
|
if (!nodeIds.has(tableNodeId)) {
|
|
1780
|
-
|
|
1781
|
-
id: tableNodeId,
|
|
1782
|
-
type: "table",
|
|
1783
|
-
name: op.target,
|
|
1784
|
-
databaseType: "dynamodb"
|
|
1785
|
-
});
|
|
1786
|
-
nodeIds.add(tableNodeId);
|
|
2527
|
+
addNode({ id: tableNodeId, type: "table", name: op.target, databaseType: "dynamodb" });
|
|
1787
2528
|
}
|
|
1788
2529
|
} else if (op.databaseType === "mysql") {
|
|
1789
|
-
const
|
|
1790
|
-
tableNodeId = `table:mysql:${
|
|
2530
|
+
const q = op.target.includes(".") ? op.target : `default.${op.target}`;
|
|
2531
|
+
tableNodeId = `table:mysql:${q}`;
|
|
1791
2532
|
if (!nodeIds.has(tableNodeId)) {
|
|
1792
|
-
|
|
1793
|
-
id: tableNodeId,
|
|
1794
|
-
type: "table",
|
|
1795
|
-
name: qualifiedTarget,
|
|
1796
|
-
databaseType: "mysql"
|
|
1797
|
-
});
|
|
1798
|
-
nodeIds.add(tableNodeId);
|
|
2533
|
+
addNode({ id: tableNodeId, type: "table", name: q, databaseType: "mysql" });
|
|
1799
2534
|
}
|
|
1800
2535
|
} else if (op.databaseType === "mongodb") {
|
|
1801
|
-
const
|
|
1802
|
-
tableNodeId = `table:mongodb:${
|
|
2536
|
+
const q = op.target.includes(".") ? op.target : `default.${op.target}`;
|
|
2537
|
+
tableNodeId = `table:mongodb:${q}`;
|
|
1803
2538
|
if (!nodeIds.has(tableNodeId)) {
|
|
1804
|
-
|
|
1805
|
-
id: tableNodeId,
|
|
1806
|
-
type: "table",
|
|
1807
|
-
name: qualifiedTarget,
|
|
1808
|
-
databaseType: "mongodb"
|
|
1809
|
-
});
|
|
1810
|
-
nodeIds.add(tableNodeId);
|
|
2539
|
+
addNode({ id: tableNodeId, type: "table", name: q, databaseType: "mongodb" });
|
|
1811
2540
|
}
|
|
1812
2541
|
} else {
|
|
1813
|
-
const
|
|
1814
|
-
tableNodeId = `table:postgres:${
|
|
2542
|
+
const q = op.target.includes(".") ? op.target : `public.${op.target}`;
|
|
2543
|
+
tableNodeId = `table:postgres:${q}`;
|
|
1815
2544
|
if (!nodeIds.has(tableNodeId)) {
|
|
1816
|
-
const parts =
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
type: "table",
|
|
1820
|
-
name: qualifiedTarget,
|
|
1821
|
-
databaseType: "postgres"
|
|
1822
|
-
});
|
|
1823
|
-
nodeIds.add(tableNodeId);
|
|
1824
|
-
if (!postgresMeta.find((t) => `${t.schema}.${t.table}` === qualifiedTarget)) {
|
|
2545
|
+
const parts = q.split(".");
|
|
2546
|
+
addNode({ id: tableNodeId, type: "table", name: q, databaseType: "postgres" });
|
|
2547
|
+
if (!postgresMeta.find((t) => `${t.schema}.${t.table}` === q)) {
|
|
1825
2548
|
postgresMeta.push({
|
|
1826
2549
|
schema: parts[0] ?? "public",
|
|
1827
2550
|
table: parts[1] ?? op.target,
|
|
@@ -1854,6 +2577,24 @@ var require_dist8 = __commonJS({
|
|
|
1854
2577
|
function getIndexNodes(graph) {
|
|
1855
2578
|
return graph.nodes.filter((n) => n.type === "index");
|
|
1856
2579
|
}
|
|
2580
|
+
function getQueueNodes(graph) {
|
|
2581
|
+
return graph.nodes.filter((n) => n.type === "queue");
|
|
2582
|
+
}
|
|
2583
|
+
function getTopicNodes(graph) {
|
|
2584
|
+
return graph.nodes.filter((n) => n.type === "topic");
|
|
2585
|
+
}
|
|
2586
|
+
function getSecretNodes(graph) {
|
|
2587
|
+
return graph.nodes.filter((n) => n.type === "secret");
|
|
2588
|
+
}
|
|
2589
|
+
function getParameterNodes(graph) {
|
|
2590
|
+
return graph.nodes.filter((n) => n.type === "parameter");
|
|
2591
|
+
}
|
|
2592
|
+
function getLogGroupNodes(graph) {
|
|
2593
|
+
return graph.nodes.filter((n) => n.type === "log_group");
|
|
2594
|
+
}
|
|
2595
|
+
function getLambdaNodes(graph) {
|
|
2596
|
+
return graph.nodes.filter((n) => n.type === "lambda");
|
|
2597
|
+
}
|
|
1857
2598
|
function getEdgesForNode(graph, nodeId) {
|
|
1858
2599
|
return graph.edges.filter((e) => e.from === nodeId || e.to === nodeId);
|
|
1859
2600
|
}
|
|
@@ -1883,7 +2624,7 @@ var require_dynamodb = __commonJS({
|
|
|
1883
2624
|
"use strict";
|
|
1884
2625
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1885
2626
|
exports2.HotPartitionAnalyzer = exports2.MissingGSIAnalyzer = exports2.FullTableScanAnalyzer = void 0;
|
|
1886
|
-
var graph_1 =
|
|
2627
|
+
var graph_1 = require_dist10();
|
|
1887
2628
|
var FullTableScanAnalyzer = class {
|
|
1888
2629
|
name = "FullTableScanAnalyzer";
|
|
1889
2630
|
async analyze(graph) {
|
|
@@ -2136,7 +2877,7 @@ var require_mysql = __commonJS({
|
|
|
2136
2877
|
"use strict";
|
|
2137
2878
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2138
2879
|
exports2.MySQLFullTableScanAnalyzer = exports2.MissingMySQLIndexAnalyzer = void 0;
|
|
2139
|
-
var graph_1 =
|
|
2880
|
+
var graph_1 = require_dist10();
|
|
2140
2881
|
var MissingMySQLIndexAnalyzer = class {
|
|
2141
2882
|
name = "MissingMySQLIndexAnalyzer";
|
|
2142
2883
|
async analyze(graph) {
|
|
@@ -2214,7 +2955,7 @@ var require_mongodb = __commonJS({
|
|
|
2214
2955
|
"use strict";
|
|
2215
2956
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2216
2957
|
exports2.MongoCollectionScanAnalyzer = exports2.MissingMongoIndexAnalyzer = void 0;
|
|
2217
|
-
var graph_1 =
|
|
2958
|
+
var graph_1 = require_dist10();
|
|
2218
2959
|
var MissingMongoIndexAnalyzer = class {
|
|
2219
2960
|
name = "MissingMongoIndexAnalyzer";
|
|
2220
2961
|
async analyze(graph) {
|
|
@@ -2300,40 +3041,78 @@ var require_terraform = __commonJS({
|
|
|
2300
3041
|
}
|
|
2301
3042
|
async analyze(graph) {
|
|
2302
3043
|
const findings = [];
|
|
2303
|
-
if (!this.iacSchema)
|
|
3044
|
+
if (!this.iacSchema)
|
|
2304
3045
|
return findings;
|
|
3046
|
+
const iac = this.iacSchema;
|
|
3047
|
+
const deployedDynamo = new Set(graph.nodes.filter((n) => n.type === "table" && n.databaseType === "dynamodb").map((n) => n.name));
|
|
3048
|
+
const iacDynamo = new Map(iac.dynamoTables.map((t) => [t.name, t.filePath]));
|
|
3049
|
+
for (const [name, fp] of iacDynamo) {
|
|
3050
|
+
if (!deployedDynamo.has(name)) {
|
|
3051
|
+
findings.push({
|
|
3052
|
+
severity: "medium",
|
|
3053
|
+
issue: `IaC drift: DynamoDB table "${name}" defined in IaC but not deployed`,
|
|
3054
|
+
description: `"${name}" is in ${fp} but not found in AWS. It may be undeployed or deleted manually.`,
|
|
3055
|
+
recommendation: "Run `terraform apply` / deploy your stack, or remove the definition from IaC.",
|
|
3056
|
+
metadata: { resourceType: "dynamodb_table", name, filePath: fp, driftType: "defined_not_deployed" }
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
2305
3059
|
}
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
3060
|
+
for (const name of deployedDynamo) {
|
|
3061
|
+
if (!iacDynamo.has(name)) {
|
|
3062
|
+
findings.push({
|
|
3063
|
+
severity: "medium",
|
|
3064
|
+
issue: `IaC drift: DynamoDB table "${name}" deployed but not in IaC`,
|
|
3065
|
+
description: `"${name}" exists in AWS DynamoDB but has no IaC definition. It may have been created manually.`,
|
|
3066
|
+
recommendation: "Import the table with `terraform import` or add a CloudFormation resource, then track all future changes through IaC.",
|
|
3067
|
+
metadata: { resourceType: "dynamodb_table", name, driftType: "deployed_not_defined" }
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
2310
3070
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
3071
|
+
const deployedQueues = new Set(graph.nodes.filter((n) => n.type === "queue").map((n) => n.name));
|
|
3072
|
+
const iacQueues = new Map(iac.queues.map((q) => [q.name, q.filePath]));
|
|
3073
|
+
for (const [name, fp] of iacQueues) {
|
|
3074
|
+
if (!deployedQueues.has(name)) {
|
|
2313
3075
|
findings.push({
|
|
2314
3076
|
severity: "medium",
|
|
2315
|
-
issue: `IaC drift:
|
|
2316
|
-
description: `
|
|
2317
|
-
recommendation: "
|
|
2318
|
-
metadata: {
|
|
2319
|
-
tableName,
|
|
2320
|
-
filePath,
|
|
2321
|
-
driftType: "defined_not_deployed"
|
|
2322
|
-
}
|
|
3077
|
+
issue: `IaC drift: SQS queue "${name}" defined in IaC but not deployed`,
|
|
3078
|
+
description: `SQS queue "${name}" is defined in ${fp} but not found in the live account.`,
|
|
3079
|
+
recommendation: "Deploy the queue via `terraform apply` or your CFN/CDK stack.",
|
|
3080
|
+
metadata: { resourceType: "sqs_queue", name, filePath: fp, driftType: "defined_not_deployed" }
|
|
2323
3081
|
});
|
|
2324
3082
|
}
|
|
2325
3083
|
}
|
|
2326
|
-
for (const
|
|
2327
|
-
if (!
|
|
3084
|
+
for (const name of deployedQueues) {
|
|
3085
|
+
if (!iacQueues.has(name)) {
|
|
3086
|
+
findings.push({
|
|
3087
|
+
severity: "low",
|
|
3088
|
+
issue: `IaC drift: SQS queue "${name}" deployed but not in IaC`,
|
|
3089
|
+
description: `SQS queue "${name}" exists in AWS but is not tracked in IaC. Manual resources can't be audited or reproduced reliably.`,
|
|
3090
|
+
recommendation: "Import or define the queue in IaC to bring it under version control.",
|
|
3091
|
+
metadata: { resourceType: "sqs_queue", name, driftType: "deployed_not_defined" }
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
const deployedLambdas = new Set(graph.nodes.filter((n) => n.type === "lambda").map((n) => n.name));
|
|
3096
|
+
const iacLambdas = new Map(iac.lambdas.map((l) => [l.name, l.filePath]));
|
|
3097
|
+
for (const [name, fp] of iacLambdas) {
|
|
3098
|
+
if (!deployedLambdas.has(name)) {
|
|
2328
3099
|
findings.push({
|
|
2329
3100
|
severity: "medium",
|
|
2330
|
-
issue: `IaC drift:
|
|
2331
|
-
description: `
|
|
2332
|
-
recommendation: "
|
|
2333
|
-
metadata: {
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
3101
|
+
issue: `IaC drift: Lambda "${name}" defined in IaC but not deployed`,
|
|
3102
|
+
description: `Lambda function "${name}" is defined in ${fp} but not found in the live account.`,
|
|
3103
|
+
recommendation: "Deploy the function via `terraform apply` or your CFN/CDK stack.",
|
|
3104
|
+
metadata: { resourceType: "lambda_function", name, filePath: fp, driftType: "defined_not_deployed" }
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
for (const name of deployedLambdas) {
|
|
3109
|
+
if (!iacLambdas.has(name)) {
|
|
3110
|
+
findings.push({
|
|
3111
|
+
severity: "low",
|
|
3112
|
+
issue: `IaC drift: Lambda "${name}" deployed but not in IaC`,
|
|
3113
|
+
description: `Lambda "${name}" exists in AWS but is not tracked in IaC.`,
|
|
3114
|
+
recommendation: "Import the function into IaC or add it as a resource.",
|
|
3115
|
+
metadata: { resourceType: "lambda_function", name, driftType: "deployed_not_defined" }
|
|
2337
3116
|
});
|
|
2338
3117
|
}
|
|
2339
3118
|
}
|
|
@@ -2344,12 +3123,181 @@ var require_terraform = __commonJS({
|
|
|
2344
3123
|
}
|
|
2345
3124
|
});
|
|
2346
3125
|
|
|
3126
|
+
// ../analyzers/dist/aws-services.js
|
|
3127
|
+
var require_aws_services = __commonJS({
|
|
3128
|
+
"../analyzers/dist/aws-services.js"(exports2) {
|
|
3129
|
+
"use strict";
|
|
3130
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
3131
|
+
exports2.LambdaHighTimeoutAnalyzer = exports2.LambdaDefaultMemoryAnalyzer = exports2.MissingLogRetentionAnalyzer = exports2.MissingSecretRotationAnalyzer = exports2.LargeQueueBacklogAnalyzer = exports2.UnencryptedQueueAnalyzer = exports2.MissingDLQAnalyzer = void 0;
|
|
3132
|
+
var MissingDLQAnalyzer = class {
|
|
3133
|
+
name = "MissingDLQAnalyzer";
|
|
3134
|
+
async analyze(graph) {
|
|
3135
|
+
const findings = [];
|
|
3136
|
+
for (const node of graph.nodes) {
|
|
3137
|
+
if (node.type !== "queue")
|
|
3138
|
+
continue;
|
|
3139
|
+
if (!node.hasDLQ) {
|
|
3140
|
+
findings.push({
|
|
3141
|
+
severity: "high",
|
|
3142
|
+
issue: `Queue "${node.name}" has no Dead Letter Queue`,
|
|
3143
|
+
description: `SQS queue "${node.name}" has no DLQ configured. Failed messages will be discarded after maxReceiveCount retries, causing silent data loss.`,
|
|
3144
|
+
recommendation: `Add a Dead Letter Queue to "${node.name}". Set maxReceiveCount to 3\u20135 retries before routing to DLQ. Alert on DLQ depth.`,
|
|
3145
|
+
metadata: { queueName: node.name, provider: node.provider }
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
return findings;
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
exports2.MissingDLQAnalyzer = MissingDLQAnalyzer;
|
|
3153
|
+
var UnencryptedQueueAnalyzer = class {
|
|
3154
|
+
name = "UnencryptedQueueAnalyzer";
|
|
3155
|
+
async analyze(graph) {
|
|
3156
|
+
const findings = [];
|
|
3157
|
+
for (const node of graph.nodes) {
|
|
3158
|
+
if (node.type !== "queue")
|
|
3159
|
+
continue;
|
|
3160
|
+
if (!node.encrypted) {
|
|
3161
|
+
findings.push({
|
|
3162
|
+
severity: "low",
|
|
3163
|
+
issue: `Queue "${node.name}" is not encrypted`,
|
|
3164
|
+
description: `SQS queue "${node.name}" does not have server-side encryption enabled. Messages at rest are unencrypted.`,
|
|
3165
|
+
recommendation: `Enable SQS-managed SSE (SqsManagedSseEnabled=true) or bring your own KMS key for "${node.name}".`,
|
|
3166
|
+
metadata: { queueName: node.name }
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
return findings;
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
exports2.UnencryptedQueueAnalyzer = UnencryptedQueueAnalyzer;
|
|
3174
|
+
var LargeQueueBacklogAnalyzer = class {
|
|
3175
|
+
name = "LargeQueueBacklogAnalyzer";
|
|
3176
|
+
threshold;
|
|
3177
|
+
constructor(threshold = 1e3) {
|
|
3178
|
+
this.threshold = threshold;
|
|
3179
|
+
}
|
|
3180
|
+
async analyze(graph) {
|
|
3181
|
+
const findings = [];
|
|
3182
|
+
for (const node of graph.nodes) {
|
|
3183
|
+
if (node.type !== "queue")
|
|
3184
|
+
continue;
|
|
3185
|
+
const count = node.approximateMessages ?? 0;
|
|
3186
|
+
if (count > this.threshold) {
|
|
3187
|
+
findings.push({
|
|
3188
|
+
severity: "medium",
|
|
3189
|
+
issue: `Queue "${node.name}" has a large backlog (${count.toLocaleString()} messages)`,
|
|
3190
|
+
description: `The approximate message count for "${node.name}" is ${count.toLocaleString()}, indicating consumers may be falling behind or stuck.`,
|
|
3191
|
+
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.`,
|
|
3192
|
+
metadata: { queueName: node.name, messageCount: count }
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
return findings;
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
exports2.LargeQueueBacklogAnalyzer = LargeQueueBacklogAnalyzer;
|
|
3200
|
+
var MissingSecretRotationAnalyzer = class {
|
|
3201
|
+
name = "MissingSecretRotationAnalyzer";
|
|
3202
|
+
async analyze(graph) {
|
|
3203
|
+
const findings = [];
|
|
3204
|
+
for (const node of graph.nodes) {
|
|
3205
|
+
if (node.type !== "secret")
|
|
3206
|
+
continue;
|
|
3207
|
+
if (!node.rotationEnabled) {
|
|
3208
|
+
findings.push({
|
|
3209
|
+
severity: "medium",
|
|
3210
|
+
issue: `Secret "${node.name}" has no automatic rotation`,
|
|
3211
|
+
description: `Secrets Manager secret "${node.name}" does not have automatic rotation enabled. Long-lived credentials increase the blast radius of a compromise.`,
|
|
3212
|
+
recommendation: `Enable automatic rotation for "${node.name}" using a Lambda rotation function. AWS provides pre-built rotators for RDS, Redshift, and custom secrets.`,
|
|
3213
|
+
metadata: { secretName: node.name, provider: node.provider }
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return findings;
|
|
3218
|
+
}
|
|
3219
|
+
};
|
|
3220
|
+
exports2.MissingSecretRotationAnalyzer = MissingSecretRotationAnalyzer;
|
|
3221
|
+
var MissingLogRetentionAnalyzer = class {
|
|
3222
|
+
name = "MissingLogRetentionAnalyzer";
|
|
3223
|
+
async analyze(graph) {
|
|
3224
|
+
const findings = [];
|
|
3225
|
+
for (const node of graph.nodes) {
|
|
3226
|
+
if (node.type !== "log_group")
|
|
3227
|
+
continue;
|
|
3228
|
+
if (node.retentionDays === void 0) {
|
|
3229
|
+
findings.push({
|
|
3230
|
+
severity: "medium",
|
|
3231
|
+
issue: `Log group "${node.name}" has no retention policy`,
|
|
3232
|
+
description: `CloudWatch Log group "${node.name}" retains logs indefinitely. This increases storage costs and can expose sensitive data longer than necessary.`,
|
|
3233
|
+
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).`,
|
|
3234
|
+
metadata: { logGroupName: node.name }
|
|
3235
|
+
});
|
|
3236
|
+
} else if (node.retentionDays > 365) {
|
|
3237
|
+
findings.push({
|
|
3238
|
+
severity: "low",
|
|
3239
|
+
issue: `Log group "${node.name}" retains logs for ${node.retentionDays} days`,
|
|
3240
|
+
description: `Log group "${node.name}" has a ${node.retentionDays}-day retention period. Unless required by compliance, this may be longer than needed.`,
|
|
3241
|
+
recommendation: `Review whether ${node.retentionDays} days of retention is required for "${node.name}". Consider archiving older logs to S3 Glacier for cost savings.`,
|
|
3242
|
+
metadata: { logGroupName: node.name, retentionDays: node.retentionDays }
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
return findings;
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
exports2.MissingLogRetentionAnalyzer = MissingLogRetentionAnalyzer;
|
|
3250
|
+
var LambdaDefaultMemoryAnalyzer = class {
|
|
3251
|
+
name = "LambdaDefaultMemoryAnalyzer";
|
|
3252
|
+
async analyze(graph) {
|
|
3253
|
+
const findings = [];
|
|
3254
|
+
for (const node of graph.nodes) {
|
|
3255
|
+
if (node.type !== "lambda")
|
|
3256
|
+
continue;
|
|
3257
|
+
if (node.memoryMB === 128) {
|
|
3258
|
+
findings.push({
|
|
3259
|
+
severity: "low",
|
|
3260
|
+
issue: `Lambda "${node.name}" uses the default 128 MB memory`,
|
|
3261
|
+
description: `"${node.name}" uses the default 128 MB. Undersized memory causes throttled CPU and higher durations. AWS Lambda pricing is duration \xD7 memory, so more memory often lowers cost by reducing duration.`,
|
|
3262
|
+
recommendation: `Run Lambda Power Tuning on "${node.name}" to find the optimal memory/cost balance. Most workloads perform better at 512 MB\u20131 GB.`,
|
|
3263
|
+
metadata: { functionName: node.name, memoryMB: node.memoryMB }
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
return findings;
|
|
3268
|
+
}
|
|
3269
|
+
};
|
|
3270
|
+
exports2.LambdaDefaultMemoryAnalyzer = LambdaDefaultMemoryAnalyzer;
|
|
3271
|
+
var LambdaHighTimeoutAnalyzer = class {
|
|
3272
|
+
name = "LambdaHighTimeoutAnalyzer";
|
|
3273
|
+
async analyze(graph) {
|
|
3274
|
+
const findings = [];
|
|
3275
|
+
for (const node of graph.nodes) {
|
|
3276
|
+
if (node.type !== "lambda")
|
|
3277
|
+
continue;
|
|
3278
|
+
if ((node.timeoutSec ?? 0) >= 300) {
|
|
3279
|
+
findings.push({
|
|
3280
|
+
severity: "low",
|
|
3281
|
+
issue: `Lambda "${node.name}" has a very high timeout (${node.timeoutSec}s)`,
|
|
3282
|
+
description: `"${node.name}" has a ${node.timeoutSec}-second timeout. High timeouts mask latency issues and increase worst-case cost when functions hang.`,
|
|
3283
|
+
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.`,
|
|
3284
|
+
metadata: { functionName: node.name, timeoutSec: node.timeoutSec }
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
return findings;
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
exports2.LambdaHighTimeoutAnalyzer = LambdaHighTimeoutAnalyzer;
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
|
|
2347
3295
|
// ../analyzers/dist/index.js
|
|
2348
|
-
var
|
|
3296
|
+
var require_dist11 = __commonJS({
|
|
2349
3297
|
"../analyzers/dist/index.js"(exports2) {
|
|
2350
3298
|
"use strict";
|
|
2351
3299
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2352
|
-
exports2.IaCDriftAnalyzer = exports2.MongoCollectionScanAnalyzer = exports2.MissingMongoIndexAnalyzer = exports2.MySQLFullTableScanAnalyzer = exports2.MissingMySQLIndexAnalyzer = exports2.LargeSelectAnalyzer = exports2.NplusOneAnalyzer = exports2.MissingIndexAnalyzer = exports2.HotPartitionAnalyzer = exports2.MissingGSIAnalyzer = exports2.FullTableScanAnalyzer = void 0;
|
|
3300
|
+
exports2.LambdaHighTimeoutAnalyzer = exports2.LambdaDefaultMemoryAnalyzer = exports2.MissingLogRetentionAnalyzer = exports2.MissingSecretRotationAnalyzer = exports2.LargeQueueBacklogAnalyzer = exports2.UnencryptedQueueAnalyzer = exports2.MissingDLQAnalyzer = exports2.IaCDriftAnalyzer = exports2.MongoCollectionScanAnalyzer = exports2.MissingMongoIndexAnalyzer = exports2.MySQLFullTableScanAnalyzer = exports2.MissingMySQLIndexAnalyzer = exports2.LargeSelectAnalyzer = exports2.NplusOneAnalyzer = exports2.MissingIndexAnalyzer = exports2.HotPartitionAnalyzer = exports2.MissingGSIAnalyzer = exports2.FullTableScanAnalyzer = void 0;
|
|
2353
3301
|
exports2.runAllAnalyzers = runAllAnalyzers2;
|
|
2354
3302
|
exports2.summarizeFindings = summarizeFindings;
|
|
2355
3303
|
var core_1 = require_dist();
|
|
@@ -2358,6 +3306,7 @@ var require_dist9 = __commonJS({
|
|
|
2358
3306
|
var mysql_1 = require_mysql();
|
|
2359
3307
|
var mongodb_1 = require_mongodb();
|
|
2360
3308
|
var terraform_1 = require_terraform();
|
|
3309
|
+
var aws_services_1 = require_aws_services();
|
|
2361
3310
|
var dynamodb_2 = require_dynamodb();
|
|
2362
3311
|
Object.defineProperty(exports2, "FullTableScanAnalyzer", { enumerable: true, get: function() {
|
|
2363
3312
|
return dynamodb_2.FullTableScanAnalyzer;
|
|
@@ -2396,18 +3345,56 @@ var require_dist9 = __commonJS({
|
|
|
2396
3345
|
Object.defineProperty(exports2, "IaCDriftAnalyzer", { enumerable: true, get: function() {
|
|
2397
3346
|
return terraform_2.IaCDriftAnalyzer;
|
|
2398
3347
|
} });
|
|
3348
|
+
var aws_services_2 = require_aws_services();
|
|
3349
|
+
Object.defineProperty(exports2, "MissingDLQAnalyzer", { enumerable: true, get: function() {
|
|
3350
|
+
return aws_services_2.MissingDLQAnalyzer;
|
|
3351
|
+
} });
|
|
3352
|
+
Object.defineProperty(exports2, "UnencryptedQueueAnalyzer", { enumerable: true, get: function() {
|
|
3353
|
+
return aws_services_2.UnencryptedQueueAnalyzer;
|
|
3354
|
+
} });
|
|
3355
|
+
Object.defineProperty(exports2, "LargeQueueBacklogAnalyzer", { enumerable: true, get: function() {
|
|
3356
|
+
return aws_services_2.LargeQueueBacklogAnalyzer;
|
|
3357
|
+
} });
|
|
3358
|
+
Object.defineProperty(exports2, "MissingSecretRotationAnalyzer", { enumerable: true, get: function() {
|
|
3359
|
+
return aws_services_2.MissingSecretRotationAnalyzer;
|
|
3360
|
+
} });
|
|
3361
|
+
Object.defineProperty(exports2, "MissingLogRetentionAnalyzer", { enumerable: true, get: function() {
|
|
3362
|
+
return aws_services_2.MissingLogRetentionAnalyzer;
|
|
3363
|
+
} });
|
|
3364
|
+
Object.defineProperty(exports2, "LambdaDefaultMemoryAnalyzer", { enumerable: true, get: function() {
|
|
3365
|
+
return aws_services_2.LambdaDefaultMemoryAnalyzer;
|
|
3366
|
+
} });
|
|
3367
|
+
Object.defineProperty(exports2, "LambdaHighTimeoutAnalyzer", { enumerable: true, get: function() {
|
|
3368
|
+
return aws_services_2.LambdaHighTimeoutAnalyzer;
|
|
3369
|
+
} });
|
|
2399
3370
|
var DEFAULT_ANALYZERS = [
|
|
3371
|
+
// DynamoDB
|
|
2400
3372
|
new dynamodb_1.FullTableScanAnalyzer(),
|
|
2401
3373
|
new dynamodb_1.MissingGSIAnalyzer(),
|
|
2402
3374
|
new dynamodb_1.HotPartitionAnalyzer(),
|
|
3375
|
+
// PostgreSQL
|
|
2403
3376
|
new postgres_1.MissingIndexAnalyzer(),
|
|
2404
3377
|
new postgres_1.NplusOneAnalyzer(),
|
|
2405
3378
|
new postgres_1.LargeSelectAnalyzer(),
|
|
3379
|
+
// MySQL
|
|
2406
3380
|
new mysql_1.MissingMySQLIndexAnalyzer(),
|
|
2407
3381
|
new mysql_1.MySQLFullTableScanAnalyzer(),
|
|
3382
|
+
// MongoDB
|
|
2408
3383
|
new mongodb_1.MissingMongoIndexAnalyzer(),
|
|
2409
3384
|
new mongodb_1.MongoCollectionScanAnalyzer(),
|
|
2410
|
-
|
|
3385
|
+
// IaC drift
|
|
3386
|
+
new terraform_1.IaCDriftAnalyzer(),
|
|
3387
|
+
// SQS / messaging
|
|
3388
|
+
new aws_services_1.MissingDLQAnalyzer(),
|
|
3389
|
+
new aws_services_1.UnencryptedQueueAnalyzer(),
|
|
3390
|
+
new aws_services_1.LargeQueueBacklogAnalyzer(),
|
|
3391
|
+
// Secrets Manager
|
|
3392
|
+
new aws_services_1.MissingSecretRotationAnalyzer(),
|
|
3393
|
+
// CloudWatch Logs
|
|
3394
|
+
new aws_services_1.MissingLogRetentionAnalyzer(),
|
|
3395
|
+
// Lambda
|
|
3396
|
+
new aws_services_1.LambdaDefaultMemoryAnalyzer(),
|
|
3397
|
+
new aws_services_1.LambdaHighTimeoutAnalyzer()
|
|
2411
3398
|
];
|
|
2412
3399
|
async function runAllAnalyzers2(graph, analyzers = DEFAULT_ANALYZERS) {
|
|
2413
3400
|
const allFindings = [];
|
|
@@ -2437,7 +3424,7 @@ var require_dist9 = __commonJS({
|
|
|
2437
3424
|
});
|
|
2438
3425
|
|
|
2439
3426
|
// ../server/dist/index.js
|
|
2440
|
-
var
|
|
3427
|
+
var require_dist12 = __commonJS({
|
|
2441
3428
|
"../server/dist/index.js"(exports2) {
|
|
2442
3429
|
"use strict";
|
|
2443
3430
|
var __importDefault = exports2 && exports2.__importDefault || function(mod) {
|
|
@@ -2450,7 +3437,7 @@ var require_dist10 = __commonJS({
|
|
|
2450
3437
|
var fastify_1 = __importDefault(require("fastify"));
|
|
2451
3438
|
var cors_1 = __importDefault(require("@fastify/cors"));
|
|
2452
3439
|
var core_1 = require_dist();
|
|
2453
|
-
var graph_1 =
|
|
3440
|
+
var graph_1 = require_dist10();
|
|
2454
3441
|
var currentGraph = { nodes: [], edges: [] };
|
|
2455
3442
|
exports2.currentGraph = currentGraph;
|
|
2456
3443
|
var currentFindings = [];
|
|
@@ -2460,25 +3447,20 @@ var require_dist10 = __commonJS({
|
|
|
2460
3447
|
exports2.currentFindings = currentFindings = findings;
|
|
2461
3448
|
}
|
|
2462
3449
|
function createServer2(port = 3e3) {
|
|
2463
|
-
const fastify = (0, fastify_1.default)({
|
|
2464
|
-
logger: false
|
|
2465
|
-
// We use pino directly
|
|
2466
|
-
});
|
|
3450
|
+
const fastify = (0, fastify_1.default)({ logger: false });
|
|
2467
3451
|
fastify.register(cors_1.default, { origin: true });
|
|
2468
|
-
fastify.get("/health", async () => {
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
};
|
|
2476
|
-
});
|
|
3452
|
+
fastify.get("/health", async () => ({
|
|
3453
|
+
status: "ok",
|
|
3454
|
+
version: "0.1.0",
|
|
3455
|
+
graphNodes: currentGraph.nodes.length,
|
|
3456
|
+
graphEdges: currentGraph.edges.length,
|
|
3457
|
+
findings: currentFindings.length
|
|
3458
|
+
}));
|
|
2477
3459
|
fastify.post("/mcp", async (request, reply) => {
|
|
2478
3460
|
const { tool, input } = request.body;
|
|
2479
3461
|
if (!tool) {
|
|
2480
3462
|
reply.status(400);
|
|
2481
|
-
return { success: false, error: 'Missing "tool" field
|
|
3463
|
+
return { success: false, error: 'Missing "tool" field' };
|
|
2482
3464
|
}
|
|
2483
3465
|
core_1.logger.info(`MCP tool call: ${tool}`);
|
|
2484
3466
|
try {
|
|
@@ -2487,64 +3469,142 @@ var require_dist10 = __commonJS({
|
|
|
2487
3469
|
} catch (err) {
|
|
2488
3470
|
core_1.logger.error(`MCP tool "${tool}" failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2489
3471
|
reply.status(500);
|
|
2490
|
-
return {
|
|
2491
|
-
success: false,
|
|
2492
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
2493
|
-
};
|
|
3472
|
+
return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
|
|
2494
3473
|
}
|
|
2495
3474
|
});
|
|
2496
|
-
fastify.get("/mcp/tools", async () => {
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
3475
|
+
fastify.get("/mcp/tools", async () => ({
|
|
3476
|
+
tools: [
|
|
3477
|
+
// ── Overview ──────────────────────────────────────────────────────────
|
|
3478
|
+
{
|
|
3479
|
+
name: "get_infra_overview",
|
|
3480
|
+
description: "Returns a complete snapshot of all infrastructure: databases, queues, topics, secrets, parameters, log groups, lambdas, and all findings. Start here for a full picture.",
|
|
3481
|
+
input: {}
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
name: "get_graph_summary",
|
|
3485
|
+
description: "Returns the full infrastructure graph (all nodes and edges) plus findings summary.",
|
|
3486
|
+
input: {}
|
|
3487
|
+
},
|
|
3488
|
+
// ── Code analysis ────────────────────────────────────────────────────
|
|
3489
|
+
{
|
|
3490
|
+
name: "analyze_function",
|
|
3491
|
+
description: "Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, etc.",
|
|
3492
|
+
input: { function: "string \u2014 function name" }
|
|
3493
|
+
},
|
|
3494
|
+
// ── Database helpers ─────────────────────────────────────────────────
|
|
3495
|
+
{
|
|
3496
|
+
name: "suggest_gsi",
|
|
3497
|
+
description: "Get GSI suggestions for a DynamoDB table and attribute",
|
|
3498
|
+
input: { table: "string", attribute: "string" }
|
|
3499
|
+
},
|
|
3500
|
+
{
|
|
3501
|
+
name: "postgres_index_suggestions",
|
|
3502
|
+
description: "Get PostgreSQL index suggestions for a table column",
|
|
3503
|
+
input: { table: "string", column: "string" }
|
|
3504
|
+
},
|
|
3505
|
+
{
|
|
3506
|
+
name: "suggest_mongo_index",
|
|
3507
|
+
description: "Get index suggestions for a MongoDB collection field",
|
|
3508
|
+
input: { collection: "string", field: "string" }
|
|
3509
|
+
},
|
|
3510
|
+
{
|
|
3511
|
+
name: "mysql_index_suggestions",
|
|
3512
|
+
description: "Get MySQL index suggestions for a table column",
|
|
3513
|
+
input: { table: "string", column: "string" }
|
|
3514
|
+
},
|
|
3515
|
+
// ── Messaging ────────────────────────────────────────────────────────
|
|
3516
|
+
{
|
|
3517
|
+
name: "get_queue_details",
|
|
3518
|
+
description: "Returns all SQS queues with DLQ status, encryption, message counts, and retention. Use to audit messaging infrastructure.",
|
|
3519
|
+
input: {}
|
|
3520
|
+
},
|
|
3521
|
+
{
|
|
3522
|
+
name: "get_topic_details",
|
|
3523
|
+
description: "Returns all SNS topics with subscription counts and protocols.",
|
|
3524
|
+
input: {}
|
|
3525
|
+
},
|
|
3526
|
+
// ── Secrets & config ─────────────────────────────────────────────────
|
|
3527
|
+
{
|
|
3528
|
+
name: "get_secrets_overview",
|
|
3529
|
+
description: "Returns all Secrets Manager secrets: names, rotation status, last accessed. Secret VALUES are never included.",
|
|
3530
|
+
input: {}
|
|
3531
|
+
},
|
|
3532
|
+
{
|
|
3533
|
+
name: "get_parameter_overview",
|
|
3534
|
+
description: "Returns all SSM Parameter Store parameters: names, types, tiers. Parameter VALUES are never included.",
|
|
3535
|
+
input: {}
|
|
3536
|
+
},
|
|
3537
|
+
// ── Compute ──────────────────────────────────────────────────────────
|
|
3538
|
+
{
|
|
3539
|
+
name: "get_lambda_overview",
|
|
3540
|
+
description: "Returns all Lambda functions: runtime, memory, timeout, env var key names (values never included).",
|
|
3541
|
+
input: {}
|
|
3542
|
+
},
|
|
3543
|
+
// ── Observability ────────────────────────────────────────────────────
|
|
3544
|
+
{
|
|
3545
|
+
name: "get_log_errors",
|
|
3546
|
+
description: "Returns recent error patterns from CloudWatch log groups. Returns pattern counts and frequencies \u2014 never raw log messages. Safe for context.",
|
|
3547
|
+
input: {
|
|
3548
|
+
logGroup: "string (optional) \u2014 filter to a specific log group name"
|
|
2540
3549
|
}
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
});
|
|
3550
|
+
}
|
|
3551
|
+
]
|
|
3552
|
+
}));
|
|
2544
3553
|
return { fastify, start: () => startServer(fastify, port) };
|
|
2545
3554
|
}
|
|
2546
3555
|
async function handleToolCall(tool, input) {
|
|
2547
3556
|
switch (tool) {
|
|
3557
|
+
// ── Overview ─────────────────────────────────────────────────────────────
|
|
3558
|
+
case "get_infra_overview": {
|
|
3559
|
+
const tables = (0, graph_1.getTableNodes)(currentGraph);
|
|
3560
|
+
const queues = (0, graph_1.getQueueNodes)(currentGraph);
|
|
3561
|
+
const topics = (0, graph_1.getTopicNodes)(currentGraph);
|
|
3562
|
+
const secrets = (0, graph_1.getSecretNodes)(currentGraph);
|
|
3563
|
+
const parameters = (0, graph_1.getParameterNodes)(currentGraph);
|
|
3564
|
+
const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph);
|
|
3565
|
+
const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
|
|
3566
|
+
const functions = (0, graph_1.getFunctionNodes)(currentGraph);
|
|
3567
|
+
return {
|
|
3568
|
+
summary: {
|
|
3569
|
+
tables: tables.length,
|
|
3570
|
+
functions: functions.length,
|
|
3571
|
+
queues: queues.length,
|
|
3572
|
+
topics: topics.length,
|
|
3573
|
+
secrets: secrets.length,
|
|
3574
|
+
parameters: parameters.length,
|
|
3575
|
+
logGroups: logGroups.length,
|
|
3576
|
+
lambdas: lambdas.length,
|
|
3577
|
+
totalNodes: currentGraph.nodes.length,
|
|
3578
|
+
totalEdges: currentGraph.edges.length,
|
|
3579
|
+
findings: {
|
|
3580
|
+
total: currentFindings.length,
|
|
3581
|
+
high: currentFindings.filter((f) => f.severity === "high").length,
|
|
3582
|
+
medium: currentFindings.filter((f) => f.severity === "medium").length,
|
|
3583
|
+
low: currentFindings.filter((f) => f.severity === "low").length
|
|
3584
|
+
}
|
|
3585
|
+
},
|
|
3586
|
+
databases: tables.map((t) => ({ name: t.name, type: t.databaseType })),
|
|
3587
|
+
queues: queues.map((q) => ({
|
|
3588
|
+
name: q.name,
|
|
3589
|
+
hasDLQ: q.hasDLQ,
|
|
3590
|
+
encrypted: q.encrypted,
|
|
3591
|
+
approximateMessages: q.approximateMessages
|
|
3592
|
+
})),
|
|
3593
|
+
topics: topics.map((t) => ({ name: t.name, subscriptions: t.subscriptionCount })),
|
|
3594
|
+
secrets: secrets.map((s) => ({ name: s.name, rotationEnabled: s.rotationEnabled })),
|
|
3595
|
+
parameters: parameters.map((p) => ({ name: p.name, type: p.paramType, tier: p.tier })),
|
|
3596
|
+
lambdas: lambdas.map((l) => ({ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB })),
|
|
3597
|
+
logGroups: logGroups.map((lg) => ({
|
|
3598
|
+
name: lg.name,
|
|
3599
|
+
retentionDays: lg.retentionDays ?? "never",
|
|
3600
|
+
errorCount: lg.errorCount
|
|
3601
|
+
})),
|
|
3602
|
+
highFindings: currentFindings.filter((f) => f.severity === "high").map((f) => ({
|
|
3603
|
+
issue: f.issue,
|
|
3604
|
+
recommendation: f.recommendation
|
|
3605
|
+
}))
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
2548
3608
|
case "get_graph_summary": {
|
|
2549
3609
|
return {
|
|
2550
3610
|
nodes: currentGraph.nodes,
|
|
@@ -2555,6 +3615,7 @@ var require_dist10 = __commonJS({
|
|
|
2555
3615
|
totalEdges: currentGraph.edges.length,
|
|
2556
3616
|
tables: (0, graph_1.getTableNodes)(currentGraph).length,
|
|
2557
3617
|
functions: (0, graph_1.getFunctionNodes)(currentGraph).length,
|
|
3618
|
+
queues: (0, graph_1.getQueueNodes)(currentGraph).length,
|
|
2558
3619
|
scans: (0, graph_1.getScanEdges)(currentGraph).length,
|
|
2559
3620
|
totalFindings: currentFindings.length,
|
|
2560
3621
|
highSeverity: currentFindings.filter((f) => f.severity === "high").length,
|
|
@@ -2563,11 +3624,11 @@ var require_dist10 = __commonJS({
|
|
|
2563
3624
|
}
|
|
2564
3625
|
};
|
|
2565
3626
|
}
|
|
3627
|
+
// ── Code analysis ──────────────────────────────────────────────────────────
|
|
2566
3628
|
case "analyze_function": {
|
|
2567
3629
|
const functionName = String(input.function ?? "");
|
|
2568
|
-
if (!functionName)
|
|
3630
|
+
if (!functionName)
|
|
2569
3631
|
throw new Error("Missing input.function");
|
|
2570
|
-
}
|
|
2571
3632
|
const funcNode = currentGraph.nodes.find((n) => n.type === "function" && n.name === functionName);
|
|
2572
3633
|
if (!funcNode) {
|
|
2573
3634
|
return {
|
|
@@ -2580,141 +3641,203 @@ var require_dist10 = __commonJS({
|
|
|
2580
3641
|
const outEdges = (0, graph_1.getOutgoingEdges)(currentGraph, funcNode.id);
|
|
2581
3642
|
const relatedFindings = currentFindings.filter((f) => {
|
|
2582
3643
|
const meta = f.metadata;
|
|
2583
|
-
return meta?.functionName === functionName || meta?.callerFunctions
|
|
3644
|
+
return meta?.functionName === functionName || String(meta?.callerFunctions ?? "").includes(functionName);
|
|
2584
3645
|
});
|
|
2585
|
-
const issues = relatedFindings.map((f) => ({
|
|
2586
|
-
severity: f.severity,
|
|
2587
|
-
issue: f.issue,
|
|
2588
|
-
description: f.description
|
|
2589
|
-
}));
|
|
2590
|
-
const recommendations = relatedFindings.map((f) => f.recommendation);
|
|
2591
|
-
const uniqueRecs = [...new Set(recommendations)];
|
|
2592
3646
|
return {
|
|
2593
3647
|
function: functionName,
|
|
2594
3648
|
found: true,
|
|
2595
3649
|
file: funcNode.type === "function" ? funcNode.file : void 0,
|
|
2596
|
-
|
|
3650
|
+
accesses: outEdges.map((e) => {
|
|
2597
3651
|
const target = currentGraph.nodes.find((n) => n.id === e.to);
|
|
2598
|
-
return {
|
|
3652
|
+
return {
|
|
3653
|
+
targetId: e.to,
|
|
3654
|
+
edgeType: e.type,
|
|
3655
|
+
targetName: target && "name" in target ? target.name : e.to,
|
|
3656
|
+
targetType: target?.type
|
|
3657
|
+
};
|
|
2599
3658
|
}),
|
|
2600
|
-
issues
|
|
2601
|
-
|
|
3659
|
+
issues: relatedFindings.map((f) => ({
|
|
3660
|
+
severity: f.severity,
|
|
3661
|
+
issue: f.issue,
|
|
3662
|
+
description: f.description
|
|
3663
|
+
})),
|
|
3664
|
+
recommendations: [...new Set(relatedFindings.map((f) => f.recommendation))]
|
|
2602
3665
|
};
|
|
2603
3666
|
}
|
|
3667
|
+
// ── Database helpers ───────────────────────────────────────────────────────
|
|
2604
3668
|
case "suggest_gsi": {
|
|
2605
3669
|
const tableName = String(input.table ?? "");
|
|
2606
3670
|
const attribute = String(input.attribute ?? "");
|
|
2607
|
-
if (!tableName || !attribute)
|
|
3671
|
+
if (!tableName || !attribute)
|
|
2608
3672
|
throw new Error("Missing input.table or input.attribute");
|
|
2609
|
-
}
|
|
2610
3673
|
const tableNode = currentGraph.nodes.find((n) => n.type === "table" && n.databaseType === "dynamodb" && "name" in n && n.name === tableName);
|
|
2611
|
-
if (!tableNode) {
|
|
2612
|
-
return {
|
|
2613
|
-
table: tableName,
|
|
2614
|
-
attribute,
|
|
2615
|
-
found: false,
|
|
2616
|
-
message: `DynamoDB table "${tableName}" not found in analyzed graph`
|
|
2617
|
-
};
|
|
2618
|
-
}
|
|
2619
3674
|
const sanitizedAttr = attribute.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2620
3675
|
const indexName = `${tableName}-${sanitizedAttr}-index`;
|
|
2621
3676
|
return {
|
|
2622
3677
|
table: tableName,
|
|
2623
3678
|
attribute,
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
billingMode: "PAY_PER_REQUEST"
|
|
2629
|
-
},
|
|
2630
|
-
rationale: `Adding a GSI with partition key "${attribute}" will allow efficient Query operations instead of full table Scans when filtering by this attribute.`,
|
|
2631
|
-
estimatedCost: "Approximately double the storage cost for the projected attributes.",
|
|
2632
|
-
recommendation: `Add the following GSI to your CloudFormation/Terraform definition:
|
|
2633
|
-
|
|
2634
|
-
GSI Name: ${indexName}
|
|
2635
|
-
Partition Key: ${attribute}
|
|
2636
|
-
Sort Key: (optional, add for range queries)
|
|
2637
|
-
Projection: ALL`
|
|
3679
|
+
found: !!tableNode,
|
|
3680
|
+
index: { name: indexName, partitionKey: attribute, projectionType: "ALL", billingMode: "PAY_PER_REQUEST" },
|
|
3681
|
+
rationale: `A GSI on "${attribute}" allows Query instead of Scan when filtering by this attribute.`,
|
|
3682
|
+
recommendation: `Add GSI "${indexName}" with partition key "${attribute}" to your IaC definition.`
|
|
2638
3683
|
};
|
|
2639
3684
|
}
|
|
2640
3685
|
case "postgres_index_suggestions": {
|
|
2641
3686
|
const tableName = String(input.table ?? "");
|
|
2642
3687
|
const column = String(input.column ?? "");
|
|
2643
|
-
if (!tableName || !column)
|
|
3688
|
+
if (!tableName || !column)
|
|
2644
3689
|
throw new Error("Missing input.table or input.column");
|
|
2645
|
-
}
|
|
2646
|
-
const tableNode = currentGraph.nodes.find((n) => n.type === "table" && n.databaseType === "postgres" && "name" in n && (n.name === tableName || n.name.endsWith(`.${tableName}`)));
|
|
2647
3690
|
const sanitizedCol = column.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2648
3691
|
const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2649
3692
|
const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
|
|
2650
3693
|
return {
|
|
2651
3694
|
table: tableName,
|
|
2652
3695
|
column,
|
|
2653
|
-
found: !!tableNode,
|
|
2654
3696
|
recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${tableName} (${column});`,
|
|
2655
|
-
rationale: `An index on
|
|
3697
|
+
rationale: `An index on "${column}" eliminates sequential scans when filtering on this column.`,
|
|
2656
3698
|
notes: [
|
|
2657
|
-
"Use CONCURRENTLY to avoid locking the table
|
|
2658
|
-
"
|
|
2659
|
-
|
|
2660
|
-
`Example partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${tableName} (${column}) WHERE ${column} IS NOT NULL;`
|
|
3699
|
+
"Use CONCURRENTLY to avoid locking the table",
|
|
3700
|
+
"Run ANALYZE after creation",
|
|
3701
|
+
`Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${tableName} (${column}) WHERE ${column} IS NOT NULL;`
|
|
2661
3702
|
]
|
|
2662
3703
|
};
|
|
2663
3704
|
}
|
|
2664
3705
|
case "suggest_mongo_index": {
|
|
2665
3706
|
const collection = String(input.collection ?? "");
|
|
2666
3707
|
const field = String(input.field ?? "");
|
|
2667
|
-
if (!collection || !field)
|
|
3708
|
+
if (!collection || !field)
|
|
2668
3709
|
throw new Error("Missing input.collection or input.field");
|
|
2669
|
-
}
|
|
2670
|
-
const sanitizedField = field.replace(/[^a-zA-Z0-9_.]/g, "_");
|
|
2671
3710
|
return {
|
|
2672
3711
|
collection,
|
|
2673
3712
|
field,
|
|
2674
3713
|
recommendation: `db.${collection}.createIndex({ ${field}: 1 })`,
|
|
2675
|
-
rationale: `An index on
|
|
3714
|
+
rationale: `An index on "${field}" eliminates full collection scans when filtering on this field.`,
|
|
2676
3715
|
notes: [
|
|
2677
|
-
|
|
2678
|
-
`
|
|
2679
|
-
`
|
|
2680
|
-
`Run db.${collection}.explain("executionStats").find({ ${field}: value }) to verify the index is used`
|
|
3716
|
+
`Compound: db.${collection}.createIndex({ ${field}: 1, otherField: 1 })`,
|
|
3717
|
+
`Text: db.${collection}.createIndex({ ${field}: "text" })`,
|
|
3718
|
+
`Verify: db.${collection}.explain("executionStats").find({ ${field}: value })`
|
|
2681
3719
|
]
|
|
2682
3720
|
};
|
|
2683
3721
|
}
|
|
2684
3722
|
case "mysql_index_suggestions": {
|
|
2685
3723
|
const tableName = String(input.table ?? "");
|
|
2686
3724
|
const column = String(input.column ?? "");
|
|
2687
|
-
if (!tableName || !column)
|
|
3725
|
+
if (!tableName || !column)
|
|
2688
3726
|
throw new Error("Missing input.table or input.column");
|
|
2689
|
-
}
|
|
2690
3727
|
const sanitizedCol = column.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2691
3728
|
const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2692
3729
|
const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
|
|
2693
|
-
const tableNode = currentGraph.nodes.find((n) => n.type === "table" && n.databaseType === "mysql" && "name" in n && (n.name === tableName || n.name.endsWith(`.${tableName}`)));
|
|
2694
3730
|
return {
|
|
2695
3731
|
table: tableName,
|
|
2696
3732
|
column,
|
|
2697
|
-
found: !!tableNode,
|
|
2698
3733
|
recommendation: `ALTER TABLE ${tableName} ADD INDEX ${indexName} (${column});`,
|
|
2699
|
-
rationale: `An index on
|
|
3734
|
+
rationale: `An index on "${column}" eliminates full table scans when filtering on this column.`,
|
|
2700
3735
|
notes: [
|
|
2701
|
-
"MySQL adds indexes online (no full
|
|
2702
|
-
`
|
|
2703
|
-
`
|
|
2704
|
-
`Example composite: ALTER TABLE ${tableName} ADD INDEX idx_${sanitizedTable}_composite (${column}, other_column);`
|
|
3736
|
+
"MySQL InnoDB adds indexes online (no full lock for 5.6+)",
|
|
3737
|
+
`EXPLAIN SELECT ... to verify after adding`,
|
|
3738
|
+
`Composite: ALTER TABLE ${tableName} ADD INDEX idx_composite (${column}, other_column);`
|
|
2705
3739
|
]
|
|
2706
3740
|
};
|
|
2707
3741
|
}
|
|
3742
|
+
// ── Messaging ─────────────────────────────────────────────────────────────
|
|
3743
|
+
case "get_queue_details": {
|
|
3744
|
+
const queues = (0, graph_1.getQueueNodes)(currentGraph);
|
|
3745
|
+
const queueFindings = currentFindings.filter((f) => f.metadata?.queueName);
|
|
3746
|
+
return {
|
|
3747
|
+
total: queues.length,
|
|
3748
|
+
queues: queues.map((q) => ({
|
|
3749
|
+
name: q.name,
|
|
3750
|
+
provider: q.provider,
|
|
3751
|
+
hasDLQ: q.hasDLQ,
|
|
3752
|
+
encrypted: q.encrypted,
|
|
3753
|
+
approximateMessages: q.approximateMessages,
|
|
3754
|
+
retentionDays: q.retentionDays,
|
|
3755
|
+
findings: queueFindings.filter((f) => f.metadata.queueName === q.name).map((f) => ({ severity: f.severity, issue: f.issue }))
|
|
3756
|
+
}))
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
case "get_topic_details": {
|
|
3760
|
+
const topics = (0, graph_1.getTopicNodes)(currentGraph);
|
|
3761
|
+
return {
|
|
3762
|
+
total: topics.length,
|
|
3763
|
+
topics: topics.map((t) => ({
|
|
3764
|
+
name: t.name,
|
|
3765
|
+
provider: t.provider,
|
|
3766
|
+
subscriptionCount: t.subscriptionCount,
|
|
3767
|
+
encrypted: t.encrypted
|
|
3768
|
+
}))
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
// ── Secrets & config ──────────────────────────────────────────────────────
|
|
3772
|
+
case "get_secrets_overview": {
|
|
3773
|
+
const secrets = (0, graph_1.getSecretNodes)(currentGraph);
|
|
3774
|
+
const secretFindings = currentFindings.filter((f) => f.metadata?.secretName);
|
|
3775
|
+
return {
|
|
3776
|
+
total: secrets.length,
|
|
3777
|
+
note: "Secret values are never included in this response.",
|
|
3778
|
+
secrets: secrets.map((s) => ({
|
|
3779
|
+
name: s.name,
|
|
3780
|
+
provider: s.provider,
|
|
3781
|
+
rotationEnabled: s.rotationEnabled,
|
|
3782
|
+
rotationDays: s.rotationDays,
|
|
3783
|
+
findings: secretFindings.filter((f) => f.metadata.secretName === s.name).map((f) => ({ severity: f.severity, issue: f.issue }))
|
|
3784
|
+
}))
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
case "get_parameter_overview": {
|
|
3788
|
+
const parameters = (0, graph_1.getParameterNodes)(currentGraph);
|
|
3789
|
+
return {
|
|
3790
|
+
total: parameters.length,
|
|
3791
|
+
note: "Parameter values are never included in this response.",
|
|
3792
|
+
parameters: parameters.map((p) => ({
|
|
3793
|
+
name: p.name,
|
|
3794
|
+
provider: p.provider,
|
|
3795
|
+
type: p.paramType,
|
|
3796
|
+
tier: p.tier
|
|
3797
|
+
}))
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
// ── Compute ───────────────────────────────────────────────────────────────
|
|
3801
|
+
case "get_lambda_overview": {
|
|
3802
|
+
const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
|
|
3803
|
+
const lambdaFindings = currentFindings.filter((f) => f.metadata?.functionName);
|
|
3804
|
+
return {
|
|
3805
|
+
total: lambdas.length,
|
|
3806
|
+
note: "Environment variable values are never included.",
|
|
3807
|
+
lambdas: lambdas.map((l) => ({
|
|
3808
|
+
name: l.name,
|
|
3809
|
+
runtime: l.runtime,
|
|
3810
|
+
memoryMB: l.memoryMB,
|
|
3811
|
+
timeoutSec: l.timeoutSec,
|
|
3812
|
+
envVarCount: l.envVarKeys?.length ?? 0,
|
|
3813
|
+
envVarKeys: l.envVarKeys,
|
|
3814
|
+
findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue }))
|
|
3815
|
+
}))
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
// ── Observability ─────────────────────────────────────────────────────────
|
|
3819
|
+
case "get_log_errors": {
|
|
3820
|
+
const filterName = input.logGroup ? String(input.logGroup) : void 0;
|
|
3821
|
+
const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph).filter((lg) => !filterName || lg.name.includes(filterName));
|
|
3822
|
+
return {
|
|
3823
|
+
note: "Only error patterns and counts are returned \u2014 no raw log messages.",
|
|
3824
|
+
windowHours: 24,
|
|
3825
|
+
logGroups: logGroups.map((lg) => ({
|
|
3826
|
+
name: lg.name,
|
|
3827
|
+
retentionDays: lg.retentionDays ?? "never-expires",
|
|
3828
|
+
errorCount: lg.errorCount,
|
|
3829
|
+
topErrorPatterns: lg.topErrorPatterns
|
|
3830
|
+
}))
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
2708
3833
|
default:
|
|
2709
|
-
throw new Error(`Unknown tool: "${tool}".
|
|
3834
|
+
throw new Error(`Unknown tool: "${tool}". Call GET /mcp/tools for the list of available tools.`);
|
|
2710
3835
|
}
|
|
2711
3836
|
}
|
|
2712
3837
|
async function startServer(fastify, port) {
|
|
2713
3838
|
try {
|
|
2714
3839
|
await fastify.listen({ port, host: "0.0.0.0" });
|
|
2715
3840
|
core_1.logger.info(`Infrawise MCP server running at http://localhost:${port}`);
|
|
2716
|
-
core_1.logger.info(`MCP endpoint: http://localhost:${port}/mcp`);
|
|
2717
|
-
core_1.logger.info(`Available tools: http://localhost:${port}/mcp/tools`);
|
|
2718
3841
|
} catch (err) {
|
|
2719
3842
|
core_1.logger.error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`);
|
|
2720
3843
|
process.exit(1);
|
|
@@ -2810,18 +3933,7 @@ function severityBadge(severity) {
|
|
|
2810
3933
|
return import_chalk.default.bgCyan.black.bold(` LOW `);
|
|
2811
3934
|
}
|
|
2812
3935
|
}
|
|
2813
|
-
function severityColor(severity) {
|
|
2814
|
-
switch (severity) {
|
|
2815
|
-
case "high":
|
|
2816
|
-
return import_chalk.default.red;
|
|
2817
|
-
case "medium":
|
|
2818
|
-
return import_chalk.default.yellow;
|
|
2819
|
-
case "low":
|
|
2820
|
-
return import_chalk.default.cyan;
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
3936
|
function printFinding(finding, index) {
|
|
2824
|
-
const color = severityColor(finding.severity);
|
|
2825
3937
|
const badge = severityBadge(finding.severity);
|
|
2826
3938
|
const num = import_chalk.default.dim(`${index + 1}.`);
|
|
2827
3939
|
console.log(`
|
|
@@ -2869,7 +3981,7 @@ async function runInit(options = {}) {
|
|
|
2869
3981
|
log.success(`Type`, repoType);
|
|
2870
3982
|
log.success(`AWS profiles found`, String(profiles.length));
|
|
2871
3983
|
console.log("");
|
|
2872
|
-
const
|
|
3984
|
+
const core = await import_inquirer.default.prompt([
|
|
2873
3985
|
{
|
|
2874
3986
|
type: "input",
|
|
2875
3987
|
name: "project",
|
|
@@ -2888,13 +4000,16 @@ async function runInit(options = {}) {
|
|
|
2888
4000
|
name: "region",
|
|
2889
4001
|
message: "AWS region:",
|
|
2890
4002
|
default: detectedRegion
|
|
2891
|
-
}
|
|
4003
|
+
}
|
|
4004
|
+
]);
|
|
4005
|
+
console.log("\n " + import_chalk2.default.bold("Databases"));
|
|
4006
|
+
const databases = await import_inquirer.default.prompt([
|
|
2892
4007
|
{
|
|
2893
4008
|
type: "input",
|
|
2894
4009
|
name: "dynamoTables",
|
|
2895
4010
|
message: "DynamoDB tables to include:",
|
|
2896
4011
|
default: "",
|
|
2897
|
-
suffix: import_chalk2.default.dim(" (comma-separated,
|
|
4012
|
+
suffix: import_chalk2.default.dim(" (comma-separated, blank = all)")
|
|
2898
4013
|
},
|
|
2899
4014
|
{
|
|
2900
4015
|
type: "confirm",
|
|
@@ -2936,21 +4051,80 @@ async function runInit(options = {}) {
|
|
|
2936
4051
|
when: (a) => a.mongoEnabled
|
|
2937
4052
|
}
|
|
2938
4053
|
]);
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
4054
|
+
console.log("\n " + import_chalk2.default.bold("AWS Services"));
|
|
4055
|
+
console.log(import_chalk2.default.dim(" Infrawise will introspect these services \u2014 credentials from the AWS profile above."));
|
|
4056
|
+
const services = await import_inquirer.default.prompt([
|
|
4057
|
+
{
|
|
4058
|
+
type: "confirm",
|
|
4059
|
+
name: "sqsEnabled",
|
|
4060
|
+
message: "Introspect SQS queues?",
|
|
4061
|
+
default: true
|
|
4062
|
+
},
|
|
4063
|
+
{
|
|
4064
|
+
type: "confirm",
|
|
4065
|
+
name: "snsEnabled",
|
|
4066
|
+
message: "Introspect SNS topics?",
|
|
4067
|
+
default: true
|
|
2946
4068
|
},
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
4069
|
+
{
|
|
4070
|
+
type: "confirm",
|
|
4071
|
+
name: "ssmEnabled",
|
|
4072
|
+
message: "Introspect SSM Parameter Store? (metadata only, no values)",
|
|
4073
|
+
default: true
|
|
4074
|
+
},
|
|
4075
|
+
{
|
|
4076
|
+
type: "input",
|
|
4077
|
+
name: "ssmPaths",
|
|
4078
|
+
message: "SSM path prefixes to filter:",
|
|
4079
|
+
default: "",
|
|
4080
|
+
suffix: import_chalk2.default.dim(" (comma-separated, blank = all e.g. /myapp/prod)"),
|
|
4081
|
+
when: (a) => a.ssmEnabled
|
|
2950
4082
|
},
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
4083
|
+
{
|
|
4084
|
+
type: "confirm",
|
|
4085
|
+
name: "secretsEnabled",
|
|
4086
|
+
message: "Introspect Secrets Manager? (names & rotation only, no values)",
|
|
4087
|
+
default: true
|
|
4088
|
+
},
|
|
4089
|
+
{
|
|
4090
|
+
type: "confirm",
|
|
4091
|
+
name: "lambdaEnabled",
|
|
4092
|
+
message: "Introspect Lambda functions?",
|
|
4093
|
+
default: true
|
|
4094
|
+
},
|
|
4095
|
+
{
|
|
4096
|
+
type: "confirm",
|
|
4097
|
+
name: "logsEnabled",
|
|
4098
|
+
message: "Sample CloudWatch Logs? (error patterns only, no raw logs)",
|
|
4099
|
+
default: false
|
|
4100
|
+
},
|
|
4101
|
+
{
|
|
4102
|
+
type: "input",
|
|
4103
|
+
name: "logGroupPrefixes",
|
|
4104
|
+
message: "CloudWatch log group prefixes:",
|
|
4105
|
+
default: "",
|
|
4106
|
+
suffix: import_chalk2.default.dim(" (comma-separated, blank = all)"),
|
|
4107
|
+
when: (a) => a.logsEnabled
|
|
4108
|
+
}
|
|
4109
|
+
]);
|
|
4110
|
+
const includeTables = databases.dynamoTables ? databases.dynamoTables.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
4111
|
+
const ssmPaths = services.ssmPaths ? services.ssmPaths.split(",").map((p) => p.trim()).filter(Boolean) : [];
|
|
4112
|
+
const logGroupPrefixes = services.logGroupPrefixes ? services.logGroupPrefixes.split(",").map((p) => p.trim()).filter(Boolean) : [];
|
|
4113
|
+
const configContent = (0, import_core.generateDefaultConfig)(core.project, {
|
|
4114
|
+
aws: { profile: core.awsProfile, region: core.region },
|
|
4115
|
+
dynamodb: { includeTables },
|
|
4116
|
+
postgres: { enabled: databases.pgEnabled, connectionString: databases.pgConnectionString ?? "" },
|
|
4117
|
+
mysql: { enabled: databases.mysqlEnabled, connectionString: databases.mysqlConnectionString ?? "" },
|
|
4118
|
+
mongodb: { enabled: databases.mongoEnabled, connectionString: databases.mongoConnectionString ?? "" },
|
|
4119
|
+
sqs: { enabled: services.sqsEnabled },
|
|
4120
|
+
sns: { enabled: services.snsEnabled },
|
|
4121
|
+
ssm: { enabled: services.ssmEnabled, paths: ssmPaths },
|
|
4122
|
+
secretsManager: { enabled: services.secretsEnabled },
|
|
4123
|
+
lambda: { enabled: services.lambdaEnabled },
|
|
4124
|
+
cloudwatchLogs: {
|
|
4125
|
+
enabled: services.logsEnabled,
|
|
4126
|
+
logGroupPrefixes,
|
|
4127
|
+
windowHours: 24
|
|
2954
4128
|
}
|
|
2955
4129
|
});
|
|
2956
4130
|
fs2.writeFileSync(configPath, configContent, "utf-8");
|
|
@@ -3026,9 +4200,11 @@ var import_adapters_postgres = __toESM(require_dist3());
|
|
|
3026
4200
|
var import_adapters_mysql = __toESM(require_dist4());
|
|
3027
4201
|
var import_adapters_mongodb = __toESM(require_dist5());
|
|
3028
4202
|
var import_adapters_terraform = __toESM(require_dist6());
|
|
3029
|
-
var
|
|
3030
|
-
var
|
|
3031
|
-
var
|
|
4203
|
+
var import_adapters_aws_services = __toESM(require_dist7());
|
|
4204
|
+
var import_adapters_logs = __toESM(require_dist8());
|
|
4205
|
+
var import_context = __toESM(require_dist9());
|
|
4206
|
+
var import_graph = __toESM(require_dist10());
|
|
4207
|
+
var import_analyzers = __toESM(require_dist11());
|
|
3032
4208
|
async function runAnalyze(options = {}) {
|
|
3033
4209
|
printHeader("Running Analysis");
|
|
3034
4210
|
let config;
|
|
@@ -3040,16 +4216,18 @@ async function runAnalyze(options = {}) {
|
|
|
3040
4216
|
process.exit(1);
|
|
3041
4217
|
}
|
|
3042
4218
|
const repoPath = options.repo ?? process.cwd();
|
|
4219
|
+
const awsCfg = { region: config.aws?.region, profile: config.aws?.profile };
|
|
3043
4220
|
const dynamoMeta = [];
|
|
3044
4221
|
const postgresMeta = [];
|
|
3045
4222
|
const mysqlMeta = [];
|
|
3046
4223
|
const mongoMeta = [];
|
|
4224
|
+
const servicesMeta = {};
|
|
3047
4225
|
{
|
|
3048
|
-
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting DynamoDB
|
|
4226
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting DynamoDB tables..."), color: "cyan" }).start();
|
|
3049
4227
|
try {
|
|
3050
4228
|
const result = await (0, import_adapters_dynamodb2.extractDynamoMetadata)(config);
|
|
3051
4229
|
dynamoMeta.push(...result);
|
|
3052
|
-
spin.succeed(import_chalk4.default.green("DynamoDB") + import_chalk4.default.dim(` ${result.length} table(s)
|
|
4230
|
+
spin.succeed(import_chalk4.default.green("DynamoDB") + import_chalk4.default.dim(` ${result.length} table(s)`));
|
|
3053
4231
|
} catch (err) {
|
|
3054
4232
|
spin.warn(import_chalk4.default.yellow("DynamoDB skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
3055
4233
|
}
|
|
@@ -3059,7 +4237,7 @@ async function runAnalyze(options = {}) {
|
|
|
3059
4237
|
try {
|
|
3060
4238
|
const result = await (0, import_adapters_postgres.extractPostgresMetadata)(config.postgres.connectionString);
|
|
3061
4239
|
postgresMeta.push(...result);
|
|
3062
|
-
spin.succeed(import_chalk4.default.green("PostgreSQL") + import_chalk4.default.dim(` ${result.length} table(s)
|
|
4240
|
+
spin.succeed(import_chalk4.default.green("PostgreSQL") + import_chalk4.default.dim(` ${result.length} table(s)`));
|
|
3063
4241
|
} catch (err) {
|
|
3064
4242
|
spin.warn(import_chalk4.default.yellow("PostgreSQL skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
3065
4243
|
}
|
|
@@ -3069,7 +4247,7 @@ async function runAnalyze(options = {}) {
|
|
|
3069
4247
|
try {
|
|
3070
4248
|
const result = await (0, import_adapters_mysql.extractMySQLMetadata)(config.mysql.connectionString);
|
|
3071
4249
|
mysqlMeta.push(...result);
|
|
3072
|
-
spin.succeed(import_chalk4.default.green("MySQL") + import_chalk4.default.dim(` ${result.length} table(s)
|
|
4250
|
+
spin.succeed(import_chalk4.default.green("MySQL") + import_chalk4.default.dim(` ${result.length} table(s)`));
|
|
3073
4251
|
} catch (err) {
|
|
3074
4252
|
spin.warn(import_chalk4.default.yellow("MySQL skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
3075
4253
|
}
|
|
@@ -3077,43 +4255,106 @@ async function runAnalyze(options = {}) {
|
|
|
3077
4255
|
if (config.mongodb?.enabled && config.mongodb.connectionString) {
|
|
3078
4256
|
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting MongoDB schema..."), color: "cyan" }).start();
|
|
3079
4257
|
try {
|
|
3080
|
-
const result = await (0, import_adapters_mongodb.extractMongoMetadata)(
|
|
3081
|
-
config.mongodb.connectionString,
|
|
3082
|
-
config.mongodb.databases
|
|
3083
|
-
);
|
|
4258
|
+
const result = await (0, import_adapters_mongodb.extractMongoMetadata)(config.mongodb.connectionString, config.mongodb.databases);
|
|
3084
4259
|
mongoMeta.push(...result);
|
|
3085
|
-
spin.succeed(import_chalk4.default.green("MongoDB") + import_chalk4.default.dim(` ${result.length} collection(s)
|
|
4260
|
+
spin.succeed(import_chalk4.default.green("MongoDB") + import_chalk4.default.dim(` ${result.length} collection(s)`));
|
|
3086
4261
|
} catch (err) {
|
|
3087
4262
|
spin.warn(import_chalk4.default.yellow("MongoDB skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
3088
4263
|
}
|
|
3089
4264
|
}
|
|
4265
|
+
if (config.sqs?.enabled !== false) {
|
|
4266
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting SQS queues..."), color: "cyan" }).start();
|
|
4267
|
+
try {
|
|
4268
|
+
const result = await (0, import_adapters_aws_services.extractSQSMetadata)(awsCfg);
|
|
4269
|
+
servicesMeta.sqs = result;
|
|
4270
|
+
spin.succeed(import_chalk4.default.green("SQS") + import_chalk4.default.dim(` ${result.length} queue(s)`));
|
|
4271
|
+
} catch (err) {
|
|
4272
|
+
spin.warn(import_chalk4.default.yellow("SQS skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
if (config.sns?.enabled !== false) {
|
|
4276
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting SNS topics..."), color: "cyan" }).start();
|
|
4277
|
+
try {
|
|
4278
|
+
const result = await (0, import_adapters_aws_services.extractSNSMetadata)(awsCfg);
|
|
4279
|
+
servicesMeta.sns = result;
|
|
4280
|
+
spin.succeed(import_chalk4.default.green("SNS") + import_chalk4.default.dim(` ${result.length} topic(s)`));
|
|
4281
|
+
} catch (err) {
|
|
4282
|
+
spin.warn(import_chalk4.default.yellow("SNS skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
if (config.ssm?.enabled !== false) {
|
|
4286
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting SSM parameters..."), color: "cyan" }).start();
|
|
4287
|
+
try {
|
|
4288
|
+
const result = await (0, import_adapters_aws_services.extractSSMMetadata)({ ...awsCfg, paths: config.ssm?.paths });
|
|
4289
|
+
servicesMeta.ssm = result;
|
|
4290
|
+
spin.succeed(import_chalk4.default.green("SSM") + import_chalk4.default.dim(` ${result.length} parameter(s) `) + import_chalk4.default.dim("(metadata only, no values)"));
|
|
4291
|
+
} catch (err) {
|
|
4292
|
+
spin.warn(import_chalk4.default.yellow("SSM skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
if (config.secretsManager?.enabled !== false) {
|
|
4296
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting Secrets Manager metadata..."), color: "cyan" }).start();
|
|
4297
|
+
try {
|
|
4298
|
+
const result = await (0, import_adapters_aws_services.extractSecretsMetadata)(awsCfg);
|
|
4299
|
+
servicesMeta.secrets = result;
|
|
4300
|
+
spin.succeed(import_chalk4.default.green("Secrets Manager") + import_chalk4.default.dim(` ${result.length} secret(s) `) + import_chalk4.default.dim("(names/rotation only, no values)"));
|
|
4301
|
+
} catch (err) {
|
|
4302
|
+
spin.warn(import_chalk4.default.yellow("Secrets Manager skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
if (config.lambda?.enabled !== false) {
|
|
4306
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting Lambda functions..."), color: "cyan" }).start();
|
|
4307
|
+
try {
|
|
4308
|
+
const result = await (0, import_adapters_aws_services.extractLambdaMetadata)(awsCfg);
|
|
4309
|
+
servicesMeta.lambda = result;
|
|
4310
|
+
spin.succeed(import_chalk4.default.green("Lambda") + import_chalk4.default.dim(` ${result.length} function(s)`));
|
|
4311
|
+
} catch (err) {
|
|
4312
|
+
spin.warn(import_chalk4.default.yellow("Lambda skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
if (config.cloudwatchLogs?.enabled) {
|
|
4316
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Sampling CloudWatch Logs (errors only, max 50 groups)..."), color: "cyan" }).start();
|
|
4317
|
+
try {
|
|
4318
|
+
const result = await (0, import_adapters_logs.extractLogsSummary)({
|
|
4319
|
+
...awsCfg,
|
|
4320
|
+
logGroupPrefixes: config.cloudwatchLogs.logGroupPrefixes,
|
|
4321
|
+
windowHours: config.cloudwatchLogs.windowHours
|
|
4322
|
+
});
|
|
4323
|
+
servicesMeta.logs = result;
|
|
4324
|
+
const errorGroups = result.filter((lg) => lg.errorCount > 0).length;
|
|
4325
|
+
spin.succeed(import_chalk4.default.green("CloudWatch Logs") + import_chalk4.default.dim(` ${result.length} group(s), ${errorGroups} with errors`));
|
|
4326
|
+
} catch (err) {
|
|
4327
|
+
spin.warn(import_chalk4.default.yellow("CloudWatch Logs skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
3090
4330
|
let iacDriftAnalyzer;
|
|
3091
4331
|
{
|
|
3092
|
-
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting IaC schema (Terraform/CloudFormation)..."), color: "cyan" }).start();
|
|
4332
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Extracting IaC schema (Terraform / CloudFormation / CDK)..."), color: "cyan" }).start();
|
|
3093
4333
|
try {
|
|
3094
4334
|
const iacSchema = await (0, import_adapters_terraform.extractIaCSchema)(repoPath);
|
|
3095
|
-
const
|
|
4335
|
+
const total = iacSchema.dynamoTables.length + iacSchema.rdsInstances.length + iacSchema.mongoClusters.length + iacSchema.queues.length + iacSchema.topics.length + iacSchema.lambdas.length + iacSchema.buckets.length + iacSchema.parameters.length + iacSchema.secrets.length + iacSchema.apiGateways.length;
|
|
3096
4336
|
iacDriftAnalyzer = new import_analyzers.IaCDriftAnalyzer();
|
|
3097
4337
|
iacDriftAnalyzer.setIaCSchema(iacSchema);
|
|
3098
|
-
spin.succeed(import_chalk4.default.green("IaC schema") + import_chalk4.default.dim(` ${
|
|
4338
|
+
spin.succeed(import_chalk4.default.green("IaC schema") + import_chalk4.default.dim(` ${total} resource(s) across TF/CFN/CDK`));
|
|
3099
4339
|
} catch (err) {
|
|
3100
4340
|
spin.warn(import_chalk4.default.yellow("IaC scan skipped") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
3101
4341
|
}
|
|
3102
4342
|
}
|
|
3103
4343
|
let operations;
|
|
3104
4344
|
{
|
|
3105
|
-
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim(`Scanning ${path3.basename(repoPath)}...`), color: "cyan" }).start();
|
|
4345
|
+
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim(`Scanning ${path3.basename(repoPath)} for service usage...`), color: "cyan" }).start();
|
|
3106
4346
|
try {
|
|
3107
4347
|
operations = await (0, import_context.scanRepository)(repoPath);
|
|
3108
|
-
spin.succeed(import_chalk4.default.green("Repository scanned") + import_chalk4.default.dim(` ${operations.length}
|
|
4348
|
+
spin.succeed(import_chalk4.default.green("Repository scanned") + import_chalk4.default.dim(` ${operations.length} service operation(s) found`));
|
|
3109
4349
|
} catch (err) {
|
|
3110
4350
|
spin.warn(import_chalk4.default.yellow("Repository scan failed") + import_chalk4.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
3111
4351
|
operations = [];
|
|
3112
4352
|
}
|
|
3113
4353
|
}
|
|
4354
|
+
let graph;
|
|
3114
4355
|
{
|
|
3115
4356
|
const spin = (0, import_ora2.default)({ text: import_chalk4.default.dim("Building infrastructure graph..."), color: "cyan" }).start();
|
|
3116
|
-
|
|
4357
|
+
graph = (0, import_graph.buildGraph)(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
|
|
3117
4358
|
spin.succeed(import_chalk4.default.green("Graph built") + import_chalk4.default.dim(` ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
|
|
3118
4359
|
}
|
|
3119
4360
|
const findings = await (async () => {
|
|
@@ -3128,8 +4369,15 @@ async function runAnalyze(options = {}) {
|
|
|
3128
4369
|
MissingMySQLIndexAnalyzer,
|
|
3129
4370
|
MySQLFullTableScanAnalyzer,
|
|
3130
4371
|
MissingMongoIndexAnalyzer,
|
|
3131
|
-
MongoCollectionScanAnalyzer
|
|
3132
|
-
|
|
4372
|
+
MongoCollectionScanAnalyzer,
|
|
4373
|
+
MissingDLQAnalyzer,
|
|
4374
|
+
UnencryptedQueueAnalyzer,
|
|
4375
|
+
LargeQueueBacklogAnalyzer,
|
|
4376
|
+
MissingSecretRotationAnalyzer,
|
|
4377
|
+
MissingLogRetentionAnalyzer,
|
|
4378
|
+
LambdaDefaultMemoryAnalyzer,
|
|
4379
|
+
LambdaHighTimeoutAnalyzer
|
|
4380
|
+
} = await Promise.resolve().then(() => __toESM(require_dist11()));
|
|
3133
4381
|
const analyzers = [
|
|
3134
4382
|
new FullTableScanAnalyzer(),
|
|
3135
4383
|
new MissingGSIAnalyzer(),
|
|
@@ -3141,6 +4389,13 @@ async function runAnalyze(options = {}) {
|
|
|
3141
4389
|
new MySQLFullTableScanAnalyzer(),
|
|
3142
4390
|
new MissingMongoIndexAnalyzer(),
|
|
3143
4391
|
new MongoCollectionScanAnalyzer(),
|
|
4392
|
+
new MissingDLQAnalyzer(),
|
|
4393
|
+
new UnencryptedQueueAnalyzer(),
|
|
4394
|
+
new LargeQueueBacklogAnalyzer(),
|
|
4395
|
+
new MissingSecretRotationAnalyzer(),
|
|
4396
|
+
new MissingLogRetentionAnalyzer(),
|
|
4397
|
+
new LambdaDefaultMemoryAnalyzer(),
|
|
4398
|
+
new LambdaHighTimeoutAnalyzer(),
|
|
3144
4399
|
...iacDriftAnalyzer ? [iacDriftAnalyzer] : []
|
|
3145
4400
|
];
|
|
3146
4401
|
const result = await (0, import_analyzers.runAllAnalyzers)(graph, analyzers);
|
|
@@ -3172,13 +4427,12 @@ async function runAnalyze(options = {}) {
|
|
|
3172
4427
|
var import_chalk5 = __toESM(require("chalk"));
|
|
3173
4428
|
var import_ora3 = __toESM(require("ora"));
|
|
3174
4429
|
var import_core3 = __toESM(require_dist());
|
|
3175
|
-
var import_server = __toESM(
|
|
4430
|
+
var import_server = __toESM(require_dist12());
|
|
3176
4431
|
async function runDev(options = {}) {
|
|
3177
4432
|
const port = options.port ?? 3e3;
|
|
3178
4433
|
printHeader("MCP Server");
|
|
3179
|
-
let config;
|
|
3180
4434
|
try {
|
|
3181
|
-
|
|
4435
|
+
(0, import_core3.loadConfig)(options.config);
|
|
3182
4436
|
log.success("Config loaded", options.config ?? "infrawise.yaml");
|
|
3183
4437
|
} catch (err) {
|
|
3184
4438
|
console.error((0, import_core3.formatError)(err));
|
|
@@ -3235,6 +4489,7 @@ async function runDev(options = {}) {
|
|
|
3235
4489
|
// src/commands/doctor.ts
|
|
3236
4490
|
var fs3 = __toESM(require("fs"));
|
|
3237
4491
|
var path4 = __toESM(require("path"));
|
|
4492
|
+
var os2 = __toESM(require("os"));
|
|
3238
4493
|
var import_chalk6 = __toESM(require("chalk"));
|
|
3239
4494
|
var import_ora4 = __toESM(require("ora"));
|
|
3240
4495
|
var import_core4 = __toESM(require_dist());
|
|
@@ -3242,7 +4497,8 @@ var import_adapters_dynamodb3 = __toESM(require_dist2());
|
|
|
3242
4497
|
var import_adapters_postgres2 = __toESM(require_dist3());
|
|
3243
4498
|
var import_adapters_mysql2 = __toESM(require_dist4());
|
|
3244
4499
|
var import_adapters_mongodb2 = __toESM(require_dist5());
|
|
3245
|
-
var
|
|
4500
|
+
var import_adapters_aws_services2 = __toESM(require_dist7());
|
|
4501
|
+
var import_adapters_logs2 = __toESM(require_dist8());
|
|
3246
4502
|
async function runCheck(label, fn) {
|
|
3247
4503
|
const spin = (0, import_ora4.default)({ text: import_chalk6.default.dim(label), color: "cyan" }).start();
|
|
3248
4504
|
const result = await fn();
|
|
@@ -3260,9 +4516,7 @@ async function runCheck(label, fn) {
|
|
|
3260
4516
|
spin.info(import_chalk6.default.dim(`${result.name} ${result.message}`));
|
|
3261
4517
|
break;
|
|
3262
4518
|
}
|
|
3263
|
-
if (result.detail) {
|
|
3264
|
-
console.log(import_chalk6.default.dim(` ${result.detail}`));
|
|
3265
|
-
}
|
|
4519
|
+
if (result.detail) console.log(import_chalk6.default.dim(` ${result.detail}`));
|
|
3266
4520
|
return result;
|
|
3267
4521
|
}
|
|
3268
4522
|
async function runDoctor(options = {}) {
|
|
@@ -3280,9 +4534,7 @@ async function runDoctor(options = {}) {
|
|
|
3280
4534
|
}));
|
|
3281
4535
|
let config;
|
|
3282
4536
|
results.push(await runCheck("Validating config...", async () => {
|
|
3283
|
-
if (!fs3.existsSync(configPath)) {
|
|
3284
|
-
return { name: "Config validation", status: "skip", message: "No config file" };
|
|
3285
|
-
}
|
|
4537
|
+
if (!fs3.existsSync(configPath)) return { name: "Config validation", status: "skip", message: "No config file" };
|
|
3286
4538
|
try {
|
|
3287
4539
|
config = (0, import_core4.loadConfig)(options.config);
|
|
3288
4540
|
return { name: "Config validation", status: "pass", message: `project: ${config.project}` };
|
|
@@ -3305,21 +4557,102 @@ async function runDoctor(options = {}) {
|
|
|
3305
4557
|
detail: hasCreds ? void 0 : "Run: aws configure"
|
|
3306
4558
|
};
|
|
3307
4559
|
}));
|
|
4560
|
+
const awsCfg = { region: config?.aws?.region, profile: config?.aws?.profile };
|
|
3308
4561
|
results.push(await runCheck("Testing DynamoDB access...", async () => {
|
|
3309
|
-
if (!config) return { name: "DynamoDB
|
|
4562
|
+
if (!config) return { name: "DynamoDB", status: "skip", message: "No valid config" };
|
|
3310
4563
|
try {
|
|
3311
4564
|
const ok = await (0, import_adapters_dynamodb3.validateDynamoAccess)(config);
|
|
3312
4565
|
return {
|
|
3313
|
-
name: "DynamoDB
|
|
4566
|
+
name: "DynamoDB",
|
|
3314
4567
|
status: ok ? "pass" : "fail",
|
|
3315
4568
|
message: ok ? `Connected (profile: ${config.aws?.profile ?? "default"})` : "Cannot connect",
|
|
3316
4569
|
detail: ok ? void 0 : "Check IAM: dynamodb:ListTables, dynamodb:DescribeTable"
|
|
3317
4570
|
};
|
|
4571
|
+
} catch (err) {
|
|
4572
|
+
return { name: "DynamoDB", status: "fail", message: err instanceof Error ? err.message : String(err) };
|
|
4573
|
+
}
|
|
4574
|
+
}));
|
|
4575
|
+
results.push(await runCheck("Testing SQS access...", async () => {
|
|
4576
|
+
if (config?.sqs?.enabled === false) return { name: "SQS", status: "skip", message: "Disabled in config" };
|
|
4577
|
+
try {
|
|
4578
|
+
await (0, import_adapters_aws_services2.validateSQSAccess)(awsCfg);
|
|
4579
|
+
return { name: "SQS", status: "pass", message: "Connected" };
|
|
3318
4580
|
} catch (err) {
|
|
3319
4581
|
return {
|
|
3320
|
-
name: "
|
|
3321
|
-
status: "
|
|
3322
|
-
message: err instanceof Error ? err.message : String(err)
|
|
4582
|
+
name: "SQS",
|
|
4583
|
+
status: "warn",
|
|
4584
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4585
|
+
detail: "Check IAM: sqs:ListQueues, sqs:GetQueueAttributes"
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
}));
|
|
4589
|
+
results.push(await runCheck("Testing SNS access...", async () => {
|
|
4590
|
+
if (config?.sns?.enabled === false) return { name: "SNS", status: "skip", message: "Disabled in config" };
|
|
4591
|
+
try {
|
|
4592
|
+
await (0, import_adapters_aws_services2.validateSNSAccess)(awsCfg);
|
|
4593
|
+
return { name: "SNS", status: "pass", message: "Connected" };
|
|
4594
|
+
} catch (err) {
|
|
4595
|
+
return {
|
|
4596
|
+
name: "SNS",
|
|
4597
|
+
status: "warn",
|
|
4598
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4599
|
+
detail: "Check IAM: sns:ListTopics, sns:GetTopicAttributes, sns:ListSubscriptionsByTopic"
|
|
4600
|
+
};
|
|
4601
|
+
}
|
|
4602
|
+
}));
|
|
4603
|
+
results.push(await runCheck("Testing SSM Parameter Store access...", async () => {
|
|
4604
|
+
if (config?.ssm?.enabled === false) return { name: "SSM", status: "skip", message: "Disabled in config" };
|
|
4605
|
+
try {
|
|
4606
|
+
await (0, import_adapters_aws_services2.validateSSMAccess)(awsCfg);
|
|
4607
|
+
return { name: "SSM", status: "pass", message: "Connected (metadata only)" };
|
|
4608
|
+
} catch (err) {
|
|
4609
|
+
return {
|
|
4610
|
+
name: "SSM",
|
|
4611
|
+
status: "warn",
|
|
4612
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4613
|
+
detail: "Check IAM: ssm:DescribeParameters"
|
|
4614
|
+
};
|
|
4615
|
+
}
|
|
4616
|
+
}));
|
|
4617
|
+
results.push(await runCheck("Testing Secrets Manager access...", async () => {
|
|
4618
|
+
if (config?.secretsManager?.enabled === false) return { name: "Secrets Manager", status: "skip", message: "Disabled in config" };
|
|
4619
|
+
try {
|
|
4620
|
+
await (0, import_adapters_aws_services2.validateSecretsAccess)(awsCfg);
|
|
4621
|
+
return { name: "Secrets Manager", status: "pass", message: "Connected (names/rotation only)" };
|
|
4622
|
+
} catch (err) {
|
|
4623
|
+
return {
|
|
4624
|
+
name: "Secrets Manager",
|
|
4625
|
+
status: "warn",
|
|
4626
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4627
|
+
detail: "Check IAM: secretsmanager:ListSecrets"
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
}));
|
|
4631
|
+
results.push(await runCheck("Testing Lambda access...", async () => {
|
|
4632
|
+
if (config?.lambda?.enabled === false) return { name: "Lambda", status: "skip", message: "Disabled in config" };
|
|
4633
|
+
try {
|
|
4634
|
+
await (0, import_adapters_aws_services2.validateLambdaAccess)(awsCfg);
|
|
4635
|
+
return { name: "Lambda", status: "pass", message: "Connected" };
|
|
4636
|
+
} catch (err) {
|
|
4637
|
+
return {
|
|
4638
|
+
name: "Lambda",
|
|
4639
|
+
status: "warn",
|
|
4640
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4641
|
+
detail: "Check IAM: lambda:ListFunctions"
|
|
4642
|
+
};
|
|
4643
|
+
}
|
|
4644
|
+
}));
|
|
4645
|
+
results.push(await runCheck("Testing CloudWatch Logs access...", async () => {
|
|
4646
|
+
if (!config?.cloudwatchLogs?.enabled) return { name: "CloudWatch Logs", status: "skip", message: "Not enabled in config" };
|
|
4647
|
+
try {
|
|
4648
|
+
await (0, import_adapters_logs2.validateLogsAccess)(awsCfg);
|
|
4649
|
+
return { name: "CloudWatch Logs", status: "pass", message: "Connected" };
|
|
4650
|
+
} catch (err) {
|
|
4651
|
+
return {
|
|
4652
|
+
name: "CloudWatch Logs",
|
|
4653
|
+
status: "warn",
|
|
4654
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4655
|
+
detail: "Check IAM: logs:DescribeLogGroups, logs:FilterLogEvents"
|
|
3323
4656
|
};
|
|
3324
4657
|
}
|
|
3325
4658
|
}));
|
|
@@ -3333,14 +4666,10 @@ async function runDoctor(options = {}) {
|
|
|
3333
4666
|
name: "PostgreSQL",
|
|
3334
4667
|
status: ok ? "pass" : "fail",
|
|
3335
4668
|
message: ok ? "Connected" : "Cannot connect",
|
|
3336
|
-
detail: ok ? void 0 : "Check connection string
|
|
4669
|
+
detail: ok ? void 0 : "Check connection string and security group"
|
|
3337
4670
|
};
|
|
3338
4671
|
} catch (err) {
|
|
3339
|
-
return {
|
|
3340
|
-
name: "PostgreSQL",
|
|
3341
|
-
status: "fail",
|
|
3342
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3343
|
-
};
|
|
4672
|
+
return { name: "PostgreSQL", status: "fail", message: err instanceof Error ? err.message : String(err) };
|
|
3344
4673
|
}
|
|
3345
4674
|
}));
|
|
3346
4675
|
results.push(await runCheck("Testing MySQL...", async () => {
|
|
@@ -3353,14 +4682,10 @@ async function runDoctor(options = {}) {
|
|
|
3353
4682
|
name: "MySQL",
|
|
3354
4683
|
status: ok ? "pass" : "fail",
|
|
3355
4684
|
message: ok ? "Connected" : "Cannot connect",
|
|
3356
|
-
detail: ok ? void 0 : "Check connection string, host, port 3306
|
|
4685
|
+
detail: ok ? void 0 : "Check connection string, host, port 3306"
|
|
3357
4686
|
};
|
|
3358
4687
|
} catch (err) {
|
|
3359
|
-
return {
|
|
3360
|
-
name: "MySQL",
|
|
3361
|
-
status: "fail",
|
|
3362
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3363
|
-
};
|
|
4688
|
+
return { name: "MySQL", status: "fail", message: err instanceof Error ? err.message : String(err) };
|
|
3364
4689
|
}
|
|
3365
4690
|
}));
|
|
3366
4691
|
results.push(await runCheck("Testing MongoDB...", async () => {
|
|
@@ -3373,14 +4698,10 @@ async function runDoctor(options = {}) {
|
|
|
3373
4698
|
name: "MongoDB",
|
|
3374
4699
|
status: ok ? "pass" : "fail",
|
|
3375
4700
|
message: ok ? "Connected" : "Cannot connect",
|
|
3376
|
-
detail: ok ? void 0 : "Check connection string
|
|
4701
|
+
detail: ok ? void 0 : "Check connection string and port 27017"
|
|
3377
4702
|
};
|
|
3378
4703
|
} catch (err) {
|
|
3379
|
-
return {
|
|
3380
|
-
name: "MongoDB",
|
|
3381
|
-
status: "fail",
|
|
3382
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3383
|
-
};
|
|
4704
|
+
return { name: "MongoDB", status: "fail", message: err instanceof Error ? err.message : String(err) };
|
|
3384
4705
|
}
|
|
3385
4706
|
}));
|
|
3386
4707
|
results.push(await runCheck("Detecting project type...", async () => {
|
|
@@ -3389,10 +4710,27 @@ async function runDoctor(options = {}) {
|
|
|
3389
4710
|
return {
|
|
3390
4711
|
name: "Project type",
|
|
3391
4712
|
status: hasTsConfig ? "pass" : "warn",
|
|
3392
|
-
message: hasTsConfig ? "TypeScript
|
|
4713
|
+
message: hasTsConfig ? "TypeScript" : hasPkg ? "JavaScript (no tsconfig.json)" : "Unknown",
|
|
3393
4714
|
detail: hasTsConfig ? void 0 : "Scanner works best with TypeScript projects"
|
|
3394
4715
|
};
|
|
3395
4716
|
}));
|
|
4717
|
+
results.push(await runCheck("Detecting IaC files...", async () => {
|
|
4718
|
+
const cwd = process.cwd();
|
|
4719
|
+
const hasTF = fs3.existsSync(path4.join(cwd, "main.tf")) || fs3.readdirSync(cwd).some((f) => f.endsWith(".tf"));
|
|
4720
|
+
const hasCFN = fs3.existsSync(path4.join(cwd, "template.yaml")) || fs3.existsSync(path4.join(cwd, "template.json"));
|
|
4721
|
+
const hasCDK = fs3.existsSync(path4.join(cwd, "cdk.json"));
|
|
4722
|
+
const hasCDKOut = fs3.existsSync(path4.join(cwd, "cdk.out"));
|
|
4723
|
+
const found = [];
|
|
4724
|
+
if (hasTF) found.push("Terraform");
|
|
4725
|
+
if (hasCFN) found.push("CloudFormation");
|
|
4726
|
+
if (hasCDK) found.push(`CDK${hasCDKOut ? " (synthesized)" : " (run cdk synth)"}`);
|
|
4727
|
+
return {
|
|
4728
|
+
name: "IaC files",
|
|
4729
|
+
status: found.length > 0 ? "pass" : "warn",
|
|
4730
|
+
message: found.length > 0 ? found.join(", ") : "No Terraform/CFN/CDK files detected",
|
|
4731
|
+
detail: found.length === 0 ? "IaC analysis will be skipped without TF/CFN/CDK files" : void 0
|
|
4732
|
+
};
|
|
4733
|
+
}));
|
|
3396
4734
|
results.push(await runCheck("Checking analysis cache...", async () => {
|
|
3397
4735
|
const cached = fs3.existsSync(path4.join(process.cwd(), ".infrawise", "cache", "graph.json"));
|
|
3398
4736
|
return {
|