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.
package/dist/src/index.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
 
6
6
  // src/version.ts
7
- var VERSION = "0.6.0";
7
+ var VERSION = "0.6.2";
8
8
 
9
9
  // src/utils/aws-client.ts
10
10
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
@@ -274,10 +274,6 @@ import {
274
274
  ConfigServiceClient,
275
275
  DescribeConfigurationRecordersCommand
276
276
  } from "@aws-sdk/client-config-service";
277
- import {
278
- Macie2Client,
279
- GetMacieSessionCommand
280
- } from "@aws-sdk/client-macie2";
281
277
  import {
282
278
  CloudTrailClient,
283
279
  DescribeTrailsCommand
@@ -321,7 +317,7 @@ function isNotEnabled(err) {
321
317
  err.name === "DisabledException" || err.message.includes("not enabled") || err.message.includes("not subscribed");
322
318
  }
323
319
  function computeMaturityLevel(enabledCount) {
324
- if (enabledCount >= 6) return "comprehensive";
320
+ if (enabledCount >= 5) return "comprehensive";
325
321
  if (enabledCount >= 4) return "advanced";
326
322
  if (enabledCount >= 2) return "intermediate";
327
323
  return "basic";
@@ -625,53 +621,6 @@ var ServiceDetectionScanner = class {
625
621
  services.push({ name: "AWS Config", enabled: null, details: "Detection error" });
626
622
  }
627
623
  }
628
- if (region.startsWith("cn-")) {
629
- services.push({ name: "Macie", enabled: null, details: "Not available in China regions" });
630
- warnings.push("Macie is not available in AWS China regions.");
631
- } else {
632
- try {
633
- const mc = createClient(Macie2Client, region, ctx.credentials);
634
- await mc.send(new GetMacieSessionCommand({}));
635
- services.push({
636
- name: "Macie",
637
- enabled: true,
638
- details: "Sensitive data detection active"
639
- });
640
- } catch (err) {
641
- if (isAccessDenied(err)) {
642
- warnings.push("Macie: insufficient permissions to check status");
643
- services.push({ name: "Macie", enabled: null, details: "Access denied" });
644
- } else if (isNotEnabled(err)) {
645
- services.push({
646
- name: "Macie",
647
- enabled: false,
648
- recommendation: "Enable Macie to detect sensitive data in S3",
649
- freeTrialAvailable: true
650
- });
651
- findings.push(
652
- makeFinding({
653
- riskScore: 5,
654
- title: "Amazon Macie is not enabled",
655
- resourceType: "AWS::Macie::Session",
656
- resourceId: "macie",
657
- resourceArn: `arn:${partition}:macie2:${region}:${accountId}:session`,
658
- region,
659
- description: "Amazon Macie is not enabled in this region. Macie uses machine learning to discover and protect sensitive data stored in S3.",
660
- impact: "Detects sensitive data (PII, credentials, financial data) in S3 buckets. Without it, sensitive data exposure may go unnoticed.",
661
- remediationSteps: [
662
- "Open the Amazon Macie console.",
663
- "Click 'Get Started' and enable Macie.",
664
- "Macie offers a 30-day free trial for sensitive data discovery.",
665
- "Configure automated sensitive data discovery jobs."
666
- ]
667
- })
668
- );
669
- } else {
670
- warnings.push(`Macie detection failed: ${err instanceof Error ? err.message : String(err)}`);
671
- services.push({ name: "Macie", enabled: null, details: "Detection error" });
672
- }
673
- }
674
- }
675
624
  const knownServices = services.filter((s) => s.enabled !== null);
676
625
  const enabledCount = services.filter((s) => s.enabled === true).length;
677
626
  const coveragePercent = knownServices.length > 0 ? Math.round(enabledCount / knownServices.length * 100) : 0;
@@ -2818,126 +2767,37 @@ var SecurityHubFindingsScanner = class {
2818
2767
  // src/scanners/guardduty-findings.ts
2819
2768
  import {
2820
2769
  GuardDutyClient as GuardDutyClient2,
2821
- ListDetectorsCommand as ListDetectorsCommand2,
2822
- ListFindingsCommand,
2823
- GetFindingsCommand as GetFindingsCommand2
2770
+ ListDetectorsCommand as ListDetectorsCommand2
2824
2771
  } from "@aws-sdk/client-guardduty";
2825
- function gdSeverityToScore(severity) {
2826
- if (severity >= 7) return 8;
2827
- if (severity >= 4) return 5.5;
2828
- return 3;
2829
- }
2830
2772
  var GuardDutyFindingsScanner = class {
2831
2773
  moduleName = "guardduty_findings";
2832
2774
  async scan(ctx) {
2833
- const { region, partition, accountId } = ctx;
2775
+ const { region } = ctx;
2834
2776
  const startMs = Date.now();
2835
- const findings = [];
2836
2777
  const warnings = [];
2837
- let resourcesScanned = 0;
2838
2778
  try {
2839
2779
  const client = createClient(GuardDutyClient2, region, ctx.credentials);
2840
- const detectorsResp = await client.send(new ListDetectorsCommand2({}));
2841
- const detectorIds = detectorsResp.DetectorIds ?? [];
2780
+ const resp = await client.send(new ListDetectorsCommand2({}));
2781
+ const detectorIds = resp.DetectorIds ?? [];
2842
2782
  if (detectorIds.length === 0) {
2843
2783
  warnings.push("GuardDuty is not enabled in this region (no detectors found).");
2844
- return {
2845
- module: this.moduleName,
2846
- status: "success",
2847
- warnings,
2848
- resourcesScanned: 0,
2849
- findingsCount: 0,
2850
- scanTimeMs: Date.now() - startMs,
2851
- findings: []
2852
- };
2853
- }
2854
- const detectorId = detectorIds[0];
2855
- let nextToken;
2856
- const findingIds = [];
2857
- do {
2858
- const listResp = await client.send(
2859
- new ListFindingsCommand({
2860
- DetectorId: detectorId,
2861
- FindingCriteria: {
2862
- Criterion: {
2863
- "service.archived": {
2864
- Eq: ["false"]
2865
- }
2866
- }
2867
- },
2868
- MaxResults: 50,
2869
- NextToken: nextToken
2870
- })
2871
- );
2872
- findingIds.push(...listResp.FindingIds ?? []);
2873
- nextToken = listResp.NextToken;
2874
- } while (nextToken);
2875
- resourcesScanned = findingIds.length;
2876
- if (findingIds.length === 0) {
2877
- return {
2878
- module: this.moduleName,
2879
- status: "success",
2880
- warnings: warnings.length > 0 ? warnings : void 0,
2881
- resourcesScanned: 0,
2882
- findingsCount: 0,
2883
- scanTimeMs: Date.now() - startMs,
2884
- findings: []
2885
- };
2886
- }
2887
- for (let i = 0; i < findingIds.length; i += 50) {
2888
- const batch = findingIds.slice(i, i + 50);
2889
- const detailsResp = await client.send(
2890
- new GetFindingsCommand2({
2891
- DetectorId: detectorId,
2892
- FindingIds: batch
2893
- })
2894
- );
2895
- for (const gdf of detailsResp.Findings ?? []) {
2896
- const gdSeverity = gdf.Severity ?? 0;
2897
- const score = gdSeverityToScore(gdSeverity);
2898
- const severity = severityFromScore(score);
2899
- const resourceType = gdf.Resource?.ResourceType ?? "AWS::Unknown";
2900
- const resourceId = gdf.Resource?.InstanceDetails?.InstanceId ?? gdf.Resource?.AccessKeyDetails?.AccessKeyId ?? gdf.Arn ?? "unknown";
2901
- const resourceArn = gdf.Arn ?? `arn:${partition}:guardduty:${region}:${accountId}:detector/${detectorId}/finding/${gdf.Id ?? "unknown"}`;
2902
- findings.push({
2903
- severity,
2904
- title: `[GuardDuty] ${gdf.Title ?? gdf.Type ?? "Finding"}`,
2905
- resourceType,
2906
- resourceId,
2907
- resourceArn,
2908
- region: gdf.Region ?? region,
2909
- description: gdf.Description ?? gdf.Title ?? "No description",
2910
- impact: `GuardDuty threat type: ${gdf.Type ?? "unknown"} (severity ${gdSeverity})`,
2911
- riskScore: score,
2912
- remediationSteps: [
2913
- `Investigate ${gdf.Type ?? "unknown threat"}: ${gdf.Title ?? "threat detected"}`,
2914
- gdf.Description ? `Details: ${gdf.Description.substring(0, 200)}` : "",
2915
- "Isolate affected resources if compromise is confirmed.",
2916
- "Review CloudTrail logs for related suspicious activity."
2917
- ].filter(Boolean),
2918
- priority: priorityFromSeverity(severity),
2919
- module: this.moduleName,
2920
- accountId: gdf.AccountId ?? accountId
2921
- });
2922
- }
2923
2784
  }
2924
2785
  return {
2925
2786
  module: this.moduleName,
2926
2787
  status: "success",
2927
2788
  warnings: warnings.length > 0 ? warnings : void 0,
2928
- resourcesScanned,
2929
- findingsCount: findings.length,
2789
+ resourcesScanned: 0,
2790
+ findingsCount: 0,
2930
2791
  scanTimeMs: Date.now() - startMs,
2931
- findings
2792
+ findings: []
2932
2793
  };
2933
2794
  } catch (err) {
2934
2795
  const msg = err instanceof Error ? err.message : String(err);
2935
2796
  return {
2936
2797
  module: this.moduleName,
2937
2798
  status: "error",
2938
- error: `GuardDuty findings scan failed: ${msg}`,
2939
- warnings: warnings.length > 0 ? warnings : void 0,
2940
- resourcesScanned,
2799
+ error: `GuardDuty detection check failed: ${msg}`,
2800
+ resourcesScanned: 0,
2941
2801
  findingsCount: 0,
2942
2802
  scanTimeMs: Date.now() - startMs,
2943
2803
  findings: []
@@ -2949,164 +2809,59 @@ var GuardDutyFindingsScanner = class {
2949
2809
  // src/scanners/inspector-findings.ts
2950
2810
  import {
2951
2811
  Inspector2Client as Inspector2Client2,
2952
- ListFindingsCommand as ListFindingsCommand2
2812
+ BatchGetAccountStatusCommand as BatchGetAccountStatusCommand2
2953
2813
  } from "@aws-sdk/client-inspector2";
2954
- function inspectorSeverityToScore(label) {
2955
- switch (label) {
2956
- case "CRITICAL":
2957
- return 9.5;
2958
- case "HIGH":
2959
- return 8;
2960
- case "MEDIUM":
2961
- return 5.5;
2962
- case "LOW":
2963
- return 3;
2964
- case "INFORMATIONAL":
2965
- return null;
2966
- case "UNTRIAGED":
2967
- return 5.5;
2968
- default:
2969
- return null;
2970
- }
2971
- }
2972
2814
  var InspectorFindingsScanner = class {
2973
2815
  moduleName = "inspector_findings";
2974
2816
  async scan(ctx) {
2975
- const { region, partition, accountId } = ctx;
2817
+ const { region } = ctx;
2976
2818
  const startMs = Date.now();
2977
- const findings = [];
2978
2819
  const warnings = [];
2979
- let resourcesScanned = 0;
2980
2820
  try {
2981
2821
  const client = createClient(Inspector2Client2, region, ctx.credentials);
2982
- let nextToken;
2983
- const filterCriteria = {
2984
- findingStatus: [{ comparison: "EQUALS", value: "ACTIVE" }]
2985
- };
2986
- do {
2987
- const resp = await client.send(
2988
- new ListFindingsCommand2({
2989
- filterCriteria,
2990
- maxResults: 100,
2991
- nextToken
2992
- })
2993
- );
2994
- const inspFindings = resp.findings ?? [];
2995
- resourcesScanned += inspFindings.length;
2996
- for (const f of inspFindings) {
2997
- const severityLabel = f.severity ?? "INFORMATIONAL";
2998
- const score = inspectorSeverityToScore(severityLabel);
2999
- if (score === null) continue;
3000
- const severity = severityFromScore(score);
3001
- const cveId = f.packageVulnerabilityDetails?.vulnerabilityId;
3002
- const titleBase = f.title ?? "Inspector Finding";
3003
- const title = cveId ? `[${cveId}] ${titleBase}` : titleBase;
3004
- const resourceId = f.resources?.[0]?.id ?? "unknown";
3005
- const resourceType = f.resources?.[0]?.type ?? "AWS::Unknown";
3006
- const resourceArn = resourceId.startsWith("arn:") ? resourceId : `arn:${partition}:inspector2:${region}:${accountId}:finding/${f.findingArn ?? "unknown"}`;
3007
- const remediationSteps = [];
3008
- const vulnPkgs = f.packageVulnerabilityDetails?.vulnerablePackages;
3009
- if (vulnPkgs?.length) {
3010
- for (const pkg of vulnPkgs.slice(0, 3)) {
3011
- const name = pkg.name ?? "unknown-package";
3012
- const installed = pkg.version ?? "unknown";
3013
- const fixed = pkg.fixedInVersion ?? "latest";
3014
- const cveRef = cveId ? ` to fix ${cveId}` : "";
3015
- remediationSteps.push(`Update ${name} from ${installed} to ${fixed}${cveRef}`);
3016
- }
3017
- } else if (f.remediation?.recommendation?.text) {
3018
- remediationSteps.push(f.remediation.recommendation.text);
3019
- }
3020
- const genericPatterns = ["See References", "None Provided", "Review the finding"];
3021
- if (remediationSteps.length === 0 || genericPatterns.some((p) => remediationSteps[0]?.startsWith(p))) {
3022
- remediationSteps.length = 0;
3023
- const rawTitle = f.title ?? "";
3024
- if (rawTitle.includes("KB")) {
3025
- const kbMatch = rawTitle.match(/KB\d+/);
3026
- const kb = kbMatch ? kbMatch[0] : "patch";
3027
- remediationSteps.push(`Install Windows patch ${kb} via WSUS or AWS Systems Manager Patch Manager`);
3028
- remediationSteps.push(`Run: aws ssm send-command --document-name "AWS-InstallWindowsUpdates" --targets "Key=InstanceIds,Values=${resourceId}"`);
3029
- if (kbMatch) {
3030
- remediationSteps.push(`Microsoft KB article: https://support.microsoft.com/help/${kb}`);
3031
- }
3032
- } else if (rawTitle.includes("CVE-") || cveId) {
3033
- const cveMatch = rawTitle.match(/CVE-[\d-]+/);
3034
- const cve = cveMatch ? cveMatch[0] : cveId ?? "vulnerability";
3035
- remediationSteps.push(`Fix ${cve}: update the affected software package to the latest patched version`);
3036
- } else {
3037
- remediationSteps.push(`Review and remediate: ${rawTitle}`);
3038
- }
3039
- }
3040
- if (f.remediation?.recommendation?.Url) {
3041
- remediationSteps.push(`Documentation: ${f.remediation.recommendation.Url}`);
3042
- }
3043
- if (f.packageVulnerabilityDetails?.referenceUrls?.length) {
3044
- remediationSteps.push(`CVE references: ${f.packageVulnerabilityDetails.referenceUrls.slice(0, 3).join(", ")}`);
3045
- }
3046
- const description = f.description ?? titleBase;
3047
- const impact = cveId ? `Vulnerability ${cveId} \u2014 CVSS: ${f.packageVulnerabilityDetails?.cvss?.[0]?.baseScore ?? "N/A"}` : `Inspector finding type: ${f.type ?? "unknown"}`;
3048
- findings.push({
3049
- severity,
3050
- title,
3051
- resourceType,
3052
- resourceId,
3053
- resourceArn,
3054
- region,
3055
- description,
3056
- impact,
3057
- riskScore: score,
3058
- remediationSteps,
3059
- priority: priorityFromSeverity(severity),
3060
- module: this.moduleName,
3061
- accountId: f.awsAccountId ?? accountId
3062
- });
2822
+ const resp = await client.send(new BatchGetAccountStatusCommand2({ accountIds: [] }));
2823
+ const account = resp.accounts?.[0];
2824
+ if (!account || account.state?.status !== "ENABLED") {
2825
+ warnings.push("Inspector is not enabled in this region. Enable it to scan for software vulnerabilities.");
2826
+ } else {
2827
+ const rs = account.resourceState;
2828
+ const types = [
2829
+ { name: "EC2", status: rs?.ec2?.status },
2830
+ { name: "Lambda", status: rs?.lambda?.status },
2831
+ { name: "ECR", status: rs?.ecr?.status },
2832
+ { name: "Lambda Code", status: rs?.lambdaCode?.status },
2833
+ { name: "Code Repository", status: rs?.codeRepository?.status }
2834
+ ];
2835
+ const disabled = types.filter((t) => t.status && t.status !== "ENABLED");
2836
+ if (disabled.length > 0) {
2837
+ warnings.push(
2838
+ `Inspector scan types not enabled: ${disabled.map((t) => t.name).join(", ")}. Enable them for full vulnerability coverage.`
2839
+ );
3063
2840
  }
3064
- nextToken = resp.nextToken;
3065
- } while (nextToken);
2841
+ }
3066
2842
  return {
3067
2843
  module: this.moduleName,
3068
2844
  status: "success",
3069
2845
  warnings: warnings.length > 0 ? warnings : void 0,
3070
- resourcesScanned,
3071
- findingsCount: findings.length,
2846
+ resourcesScanned: 0,
2847
+ findingsCount: 0,
3072
2848
  scanTimeMs: Date.now() - startMs,
3073
- findings
2849
+ findings: []
3074
2850
  };
3075
2851
  } catch (err) {
3076
2852
  const msg = err instanceof Error ? err.message : String(err);
3077
2853
  const errName = err instanceof Error ? err.name : "";
3078
2854
  const isAccessDenied2 = errName === "AccessDeniedException" || msg.includes("AccessDeniedException");
3079
- const isNotEnabled2 = msg.includes("not enabled") || msg.includes("not subscribed");
3080
2855
  if (isAccessDenied2) {
3081
- warnings.push("Insufficient permissions to access Inspector. Grant inspector2:ListFindings to scan for vulnerabilities.");
3082
- return {
3083
- module: this.moduleName,
3084
- status: "success",
3085
- warnings,
3086
- resourcesScanned: 0,
3087
- findingsCount: 0,
3088
- scanTimeMs: Date.now() - startMs,
3089
- findings: []
3090
- };
3091
- }
3092
- if (isNotEnabled2) {
2856
+ warnings.push("Insufficient permissions to access Inspector. Grant inspector2:BatchGetAccountStatus to check enablement.");
2857
+ } else {
3093
2858
  warnings.push("Inspector is not enabled in this region. Enable it to scan for software vulnerabilities.");
3094
- return {
3095
- module: this.moduleName,
3096
- status: "success",
3097
- warnings,
3098
- resourcesScanned: 0,
3099
- findingsCount: 0,
3100
- scanTimeMs: Date.now() - startMs,
3101
- findings: []
3102
- };
3103
2859
  }
3104
2860
  return {
3105
2861
  module: this.moduleName,
3106
- status: "error",
3107
- error: `Inspector findings scan failed: ${msg}`,
3108
- warnings: warnings.length > 0 ? warnings : void 0,
3109
- resourcesScanned,
2862
+ status: "success",
2863
+ warnings,
2864
+ resourcesScanned: 0,
3110
2865
  findingsCount: 0,
3111
2866
  scanTimeMs: Date.now() - startMs,
3112
2867
  findings: []
@@ -3279,128 +3034,29 @@ var TrustedAdvisorFindingsScanner = class {
3279
3034
  // src/scanners/config-rules-findings.ts
3280
3035
  import {
3281
3036
  ConfigServiceClient as ConfigServiceClient2,
3282
- DescribeComplianceByConfigRuleCommand,
3283
- GetComplianceDetailsByConfigRuleCommand
3037
+ DescribeConfigurationRecordersCommand as DescribeConfigurationRecordersCommand2
3284
3038
  } from "@aws-sdk/client-config-service";
3285
- var SECURITY_RULE_PATTERNS = [
3286
- "securitygroup",
3287
- "security-group",
3288
- "encryption",
3289
- "encrypted",
3290
- "public",
3291
- "unrestricted",
3292
- "mfa",
3293
- "password",
3294
- "access-key",
3295
- "root",
3296
- "admin",
3297
- "logging",
3298
- "cloudtrail",
3299
- "iam",
3300
- "kms",
3301
- "ssl",
3302
- "tls",
3303
- "vpc-flow",
3304
- "guardduty",
3305
- "securityhub"
3306
- ];
3307
- function ruleIsSecurityRelated(ruleName) {
3308
- const lower = ruleName.toLowerCase();
3309
- return SECURITY_RULE_PATTERNS.some((pat) => lower.includes(pat));
3310
- }
3311
3039
  var ConfigRulesFindingsScanner = class {
3312
3040
  moduleName = "config_rules_findings";
3313
3041
  async scan(ctx) {
3314
- const { region, partition, accountId } = ctx;
3042
+ const { region } = ctx;
3315
3043
  const startMs = Date.now();
3316
- const findings = [];
3317
3044
  const warnings = [];
3318
- let resourcesScanned = 0;
3319
3045
  try {
3320
3046
  const client = createClient(ConfigServiceClient2, region, ctx.credentials);
3321
- let nextToken;
3322
- const nonCompliantRules = [];
3323
- do {
3324
- const resp = await client.send(
3325
- new DescribeComplianceByConfigRuleCommand({ NextToken: nextToken })
3326
- );
3327
- for (const rule of resp.ComplianceByConfigRules ?? []) {
3328
- resourcesScanned++;
3329
- if (rule.Compliance?.ComplianceType === "NON_COMPLIANT") {
3330
- nonCompliantRules.push(rule);
3331
- }
3332
- }
3333
- nextToken = resp.NextToken;
3334
- } while (nextToken);
3335
- if (resourcesScanned === 0) {
3336
- warnings.push("AWS Config is not enabled in this region or no Config Rules are defined.");
3337
- return {
3338
- module: this.moduleName,
3339
- status: "success",
3340
- warnings,
3341
- resourcesScanned: 0,
3342
- findingsCount: 0,
3343
- scanTimeMs: Date.now() - startMs,
3344
- findings: []
3345
- };
3346
- }
3347
- for (const rule of nonCompliantRules) {
3348
- const ruleName = rule.ConfigRuleName ?? "unknown";
3349
- try {
3350
- let detailToken;
3351
- do {
3352
- const detailResp = await client.send(
3353
- new GetComplianceDetailsByConfigRuleCommand({
3354
- ConfigRuleName: ruleName,
3355
- ComplianceTypes: ["NON_COMPLIANT"],
3356
- NextToken: detailToken
3357
- })
3358
- );
3359
- for (const evalResult of detailResp.EvaluationResults ?? []) {
3360
- const qualifier = evalResult.EvaluationResultIdentifier?.EvaluationResultQualifier;
3361
- const resourceType = qualifier?.ResourceType ?? "AWS::Unknown";
3362
- const resourceId = qualifier?.ResourceId ?? "unknown";
3363
- const annotation = evalResult.Annotation;
3364
- const isSecurityRule = ruleIsSecurityRelated(ruleName);
3365
- const riskScore = isSecurityRule ? 7.5 : 5.5;
3366
- const severity = severityFromScore(riskScore);
3367
- const descParts = [`Config Rule: ${ruleName}`, `Resource Type: ${resourceType}`];
3368
- if (annotation) descParts.push(`Annotation: ${annotation}`);
3369
- findings.push({
3370
- severity,
3371
- title: `Config Rule: ${ruleName} - ${resourceType}/${resourceId} Non-Compliant`,
3372
- resourceType,
3373
- resourceId,
3374
- resourceArn: resourceId,
3375
- region,
3376
- description: descParts.join(". "),
3377
- impact: `Resource is non-compliant with Config Rule: ${ruleName}`,
3378
- riskScore,
3379
- remediationSteps: [
3380
- `Fix Config Rule violation: ${ruleName}`,
3381
- annotation ? `Details: ${annotation}` : "",
3382
- `Resource: ${resourceType}/${resourceId}`
3383
- ].filter(Boolean),
3384
- priority: priorityFromSeverity(severity),
3385
- module: this.moduleName,
3386
- accountId
3387
- });
3388
- }
3389
- detailToken = detailResp.NextToken;
3390
- } while (detailToken);
3391
- } catch (detailErr) {
3392
- const msg = detailErr instanceof Error ? detailErr.message : String(detailErr);
3393
- warnings.push(`Failed to get details for rule ${ruleName}: ${msg}`);
3394
- }
3047
+ const resp = await client.send(new DescribeConfigurationRecordersCommand2({}));
3048
+ const recorders = resp.ConfigurationRecorders ?? [];
3049
+ if (recorders.length === 0) {
3050
+ warnings.push("AWS Config is not enabled in this region.");
3395
3051
  }
3396
3052
  return {
3397
3053
  module: this.moduleName,
3398
3054
  status: "success",
3399
3055
  warnings: warnings.length > 0 ? warnings : void 0,
3400
- resourcesScanned,
3401
- findingsCount: findings.length,
3056
+ resourcesScanned: 0,
3057
+ findingsCount: 0,
3402
3058
  scanTimeMs: Date.now() - startMs,
3403
- findings
3059
+ findings: []
3404
3060
  };
3405
3061
  } catch (err) {
3406
3062
  const msg = err instanceof Error ? err.message : String(err);
@@ -3419,9 +3075,8 @@ var ConfigRulesFindingsScanner = class {
3419
3075
  return {
3420
3076
  module: this.moduleName,
3421
3077
  status: "error",
3422
- error: `Config Rules scan failed: ${msg}`,
3423
- warnings: warnings.length > 0 ? warnings : void 0,
3424
- resourcesScanned,
3078
+ error: `Config Rules detection check failed: ${msg}`,
3079
+ resourcesScanned: 0,
3425
3080
  findingsCount: 0,
3426
3081
  scanTimeMs: Date.now() - startMs,
3427
3082
  findings: []
@@ -3433,146 +3088,50 @@ var ConfigRulesFindingsScanner = class {
3433
3088
  // src/scanners/access-analyzer-findings.ts
3434
3089
  import {
3435
3090
  AccessAnalyzerClient,
3436
- ListAnalyzersCommand,
3437
- ListFindingsV2Command
3091
+ ListAnalyzersCommand
3438
3092
  } from "@aws-sdk/client-accessanalyzer";
3439
- function findingTypeToScore(findingType) {
3440
- const ft = findingType;
3441
- switch (ft) {
3442
- case "ExternalAccess":
3443
- return 8;
3444
- case "UnusedIAMRole":
3445
- case "UnusedIAMUserAccessKey":
3446
- case "UnusedIAMUserPassword":
3447
- return 5.5;
3448
- case "UnusedPermission":
3449
- return 3;
3450
- default:
3451
- return 5.5;
3452
- }
3453
- }
3454
- var UNUSED_FINDING_TYPES = /* @__PURE__ */ new Set([
3455
- "UnusedIAMRole",
3456
- "UnusedIAMUserAccessKey",
3457
- "UnusedIAMUserPassword",
3458
- "UnusedPermission"
3459
- ]);
3460
- function isSecurityRelevant(findingType) {
3461
- const ft = findingType;
3462
- return ft === "ExternalAccess" || UNUSED_FINDING_TYPES.has(ft ?? "");
3463
- }
3464
- function isExternalAccess(findingType) {
3465
- return findingType === "ExternalAccess";
3466
- }
3467
3093
  var AccessAnalyzerFindingsScanner = class {
3468
3094
  moduleName = "access_analyzer_findings";
3469
3095
  async scan(ctx) {
3470
- const { region, partition, accountId } = ctx;
3096
+ const { region } = ctx;
3471
3097
  const startMs = Date.now();
3472
- const findings = [];
3473
3098
  const warnings = [];
3474
- let resourcesScanned = 0;
3475
3099
  try {
3476
3100
  const client = createClient(AccessAnalyzerClient, region, ctx.credentials);
3477
3101
  let analyzerToken;
3478
- const analyzers = [];
3102
+ let hasActiveAnalyzer = false;
3479
3103
  do {
3480
3104
  const resp = await client.send(
3481
3105
  new ListAnalyzersCommand({ nextToken: analyzerToken })
3482
3106
  );
3483
3107
  for (const analyzer of resp.analyzers ?? []) {
3484
3108
  if (analyzer.status === "ACTIVE") {
3485
- analyzers.push(analyzer);
3109
+ hasActiveAnalyzer = true;
3110
+ break;
3486
3111
  }
3487
3112
  }
3113
+ if (hasActiveAnalyzer) break;
3488
3114
  analyzerToken = resp.nextToken;
3489
3115
  } while (analyzerToken);
3490
- if (analyzers.length === 0) {
3116
+ if (!hasActiveAnalyzer) {
3491
3117
  warnings.push("No IAM Access Analyzer found. Create an analyzer to detect external access to your resources.");
3492
- return {
3493
- module: this.moduleName,
3494
- status: "success",
3495
- warnings,
3496
- resourcesScanned: 0,
3497
- findingsCount: 0,
3498
- scanTimeMs: Date.now() - startMs,
3499
- findings: []
3500
- };
3501
- }
3502
- for (const analyzer of analyzers) {
3503
- const analyzerArn = analyzer.arn ?? "unknown";
3504
- let findingToken;
3505
- do {
3506
- const listResp = await client.send(
3507
- new ListFindingsV2Command({
3508
- analyzerArn,
3509
- filter: {
3510
- status: { eq: ["ACTIVE"] }
3511
- },
3512
- nextToken: findingToken
3513
- })
3514
- );
3515
- for (const aaf of listResp.findings ?? []) {
3516
- if (!isSecurityRelevant(aaf.findingType)) {
3517
- continue;
3518
- }
3519
- resourcesScanned++;
3520
- const score = findingTypeToScore(aaf.findingType);
3521
- const severity = severityFromScore(score);
3522
- const resourceArn = aaf.resource ?? "unknown";
3523
- const resourceType = aaf.resourceType ?? "AWS::Unknown";
3524
- const resourceId = resourceArn.split("/").pop() ?? resourceArn.split(":").pop() ?? "unknown";
3525
- const external = isExternalAccess(aaf.findingType);
3526
- const descParts = [`Resource Type: ${resourceType}`];
3527
- if (aaf.resourceOwnerAccount) descParts.push(`Owner Account: ${aaf.resourceOwnerAccount}`);
3528
- if (aaf.findingType) descParts.push(`Finding Type: ${aaf.findingType}`);
3529
- const title = buildFindingTitle(aaf);
3530
- 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"}`;
3531
- const remediationSteps = external ? [
3532
- `Restrict external access on ${resourceType} ${resourceId}`,
3533
- "Remove or narrow the resource policy to eliminate unintended external access.",
3534
- `Resource ARN: ${resourceArn}`
3535
- ] : [
3536
- `Remove unused access on ${resourceType} ${resourceId}`,
3537
- "Remove unused permissions, roles, or credentials to follow least-privilege.",
3538
- `Resource ARN: ${resourceArn}`
3539
- ];
3540
- findings.push({
3541
- severity,
3542
- title,
3543
- resourceType: mapResourceType(resourceType),
3544
- resourceId,
3545
- resourceArn,
3546
- region,
3547
- description: descParts.join(". "),
3548
- impact,
3549
- riskScore: score,
3550
- remediationSteps,
3551
- priority: priorityFromSeverity(severity),
3552
- module: this.moduleName,
3553
- accountId: aaf.resourceOwnerAccount ?? accountId
3554
- });
3555
- }
3556
- findingToken = listResp.nextToken;
3557
- } while (findingToken);
3558
3118
  }
3559
3119
  return {
3560
3120
  module: this.moduleName,
3561
3121
  status: "success",
3562
3122
  warnings: warnings.length > 0 ? warnings : void 0,
3563
- resourcesScanned,
3564
- findingsCount: findings.length,
3123
+ resourcesScanned: 0,
3124
+ findingsCount: 0,
3565
3125
  scanTimeMs: Date.now() - startMs,
3566
- findings
3126
+ findings: []
3567
3127
  };
3568
3128
  } catch (err) {
3569
3129
  const msg = err instanceof Error ? err.message : String(err);
3570
3130
  return {
3571
3131
  module: this.moduleName,
3572
3132
  status: "error",
3573
- error: `Access Analyzer scan failed: ${msg}`,
3574
- warnings: warnings.length > 0 ? warnings : void 0,
3575
- resourcesScanned,
3133
+ error: `Access Analyzer detection check failed: ${msg}`,
3134
+ resourcesScanned: 0,
3576
3135
  findingsCount: 0,
3577
3136
  scanTimeMs: Date.now() - startMs,
3578
3137
  findings: []
@@ -3580,29 +3139,6 @@ var AccessAnalyzerFindingsScanner = class {
3580
3139
  }
3581
3140
  }
3582
3141
  };
3583
- function buildFindingTitle(finding) {
3584
- const resourceType = finding.resourceType ?? "Resource";
3585
- const resource = finding.resource ? finding.resource.split("/").pop() ?? finding.resource.split(":").pop() ?? finding.resource : "unknown";
3586
- const label = isExternalAccess(finding.findingType) ? "external access detected" : "unused access detected";
3587
- return `[Access Analyzer] ${resourceType} ${resource} \u2014 ${label}`;
3588
- }
3589
- function mapResourceType(aaType) {
3590
- const mapping = {
3591
- "AWS::S3::Bucket": "AWS::S3::Bucket",
3592
- "AWS::IAM::Role": "AWS::IAM::Role",
3593
- "AWS::SQS::Queue": "AWS::SQS::Queue",
3594
- "AWS::Lambda::Function": "AWS::Lambda::Function",
3595
- "AWS::Lambda::LayerVersion": "AWS::Lambda::LayerVersion",
3596
- "AWS::KMS::Key": "AWS::KMS::Key",
3597
- "AWS::SecretsManager::Secret": "AWS::SecretsManager::Secret",
3598
- "AWS::SNS::Topic": "AWS::SNS::Topic",
3599
- "AWS::EFS::FileSystem": "AWS::EFS::FileSystem",
3600
- "AWS::RDS::DBSnapshot": "AWS::RDS::DBSnapshot",
3601
- "AWS::RDS::DBClusterSnapshot": "AWS::RDS::DBClusterSnapshot",
3602
- "AWS::ECR::Repository": "AWS::ECR::Repository"
3603
- };
3604
- return mapping[aaType] ?? aaType;
3605
- }
3606
3142
 
3607
3143
  // src/scanners/patch-compliance-findings.ts
3608
3144
  import {
@@ -4140,6 +3676,44 @@ var zhI18n = {
4140
3676
  "\u5B89\u5168\u8BA1\u7B97\u73AF\u5883": "\u56DB\u3001\u5B89\u5168\u8BA1\u7B97\u73AF\u5883",
4141
3677
  "\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3": "\u4E94\u3001\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3"
4142
3678
  },
3679
+ // Module display names
3680
+ moduleNames: {
3681
+ service_detection: "\u5B89\u5168\u670D\u52A1\u68C0\u6D4B",
3682
+ secret_exposure: "\u5BC6\u94A5\u66B4\u9732",
3683
+ ssl_certificate: "SSL \u8BC1\u4E66",
3684
+ dns_dangling: "\u60AC\u6302 DNS",
3685
+ network_reachability: "\u7F51\u7EDC\u53EF\u8FBE\u6027",
3686
+ iam_privilege_escalation: "IAM \u63D0\u6743\u5206\u6790",
3687
+ public_access_verify: "\u516C\u7F51\u8BBF\u95EE\u9A8C\u8BC1",
3688
+ tag_compliance: "\u6807\u7B7E\u5408\u89C4",
3689
+ idle_resources: "\u95F2\u7F6E\u8D44\u6E90",
3690
+ disaster_recovery: "\u707E\u5907\u8BC4\u4F30",
3691
+ security_hub_findings: "Security Hub",
3692
+ guardduty_findings: "GuardDuty",
3693
+ inspector_findings: "Inspector",
3694
+ trusted_advisor_findings: "Trusted Advisor",
3695
+ config_rules_findings: "Config Rules",
3696
+ access_analyzer_findings: "Access Analyzer",
3697
+ patch_compliance_findings: "\u8865\u4E01\u5408\u89C4",
3698
+ imdsv2_enforcement: "IMDSv2 \u5F3A\u5236",
3699
+ waf_coverage: "WAF \u8986\u76D6",
3700
+ // Security Hub sub-categories
3701
+ "sh:FSBP": "\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5",
3702
+ "sh:Inspector": "\u8F6F\u4EF6\u6F0F\u6D1E",
3703
+ "sh:GuardDuty": "\u5A01\u80C1\u68C0\u6D4B",
3704
+ "sh:Config": "\u914D\u7F6E\u5408\u89C4",
3705
+ "sh:Access Analyzer": "\u5916\u90E8\u8BBF\u95EE",
3706
+ "sh:Other": "\u5176\u4ED6\u5B89\u5168\u53D1\u73B0"
3707
+ },
3708
+ // Security Hub sub-categories
3709
+ securityHubSubCategories: {
3710
+ FSBP: { label: "\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5" },
3711
+ Inspector: { label: "\u8F6F\u4EF6\u6F0F\u6D1E" },
3712
+ GuardDuty: { label: "\u5A01\u80C1\u68C0\u6D4B" },
3713
+ Config: { label: "\u914D\u7F6E\u5408\u89C4" },
3714
+ "Access Analyzer": { label: "\u5916\u90E8\u8BBF\u95EE" },
3715
+ Other: { label: "\u5176\u4ED6\u5B89\u5168\u53D1\u73B0" }
3716
+ },
4143
3717
  // Service Recommendations
4144
3718
  notEnabled: "\u672A\u542F\u7528",
4145
3719
  serviceRecommendations: {
@@ -4372,6 +3946,44 @@ var enI18n = {
4372
3946
  "\u5B89\u5168\u8BA1\u7B97\u73AF\u5883": "IV. Computing Environment Security",
4373
3947
  "\u5B89\u5168\u7BA1\u7406\u4E2D\u5FC3": "V. Security Management Center"
4374
3948
  },
3949
+ // Module display names
3950
+ moduleNames: {
3951
+ service_detection: "Security Service Detection",
3952
+ secret_exposure: "Secret Exposure",
3953
+ ssl_certificate: "SSL Certificate",
3954
+ dns_dangling: "Dangling DNS",
3955
+ network_reachability: "Network Reachability",
3956
+ iam_privilege_escalation: "IAM Privilege Escalation",
3957
+ public_access_verify: "Public Access Verification",
3958
+ tag_compliance: "Tag Compliance",
3959
+ idle_resources: "Idle Resources",
3960
+ disaster_recovery: "Disaster Recovery",
3961
+ security_hub_findings: "Security Hub",
3962
+ guardduty_findings: "GuardDuty",
3963
+ inspector_findings: "Inspector",
3964
+ trusted_advisor_findings: "Trusted Advisor",
3965
+ config_rules_findings: "Config Rules",
3966
+ access_analyzer_findings: "Access Analyzer",
3967
+ patch_compliance_findings: "Patch Compliance",
3968
+ imdsv2_enforcement: "IMDSv2 Enforcement",
3969
+ waf_coverage: "WAF Coverage",
3970
+ // Security Hub sub-categories
3971
+ "sh:FSBP": "Security Best Practices",
3972
+ "sh:Inspector": "Software Vulnerabilities",
3973
+ "sh:GuardDuty": "Threat Detection",
3974
+ "sh:Config": "Configuration Compliance",
3975
+ "sh:Access Analyzer": "External Access",
3976
+ "sh:Other": "Other Security Findings"
3977
+ },
3978
+ // Security Hub sub-categories
3979
+ securityHubSubCategories: {
3980
+ FSBP: { label: "Security Best Practices" },
3981
+ Inspector: { label: "Software Vulnerabilities" },
3982
+ GuardDuty: { label: "Threat Detection" },
3983
+ Config: { label: "Configuration Compliance" },
3984
+ "Access Analyzer": { label: "External Access" },
3985
+ Other: { label: "Other Security Findings" }
3986
+ },
4375
3987
  // Service Recommendations
4376
3988
  notEnabled: "Not Enabled",
4377
3989
  serviceRecommendations: {
@@ -4576,7 +4188,7 @@ function generateMarkdownReport(scanResults, lang) {
4576
4188
  for (const m of modules) {
4577
4189
  const status = m.status === "success" ? "\u2705" : "\u274C";
4578
4190
  lines.push(
4579
- `| ${m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
4191
+ `| ${t.moduleNames[m.module] ?? m.module} | ${m.resourcesScanned} | ${m.findingsCount} | ${status} |`
4580
4192
  );
4581
4193
  }
4582
4194
  lines.push("");
@@ -7419,6 +7031,22 @@ var SEV_COLOR = {
7419
7031
  LOW: "#22c55e"
7420
7032
  };
7421
7033
  var SEVERITY_ORDER2 = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
7034
+ function getRecommendationTemplate(rem) {
7035
+ 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}");
7036
+ }
7037
+ function getSecurityHubSource(finding) {
7038
+ const impact = finding.impact ?? "";
7039
+ const match = impact.match(/^Source:\s*([^(]+)/);
7040
+ if (!match) return "Other";
7041
+ const product = match[1].trim();
7042
+ if (product === "Security Hub" || product.includes("Foundational")) return "FSBP";
7043
+ if (product === "Inspector" || product.includes("Inspector")) return "Inspector";
7044
+ if (product === "GuardDuty" || product.includes("GuardDuty")) return "GuardDuty";
7045
+ if (product === "Config" || product.includes("Config")) return "Config";
7046
+ if (product === "IAM Access Analyzer" || product.includes("Access Analyzer")) return "Access Analyzer";
7047
+ return "Other";
7048
+ }
7049
+ var SECURITY_HUB_SUB_CAT_ORDER = ["FSBP", "Inspector", "GuardDuty", "Config", "Access Analyzer", "Other"];
7422
7050
  function scoreColor(score) {
7423
7051
  if (score >= 80) return "#22c55e";
7424
7052
  if (score >= 50) return "#eab308";
@@ -7796,6 +7424,47 @@ function generateHtmlReport(scanResults, history, lang) {
7796
7424
  const allFindings = modules.flatMap(
7797
7425
  (m) => m.findings.map((f) => ({ ...f, module: f.module ?? m.module }))
7798
7426
  );
7427
+ const shModule = modules.find((m) => m.module === "security_hub_findings");
7428
+ const shSubCats = [];
7429
+ if (shModule && shModule.findingsCount > 0) {
7430
+ const catMap = {};
7431
+ for (const f of shModule.findings) {
7432
+ const cat = getSecurityHubSource(f);
7433
+ if (!catMap[cat]) catMap[cat] = [];
7434
+ catMap[cat].push(f);
7435
+ }
7436
+ for (const cat of SECURITY_HUB_SUB_CAT_ORDER) {
7437
+ const catFindings = catMap[cat];
7438
+ if (catFindings && catFindings.length > 0) {
7439
+ const meta = t.securityHubSubCategories[cat];
7440
+ const shLabel = t.moduleNames[`sh:${cat}`] ?? meta?.label ?? cat;
7441
+ shSubCats.push({
7442
+ key: cat,
7443
+ label: shLabel,
7444
+ count: catFindings.length,
7445
+ findings: catFindings
7446
+ });
7447
+ }
7448
+ }
7449
+ }
7450
+ const DETECTION_ONLY_MODULES = /* @__PURE__ */ new Set([
7451
+ "guardduty_findings",
7452
+ "inspector_findings",
7453
+ "config_rules_findings",
7454
+ "access_analyzer_findings"
7455
+ ]);
7456
+ const barChartModules = modules.flatMap((m) => {
7457
+ if (DETECTION_ONLY_MODULES.has(m.module)) return [];
7458
+ if (m.module === "security_hub_findings" && shSubCats.length > 0) {
7459
+ return shSubCats.map((sc) => ({
7460
+ ...m,
7461
+ module: t.moduleNames[`sh:${sc.key}`] ?? sc.key,
7462
+ findingsCount: sc.count,
7463
+ findings: sc.findings
7464
+ }));
7465
+ }
7466
+ return [{ ...m, module: t.moduleNames[m.module] ?? m.module }];
7467
+ });
7799
7468
  let top5Html = "";
7800
7469
  if (allFindings.length > 0) {
7801
7470
  const top5 = [...allFindings].sort((a, b) => b.riskScore - a.riskScore).slice(0, 5);
@@ -7861,34 +7530,51 @@ ${rest}
7861
7530
  if (!moduleMap.has(mod)) moduleMap.set(mod, []);
7862
7531
  moduleMap.get(mod).push(f);
7863
7532
  }
7864
- const moduleEntries = [...moduleMap.entries()].sort((a, b) => {
7533
+ const expandedEntries = [];
7534
+ for (const [mod, findings] of moduleMap.entries()) {
7535
+ if (DETECTION_ONLY_MODULES.has(mod)) continue;
7536
+ if (mod === "security_hub_findings" && shSubCats.length > 0) {
7537
+ for (const sc of shSubCats) {
7538
+ expandedEntries.push([sc.key, sc.findings, sc.label]);
7539
+ }
7540
+ } else {
7541
+ expandedEntries.push([mod, findings, null]);
7542
+ }
7543
+ }
7544
+ const moduleEntries = expandedEntries.sort((a, b) => {
7865
7545
  const aHasCritHigh = a[1].some((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
7866
7546
  const bHasCritHigh = b[1].some((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
7867
7547
  if (aHasCritHigh !== bHasCritHigh) return aHasCritHigh ? -1 : 1;
7868
7548
  return b[1].length - a[1].length;
7869
7549
  });
7870
- findingsHtml = moduleEntries.map(([modName, modFindings]) => {
7871
- const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
7872
- for (const f of modFindings) sevCounts[f.severity]++;
7873
- 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(" ");
7874
- const sevGroups = SEVERITY_ORDER2.map((sev) => {
7875
- const findings = modFindings.filter((f) => f.severity === sev);
7876
- if (findings.length === 0) return "";
7877
- findings.sort((a, b) => b.riskScore - a.riskScore);
7550
+ const renderSeverityGroups = (findings) => {
7551
+ return SEVERITY_ORDER2.map((sev) => {
7552
+ const sevFindings = findings.filter((f) => f.severity === sev);
7553
+ if (sevFindings.length === 0) return "";
7554
+ sevFindings.sort((a, b) => b.riskScore - a.riskScore);
7878
7555
  const emoji = SEV_EMOJI[sev] ?? "";
7879
7556
  const label = sev.charAt(0) + sev.slice(1).toLowerCase();
7880
7557
  return `<details class="severity-group-fold">
7881
- <summary><h4>${emoji} ${label} (${findings.length})</h4></summary>
7882
- ${renderCards(findings)}
7558
+ <summary><h4>${emoji} ${label} (${sevFindings.length})</h4></summary>
7559
+ ${renderCards(sevFindings)}
7883
7560
  </details>`;
7884
7561
  }).filter(Boolean).join("\n");
7562
+ };
7563
+ const renderModuleBadges = (findings) => {
7564
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
7565
+ for (const f of findings) sevCounts[f.severity]++;
7566
+ 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(" ");
7567
+ };
7568
+ findingsHtml = moduleEntries.map(([modName, modFindings, subCatLabel]) => {
7569
+ const badges = renderModuleBadges(modFindings);
7570
+ const displayName = subCatLabel ?? (t.moduleNames[modName] ?? modName);
7885
7571
  return `<details class="module-fold">
7886
7572
  <summary>
7887
- <h3>&#128274; ${esc(modName)} (${modFindings.length})</h3>
7573
+ <h3>&#128274; ${esc(displayName)} (${modFindings.length})</h3>
7888
7574
  <span class="module-badges">${badges}</span>
7889
7575
  </summary>
7890
7576
  <div class="module-body">
7891
- ${sevGroups}
7577
+ ${renderSeverityGroups(modFindings)}
7892
7578
  </div>
7893
7579
  </details>`;
7894
7580
  }).join("\n");
@@ -7908,8 +7594,29 @@ ${rest}
7908
7594
  </div>
7909
7595
  </section>`;
7910
7596
  }
7911
- const statsRows = modules.map(
7912
- (m) => `<tr><td>${esc(m.module)}</td><td>${m.resourcesScanned}</td><td>${m.findingsCount}</td><td>${m.status === "success" ? "&#10003;" : "&#10007;"}</td></tr>`
7597
+ const isModuleDisabled = (m) => {
7598
+ if (!m.warnings?.length) return void 0;
7599
+ const w = m.warnings.find(
7600
+ (w2) => SERVICE_NOT_ENABLED_PATTERNS.some((p) => w2.includes(p))
7601
+ );
7602
+ return w;
7603
+ };
7604
+ const statsRows = modules.flatMap(
7605
+ (m) => {
7606
+ if (DETECTION_ONLY_MODULES.has(m.module)) return [];
7607
+ if (m.module === "security_hub_findings" && shSubCats.length > 0) {
7608
+ return shSubCats.map(
7609
+ (sc) => `<tr><td>${esc(sc.label)}</td><td>${m.resourcesScanned}</td><td>${sc.count}</td><td>&#10003;</td></tr>`
7610
+ );
7611
+ }
7612
+ const disabledWarning = isModuleDisabled(m);
7613
+ if (disabledWarning) {
7614
+ const rec = t.serviceRecommendations[m.module];
7615
+ const reason = rec ? rec.action : disabledWarning;
7616
+ return [`<tr><td>${esc(t.moduleNames[m.module] ?? m.module)}</td><td>-</td><td>-</td><td style="color:#eab308">&#9888; ${esc(reason)}</td></tr>`];
7617
+ }
7618
+ 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>`];
7619
+ }
7913
7620
  ).join("\n");
7914
7621
  let recsHtml = "";
7915
7622
  if (summary.totalFindings > 0) {
@@ -7917,6 +7624,9 @@ ${rest}
7917
7624
  const kbPatches = [];
7918
7625
  let kbSeverity = "LOW";
7919
7626
  let kbUrl;
7627
+ const cveList = [];
7628
+ let cveSeverity = "LOW";
7629
+ let cveUrl;
7920
7630
  const genericPatterns = ["See References", "None Provided", "Review the finding", "Review and remediate."];
7921
7631
  for (const f of allFindings) {
7922
7632
  const rem = f.remediationSteps[0] ?? "Review and remediate.";
@@ -7929,6 +7639,13 @@ ${rest}
7929
7639
  if (!kbUrl && url) kbUrl = url;
7930
7640
  continue;
7931
7641
  }
7642
+ const cveMatch = f.title.match(/CVE-[\d-]+/);
7643
+ if (cveMatch && (f.module === "security_hub_findings" || f.module === "inspector_findings")) {
7644
+ cveList.push(cveMatch[0]);
7645
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(cveSeverity)) cveSeverity = f.severity;
7646
+ if (!cveUrl && url) cveUrl = url;
7647
+ continue;
7648
+ }
7932
7649
  if (f.module === "security_hub_findings") {
7933
7650
  const controlMatch = f.title.match(/^([A-Z][A-Za-z0-9]*\.\d+)\s/);
7934
7651
  if (controlMatch) {
@@ -7945,6 +7662,23 @@ ${rest}
7945
7662
  continue;
7946
7663
  }
7947
7664
  }
7665
+ if (f.module !== "security_hub_findings" && f.module !== "inspector_findings") {
7666
+ const template = getRecommendationTemplate(rem);
7667
+ if (template !== rem) {
7668
+ const templateKey = `tmpl:${f.module}:${template}`;
7669
+ const existingTmpl = recMap.get(templateKey);
7670
+ if (existingTmpl) {
7671
+ existingTmpl.count++;
7672
+ if (!existingTmpl.url && url) existingTmpl.url = url;
7673
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(existingTmpl.severity)) {
7674
+ existingTmpl.severity = f.severity;
7675
+ }
7676
+ continue;
7677
+ }
7678
+ recMap.set(templateKey, { text: rem, severity: f.severity, count: 1, url });
7679
+ continue;
7680
+ }
7681
+ }
7948
7682
  const existing = recMap.get(rem);
7949
7683
  if (existing) {
7950
7684
  existing.count++;
@@ -7961,8 +7695,14 @@ ${rest}
7961
7695
  const kbList = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
7962
7696
  recMap.set("__kb__", { text: t.installWindowsPatches(unique.length, kbList), severity: kbSeverity, count: 1, url: kbUrl });
7963
7697
  }
7698
+ if (cveList.length > 0) {
7699
+ const unique = [...new Set(cveList)];
7700
+ const cveDisplay = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
7701
+ 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`;
7702
+ recMap.set("__cve__", { text: cveText, severity: cveSeverity, count: 1, url: cveUrl });
7703
+ }
7964
7704
  for (const [key, rec] of recMap) {
7965
- if (key.startsWith("ctrl:") && rec.count > 1) {
7705
+ if ((key.startsWith("ctrl:") || key.startsWith("tmpl:")) && rec.count > 1) {
7966
7706
  rec.text += ` \u2014 ${t.affectedResources(rec.count)}`;
7967
7707
  rec.count = 1;
7968
7708
  }
@@ -8029,7 +7769,7 @@ ${remaining.map(renderRec).join("\n")}
8029
7769
  </div>
8030
7770
  <div class="chart-box">
8031
7771
  <div class="chart-title">${esc(t.findingsByModule)}</div>
8032
- ${barChart(modules, t.allModulesClean)}
7772
+ ${barChart(barChartModules, t.allModulesClean)}
8033
7773
  </div>
8034
7774
  </section>
8035
7775
 
@@ -8202,6 +7942,9 @@ ${itemsHtml}
8202
7942
  const mlpsKbPatches = [];
8203
7943
  let mlpsKbSeverity = "LOW";
8204
7944
  let mlpsKbUrl;
7945
+ const mlpsCveList = [];
7946
+ let mlpsCveSeverity = "LOW";
7947
+ let mlpsCveUrl;
8205
7948
  const mlpsGenericPatterns = ["See References", "None Provided", "Review the finding", "Review and remediate."];
8206
7949
  for (const r of failedResults) {
8207
7950
  for (const f of r.relatedFindings) {
@@ -8215,6 +7958,13 @@ ${itemsHtml}
8215
7958
  if (!mlpsKbUrl && url) mlpsKbUrl = url;
8216
7959
  continue;
8217
7960
  }
7961
+ const cveMatch = f.title.match(/CVE-[\d-]+/);
7962
+ if (cveMatch) {
7963
+ mlpsCveList.push(cveMatch[0]);
7964
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(mlpsCveSeverity)) mlpsCveSeverity = f.severity;
7965
+ if (!mlpsCveUrl && url) mlpsCveUrl = url;
7966
+ continue;
7967
+ }
8218
7968
  if (f.module === "security_hub_findings") {
8219
7969
  const controlMatch = f.title.match(/^([A-Z][A-Za-z0-9]*\.\d+)\s/);
8220
7970
  if (controlMatch) {
@@ -8231,6 +7981,23 @@ ${itemsHtml}
8231
7981
  continue;
8232
7982
  }
8233
7983
  }
7984
+ if (f.module !== "security_hub_findings" && f.module !== "inspector_findings") {
7985
+ const template = getRecommendationTemplate(rem);
7986
+ if (template !== rem) {
7987
+ const templateKey = `tmpl:${f.module}:${template}`;
7988
+ const existingTmpl = mlpsRecMap.get(templateKey);
7989
+ if (existingTmpl) {
7990
+ existingTmpl.count++;
7991
+ if (!existingTmpl.url && url) existingTmpl.url = url;
7992
+ if (SEVERITY_ORDER2.indexOf(f.severity) < SEVERITY_ORDER2.indexOf(existingTmpl.severity)) {
7993
+ existingTmpl.severity = f.severity;
7994
+ }
7995
+ continue;
7996
+ }
7997
+ mlpsRecMap.set(templateKey, { text: rem, severity: f.severity, count: 1, url });
7998
+ continue;
7999
+ }
8000
+ }
8234
8001
  const existing = mlpsRecMap.get(rem);
8235
8002
  if (existing) {
8236
8003
  existing.count++;
@@ -8248,8 +8015,14 @@ ${itemsHtml}
8248
8015
  const kbList = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
8249
8016
  mlpsRecMap.set("__kb__", { text: t.installWindowsPatches(unique.length, kbList), severity: mlpsKbSeverity, count: 1, url: mlpsKbUrl });
8250
8017
  }
8018
+ if (mlpsCveList.length > 0) {
8019
+ const unique = [...new Set(mlpsCveList)];
8020
+ const cveDisplay = unique.slice(0, 5).join(", ") + (unique.length > 5 ? ", \u2026" : "");
8021
+ 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`;
8022
+ mlpsRecMap.set("__cve__", { text: cveText, severity: mlpsCveSeverity, count: 1, url: mlpsCveUrl });
8023
+ }
8251
8024
  for (const [key, rec] of mlpsRecMap) {
8252
- if (key.startsWith("ctrl:") && rec.count > 1) {
8025
+ if ((key.startsWith("ctrl:") || key.startsWith("tmpl:")) && rec.count > 1) {
8253
8026
  rec.text += ` \u2014 ${t.affectedResources(rec.count)}`;
8254
8027
  rec.count = 1;
8255
8028
  }
@@ -8545,7 +8318,6 @@ Detects which AWS security services are enabled and assesses overall security ma
8545
8318
  - **GuardDuty not enabled** \u2014 Risk 7.5: Provides continuous threat detection.
8546
8319
  - **Inspector not enabled** \u2014 Risk 6.0: Scans for software vulnerabilities.
8547
8320
  - **AWS Config not enabled** \u2014 Risk 6.0: Tracks configuration changes.
8548
- - **Macie not enabled** \u2014 Risk 5.0: Detects sensitive data in S3 (not available in China regions).
8549
8321
  - CloudTrail detection is included for coverage metrics.
8550
8322
 
8551
8323
  ### Maturity Levels
@@ -8553,8 +8325,8 @@ Detects which AWS security services are enabled and assesses overall security ma
8553
8325
  |------------------|-------|
8554
8326
  | 0\u20131 | Basic |
8555
8327
  | 2\u20133 | Intermediate |
8556
- | 4\u20135 | Advanced |
8557
- | 6 | Comprehensive |
8328
+ | 4 | Advanced |
8329
+ | 5 | Comprehensive |
8558
8330
 
8559
8331
  ## 2. Security Hub Findings (security_hub_findings)
8560
8332
  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.
@@ -8688,7 +8460,7 @@ import { readFileSync as readFileSync2 } from "fs";
8688
8460
  import { join as join2, dirname } from "path";
8689
8461
  import { fileURLToPath } from "url";
8690
8462
  var MODULE_DESCRIPTIONS = {
8691
- service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config, Macie) are enabled and assesses security maturity.",
8463
+ service_detection: "Detects which AWS security services (Security Hub, GuardDuty, Inspector, Config) are enabled and assesses security maturity.",
8692
8464
  secret_exposure: "Checks Lambda env vars and EC2 userData for exposed secrets (AWS keys, private keys, passwords).",
8693
8465
  ssl_certificate: "Checks ACM certificates for expiry, failed status, and upcoming renewals.",
8694
8466
  dns_dangling: "Checks Route53 CNAME records for dangling DNS (subdomain takeover risk).",
@@ -9123,14 +8895,12 @@ function createServer(defaultRegion) {
9123
8895
  "Security Hub": "+300 security checks",
9124
8896
  "GuardDuty": "Threat detection",
9125
8897
  "Inspector": "Vulnerability scanning",
9126
- "AWS Config": "Configuration tracking",
9127
- "Macie": "Sensitive data detection"
8898
+ "AWS Config": "Configuration tracking"
9128
8899
  };
9129
8900
  const serviceFreeTrials = {
9130
8901
  "Security Hub": true,
9131
8902
  "GuardDuty": true,
9132
- "Inspector": true,
9133
- "Macie": true
8903
+ "Inspector": true
9134
8904
  };
9135
8905
  const services = detection.services;
9136
8906
  const coveragePercent = detection.coveragePercent;
@@ -9165,7 +8935,7 @@ function createServer(defaultRegion) {
9165
8935
  lines.push("");
9166
8936
  lines.push("### Recommendations (Priority Order)");
9167
8937
  lines.push("");
9168
- const priorityOrder = ["Security Hub", "GuardDuty", "Inspector", "AWS Config", "Macie", "CloudTrail"];
8938
+ const priorityOrder = ["Security Hub", "GuardDuty", "Inspector", "AWS Config", "CloudTrail"];
9169
8939
  const sorted = disabled.sort(
9170
8940
  (a, b) => priorityOrder.indexOf(a.name) - priorityOrder.indexOf(b.name)
9171
8941
  );
@@ -9188,7 +8958,7 @@ function createServer(defaultRegion) {
9188
8958
  const nextMilestones = {
9189
8959
  basic: { level: "Intermediate", target: 2, suggestions: ["Security Hub", "GuardDuty"] },
9190
8960
  intermediate: { level: "Advanced", target: 4, suggestions: ["Inspector", "AWS Config"] },
9191
- advanced: { level: "Comprehensive", target: 6, suggestions: ["Macie"] }
8961
+ advanced: { level: "Comprehensive", target: 5, suggestions: ["CloudTrail"] }
9192
8962
  };
9193
8963
  const next = nextMilestones[maturityLevel];
9194
8964
  if (next) {