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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/dist/index.js +1891 -553
  3. 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
- enabled: zod_1.z.boolean().optional().default(true)
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
- enabled: options?.terraform?.enabled ?? true
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
- analysis: {
176
- sampleSize: options?.analysis?.sampleSize ?? 100
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 findFilesRecursively(dir, extensions) {
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 (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist")
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
- const body = content.slice(startBrace + 1, i - 1);
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 extractTerraformStringAttr(body, attr) {
1032
- const pattern = new RegExp(`${attr}\\s*=\\s*"([^"]*)"`, "i");
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 extractTerraformGSINames(body) {
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 gsiPattern = /global_secondary_index\s*\{([^}]*)\}/g;
1065
+ const pat = /global_secondary_index\s*\{([^}]*)\}/g;
1039
1066
  let m;
1040
- while ((m = gsiPattern.exec(body)) !== null) {
1041
- const gsiBody = m[1];
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 = { dynamoTables: [], rdsInstances: [], mongoClusters: [] };
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 blocks = extractTerraformResourceBlocks(content);
1060
- for (const block of blocks) {
1061
- const { resourceType, resourceName, body } = block;
1062
- if (resourceType === "aws_dynamodb_table") {
1063
- const partitionKey = extractTerraformStringAttr(body, "hash_key");
1064
- const sortKey = extractTerraformStringAttr(body, "range_key");
1065
- const gsiNames = extractTerraformGSINames(body);
1066
- const tableName = extractTerraformStringAttr(body, "name") ?? resourceName;
1067
- schema.dynamoTables.push({
1068
- name: tableName,
1069
- partitionKey,
1070
- sortKey,
1071
- gsiNames,
1072
- source: "terraform",
1073
- filePath
1074
- });
1075
- } else if (resourceType === "aws_db_instance") {
1076
- const identifier = extractTerraformStringAttr(body, "identifier") ?? resourceName;
1077
- const engine = extractTerraformStringAttr(body, "engine") ?? "unknown";
1078
- schema.rdsInstances.push({
1079
- identifier,
1080
- engine,
1081
- source: "terraform",
1082
- filePath
1083
- });
1084
- } else if (resourceType === "aws_docdb_cluster") {
1085
- const identifier = extractTerraformStringAttr(body, "cluster_identifier") ?? resourceName;
1086
- schema.mongoClusters.push({
1087
- identifier,
1088
- source: "terraform",
1089
- filePath
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
- if (filePath.endsWith(".json")) {
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 getStringProp(obj, ...keys) {
1197
+ function cfnStr(props, ...keys) {
1127
1198
  for (const key of keys) {
1128
- if (typeof obj[key] === "string")
1129
- return obj[key];
1199
+ if (typeof props[key] === "string")
1200
+ return props[key];
1130
1201
  }
1131
1202
  return void 0;
1132
1203
  }
1133
- async function extractCloudFormationSchema(repoPath) {
1134
- const schema = { dynamoTables: [], rdsInstances: [], mongoClusters: [] };
1135
- const cfnFiles = findFilesRecursively(repoPath, [".yaml", ".yml", ".json"]);
1136
- core_1.logger.info(`Scanning ${cfnFiles.length} potential CloudFormation file(s)`);
1137
- for (const filePath of cfnFiles) {
1138
- const parsed = parseCFNFile(filePath);
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
- for (const [logicalId, rawResource] of Object.entries(resources)) {
1145
- if (typeof rawResource !== "object" || rawResource === null)
1146
- continue;
1147
- const resource = rawResource;
1148
- const resourceType = resource["Type"];
1149
- const props = resource["Properties"] ?? {};
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 keyDef of keySchema) {
1156
- if (typeof keyDef !== "object" || keyDef === null)
1219
+ for (const kd of keySchema) {
1220
+ if (typeof kd !== "object" || kd === null)
1157
1221
  continue;
1158
- const kd = keyDef;
1159
- if (kd["KeyType"] === "HASH")
1160
- partitionKey = kd["AttributeName"];
1161
- if (kd["KeyType"] === "RANGE")
1162
- sortKey = kd["AttributeName"];
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 gsi of gsis) {
1169
- if (typeof gsi !== "object" || gsi === null)
1170
- continue;
1171
- const g = gsi;
1172
- if (typeof g["IndexName"] === "string")
1173
- gsiNames.push(g["IndexName"]);
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: tableName,
1179
- partitionKey,
1180
- sortKey,
1241
+ name: cfnStr(props, "TableName") ?? logicalId,
1242
+ partitionKey: pk,
1243
+ sortKey: sk,
1181
1244
  gsiNames,
1182
- source: "cloudformation",
1245
+ source,
1183
1246
  filePath
1184
1247
  });
1185
- } else if (resourceType === "AWS::RDS::DBInstance") {
1186
- const identifier = getStringProp(props, "DBInstanceIdentifier") ?? logicalId;
1187
- const engine = getStringProp(props, "Engine") ?? "unknown";
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: "cloudformation",
1260
+ identifier: cfnStr(props, "DBClusterIdentifier") ?? logicalId,
1261
+ engine: cfnStr(props, "Engine") ?? "aurora",
1262
+ source,
1192
1263
  filePath
1193
1264
  });
1194
- } else if (resourceType === "AWS::DocDB::DBCluster") {
1195
- const identifier = getStringProp(props, "DBClusterIdentifier") ?? logicalId;
1265
+ break;
1266
+ case "AWS::DocDB::DBCluster":
1196
1267
  schema.mongoClusters.push({
1197
- identifier,
1198
- source: "cloudformation",
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 dynamoKey = (t) => `${t.source}::${t.name}`;
1212
- const rdsKey = (r) => `${r.source}::${r.identifier}`;
1213
- const mongoKey = (m) => `${m.source}::${m.identifier}`;
1214
- const seenDynamo = /* @__PURE__ */ new Set();
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
- // ../context/dist/index.js
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
- async function scanRepository2(repoPath) {
1607
- const resolvedPath = path5.resolve(repoPath);
1608
- if (!fs4.existsSync(resolvedPath)) {
1609
- throw new core_1.RepositoryScanError(`Path does not exist: ${resolvedPath}`);
1610
- }
1611
- const tsconfigPath = path5.join(resolvedPath, "tsconfig.json");
1612
- const hasTsConfig = fs4.existsSync(tsconfigPath);
1613
- const project = new ts_morph_1.Project({
1614
- tsConfigFilePath: hasTsConfig ? tsconfigPath : void 0,
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
- core_1.logger.info(`Extracted ${operations.length} database operation(s)`);
1660
- return operations;
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 require_dist8 = __commonJS({
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
- if (!nodeIds.has(nodeId)) {
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
- if (!nodeIds.has(indexNodeId)) {
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
- if (!nodeIds.has(nodeId)) {
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
- if (!nodeIds.has(indexNodeId)) {
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
- if (!nodeIds.has(nodeId)) {
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
- if (!nodeIds.has(indexNodeId)) {
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
- if (!nodeIds.has(nodeId)) {
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
- if (!nodeIds.has(indexNodeId)) {
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
- nodes.push({
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 qualifiedTarget = op.target.includes(".") ? op.target : `default.${op.target}`;
1790
- tableNodeId = `table:mysql:${qualifiedTarget}`;
2530
+ const q = op.target.includes(".") ? op.target : `default.${op.target}`;
2531
+ tableNodeId = `table:mysql:${q}`;
1791
2532
  if (!nodeIds.has(tableNodeId)) {
1792
- nodes.push({
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 qualifiedTarget = op.target.includes(".") ? op.target : `default.${op.target}`;
1802
- tableNodeId = `table:mongodb:${qualifiedTarget}`;
2536
+ const q = op.target.includes(".") ? op.target : `default.${op.target}`;
2537
+ tableNodeId = `table:mongodb:${q}`;
1803
2538
  if (!nodeIds.has(tableNodeId)) {
1804
- nodes.push({
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 qualifiedTarget = op.target.includes(".") ? op.target : `public.${op.target}`;
1814
- tableNodeId = `table:postgres:${qualifiedTarget}`;
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 = qualifiedTarget.split(".");
1817
- nodes.push({
1818
- id: tableNodeId,
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 = require_dist8();
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 = require_dist8();
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 = require_dist8();
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
- const deployedDynamoTables = new Set(graph.nodes.filter((n) => n.type === "table" && n.databaseType === "dynamodb").map((n) => n.name));
2307
- const iacDynamoTables = /* @__PURE__ */ new Map();
2308
- for (const t of this.iacSchema.dynamoTables) {
2309
- iacDynamoTables.set(t.name, t.filePath);
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
- for (const [tableName, filePath] of iacDynamoTables) {
2312
- if (!deployedDynamoTables.has(tableName)) {
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: DynamoDB table "${tableName}" is defined in IaC but not found in AWS`,
2316
- description: `Table "${tableName}" is defined in ${filePath} but was not found among deployed AWS DynamoDB tables. It may not have been deployed yet, or may have been deleted from AWS without updating the IaC.`,
2317
- recommendation: "Run `terraform apply` or deploy your CloudFormation stack to create the missing table, or remove the definition from your IaC if the table is intentionally absent.",
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 tableName of deployedDynamoTables) {
2327
- if (!iacDynamoTables.has(tableName)) {
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: DynamoDB table "${tableName}" is deployed in AWS but not defined in IaC`,
2331
- description: `Table "${tableName}" exists in AWS DynamoDB but has no corresponding definition in Terraform or CloudFormation. This resource may have been created manually and is not tracked by IaC.`,
2332
- recommendation: "Import the table into your IaC using `terraform import` or add a CloudFormation resource definition. Untracked resources can lead to configuration drift and accidental deletion.",
2333
- metadata: {
2334
- tableName,
2335
- driftType: "deployed_not_defined"
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 require_dist9 = __commonJS({
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
- new terraform_1.IaCDriftAnalyzer()
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 require_dist10 = __commonJS({
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 = require_dist8();
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
- return {
2470
- status: "ok",
2471
- version: "0.1.0",
2472
- graphNodes: currentGraph.nodes.length,
2473
- graphEdges: currentGraph.edges.length,
2474
- findings: currentFindings.length
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 in request body' };
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
- return {
2498
- tools: [
2499
- {
2500
- name: "get_graph_summary",
2501
- description: "Returns the full infrastructure graph with all findings",
2502
- input: {}
2503
- },
2504
- {
2505
- name: "analyze_function",
2506
- description: "Analyze a specific function for database issues",
2507
- input: { function: "string \u2014 function name to analyze" }
2508
- },
2509
- {
2510
- name: "suggest_gsi",
2511
- description: "Get GSI suggestions for a DynamoDB table and attribute",
2512
- input: {
2513
- table: "string \u2014 DynamoDB table name",
2514
- attribute: "string \u2014 attribute to index"
2515
- }
2516
- },
2517
- {
2518
- name: "postgres_index_suggestions",
2519
- description: "Get PostgreSQL index suggestions for a table column",
2520
- input: {
2521
- table: "string \u2014 PostgreSQL table name",
2522
- column: "string \u2014 column to analyze"
2523
- }
2524
- },
2525
- {
2526
- name: "suggest_mongo_index",
2527
- description: "Get index suggestions for a MongoDB collection field",
2528
- input: {
2529
- collection: "string \u2014 MongoDB collection name",
2530
- field: "string \u2014 field to index"
2531
- }
2532
- },
2533
- {
2534
- name: "mysql_index_suggestions",
2535
- description: "Get MySQL index suggestions for a table column",
2536
- input: {
2537
- table: "string \u2014 MySQL table name",
2538
- column: "string \u2014 column to analyze"
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?.toString().includes(functionName);
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
- accessedTables: outEdges.map((e) => {
3650
+ accesses: outEdges.map((e) => {
2597
3651
  const target = currentGraph.nodes.find((n) => n.id === e.to);
2598
- return { tableId: e.to, edgeType: e.type, tableName: target && "name" in target ? target.name : e.to };
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
- recommendations: uniqueRecs
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
- index: {
2625
- name: indexName,
2626
- partitionKey: attribute,
2627
- projectionType: "ALL",
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 column "${column}" of table "${tableName}" will eliminate sequential scans when filtering on this column.`,
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 during index creation",
2658
- "Consider a partial index if the column has high cardinality and you commonly filter on specific values",
2659
- "Run ANALYZE after creating the index to update statistics",
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 field "${field}" of collection "${collection}" will eliminate full collection scans when filtering on this field.`,
3714
+ rationale: `An index on "${field}" eliminates full collection scans when filtering on this field.`,
2676
3715
  notes: [
2677
- "Use { background: true } in MongoDB < 4.2 to avoid blocking reads during index creation",
2678
- `For compound queries, consider a compound index: db.${collection}.createIndex({ ${field}: 1, otherField: 1 })`,
2679
- `For text search, use a text index: db.${collection}.createIndex({ ${sanitizedField}: "text" })`,
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 column "${column}" of table "${tableName}" will eliminate full table scans when filtering on this column.`,
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 table lock for InnoDB with MySQL 5.6+)",
2702
- `Consider a composite index if you filter on multiple columns together`,
2703
- `Use EXPLAIN SELECT ... to verify the index is used after adding it`,
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}". Available tools: get_graph_summary, analyze_function, suggest_gsi, postgres_index_suggestions, suggest_mongo_index, mysql_index_suggestions`);
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 answers = await import_inquirer.default.prompt([
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, leave blank for all)")
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
- const includeTables = answers.dynamoTables ? answers.dynamoTables.split(",").map((t) => t.trim()).filter(Boolean) : [];
2940
- const configContent = (0, import_core.generateDefaultConfig)(answers.project, {
2941
- aws: { profile: answers.awsProfile, region: answers.region },
2942
- dynamodb: { includeTables },
2943
- postgres: {
2944
- enabled: answers.pgEnabled,
2945
- connectionString: answers.pgConnectionString ?? ""
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
- mysql: {
2948
- enabled: answers.mysqlEnabled,
2949
- connectionString: answers.mysqlConnectionString ?? ""
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
- mongodb: {
2952
- enabled: answers.mongoEnabled,
2953
- connectionString: answers.mongoConnectionString ?? ""
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 import_context = __toESM(require_dist7());
3030
- var import_graph = __toESM(require_dist8());
3031
- var import_analyzers = __toESM(require_dist9());
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 table metadata..."), color: "cyan" }).start();
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) found`));
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) found`));
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) found`));
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) found`));
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 totalIaC = iacSchema.dynamoTables.length + iacSchema.rdsInstances.length + iacSchema.mongoClusters.length;
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(` ${totalIaC} resource(s) found`));
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} database operation(s) found`));
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
- var graph = (0, import_graph.buildGraph)(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta);
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
- } = await Promise.resolve().then(() => __toESM(require_dist9()));
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(require_dist10());
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
- config = (0, import_core3.loadConfig)(options.config);
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 os2 = __toESM(require("os"));
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 access", status: "skip", message: "No valid config" };
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 access",
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: "DynamoDB access",
3321
- status: "fail",
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, security group, and credentials"
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, and credentials"
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, host, port 27017, and credentials"
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 (tsconfig.json found)" : hasPkg ? "JavaScript (no tsconfig.json)" : "Unknown project",
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 {