aws-security-mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -115,7 +115,7 @@ import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync3,
115
115
  import { join as join3, extname as extname2, relative } from "path";
116
116
  import { fileURLToPath as fileURLToPath2 } from "url";
117
117
  import {
118
- S3Client as S3Client2,
118
+ S3Client as S3Client8,
119
119
  PutObjectCommand,
120
120
  PutBucketWebsiteCommand,
121
121
  PutBucketPolicyCommand
@@ -154,7 +154,7 @@ Expected: ${dashboardDir}`
154
154
  "No scan data at ~/.aws-security/dashboard/data.json \u2014 deploying with bundled sample data"
155
155
  );
156
156
  }
157
- const s3 = new S3Client2({ region });
157
+ const s3 = new S3Client8({ region });
158
158
  console.log(`Configuring s3://${bucket} for static website hosting...`);
159
159
  await s3.send(
160
160
  new PutBucketWebsiteCommand({
@@ -15911,116 +15911,3294 @@ var ServiceDetectionScanner = class {
15911
15911
  }
15912
15912
  };
15913
15913
 
15914
- // src/tools/report-tool.ts
15915
- var SEVERITY_ICON = {
15916
- CRITICAL: "\u{1F534}",
15917
- HIGH: "\u{1F7E0}",
15918
- MEDIUM: "\u{1F7E1}",
15919
- LOW: "\u{1F7E2}"
15920
- };
15921
- var SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
15922
- function formatDuration(start, end) {
15923
- const ms = new Date(end).getTime() - new Date(start).getTime();
15924
- if (ms < 1e3) return `${ms}ms`;
15925
- const secs = Math.round(ms / 1e3);
15926
- if (secs < 60) return `${secs}s`;
15927
- const mins = Math.floor(secs / 60);
15928
- const remainSecs = secs % 60;
15929
- return `${mins}m ${remainSecs}s`;
15930
- }
15931
- function renderFinding(f) {
15932
- const steps = f.remediationSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
15933
- return [
15934
- `#### ${f.title}`,
15935
- `- **Resource:** ${f.resourceId} (\`${f.resourceArn}\`)`,
15936
- `- **Description:** ${f.description}`,
15937
- `- **Impact:** ${f.impact}`,
15938
- `- **Risk Score:** ${f.riskScore}/10`,
15939
- `- **Remediation:**`,
15940
- steps,
15941
- `- **Priority:** ${f.priority}`
15942
- ].join("\n");
15914
+ // src/scanners/iam-password-policy.ts
15915
+ import {
15916
+ IAMClient as IAMClient2,
15917
+ GetAccountPasswordPolicyCommand
15918
+ } from "@aws-sdk/client-iam";
15919
+ function makeFinding9(opts) {
15920
+ const severity = severityFromScore(opts.riskScore);
15921
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
15943
15922
  }
15944
- function generateMarkdownReport(scanResults) {
15945
- const { summary, modules, accountId, region, scanStart, scanEnd } = scanResults;
15946
- const date5 = scanStart.split("T")[0];
15947
- const duration3 = formatDuration(scanStart, scanEnd);
15948
- const lines = [];
15949
- lines.push(`# AWS Security Scan Report \u2014 ${date5}`);
15950
- lines.push("");
15951
- lines.push("## Executive Summary");
15952
- lines.push(`- **Account:** ${accountId}`);
15953
- lines.push(`- **Region:** ${region}`);
15954
- lines.push(`- **Scan Duration:** ${duration3}`);
15955
- lines.push(
15956
- `- **Total Findings:** ${summary.totalFindings} (\u{1F534} ${summary.critical} Critical | \u{1F7E0} ${summary.high} High | \u{1F7E1} ${summary.medium} Medium | \u{1F7E2} ${summary.low} Low)`
15957
- );
15958
- lines.push("");
15959
- if (summary.totalFindings === 0) {
15960
- lines.push("## Findings by Severity");
15961
- lines.push("");
15962
- lines.push("\u2705 No security issues found.");
15963
- lines.push("");
15964
- } else {
15965
- const allFindings = modules.flatMap((m) => m.findings);
15966
- const grouped = /* @__PURE__ */ new Map();
15967
- for (const sev of SEVERITY_ORDER) {
15968
- grouped.set(sev, []);
15969
- }
15970
- for (const f of allFindings) {
15971
- grouped.get(f.severity).push(f);
15972
- }
15973
- lines.push("## Findings by Severity");
15974
- lines.push("");
15975
- for (const sev of SEVERITY_ORDER) {
15976
- const findings = grouped.get(sev);
15977
- const icon = SEVERITY_ICON[sev];
15978
- lines.push(`### ${icon} ${sev.charAt(0)}${sev.slice(1).toLowerCase()}`);
15979
- lines.push("");
15980
- if (findings.length === 0) {
15981
- lines.push(`No ${sev.toLowerCase()} findings.`);
15982
- lines.push("");
15983
- continue;
15923
+ var IamPasswordPolicyScanner = class {
15924
+ moduleName = "iam_password_policy";
15925
+ async scan(ctx) {
15926
+ const { region, partition, accountId } = ctx;
15927
+ const startMs = Date.now();
15928
+ const findings = [];
15929
+ const warnings = [];
15930
+ try {
15931
+ const iamRegion = getIamRegion(region);
15932
+ const client = createClient(IAMClient2, iamRegion);
15933
+ const policyArn = `arn:${partition}:iam::${accountId}:account-password-policy`;
15934
+ let policy;
15935
+ try {
15936
+ const resp = await client.send(new GetAccountPasswordPolicyCommand({}));
15937
+ policy = resp.PasswordPolicy;
15938
+ } catch (e) {
15939
+ if (e instanceof Error && e.name === "NoSuchEntityException") {
15940
+ findings.push(
15941
+ makeFinding9({
15942
+ riskScore: 7.5,
15943
+ title: "No IAM password policy configured",
15944
+ resourceType: "AWS::IAM::AccountPasswordPolicy",
15945
+ resourceId: "account-password-policy",
15946
+ resourceArn: policyArn,
15947
+ region: iamRegion,
15948
+ description: "The AWS account does not have a custom password policy. The default policy has weak requirements.",
15949
+ impact: "Users can set weak passwords that are easily compromised via brute-force or credential stuffing attacks.",
15950
+ remediationSteps: [
15951
+ "Create an IAM password policy with minimum length >= 8.",
15952
+ "Require uppercase, lowercase, numbers, and symbols.",
15953
+ "Set maximum password age to 90 days or less.",
15954
+ "Set password reuse prevention to at least 5 previous passwords."
15955
+ ]
15956
+ })
15957
+ );
15958
+ return {
15959
+ module: this.moduleName,
15960
+ status: "success",
15961
+ warnings: warnings.length > 0 ? warnings : void 0,
15962
+ resourcesScanned: 1,
15963
+ findingsCount: findings.length,
15964
+ scanTimeMs: Date.now() - startMs,
15965
+ findings
15966
+ };
15967
+ }
15968
+ throw e;
15984
15969
  }
15985
- findings.sort((a, b) => b.riskScore - a.riskScore);
15986
- for (const f of findings) {
15987
- lines.push(renderFinding(f));
15988
- lines.push("");
15970
+ if (!policy) {
15971
+ warnings.push("Password policy response was empty.");
15972
+ return {
15973
+ module: this.moduleName,
15974
+ status: "success",
15975
+ warnings,
15976
+ resourcesScanned: 0,
15977
+ findingsCount: 0,
15978
+ scanTimeMs: Date.now() - startMs,
15979
+ findings: []
15980
+ };
15981
+ }
15982
+ if ((policy.MinimumPasswordLength ?? 0) < 8) {
15983
+ findings.push(
15984
+ makeFinding9({
15985
+ riskScore: 7,
15986
+ title: "IAM password policy minimum length is too short",
15987
+ resourceType: "AWS::IAM::AccountPasswordPolicy",
15988
+ resourceId: "account-password-policy",
15989
+ resourceArn: policyArn,
15990
+ region: iamRegion,
15991
+ description: `Minimum password length is ${policy.MinimumPasswordLength ?? 0}, which is below the recommended minimum of 8 characters.`,
15992
+ impact: "Short passwords are more vulnerable to brute-force attacks.",
15993
+ remediationSteps: [
15994
+ "Update the password policy to require at least 8 characters.",
15995
+ "Consider requiring 14+ characters for stronger security."
15996
+ ]
15997
+ })
15998
+ );
15999
+ }
16000
+ const complexityChecks = [
16001
+ { field: policy.RequireUppercaseCharacters, label: "uppercase characters" },
16002
+ { field: policy.RequireLowercaseCharacters, label: "lowercase characters" },
16003
+ { field: policy.RequireNumbers, label: "numbers" },
16004
+ { field: policy.RequireSymbols, label: "symbols" }
16005
+ ];
16006
+ const missing = complexityChecks.filter((c) => !c.field).map((c) => c.label);
16007
+ if (missing.length > 0) {
16008
+ findings.push(
16009
+ makeFinding9({
16010
+ riskScore: 6,
16011
+ title: "IAM password policy missing complexity requirements",
16012
+ resourceType: "AWS::IAM::AccountPasswordPolicy",
16013
+ resourceId: "account-password-policy",
16014
+ resourceArn: policyArn,
16015
+ region: iamRegion,
16016
+ description: `Password policy does not require: ${missing.join(", ")}.`,
16017
+ impact: "Passwords without complexity requirements are easier to guess or crack.",
16018
+ remediationSteps: [
16019
+ "Update the password policy to require uppercase, lowercase, numbers, and symbols."
16020
+ ]
16021
+ })
16022
+ );
16023
+ }
16024
+ if (!policy.MaxPasswordAge || policy.MaxPasswordAge === 0) {
16025
+ findings.push(
16026
+ makeFinding9({
16027
+ riskScore: 5.5,
16028
+ title: "IAM password policy has no password expiry",
16029
+ resourceType: "AWS::IAM::AccountPasswordPolicy",
16030
+ resourceId: "account-password-policy",
16031
+ resourceArn: policyArn,
16032
+ region: iamRegion,
16033
+ description: "No maximum password age is set. Passwords never expire.",
16034
+ impact: "Compromised passwords remain valid indefinitely, increasing the window for unauthorized access.",
16035
+ remediationSteps: [
16036
+ "Set MaxPasswordAge to 90 days or less.",
16037
+ "Combine with MFA for defense in depth."
16038
+ ]
16039
+ })
16040
+ );
16041
+ }
16042
+ if (!policy.PasswordReusePrevention || policy.PasswordReusePrevention === 0) {
16043
+ findings.push(
16044
+ makeFinding9({
16045
+ riskScore: 5,
16046
+ title: "IAM password policy has no reuse prevention",
16047
+ resourceType: "AWS::IAM::AccountPasswordPolicy",
16048
+ resourceId: "account-password-policy",
16049
+ resourceArn: policyArn,
16050
+ region: iamRegion,
16051
+ description: "No password reuse prevention is configured. Users can reuse previous passwords.",
16052
+ impact: "Users may cycle back to previously compromised passwords.",
16053
+ remediationSteps: [
16054
+ "Set PasswordReusePrevention to at least 5.",
16055
+ "This prevents reuse of the last 5 passwords."
16056
+ ]
16057
+ })
16058
+ );
15989
16059
  }
16060
+ return {
16061
+ module: this.moduleName,
16062
+ status: "success",
16063
+ warnings: warnings.length > 0 ? warnings : void 0,
16064
+ resourcesScanned: 1,
16065
+ findingsCount: findings.length,
16066
+ scanTimeMs: Date.now() - startMs,
16067
+ findings
16068
+ };
16069
+ } catch (err) {
16070
+ return {
16071
+ module: this.moduleName,
16072
+ status: "error",
16073
+ error: err instanceof Error ? err.message : String(err),
16074
+ warnings: warnings.length > 0 ? warnings : void 0,
16075
+ resourcesScanned: 0,
16076
+ findingsCount: 0,
16077
+ scanTimeMs: Date.now() - startMs,
16078
+ findings: []
16079
+ };
15990
16080
  }
15991
16081
  }
15992
- lines.push("## Scan Statistics");
15993
- lines.push(
15994
- "| Module | Resources Scanned | Findings | Status |"
15995
- );
15996
- lines.push("|--------|------------------|----------|--------|");
15997
- for (const m of modules) {
15998
- const status = m.status === "success" ? "\u2705" : "\u274C";
15999
- lines.push(
16000
- `| ${m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
16001
- );
16002
- }
16003
- lines.push("");
16004
- if (summary.totalFindings > 0) {
16005
- const allFindings = modules.flatMap((m) => m.findings);
16006
- allFindings.sort((a, b) => b.riskScore - a.riskScore);
16007
- lines.push("## Recommendations (Priority Order)");
16008
- for (let i = 0; i < allFindings.length; i++) {
16009
- const f = allFindings[i];
16010
- lines.push(`${i + 1}. [${f.priority}] ${f.title}: ${f.remediationSteps[0] ?? "Review and remediate."}`);
16082
+ };
16083
+
16084
+ // src/scanners/iam-mfa-audit.ts
16085
+ import {
16086
+ IAMClient as IAMClient3,
16087
+ ListUsersCommand as ListUsersCommand2,
16088
+ ListMFADevicesCommand,
16089
+ GetLoginProfileCommand
16090
+ } from "@aws-sdk/client-iam";
16091
+ function makeFinding10(opts) {
16092
+ const severity = severityFromScore(opts.riskScore);
16093
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
16094
+ }
16095
+ var IamMfaAuditScanner = class {
16096
+ moduleName = "iam_mfa_audit";
16097
+ async scan(ctx) {
16098
+ const { region, partition, accountId } = ctx;
16099
+ const startMs = Date.now();
16100
+ const findings = [];
16101
+ const warnings = [];
16102
+ try {
16103
+ const iamRegion = getIamRegion(region);
16104
+ const client = createClient(IAMClient3, iamRegion);
16105
+ const users = [];
16106
+ let marker;
16107
+ do {
16108
+ const resp = await client.send(new ListUsersCommand2({ Marker: marker }));
16109
+ if (resp.Users) {
16110
+ for (const u of resp.Users) {
16111
+ users.push({
16112
+ UserName: u.UserName ?? "unknown",
16113
+ Arn: u.Arn ?? `arn:${partition}:iam::${accountId}:user/${u.UserName ?? "unknown"}`
16114
+ });
16115
+ }
16116
+ }
16117
+ marker = resp.IsTruncated ? resp.Marker : void 0;
16118
+ } while (marker);
16119
+ if (users.length === 0) {
16120
+ return {
16121
+ module: this.moduleName,
16122
+ status: "success",
16123
+ warnings: warnings.length > 0 ? warnings : void 0,
16124
+ resourcesScanned: 0,
16125
+ findingsCount: 0,
16126
+ scanTimeMs: Date.now() - startMs,
16127
+ findings
16128
+ };
16129
+ }
16130
+ let usersWithConsole = 0;
16131
+ let usersWithMfa = 0;
16132
+ let totalChecked = 0;
16133
+ for (const user of users) {
16134
+ let hasConsole = false;
16135
+ try {
16136
+ await client.send(new GetLoginProfileCommand({ UserName: user.UserName }));
16137
+ hasConsole = true;
16138
+ usersWithConsole++;
16139
+ } catch (e) {
16140
+ if (e instanceof Error && e.name === "NoSuchEntityException") {
16141
+ continue;
16142
+ }
16143
+ warnings.push(`Could not check login profile for ${user.UserName}: ${e instanceof Error ? e.message : String(e)}`);
16144
+ hasConsole = true;
16145
+ usersWithConsole++;
16146
+ }
16147
+ totalChecked++;
16148
+ const mfaResp = await client.send(
16149
+ new ListMFADevicesCommand({ UserName: user.UserName })
16150
+ );
16151
+ const mfaDevices = mfaResp.MFADevices ?? [];
16152
+ if (mfaDevices.length > 0) {
16153
+ usersWithMfa++;
16154
+ } else if (hasConsole) {
16155
+ findings.push(
16156
+ makeFinding10({
16157
+ riskScore: 7.5,
16158
+ title: `IAM user ${user.UserName} has console access without MFA`,
16159
+ resourceType: "AWS::IAM::User",
16160
+ resourceId: user.UserName,
16161
+ resourceArn: user.Arn,
16162
+ region: iamRegion,
16163
+ description: `User "${user.UserName}" has console login enabled but no MFA device configured.`,
16164
+ impact: "Account is vulnerable to credential theft. If the password is compromised, there is no second factor to prevent unauthorized access.",
16165
+ remediationSteps: [
16166
+ `Enable MFA for user ${user.UserName}.`,
16167
+ "Use a virtual MFA device (e.g., Google Authenticator) or a hardware security key.",
16168
+ "Consider enforcing MFA via IAM policy conditions."
16169
+ ]
16170
+ })
16171
+ );
16172
+ }
16173
+ }
16174
+ if (usersWithConsole > 0 && usersWithMfa < usersWithConsole) {
16175
+ const adoptionPercent = Math.round(usersWithMfa / usersWithConsole * 100);
16176
+ findings.push(
16177
+ makeFinding10({
16178
+ riskScore: 6,
16179
+ title: "MFA adoption is not 100% for console users",
16180
+ resourceType: "AWS::IAM::Account",
16181
+ resourceId: "mfa-adoption",
16182
+ resourceArn: `arn:${partition}:iam::${accountId}:root`,
16183
+ region: iamRegion,
16184
+ description: `MFA is enabled for ${usersWithMfa}/${usersWithConsole} console users (${adoptionPercent}% adoption).`,
16185
+ impact: "Users without MFA present a higher risk of account compromise.",
16186
+ remediationSteps: [
16187
+ "Enable MFA for all IAM users with console access.",
16188
+ "Use an SCP or IAM policy to deny actions without MFA.",
16189
+ "Consider using AWS SSO with mandatory MFA for centralized access."
16190
+ ]
16191
+ })
16192
+ );
16193
+ }
16194
+ return {
16195
+ module: this.moduleName,
16196
+ status: "success",
16197
+ warnings: warnings.length > 0 ? warnings : void 0,
16198
+ resourcesScanned: users.length,
16199
+ findingsCount: findings.length,
16200
+ scanTimeMs: Date.now() - startMs,
16201
+ findings
16202
+ };
16203
+ } catch (err) {
16204
+ return {
16205
+ module: this.moduleName,
16206
+ status: "error",
16207
+ error: err instanceof Error ? err.message : String(err),
16208
+ warnings: warnings.length > 0 ? warnings : void 0,
16209
+ resourcesScanned: 0,
16210
+ findingsCount: 0,
16211
+ scanTimeMs: Date.now() - startMs,
16212
+ findings: []
16213
+ };
16011
16214
  }
16012
- lines.push("");
16013
16215
  }
16014
- return lines.join("\n");
16015
- }
16216
+ };
16016
16217
 
16017
- // src/tools/save-results.ts
16018
- import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
16019
- import { join } from "path";
16020
- import { homedir } from "os";
16021
- function calculateScore(summary) {
16022
- const raw = 100 - summary.critical * 15 - summary.high * 5 - summary.medium * 2 - summary.low * 0.5;
16023
- return Math.max(0, Math.min(100, raw));
16218
+ // src/scanners/cloudtrail-protection.ts
16219
+ import {
16220
+ CloudTrailClient as CloudTrailClient3,
16221
+ DescribeTrailsCommand as DescribeTrailsCommand3
16222
+ } from "@aws-sdk/client-cloudtrail";
16223
+ import {
16224
+ S3Client as S3Client2,
16225
+ GetBucketEncryptionCommand as GetBucketEncryptionCommand2,
16226
+ GetBucketVersioningCommand as GetBucketVersioningCommand2,
16227
+ GetPublicAccessBlockCommand as GetPublicAccessBlockCommand2,
16228
+ GetBucketLocationCommand as GetBucketLocationCommand2
16229
+ } from "@aws-sdk/client-s3";
16230
+ function makeFinding11(opts) {
16231
+ const severity = severityFromScore(opts.riskScore);
16232
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
16233
+ }
16234
+ async function getBucketRegion2(client, bucketName, defaultRegion, warnings) {
16235
+ try {
16236
+ const resp = await client.send(
16237
+ new GetBucketLocationCommand2({ Bucket: bucketName })
16238
+ );
16239
+ return String(resp.LocationConstraint ?? "") || "us-east-1";
16240
+ } catch (e) {
16241
+ const msg = e instanceof Error ? e.message : String(e);
16242
+ warnings.push(`Failed to detect region for bucket ${bucketName}, using ${defaultRegion}: ${msg}`);
16243
+ return defaultRegion;
16244
+ }
16245
+ }
16246
+ var CloudTrailProtectionScanner = class {
16247
+ moduleName = "cloudtrail_protection";
16248
+ async scan(ctx) {
16249
+ const { region, partition } = ctx;
16250
+ const startMs = Date.now();
16251
+ const findings = [];
16252
+ const warnings = [];
16253
+ try {
16254
+ const ctClient = createClient(CloudTrailClient3, region);
16255
+ const resp = await ctClient.send(new DescribeTrailsCommand3({}));
16256
+ const trails = resp.trailList ?? [];
16257
+ if (trails.length === 0) {
16258
+ warnings.push("No CloudTrail trails found \u2014 nothing to check for log protection.");
16259
+ return {
16260
+ module: this.moduleName,
16261
+ status: "success",
16262
+ warnings,
16263
+ resourcesScanned: 0,
16264
+ findingsCount: 0,
16265
+ scanTimeMs: Date.now() - startMs,
16266
+ findings
16267
+ };
16268
+ }
16269
+ const checkedBuckets = /* @__PURE__ */ new Set();
16270
+ let resourcesScanned = 0;
16271
+ for (const trail of trails) {
16272
+ const bucketName = trail.S3BucketName;
16273
+ if (!bucketName || checkedBuckets.has(bucketName)) continue;
16274
+ checkedBuckets.add(bucketName);
16275
+ resourcesScanned++;
16276
+ const trailName = trail.Name ?? "unknown";
16277
+ const bucketArn = `arn:${partition}:s3:::${bucketName}`;
16278
+ const defaultS3 = createClient(S3Client2, region);
16279
+ const bucketRegion = await getBucketRegion2(defaultS3, bucketName, region, warnings);
16280
+ const s3Client = bucketRegion === region ? defaultS3 : createClient(S3Client2, bucketRegion);
16281
+ try {
16282
+ await s3Client.send(
16283
+ new GetBucketEncryptionCommand2({ Bucket: bucketName })
16284
+ );
16285
+ } catch (e) {
16286
+ if (e instanceof Error && (e.name === "ServerSideEncryptionConfigurationNotFoundError" || e.name === "NoSuchBucketEncryption")) {
16287
+ findings.push(
16288
+ makeFinding11({
16289
+ riskScore: 7.5,
16290
+ title: `CloudTrail S3 bucket ${bucketName} is not encrypted`,
16291
+ resourceType: "AWS::S3::Bucket",
16292
+ resourceId: bucketName,
16293
+ resourceArn: bucketArn,
16294
+ region,
16295
+ description: `S3 bucket "${bucketName}" used by trail "${trailName}" does not have default encryption enabled.`,
16296
+ impact: "CloudTrail logs stored without encryption can be read by anyone with access to the S3 bucket, exposing sensitive API activity data.",
16297
+ remediationSteps: [
16298
+ "Enable default encryption on the S3 bucket (SSE-S3 or SSE-KMS).",
16299
+ "Consider using a CMK for encryption to enable key rotation and access auditing."
16300
+ ]
16301
+ })
16302
+ );
16303
+ } else {
16304
+ warnings.push(`Could not check encryption for bucket ${bucketName}: ${e instanceof Error ? e.message : String(e)}`);
16305
+ }
16306
+ }
16307
+ try {
16308
+ const versionResp = await s3Client.send(
16309
+ new GetBucketVersioningCommand2({ Bucket: bucketName })
16310
+ );
16311
+ if (versionResp.Status !== "Enabled") {
16312
+ findings.push(
16313
+ makeFinding11({
16314
+ riskScore: 6,
16315
+ title: `CloudTrail S3 bucket ${bucketName} does not have versioning enabled`,
16316
+ resourceType: "AWS::S3::Bucket",
16317
+ resourceId: bucketName,
16318
+ resourceArn: bucketArn,
16319
+ region,
16320
+ description: `S3 bucket "${bucketName}" used by trail "${trailName}" does not have versioning enabled.`,
16321
+ impact: "Without versioning, deleted or overwritten log files cannot be recovered. An attacker could tamper with or destroy audit logs.",
16322
+ remediationSteps: [
16323
+ "Enable versioning on the CloudTrail S3 bucket.",
16324
+ "Consider enabling MFA Delete for additional protection."
16325
+ ]
16326
+ })
16327
+ );
16328
+ }
16329
+ } catch (e) {
16330
+ warnings.push(`Could not check versioning for bucket ${bucketName}: ${e instanceof Error ? e.message : String(e)}`);
16331
+ }
16332
+ try {
16333
+ const bpaResp = await s3Client.send(
16334
+ new GetPublicAccessBlockCommand2({ Bucket: bucketName })
16335
+ );
16336
+ const config2 = bpaResp.PublicAccessBlockConfiguration;
16337
+ if (!config2 || !config2.BlockPublicAcls || !config2.IgnorePublicAcls || !config2.BlockPublicPolicy || !config2.RestrictPublicBuckets) {
16338
+ findings.push(
16339
+ makeFinding11({
16340
+ riskScore: 7.5,
16341
+ title: `CloudTrail S3 bucket ${bucketName} does not have full Block Public Access`,
16342
+ resourceType: "AWS::S3::Bucket",
16343
+ resourceId: bucketName,
16344
+ resourceArn: bucketArn,
16345
+ region,
16346
+ description: `S3 bucket "${bucketName}" used by trail "${trailName}" does not have all Block Public Access settings enabled.`,
16347
+ impact: "CloudTrail logs could be exposed publicly, leaking API activity, IP addresses, and resource details to attackers.",
16348
+ remediationSteps: [
16349
+ "Enable all four Block Public Access settings on the bucket.",
16350
+ "Verify no bucket policy grants public access."
16351
+ ]
16352
+ })
16353
+ );
16354
+ }
16355
+ } catch (e) {
16356
+ if (e instanceof Error && e.name === "NoSuchPublicAccessBlockConfiguration") {
16357
+ findings.push(
16358
+ makeFinding11({
16359
+ riskScore: 9,
16360
+ title: `CloudTrail S3 bucket ${bucketName} has no Block Public Access configuration`,
16361
+ resourceType: "AWS::S3::Bucket",
16362
+ resourceId: bucketName,
16363
+ resourceArn: bucketArn,
16364
+ region,
16365
+ description: `S3 bucket "${bucketName}" used by trail "${trailName}" has no Block Public Access configuration at all.`,
16366
+ impact: "Without any Block Public Access, the bucket is at high risk of accidental or malicious public exposure of CloudTrail audit logs.",
16367
+ remediationSteps: [
16368
+ "Immediately enable Block Public Access on this bucket.",
16369
+ "Review bucket policy and ACLs for any existing public grants."
16370
+ ]
16371
+ })
16372
+ );
16373
+ } else {
16374
+ warnings.push(`Could not check Block Public Access for bucket ${bucketName}: ${e instanceof Error ? e.message : String(e)}`);
16375
+ }
16376
+ }
16377
+ }
16378
+ return {
16379
+ module: this.moduleName,
16380
+ status: "success",
16381
+ warnings: warnings.length > 0 ? warnings : void 0,
16382
+ resourcesScanned,
16383
+ findingsCount: findings.length,
16384
+ scanTimeMs: Date.now() - startMs,
16385
+ findings
16386
+ };
16387
+ } catch (err) {
16388
+ return {
16389
+ module: this.moduleName,
16390
+ status: "error",
16391
+ error: err instanceof Error ? err.message : String(err),
16392
+ warnings: warnings.length > 0 ? warnings : void 0,
16393
+ resourcesScanned: 0,
16394
+ findingsCount: 0,
16395
+ scanTimeMs: Date.now() - startMs,
16396
+ findings: []
16397
+ };
16398
+ }
16399
+ }
16400
+ };
16401
+
16402
+ // src/scanners/elb-https.ts
16403
+ import {
16404
+ ElasticLoadBalancingV2Client,
16405
+ DescribeLoadBalancersCommand,
16406
+ DescribeListenersCommand
16407
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
16408
+ function makeFinding12(opts) {
16409
+ const severity = severityFromScore(opts.riskScore);
16410
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
16411
+ }
16412
+ var ElbHttpsScanner = class {
16413
+ moduleName = "elb_https";
16414
+ async scan(ctx) {
16415
+ const { region } = ctx;
16416
+ const startMs = Date.now();
16417
+ const findings = [];
16418
+ const warnings = [];
16419
+ try {
16420
+ const client = createClient(ElasticLoadBalancingV2Client, region);
16421
+ const loadBalancers = [];
16422
+ let marker;
16423
+ do {
16424
+ const resp = await client.send(
16425
+ new DescribeLoadBalancersCommand({ Marker: marker })
16426
+ );
16427
+ if (resp.LoadBalancers) {
16428
+ loadBalancers.push(...resp.LoadBalancers);
16429
+ }
16430
+ marker = resp.NextMarker;
16431
+ } while (marker);
16432
+ for (const lb of loadBalancers) {
16433
+ const lbName = lb.LoadBalancerName ?? "unknown";
16434
+ const lbArn = lb.LoadBalancerArn ?? "unknown";
16435
+ const lbType = lb.Type ?? "application";
16436
+ let listeners = [];
16437
+ try {
16438
+ let listenerMarker;
16439
+ do {
16440
+ const listenerResp = await client.send(
16441
+ new DescribeListenersCommand({
16442
+ LoadBalancerArn: lbArn,
16443
+ Marker: listenerMarker
16444
+ })
16445
+ );
16446
+ if (listenerResp.Listeners) {
16447
+ listeners.push(...listenerResp.Listeners);
16448
+ }
16449
+ listenerMarker = listenerResp.NextMarker;
16450
+ } while (listenerMarker);
16451
+ } catch (e) {
16452
+ warnings.push(`Could not list listeners for ${lbName}: ${e instanceof Error ? e.message : String(e)}`);
16453
+ continue;
16454
+ }
16455
+ for (const listener of listeners) {
16456
+ const protocol = listener.Protocol ?? "unknown";
16457
+ const port = listener.Port ?? 0;
16458
+ if (lbType === "application") {
16459
+ if (protocol === "HTTP") {
16460
+ const hasRedirect = (listener.DefaultActions ?? []).some(
16461
+ (action) => action.Type === "redirect" && action.RedirectConfig?.Protocol === "HTTPS"
16462
+ );
16463
+ if (!hasRedirect) {
16464
+ findings.push(
16465
+ makeFinding12({
16466
+ riskScore: 7.5,
16467
+ title: `ALB ${lbName} has HTTP listener on port ${port} without HTTPS redirect`,
16468
+ resourceType: "AWS::ElasticLoadBalancingV2::Listener",
16469
+ resourceId: `${lbName}:${port}`,
16470
+ resourceArn: listener.ListenerArn ?? lbArn,
16471
+ region,
16472
+ description: `ALB "${lbName}" has an HTTP listener on port ${port} that does not redirect to HTTPS.`,
16473
+ impact: "Traffic is transmitted in plaintext, exposing sensitive data to interception and man-in-the-middle attacks.",
16474
+ remediationSteps: [
16475
+ "Add a redirect action on the HTTP listener to forward all traffic to HTTPS.",
16476
+ "Alternatively, remove the HTTP listener if HTTPS is already configured."
16477
+ ]
16478
+ })
16479
+ );
16480
+ }
16481
+ }
16482
+ if (protocol === "HTTPS" && listener.Certificates) {
16483
+ for (const cert of listener.Certificates) {
16484
+ if (!cert.CertificateArn) continue;
16485
+ }
16486
+ }
16487
+ } else if (lbType === "network") {
16488
+ if (protocol === "TCP" || protocol === "UDP") {
16489
+ findings.push(
16490
+ makeFinding12({
16491
+ riskScore: 6,
16492
+ title: `NLB ${lbName} has ${protocol} listener on port ${port} without TLS`,
16493
+ resourceType: "AWS::ElasticLoadBalancingV2::Listener",
16494
+ resourceId: `${lbName}:${port}`,
16495
+ resourceArn: listener.ListenerArn ?? lbArn,
16496
+ region,
16497
+ description: `NLB "${lbName}" has a ${protocol} listener on port ${port} without TLS termination.`,
16498
+ impact: "Traffic is not encrypted at the load balancer level. If backend services do not implement their own TLS, data is transmitted in plaintext.",
16499
+ remediationSteps: [
16500
+ "Switch the listener protocol to TLS and attach an ACM certificate.",
16501
+ "If end-to-end encryption is handled by the application, document this as an accepted risk."
16502
+ ]
16503
+ })
16504
+ );
16505
+ }
16506
+ }
16507
+ }
16508
+ }
16509
+ return {
16510
+ module: this.moduleName,
16511
+ status: "success",
16512
+ warnings: warnings.length > 0 ? warnings : void 0,
16513
+ resourcesScanned: loadBalancers.length,
16514
+ findingsCount: findings.length,
16515
+ scanTimeMs: Date.now() - startMs,
16516
+ findings
16517
+ };
16518
+ } catch (err) {
16519
+ return {
16520
+ module: this.moduleName,
16521
+ status: "error",
16522
+ error: err instanceof Error ? err.message : String(err),
16523
+ warnings: warnings.length > 0 ? warnings : void 0,
16524
+ resourcesScanned: 0,
16525
+ findingsCount: 0,
16526
+ scanTimeMs: Date.now() - startMs,
16527
+ findings: []
16528
+ };
16529
+ }
16530
+ }
16531
+ };
16532
+
16533
+ // src/scanners/secret-exposure.ts
16534
+ import {
16535
+ LambdaClient,
16536
+ ListFunctionsCommand
16537
+ } from "@aws-sdk/client-lambda";
16538
+ import {
16539
+ EC2Client as EC2Client4,
16540
+ DescribeInstancesCommand as DescribeInstancesCommand3,
16541
+ DescribeInstanceAttributeCommand
16542
+ } from "@aws-sdk/client-ec2";
16543
+ var SECRET_PATTERNS = [
16544
+ { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/, matchType: "value" },
16545
+ { name: "Private Key", pattern: /-----BEGIN.*PRIVATE KEY-----/, matchType: "value" },
16546
+ { name: "Password in env var", pattern: /^(PASSWORD|PASSWD|DB_PASSWORD|SECRET|API_KEY|APIKEY|TOKEN|AUTH_TOKEN)$/i, matchType: "name" }
16547
+ ];
16548
+ function makeFinding13(opts) {
16549
+ const severity = severityFromScore(opts.riskScore);
16550
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
16551
+ }
16552
+ var SecretExposureScanner = class {
16553
+ moduleName = "secret_exposure";
16554
+ async scan(ctx) {
16555
+ const { region, partition, accountId } = ctx;
16556
+ const startMs = Date.now();
16557
+ const findings = [];
16558
+ const warnings = [];
16559
+ let resourcesScanned = 0;
16560
+ try {
16561
+ try {
16562
+ const lambda = createClient(LambdaClient, region);
16563
+ const functions = [];
16564
+ let marker;
16565
+ do {
16566
+ const resp = await lambda.send(
16567
+ new ListFunctionsCommand({ Marker: marker })
16568
+ );
16569
+ if (resp.Functions) functions.push(...resp.Functions);
16570
+ marker = resp.NextMarker;
16571
+ } while (marker);
16572
+ resourcesScanned += functions.length;
16573
+ for (const fn of functions) {
16574
+ const fnName = fn.FunctionName ?? "unknown";
16575
+ const fnArn = fn.FunctionArn ?? `arn:${partition}:lambda:${region}:${accountId}:function:${fnName}`;
16576
+ const envVars = fn.Environment?.Variables ?? {};
16577
+ for (const [varName, varValue] of Object.entries(envVars)) {
16578
+ for (const sp of SECRET_PATTERNS) {
16579
+ if (sp.matchType === "name") {
16580
+ if (sp.pattern.test(varName)) {
16581
+ findings.push(
16582
+ makeFinding13({
16583
+ riskScore: 7.5,
16584
+ title: `Lambda ${fnName} has suspicious env var "${varName}"`,
16585
+ resourceType: "AWS::Lambda::Function",
16586
+ resourceId: fnName,
16587
+ resourceArn: fnArn,
16588
+ region,
16589
+ description: `Lambda function "${fnName}" has an environment variable named "${varName}" which may contain a secret.`,
16590
+ impact: "Secrets in Lambda environment variables are visible to anyone with lambda:GetFunctionConfiguration permission and may leak through logs.",
16591
+ remediationSteps: [
16592
+ "Move the secret to AWS Secrets Manager or SSM Parameter Store (SecureString).",
16593
+ "Update the Lambda function to fetch the secret at runtime.",
16594
+ "Rotate the exposed credential immediately."
16595
+ ]
16596
+ })
16597
+ );
16598
+ }
16599
+ } else {
16600
+ if (sp.pattern.test(varValue)) {
16601
+ const riskScore = sp.name === "AWS Access Key" ? 9.5 : 9;
16602
+ findings.push(
16603
+ makeFinding13({
16604
+ riskScore,
16605
+ title: `Lambda ${fnName} env var contains ${sp.name}`,
16606
+ resourceType: "AWS::Lambda::Function",
16607
+ resourceId: fnName,
16608
+ resourceArn: fnArn,
16609
+ region,
16610
+ description: `Lambda function "${fnName}" has an environment variable containing a ${sp.name} pattern.`,
16611
+ impact: "Hard-coded credentials in Lambda environment variables can be extracted by any principal with read access to the function configuration.",
16612
+ remediationSteps: [
16613
+ "Remove the hard-coded credential from environment variables.",
16614
+ "Use AWS Secrets Manager or SSM Parameter Store (SecureString) instead.",
16615
+ "Rotate the exposed credential immediately.",
16616
+ "Review CloudTrail logs for unauthorized use of the credential."
16617
+ ]
16618
+ })
16619
+ );
16620
+ }
16621
+ }
16622
+ }
16623
+ }
16624
+ }
16625
+ } catch (e) {
16626
+ warnings.push(`Lambda scan error: ${e instanceof Error ? e.message : String(e)}`);
16627
+ }
16628
+ try {
16629
+ const ec2 = createClient(EC2Client4, region);
16630
+ const instances = [];
16631
+ let nextToken;
16632
+ do {
16633
+ const resp = await ec2.send(
16634
+ new DescribeInstancesCommand3({ NextToken: nextToken })
16635
+ );
16636
+ for (const res of resp.Reservations ?? []) {
16637
+ if (res.Instances) instances.push(...res.Instances);
16638
+ }
16639
+ nextToken = resp.NextToken;
16640
+ } while (nextToken);
16641
+ resourcesScanned += instances.length;
16642
+ for (const inst of instances) {
16643
+ const instId = inst.InstanceId ?? "unknown";
16644
+ const instArn = `arn:${partition}:ec2:${region}:${accountId}:instance/${instId}`;
16645
+ let userData;
16646
+ try {
16647
+ const attrResp = await ec2.send(
16648
+ new DescribeInstanceAttributeCommand({
16649
+ InstanceId: instId,
16650
+ Attribute: "userData"
16651
+ })
16652
+ );
16653
+ const raw = attrResp.UserData?.Value;
16654
+ if (raw) {
16655
+ userData = Buffer.from(raw, "base64").toString("utf-8");
16656
+ }
16657
+ } catch (e) {
16658
+ warnings.push(`Could not read userData for ${instId}: ${e instanceof Error ? e.message : String(e)}`);
16659
+ continue;
16660
+ }
16661
+ if (!userData) continue;
16662
+ for (const sp of SECRET_PATTERNS) {
16663
+ if (sp.matchType === "name") continue;
16664
+ if (sp.pattern.test(userData)) {
16665
+ const riskScore = sp.name === "AWS Access Key" ? 9.5 : 8;
16666
+ findings.push(
16667
+ makeFinding13({
16668
+ riskScore,
16669
+ title: `EC2 ${instId} userData contains ${sp.name}`,
16670
+ resourceType: "AWS::EC2::Instance",
16671
+ resourceId: instId,
16672
+ resourceArn: instArn,
16673
+ region,
16674
+ description: `EC2 instance "${instId}" has user data containing a ${sp.name} pattern.`,
16675
+ impact: "Instance user data is accessible to anyone with ec2:DescribeInstanceAttribute permission and from the instance metadata service.",
16676
+ remediationSteps: [
16677
+ "Remove the secret from instance user data.",
16678
+ "Use IAM instance profiles for AWS API access instead of embedding keys.",
16679
+ "Use Secrets Manager or SSM Parameter Store for other secrets.",
16680
+ "Rotate the exposed credential immediately."
16681
+ ]
16682
+ })
16683
+ );
16684
+ }
16685
+ }
16686
+ }
16687
+ } catch (e) {
16688
+ warnings.push(`EC2 userData scan error: ${e instanceof Error ? e.message : String(e)}`);
16689
+ }
16690
+ return {
16691
+ module: this.moduleName,
16692
+ status: "success",
16693
+ warnings: warnings.length > 0 ? warnings : void 0,
16694
+ resourcesScanned,
16695
+ findingsCount: findings.length,
16696
+ scanTimeMs: Date.now() - startMs,
16697
+ findings
16698
+ };
16699
+ } catch (err) {
16700
+ return {
16701
+ module: this.moduleName,
16702
+ status: "error",
16703
+ error: err instanceof Error ? err.message : String(err),
16704
+ warnings: warnings.length > 0 ? warnings : void 0,
16705
+ resourcesScanned: 0,
16706
+ findingsCount: 0,
16707
+ scanTimeMs: Date.now() - startMs,
16708
+ findings: []
16709
+ };
16710
+ }
16711
+ }
16712
+ };
16713
+
16714
+ // src/scanners/ssl-certificate.ts
16715
+ import {
16716
+ ACMClient,
16717
+ ListCertificatesCommand,
16718
+ DescribeCertificateCommand
16719
+ } from "@aws-sdk/client-acm";
16720
+ function makeFinding14(opts) {
16721
+ const severity = severityFromScore(opts.riskScore);
16722
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
16723
+ }
16724
+ var SslCertificateScanner = class {
16725
+ moduleName = "ssl_certificate";
16726
+ async scan(ctx) {
16727
+ const { region } = ctx;
16728
+ const startMs = Date.now();
16729
+ const findings = [];
16730
+ const warnings = [];
16731
+ try {
16732
+ const client = createClient(ACMClient, region);
16733
+ const certs = [];
16734
+ let nextToken;
16735
+ do {
16736
+ const resp = await client.send(
16737
+ new ListCertificatesCommand({ NextToken: nextToken })
16738
+ );
16739
+ if (resp.CertificateSummaryList) {
16740
+ certs.push(...resp.CertificateSummaryList);
16741
+ }
16742
+ nextToken = resp.NextToken;
16743
+ } while (nextToken);
16744
+ for (const cert of certs) {
16745
+ const certArn = cert.CertificateArn ?? "unknown";
16746
+ const domainName = cert.DomainName ?? "unknown";
16747
+ let detail;
16748
+ try {
16749
+ const descResp = await client.send(
16750
+ new DescribeCertificateCommand({ CertificateArn: certArn })
16751
+ );
16752
+ detail = descResp.Certificate;
16753
+ } catch (e) {
16754
+ warnings.push(`Could not describe certificate ${certArn}: ${e instanceof Error ? e.message : String(e)}`);
16755
+ continue;
16756
+ }
16757
+ if (!detail) continue;
16758
+ const status = detail.Status ?? "UNKNOWN";
16759
+ const inUseBy = detail.InUseBy ?? [];
16760
+ const inUseStr = inUseBy.length > 0 ? ` In use by ${inUseBy.length} resource(s).` : " Not currently in use.";
16761
+ if (status === "FAILED") {
16762
+ findings.push(
16763
+ makeFinding14({
16764
+ riskScore: 7.5,
16765
+ title: `Certificate for ${domainName} is in FAILED status`,
16766
+ resourceType: "AWS::ACM::Certificate",
16767
+ resourceId: domainName,
16768
+ resourceArn: certArn,
16769
+ region,
16770
+ description: `ACM certificate for "${domainName}" has status FAILED.${inUseStr}`,
16771
+ impact: "The certificate failed validation and cannot be used for TLS termination. Services relying on it may lose HTTPS protection.",
16772
+ remediationSteps: [
16773
+ "Check the failure reason in the ACM console.",
16774
+ "Request a new certificate with correct domain validation.",
16775
+ "If using DNS validation, ensure the CNAME records are correctly configured."
16776
+ ]
16777
+ })
16778
+ );
16779
+ continue;
16780
+ }
16781
+ if (status === "ISSUED" && detail.NotAfter) {
16782
+ const now = /* @__PURE__ */ new Date();
16783
+ const expiryDate = new Date(detail.NotAfter);
16784
+ const daysUntilExpiry = Math.floor(
16785
+ (expiryDate.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)
16786
+ );
16787
+ if (daysUntilExpiry < 0) {
16788
+ findings.push(
16789
+ makeFinding14({
16790
+ riskScore: 8,
16791
+ title: `Certificate for ${domainName} has expired`,
16792
+ resourceType: "AWS::ACM::Certificate",
16793
+ resourceId: domainName,
16794
+ resourceArn: certArn,
16795
+ region,
16796
+ description: `ACM certificate for "${domainName}" expired ${Math.abs(daysUntilExpiry)} days ago.${inUseStr}`,
16797
+ impact: "Expired certificates cause TLS errors for end users. Browsers will display security warnings and block access.",
16798
+ remediationSteps: [
16799
+ "Renew or replace the certificate immediately.",
16800
+ "If using ACM-managed renewal, check why automatic renewal failed.",
16801
+ "Verify domain validation records are still in place."
16802
+ ]
16803
+ })
16804
+ );
16805
+ } else if (daysUntilExpiry < 30) {
16806
+ findings.push(
16807
+ makeFinding14({
16808
+ riskScore: 6,
16809
+ title: `Certificate for ${domainName} expires in ${daysUntilExpiry} days`,
16810
+ resourceType: "AWS::ACM::Certificate",
16811
+ resourceId: domainName,
16812
+ resourceArn: certArn,
16813
+ region,
16814
+ description: `ACM certificate for "${domainName}" expires in ${daysUntilExpiry} days (${expiryDate.toISOString().split("T")[0]}).${inUseStr}`,
16815
+ impact: "Certificate will expire soon. If not renewed, services will experience TLS errors.",
16816
+ remediationSteps: [
16817
+ "Verify ACM automatic renewal is working (check renewal status).",
16818
+ "If imported certificate, prepare and import the renewed certificate.",
16819
+ "Set up CloudWatch alarms for certificate expiry."
16820
+ ]
16821
+ })
16822
+ );
16823
+ } else if (daysUntilExpiry < 90) {
16824
+ findings.push(
16825
+ makeFinding14({
16826
+ riskScore: 4,
16827
+ title: `Certificate for ${domainName} expires in ${daysUntilExpiry} days`,
16828
+ resourceType: "AWS::ACM::Certificate",
16829
+ resourceId: domainName,
16830
+ resourceArn: certArn,
16831
+ region,
16832
+ description: `ACM certificate for "${domainName}" expires in ${daysUntilExpiry} days (${expiryDate.toISOString().split("T")[0]}).${inUseStr}`,
16833
+ impact: "Certificate is approaching expiry. Plan renewal to avoid service disruption.",
16834
+ remediationSteps: [
16835
+ "Verify ACM automatic renewal is configured and working.",
16836
+ "If imported certificate, begin the renewal process.",
16837
+ "Consider setting up monitoring for certificate expiry dates."
16838
+ ]
16839
+ })
16840
+ );
16841
+ }
16842
+ }
16843
+ }
16844
+ return {
16845
+ module: this.moduleName,
16846
+ status: "success",
16847
+ warnings: warnings.length > 0 ? warnings : void 0,
16848
+ resourcesScanned: certs.length,
16849
+ findingsCount: findings.length,
16850
+ scanTimeMs: Date.now() - startMs,
16851
+ findings
16852
+ };
16853
+ } catch (err) {
16854
+ return {
16855
+ module: this.moduleName,
16856
+ status: "error",
16857
+ error: err instanceof Error ? err.message : String(err),
16858
+ warnings: warnings.length > 0 ? warnings : void 0,
16859
+ resourcesScanned: 0,
16860
+ findingsCount: 0,
16861
+ scanTimeMs: Date.now() - startMs,
16862
+ findings: []
16863
+ };
16864
+ }
16865
+ }
16866
+ };
16867
+
16868
+ // src/scanners/dns-dangling.ts
16869
+ import {
16870
+ Route53Client,
16871
+ ListHostedZonesCommand,
16872
+ ListResourceRecordSetsCommand
16873
+ } from "@aws-sdk/client-route-53";
16874
+ import {
16875
+ S3Client as S3Client3,
16876
+ HeadBucketCommand
16877
+ } from "@aws-sdk/client-s3";
16878
+ import { promises as dns } from "dns";
16879
+ function makeFinding15(opts) {
16880
+ const severity = severityFromScore(opts.riskScore);
16881
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
16882
+ }
16883
+ function extractS3BucketName(target) {
16884
+ const s3Pattern = /^([^.]+)\.s3[.-]/;
16885
+ const m = target.match(s3Pattern);
16886
+ return m ? m[1] : null;
16887
+ }
16888
+ function classifyTarget(target) {
16889
+ if (/\.s3[.-](.*\.)?amazonaws\.com(\.cn)?\.?$/.test(target)) return "s3";
16890
+ if (/\.elb\.amazonaws\.com(\.cn)?\.?$/.test(target)) return "elb";
16891
+ if (/\.cloudfront\.net\.?$/.test(target)) return "cloudfront";
16892
+ return null;
16893
+ }
16894
+ async function dnsResolves(hostname3) {
16895
+ try {
16896
+ const h = hostname3.endsWith(".") ? hostname3.slice(0, -1) : hostname3;
16897
+ await dns.resolve(h);
16898
+ return true;
16899
+ } catch {
16900
+ return false;
16901
+ }
16902
+ }
16903
+ var DnsDanglingScanner = class {
16904
+ moduleName = "dns_dangling";
16905
+ async scan(ctx) {
16906
+ const { region, partition, accountId } = ctx;
16907
+ const startMs = Date.now();
16908
+ const findings = [];
16909
+ const warnings = [];
16910
+ let resourcesScanned = 0;
16911
+ try {
16912
+ const route53 = createClient(Route53Client, region);
16913
+ const zones = [];
16914
+ let marker;
16915
+ do {
16916
+ const resp = await route53.send(
16917
+ new ListHostedZonesCommand({ Marker: marker })
16918
+ );
16919
+ if (resp.HostedZones) zones.push(...resp.HostedZones);
16920
+ marker = resp.IsTruncated ? resp.NextMarker : void 0;
16921
+ } while (marker);
16922
+ for (const zone of zones) {
16923
+ const zoneId = zone.Id ?? "unknown";
16924
+ const zoneName = zone.Name ?? "unknown";
16925
+ const shortZoneId = zoneId.replace("/hostedzone/", "");
16926
+ const records = [];
16927
+ let nextName;
16928
+ let nextType;
16929
+ do {
16930
+ const resp = await route53.send(
16931
+ new ListResourceRecordSetsCommand({
16932
+ HostedZoneId: shortZoneId,
16933
+ StartRecordName: nextName,
16934
+ StartRecordType: nextType
16935
+ })
16936
+ );
16937
+ if (resp.ResourceRecordSets) records.push(...resp.ResourceRecordSets);
16938
+ if (resp.IsTruncated) {
16939
+ nextName = resp.NextRecordName;
16940
+ nextType = resp.NextRecordType;
16941
+ } else {
16942
+ nextName = void 0;
16943
+ nextType = void 0;
16944
+ }
16945
+ } while (nextName);
16946
+ const cnameRecords = records.filter(
16947
+ (r) => r.Type === "CNAME" && r.ResourceRecords && r.ResourceRecords.length > 0
16948
+ );
16949
+ resourcesScanned += cnameRecords.length;
16950
+ for (const record2 of cnameRecords) {
16951
+ const recordName = record2.Name ?? "unknown";
16952
+ const target = record2.ResourceRecords[0].Value ?? "";
16953
+ const recordArn = `arn:${partition}:route53:::hostedzone/${shortZoneId}`;
16954
+ const targetType = classifyTarget(target);
16955
+ if (targetType === "s3") {
16956
+ const bucketName = extractS3BucketName(target);
16957
+ if (bucketName) {
16958
+ let bucketExists = false;
16959
+ try {
16960
+ const s3 = createClient(S3Client3, region);
16961
+ await s3.send(new HeadBucketCommand({ Bucket: bucketName }));
16962
+ bucketExists = true;
16963
+ } catch (e) {
16964
+ const errName = e.name ?? "";
16965
+ if (errName === "Forbidden" || errName === "AccessDenied" || errName === "403") {
16966
+ bucketExists = true;
16967
+ }
16968
+ }
16969
+ if (!bucketExists) {
16970
+ findings.push(
16971
+ makeFinding15({
16972
+ riskScore: 9.5,
16973
+ title: `CNAME ${recordName} points to non-existent S3 bucket "${bucketName}"`,
16974
+ resourceType: "AWS::Route53::RecordSet",
16975
+ resourceId: recordName,
16976
+ resourceArn: recordArn,
16977
+ region,
16978
+ description: `DNS record "${recordName}" in zone "${zoneName}" has a CNAME to S3 bucket "${bucketName}" which does not exist. An attacker can claim this bucket for subdomain takeover.`,
16979
+ impact: "Critical subdomain takeover vulnerability. An attacker can create the S3 bucket and serve arbitrary content on your domain, enabling phishing, cookie theft, and reputation damage.",
16980
+ remediationSteps: [
16981
+ "Immediately create the S3 bucket to prevent takeover.",
16982
+ "Remove the dangling DNS record if the bucket is no longer needed.",
16983
+ "Audit all CNAME records pointing to S3 buckets."
16984
+ ]
16985
+ })
16986
+ );
16987
+ }
16988
+ }
16989
+ } else if (targetType === "elb") {
16990
+ const resolves = await dnsResolves(target);
16991
+ if (!resolves) {
16992
+ findings.push(
16993
+ makeFinding15({
16994
+ riskScore: 8,
16995
+ title: `CNAME ${recordName} points to non-resolving ELB`,
16996
+ resourceType: "AWS::Route53::RecordSet",
16997
+ resourceId: recordName,
16998
+ resourceArn: recordArn,
16999
+ region,
17000
+ description: `DNS record "${recordName}" in zone "${zoneName}" has a CNAME to ELB "${target}" which does not resolve. The load balancer may have been deleted.`,
17001
+ impact: "Potential subdomain takeover if the ELB DNS name can be re-registered. Dangling DNS records indicate resource lifecycle gaps.",
17002
+ remediationSteps: [
17003
+ "Remove the dangling DNS record.",
17004
+ "If the ELB was deleted, clean up all associated DNS records.",
17005
+ "Implement automated DNS record cleanup when decommissioning resources."
17006
+ ]
17007
+ })
17008
+ );
17009
+ }
17010
+ } else if (targetType === "cloudfront") {
17011
+ const resolves = await dnsResolves(target);
17012
+ if (!resolves) {
17013
+ findings.push(
17014
+ makeFinding15({
17015
+ riskScore: 7.5,
17016
+ title: `CNAME ${recordName} points to non-resolving CloudFront distribution`,
17017
+ resourceType: "AWS::Route53::RecordSet",
17018
+ resourceId: recordName,
17019
+ resourceArn: recordArn,
17020
+ region,
17021
+ description: `DNS record "${recordName}" in zone "${zoneName}" has a CNAME to CloudFront "${target}" which does not resolve. The distribution may have been deleted.`,
17022
+ impact: "Potential subdomain takeover via CloudFront. An attacker may create a distribution with this alternate domain name.",
17023
+ remediationSteps: [
17024
+ "Remove the dangling DNS record.",
17025
+ "If the CloudFront distribution was deleted, clean up associated DNS records.",
17026
+ "Use CloudFront Origin Access Identity to limit exposure."
17027
+ ]
17028
+ })
17029
+ );
17030
+ }
17031
+ } else if (targetType === null) {
17032
+ const resolves = await dnsResolves(target);
17033
+ if (!resolves) {
17034
+ findings.push(
17035
+ makeFinding15({
17036
+ riskScore: 5,
17037
+ title: `CNAME ${recordName} target does not resolve`,
17038
+ resourceType: "AWS::Route53::RecordSet",
17039
+ resourceId: recordName,
17040
+ resourceArn: recordArn,
17041
+ region,
17042
+ description: `DNS record "${recordName}" in zone "${zoneName}" has a CNAME to "${target}" which does not resolve.`,
17043
+ impact: "Orphaned DNS record pointing to a non-existent target. May indicate incomplete resource cleanup.",
17044
+ remediationSteps: [
17045
+ "Verify the target resource still exists.",
17046
+ "Remove the DNS record if it is no longer needed.",
17047
+ "Implement DNS record lifecycle management."
17048
+ ]
17049
+ })
17050
+ );
17051
+ }
17052
+ }
17053
+ }
17054
+ }
17055
+ return {
17056
+ module: this.moduleName,
17057
+ status: "success",
17058
+ warnings: warnings.length > 0 ? warnings : void 0,
17059
+ resourcesScanned,
17060
+ findingsCount: findings.length,
17061
+ scanTimeMs: Date.now() - startMs,
17062
+ findings
17063
+ };
17064
+ } catch (err) {
17065
+ return {
17066
+ module: this.moduleName,
17067
+ status: "error",
17068
+ error: err instanceof Error ? err.message : String(err),
17069
+ warnings: warnings.length > 0 ? warnings : void 0,
17070
+ resourcesScanned: 0,
17071
+ findingsCount: 0,
17072
+ scanTimeMs: Date.now() - startMs,
17073
+ findings: []
17074
+ };
17075
+ }
17076
+ }
17077
+ };
17078
+
17079
+ // src/scanners/network-reachability.ts
17080
+ import {
17081
+ EC2Client as EC2Client5,
17082
+ DescribeInstancesCommand as DescribeInstancesCommand4,
17083
+ DescribeSecurityGroupsCommand as DescribeSecurityGroupsCommand3,
17084
+ DescribeNetworkAclsCommand,
17085
+ DescribeAddressesCommand
17086
+ } from "@aws-sdk/client-ec2";
17087
+ var HIGH_RISK_PORTS2 = {
17088
+ 22: "SSH",
17089
+ 3389: "RDP",
17090
+ 3306: "MySQL",
17091
+ 5432: "PostgreSQL",
17092
+ 1433: "MSSQL",
17093
+ 27017: "MongoDB",
17094
+ 6379: "Redis",
17095
+ 9200: "Elasticsearch",
17096
+ 11211: "Memcached"
17097
+ };
17098
+ function makeFinding16(opts) {
17099
+ const severity = severityFromScore(opts.riskScore);
17100
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
17101
+ }
17102
+ function sgAllowsPort(sgs, port) {
17103
+ for (const sg of sgs) {
17104
+ for (const perm of sg.IpPermissions ?? []) {
17105
+ if (permissionAllowsWorldPort(perm, port)) return true;
17106
+ }
17107
+ }
17108
+ return false;
17109
+ }
17110
+ function sgAllowsAllPorts(sgs) {
17111
+ for (const sg of sgs) {
17112
+ for (const perm of sg.IpPermissions ?? []) {
17113
+ if (isAllPorts2(perm) && hasWorldCidr(perm)) return true;
17114
+ }
17115
+ }
17116
+ return false;
17117
+ }
17118
+ function permissionAllowsWorldPort(perm, port) {
17119
+ if (!hasWorldCidr(perm)) return false;
17120
+ const from = perm.FromPort ?? -1;
17121
+ const to = perm.ToPort ?? -1;
17122
+ if (from === -1 && to === -1) return true;
17123
+ return port >= from && port <= to;
17124
+ }
17125
+ function hasWorldCidr(perm) {
17126
+ const hasIpv4 = (perm.IpRanges ?? []).some((r) => r.CidrIp === "0.0.0.0/0");
17127
+ const hasIpv6 = (perm.Ipv6Ranges ?? []).some((r) => r.CidrIpv6 === "::/0");
17128
+ return hasIpv4 || hasIpv6;
17129
+ }
17130
+ function isAllPorts2(perm) {
17131
+ const from = perm.FromPort ?? -1;
17132
+ const to = perm.ToPort ?? -1;
17133
+ return from === -1 && to === -1 || from === 0 && to === 65535;
17134
+ }
17135
+ function naclAllowsPort(nacl, port) {
17136
+ const inboundRules = (nacl.Entries ?? []).filter((e) => e.Egress === false).sort((a, b) => (a.RuleNumber ?? 0) - (b.RuleNumber ?? 0));
17137
+ for (const rule of inboundRules) {
17138
+ if (naclRuleMatchesPort(rule, port) && naclRuleMatchesWorldCidr(rule)) {
17139
+ return rule.RuleAction === "allow";
17140
+ }
17141
+ }
17142
+ return false;
17143
+ }
17144
+ function naclRuleMatchesPort(rule, port) {
17145
+ if (rule.Protocol === "-1") return true;
17146
+ if (rule.Protocol !== "6" && rule.Protocol !== "17") return false;
17147
+ const from = rule.PortRange?.From ?? 0;
17148
+ const to = rule.PortRange?.To ?? 65535;
17149
+ return port >= from && port <= to;
17150
+ }
17151
+ function naclRuleMatchesWorldCidr(rule) {
17152
+ return rule.CidrBlock === "0.0.0.0/0" || rule.Ipv6CidrBlock === "::/0";
17153
+ }
17154
+ var NetworkReachabilityScanner = class {
17155
+ moduleName = "network_reachability";
17156
+ async scan(ctx) {
17157
+ const { region, partition, accountId } = ctx;
17158
+ const startMs = Date.now();
17159
+ const findings = [];
17160
+ const warnings = [];
17161
+ try {
17162
+ const client = createClient(EC2Client5, region);
17163
+ const eipMap = /* @__PURE__ */ new Map();
17164
+ try {
17165
+ const eipResp = await client.send(new DescribeAddressesCommand({}));
17166
+ for (const addr of eipResp.Addresses ?? []) {
17167
+ if (addr.InstanceId && addr.PublicIp) {
17168
+ eipMap.set(addr.InstanceId, addr.PublicIp);
17169
+ }
17170
+ }
17171
+ } catch (e) {
17172
+ warnings.push(`Could not list Elastic IPs: ${e instanceof Error ? e.message : String(e)}`);
17173
+ }
17174
+ const instances = [];
17175
+ let nextToken;
17176
+ do {
17177
+ const resp = await client.send(
17178
+ new DescribeInstancesCommand4({ NextToken: nextToken })
17179
+ );
17180
+ for (const res of resp.Reservations ?? []) {
17181
+ if (res.Instances) instances.push(...res.Instances);
17182
+ }
17183
+ nextToken = resp.NextToken;
17184
+ } while (nextToken);
17185
+ const publicInstances = instances.filter((inst) => {
17186
+ const instId = inst.InstanceId ?? "";
17187
+ return inst.PublicIpAddress || eipMap.has(instId);
17188
+ });
17189
+ const sgIds = /* @__PURE__ */ new Set();
17190
+ const subnetIds = /* @__PURE__ */ new Set();
17191
+ for (const inst of publicInstances) {
17192
+ for (const sg of inst.SecurityGroups ?? []) {
17193
+ if (sg.GroupId) sgIds.add(sg.GroupId);
17194
+ }
17195
+ if (inst.SubnetId) subnetIds.add(inst.SubnetId);
17196
+ }
17197
+ const sgMap = /* @__PURE__ */ new Map();
17198
+ if (sgIds.size > 0) {
17199
+ const sgResp = await client.send(
17200
+ new DescribeSecurityGroupsCommand3({
17201
+ GroupIds: [...sgIds]
17202
+ })
17203
+ );
17204
+ for (const sg of sgResp.SecurityGroups ?? []) {
17205
+ if (sg.GroupId) sgMap.set(sg.GroupId, sg);
17206
+ }
17207
+ }
17208
+ const subnetNaclMap = /* @__PURE__ */ new Map();
17209
+ if (subnetIds.size > 0) {
17210
+ let naclToken;
17211
+ const allNacls = [];
17212
+ do {
17213
+ const naclResp = await client.send(
17214
+ new DescribeNetworkAclsCommand({
17215
+ Filters: [{ Name: "association.subnet-id", Values: [...subnetIds] }],
17216
+ NextToken: naclToken
17217
+ })
17218
+ );
17219
+ if (naclResp.NetworkAcls) allNacls.push(...naclResp.NetworkAcls);
17220
+ naclToken = naclResp.NextToken;
17221
+ } while (naclToken);
17222
+ for (const nacl of allNacls) {
17223
+ for (const assoc of nacl.Associations ?? []) {
17224
+ if (assoc.SubnetId) {
17225
+ subnetNaclMap.set(assoc.SubnetId, nacl);
17226
+ }
17227
+ }
17228
+ }
17229
+ }
17230
+ for (const inst of publicInstances) {
17231
+ const instId = inst.InstanceId ?? "unknown";
17232
+ const instArn = `arn:${partition}:ec2:${region}:${accountId}:instance/${instId}`;
17233
+ const publicIp = inst.PublicIpAddress ?? eipMap.get(instId) ?? "unknown";
17234
+ const subnetId = inst.SubnetId ?? "";
17235
+ const instSgs = [];
17236
+ for (const sg of inst.SecurityGroups ?? []) {
17237
+ if (sg.GroupId) {
17238
+ const fullSg = sgMap.get(sg.GroupId);
17239
+ if (fullSg) instSgs.push(fullSg);
17240
+ }
17241
+ }
17242
+ const nacl = subnetNaclMap.get(subnetId);
17243
+ for (const [portStr, portName] of Object.entries(HIGH_RISK_PORTS2)) {
17244
+ const port = Number(portStr);
17245
+ const sgAllows = sgAllowsPort(instSgs, port);
17246
+ const naclAllows = nacl ? naclAllowsPort(nacl, port) : true;
17247
+ if (sgAllows && naclAllows) {
17248
+ findings.push(
17249
+ makeFinding16({
17250
+ riskScore: 9.5,
17251
+ title: `EC2 ${instId} (${publicIp}): ${portName} (${port}) reachable from internet`,
17252
+ resourceType: "AWS::EC2::Instance",
17253
+ resourceId: instId,
17254
+ resourceArn: instArn,
17255
+ region,
17256
+ description: `EC2 instance "${instId}" has public IP ${publicIp} and both its security group(s) and subnet NACL allow inbound ${portName} (port ${port}) from the internet.`,
17257
+ impact: `${portName} is directly reachable from the internet, enabling brute-force, exploitation, or unauthorized access.`,
17258
+ remediationSteps: [
17259
+ `Restrict security group inbound rules for port ${port} to specific IPs.`,
17260
+ "Use Systems Manager Session Manager or a bastion host instead of direct access.",
17261
+ "Add NACL deny rules for high-risk ports as an additional layer.",
17262
+ "Enable VPC Flow Logs to monitor connection attempts."
17263
+ ]
17264
+ })
17265
+ );
17266
+ } else if (sgAllows && !naclAllows) {
17267
+ findings.push(
17268
+ makeFinding16({
17269
+ riskScore: 2,
17270
+ title: `EC2 ${instId}: ${portName} (${port}) allowed by SG but blocked by NACL`,
17271
+ resourceType: "AWS::EC2::Instance",
17272
+ resourceId: instId,
17273
+ resourceArn: instArn,
17274
+ region,
17275
+ description: `EC2 instance "${instId}" (${publicIp}) has security group rules allowing ${portName} (port ${port}) from the internet, but the subnet NACL blocks it.`,
17276
+ impact: "Currently protected by NACL, but the SG is overly permissive. NACL changes could expose the port.",
17277
+ remediationSteps: [
17278
+ `Tighten the security group rules for port ${port} to match the intended access.`,
17279
+ "Do not rely solely on NACLs for access control."
17280
+ ]
17281
+ })
17282
+ );
17283
+ }
17284
+ }
17285
+ if (sgAllowsAllPorts(instSgs)) {
17286
+ const naclOpen = nacl ? naclAllowsPort(nacl, 80) : true;
17287
+ if (naclOpen) {
17288
+ findings.push(
17289
+ makeFinding16({
17290
+ riskScore: 8,
17291
+ title: `EC2 ${instId} (${publicIp}): all ports reachable from internet`,
17292
+ resourceType: "AWS::EC2::Instance",
17293
+ resourceId: instId,
17294
+ resourceArn: instArn,
17295
+ region,
17296
+ description: `EC2 instance "${instId}" has public IP ${publicIp} and its security group allows all ports from the internet with no NACL restriction.`,
17297
+ impact: "All services on this instance are exposed to the internet, creating a large attack surface.",
17298
+ remediationSteps: [
17299
+ "Replace the all-ports SG rule with specific port rules.",
17300
+ "Implement NACL rules to restrict inbound traffic as defense in depth.",
17301
+ "Audit all services running on the instance."
17302
+ ]
17303
+ })
17304
+ );
17305
+ }
17306
+ }
17307
+ }
17308
+ return {
17309
+ module: this.moduleName,
17310
+ status: "success",
17311
+ warnings: warnings.length > 0 ? warnings : void 0,
17312
+ resourcesScanned: publicInstances.length,
17313
+ findingsCount: findings.length,
17314
+ scanTimeMs: Date.now() - startMs,
17315
+ findings
17316
+ };
17317
+ } catch (err) {
17318
+ return {
17319
+ module: this.moduleName,
17320
+ status: "error",
17321
+ error: err instanceof Error ? err.message : String(err),
17322
+ warnings: warnings.length > 0 ? warnings : void 0,
17323
+ resourcesScanned: 0,
17324
+ findingsCount: 0,
17325
+ scanTimeMs: Date.now() - startMs,
17326
+ findings: []
17327
+ };
17328
+ }
17329
+ }
17330
+ };
17331
+
17332
+ // src/scanners/iam-privilege-escalation.ts
17333
+ import {
17334
+ IAMClient as IAMClient4,
17335
+ ListUsersCommand as ListUsersCommand3,
17336
+ ListAttachedUserPoliciesCommand as ListAttachedUserPoliciesCommand2,
17337
+ GetPolicyCommand,
17338
+ GetPolicyVersionCommand,
17339
+ ListUserPoliciesCommand,
17340
+ GetUserPolicyCommand
17341
+ } from "@aws-sdk/client-iam";
17342
+ function makeFinding17(opts) {
17343
+ const severity = severityFromScore(opts.riskScore);
17344
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
17345
+ }
17346
+ function extractActions(doc) {
17347
+ const actions = [];
17348
+ if (!doc || typeof doc !== "object") return actions;
17349
+ const policy = doc;
17350
+ const stmts = Array.isArray(policy.Statement) ? policy.Statement : policy.Statement ? [policy.Statement] : [];
17351
+ for (const stmt of stmts) {
17352
+ if (stmt.Effect !== "Allow") continue;
17353
+ const acts = Array.isArray(stmt.Action) ? stmt.Action : stmt.Action ? [stmt.Action] : [];
17354
+ actions.push(...acts);
17355
+ }
17356
+ return actions.map((a) => a.toLowerCase());
17357
+ }
17358
+ function hasAction(actions, pattern) {
17359
+ const pat = pattern.toLowerCase();
17360
+ return actions.some((a) => {
17361
+ if (a === "*") return true;
17362
+ if (a === pat) return true;
17363
+ if (a.endsWith("*")) {
17364
+ const prefix = a.slice(0, -1);
17365
+ if (pat.startsWith(prefix)) return true;
17366
+ }
17367
+ return false;
17368
+ });
17369
+ }
17370
+ var IamPrivilegeEscalationScanner = class {
17371
+ moduleName = "iam_privilege_escalation";
17372
+ async scan(ctx) {
17373
+ const { region, partition, accountId } = ctx;
17374
+ const startMs = Date.now();
17375
+ const findings = [];
17376
+ const warnings = [];
17377
+ const iamRegion = getIamRegion(region);
17378
+ warnings.push(
17379
+ "Note: This scanner currently checks IAM users only. Role and group policy analysis will be added in a future version."
17380
+ );
17381
+ try {
17382
+ const client = createClient(IAMClient4, iamRegion);
17383
+ const users = [];
17384
+ let marker;
17385
+ do {
17386
+ const resp = await client.send(
17387
+ new ListUsersCommand3({ Marker: marker })
17388
+ );
17389
+ if (resp.Users) users.push(...resp.Users);
17390
+ marker = resp.IsTruncated ? resp.Marker : void 0;
17391
+ } while (marker);
17392
+ for (const user of users) {
17393
+ const userName = user.UserName ?? "unknown";
17394
+ const userArn = user.Arn ?? `arn:${partition}:iam::${accountId}:user/${userName}`;
17395
+ const allActions = [];
17396
+ try {
17397
+ const attachedResp = await client.send(
17398
+ new ListAttachedUserPoliciesCommand2({ UserName: userName })
17399
+ );
17400
+ for (const policy of attachedResp.AttachedPolicies ?? []) {
17401
+ const policyArn = policy.PolicyArn;
17402
+ if (!policyArn) continue;
17403
+ try {
17404
+ const policyResp = await client.send(
17405
+ new GetPolicyCommand({ PolicyArn: policyArn })
17406
+ );
17407
+ const versionId = policyResp.Policy?.DefaultVersionId ?? "v1";
17408
+ const versionResp = await client.send(
17409
+ new GetPolicyVersionCommand({
17410
+ PolicyArn: policyArn,
17411
+ VersionId: versionId
17412
+ })
17413
+ );
17414
+ const doc = versionResp.PolicyVersion?.Document;
17415
+ if (doc) {
17416
+ const parsed = JSON.parse(decodeURIComponent(doc));
17417
+ allActions.push(...extractActions(parsed));
17418
+ }
17419
+ } catch (e) {
17420
+ const msg = e instanceof Error ? e.message : String(e);
17421
+ warnings.push(
17422
+ `Could not read policy ${policyArn} for user ${userName}: ${msg}`
17423
+ );
17424
+ }
17425
+ }
17426
+ } catch (e) {
17427
+ const msg = e instanceof Error ? e.message : String(e);
17428
+ warnings.push(
17429
+ `Could not list attached policies for user ${userName}: ${msg}`
17430
+ );
17431
+ }
17432
+ try {
17433
+ const inlineResp = await client.send(
17434
+ new ListUserPoliciesCommand({ UserName: userName })
17435
+ );
17436
+ for (const policyName of inlineResp.PolicyNames ?? []) {
17437
+ try {
17438
+ const inlinePolicyResp = await client.send(
17439
+ new GetUserPolicyCommand({
17440
+ UserName: userName,
17441
+ PolicyName: policyName
17442
+ })
17443
+ );
17444
+ const doc = inlinePolicyResp.PolicyDocument;
17445
+ if (doc) {
17446
+ const parsed = JSON.parse(decodeURIComponent(doc));
17447
+ allActions.push(...extractActions(parsed));
17448
+ }
17449
+ } catch (e) {
17450
+ const msg = e instanceof Error ? e.message : String(e);
17451
+ warnings.push(
17452
+ `Could not read inline policy ${policyName} for user ${userName}: ${msg}`
17453
+ );
17454
+ }
17455
+ }
17456
+ } catch (e) {
17457
+ const msg = e instanceof Error ? e.message : String(e);
17458
+ warnings.push(
17459
+ `Could not list inline policies for user ${userName}: ${msg}`
17460
+ );
17461
+ }
17462
+ if (allActions.length === 0) continue;
17463
+ if (hasAction(allActions, "iam:*") || allActions.includes("*")) {
17464
+ findings.push(
17465
+ makeFinding17({
17466
+ riskScore: 9,
17467
+ title: `IAM user ${userName} has iam:* wildcard permissions`,
17468
+ resourceType: "AWS::IAM::User",
17469
+ resourceId: userName,
17470
+ resourceArn: userArn,
17471
+ region: "global",
17472
+ description: `User "${userName}" has wildcard IAM permissions (iam:* or *), granting full control over identity and access management.`,
17473
+ impact: "The user can create, modify, or delete any IAM resource including creating admin users, modifying policies, and escalating privileges without restriction.",
17474
+ remediationSteps: [
17475
+ `Remove wildcard IAM permissions from user "${userName}".`,
17476
+ "Replace with specific, least-privilege IAM permissions.",
17477
+ "Use IAM Access Analyzer to identify actually used permissions."
17478
+ ]
17479
+ })
17480
+ );
17481
+ continue;
17482
+ }
17483
+ if (hasAction(allActions, "iam:putuserpolicy") || hasAction(allActions, "iam:attachuserpolicy")) {
17484
+ findings.push(
17485
+ makeFinding17({
17486
+ riskScore: 9.5,
17487
+ title: `IAM user ${userName} can self-grant admin via policy attachment`,
17488
+ resourceType: "AWS::IAM::User",
17489
+ resourceId: userName,
17490
+ resourceArn: userArn,
17491
+ region: "global",
17492
+ description: `User "${userName}" has iam:PutUserPolicy or iam:AttachUserPolicy, allowing them to attach AdministratorAccess or any policy to themselves.`,
17493
+ impact: "The user can escalate to full administrator access by attaching an admin policy to their own account.",
17494
+ remediationSteps: [
17495
+ `Remove iam:PutUserPolicy and iam:AttachUserPolicy from user "${userName}".`,
17496
+ "Use permission boundaries to restrict policy attachment scope.",
17497
+ "Require MFA for sensitive IAM operations via condition keys."
17498
+ ]
17499
+ })
17500
+ );
17501
+ }
17502
+ if (hasAction(allActions, "iam:createrole") && hasAction(allActions, "iam:attachrolepolicy")) {
17503
+ findings.push(
17504
+ makeFinding17({
17505
+ riskScore: 8,
17506
+ title: `IAM user ${userName} can create admin roles`,
17507
+ resourceType: "AWS::IAM::User",
17508
+ resourceId: userName,
17509
+ resourceArn: userArn,
17510
+ region: "global",
17511
+ description: `User "${userName}" has both iam:CreateRole and iam:AttachRolePolicy, allowing creation of new roles with admin policies.`,
17512
+ impact: "The user can create a new IAM role with AdministratorAccess and assume it to gain full account access.",
17513
+ remediationSteps: [
17514
+ `Restrict iam:CreateRole and iam:AttachRolePolicy with resource conditions for user "${userName}".`,
17515
+ "Use permission boundaries on all created roles.",
17516
+ "Monitor IAM role creation via CloudTrail alerts."
17517
+ ]
17518
+ })
17519
+ );
17520
+ }
17521
+ if (hasAction(allActions, "iam:passrole") && hasAction(allActions, "lambda:createfunction")) {
17522
+ findings.push(
17523
+ makeFinding17({
17524
+ riskScore: 7.5,
17525
+ title: `IAM user ${userName} can escalate via Lambda role passing`,
17526
+ resourceType: "AWS::IAM::User",
17527
+ resourceId: userName,
17528
+ resourceArn: userArn,
17529
+ region: "global",
17530
+ description: `User "${userName}" has iam:PassRole and lambda:CreateFunction, allowing them to create a Lambda function with an admin role.`,
17531
+ impact: "The user can pass a high-privilege role to a Lambda function and invoke it to execute actions beyond their own permissions.",
17532
+ remediationSteps: [
17533
+ `Restrict iam:PassRole to specific role ARNs for user "${userName}".`,
17534
+ "Use condition keys to limit which roles can be passed to Lambda.",
17535
+ "Implement SCP guardrails for privilege escalation paths."
17536
+ ]
17537
+ })
17538
+ );
17539
+ }
17540
+ if (hasAction(allActions, "iam:createaccesskey")) {
17541
+ findings.push(
17542
+ makeFinding17({
17543
+ riskScore: 8,
17544
+ title: `IAM user ${userName} can create access keys for other users`,
17545
+ resourceType: "AWS::IAM::User",
17546
+ resourceId: userName,
17547
+ resourceArn: userArn,
17548
+ region: "global",
17549
+ description: `User "${userName}" has iam:CreateAccessKey, which allows creating access keys for any IAM user unless restricted by resource conditions.`,
17550
+ impact: "The user can impersonate other IAM users (including admins) by generating access keys on their behalf.",
17551
+ remediationSteps: [
17552
+ `Restrict iam:CreateAccessKey to the user's own ARN using a resource condition.`,
17553
+ "Implement SCP to prevent cross-user key creation.",
17554
+ "Monitor CreateAccessKey events in CloudTrail."
17555
+ ]
17556
+ })
17557
+ );
17558
+ }
17559
+ if (hasAction(allActions, "sts:assumerole")) {
17560
+ findings.push(
17561
+ makeFinding17({
17562
+ riskScore: 8,
17563
+ title: `IAM user ${userName} can assume roles (potential admin escalation)`,
17564
+ resourceType: "AWS::IAM::User",
17565
+ resourceId: userName,
17566
+ resourceArn: userArn,
17567
+ region: "global",
17568
+ description: `User "${userName}" has sts:AssumeRole, which may allow assuming high-privilege or admin roles if not restricted by resource ARN.`,
17569
+ impact: "The user can escalate privileges by assuming roles with higher permissions than their own.",
17570
+ remediationSteps: [
17571
+ `Restrict sts:AssumeRole to specific role ARNs for user "${userName}".`,
17572
+ "Require MFA for assuming sensitive roles via role trust policy conditions.",
17573
+ "Audit which roles this user can assume and their permission levels."
17574
+ ]
17575
+ })
17576
+ );
17577
+ }
17578
+ }
17579
+ return {
17580
+ module: this.moduleName,
17581
+ status: "success",
17582
+ warnings: warnings.length > 0 ? warnings : void 0,
17583
+ resourcesScanned: users.length,
17584
+ findingsCount: findings.length,
17585
+ scanTimeMs: Date.now() - startMs,
17586
+ findings
17587
+ };
17588
+ } catch (err) {
17589
+ return {
17590
+ module: this.moduleName,
17591
+ status: "error",
17592
+ error: err instanceof Error ? err.message : String(err),
17593
+ warnings: warnings.length > 0 ? warnings : void 0,
17594
+ resourcesScanned: 0,
17595
+ findingsCount: 0,
17596
+ scanTimeMs: Date.now() - startMs,
17597
+ findings: []
17598
+ };
17599
+ }
17600
+ }
17601
+ };
17602
+
17603
+ // src/scanners/public-access-verify.ts
17604
+ import {
17605
+ S3Client as S3Client4,
17606
+ ListBucketsCommand as ListBucketsCommand2,
17607
+ GetPublicAccessBlockCommand as GetPublicAccessBlockCommand3,
17608
+ GetBucketAclCommand as GetBucketAclCommand2,
17609
+ GetBucketPolicyStatusCommand as GetBucketPolicyStatusCommand2,
17610
+ GetBucketLocationCommand as GetBucketLocationCommand3
17611
+ } from "@aws-sdk/client-s3";
17612
+ import {
17613
+ RDSClient as RDSClient2,
17614
+ DescribeDBInstancesCommand as DescribeDBInstancesCommand2
17615
+ } from "@aws-sdk/client-rds";
17616
+ import dns2 from "dns";
17617
+ function makeFinding18(opts) {
17618
+ const severity = severityFromScore(opts.riskScore);
17619
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
17620
+ }
17621
+ function s3Endpoint(bucket, region) {
17622
+ const suffix = region.startsWith("cn-") ? "amazonaws.com.cn" : "amazonaws.com";
17623
+ return `https://${bucket}.s3.${region}.${suffix}/`;
17624
+ }
17625
+ async function getBucketRegion3(client, bucketName, defaultRegion, warnings) {
17626
+ try {
17627
+ const resp = await client.send(
17628
+ new GetBucketLocationCommand3({ Bucket: bucketName })
17629
+ );
17630
+ const loc = String(resp.LocationConstraint ?? "") || "us-east-1";
17631
+ return loc;
17632
+ } catch (e) {
17633
+ const msg = e instanceof Error ? e.message : String(e);
17634
+ warnings.push(`Failed to detect region for bucket ${bucketName}, using ${defaultRegion}: ${msg}`);
17635
+ return defaultRegion;
17636
+ }
17637
+ }
17638
+ async function isBucketMarkedPublic(client, bucketName, warnings) {
17639
+ let bpaBlocks = false;
17640
+ try {
17641
+ const bpa = await client.send(
17642
+ new GetPublicAccessBlockCommand3({ Bucket: bucketName })
17643
+ );
17644
+ const cfg = bpa.PublicAccessBlockConfiguration;
17645
+ bpaBlocks = !!(cfg?.BlockPublicAcls && cfg?.IgnorePublicAcls && cfg?.BlockPublicPolicy && cfg?.RestrictPublicBuckets);
17646
+ } catch (e) {
17647
+ if (e instanceof Error && e.name === "NoSuchPublicAccessBlockConfiguration") {
17648
+ bpaBlocks = false;
17649
+ } else {
17650
+ const msg = e instanceof Error ? e.message : String(e);
17651
+ warnings.push(`Could not check public access for bucket ${bucketName}: ${msg}`);
17652
+ return "skip";
17653
+ }
17654
+ }
17655
+ if (bpaBlocks) return false;
17656
+ try {
17657
+ const acl = await client.send(
17658
+ new GetBucketAclCommand2({ Bucket: bucketName })
17659
+ );
17660
+ for (const grant of acl.Grants ?? []) {
17661
+ const uri = grant.Grantee?.URI ?? "";
17662
+ if (uri.includes("AllUsers") || uri.includes("AuthenticatedUsers")) {
17663
+ return true;
17664
+ }
17665
+ }
17666
+ } catch (e) {
17667
+ const msg = e instanceof Error ? e.message : String(e);
17668
+ warnings.push(`Could not check ACL for bucket ${bucketName}: ${msg}`);
17669
+ }
17670
+ try {
17671
+ const policyStatus = await client.send(
17672
+ new GetBucketPolicyStatusCommand2({ Bucket: bucketName })
17673
+ );
17674
+ if (policyStatus.PolicyStatus?.IsPublic) return true;
17675
+ } catch (e) {
17676
+ if (e instanceof Error && !e.name.includes("NoSuchBucketPolicy")) {
17677
+ const msg = e instanceof Error ? e.message : String(e);
17678
+ warnings.push(`Could not check policy status for bucket ${bucketName}: ${msg}`);
17679
+ }
17680
+ }
17681
+ return false;
17682
+ }
17683
+ function isPrivateIp(ip) {
17684
+ if (ip.startsWith("10.")) return true;
17685
+ if (ip.startsWith("192.168.")) return true;
17686
+ if (ip.startsWith("172.")) {
17687
+ const second = parseInt(ip.split(".")[1], 10);
17688
+ return second >= 16 && second <= 31;
17689
+ }
17690
+ if (ip.startsWith("127.")) return true;
17691
+ return false;
17692
+ }
17693
+ var PublicAccessVerifyScanner = class {
17694
+ moduleName = "public_access_verify";
17695
+ async scan(ctx) {
17696
+ const { region, partition, accountId } = ctx;
17697
+ const startMs = Date.now();
17698
+ const findings = [];
17699
+ const warnings = [];
17700
+ let resourcesScanned = 0;
17701
+ try {
17702
+ try {
17703
+ const s3Client = createClient(S3Client4, region);
17704
+ const listResp = await s3Client.send(new ListBucketsCommand2({}));
17705
+ const buckets = listResp.Buckets ?? [];
17706
+ for (const bucket of buckets) {
17707
+ const name = bucket.Name ?? "unknown";
17708
+ const arn = `arn:${partition}:s3:::${name}`;
17709
+ const bucketRegion = await getBucketRegion3(s3Client, name, region, warnings);
17710
+ const bucketClient = bucketRegion === region ? s3Client : createClient(S3Client4, bucketRegion);
17711
+ const markedPublic = await isBucketMarkedPublic(bucketClient, name, warnings);
17712
+ if (markedPublic === "skip" || !markedPublic) continue;
17713
+ resourcesScanned++;
17714
+ const url2 = s3Endpoint(name, bucketRegion);
17715
+ try {
17716
+ const resp = await fetch(url2, {
17717
+ method: "HEAD",
17718
+ signal: AbortSignal.timeout(5e3)
17719
+ });
17720
+ if (resp.ok || resp.status === 200) {
17721
+ findings.push(
17722
+ makeFinding18({
17723
+ riskScore: 9.5,
17724
+ title: `S3 bucket ${name} is publicly readable (verified)`,
17725
+ resourceType: "AWS::S3::Bucket",
17726
+ resourceId: name,
17727
+ resourceArn: arn,
17728
+ region: bucketRegion,
17729
+ description: `HTTP HEAD to ${url2} returned status ${resp.status}. The bucket is confirmed publicly accessible from the internet.`,
17730
+ impact: "Anyone on the internet can read objects from this bucket, potentially exposing sensitive data.",
17731
+ remediationSteps: [
17732
+ "Enable Block Public Access on the bucket immediately.",
17733
+ "Review and remove public ACL grants and public bucket policies.",
17734
+ "Audit bucket contents for sensitive data exposure."
17735
+ ]
17736
+ })
17737
+ );
17738
+ } else if (resp.status === 403) {
17739
+ findings.push(
17740
+ makeFinding18({
17741
+ riskScore: 2,
17742
+ title: `S3 bucket ${name} is marked public but returns 403 (blocked)`,
17743
+ resourceType: "AWS::S3::Bucket",
17744
+ resourceId: name,
17745
+ resourceArn: arn,
17746
+ region: bucketRegion,
17747
+ description: `Bucket "${name}" has public ACL/policy configuration but HTTP access returns 403 Forbidden, likely blocked by other controls.`,
17748
+ impact: "Currently not accessible, but the public configuration is a risk if blocking controls are removed.",
17749
+ remediationSteps: [
17750
+ "Clean up the public ACL or policy to match the intended access model.",
17751
+ "Enable Block Public Access to formalize the restriction."
17752
+ ]
17753
+ })
17754
+ );
17755
+ }
17756
+ } catch (e) {
17757
+ const msg = e instanceof Error ? e.message : String(e);
17758
+ warnings.push(`HTTP check for bucket ${name} failed: ${msg}`);
17759
+ }
17760
+ }
17761
+ } catch (e) {
17762
+ const msg = e instanceof Error ? e.message : String(e);
17763
+ warnings.push(`S3 public access verification failed: ${msg}`);
17764
+ }
17765
+ try {
17766
+ const rdsClient = createClient(RDSClient2, region);
17767
+ const instances = [];
17768
+ let marker;
17769
+ do {
17770
+ const resp = await rdsClient.send(
17771
+ new DescribeDBInstancesCommand2({ Marker: marker })
17772
+ );
17773
+ if (resp.DBInstances) instances.push(...resp.DBInstances);
17774
+ marker = resp.Marker;
17775
+ } while (marker);
17776
+ for (const db of instances) {
17777
+ if (!db.PubliclyAccessible) continue;
17778
+ const dbId = db.DBInstanceIdentifier ?? "unknown";
17779
+ const dbArn = db.DBInstanceArn ?? `arn:${partition}:rds:${region}:${accountId}:db/${dbId}`;
17780
+ const endpoint = db.Endpoint?.Address;
17781
+ if (!endpoint) continue;
17782
+ resourcesScanned++;
17783
+ try {
17784
+ const addresses = await dns2.promises.resolve4(endpoint);
17785
+ const hasPublicIp = addresses.some((ip) => !isPrivateIp(ip));
17786
+ if (hasPublicIp) {
17787
+ findings.push(
17788
+ makeFinding18({
17789
+ riskScore: 8,
17790
+ title: `RDS instance ${dbId} endpoint resolves to public IP (verified)`,
17791
+ resourceType: "AWS::RDS::DBInstance",
17792
+ resourceId: dbId,
17793
+ resourceArn: dbArn,
17794
+ region,
17795
+ description: `RDS endpoint ${endpoint} resolves to public IP(s): ${addresses.join(", ")}. The database is network-reachable from the internet.`,
17796
+ impact: "The database can be reached from the public internet, making it vulnerable to brute-force, credential stuffing, and exploitation of database vulnerabilities.",
17797
+ remediationSteps: [
17798
+ "Set PubliclyAccessible to false on the RDS instance.",
17799
+ "Move the instance to a private subnet.",
17800
+ "Use VPN or bastion host for database access.",
17801
+ "Restrict security group inbound rules to known IPs."
17802
+ ]
17803
+ })
17804
+ );
17805
+ }
17806
+ } catch (e) {
17807
+ const msg = e instanceof Error ? e.message : String(e);
17808
+ warnings.push(`DNS resolution for RDS ${dbId} (${endpoint}) failed: ${msg}`);
17809
+ }
17810
+ }
17811
+ } catch (e) {
17812
+ const msg = e instanceof Error ? e.message : String(e);
17813
+ warnings.push(`RDS public access verification failed: ${msg}`);
17814
+ }
17815
+ return {
17816
+ module: this.moduleName,
17817
+ status: "success",
17818
+ warnings: warnings.length > 0 ? warnings : void 0,
17819
+ resourcesScanned,
17820
+ findingsCount: findings.length,
17821
+ scanTimeMs: Date.now() - startMs,
17822
+ findings
17823
+ };
17824
+ } catch (err) {
17825
+ return {
17826
+ module: this.moduleName,
17827
+ status: "error",
17828
+ error: err instanceof Error ? err.message : String(err),
17829
+ warnings: warnings.length > 0 ? warnings : void 0,
17830
+ resourcesScanned: 0,
17831
+ findingsCount: 0,
17832
+ scanTimeMs: Date.now() - startMs,
17833
+ findings: []
17834
+ };
17835
+ }
17836
+ }
17837
+ };
17838
+
17839
+ // src/scanners/log-integrity.ts
17840
+ import {
17841
+ CloudTrailClient as CloudTrailClient4,
17842
+ DescribeTrailsCommand as DescribeTrailsCommand4,
17843
+ GetTrailStatusCommand
17844
+ } from "@aws-sdk/client-cloudtrail";
17845
+ import {
17846
+ EC2Client as EC2Client6,
17847
+ DescribeFlowLogsCommand as DescribeFlowLogsCommand2,
17848
+ DescribeVpcsCommand as DescribeVpcsCommand2
17849
+ } from "@aws-sdk/client-ec2";
17850
+ import {
17851
+ S3Client as S3Client5,
17852
+ ListBucketsCommand as ListBucketsCommand3,
17853
+ GetBucketLoggingCommand,
17854
+ GetBucketLocationCommand as GetBucketLocationCommand4
17855
+ } from "@aws-sdk/client-s3";
17856
+ import {
17857
+ ElasticLoadBalancingV2Client as ElasticLoadBalancingV2Client2,
17858
+ DescribeLoadBalancersCommand as DescribeLoadBalancersCommand2,
17859
+ DescribeLoadBalancerAttributesCommand
17860
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
17861
+ function makeFinding19(opts) {
17862
+ const severity = severityFromScore(opts.riskScore);
17863
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
17864
+ }
17865
+ var LogIntegrityScanner = class {
17866
+ moduleName = "log_integrity_audit";
17867
+ async scan(ctx) {
17868
+ const { region, partition, accountId } = ctx;
17869
+ const startMs = Date.now();
17870
+ const findings = [];
17871
+ const warnings = [];
17872
+ let resourcesScanned = 0;
17873
+ try {
17874
+ try {
17875
+ const ctClient = createClient(CloudTrailClient4, region);
17876
+ const trailsResp = await ctClient.send(new DescribeTrailsCommand4({}));
17877
+ const trails = trailsResp.trailList ?? [];
17878
+ resourcesScanned += trails.length;
17879
+ const hasMultiRegionTrail = trails.some(
17880
+ (t) => t.IsMultiRegionTrail && t.HomeRegion === region
17881
+ );
17882
+ if (!hasMultiRegionTrail) {
17883
+ findings.push(
17884
+ makeFinding19({
17885
+ riskScore: 7.5,
17886
+ title: "No multi-region CloudTrail trail configured",
17887
+ resourceType: "AWS::CloudTrail::Trail",
17888
+ resourceId: "cloudtrail-multi-region",
17889
+ resourceArn: `arn:${partition}:cloudtrail:${region}:${accountId}:trail/*`,
17890
+ region,
17891
+ description: "No CloudTrail trail is configured to log events across all AWS regions.",
17892
+ impact: "API activity in other regions will not be logged, creating blind spots for security monitoring and incident investigation.",
17893
+ remediationSteps: [
17894
+ "Create a CloudTrail trail with IsMultiRegionTrail enabled.",
17895
+ "Ensure the trail logs management events at minimum.",
17896
+ "Configure the trail to deliver logs to a centralized S3 bucket."
17897
+ ]
17898
+ })
17899
+ );
17900
+ }
17901
+ for (const trail of trails) {
17902
+ if (trail.HomeRegion && trail.HomeRegion !== region) continue;
17903
+ const trailName = trail.Name ?? "unknown";
17904
+ const trailArn = trail.TrailARN ?? `arn:${partition}:cloudtrail:${region}:${accountId}:trail/${trailName}`;
17905
+ if (!trail.LogFileValidationEnabled) {
17906
+ findings.push(
17907
+ makeFinding19({
17908
+ riskScore: 6,
17909
+ title: `CloudTrail trail ${trailName} has no log file validation`,
17910
+ resourceType: "AWS::CloudTrail::Trail",
17911
+ resourceId: trailName,
17912
+ resourceArn: trailArn,
17913
+ region,
17914
+ description: `Trail "${trailName}" does not have log file validation enabled, making it impossible to verify log integrity.`,
17915
+ impact: "Log files could be tampered with or deleted without detection, undermining forensic investigations.",
17916
+ remediationSteps: [
17917
+ "Enable log file validation on the trail.",
17918
+ "Use AWS CLI `cloudtrail validate-logs` to verify existing logs."
17919
+ ]
17920
+ })
17921
+ );
17922
+ }
17923
+ if (!trail.CloudWatchLogsLogGroupArn) {
17924
+ findings.push(
17925
+ makeFinding19({
17926
+ riskScore: 5.5,
17927
+ title: `CloudTrail trail ${trailName} is not integrated with CloudWatch Logs`,
17928
+ resourceType: "AWS::CloudTrail::Trail",
17929
+ resourceId: trailName,
17930
+ resourceArn: trailArn,
17931
+ region,
17932
+ description: `Trail "${trailName}" does not deliver logs to CloudWatch Logs for real-time monitoring and alerting.`,
17933
+ impact: "No real-time alerting on suspicious API activity. Security events can only be detected via delayed S3 log analysis.",
17934
+ remediationSteps: [
17935
+ "Configure CloudWatch Logs integration for the trail.",
17936
+ "Create metric filters and alarms for critical API calls (e.g., unauthorized access, root login)."
17937
+ ]
17938
+ })
17939
+ );
17940
+ }
17941
+ try {
17942
+ const statusResp = await ctClient.send(
17943
+ new GetTrailStatusCommand({ Name: trailArn })
17944
+ );
17945
+ if (!statusResp.IsLogging) {
17946
+ findings.push(
17947
+ makeFinding19({
17948
+ riskScore: 8,
17949
+ title: `CloudTrail trail ${trailName} is not actively logging`,
17950
+ resourceType: "AWS::CloudTrail::Trail",
17951
+ resourceId: trailName,
17952
+ resourceArn: trailArn,
17953
+ region,
17954
+ description: `Trail "${trailName}" exists but is not currently logging API activity.`,
17955
+ impact: "No API activity is being recorded, leaving the account without audit trail coverage.",
17956
+ remediationSteps: [
17957
+ "Start logging on the trail immediately.",
17958
+ "Investigate why logging was stopped (potential attacker action).",
17959
+ "Set up CloudWatch alarms for StopLogging events."
17960
+ ]
17961
+ })
17962
+ );
17963
+ }
17964
+ } catch (e) {
17965
+ const msg = e instanceof Error ? e.message : String(e);
17966
+ warnings.push(`Could not get trail status for ${trailName}: ${msg}`);
17967
+ }
17968
+ }
17969
+ } catch (e) {
17970
+ const msg = e instanceof Error ? e.message : String(e);
17971
+ warnings.push(`CloudTrail audit failed: ${msg}`);
17972
+ }
17973
+ try {
17974
+ const ec2Client = createClient(EC2Client6, region);
17975
+ const vpcs = [];
17976
+ let nextToken;
17977
+ do {
17978
+ const resp = await ec2Client.send(
17979
+ new DescribeVpcsCommand2({ NextToken: nextToken })
17980
+ );
17981
+ if (resp.Vpcs) vpcs.push(...resp.Vpcs);
17982
+ nextToken = resp.NextToken;
17983
+ } while (nextToken);
17984
+ resourcesScanned += vpcs.length;
17985
+ const flowLogsResp = await ec2Client.send(
17986
+ new DescribeFlowLogsCommand2({
17987
+ Filter: [{ Name: "resource-type", Values: ["VPC"] }]
17988
+ })
17989
+ );
17990
+ const flowLogVpcIds = new Set(
17991
+ (flowLogsResp.FlowLogs ?? []).map((fl) => fl.ResourceId)
17992
+ );
17993
+ for (const vpc of vpcs) {
17994
+ const vpcId = vpc.VpcId ?? "unknown";
17995
+ if (!flowLogVpcIds.has(vpcId)) {
17996
+ const vpcArn = `arn:${partition}:ec2:${region}:${accountId}:vpc/${vpcId}`;
17997
+ findings.push(
17998
+ makeFinding19({
17999
+ riskScore: 7,
18000
+ title: `VPC ${vpcId} has no Flow Logs enabled`,
18001
+ resourceType: "AWS::EC2::VPC",
18002
+ resourceId: vpcId,
18003
+ resourceArn: vpcArn,
18004
+ region,
18005
+ description: `VPC "${vpcId}" does not have VPC Flow Logs configured, leaving network traffic unmonitored.`,
18006
+ impact: "No visibility into accepted/rejected network traffic, making it difficult to detect lateral movement, data exfiltration, or unauthorized access.",
18007
+ remediationSteps: [
18008
+ "Enable VPC Flow Logs for this VPC (at minimum REJECT traffic).",
18009
+ "Deliver logs to CloudWatch Logs or S3 for analysis.",
18010
+ "Consider enabling flow logs at the VPC level rather than individual ENIs."
18011
+ ]
18012
+ })
18013
+ );
18014
+ }
18015
+ }
18016
+ } catch (e) {
18017
+ const msg = e instanceof Error ? e.message : String(e);
18018
+ warnings.push(`VPC Flow Logs audit failed: ${msg}`);
18019
+ }
18020
+ try {
18021
+ const s3Client = createClient(S3Client5, region);
18022
+ const listResp = await s3Client.send(new ListBucketsCommand3({}));
18023
+ const buckets = listResp.Buckets ?? [];
18024
+ for (const bucket of buckets) {
18025
+ const name = bucket.Name ?? "unknown";
18026
+ const arn = `arn:${partition}:s3:::${name}`;
18027
+ let bucketRegion = region;
18028
+ try {
18029
+ const locResp = await s3Client.send(
18030
+ new GetBucketLocationCommand4({ Bucket: name })
18031
+ );
18032
+ bucketRegion = String(locResp.LocationConstraint ?? "") || "us-east-1";
18033
+ } catch {
18034
+ }
18035
+ const bucketClient = bucketRegion === region ? s3Client : createClient(S3Client5, bucketRegion);
18036
+ resourcesScanned++;
18037
+ try {
18038
+ const loggingResp = await bucketClient.send(
18039
+ new GetBucketLoggingCommand({ Bucket: name })
18040
+ );
18041
+ if (!loggingResp.LoggingEnabled) {
18042
+ findings.push(
18043
+ makeFinding19({
18044
+ riskScore: 5,
18045
+ title: `S3 bucket ${name} has no access logging`,
18046
+ resourceType: "AWS::S3::Bucket",
18047
+ resourceId: name,
18048
+ resourceArn: arn,
18049
+ region: bucketRegion,
18050
+ description: `Bucket "${name}" does not have server access logging enabled.`,
18051
+ impact: "No visibility into who is accessing the bucket, making it difficult to detect unauthorized data access or exfiltration.",
18052
+ remediationSteps: [
18053
+ "Enable server access logging on the bucket.",
18054
+ "Direct logs to a dedicated logging bucket.",
18055
+ "Consider using CloudTrail S3 data events for more detailed logging."
18056
+ ]
18057
+ })
18058
+ );
18059
+ }
18060
+ } catch (e) {
18061
+ const msg = e instanceof Error ? e.message : String(e);
18062
+ warnings.push(`S3 access logging check for ${name} failed: ${msg}`);
18063
+ }
18064
+ }
18065
+ } catch (e) {
18066
+ const msg = e instanceof Error ? e.message : String(e);
18067
+ warnings.push(`S3 access logging audit failed: ${msg}`);
18068
+ }
18069
+ try {
18070
+ const elbClient = createClient(
18071
+ ElasticLoadBalancingV2Client2,
18072
+ region
18073
+ );
18074
+ const loadBalancers = [];
18075
+ let lbMarker;
18076
+ do {
18077
+ const resp = await elbClient.send(
18078
+ new DescribeLoadBalancersCommand2({ Marker: lbMarker })
18079
+ );
18080
+ if (resp.LoadBalancers) loadBalancers.push(...resp.LoadBalancers);
18081
+ lbMarker = resp.NextMarker;
18082
+ } while (lbMarker);
18083
+ resourcesScanned += loadBalancers.length;
18084
+ for (const lb of loadBalancers) {
18085
+ const lbName = lb.LoadBalancerName ?? "unknown";
18086
+ const lbArn = lb.LoadBalancerArn ?? "unknown";
18087
+ try {
18088
+ const attrsResp = await elbClient.send(
18089
+ new DescribeLoadBalancerAttributesCommand({
18090
+ LoadBalancerArn: lbArn
18091
+ })
18092
+ );
18093
+ const accessLogEnabled = (attrsResp.Attributes ?? []).find(
18094
+ (a) => a.Key === "access_logs.s3.enabled"
18095
+ );
18096
+ if (!accessLogEnabled || accessLogEnabled.Value !== "true") {
18097
+ findings.push(
18098
+ makeFinding19({
18099
+ riskScore: 5.5,
18100
+ title: `ELB ${lbName} has no access logging enabled`,
18101
+ resourceType: "AWS::ElasticLoadBalancingV2::LoadBalancer",
18102
+ resourceId: lbName,
18103
+ resourceArn: lbArn,
18104
+ region,
18105
+ description: `Load balancer "${lbName}" does not have access logging enabled.`,
18106
+ impact: "No visibility into request patterns, client IPs, or error rates \u2014 limits incident investigation and abuse detection.",
18107
+ remediationSteps: [
18108
+ "Enable access logging on the load balancer.",
18109
+ "Configure an S3 bucket for log delivery.",
18110
+ "Ensure the S3 bucket policy allows ELB to write logs."
18111
+ ]
18112
+ })
18113
+ );
18114
+ }
18115
+ } catch (e) {
18116
+ const msg = e instanceof Error ? e.message : String(e);
18117
+ warnings.push(
18118
+ `ELB access logging check for ${lbName} failed: ${msg}`
18119
+ );
18120
+ }
18121
+ }
18122
+ } catch (e) {
18123
+ const msg = e instanceof Error ? e.message : String(e);
18124
+ warnings.push(`ELB access logging audit failed: ${msg}`);
18125
+ }
18126
+ return {
18127
+ module: this.moduleName,
18128
+ status: "success",
18129
+ warnings: warnings.length > 0 ? warnings : void 0,
18130
+ resourcesScanned,
18131
+ findingsCount: findings.length,
18132
+ scanTimeMs: Date.now() - startMs,
18133
+ findings
18134
+ };
18135
+ } catch (err) {
18136
+ return {
18137
+ module: this.moduleName,
18138
+ status: "error",
18139
+ error: err instanceof Error ? err.message : String(err),
18140
+ warnings: warnings.length > 0 ? warnings : void 0,
18141
+ resourcesScanned: 0,
18142
+ findingsCount: 0,
18143
+ scanTimeMs: Date.now() - startMs,
18144
+ findings: []
18145
+ };
18146
+ }
18147
+ }
18148
+ };
18149
+
18150
+ // src/scanners/tag-compliance.ts
18151
+ import {
18152
+ EC2Client as EC2Client7,
18153
+ DescribeInstancesCommand as DescribeInstancesCommand5
18154
+ } from "@aws-sdk/client-ec2";
18155
+ import {
18156
+ RDSClient as RDSClient3,
18157
+ DescribeDBInstancesCommand as DescribeDBInstancesCommand3
18158
+ } from "@aws-sdk/client-rds";
18159
+ import {
18160
+ S3Client as S3Client6,
18161
+ ListBucketsCommand as ListBucketsCommand4,
18162
+ GetBucketTaggingCommand
18163
+ } from "@aws-sdk/client-s3";
18164
+ var DEFAULT_REQUIRED_TAGS = ["Environment", "Project", "Owner"];
18165
+ function makeFinding20(opts) {
18166
+ const severity = severityFromScore(opts.riskScore);
18167
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
18168
+ }
18169
+ function getMissingTags(tags, requiredTags) {
18170
+ const tagKeys = new Set(tags.map((t) => t.Key ?? ""));
18171
+ return requiredTags.filter((rt) => !tagKeys.has(rt));
18172
+ }
18173
+ var TagComplianceScanner = class {
18174
+ moduleName = "tag_compliance";
18175
+ async scan(ctx) {
18176
+ const { region, partition, accountId } = ctx;
18177
+ const startMs = Date.now();
18178
+ const findings = [];
18179
+ const warnings = [];
18180
+ let resourcesScanned = 0;
18181
+ const requiredTags = DEFAULT_REQUIRED_TAGS;
18182
+ try {
18183
+ try {
18184
+ const ec2Client = createClient(EC2Client7, region);
18185
+ const instances = [];
18186
+ let nextToken;
18187
+ do {
18188
+ const resp = await ec2Client.send(
18189
+ new DescribeInstancesCommand5({ NextToken: nextToken })
18190
+ );
18191
+ for (const res of resp.Reservations ?? []) {
18192
+ if (res.Instances) instances.push(...res.Instances);
18193
+ }
18194
+ nextToken = resp.NextToken;
18195
+ } while (nextToken);
18196
+ resourcesScanned += instances.length;
18197
+ for (const instance of instances) {
18198
+ const id = instance.InstanceId ?? "unknown";
18199
+ const arn = `arn:${partition}:ec2:${region}:${accountId}:instance/${id}`;
18200
+ const tags = instance.Tags ?? [];
18201
+ const missing = getMissingTags(tags, requiredTags);
18202
+ if (missing.length > 0) {
18203
+ findings.push(
18204
+ makeFinding20({
18205
+ riskScore: 4,
18206
+ title: `EC2 instance ${id} missing required tags: ${missing.join(", ")}`,
18207
+ resourceType: "AWS::EC2::Instance",
18208
+ resourceId: id,
18209
+ resourceArn: arn,
18210
+ region,
18211
+ description: `EC2 instance "${id}" is missing the following required tags: ${missing.join(", ")}.`,
18212
+ impact: "Resources without proper tags cannot be tracked for cost allocation, ownership, or compliance purposes.",
18213
+ remediationSteps: [
18214
+ `Add the missing tags (${missing.join(", ")}) to instance ${id}.`,
18215
+ "Implement AWS Config rules or Tag Policies to enforce tagging.",
18216
+ "Use AWS Tag Editor for bulk tagging operations."
18217
+ ]
18218
+ })
18219
+ );
18220
+ }
18221
+ }
18222
+ } catch (e) {
18223
+ const msg = e instanceof Error ? e.message : String(e);
18224
+ warnings.push(`EC2 tag compliance check failed: ${msg}`);
18225
+ }
18226
+ try {
18227
+ const rdsClient = createClient(RDSClient3, region);
18228
+ const dbInstances = [];
18229
+ let marker;
18230
+ do {
18231
+ const resp = await rdsClient.send(
18232
+ new DescribeDBInstancesCommand3({ Marker: marker })
18233
+ );
18234
+ if (resp.DBInstances) dbInstances.push(...resp.DBInstances);
18235
+ marker = resp.Marker;
18236
+ } while (marker);
18237
+ resourcesScanned += dbInstances.length;
18238
+ for (const db of dbInstances) {
18239
+ const dbId = db.DBInstanceIdentifier ?? "unknown";
18240
+ const dbArn = db.DBInstanceArn ?? `arn:${partition}:rds:${region}:${accountId}:db/${dbId}`;
18241
+ const tags = (db.TagList ?? []).map((t) => ({
18242
+ Key: t.Key,
18243
+ Value: t.Value
18244
+ }));
18245
+ const missing = getMissingTags(tags, requiredTags);
18246
+ if (missing.length > 0) {
18247
+ findings.push(
18248
+ makeFinding20({
18249
+ riskScore: 4,
18250
+ title: `RDS instance ${dbId} missing required tags: ${missing.join(", ")}`,
18251
+ resourceType: "AWS::RDS::DBInstance",
18252
+ resourceId: dbId,
18253
+ resourceArn: dbArn,
18254
+ region,
18255
+ description: `RDS instance "${dbId}" is missing the following required tags: ${missing.join(", ")}.`,
18256
+ impact: "Resources without proper tags cannot be tracked for cost allocation, ownership, or compliance purposes.",
18257
+ remediationSteps: [
18258
+ `Add the missing tags (${missing.join(", ")}) to RDS instance ${dbId}.`,
18259
+ "Implement AWS Config rules or Tag Policies to enforce tagging.",
18260
+ "Use AWS Tag Editor for bulk tagging operations."
18261
+ ]
18262
+ })
18263
+ );
18264
+ }
18265
+ }
18266
+ } catch (e) {
18267
+ const msg = e instanceof Error ? e.message : String(e);
18268
+ warnings.push(`RDS tag compliance check failed: ${msg}`);
18269
+ }
18270
+ try {
18271
+ const s3Client = createClient(S3Client6, region);
18272
+ const listResp = await s3Client.send(new ListBucketsCommand4({}));
18273
+ const buckets = listResp.Buckets ?? [];
18274
+ resourcesScanned += buckets.length;
18275
+ for (const bucket of buckets) {
18276
+ const name = bucket.Name ?? "unknown";
18277
+ const arn = `arn:${partition}:s3:::${name}`;
18278
+ try {
18279
+ const taggingResp = await s3Client.send(
18280
+ new GetBucketTaggingCommand({ Bucket: name })
18281
+ );
18282
+ const tags = (taggingResp.TagSet ?? []).map((t) => ({
18283
+ Key: t.Key,
18284
+ Value: t.Value
18285
+ }));
18286
+ const missing = getMissingTags(tags, requiredTags);
18287
+ if (missing.length > 0) {
18288
+ findings.push(
18289
+ makeFinding20({
18290
+ riskScore: 4,
18291
+ title: `S3 bucket ${name} missing required tags: ${missing.join(", ")}`,
18292
+ resourceType: "AWS::S3::Bucket",
18293
+ resourceId: name,
18294
+ resourceArn: arn,
18295
+ region: "global",
18296
+ description: `S3 bucket "${name}" is missing the following required tags: ${missing.join(", ")}.`,
18297
+ impact: "Resources without proper tags cannot be tracked for cost allocation, ownership, or compliance purposes.",
18298
+ remediationSteps: [
18299
+ `Add the missing tags (${missing.join(", ")}) to bucket ${name}.`,
18300
+ "Implement AWS Config rules or Tag Policies to enforce tagging.",
18301
+ "Use AWS Tag Editor for bulk tagging operations."
18302
+ ]
18303
+ })
18304
+ );
18305
+ }
18306
+ } catch (e) {
18307
+ if (e instanceof Error && e.name === "NoSuchTagSet") {
18308
+ findings.push(
18309
+ makeFinding20({
18310
+ riskScore: 4,
18311
+ title: `S3 bucket ${name} missing required tags: ${requiredTags.join(", ")}`,
18312
+ resourceType: "AWS::S3::Bucket",
18313
+ resourceId: name,
18314
+ resourceArn: arn,
18315
+ region: "global",
18316
+ description: `S3 bucket "${name}" has no tags configured. Missing all required tags: ${requiredTags.join(", ")}.`,
18317
+ impact: "Resources without proper tags cannot be tracked for cost allocation, ownership, or compliance purposes.",
18318
+ remediationSteps: [
18319
+ `Add the required tags (${requiredTags.join(", ")}) to bucket ${name}.`,
18320
+ "Implement AWS Config rules or Tag Policies to enforce tagging.",
18321
+ "Use AWS Tag Editor for bulk tagging operations."
18322
+ ]
18323
+ })
18324
+ );
18325
+ } else {
18326
+ const msg = e instanceof Error ? e.message : String(e);
18327
+ warnings.push(`S3 tag check for ${name} failed: ${msg}`);
18328
+ }
18329
+ }
18330
+ }
18331
+ } catch (e) {
18332
+ const msg = e instanceof Error ? e.message : String(e);
18333
+ warnings.push(`S3 tag compliance check failed: ${msg}`);
18334
+ }
18335
+ return {
18336
+ module: this.moduleName,
18337
+ status: "success",
18338
+ warnings: warnings.length > 0 ? warnings : void 0,
18339
+ resourcesScanned,
18340
+ findingsCount: findings.length,
18341
+ scanTimeMs: Date.now() - startMs,
18342
+ findings
18343
+ };
18344
+ } catch (err) {
18345
+ return {
18346
+ module: this.moduleName,
18347
+ status: "error",
18348
+ error: err instanceof Error ? err.message : String(err),
18349
+ warnings: warnings.length > 0 ? warnings : void 0,
18350
+ resourcesScanned: 0,
18351
+ findingsCount: 0,
18352
+ scanTimeMs: Date.now() - startMs,
18353
+ findings: []
18354
+ };
18355
+ }
18356
+ }
18357
+ };
18358
+
18359
+ // src/scanners/idle-resources.ts
18360
+ import {
18361
+ EC2Client as EC2Client8,
18362
+ DescribeVolumesCommand as DescribeVolumesCommand2,
18363
+ DescribeAddressesCommand as DescribeAddressesCommand2,
18364
+ DescribeInstancesCommand as DescribeInstancesCommand6,
18365
+ DescribeNetworkInterfacesCommand,
18366
+ DescribeSecurityGroupsCommand as DescribeSecurityGroupsCommand4
18367
+ } from "@aws-sdk/client-ec2";
18368
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
18369
+ function makeFinding21(opts) {
18370
+ const severity = severityFromScore(opts.riskScore);
18371
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
18372
+ }
18373
+ var IdleResourcesScanner = class {
18374
+ moduleName = "idle_resources";
18375
+ async scan(ctx) {
18376
+ const { region, partition, accountId } = ctx;
18377
+ const startMs = Date.now();
18378
+ const findings = [];
18379
+ const warnings = [];
18380
+ try {
18381
+ const client = createClient(EC2Client8, region);
18382
+ let resourcesScanned = 0;
18383
+ const volumes = [];
18384
+ let volToken;
18385
+ do {
18386
+ const resp = await client.send(
18387
+ new DescribeVolumesCommand2({ NextToken: volToken })
18388
+ );
18389
+ if (resp.Volumes) volumes.push(...resp.Volumes);
18390
+ volToken = resp.NextToken;
18391
+ } while (volToken);
18392
+ resourcesScanned += volumes.length;
18393
+ for (const vol of volumes) {
18394
+ if (vol.State === "available") {
18395
+ const volId = vol.VolumeId ?? "unknown";
18396
+ findings.push(
18397
+ makeFinding21({
18398
+ riskScore: 3,
18399
+ title: `EBS volume ${volId} is unattached`,
18400
+ resourceType: "AWS::EC2::Volume",
18401
+ resourceId: volId,
18402
+ resourceArn: `arn:${partition}:ec2:${region}:${accountId}:volume/${volId}`,
18403
+ region,
18404
+ description: `EBS volume "${volId}" (${vol.Size ?? "?"}GB, ${vol.VolumeType ?? "unknown"}) is in "available" state with no attachments.`,
18405
+ impact: "Unattached volumes incur storage costs and may contain sensitive data that is no longer actively managed.",
18406
+ remediationSteps: [
18407
+ "Determine if the volume is still needed.",
18408
+ "If not needed, create a snapshot for archival and delete the volume.",
18409
+ "If needed, attach it to the appropriate instance."
18410
+ ]
18411
+ })
18412
+ );
18413
+ }
18414
+ }
18415
+ let addresses = [];
18416
+ try {
18417
+ const addrResp = await client.send(new DescribeAddressesCommand2({}));
18418
+ addresses = addrResp.Addresses ?? [];
18419
+ } catch (e) {
18420
+ const msg = e instanceof Error ? e.message : String(e);
18421
+ warnings.push(`Elastic IP check failed: ${msg}`);
18422
+ }
18423
+ resourcesScanned += addresses.length;
18424
+ for (const addr of addresses) {
18425
+ if (!addr.AssociationId) {
18426
+ const allocId = addr.AllocationId ?? "unknown";
18427
+ const publicIp = addr.PublicIp ?? "unknown";
18428
+ findings.push(
18429
+ makeFinding21({
18430
+ riskScore: 2,
18431
+ title: `Elastic IP ${publicIp} is not associated`,
18432
+ resourceType: "AWS::EC2::EIP",
18433
+ resourceId: allocId,
18434
+ resourceArn: `arn:${partition}:ec2:${region}:${accountId}:elastic-ip/${allocId}`,
18435
+ region,
18436
+ description: `Elastic IP ${publicIp} (${allocId}) is allocated but not associated with any instance or network interface.`,
18437
+ impact: "Unused Elastic IPs cost ~$3.60/month each and represent unnecessary spend.",
18438
+ remediationSteps: [
18439
+ "Associate the EIP with an instance or network interface if needed.",
18440
+ "Release the EIP if it is no longer required."
18441
+ ]
18442
+ })
18443
+ );
18444
+ }
18445
+ }
18446
+ const instances = [];
18447
+ let instToken;
18448
+ do {
18449
+ const instResp = await client.send(
18450
+ new DescribeInstancesCommand6({ NextToken: instToken })
18451
+ );
18452
+ for (const res of instResp.Reservations ?? []) {
18453
+ if (res.Instances) instances.push(...res.Instances);
18454
+ }
18455
+ instToken = instResp.NextToken;
18456
+ } while (instToken);
18457
+ resourcesScanned += instances.length;
18458
+ const now = Date.now();
18459
+ for (const inst of instances) {
18460
+ if (inst.State?.Name === "stopped") {
18461
+ const instId = inst.InstanceId ?? "unknown";
18462
+ const reason = inst.StateTransitionReason ?? "";
18463
+ const stoppedTime = reason ? parseStopTime(reason) : null;
18464
+ if (!stoppedTime) {
18465
+ warnings.push(
18466
+ `Could not determine stop date for instance ${instId}. StateTransitionReason: ${reason}`
18467
+ );
18468
+ continue;
18469
+ }
18470
+ const stoppedDays = Math.round(
18471
+ (now - stoppedTime) / (24 * 60 * 60 * 1e3)
18472
+ );
18473
+ if (stoppedDays > 30) {
18474
+ findings.push(
18475
+ makeFinding21({
18476
+ riskScore: 3,
18477
+ title: `EC2 instance ${instId} has been stopped for ${stoppedDays} days`,
18478
+ resourceType: "AWS::EC2::Instance",
18479
+ resourceId: instId,
18480
+ resourceArn: `arn:${partition}:ec2:${region}:${accountId}:instance/${instId}`,
18481
+ region,
18482
+ description: `EC2 instance "${instId}" (${inst.InstanceType ?? "unknown"}) is in stopped state for ${stoppedDays} days. Attached EBS volumes continue to incur charges.`,
18483
+ impact: "Stopped instances still incur EBS storage costs and may contain stale configurations or unpatched AMIs.",
18484
+ remediationSteps: [
18485
+ "Determine if the instance is still needed.",
18486
+ "If not needed, create an AMI for archival and terminate the instance.",
18487
+ "If needed temporarily, consider using a launch template for on-demand recreation."
18488
+ ]
18489
+ })
18490
+ );
18491
+ }
18492
+ }
18493
+ }
18494
+ const securityGroups = [];
18495
+ let sgToken;
18496
+ do {
18497
+ const sgResp = await client.send(
18498
+ new DescribeSecurityGroupsCommand4({ NextToken: sgToken })
18499
+ );
18500
+ if (sgResp.SecurityGroups) securityGroups.push(...sgResp.SecurityGroups);
18501
+ sgToken = sgResp.NextToken;
18502
+ } while (sgToken);
18503
+ const usedSgIds = /* @__PURE__ */ new Set();
18504
+ let eniToken;
18505
+ do {
18506
+ const eniResp = await client.send(
18507
+ new DescribeNetworkInterfacesCommand({ NextToken: eniToken })
18508
+ );
18509
+ for (const eni of eniResp.NetworkInterfaces ?? []) {
18510
+ for (const group of eni.Groups ?? []) {
18511
+ if (group.GroupId) usedSgIds.add(group.GroupId);
18512
+ }
18513
+ }
18514
+ eniToken = eniResp.NextToken;
18515
+ } while (eniToken);
18516
+ resourcesScanned += securityGroups.length;
18517
+ for (const sg of securityGroups) {
18518
+ const sgId = sg.GroupId ?? "unknown";
18519
+ if (sg.GroupName === "default") continue;
18520
+ if (!usedSgIds.has(sgId)) {
18521
+ findings.push(
18522
+ makeFinding21({
18523
+ riskScore: 2,
18524
+ title: `Security group ${sgId} is not attached to any resource`,
18525
+ resourceType: "AWS::EC2::SecurityGroup",
18526
+ resourceId: sgId,
18527
+ resourceArn: `arn:${partition}:ec2:${region}:${sg.OwnerId ?? accountId}:security-group/${sgId}`,
18528
+ region,
18529
+ description: `Security group "${sg.GroupName}" (${sgId}) is not associated with any network interface.`,
18530
+ impact: "Unused security groups add clutter and may cause confusion during security reviews.",
18531
+ remediationSteps: [
18532
+ "Verify the security group is not referenced by other resources (e.g., launch templates).",
18533
+ "Delete the security group if it is no longer needed."
18534
+ ]
18535
+ })
18536
+ );
18537
+ }
18538
+ }
18539
+ return {
18540
+ module: this.moduleName,
18541
+ status: "success",
18542
+ warnings: warnings.length > 0 ? warnings : void 0,
18543
+ resourcesScanned,
18544
+ findingsCount: findings.length,
18545
+ scanTimeMs: Date.now() - startMs,
18546
+ findings
18547
+ };
18548
+ } catch (err) {
18549
+ return {
18550
+ module: this.moduleName,
18551
+ status: "error",
18552
+ error: err instanceof Error ? err.message : String(err),
18553
+ warnings: warnings.length > 0 ? warnings : void 0,
18554
+ resourcesScanned: 0,
18555
+ findingsCount: 0,
18556
+ scanTimeMs: Date.now() - startMs,
18557
+ findings: []
18558
+ };
18559
+ }
18560
+ }
18561
+ };
18562
+ function parseStopTime(reason) {
18563
+ const match = reason.match(/\((\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\s\w+)\)/);
18564
+ if (!match) return null;
18565
+ const parsed = Date.parse(match[1]);
18566
+ return isNaN(parsed) ? null : parsed;
18567
+ }
18568
+
18569
+ // src/scanners/disaster-recovery.ts
18570
+ import {
18571
+ RDSClient as RDSClient4,
18572
+ DescribeDBInstancesCommand as DescribeDBInstancesCommand4
18573
+ } from "@aws-sdk/client-rds";
18574
+ import {
18575
+ EC2Client as EC2Client9,
18576
+ DescribeVolumesCommand as DescribeVolumesCommand3,
18577
+ DescribeSnapshotsCommand as DescribeSnapshotsCommand2
18578
+ } from "@aws-sdk/client-ec2";
18579
+ import {
18580
+ S3Client as S3Client7,
18581
+ ListBucketsCommand as ListBucketsCommand5,
18582
+ GetBucketVersioningCommand as GetBucketVersioningCommand3,
18583
+ GetBucketReplicationCommand
18584
+ } from "@aws-sdk/client-s3";
18585
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
18586
+ function makeFinding22(opts) {
18587
+ const severity = severityFromScore(opts.riskScore);
18588
+ return { ...opts, severity, priority: priorityFromSeverity(severity) };
18589
+ }
18590
+ var DisasterRecoveryScanner = class {
18591
+ moduleName = "disaster_recovery";
18592
+ async scan(ctx) {
18593
+ const { region, partition, accountId } = ctx;
18594
+ const startMs = Date.now();
18595
+ const findings = [];
18596
+ const warnings = [];
18597
+ try {
18598
+ let resourcesScanned = 0;
18599
+ const rdsClient = createClient(RDSClient4, region);
18600
+ const instances = [];
18601
+ let marker;
18602
+ do {
18603
+ const resp = await rdsClient.send(
18604
+ new DescribeDBInstancesCommand4({ Marker: marker })
18605
+ );
18606
+ if (resp.DBInstances) instances.push(...resp.DBInstances);
18607
+ marker = resp.Marker;
18608
+ } while (marker);
18609
+ resourcesScanned += instances.length;
18610
+ for (const db of instances) {
18611
+ const dbId = db.DBInstanceIdentifier ?? "unknown";
18612
+ const dbArn = db.DBInstanceArn ?? `arn:${partition}:rds:${region}:${accountId}:db/${dbId}`;
18613
+ const engine = db.Engine ?? "unknown";
18614
+ if (!db.MultiAZ) {
18615
+ findings.push(
18616
+ makeFinding22({
18617
+ riskScore: 6,
18618
+ title: `RDS instance ${dbId} is not Multi-AZ`,
18619
+ resourceType: "AWS::RDS::DBInstance",
18620
+ resourceId: dbId,
18621
+ resourceArn: dbArn,
18622
+ region,
18623
+ description: `RDS instance "${dbId}" (${engine}) does not have Multi-AZ deployment enabled.`,
18624
+ impact: "Single-AZ deployments have no automatic failover. An AZ outage will cause downtime and potential data loss.",
18625
+ remediationSteps: [
18626
+ "Enable Multi-AZ deployment for the RDS instance.",
18627
+ "This provides automatic failover to a standby in a different AZ."
18628
+ ]
18629
+ })
18630
+ );
18631
+ }
18632
+ const retention = db.BackupRetentionPeriod ?? 0;
18633
+ if (retention === 0) {
18634
+ findings.push(
18635
+ makeFinding22({
18636
+ riskScore: 5.5,
18637
+ title: `RDS instance ${dbId} has automated backups disabled`,
18638
+ resourceType: "AWS::RDS::DBInstance",
18639
+ resourceId: dbId,
18640
+ resourceArn: dbArn,
18641
+ region,
18642
+ description: `RDS instance "${dbId}" (${engine}) has backup retention period set to 0 (disabled).`,
18643
+ impact: "No automated backups or point-in-time recovery. Data loss from failures or corruption is unrecoverable.",
18644
+ remediationSteps: [
18645
+ "Set the backup retention period to at least 7 days.",
18646
+ "Consider cross-region backup replication for critical databases."
18647
+ ]
18648
+ })
18649
+ );
18650
+ } else if (retention < 7) {
18651
+ findings.push(
18652
+ makeFinding22({
18653
+ riskScore: 5.5,
18654
+ title: `RDS instance ${dbId} backup retention is only ${retention} day(s)`,
18655
+ resourceType: "AWS::RDS::DBInstance",
18656
+ resourceId: dbId,
18657
+ resourceArn: dbArn,
18658
+ region,
18659
+ description: `RDS instance "${dbId}" (${engine}) has backup retention period of ${retention} day(s), below the recommended 7 days.`,
18660
+ impact: "Short retention windows limit point-in-time recovery options and may not meet compliance requirements.",
18661
+ remediationSteps: [
18662
+ "Increase the backup retention period to at least 7 days.",
18663
+ "For production databases, consider 14-35 days retention."
18664
+ ]
18665
+ })
18666
+ );
18667
+ }
18668
+ }
18669
+ const ec2Client = createClient(EC2Client9, region);
18670
+ const volumes = [];
18671
+ let volToken;
18672
+ do {
18673
+ const resp = await ec2Client.send(
18674
+ new DescribeVolumesCommand3({ NextToken: volToken })
18675
+ );
18676
+ if (resp.Volumes) volumes.push(...resp.Volumes);
18677
+ volToken = resp.NextToken;
18678
+ } while (volToken);
18679
+ const snapshots = [];
18680
+ let snapToken;
18681
+ do {
18682
+ const resp = await ec2Client.send(
18683
+ new DescribeSnapshotsCommand2({
18684
+ OwnerIds: ["self"],
18685
+ NextToken: snapToken
18686
+ })
18687
+ );
18688
+ if (resp.Snapshots) snapshots.push(...resp.Snapshots);
18689
+ snapToken = resp.NextToken;
18690
+ } while (snapToken);
18691
+ const latestSnapshotByVolume = /* @__PURE__ */ new Map();
18692
+ for (const snap of snapshots) {
18693
+ if (!snap.VolumeId || snap.State !== "completed") continue;
18694
+ const snapTime = snap.StartTime?.getTime() ?? 0;
18695
+ const existing = latestSnapshotByVolume.get(snap.VolumeId) ?? 0;
18696
+ if (snapTime > existing) {
18697
+ latestSnapshotByVolume.set(snap.VolumeId, snapTime);
18698
+ }
18699
+ }
18700
+ const inUseVolumes = volumes.filter((v) => v.State === "in-use");
18701
+ resourcesScanned += inUseVolumes.length;
18702
+ const now = Date.now();
18703
+ for (const vol of inUseVolumes) {
18704
+ const volId = vol.VolumeId ?? "unknown";
18705
+ const volArn = `arn:${partition}:ec2:${region}:${accountId}:volume/${volId}`;
18706
+ const latestSnap = latestSnapshotByVolume.get(volId);
18707
+ if (latestSnap === void 0) {
18708
+ findings.push(
18709
+ makeFinding22({
18710
+ riskScore: 7,
18711
+ title: `EBS volume ${volId} has no snapshots`,
18712
+ resourceType: "AWS::EC2::Volume",
18713
+ resourceId: volId,
18714
+ resourceArn: volArn,
18715
+ region,
18716
+ description: `EBS volume "${volId}" (${vol.Size ?? "?"}GB, ${vol.VolumeType ?? "unknown"}) has no snapshots. Data cannot be recovered if the volume fails.`,
18717
+ impact: "Complete data loss if the volume becomes unavailable. No backup exists for disaster recovery.",
18718
+ remediationSteps: [
18719
+ "Create a snapshot of the volume immediately.",
18720
+ "Set up automated snapshots using AWS Backup or Amazon Data Lifecycle Manager."
18721
+ ]
18722
+ })
18723
+ );
18724
+ } else if (now - latestSnap > SEVEN_DAYS_MS) {
18725
+ const daysSince = Math.round((now - latestSnap) / (24 * 60 * 60 * 1e3));
18726
+ findings.push(
18727
+ makeFinding22({
18728
+ riskScore: 5,
18729
+ title: `EBS volume ${volId} has no recent snapshot (${daysSince} days old)`,
18730
+ resourceType: "AWS::EC2::Volume",
18731
+ resourceId: volId,
18732
+ resourceArn: volArn,
18733
+ region,
18734
+ description: `EBS volume "${volId}" (${vol.Size ?? "?"}GB) most recent snapshot is ${daysSince} days old, exceeding the 7-day threshold.`,
18735
+ impact: "Recovery from the latest snapshot would lose up to ${daysSince} days of data.",
18736
+ remediationSteps: [
18737
+ "Create a fresh snapshot of the volume.",
18738
+ "Configure automated snapshot schedules using AWS Backup or Data Lifecycle Manager."
18739
+ ]
18740
+ })
18741
+ );
18742
+ }
18743
+ }
18744
+ const s3Client = createClient(S3Client7, region);
18745
+ let bucketNames = [];
18746
+ try {
18747
+ const listResp = await s3Client.send(new ListBucketsCommand5({}));
18748
+ bucketNames = (listResp.Buckets ?? []).map((b) => b.Name).filter((n) => !!n);
18749
+ } catch (e) {
18750
+ const msg = e instanceof Error ? e.message : String(e);
18751
+ warnings.push(`S3 bucket list failed: ${msg}`);
18752
+ }
18753
+ resourcesScanned += bucketNames.length;
18754
+ for (const name of bucketNames) {
18755
+ const arn = `arn:${partition}:s3:::${name}`;
18756
+ try {
18757
+ const ver = await s3Client.send(
18758
+ new GetBucketVersioningCommand3({ Bucket: name })
18759
+ );
18760
+ if (ver.Status !== "Enabled") {
18761
+ findings.push(
18762
+ makeFinding22({
18763
+ riskScore: 3,
18764
+ title: `S3 bucket ${name} does not have versioning enabled`,
18765
+ resourceType: "AWS::S3::Bucket",
18766
+ resourceId: name,
18767
+ resourceArn: arn,
18768
+ region,
18769
+ description: `Bucket "${name}" versioning is ${ver.Status ?? "not set"}. Object deletion or overwrite is irreversible.`,
18770
+ impact: "Accidental deletion or corruption of objects cannot be recovered without versioning.",
18771
+ remediationSteps: [
18772
+ "Enable versioning on the bucket.",
18773
+ "Consider adding lifecycle rules to manage version storage costs."
18774
+ ]
18775
+ })
18776
+ );
18777
+ }
18778
+ } catch (e) {
18779
+ const msg = e instanceof Error ? e.message : String(e);
18780
+ warnings.push(`Bucket ${name} versioning check failed: ${msg}`);
18781
+ }
18782
+ try {
18783
+ await s3Client.send(
18784
+ new GetBucketReplicationCommand({ Bucket: name })
18785
+ );
18786
+ } catch (e) {
18787
+ if (e instanceof Error && e.name === "ReplicationConfigurationNotFoundError") {
18788
+ findings.push(
18789
+ makeFinding22({
18790
+ riskScore: 3.5,
18791
+ title: `S3 bucket ${name} has no cross-region replication`,
18792
+ resourceType: "AWS::S3::Bucket",
18793
+ resourceId: name,
18794
+ resourceArn: arn,
18795
+ region,
18796
+ description: `Bucket "${name}" does not have cross-region replication configured.`,
18797
+ impact: "Data is stored in a single region. A regional outage could make the data unavailable.",
18798
+ remediationSteps: [
18799
+ "Enable cross-region replication to a bucket in another region.",
18800
+ "Ensure versioning is enabled (required for CRR).",
18801
+ "Consider S3 Replication Time Control for critical data."
18802
+ ]
18803
+ })
18804
+ );
18805
+ } else {
18806
+ const msg = e instanceof Error ? e.message : String(e);
18807
+ warnings.push(`Bucket ${name} replication check failed: ${msg}`);
18808
+ }
18809
+ }
18810
+ }
18811
+ return {
18812
+ module: this.moduleName,
18813
+ status: "success",
18814
+ warnings: warnings.length > 0 ? warnings : void 0,
18815
+ resourcesScanned,
18816
+ findingsCount: findings.length,
18817
+ scanTimeMs: Date.now() - startMs,
18818
+ findings
18819
+ };
18820
+ } catch (err) {
18821
+ return {
18822
+ module: this.moduleName,
18823
+ status: "error",
18824
+ error: err instanceof Error ? err.message : String(err),
18825
+ warnings: warnings.length > 0 ? warnings : void 0,
18826
+ resourcesScanned: 0,
18827
+ findingsCount: 0,
18828
+ scanTimeMs: Date.now() - startMs,
18829
+ findings: []
18830
+ };
18831
+ }
18832
+ }
18833
+ };
18834
+
18835
+ // src/tools/report-tool.ts
18836
+ var SEVERITY_ICON = {
18837
+ CRITICAL: "\u{1F534}",
18838
+ HIGH: "\u{1F7E0}",
18839
+ MEDIUM: "\u{1F7E1}",
18840
+ LOW: "\u{1F7E2}"
18841
+ };
18842
+ var SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
18843
+ function formatDuration(start, end) {
18844
+ const ms = new Date(end).getTime() - new Date(start).getTime();
18845
+ if (ms < 1e3) return `${ms}ms`;
18846
+ const secs = Math.round(ms / 1e3);
18847
+ if (secs < 60) return `${secs}s`;
18848
+ const mins = Math.floor(secs / 60);
18849
+ const remainSecs = secs % 60;
18850
+ return `${mins}m ${remainSecs}s`;
18851
+ }
18852
+ function renderFinding(f) {
18853
+ const steps = f.remediationSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
18854
+ return [
18855
+ `#### ${f.title}`,
18856
+ `- **Resource:** ${f.resourceId} (\`${f.resourceArn}\`)`,
18857
+ `- **Description:** ${f.description}`,
18858
+ `- **Impact:** ${f.impact}`,
18859
+ `- **Risk Score:** ${f.riskScore}/10`,
18860
+ `- **Remediation:**`,
18861
+ steps,
18862
+ `- **Priority:** ${f.priority}`
18863
+ ].join("\n");
18864
+ }
18865
+ function generateMarkdownReport(scanResults) {
18866
+ const { summary, modules, accountId, region, scanStart, scanEnd } = scanResults;
18867
+ const date5 = scanStart.split("T")[0];
18868
+ const duration3 = formatDuration(scanStart, scanEnd);
18869
+ const lines = [];
18870
+ lines.push(`# AWS Security Scan Report \u2014 ${date5}`);
18871
+ lines.push("");
18872
+ lines.push("## Executive Summary");
18873
+ lines.push(`- **Account:** ${accountId}`);
18874
+ lines.push(`- **Region:** ${region}`);
18875
+ lines.push(`- **Scan Duration:** ${duration3}`);
18876
+ lines.push(
18877
+ `- **Total Findings:** ${summary.totalFindings} (\u{1F534} ${summary.critical} Critical | \u{1F7E0} ${summary.high} High | \u{1F7E1} ${summary.medium} Medium | \u{1F7E2} ${summary.low} Low)`
18878
+ );
18879
+ lines.push("");
18880
+ if (summary.totalFindings === 0) {
18881
+ lines.push("## Findings by Severity");
18882
+ lines.push("");
18883
+ lines.push("\u2705 No security issues found.");
18884
+ lines.push("");
18885
+ } else {
18886
+ const allFindings = modules.flatMap((m) => m.findings);
18887
+ const grouped = /* @__PURE__ */ new Map();
18888
+ for (const sev of SEVERITY_ORDER) {
18889
+ grouped.set(sev, []);
18890
+ }
18891
+ for (const f of allFindings) {
18892
+ grouped.get(f.severity).push(f);
18893
+ }
18894
+ lines.push("## Findings by Severity");
18895
+ lines.push("");
18896
+ for (const sev of SEVERITY_ORDER) {
18897
+ const findings = grouped.get(sev);
18898
+ const icon = SEVERITY_ICON[sev];
18899
+ lines.push(`### ${icon} ${sev.charAt(0)}${sev.slice(1).toLowerCase()}`);
18900
+ lines.push("");
18901
+ if (findings.length === 0) {
18902
+ lines.push(`No ${sev.toLowerCase()} findings.`);
18903
+ lines.push("");
18904
+ continue;
18905
+ }
18906
+ findings.sort((a, b) => b.riskScore - a.riskScore);
18907
+ for (const f of findings) {
18908
+ lines.push(renderFinding(f));
18909
+ lines.push("");
18910
+ }
18911
+ }
18912
+ }
18913
+ lines.push("## Scan Statistics");
18914
+ lines.push(
18915
+ "| Module | Resources Scanned | Findings | Status |"
18916
+ );
18917
+ lines.push("|--------|------------------|----------|--------|");
18918
+ for (const m of modules) {
18919
+ const status = m.status === "success" ? "\u2705" : "\u274C";
18920
+ lines.push(
18921
+ `| ${m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
18922
+ );
18923
+ }
18924
+ lines.push("");
18925
+ if (summary.totalFindings > 0) {
18926
+ const allFindings = modules.flatMap((m) => m.findings);
18927
+ allFindings.sort((a, b) => b.riskScore - a.riskScore);
18928
+ lines.push("## Recommendations (Priority Order)");
18929
+ for (let i = 0; i < allFindings.length; i++) {
18930
+ const f = allFindings[i];
18931
+ lines.push(`${i + 1}. [${f.priority}] ${f.title}: ${f.remediationSteps[0] ?? "Review and remediate."}`);
18932
+ }
18933
+ lines.push("");
18934
+ }
18935
+ return lines.join("\n");
18936
+ }
18937
+
18938
+ // src/tools/mlps-report.ts
18939
+ var MLPS_CHECKS = [
18940
+ // 一、身份鉴别
18941
+ {
18942
+ id: "8.1.4.1a",
18943
+ category: "\u8EAB\u4EFD\u9274\u522B",
18944
+ name: "\u5BC6\u7801\u7B56\u7565",
18945
+ modules: ["iam_password_policy"],
18946
+ findingPatterns: ["password policy", "password length", "complexity", "password expiry", "reuse prevention"]
18947
+ },
18948
+ {
18949
+ id: "8.1.4.1a",
18950
+ category: "\u8EAB\u4EFD\u9274\u522B",
18951
+ name: "\u5BC6\u94A5\u8F6E\u6362",
18952
+ modules: ["iam"],
18953
+ findingPatterns: ["access key older"]
18954
+ },
18955
+ {
18956
+ id: "8.1.4.1d",
18957
+ category: "\u8EAB\u4EFD\u9274\u522B",
18958
+ name: "\u53CC\u56E0\u7D20\u8BA4\u8BC1",
18959
+ modules: ["iam_mfa_audit", "iam"],
18960
+ findingPatterns: ["MFA"]
18961
+ },
18962
+ // 二、访问控制
18963
+ {
18964
+ id: "8.1.4.2c",
18965
+ category: "\u8BBF\u95EE\u63A7\u5236",
18966
+ name: "\u6700\u5C0F\u6743\u9650",
18967
+ modules: ["iam", "iam_privilege_escalation"],
18968
+ findingPatterns: [
18969
+ "AdministratorAccess",
18970
+ "PowerUserAccess",
18971
+ "IAMFullAccess",
18972
+ "over-permissive",
18973
+ "privilege escalation",
18974
+ "self-grant",
18975
+ "iam:*",
18976
+ "create admin",
18977
+ "Lambda role passing",
18978
+ "CreateAccessKey",
18979
+ "AssumeRole"
18980
+ ]
18981
+ },
18982
+ {
18983
+ id: "8.1.4.2",
18984
+ category: "\u8BBF\u95EE\u63A7\u5236",
18985
+ name: "\u5B89\u5168\u7EC4",
18986
+ modules: ["security_group", "network_reachability"],
18987
+ findingPatterns: ["allows all ports", "allows SSH", "allows RDP", "MySQL", "PostgreSQL", "MongoDB", "Redis", "high-risk port"]
18988
+ },
18989
+ // 三、安全审计
18990
+ {
18991
+ id: "8.1.4.3a",
18992
+ category: "\u5B89\u5168\u5BA1\u8BA1",
18993
+ name: "\u5BA1\u8BA1\u529F\u80FD",
18994
+ modules: ["cloudtrail"],
18995
+ findingPatterns: ["CloudTrail", "not enabled", "multi-region", "not logging"]
18996
+ },
18997
+ {
18998
+ id: "8.1.4.3b",
18999
+ category: "\u5B89\u5168\u5BA1\u8BA1",
19000
+ name: "\u5BA1\u8BA1\u5B8C\u6574\u6027",
19001
+ modules: ["cloudtrail", "log_integrity_audit"],
19002
+ findingPatterns: ["log file validation", "log integrity", "log validation"]
19003
+ },
19004
+ {
19005
+ id: "8.1.4.3c",
19006
+ category: "\u5B89\u5168\u5BA1\u8BA1",
19007
+ name: "\u5BA1\u8BA1\u4FDD\u62A4",
19008
+ modules: ["cloudtrail_protection"],
19009
+ findingPatterns: ["CloudTrail", "S3 bucket", "encryption", "versioning", "Block Public Access"]
19010
+ },
19011
+ // 四、入侵防范
19012
+ {
19013
+ id: "8.1.4.4a",
19014
+ category: "\u5165\u4FB5\u9632\u8303",
19015
+ name: "GuardDuty \u5A01\u80C1\u68C0\u6D4B",
19016
+ modules: ["service_detection"],
19017
+ findingPatterns: ["GuardDuty"]
19018
+ },
19019
+ {
19020
+ id: "8.1.4.4a",
19021
+ category: "\u5165\u4FB5\u9632\u8303",
19022
+ name: "Inspector \u6F0F\u6D1E\u626B\u63CF",
19023
+ modules: ["service_detection"],
19024
+ findingPatterns: ["Inspector"]
19025
+ },
19026
+ // 五、数据安全
19027
+ {
19028
+ id: "8.1.4.5a",
19029
+ category: "\u6570\u636E\u5B89\u5168",
19030
+ name: "\u4F20\u8F93\u52A0\u5BC6",
19031
+ modules: ["elb_https", "ssl_certificate"],
19032
+ findingPatterns: ["HTTPS", "TLS", "HTTP listener", "certificate"]
19033
+ },
19034
+ {
19035
+ id: "8.1.4.5b",
19036
+ category: "\u6570\u636E\u5B89\u5168",
19037
+ name: "S3 \u5B58\u50A8\u52A0\u5BC6",
19038
+ modules: ["s3"],
19039
+ findingPatterns: ["no default encryption", "not encrypted"]
19040
+ },
19041
+ {
19042
+ id: "8.1.4.5b",
19043
+ category: "\u6570\u636E\u5B89\u5168",
19044
+ name: "EBS \u9ED8\u8BA4\u52A0\u5BC6",
19045
+ modules: ["ebs"],
19046
+ findingPatterns: ["EBS default encryption"]
19047
+ },
19048
+ {
19049
+ id: "8.1.4.5b",
19050
+ category: "\u6570\u636E\u5B89\u5168",
19051
+ name: "RDS \u5B58\u50A8\u52A0\u5BC6",
19052
+ modules: ["rds"],
19053
+ findingPatterns: ["storage is not encrypted"]
19054
+ },
19055
+ // 六、网络安全
19056
+ {
19057
+ id: "8.1.3.1a",
19058
+ category: "\u7F51\u7EDC\u5B89\u5168",
19059
+ name: "\u7F51\u7EDC\u67B6\u6784",
19060
+ modules: ["vpc"],
19061
+ findingPatterns: ["default VPC"]
19062
+ },
19063
+ {
19064
+ id: "8.1.3.2a",
19065
+ category: "\u7F51\u7EDC\u5B89\u5168",
19066
+ name: "\u8FB9\u754C\u9632\u62A4",
19067
+ modules: ["security_group"],
19068
+ findingPatterns: ["allows all ports", "allows SSH", "allows RDP"]
19069
+ }
19070
+ ];
19071
+ var CATEGORY_ORDER = [
19072
+ "\u8EAB\u4EFD\u9274\u522B",
19073
+ "\u8BBF\u95EE\u63A7\u5236",
19074
+ "\u5B89\u5168\u5BA1\u8BA1",
19075
+ "\u5165\u4FB5\u9632\u8303",
19076
+ "\u6570\u636E\u5B89\u5168",
19077
+ "\u7F51\u7EDC\u5B89\u5168"
19078
+ ];
19079
+ var CATEGORY_SECTION = {
19080
+ "\u8EAB\u4EFD\u9274\u522B": "\u4E00\u3001\u8EAB\u4EFD\u9274\u522B",
19081
+ "\u8BBF\u95EE\u63A7\u5236": "\u4E8C\u3001\u8BBF\u95EE\u63A7\u5236",
19082
+ "\u5B89\u5168\u5BA1\u8BA1": "\u4E09\u3001\u5B89\u5168\u5BA1\u8BA1",
19083
+ "\u5165\u4FB5\u9632\u8303": "\u56DB\u3001\u5165\u4FB5\u9632\u8303",
19084
+ "\u6570\u636E\u5B89\u5168": "\u4E94\u3001\u6570\u636E\u5B89\u5168",
19085
+ "\u7F51\u7EDC\u5B89\u5168": "\u516D\u3001\u7F51\u7EDC\u5B89\u5168"
19086
+ };
19087
+ function evaluateCheck(check2, allFindings, scanModules) {
19088
+ const allModulesPresent = check2.modules.every(
19089
+ (mod) => scanModules.some((m) => m.module === mod && m.status === "success")
19090
+ );
19091
+ if (!allModulesPresent) {
19092
+ return { check: check2, status: "unknown", relatedFindings: [] };
19093
+ }
19094
+ const relatedFindings = allFindings.filter((f) => {
19095
+ const moduleMatch = check2.modules.some((mod) => f.module === mod);
19096
+ if (!moduleMatch) return false;
19097
+ const text = `${f.title} ${f.description}`.toLowerCase();
19098
+ return check2.findingPatterns.some(
19099
+ (pattern) => text.includes(pattern.toLowerCase())
19100
+ );
19101
+ });
19102
+ return {
19103
+ check: check2,
19104
+ status: relatedFindings.length === 0 ? "pass" : "fail",
19105
+ relatedFindings
19106
+ };
19107
+ }
19108
+ function generateMlps3Report(scanResults) {
19109
+ const { accountId, region, scanStart } = scanResults;
19110
+ const scanTime = scanStart.replace("T", " ").replace(/\.\d+Z$/, " UTC");
19111
+ const allFindings = scanResults.modules.flatMap(
19112
+ (m) => m.findings.map((f) => ({ ...f, module: f.module ?? m.module }))
19113
+ );
19114
+ const scanModules = scanResults.modules.map((m) => ({
19115
+ module: m.module,
19116
+ status: m.status
19117
+ }));
19118
+ const results = MLPS_CHECKS.map(
19119
+ (check2) => evaluateCheck(check2, allFindings, scanModules)
19120
+ );
19121
+ const passCount = results.filter((r) => r.status === "pass").length;
19122
+ const failCount = results.filter((r) => r.status === "fail").length;
19123
+ const unknownCount = results.filter((r) => r.status === "unknown").length;
19124
+ const checkedTotal = passCount + failCount;
19125
+ const total = results.length;
19126
+ const percent = checkedTotal > 0 ? Math.round(passCount / checkedTotal * 100) : 0;
19127
+ const lines = [];
19128
+ lines.push("# \u7B49\u4FDD\u4E09\u7EA7\u9884\u68C0\u62A5\u544A");
19129
+ lines.push("> **\u672C\u62A5\u544A\u4E3A\u7B49\u4FDD\u9884\u68C0\u53C2\u8003\uFF0C\u4EC5\u8986\u76D6 AWS \u4E91\u5E73\u53F0\u914D\u7F6E\u68C0\u67E5\u3002\u5B8C\u6574\u7B49\u4FDD\u6D4B\u8BC4\u9700\u7531\u6301\u8BC1\u6D4B\u8BC4\u673A\u6784\u6267\u884C\u3002**");
19130
+ lines.push("");
19131
+ lines.push("## \u8D26\u6237\u4FE1\u606F");
19132
+ lines.push(`- Account: ${accountId} | Region: ${region} | \u626B\u63CF\u65F6\u95F4: ${scanTime}`);
19133
+ lines.push("");
19134
+ lines.push("## \u9884\u68C0\u603B\u89C8");
19135
+ lines.push(`- \u68C0\u67E5\u9879: ${total} | \u901A\u8FC7: ${passCount} | \u4E0D\u901A\u8FC7: ${failCount}${unknownCount > 0 ? ` | \u672A\u68C0\u67E5: ${unknownCount}` : ""}`);
19136
+ lines.push(`- \u901A\u8FC7\u7387: ${percent}%${unknownCount > 0 ? "\uFF08\u672A\u68C0\u67E5\u9879\u4E0D\u8BA1\u5165\u901A\u8FC7\u7387\uFF09" : ""}`);
19137
+ lines.push("");
19138
+ for (const category of CATEGORY_ORDER) {
19139
+ const sectionTitle = CATEGORY_SECTION[category];
19140
+ const categoryResults = results.filter((r) => r.check.category === category);
19141
+ if (categoryResults.length === 0) continue;
19142
+ lines.push(`## ${sectionTitle}`);
19143
+ lines.push("");
19144
+ const byId = /* @__PURE__ */ new Map();
19145
+ for (const r of categoryResults) {
19146
+ const existing = byId.get(r.check.id) ?? [];
19147
+ existing.push(r);
19148
+ byId.set(r.check.id, existing);
19149
+ }
19150
+ for (const [checkId, checkResults] of byId) {
19151
+ lines.push(`### ${checkId} ${checkResults[0].check.name}`);
19152
+ for (const r of checkResults) {
19153
+ const icon = r.status === "pass" ? "\u2705" : r.status === "fail" ? "\u274C" : "\u26A0\uFE0F";
19154
+ const label = r.status === "unknown" ? " \u672A\u68C0\u67E5" : "";
19155
+ lines.push(`- [${icon}] ${r.check.name}${label}`);
19156
+ if (r.status === "fail" && r.relatedFindings.length > 0) {
19157
+ for (const f of r.relatedFindings.slice(0, 3)) {
19158
+ lines.push(` - ${f.severity}: ${f.title}`);
19159
+ }
19160
+ if (r.relatedFindings.length > 3) {
19161
+ lines.push(` - ... \u53CA\u5176\u4ED6 ${r.relatedFindings.length - 3} \u9879`);
19162
+ }
19163
+ }
19164
+ }
19165
+ lines.push("");
19166
+ }
19167
+ }
19168
+ const failedResults = results.filter((r) => r.status === "fail");
19169
+ if (failedResults.length > 0) {
19170
+ lines.push("## \u5EFA\u8BAE\u6574\u6539\u9879\uFF08\u6309\u4F18\u5148\u7EA7\uFF09");
19171
+ lines.push("");
19172
+ const allFailedFindings = /* @__PURE__ */ new Map();
19173
+ for (const r of failedResults) {
19174
+ for (const f of r.relatedFindings) {
19175
+ const key = `${f.resourceId}:${f.title}`;
19176
+ if (!allFailedFindings.has(key)) {
19177
+ allFailedFindings.set(key, f);
19178
+ }
19179
+ }
19180
+ }
19181
+ const sorted = [...allFailedFindings.values()].sort(
19182
+ (a, b) => b.riskScore - a.riskScore
19183
+ );
19184
+ for (let i = 0; i < sorted.length; i++) {
19185
+ const f = sorted[i];
19186
+ const priority = f.riskScore >= 9 ? "P0" : f.riskScore >= 7 ? "P1" : f.riskScore >= 4 ? "P2" : "P3";
19187
+ const remediation = f.remediationSteps[0] ?? "Review and remediate.";
19188
+ lines.push(`${i + 1}. [${priority}] ${f.title} \u2014 ${remediation}`);
19189
+ }
19190
+ lines.push("");
19191
+ }
19192
+ return lines.join("\n");
19193
+ }
19194
+
19195
+ // src/tools/save-results.ts
19196
+ import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
19197
+ import { join } from "path";
19198
+ import { homedir } from "os";
19199
+ function calculateScore(summary) {
19200
+ const raw = 100 - summary.critical * 15 - summary.high * 5 - summary.medium * 2 - summary.low * 0.5;
19201
+ return Math.max(0, Math.min(100, raw));
16024
19202
  }
16025
19203
  function saveResults(scanResults, outputDir) {
16026
19204
  const baseDir = outputDir ?? join(homedir(), ".aws-security");
@@ -16083,6 +19261,71 @@ function saveResults(scanResults, outputDir) {
16083
19261
  return dataPath;
16084
19262
  }
16085
19263
 
19264
+ // src/tools/scan-groups.ts
19265
+ var SCAN_GROUPS = {
19266
+ mlps3_precheck: {
19267
+ name: "\u7B49\u4FDD\u4E09\u7EA7\u9884\u68C0",
19268
+ description: "GB/T 22239-2019 \u7B49\u4FDD\u4E09\u7EA7 AWS \u4E91\u79DF\u6237\u5C42\u914D\u7F6E\u68C0\u67E5",
19269
+ modules: ["security_group", "s3", "iam", "cloudtrail", "rds", "ebs", "vpc", "service_detection", "iam_password_policy", "iam_mfa_audit", "cloudtrail_protection", "elb_https", "secret_exposure", "ssl_certificate", "dns_dangling", "network_reachability", "iam_privilege_escalation", "log_integrity_audit", "tag_compliance", "disaster_recovery"],
19270
+ reportType: "mlps3"
19271
+ },
19272
+ hw_defense: {
19273
+ name: "\u62A4\u7F51\u84DD\u961F\u52A0\u56FA",
19274
+ description: "\u62A4\u7F51\u524D\u5B89\u5168\u81EA\u67E5 \u2014 \u653B\u51FB\u9762+\u5F31\u70B9\u8BC4\u4F30",
19275
+ modules: ["security_group", "s3", "iam", "ebs", "vpc", "service_detection", "secret_exposure", "network_reachability", "iam_privilege_escalation"]
19276
+ },
19277
+ exposure: {
19278
+ name: "\u516C\u7F51\u66B4\u9732\u9762\u8BC4\u4F30",
19279
+ description: "\u8BC4\u4F30\u516C\u7F51\u53EF\u8FBE\u7684\u8D44\u6E90\u548C\u7AEF\u53E3",
19280
+ modules: ["security_group", "vpc", "s3", "rds", "elb_https", "network_reachability", "dns_dangling", "public_access_verify"]
19281
+ },
19282
+ pre_launch: {
19283
+ name: "\u751F\u4EA7\u4E0A\u7EBF\u524D\u68C0\u67E5",
19284
+ description: "\u4E0A\u7EBF\u524D\u5168\u9762\u5B89\u5168\u8BC4\u4F30",
19285
+ modules: ["ALL"]
19286
+ },
19287
+ data_encryption: {
19288
+ name: "\u6570\u636E\u52A0\u5BC6\u5BA1\u8BA1",
19289
+ description: "\u5168\u9762\u68C0\u67E5\u5B58\u50A8\u548C\u4F20\u8F93\u52A0\u5BC6\u72B6\u6001",
19290
+ modules: ["s3", "ebs", "rds", "elb_https", "ssl_certificate"]
19291
+ },
19292
+ least_privilege: {
19293
+ name: "\u6700\u5C0F\u6743\u9650\u5BA1\u8BA1",
19294
+ description: "IAM \u6743\u9650\u6700\u5C0F\u5316\u8BC4\u4F30",
19295
+ modules: ["iam", "iam_password_policy", "iam_mfa_audit", "iam_privilege_escalation"]
19296
+ },
19297
+ log_integrity: {
19298
+ name: "\u65E5\u5FD7\u5B8C\u6574\u6027\u5BA1\u8BA1",
19299
+ description: "\u5BA1\u8BA1\u65E5\u5FD7\u5B8C\u6574\u6027\u548C\u4FDD\u62A4",
19300
+ modules: ["cloudtrail", "cloudtrail_protection", "vpc", "service_detection", "log_integrity_audit"]
19301
+ },
19302
+ disaster_recovery: {
19303
+ name: "\u707E\u5907\u8BC4\u4F30",
19304
+ description: "\u5907\u4EFD\u548C\u707E\u5907\u80FD\u529B\u8BC4\u4F30",
19305
+ modules: ["rds", "ebs", "s3", "disaster_recovery"]
19306
+ },
19307
+ idle_resources: {
19308
+ name: "\u95F2\u7F6E\u8D44\u6E90\u6E05\u7406",
19309
+ description: "\u53D1\u73B0\u672A\u4F7F\u7528\u7684\u8D44\u6E90",
19310
+ modules: ["iam", "ebs", "security_group", "idle_resources"]
19311
+ },
19312
+ tag_compliance: {
19313
+ name: "\u8D44\u6E90\u6807\u7B7E\u5408\u89C4",
19314
+ description: "\u68C0\u67E5\u5FC5\u9700\u6807\u7B7E",
19315
+ modules: ["tag_compliance"]
19316
+ },
19317
+ public_access_verify: {
19318
+ name: "\u516C\u7F51\u53EF\u8FBE\u6027\u9A8C\u8BC1",
19319
+ description: "\u9A8C\u8BC1\u6807\u8BB0\u4E3A\u516C\u5F00\u7684\u8D44\u6E90\u662F\u5426\u771F\u6B63\u53EF\u4ECE\u4E92\u8054\u7F51\u8BBF\u95EE",
19320
+ modules: ["public_access_verify"]
19321
+ },
19322
+ new_account_baseline: {
19323
+ name: "\u65B0\u8D26\u6237\u57FA\u7EBF\u68C0\u67E5",
19324
+ description: "\u65B0 AWS \u8D26\u6237\u5B89\u5168\u57FA\u7EBF",
19325
+ modules: ["iam", "iam_password_policy", "iam_mfa_audit", "cloudtrail", "service_detection", "vpc", "security_group", "secret_exposure"]
19326
+ }
19327
+ };
19328
+
16086
19329
  // src/resources/index.ts
16087
19330
  var SECURITY_RULES_CONTENT = `# AWS Security Scan Modules & Rules
16088
19331
 
@@ -16206,7 +19449,21 @@ var MODULE_DESCRIPTIONS = {
16206
19449
  rds: "Scans RDS instances for public accessibility, encryption, backups, and deletion protection.",
16207
19450
  ebs: "Checks EBS volumes and snapshots for encryption and public sharing.",
16208
19451
  vpc: "Reviews VPC configuration including default VPC usage, flow logs, and default security groups.",
16209
- service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config, Macie) are enabled and assesses security maturity."
19452
+ service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config, Macie) are enabled and assesses security maturity.",
19453
+ iam_password_policy: "Checks IAM account password policy against MLPS requirements (length, complexity, expiry, reuse prevention).",
19454
+ iam_mfa_audit: "Audits MFA status for all IAM users with console access and calculates MFA adoption rate.",
19455
+ cloudtrail_protection: "Checks CloudTrail log S3 bucket protection (encryption, versioning, Block Public Access).",
19456
+ elb_https: "Checks ELB/ALB/NLB listeners for HTTPS/TLS configuration.",
19457
+ secret_exposure: "Checks Lambda env vars and EC2 userData for exposed secrets (AWS keys, private keys, passwords).",
19458
+ ssl_certificate: "Checks ACM certificates for expiry, failed status, and upcoming renewals.",
19459
+ dns_dangling: "Checks Route53 CNAME records for dangling DNS (subdomain takeover risk).",
19460
+ network_reachability: "Analyzes true network reachability by combining Security Group + NACL rules for public EC2 instances.",
19461
+ iam_privilege_escalation: "Detects IAM privilege escalation paths \u2014 users/roles that can escalate to admin via policy manipulation, role creation, or service abuse.",
19462
+ public_access_verify: "Verifies actual public accessibility of resources marked as public (S3 HTTP check, RDS DNS resolution).",
19463
+ log_integrity_audit: "Comprehensive logging integrity audit \u2014 CloudTrail, VPC Flow Logs, S3 access logging, ELB access logging.",
19464
+ tag_compliance: "Checks EC2, RDS, and S3 resources for required tags (Environment, Project, Owner).",
19465
+ idle_resources: "Finds unused/idle AWS resources (unattached EBS volumes, unused EIPs, stopped instances, unused security groups) that waste money and increase attack surface.",
19466
+ disaster_recovery: "Assesses disaster recovery readiness \u2014 RDS Multi-AZ & backups, EBS snapshot coverage, S3 versioning & cross-region replication."
16210
19467
  };
16211
19468
  function summarizeResult(result) {
16212
19469
  const { summary } = result;
@@ -16249,7 +19506,21 @@ function createServer(defaultRegion) {
16249
19506
  new RdsScanner(),
16250
19507
  new EbsScanner(),
16251
19508
  new VpcScanner(),
16252
- new ServiceDetectionScanner()
19509
+ new ServiceDetectionScanner(),
19510
+ new IamPasswordPolicyScanner(),
19511
+ new IamMfaAuditScanner(),
19512
+ new CloudTrailProtectionScanner(),
19513
+ new ElbHttpsScanner(),
19514
+ new SecretExposureScanner(),
19515
+ new SslCertificateScanner(),
19516
+ new DnsDanglingScanner(),
19517
+ new NetworkReachabilityScanner(),
19518
+ new IamPrivilegeEscalationScanner(),
19519
+ new PublicAccessVerifyScanner(),
19520
+ new LogIntegrityScanner(),
19521
+ new TagComplianceScanner(),
19522
+ new IdleResourcesScanner(),
19523
+ new DisasterRecoveryScanner()
16253
19524
  ];
16254
19525
  const scannerMap = /* @__PURE__ */ new Map();
16255
19526
  for (const s of allScanners) {
@@ -16257,7 +19528,7 @@ function createServer(defaultRegion) {
16257
19528
  }
16258
19529
  server.tool(
16259
19530
  "scan_all",
16260
- "Run all 8 security scanners in parallel (including service detection). Read-only. Does not modify any AWS resources.",
19531
+ "Run all security scanners in parallel (including service detection). Read-only. Does not modify any AWS resources.",
16261
19532
  { region: external_exports.string().optional().describe("AWS region to scan (default: server region)") },
16262
19533
  async ({ region }) => {
16263
19534
  try {
@@ -16282,7 +19553,21 @@ function createServer(defaultRegion) {
16282
19553
  { toolName: "scan_rds", moduleName: "rds", label: "RDS" },
16283
19554
  { toolName: "scan_ebs", moduleName: "ebs", label: "EBS" },
16284
19555
  { toolName: "scan_vpc", moduleName: "vpc", label: "VPC" },
16285
- { toolName: "detect_services", moduleName: "service_detection", label: "Security Service Detection" }
19556
+ { toolName: "detect_services", moduleName: "service_detection", label: "Security Service Detection" },
19557
+ { toolName: "scan_iam_password_policy", moduleName: "iam_password_policy", label: "IAM Password Policy" },
19558
+ { toolName: "scan_iam_mfa_audit", moduleName: "iam_mfa_audit", label: "IAM MFA Audit" },
19559
+ { toolName: "scan_cloudtrail_protection", moduleName: "cloudtrail_protection", label: "CloudTrail Protection" },
19560
+ { toolName: "scan_elb_https", moduleName: "elb_https", label: "ELB HTTPS" },
19561
+ { toolName: "scan_secret_exposure", moduleName: "secret_exposure", label: "Secret Exposure" },
19562
+ { toolName: "scan_ssl_certificate", moduleName: "ssl_certificate", label: "SSL Certificate" },
19563
+ { toolName: "scan_dns_dangling", moduleName: "dns_dangling", label: "Dangling DNS" },
19564
+ { toolName: "scan_network_reachability", moduleName: "network_reachability", label: "Network Reachability" },
19565
+ { toolName: "scan_iam_privilege_escalation", moduleName: "iam_privilege_escalation", label: "IAM Privilege Escalation" },
19566
+ { toolName: "scan_public_access_verify", moduleName: "public_access_verify", label: "Public Access Verify" },
19567
+ { toolName: "scan_log_integrity", moduleName: "log_integrity_audit", label: "Log Integrity Audit" },
19568
+ { toolName: "scan_tag_compliance", moduleName: "tag_compliance", label: "Tag Compliance" },
19569
+ { toolName: "scan_idle_resources", moduleName: "idle_resources", label: "Idle Resources" },
19570
+ { toolName: "scan_disaster_recovery", moduleName: "disaster_recovery", label: "Disaster Recovery" }
16286
19571
  ];
16287
19572
  for (const { toolName, moduleName, label } of individualScanners) {
16288
19573
  server.tool(
@@ -16307,6 +19592,85 @@ function createServer(defaultRegion) {
16307
19592
  }
16308
19593
  );
16309
19594
  }
19595
+ server.tool(
19596
+ "scan_group",
19597
+ "Run a predefined group of security scanners for a specific scenario (e.g., MLPS compliance, network defense). Read-only.",
19598
+ {
19599
+ group: external_exports.string().describe("Scan group ID: mlps3_precheck, hw_defense, exposure, pre_launch, data_encryption, least_privilege, log_integrity, disaster_recovery, idle_resources, tag_compliance, new_account_baseline, public_access_verify"),
19600
+ region: external_exports.string().optional().describe("AWS region to scan (default: server region)")
19601
+ },
19602
+ async ({ group, region }) => {
19603
+ try {
19604
+ const groupDef = SCAN_GROUPS[group];
19605
+ if (!groupDef) {
19606
+ const available = Object.keys(SCAN_GROUPS).join(", ");
19607
+ return {
19608
+ content: [{ type: "text", text: `Error: Unknown scan group "${group}". Available groups: ${available}` }],
19609
+ isError: true
19610
+ };
19611
+ }
19612
+ const r = region ?? defaultRegion;
19613
+ let selectedScanners;
19614
+ const missingModules = [];
19615
+ if (groupDef.modules.includes("ALL")) {
19616
+ selectedScanners = allScanners;
19617
+ } else {
19618
+ selectedScanners = [];
19619
+ for (const mod of groupDef.modules) {
19620
+ const scanner = scannerMap.get(mod);
19621
+ if (scanner) {
19622
+ selectedScanners.push(scanner);
19623
+ } else {
19624
+ missingModules.push(mod);
19625
+ }
19626
+ }
19627
+ }
19628
+ if (selectedScanners.length === 0) {
19629
+ return {
19630
+ content: [{ type: "text", text: `Error: No available scanners for group "${group}". Requested modules: ${groupDef.modules.join(", ")}` }],
19631
+ isError: true
19632
+ };
19633
+ }
19634
+ const result = await runAllScanners(selectedScanners, r);
19635
+ const lines = [
19636
+ `Scan group: ${groupDef.name} (${group})`,
19637
+ groupDef.description,
19638
+ "",
19639
+ summarizeResult(result)
19640
+ ];
19641
+ if (missingModules.length > 0) {
19642
+ lines.push("");
19643
+ lines.push(`Warning: ${missingModules.length} requested module(s) not available: ${missingModules.join(", ")}`);
19644
+ }
19645
+ return {
19646
+ content: [
19647
+ { type: "text", text: lines.join("\n") },
19648
+ { type: "text", text: JSON.stringify(result, null, 2) }
19649
+ ]
19650
+ };
19651
+ } catch (err) {
19652
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
19653
+ }
19654
+ }
19655
+ );
19656
+ server.tool(
19657
+ "list_groups",
19658
+ "List available scan groups with descriptions. Read-only.",
19659
+ async () => {
19660
+ try {
19661
+ const groups = Object.entries(SCAN_GROUPS).map(([id, def]) => ({
19662
+ id,
19663
+ name: def.name,
19664
+ description: def.description,
19665
+ modules: def.modules,
19666
+ reportType: def.reportType
19667
+ }));
19668
+ return { content: [{ type: "text", text: JSON.stringify(groups, null, 2) }] };
19669
+ } catch (err) {
19670
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
19671
+ }
19672
+ }
19673
+ );
16310
19674
  server.tool(
16311
19675
  "generate_report",
16312
19676
  "Generate a Markdown security report from scan results. Read-only. Does not modify any AWS resources.",
@@ -16321,6 +19685,20 @@ function createServer(defaultRegion) {
16321
19685
  }
16322
19686
  }
16323
19687
  );
19688
+ server.tool(
19689
+ "generate_mlps3_report",
19690
+ "Generate a GB/T 22239-2019 \u7B49\u4FDD\u4E09\u7EA7 compliance pre-check report from scan results. Best used with scan_group mlps3_precheck results. Read-only.",
19691
+ { scan_results: external_exports.string().describe("JSON string of FullScanResult from scan_group mlps3_precheck or scan_all") },
19692
+ async ({ scan_results }) => {
19693
+ try {
19694
+ const parsed = JSON.parse(scan_results);
19695
+ const report = generateMlps3Report(parsed);
19696
+ return { content: [{ type: "text", text: report }] };
19697
+ } catch (err) {
19698
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
19699
+ }
19700
+ }
19701
+ );
16324
19702
  server.tool(
16325
19703
  "generate_maturity_report",
16326
19704
  "Generate a security maturity assessment report from scan_all results. Requires service_detection module output. Read-only.",