aws-security-mcp 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -237,7 +237,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
237
237
  import { z } from "zod";
238
238
 
239
239
  // src/version.ts
240
- var VERSION = "0.6.0";
240
+ var VERSION = "0.6.2";
241
241
 
242
242
  // src/utils/aws-client.ts
243
243
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -502,10 +502,6 @@ import {
502
502
  ConfigServiceClient,
503
503
  DescribeConfigurationRecordersCommand
504
504
  } from "@aws-sdk/client-config-service";
505
- import {
506
- Macie2Client,
507
- GetMacieSessionCommand
508
- } from "@aws-sdk/client-macie2";
509
505
  import {
510
506
  CloudTrailClient,
511
507
  DescribeTrailsCommand
@@ -549,7 +545,7 @@ function isNotEnabled(err) {
549
545
  err.name === "DisabledException" || err.message.includes("not enabled") || err.message.includes("not subscribed");
550
546
  }
551
547
  function computeMaturityLevel(enabledCount) {
552
- if (enabledCount >= 6) return "comprehensive";
548
+ if (enabledCount >= 5) return "comprehensive";
553
549
  if (enabledCount >= 4) return "advanced";
554
550
  if (enabledCount >= 2) return "intermediate";
555
551
  return "basic";
@@ -853,53 +849,6 @@ var ServiceDetectionScanner = class {
853
849
  services.push({ name: "AWS Config", enabled: null, details: "Detection error" });
854
850
  }
855
851
  }
856
- if (region.startsWith("cn-")) {
857
- services.push({ name: "Macie", enabled: null, details: "Not available in China regions" });
858
- warnings.push("Macie is not available in AWS China regions.");
859
- } else {
860
- try {
861
- const mc = createClient(Macie2Client, region, ctx.credentials);
862
- await mc.send(new GetMacieSessionCommand({}));
863
- services.push({
864
- name: "Macie",
865
- enabled: true,
866
- details: "Sensitive data detection active"
867
- });
868
- } catch (err) {
869
- if (isAccessDenied(err)) {
870
- warnings.push("Macie: insufficient permissions to check status");
871
- services.push({ name: "Macie", enabled: null, details: "Access denied" });
872
- } else if (isNotEnabled(err)) {
873
- services.push({
874
- name: "Macie",
875
- enabled: false,
876
- recommendation: "Enable Macie to detect sensitive data in S3",
877
- freeTrialAvailable: true
878
- });
879
- findings.push(
880
- makeFinding({
881
- riskScore: 5,
882
- title: "Amazon Macie is not enabled",
883
- resourceType: "AWS::Macie::Session",
884
- resourceId: "macie",
885
- resourceArn: `arn:${partition}:macie2:${region}:${accountId}:session`,
886
- region,
887
- description: "Amazon Macie is not enabled in this region. Macie uses machine learning to discover and protect sensitive data stored in S3.",
888
- impact: "Detects sensitive data (PII, credentials, financial data) in S3 buckets. Without it, sensitive data exposure may go unnoticed.",
889
- remediationSteps: [
890
- "Open the Amazon Macie console.",
891
- "Click 'Get Started' and enable Macie.",
892
- "Macie offers a 30-day free trial for sensitive data discovery.",
893
- "Configure automated sensitive data discovery jobs."
894
- ]
895
- })
896
- );
897
- } else {
898
- warnings.push(`Macie detection failed: ${err instanceof Error ? err.message : String(err)}`);
899
- services.push({ name: "Macie", enabled: null, details: "Detection error" });
900
- }
901
- }
902
- }
903
852
  const knownServices = services.filter((s) => s.enabled !== null);
904
853
  const enabledCount = services.filter((s) => s.enabled === true).length;
905
854
  const coveragePercent = knownServices.length > 0 ? Math.round(enabledCount / knownServices.length * 100) : 0;
@@ -3046,126 +2995,37 @@ var SecurityHubFindingsScanner = class {
3046
2995
  // src/scanners/guardduty-findings.ts
3047
2996
  import {
3048
2997
  GuardDutyClient as GuardDutyClient2,
3049
- ListDetectorsCommand as ListDetectorsCommand2,
3050
- ListFindingsCommand,
3051
- GetFindingsCommand as GetFindingsCommand2
2998
+ ListDetectorsCommand as ListDetectorsCommand2
3052
2999
  } from "@aws-sdk/client-guardduty";
3053
- function gdSeverityToScore(severity) {
3054
- if (severity >= 7) return 8;
3055
- if (severity >= 4) return 5.5;
3056
- return 3;
3057
- }
3058
3000
  var GuardDutyFindingsScanner = class {
3059
3001
  moduleName = "guardduty_findings";
3060
3002
  async scan(ctx) {
3061
- const { region, partition, accountId } = ctx;
3003
+ const { region } = ctx;
3062
3004
  const startMs = Date.now();
3063
- const findings = [];
3064
3005
  const warnings = [];
3065
- let resourcesScanned = 0;
3066
3006
  try {
3067
3007
  const client = createClient(GuardDutyClient2, region, ctx.credentials);
3068
- const detectorsResp = await client.send(new ListDetectorsCommand2({}));
3069
- const detectorIds = detectorsResp.DetectorIds ?? [];
3008
+ const resp = await client.send(new ListDetectorsCommand2({}));
3009
+ const detectorIds = resp.DetectorIds ?? [];
3070
3010
  if (detectorIds.length === 0) {
3071
3011
  warnings.push("GuardDuty is not enabled in this region (no detectors found).");
3072
- return {
3073
- module: this.moduleName,
3074
- status: "success",
3075
- warnings,
3076
- resourcesScanned: 0,
3077
- findingsCount: 0,
3078
- scanTimeMs: Date.now() - startMs,
3079
- findings: []
3080
- };
3081
- }
3082
- const detectorId = detectorIds[0];
3083
- let nextToken;
3084
- const findingIds = [];
3085
- do {
3086
- const listResp = await client.send(
3087
- new ListFindingsCommand({
3088
- DetectorId: detectorId,
3089
- FindingCriteria: {
3090
- Criterion: {
3091
- "service.archived": {
3092
- Eq: ["false"]
3093
- }
3094
- }
3095
- },
3096
- MaxResults: 50,
3097
- NextToken: nextToken
3098
- })
3099
- );
3100
- findingIds.push(...listResp.FindingIds ?? []);
3101
- nextToken = listResp.NextToken;
3102
- } while (nextToken);
3103
- resourcesScanned = findingIds.length;
3104
- if (findingIds.length === 0) {
3105
- return {
3106
- module: this.moduleName,
3107
- status: "success",
3108
- warnings: warnings.length > 0 ? warnings : void 0,
3109
- resourcesScanned: 0,
3110
- findingsCount: 0,
3111
- scanTimeMs: Date.now() - startMs,
3112
- findings: []
3113
- };
3114
- }
3115
- for (let i = 0; i < findingIds.length; i += 50) {
3116
- const batch = findingIds.slice(i, i + 50);
3117
- const detailsResp = await client.send(
3118
- new GetFindingsCommand2({
3119
- DetectorId: detectorId,
3120
- FindingIds: batch
3121
- })
3122
- );
3123
- for (const gdf of detailsResp.Findings ?? []) {
3124
- const gdSeverity = gdf.Severity ?? 0;
3125
- const score = gdSeverityToScore(gdSeverity);
3126
- const severity = severityFromScore(score);
3127
- const resourceType = gdf.Resource?.ResourceType ?? "AWS::Unknown";
3128
- const resourceId = gdf.Resource?.InstanceDetails?.InstanceId ?? gdf.Resource?.AccessKeyDetails?.AccessKeyId ?? gdf.Arn ?? "unknown";
3129
- const resourceArn = gdf.Arn ?? `arn:${partition}:guardduty:${region}:${accountId}:detector/${detectorId}/finding/${gdf.Id ?? "unknown"}`;
3130
- findings.push({
3131
- severity,
3132
- title: `[GuardDuty] ${gdf.Title ?? gdf.Type ?? "Finding"}`,
3133
- resourceType,
3134
- resourceId,
3135
- resourceArn,
3136
- region: gdf.Region ?? region,
3137
- description: gdf.Description ?? gdf.Title ?? "No description",
3138
- impact: `GuardDuty threat type: ${gdf.Type ?? "unknown"} (severity ${gdSeverity})`,
3139
- riskScore: score,
3140
- remediationSteps: [
3141
- `Investigate ${gdf.Type ?? "unknown threat"}: ${gdf.Title ?? "threat detected"}`,
3142
- gdf.Description ? `Details: ${gdf.Description.substring(0, 200)}` : "",
3143
- "Isolate affected resources if compromise is confirmed.",
3144
- "Review CloudTrail logs for related suspicious activity."
3145
- ].filter(Boolean),
3146
- priority: priorityFromSeverity(severity),
3147
- module: this.moduleName,
3148
- accountId: gdf.AccountId ?? accountId
3149
- });
3150
- }
3151
3012
  }
3152
3013
  return {
3153
3014
  module: this.moduleName,
3154
3015
  status: "success",
3155
3016
  warnings: warnings.length > 0 ? warnings : void 0,
3156
- resourcesScanned,
3157
- findingsCount: findings.length,
3017
+ resourcesScanned: 0,
3018
+ findingsCount: 0,
3158
3019
  scanTimeMs: Date.now() - startMs,
3159
- findings
3020
+ findings: []
3160
3021
  };
3161
3022
  } catch (err) {
3162
3023
  const msg = err instanceof Error ? err.message : String(err);
3163
3024
  return {
3164
3025
  module: this.moduleName,
3165
3026
  status: "error",
3166
- error: `GuardDuty findings scan failed: ${msg}`,
3167
- warnings: warnings.length > 0 ? warnings : void 0,
3168
- resourcesScanned,
3027
+ error: `GuardDuty detection check failed: ${msg}`,
3028
+ resourcesScanned: 0,
3169
3029
  findingsCount: 0,
3170
3030
  scanTimeMs: Date.now() - startMs,
3171
3031
  findings: []
@@ -3177,164 +3037,59 @@ var GuardDutyFindingsScanner = class {
3177
3037
  // src/scanners/inspector-findings.ts
3178
3038
  import {
3179
3039
  Inspector2Client as Inspector2Client2,
3180
- ListFindingsCommand as ListFindingsCommand2
3040
+ BatchGetAccountStatusCommand as BatchGetAccountStatusCommand2
3181
3041
  } from "@aws-sdk/client-inspector2";
3182
- function inspectorSeverityToScore(label) {
3183
- switch (label) {
3184
- case "CRITICAL":
3185
- return 9.5;
3186
- case "HIGH":
3187
- return 8;
3188
- case "MEDIUM":
3189
- return 5.5;
3190
- case "LOW":
3191
- return 3;
3192
- case "INFORMATIONAL":
3193
- return null;
3194
- case "UNTRIAGED":
3195
- return 5.5;
3196
- default:
3197
- return null;
3198
- }
3199
- }
3200
3042
  var InspectorFindingsScanner = class {
3201
3043
  moduleName = "inspector_findings";
3202
3044
  async scan(ctx) {
3203
- const { region, partition, accountId } = ctx;
3045
+ const { region } = ctx;
3204
3046
  const startMs = Date.now();
3205
- const findings = [];
3206
3047
  const warnings = [];
3207
- let resourcesScanned = 0;
3208
3048
  try {
3209
3049
  const client = createClient(Inspector2Client2, region, ctx.credentials);
3210
- let nextToken;
3211
- const filterCriteria = {
3212
- findingStatus: [{ comparison: "EQUALS", value: "ACTIVE" }]
3213
- };
3214
- do {
3215
- const resp = await client.send(
3216
- new ListFindingsCommand2({
3217
- filterCriteria,
3218
- maxResults: 100,
3219
- nextToken
3220
- })
3221
- );
3222
- const inspFindings = resp.findings ?? [];
3223
- resourcesScanned += inspFindings.length;
3224
- for (const f of inspFindings) {
3225
- const severityLabel = f.severity ?? "INFORMATIONAL";
3226
- const score = inspectorSeverityToScore(severityLabel);
3227
- if (score === null) continue;
3228
- const severity = severityFromScore(score);
3229
- const cveId = f.packageVulnerabilityDetails?.vulnerabilityId;
3230
- const titleBase = f.title ?? "Inspector Finding";
3231
- const title = cveId ? `[${cveId}] ${titleBase}` : titleBase;
3232
- const resourceId = f.resources?.[0]?.id ?? "unknown";
3233
- const resourceType = f.resources?.[0]?.type ?? "AWS::Unknown";
3234
- const resourceArn = resourceId.startsWith("arn:") ? resourceId : `arn:${partition}:inspector2:${region}:${accountId}:finding/${f.findingArn ?? "unknown"}`;
3235
- const remediationSteps = [];
3236
- const vulnPkgs = f.packageVulnerabilityDetails?.vulnerablePackages;
3237
- if (vulnPkgs?.length) {
3238
- for (const pkg of vulnPkgs.slice(0, 3)) {
3239
- const name = pkg.name ?? "unknown-package";
3240
- const installed = pkg.version ?? "unknown";
3241
- const fixed = pkg.fixedInVersion ?? "latest";
3242
- const cveRef = cveId ? ` to fix ${cveId}` : "";
3243
- remediationSteps.push(`Update ${name} from ${installed} to ${fixed}${cveRef}`);
3244
- }
3245
- } else if (f.remediation?.recommendation?.text) {
3246
- remediationSteps.push(f.remediation.recommendation.text);
3247
- }
3248
- const genericPatterns = ["See References", "None Provided", "Review the finding"];
3249
- if (remediationSteps.length === 0 || genericPatterns.some((p) => remediationSteps[0]?.startsWith(p))) {
3250
- remediationSteps.length = 0;
3251
- const rawTitle = f.title ?? "";
3252
- if (rawTitle.includes("KB")) {
3253
- const kbMatch = rawTitle.match(/KB\d+/);
3254
- const kb = kbMatch ? kbMatch[0] : "patch";
3255
- remediationSteps.push(`Install Windows patch ${kb} via WSUS or AWS Systems Manager Patch Manager`);
3256
- remediationSteps.push(`Run: aws ssm send-command --document-name "AWS-InstallWindowsUpdates" --targets "Key=InstanceIds,Values=${resourceId}"`);
3257
- if (kbMatch) {
3258
- remediationSteps.push(`Microsoft KB article: https://support.microsoft.com/help/${kb}`);
3259
- }
3260
- } else if (rawTitle.includes("CVE-") || cveId) {
3261
- const cveMatch = rawTitle.match(/CVE-[\d-]+/);
3262
- const cve = cveMatch ? cveMatch[0] : cveId ?? "vulnerability";
3263
- remediationSteps.push(`Fix ${cve}: update the affected software package to the latest patched version`);
3264
- } else {
3265
- remediationSteps.push(`Review and remediate: ${rawTitle}`);
3266
- }
3267
- }
3268
- if (f.remediation?.recommendation?.Url) {
3269
- remediationSteps.push(`Documentation: ${f.remediation.recommendation.Url}`);
3270
- }
3271
- if (f.packageVulnerabilityDetails?.referenceUrls?.length) {
3272
- remediationSteps.push(`CVE references: ${f.packageVulnerabilityDetails.referenceUrls.slice(0, 3).join(", ")}`);
3273
- }
3274
- const description = f.description ?? titleBase;
3275
- const impact = cveId ? `Vulnerability ${cveId} \u2014 CVSS: ${f.packageVulnerabilityDetails?.cvss?.[0]?.baseScore ?? "N/A"}` : `Inspector finding type: ${f.type ?? "unknown"}`;
3276
- findings.push({
3277
- severity,
3278
- title,
3279
- resourceType,
3280
- resourceId,
3281
- resourceArn,
3282
- region,
3283
- description,
3284
- impact,
3285
- riskScore: score,
3286
- remediationSteps,
3287
- priority: priorityFromSeverity(severity),
3288
- module: this.moduleName,
3289
- accountId: f.awsAccountId ?? accountId
3290
- });
3050
+ const resp = await client.send(new BatchGetAccountStatusCommand2({ accountIds: [] }));
3051
+ const account = resp.accounts?.[0];
3052
+ if (!account || account.state?.status !== "ENABLED") {
3053
+ warnings.push("Inspector is not enabled in this region. Enable it to scan for software vulnerabilities.");
3054
+ } else {
3055
+ const rs = account.resourceState;
3056
+ const types = [
3057
+ { name: "EC2", status: rs?.ec2?.status },
3058
+ { name: "Lambda", status: rs?.lambda?.status },
3059
+ { name: "ECR", status: rs?.ecr?.status },
3060
+ { name: "Lambda Code", status: rs?.lambdaCode?.status },
3061
+ { name: "Code Repository", status: rs?.codeRepository?.status }
3062
+ ];
3063
+ const disabled = types.filter((t) => t.status && t.status !== "ENABLED");
3064
+ if (disabled.length > 0) {
3065
+ warnings.push(
3066
+ `Inspector scan types not enabled: ${disabled.map((t) => t.name).join(", ")}. Enable them for full vulnerability coverage.`
3067
+ );
3291
3068
  }
3292
- nextToken = resp.nextToken;
3293
- } while (nextToken);
3069
+ }
3294
3070
  return {
3295
3071
  module: this.moduleName,
3296
3072
  status: "success",
3297
3073
  warnings: warnings.length > 0 ? warnings : void 0,
3298
- resourcesScanned,
3299
- findingsCount: findings.length,
3074
+ resourcesScanned: 0,
3075
+ findingsCount: 0,
3300
3076
  scanTimeMs: Date.now() - startMs,
3301
- findings
3077
+ findings: []
3302
3078
  };
3303
3079
  } catch (err) {
3304
3080
  const msg = err instanceof Error ? err.message : String(err);
3305
3081
  const errName = err instanceof Error ? err.name : "";
3306
3082
  const isAccessDenied2 = errName === "AccessDeniedException" || msg.includes("AccessDeniedException");
3307
- const isNotEnabled2 = msg.includes("not enabled") || msg.includes("not subscribed");
3308
3083
  if (isAccessDenied2) {
3309
- warnings.push("Insufficient permissions to access Inspector. Grant inspector2:ListFindings to scan for vulnerabilities.");
3310
- return {
3311
- module: this.moduleName,
3312
- status: "success",
3313
- warnings,
3314
- resourcesScanned: 0,
3315
- findingsCount: 0,
3316
- scanTimeMs: Date.now() - startMs,
3317
- findings: []
3318
- };
3319
- }
3320
- if (isNotEnabled2) {
3084
+ warnings.push("Insufficient permissions to access Inspector. Grant inspector2:BatchGetAccountStatus to check enablement.");
3085
+ } else {
3321
3086
  warnings.push("Inspector is not enabled in this region. Enable it to scan for software vulnerabilities.");
3322
- return {
3323
- module: this.moduleName,
3324
- status: "success",
3325
- warnings,
3326
- resourcesScanned: 0,
3327
- findingsCount: 0,
3328
- scanTimeMs: Date.now() - startMs,
3329
- findings: []
3330
- };
3331
3087
  }
3332
3088
  return {
3333
3089
  module: this.moduleName,
3334
- status: "error",
3335
- error: `Inspector findings scan failed: ${msg}`,
3336
- warnings: warnings.length > 0 ? warnings : void 0,
3337
- resourcesScanned,
3090
+ status: "success",
3091
+ warnings,
3092
+ resourcesScanned: 0,
3338
3093
  findingsCount: 0,
3339
3094
  scanTimeMs: Date.now() - startMs,
3340
3095
  findings: []
@@ -3507,128 +3262,29 @@ var TrustedAdvisorFindingsScanner = class {
3507
3262
  // src/scanners/config-rules-findings.ts
3508
3263
  import {
3509
3264
  ConfigServiceClient as ConfigServiceClient2,
3510
- DescribeComplianceByConfigRuleCommand,
3511
- GetComplianceDetailsByConfigRuleCommand
3265
+ DescribeConfigurationRecordersCommand as DescribeConfigurationRecordersCommand2
3512
3266
  } from "@aws-sdk/client-config-service";
3513
- var SECURITY_RULE_PATTERNS = [
3514
- "securitygroup",
3515
- "security-group",
3516
- "encryption",
3517
- "encrypted",
3518
- "public",
3519
- "unrestricted",
3520
- "mfa",
3521
- "password",
3522
- "access-key",
3523
- "root",
3524
- "admin",
3525
- "logging",
3526
- "cloudtrail",
3527
- "iam",
3528
- "kms",
3529
- "ssl",
3530
- "tls",
3531
- "vpc-flow",
3532
- "guardduty",
3533
- "securityhub"
3534
- ];
3535
- function ruleIsSecurityRelated(ruleName) {
3536
- const lower = ruleName.toLowerCase();
3537
- return SECURITY_RULE_PATTERNS.some((pat) => lower.includes(pat));
3538
- }
3539
3267
  var ConfigRulesFindingsScanner = class {
3540
3268
  moduleName = "config_rules_findings";
3541
3269
  async scan(ctx) {
3542
- const { region, partition, accountId } = ctx;
3270
+ const { region } = ctx;
3543
3271
  const startMs = Date.now();
3544
- const findings = [];
3545
3272
  const warnings = [];
3546
- let resourcesScanned = 0;
3547
3273
  try {
3548
3274
  const client = createClient(ConfigServiceClient2, region, ctx.credentials);
3549
- let nextToken;
3550
- const nonCompliantRules = [];
3551
- do {
3552
- const resp = await client.send(
3553
- new DescribeComplianceByConfigRuleCommand({ NextToken: nextToken })
3554
- );
3555
- for (const rule of resp.ComplianceByConfigRules ?? []) {
3556
- resourcesScanned++;
3557
- if (rule.Compliance?.ComplianceType === "NON_COMPLIANT") {
3558
- nonCompliantRules.push(rule);
3559
- }
3560
- }
3561
- nextToken = resp.NextToken;
3562
- } while (nextToken);
3563
- if (resourcesScanned === 0) {
3564
- warnings.push("AWS Config is not enabled in this region or no Config Rules are defined.");
3565
- return {
3566
- module: this.moduleName,
3567
- status: "success",
3568
- warnings,
3569
- resourcesScanned: 0,
3570
- findingsCount: 0,
3571
- scanTimeMs: Date.now() - startMs,
3572
- findings: []
3573
- };
3574
- }
3575
- for (const rule of nonCompliantRules) {
3576
- const ruleName = rule.ConfigRuleName ?? "unknown";
3577
- try {
3578
- let detailToken;
3579
- do {
3580
- const detailResp = await client.send(
3581
- new GetComplianceDetailsByConfigRuleCommand({
3582
- ConfigRuleName: ruleName,
3583
- ComplianceTypes: ["NON_COMPLIANT"],
3584
- NextToken: detailToken
3585
- })
3586
- );
3587
- for (const evalResult of detailResp.EvaluationResults ?? []) {
3588
- const qualifier = evalResult.EvaluationResultIdentifier?.EvaluationResultQualifier;
3589
- const resourceType = qualifier?.ResourceType ?? "AWS::Unknown";
3590
- const resourceId = qualifier?.ResourceId ?? "unknown";
3591
- const annotation = evalResult.Annotation;
3592
- const isSecurityRule = ruleIsSecurityRelated(ruleName);
3593
- const riskScore = isSecurityRule ? 7.5 : 5.5;
3594
- const severity = severityFromScore(riskScore);
3595
- const descParts = [`Config Rule: ${ruleName}`, `Resource Type: ${resourceType}`];
3596
- if (annotation) descParts.push(`Annotation: ${annotation}`);
3597
- findings.push({
3598
- severity,
3599
- title: `Config Rule: ${ruleName} - ${resourceType}/${resourceId} Non-Compliant`,
3600
- resourceType,
3601
- resourceId,
3602
- resourceArn: resourceId,
3603
- region,
3604
- description: descParts.join(". "),
3605
- impact: `Resource is non-compliant with Config Rule: ${ruleName}`,
3606
- riskScore,
3607
- remediationSteps: [
3608
- `Fix Config Rule violation: ${ruleName}`,
3609
- annotation ? `Details: ${annotation}` : "",
3610
- `Resource: ${resourceType}/${resourceId}`
3611
- ].filter(Boolean),
3612
- priority: priorityFromSeverity(severity),
3613
- module: this.moduleName,
3614
- accountId
3615
- });
3616
- }
3617
- detailToken = detailResp.NextToken;
3618
- } while (detailToken);
3619
- } catch (detailErr) {
3620
- const msg = detailErr instanceof Error ? detailErr.message : String(detailErr);
3621
- warnings.push(`Failed to get details for rule ${ruleName}: ${msg}`);
3622
- }
3275
+ const resp = await client.send(new DescribeConfigurationRecordersCommand2({}));
3276
+ const recorders = resp.ConfigurationRecorders ?? [];
3277
+ if (recorders.length === 0) {
3278
+ warnings.push("AWS Config is not enabled in this region.");
3623
3279
  }
3624
3280
  return {
3625
3281
  module: this.moduleName,
3626
3282
  status: "success",
3627
3283
  warnings: warnings.length > 0 ? warnings : void 0,
3628
- resourcesScanned,
3629
- findingsCount: findings.length,
3284
+ resourcesScanned: 0,
3285
+ findingsCount: 0,
3630
3286
  scanTimeMs: Date.now() - startMs,
3631
- findings
3287
+ findings: []
3632
3288
  };
3633
3289
  } catch (err) {
3634
3290
  const msg = err instanceof Error ? err.message : String(err);
@@ -3647,9 +3303,8 @@ var ConfigRulesFindingsScanner = class {
3647
3303
  return {
3648
3304
  module: this.moduleName,
3649
3305
  status: "error",
3650
- error: `Config Rules scan failed: ${msg}`,
3651
- warnings: warnings.length > 0 ? warnings : void 0,
3652
- resourcesScanned,
3306
+ error: `Config Rules detection check failed: ${msg}`,
3307
+ resourcesScanned: 0,
3653
3308
  findingsCount: 0,
3654
3309
  scanTimeMs: Date.now() - startMs,
3655
3310
  findings: []
@@ -3661,146 +3316,50 @@ var ConfigRulesFindingsScanner = class {
3661
3316
  // src/scanners/access-analyzer-findings.ts
3662
3317
  import {
3663
3318
  AccessAnalyzerClient,
3664
- ListAnalyzersCommand,
3665
- ListFindingsV2Command
3319
+ ListAnalyzersCommand
3666
3320
  } from "@aws-sdk/client-accessanalyzer";
3667
- function findingTypeToScore(findingType) {
3668
- const ft = findingType;
3669
- switch (ft) {
3670
- case "ExternalAccess":
3671
- return 8;
3672
- case "UnusedIAMRole":
3673
- case "UnusedIAMUserAccessKey":
3674
- case "UnusedIAMUserPassword":
3675
- return 5.5;
3676
- case "UnusedPermission":
3677
- return 3;
3678
- default:
3679
- return 5.5;
3680
- }
3681
- }
3682
- var UNUSED_FINDING_TYPES = /* @__PURE__ */ new Set([
3683
- "UnusedIAMRole",
3684
- "UnusedIAMUserAccessKey",
3685
- "UnusedIAMUserPassword",
3686
- "UnusedPermission"
3687
- ]);
3688
- function isSecurityRelevant(findingType) {
3689
- const ft = findingType;
3690
- return ft === "ExternalAccess" || UNUSED_FINDING_TYPES.has(ft ?? "");
3691
- }
3692
- function isExternalAccess(findingType) {
3693
- return findingType === "ExternalAccess";
3694
- }
3695
3321
  var AccessAnalyzerFindingsScanner = class {
3696
3322
  moduleName = "access_analyzer_findings";
3697
3323
  async scan(ctx) {
3698
- const { region, partition, accountId } = ctx;
3324
+ const { region } = ctx;
3699
3325
  const startMs = Date.now();
3700
- const findings = [];
3701
3326
  const warnings = [];
3702
- let resourcesScanned = 0;
3703
3327
  try {
3704
3328
  const client = createClient(AccessAnalyzerClient, region, ctx.credentials);
3705
3329
  let analyzerToken;
3706
- const analyzers = [];
3330
+ let hasActiveAnalyzer = false;
3707
3331
  do {
3708
3332
  const resp = await client.send(
3709
3333
  new ListAnalyzersCommand({ nextToken: analyzerToken })
3710
3334
  );
3711
3335
  for (const analyzer of resp.analyzers ?? []) {
3712
3336
  if (analyzer.status === "ACTIVE") {
3713
- analyzers.push(analyzer);
3337
+ hasActiveAnalyzer = true;
3338
+ break;
3714
3339
  }
3715
3340
  }
3341
+ if (hasActiveAnalyzer) break;
3716
3342
  analyzerToken = resp.nextToken;
3717
3343
  } while (analyzerToken);
3718
- if (analyzers.length === 0) {
3344
+ if (!hasActiveAnalyzer) {
3719
3345
  warnings.push("No IAM Access Analyzer found. Create an analyzer to detect external access to your resources.");
3720
- return {
3721
- module: this.moduleName,
3722
- status: "success",
3723
- warnings,
3724
- resourcesScanned: 0,
3725
- findingsCount: 0,
3726
- scanTimeMs: Date.now() - startMs,
3727
- findings: []
3728
- };
3729
- }
3730
- for (const analyzer of analyzers) {
3731
- const analyzerArn = analyzer.arn ?? "unknown";
3732
- let findingToken;
3733
- do {
3734
- const listResp = await client.send(
3735
- new ListFindingsV2Command({
3736
- analyzerArn,
3737
- filter: {
3738
- status: { eq: ["ACTIVE"] }
3739
- },
3740
- nextToken: findingToken
3741
- })
3742
- );
3743
- for (const aaf of listResp.findings ?? []) {
3744
- if (!isSecurityRelevant(aaf.findingType)) {
3745
- continue;
3746
- }
3747
- resourcesScanned++;
3748
- const score = findingTypeToScore(aaf.findingType);
3749
- const severity = severityFromScore(score);
3750
- const resourceArn = aaf.resource ?? "unknown";
3751
- const resourceType = aaf.resourceType ?? "AWS::Unknown";
3752
- const resourceId = resourceArn.split("/").pop() ?? resourceArn.split(":").pop() ?? "unknown";
3753
- const external = isExternalAccess(aaf.findingType);
3754
- const descParts = [`Resource Type: ${resourceType}`];
3755
- if (aaf.resourceOwnerAccount) descParts.push(`Owner Account: ${aaf.resourceOwnerAccount}`);
3756
- if (aaf.findingType) descParts.push(`Finding Type: ${aaf.findingType}`);
3757
- const title = buildFindingTitle(aaf);
3758
- const impact = external ? `Resource is accessible from outside the account. Type: ${aaf.findingType ?? "unknown"}` : `Unused access detected \u2014 review and remove to follow least-privilege. Type: ${aaf.findingType ?? "unknown"}`;
3759
- const remediationSteps = external ? [
3760
- `Restrict external access on ${resourceType} ${resourceId}`,
3761
- "Remove or narrow the resource policy to eliminate unintended external access.",
3762
- `Resource ARN: ${resourceArn}`
3763
- ] : [
3764
- `Remove unused access on ${resourceType} ${resourceId}`,
3765
- "Remove unused permissions, roles, or credentials to follow least-privilege.",
3766
- `Resource ARN: ${resourceArn}`
3767
- ];
3768
- findings.push({
3769
- severity,
3770
- title,
3771
- resourceType: mapResourceType(resourceType),
3772
- resourceId,
3773
- resourceArn,
3774
- region,
3775
- description: descParts.join(". "),
3776
- impact,
3777
- riskScore: score,
3778
- remediationSteps,
3779
- priority: priorityFromSeverity(severity),
3780
- module: this.moduleName,
3781
- accountId: aaf.resourceOwnerAccount ?? accountId
3782
- });
3783
- }
3784
- findingToken = listResp.nextToken;
3785
- } while (findingToken);
3786
3346
  }
3787
3347
  return {
3788
3348
  module: this.moduleName,
3789
3349
  status: "success",
3790
3350
  warnings: warnings.length > 0 ? warnings : void 0,
3791
- resourcesScanned,
3792
- findingsCount: findings.length,
3351
+ resourcesScanned: 0,
3352
+ findingsCount: 0,
3793
3353
  scanTimeMs: Date.now() - startMs,
3794
- findings
3354
+ findings: []
3795
3355
  };
3796
3356
  } catch (err) {
3797
3357
  const msg = err instanceof Error ? err.message : String(err);
3798
3358
  return {
3799
3359
  module: this.moduleName,
3800
3360
  status: "error",
3801
- error: `Access Analyzer scan failed: ${msg}`,
3802
- warnings: warnings.length > 0 ? warnings : void 0,
3803
- resourcesScanned,
3361
+ error: `Access Analyzer detection check failed: ${msg}`,
3362
+ resourcesScanned: 0,
3804
3363
  findingsCount: 0,
3805
3364
  scanTimeMs: Date.now() - startMs,
3806
3365
  findings: []
@@ -3808,29 +3367,6 @@ var AccessAnalyzerFindingsScanner = class {
3808
3367
  }
3809
3368
  }
3810
3369
  };
3811
- function buildFindingTitle(finding) {
3812
- const resourceType = finding.resourceType ?? "Resource";
3813
- const resource = finding.resource ? finding.resource.split("/").pop() ?? finding.resource.split(":").pop() ?? finding.resource : "unknown";
3814
- const label = isExternalAccess(finding.findingType) ? "external access detected" : "unused access detected";
3815
- return `[Access Analyzer] ${resourceType} ${resource} \u2014 ${label}`;
3816
- }
3817
- function mapResourceType(aaType) {
3818
- const mapping = {
3819
- "AWS::S3::Bucket": "AWS::S3::Bucket",
3820
- "AWS::IAM::Role": "AWS::IAM::Role",
3821
- "AWS::SQS::Queue": "AWS::SQS::Queue",
3822
- "AWS::Lambda::Function": "AWS::Lambda::Function",
3823
- "AWS::Lambda::LayerVersion": "AWS::Lambda::LayerVersion",
3824
- "AWS::KMS::Key": "AWS::KMS::Key",
3825
- "AWS::SecretsManager::Secret": "AWS::SecretsManager::Secret",
3826
- "AWS::SNS::Topic": "AWS::SNS::Topic",
3827
- "AWS::EFS::FileSystem": "AWS::EFS::FileSystem",
3828
- "AWS::RDS::DBSnapshot": "AWS::RDS::DBSnapshot",
3829
- "AWS::RDS::DBClusterSnapshot": "AWS::RDS::DBClusterSnapshot",
3830
- "AWS::ECR::Repository": "AWS::ECR::Repository"
3831
- };
3832
- return mapping[aaType] ?? aaType;
3833
- }
3834
3370
 
3835
3371
  // src/scanners/patch-compliance-findings.ts
3836
3372
  import {
@@ -4368,6 +3904,44 @@ var zhI18n = {
4368
3904
  "\u5B89\u5168\u8BA1\u7B97\u73AF\u5883": "\u56DB\u3001\u5B89\u5168\u8BA1\u7B97\u73AF\u5883",
4369
3905
  "\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3": "\u4E94\u3001\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3"
4370
3906
  },
3907
+ // Module display names
3908
+ moduleNames: {
3909
+ service_detection: "\u5B89\u5168\u670D\u52A1\u68C0\u6D4B",
3910
+ secret_exposure: "\u5BC6\u94A5\u66B4\u9732",
3911
+ ssl_certificate: "SSL \u8BC1\u4E66",
3912
+ dns_dangling: "\u60AC\u6302 DNS",
3913
+ network_reachability: "\u7F51\u7EDC\u53EF\u8FBE\u6027",
3914
+ iam_privilege_escalation: "IAM \u63D0\u6743\u5206\u6790",
3915
+ public_access_verify: "\u516C\u7F51\u8BBF\u95EE\u9A8C\u8BC1",
3916
+ tag_compliance: "\u6807\u7B7E\u5408\u89C4",
3917
+ idle_resources: "\u95F2\u7F6E\u8D44\u6E90",
3918
+ disaster_recovery: "\u707E\u5907\u8BC4\u4F30",
3919
+ security_hub_findings: "Security Hub",
3920
+ guardduty_findings: "GuardDuty",
3921
+ inspector_findings: "Inspector",
3922
+ trusted_advisor_findings: "Trusted Advisor",
3923
+ config_rules_findings: "Config Rules",
3924
+ access_analyzer_findings: "Access Analyzer",
3925
+ patch_compliance_findings: "\u8865\u4E01\u5408\u89C4",
3926
+ imdsv2_enforcement: "IMDSv2 \u5F3A\u5236",
3927
+ waf_coverage: "WAF \u8986\u76D6",
3928
+ // Security Hub sub-categories
3929
+ "sh:FSBP": "\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5",
3930
+ "sh:Inspector": "\u8F6F\u4EF6\u6F0F\u6D1E",
3931
+ "sh:GuardDuty": "\u5A01\u80C1\u68C0\u6D4B",
3932
+ "sh:Config": "\u914D\u7F6E\u5408\u89C4",
3933
+ "sh:Access Analyzer": "\u5916\u90E8\u8BBF\u95EE",
3934
+ "sh:Other": "\u5176\u4ED6\u5B89\u5168\u53D1\u73B0"
3935
+ },
3936
+ // Security Hub sub-categories
3937
+ securityHubSubCategories: {
3938
+ FSBP: { label: "\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5" },
3939
+ Inspector: { label: "\u8F6F\u4EF6\u6F0F\u6D1E" },
3940
+ GuardDuty: { label: "\u5A01\u80C1\u68C0\u6D4B" },
3941
+ Config: { label: "\u914D\u7F6E\u5408\u89C4" },
3942
+ "Access Analyzer": { label: "\u5916\u90E8\u8BBF\u95EE" },
3943
+ Other: { label: "\u5176\u4ED6\u5B89\u5168\u53D1\u73B0" }
3944
+ },
4371
3945
  // Service Recommendations
4372
3946
  notEnabled: "\u672A\u542F\u7528",
4373
3947
  serviceRecommendations: {
@@ -4600,6 +4174,44 @@ var enI18n = {
4600
4174
  "\u5B89\u5168\u8BA1\u7B97\u73AF\u5883": "IV. Computing Environment Security",
4601
4175
  "\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3": "V. Security Management Center"
4602
4176
  },
4177
+ // Module display names
4178
+ moduleNames: {
4179
+ service_detection: "Security Service Detection",
4180
+ secret_exposure: "Secret Exposure",
4181
+ ssl_certificate: "SSL Certificate",
4182
+ dns_dangling: "Dangling DNS",
4183
+ network_reachability: "Network Reachability",
4184
+ iam_privilege_escalation: "IAM Privilege Escalation",
4185
+ public_access_verify: "Public Access Verification",
4186
+ tag_compliance: "Tag Compliance",
4187
+ idle_resources: "Idle Resources",
4188
+ disaster_recovery: "Disaster Recovery",
4189
+ security_hub_findings: "Security Hub",
4190
+ guardduty_findings: "GuardDuty",
4191
+ inspector_findings: "Inspector",
4192
+ trusted_advisor_findings: "Trusted Advisor",
4193
+ config_rules_findings: "Config Rules",
4194
+ access_analyzer_findings: "Access Analyzer",
4195
+ patch_compliance_findings: "Patch Compliance",
4196
+ imdsv2_enforcement: "IMDSv2 Enforcement",
4197
+ waf_coverage: "WAF Coverage",
4198
+ // Security Hub sub-categories
4199
+ "sh:FSBP": "Security Best Practices",
4200
+ "sh:Inspector": "Software Vulnerabilities",
4201
+ "sh:GuardDuty": "Threat Detection",
4202
+ "sh:Config": "Configuration Compliance",
4203
+ "sh:Access Analyzer": "External Access",
4204
+ "sh:Other": "Other Security Findings"
4205
+ },
4206
+ // Security Hub sub-categories
4207
+ securityHubSubCategories: {
4208
+ FSBP: { label: "Security Best Practices" },
4209
+ Inspector: { label: "Software Vulnerabilities" },
4210
+ GuardDuty: { label: "Threat Detection" },
4211
+ Config: { label: "Configuration Compliance" },
4212
+ "Access Analyzer": { label: "External Access" },
4213
+ Other: { label: "Other Security Findings" }
4214
+ },
4603
4215
  // Service Recommendations
4604
4216
  notEnabled: "Not Enabled",
4605
4217
  serviceRecommendations: {
@@ -4804,7 +4416,7 @@ function generateMarkdownReport(scanResults, lang) {
4804
4416
  for (const m of modules) {
4805
4417
  const status = m.status === "success" ? "\u2705" : "\u274C";
4806
4418
  lines.push(
4807
- `| ${m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
4419
+ `| ${t.moduleNames[m.module] ?? m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
4808
4420
  );
4809
4421
  }
4810
4422
  lines.push("");
@@ -7647,6 +7259,22 @@ var SEV_COLOR = {
7647
7259
  LOW: "#22c55e"
7648
7260
  };
7649
7261
  var SEVERITY_ORDER2 = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
7262
+ function getRecommendationTemplate(rem) {
7263
+ return rem.replace(/\b(i-[0-9a-f]+)\b/g, "{instance}").replace(/\b(vol-[0-9a-f]+)\b/g, "{volume}").replace(/\b(sg-[0-9a-f]+)\b/g, "{sg}").replace(/\b(eipalloc-[0-9a-f]+)\b/g, "{eip}").replace(/\b(arn:aws[-\w]*:[^"\s]+)\b/g, "{arn}").replace(/"[^"]+"/g, "{name}").replace(/bucket \S+/g, "bucket {name}").replace(/instance \S+/g, "instance {id}").replace(/volume \S+/g, "volume {id}").replace(/rule \S+/g, "rule {name}");
7264
+ }
7265
+ function getSecurityHubSource(finding) {
7266
+ const impact = finding.impact ?? "";
7267
+ const match = impact.match(/^Source:\s*([^(]+)/);
7268
+ if (!match) return "Other";
7269
+ const product = match[1].trim();
7270
+ if (product === "Security Hub" || product.includes("Foundational")) return "FSBP";
7271
+ if (product === "Inspector" || product.includes("Inspector")) return "Inspector";
7272
+ if (product === "GuardDuty" || product.includes("GuardDuty")) return "GuardDuty";
7273
+ if (product === "Config" || product.includes("Config")) return "Config";
7274
+ if (product === "IAM Access Analyzer" || product.includes("Access Analyzer")) return "Access Analyzer";
7275
+ return "Other";
7276
+ }
7277
+ var SECURITY_HUB_SUB_CAT_ORDER = ["FSBP", "Inspector", "GuardDuty", "Config", "Access Analyzer", "Other"];
7650
7278
  function scoreColor(score) {
7651
7279
  if (score >= 80) return "#22c55e";
7652
7280
  if (score >= 50) return "#eab308";
@@ -8024,6 +7652,47 @@ function generateHtmlReport(scanResults, history, lang) {
8024
7652
  const allFindings = modules.flatMap(
8025
7653
  (m) => m.findings.map((f) => ({ ...f, module: f.module ?? m.module }))
8026
7654
  );
7655
+ const shModule = modules.find((m) => m.module === "security_hub_findings");
7656
+ const shSubCats = [];
7657
+ if (shModule && shModule.findingsCount > 0) {
7658
+ const catMap = {};
7659
+ for (const f of shModule.findings) {
7660
+ const cat = getSecurityHubSource(f);
7661
+ if (!catMap[cat]) catMap[cat] = [];
7662
+ catMap[cat].push(f);
7663
+ }
7664
+ for (const cat of SECURITY_HUB_SUB_CAT_ORDER) {
7665
+ const catFindings = catMap[cat];
7666
+ if (catFindings && catFindings.length > 0) {
7667
+ const meta = t.securityHubSubCategories[cat];
7668
+ const shLabel = t.moduleNames[`sh:${cat}`] ?? meta?.label ?? cat;
7669
+ shSubCats.push({
7670
+ key: cat,
7671
+ label: shLabel,
7672
+ count: catFindings.length,
7673
+ findings: catFindings
7674
+ });
7675
+ }
7676
+ }
7677
+ }
7678
+ const DETECTION_ONLY_MODULES = /* @__PURE__ */ new Set([
7679
+ "guardduty_findings",
7680
+ "inspector_findings",
7681
+ "config_rules_findings",
7682
+ "access_analyzer_findings"
7683
+ ]);
7684
+ const barChartModules = modules.flatMap((m) => {
7685
+ if (DETECTION_ONLY_MODULES.has(m.module)) return [];
7686
+ if (m.module === "security_hub_findings" && shSubCats.length > 0) {
7687
+ return shSubCats.map((sc) => ({
7688
+ ...m,
7689
+ module: t.moduleNames[`sh:${sc.key}`] ?? sc.key,
7690
+ findingsCount: sc.count,
7691
+ findings: sc.findings
7692
+ }));
7693
+ }
7694
+ return [{ ...m, module: t.moduleNames[m.module] ?? m.module }];
7695
+ });
8027
7696
  let top5Html = "";
8028
7697
  if (allFindings.length > 0) {
8029
7698
  const top5 = [...allFindings].sort((a, b) => b.riskScore - a.riskScore).slice(0, 5);
@@ -8089,34 +7758,51 @@ ${rest}
8089
7758
  if (!moduleMap.has(mod)) moduleMap.set(mod, []);
8090
7759
  moduleMap.get(mod).push(f);
8091
7760
  }
8092
- const moduleEntries = [...moduleMap.entries()].sort((a, b) => {
7761
+ const expandedEntries = [];
7762
+ for (const [mod, findings] of moduleMap.entries()) {
7763
+ if (DETECTION_ONLY_MODULES.has(mod)) continue;
7764
+ if (mod === "security_hub_findings" && shSubCats.length > 0) {
7765
+ for (const sc of shSubCats) {
7766
+ expandedEntries.push([sc.key, sc.findings, sc.label]);
7767
+ }
7768
+ } else {
7769
+ expandedEntries.push([mod, findings, null]);
7770
+ }
7771
+ }
7772
+ const moduleEntries = expandedEntries.sort((a, b) => {
8093
7773
  const aHasCritHigh = a[1].some((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
8094
7774
  const bHasCritHigh = b[1].some((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
8095
7775
  if (aHasCritHigh !== bHasCritHigh) return aHasCritHigh ? -1 : 1;
8096
7776
  return b[1].length - a[1].length;
8097
7777
  });
8098
- findingsHtml = moduleEntries.map(([modName, modFindings]) => {
8099
- const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
8100
- for (const f of modFindings) sevCounts[f.severity]++;
8101
- const badges = SEVERITY_ORDER2.filter((sev) => sevCounts[sev] > 0).map((sev) => `<span class="badge badge-${sev.toLowerCase()}">${sevCounts[sev]} ${sev.charAt(0) + sev.slice(1).toLowerCase()}</span>`).join(" ");
8102
- const sevGroups = SEVERITY_ORDER2.map((sev) => {
8103
- const findings = modFindings.filter((f) => f.severity === sev);
8104
- if (findings.length === 0) return "";
8105
- findings.sort((a, b) => b.riskScore - a.riskScore);
7778
+ const renderSeverityGroups = (findings) => {
7779
+ return SEVERITY_ORDER2.map((sev) => {
7780
+ const sevFindings = findings.filter((f) => f.severity === sev);
7781
+ if (sevFindings.length === 0) return "";
7782
+ sevFindings.sort((a, b) => b.riskScore - a.riskScore);
8106
7783
  const emoji = SEV_EMOJI[sev] ?? "";
8107
7784
  const label = sev.charAt(0) + sev.slice(1).toLowerCase();
8108
7785
  return `<details class="severity-group-fold">
8109
- <summary><h4>${emoji} ${label} (${findings.length})</h4></summary>
8110
- ${renderCards(findings)}
7786
+ <summary><h4>${emoji} ${label} (${sevFindings.length})</h4></summary>
7787
+ ${renderCards(sevFindings)}
8111
7788
  </details>`;
8112
7789
  }).filter(Boolean).join("\n");
7790
+ };
7791
+ const renderModuleBadges = (findings) => {
7792
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
7793
+ for (const f of findings) sevCounts[f.severity]++;
7794
+ return SEVERITY_ORDER2.filter((sev) => sevCounts[sev] > 0).map((sev) => `<span class="badge badge-${sev.toLowerCase()}">${sevCounts[sev]} ${sev.charAt(0) + sev.slice(1).toLowerCase()}</span>`).join(" ");
7795
+ };
7796
+ findingsHtml = moduleEntries.map(([modName, modFindings, subCatLabel]) => {
7797
+ const badges = renderModuleBadges(modFindings);
7798
+ const displayName = subCatLabel ?? (t.moduleNames[modName] ?? modName);
8113
7799
  return `<details class="module-fold">
8114
7800
  <summary>
8115
- <h3>&#128274; ${esc(modName)} (${modFindings.length})</h3>
7801
+ <h3>&#128274; ${esc(displayName)} (${modFindings.length})</h3>
8116
7802
  <span class="module-badges">${badges}</span>
8117
7803
  </summary>
8118
7804
  <div class="module-body">
8119
- ${sevGroups}
7805
+ ${renderSeverityGroups(modFindings)}
8120
7806
  </div>
8121
7807
  </details>`;
8122
7808
  }).join("\n");
@@ -8136,8 +7822,29 @@ ${rest}
8136
7822
  </div>
8137
7823
  </section>`;
8138
7824
  }
8139
- const statsRows = modules.map(
8140
- (m) => `<tr><td>${esc(m.module)}</td><td>${m.resourcesScanned}</td><td>${m.findingsCount}</td><td>${m.status === "success" ? "&#10003;" : "&#10007;"}</td></tr>`
7825
+ const isModuleDisabled = (m) => {
7826
+ if (!m.warnings?.length) return void 0;
7827
+ const w = m.warnings.find(
7828
+ (w2) => SERVICE_NOT_ENABLED_PATTERNS.some((p) => w2.includes(p))
7829
+ );
7830
+ return w;
7831
+ };
7832
+ const statsRows = modules.flatMap(
7833
+ (m) => {
7834
+ if (DETECTION_ONLY_MODULES.has(m.module)) return [];
7835
+ if (m.module === "security_hub_findings" && shSubCats.length > 0) {
7836
+ return shSubCats.map(
7837
+ (sc) => `<tr><td>${esc(sc.label)}</td><td>${m.resourcesScanned}</td><td>${sc.count}</td><td>&#10003;</td></tr>`
7838
+ );
7839
+ }
7840
+ const disabledWarning = isModuleDisabled(m);
7841
+ if (disabledWarning) {
7842
+ const rec = t.serviceRecommendations[m.module];
7843
+ const reason = rec ? rec.action : disabledWarning;
7844
+ return [`<tr><td>${esc(t.moduleNames[m.module] ?? m.module)}</td><td>-</td><td>-</td><td style="color:#eab308">&#9888; ${esc(reason)}</td></tr>`];
7845
+ }
7846
+ return [`<tr><td>${esc(t.moduleNames[m.module] ?? m.module)}</td><td>${m.resourcesScanned}</td><td>${m.findingsCount}</td><td>${m.status === "success" ? "&#10003;" : "&#10007;"}</td></tr>`];
7847
+ }
8141
7848
  ).join("\n");
8142
7849
  let recsHtml = "";
8143
7850
  if (summary.totalFindings > 0) {
@@ -8145,6 +7852,9 @@ ${rest}
8145
7852
  const kbPatches = [];
8146
7853
  let kbSeverity = "LOW";
8147
7854
  let kbUrl;
7855
+ const cveList = [];
7856
+ let cveSeverity = "LOW";
7857
+ let cveUrl;
8148
7858
  const genericPatterns = ["See References", "None Provided", "Review the finding", "Review and remediate."];
8149
7859
  for (const f of allFindings) {
8150
7860
  const rem = f.remediationSteps[0] ?? "Review and remediate.";
@@ -8157,6 +7867,13 @@ ${rest}
8157
7867
  if (!kbUrl && url) kbUrl = url;
8158
7868
  continue;
8159
7869
  }
7870
+ const cveMatch = f.title.match(/CVE-[\d-]+/);
7871
+ if (cveMatch && (f.module === "security_hub_findings" || f.module === "inspector_findings")) {
7872
+ cveList.push(cveMatch[0]);
7873
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(cveSeverity)) cveSeverity = f.severity;
7874
+ if (!cveUrl && url) cveUrl = url;
7875
+ continue;
7876
+ }
8160
7877
  if (f.module === "security_hub_findings") {
8161
7878
  const controlMatch = f.title.match(/^([A-Z][A-Za-z0-9]*\.\d+)\s/);
8162
7879
  if (controlMatch) {
@@ -8173,6 +7890,23 @@ ${rest}
8173
7890
  continue;
8174
7891
  }
8175
7892
  }
7893
+ if (f.module !== "security_hub_findings" && f.module !== "inspector_findings") {
7894
+ const template = getRecommendationTemplate(rem);
7895
+ if (template !== rem) {
7896
+ const templateKey = `tmpl:${f.module}:${template}`;
7897
+ const existingTmpl = recMap.get(templateKey);
7898
+ if (existingTmpl) {
7899
+ existingTmpl.count++;
7900
+ if (!existingTmpl.url && url) existingTmpl.url = url;
7901
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(existingTmpl.severity)) {
7902
+ existingTmpl.severity = f.severity;
7903
+ }
7904
+ continue;
7905
+ }
7906
+ recMap.set(templateKey, { text: rem, severity: f.severity, count: 1, url });
7907
+ continue;
7908
+ }
7909
+ }
8176
7910
  const existing = recMap.get(rem);
8177
7911
  if (existing) {
8178
7912
  existing.count++;
@@ -8189,8 +7923,14 @@ ${rest}
8189
7923
  const kbList = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
8190
7924
  recMap.set("__kb__", { text: t.installWindowsPatches(unique.length, kbList), severity: kbSeverity, count: 1, url: kbUrl });
8191
7925
  }
7926
+ if (cveList.length > 0) {
7927
+ const unique = [...new Set(cveList)];
7928
+ const cveDisplay = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
7929
+ const cveText = (lang ?? "zh") === "zh" ? `\u4FEE\u590D ${unique.length} \u4E2A\u8F6F\u4EF6\u6F0F\u6D1E (${cveDisplay})\uFF0C\u66F4\u65B0\u53D7\u5F71\u54CD\u7684\u8F6F\u4EF6\u5305\u5230\u6700\u65B0\u7248\u672C` : `Fix ${unique.length} software vulnerabilities (${cveDisplay}) \u2014 update affected packages to latest patched versions`;
7930
+ recMap.set("__cve__", { text: cveText, severity: cveSeverity, count: 1, url: cveUrl });
7931
+ }
8192
7932
  for (const [key, rec] of recMap) {
8193
- if (key.startsWith("ctrl:") && rec.count > 1) {
7933
+ if ((key.startsWith("ctrl:") || key.startsWith("tmpl:")) && rec.count > 1) {
8194
7934
  rec.text += ` \u2014 ${t.affectedResources(rec.count)}`;
8195
7935
  rec.count = 1;
8196
7936
  }
@@ -8257,7 +7997,7 @@ ${remaining.map(renderRec).join("\n")}
8257
7997
  </div>
8258
7998
  <div class="chart-box">
8259
7999
  <div class="chart-title">${esc(t.findingsByModule)}</div>
8260
- ${barChart(modules, t.allModulesClean)}
8000
+ ${barChart(barChartModules, t.allModulesClean)}
8261
8001
  </div>
8262
8002
  </section>
8263
8003
 
@@ -8430,6 +8170,9 @@ ${itemsHtml}
8430
8170
  const mlpsKbPatches = [];
8431
8171
  let mlpsKbSeverity = "LOW";
8432
8172
  let mlpsKbUrl;
8173
+ const mlpsCveList = [];
8174
+ let mlpsCveSeverity = "LOW";
8175
+ let mlpsCveUrl;
8433
8176
  const mlpsGenericPatterns = ["See References", "None Provided", "Review the finding", "Review and remediate."];
8434
8177
  for (const r of failedResults) {
8435
8178
  for (const f of r.relatedFindings) {
@@ -8443,6 +8186,13 @@ ${itemsHtml}
8443
8186
  if (!mlpsKbUrl && url) mlpsKbUrl = url;
8444
8187
  continue;
8445
8188
  }
8189
+ const cveMatch = f.title.match(/CVE-[\d-]+/);
8190
+ if (cveMatch) {
8191
+ mlpsCveList.push(cveMatch[0]);
8192
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(mlpsCveSeverity)) mlpsCveSeverity = f.severity;
8193
+ if (!mlpsCveUrl && url) mlpsCveUrl = url;
8194
+ continue;
8195
+ }
8446
8196
  if (f.module === "security_hub_findings") {
8447
8197
  const controlMatch = f.title.match(/^([A-Z][A-Za-z0-9]*\.\d+)\s/);
8448
8198
  if (controlMatch) {
@@ -8459,6 +8209,23 @@ ${itemsHtml}
8459
8209
  continue;
8460
8210
  }
8461
8211
  }
8212
+ if (f.module !== "security_hub_findings" && f.module !== "inspector_findings") {
8213
+ const template = getRecommendationTemplate(rem);
8214
+ if (template !== rem) {
8215
+ const templateKey = `tmpl:${f.module}:${template}`;
8216
+ const existingTmpl = mlpsRecMap.get(templateKey);
8217
+ if (existingTmpl) {
8218
+ existingTmpl.count++;
8219
+ if (!existingTmpl.url && url) existingTmpl.url = url;
8220
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(existingTmpl.severity)) {
8221
+ existingTmpl.severity = f.severity;
8222
+ }
8223
+ continue;
8224
+ }
8225
+ mlpsRecMap.set(templateKey, { text: rem, severity: f.severity, count: 1, url });
8226
+ continue;
8227
+ }
8228
+ }
8462
8229
  const existing = mlpsRecMap.get(rem);
8463
8230
  if (existing) {
8464
8231
  existing.count++;
@@ -8476,8 +8243,14 @@ ${itemsHtml}
8476
8243
  const kbList = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
8477
8244
  mlpsRecMap.set("__kb__", { text: t.installWindowsPatches(unique.length, kbList), severity: mlpsKbSeverity, count: 1, url: mlpsKbUrl });
8478
8245
  }
8246
+ if (mlpsCveList.length > 0) {
8247
+ const unique = [...new Set(mlpsCveList)];
8248
+ const cveDisplay = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
8249
+ const cveText = (lang ?? "zh") === "zh" ? `\u4FEE\u590D ${unique.length} \u4E2A\u8F6F\u4EF6\u6F0F\u6D1E (${cveDisplay})\uFF0C\u66F4\u65B0\u53D7\u5F71\u54CD\u7684\u8F6F\u4EF6\u5305\u5230\u6700\u65B0\u7248\u672C` : `Fix ${unique.length} software vulnerabilities (${cveDisplay}) \u2014 update affected packages to latest patched versions`;
8250
+ mlpsRecMap.set("__cve__", { text: cveText, severity: mlpsCveSeverity, count: 1, url: mlpsCveUrl });
8251
+ }
8479
8252
  for (const [key, rec] of mlpsRecMap) {
8480
- if (key.startsWith("ctrl:") && rec.count > 1) {
8253
+ if ((key.startsWith("ctrl:") || key.startsWith("tmpl:")) && rec.count > 1) {
8481
8254
  rec.text += ` \u2014 ${t.affectedResources(rec.count)}`;
8482
8255
  rec.count = 1;
8483
8256
  }
@@ -8773,7 +8546,6 @@ Detects which AWS security services are enabled and assesses overall security ma
8773
8546
  - **GuardDuty not enabled** \u2014 Risk 7.5: Provides continuous threat detection.
8774
8547
  - **Inspector not enabled** \u2014 Risk 6.0: Scans for software vulnerabilities.
8775
8548
  - **AWS Config not enabled** \u2014 Risk 6.0: Tracks configuration changes.
8776
- - **Macie not enabled** \u2014 Risk 5.0: Detects sensitive data in S3 (not available in China regions).
8777
8549
  - CloudTrail detection is included for coverage metrics.
8778
8550
 
8779
8551
  ### Maturity Levels
@@ -8781,8 +8553,8 @@ Detects which AWS security services are enabled and assesses overall security ma
8781
8553
  |------------------|-------|
8782
8554
  | 0\u20131 | Basic |
8783
8555
  | 2\u20133 | Intermediate |
8784
- | 4\u20135 | Advanced |
8785
- | 6 | Comprehensive |
8556
+ | 4 | Advanced |
8557
+ | 5 | Comprehensive |
8786
8558
 
8787
8559
  ## 2. Security Hub Findings (security_hub_findings)
8788
8560
  Aggregates active findings from AWS Security Hub. Replaces individual config scanners (SG, S3, IAM, CloudTrail, RDS, EBS, VPC, etc.) with centralized compliance checks from FSBP, CIS, and PCI DSS standards.
@@ -8916,7 +8688,7 @@ import { readFileSync as readFileSync2 } from "fs";
8916
8688
  import { join as join2, dirname } from "path";
8917
8689
  import { fileURLToPath } from "url";
8918
8690
  var MODULE_DESCRIPTIONS = {
8919
- service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config, Macie) are enabled and assesses security maturity.",
8691
+ service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config) are enabled and assesses security maturity.",
8920
8692
  secret_exposure: "Checks Lambda env vars and EC2 userData for exposed secrets (AWS keys, private keys, passwords).",
8921
8693
  ssl_certificate: "Checks ACM certificates for expiry, failed status, and upcoming renewals.",
8922
8694
  dns_dangling: "Checks Route53 CNAME records for dangling DNS (subdomain takeover risk).",
@@ -9351,14 +9123,12 @@ function createServer(defaultRegion) {
9351
9123
  "Security Hub": "+300 security checks",
9352
9124
  "GuardDuty": "Threat detection",
9353
9125
  "Inspector": "Vulnerability scanning",
9354
- "AWS Config": "Configuration tracking",
9355
- "Macie": "Sensitive data detection"
9126
+ "AWS Config": "Configuration tracking"
9356
9127
  };
9357
9128
  const serviceFreeTrials = {
9358
9129
  "Security Hub": true,
9359
9130
  "GuardDuty": true,
9360
- "Inspector": true,
9361
- "Macie": true
9131
+ "Inspector": true
9362
9132
  };
9363
9133
  const services = detection.services;
9364
9134
  const coveragePercent = detection.coveragePercent;
@@ -9393,7 +9163,7 @@ function createServer(defaultRegion) {
9393
9163
  lines.push("");
9394
9164
  lines.push("### Recommendations (Priority Order)");
9395
9165
  lines.push("");
9396
- const priorityOrder = ["Security Hub", "GuardDuty", "Inspector", "AWS Config", "Macie", "CloudTrail"];
9166
+ const priorityOrder = ["Security Hub", "GuardDuty", "Inspector", "AWS Config", "CloudTrail"];
9397
9167
  const sorted = disabled.sort(
9398
9168
  (a, b) => priorityOrder.indexOf(a.name) - priorityOrder.indexOf(b.name)
9399
9169
  );
@@ -9416,7 +9186,7 @@ function createServer(defaultRegion) {
9416
9186
  const nextMilestones = {
9417
9187
  basic: { level: "Intermediate", target: 2, suggestions: ["Security Hub", "GuardDuty"] },
9418
9188
  intermediate: { level: "Advanced", target: 4, suggestions: ["Inspector", "AWS Config"] },
9419
- advanced: { level: "Comprehensive", target: 6, suggestions: ["Macie"] }
9189
+ advanced: { level: "Comprehensive", target: 5, suggestions: ["CloudTrail"] }
9420
9190
  };
9421
9191
  const next = nextMilestones[maturityLevel];
9422
9192
  if (next) {