diff --git a/docs/user-guide/cli/tutorials/configuration_file.mdx b/docs/user-guide/cli/tutorials/configuration_file.mdx index e2b05e39db..e637ea9c9a 100644 --- a/docs/user-guide/cli/tutorials/configuration_file.mdx +++ b/docs/user-guide/cli/tutorials/configuration_file.mdx @@ -78,6 +78,7 @@ The following list includes all the AWS checks with configurable variables that | `elasticache_redis_cluster_backup_enabled` | `minimum_snapshot_retention_period` | Integer | `7` | | `elb_is_in_multiple_az` | `elb_min_azs` | Integer | `2` | | `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer | `2` | +| `elbv2_listener_pqc_tls_enabled` | `elbv2_listener_pqc_tls_allowed_policies` | List of Strings | See `config.yaml` | | `eventbridge_bus_cross_account_access` | `trusted_account_ids` | List of Strings | `[]` | | `eventbridge_schema_registry_cross_account_access` | `trusted_account_ids` | List of Strings | `[]` | | `glue_etl_jobs_no_secrets_in_arguments` | `secrets_ignore_patterns` | List of Strings | `[]` | diff --git a/prowler/changelog.d/elbv2-listener-pqc-tls-enabled.added.md b/prowler/changelog.d/elbv2-listener-pqc-tls-enabled.added.md new file mode 100644 index 0000000000..044d0bb0f4 --- /dev/null +++ b/prowler/changelog.d/elbv2-listener-pqc-tls-enabled.added.md @@ -0,0 +1 @@ +`elbv2_listener_pqc_tls_enabled` check for AWS provider, verifying that ELBv2 listeners use post-quantum TLS policies diff --git a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json index a025bb3a3c..9dd009afb3 100644 --- a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json +++ b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json @@ -1181,6 +1181,7 @@ "elb_insecure_ssl_ciphers", "elb_ssl_listeners", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/ccc_aws.json b/prowler/compliance/aws/ccc_aws.json index 7935424193..ef31bfe1ea 100644 --- a/prowler/compliance/aws/ccc_aws.json +++ b/prowler/compliance/aws/ccc_aws.json @@ -49,6 +49,7 @@ "elb_insecure_ssl_ciphers", "elb_ssl_listeners", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/ens_rd2022_aws.json b/prowler/compliance/aws/ens_rd2022_aws.json index 144437ce52..f81b8a3eea 100644 --- a/prowler/compliance/aws/ens_rd2022_aws.json +++ b/prowler/compliance/aws/ens_rd2022_aws.json @@ -2495,6 +2495,7 @@ ], "Checks": [ "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled" @@ -2521,6 +2522,7 @@ ], "Checks": [ "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled" diff --git a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json index eaa3ea25dc..13c4624572 100644 --- a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json @@ -1377,6 +1377,7 @@ "Checks": [ "apigateway_restapi_client_certificate_enabled", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", @@ -1399,6 +1400,7 @@ "Checks": [ "apigateway_restapi_client_certificate_enabled", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/ffiec_aws.json b/prowler/compliance/aws/ffiec_aws.json index 8a50b79925..4a2a1b943e 100644 --- a/prowler/compliance/aws/ffiec_aws.json +++ b/prowler/compliance/aws/ffiec_aws.json @@ -615,6 +615,7 @@ "Checks": [ "apigateway_restapi_client_certificate_enabled", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json b/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json index 871af9e726..76ad0c74a5 100644 --- a/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json +++ b/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json @@ -266,6 +266,7 @@ "ec2_ebs_default_encryption", "efs_encryption_at_rest_enabled", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/iso27001_2013_aws.json b/prowler/compliance/aws/iso27001_2013_aws.json index 1de8c23db8..c7ce030024 100644 --- a/prowler/compliance/aws/iso27001_2013_aws.json +++ b/prowler/compliance/aws/iso27001_2013_aws.json @@ -36,6 +36,7 @@ "Checks": [ "elb_insecure_ssl_ciphers", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled" diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json index 7b0446ac3f..65a6e8c8b8 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -2064,6 +2064,7 @@ "elb_ssl_listeners", "elb_ssl_listeners_use_acm_certificate", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", @@ -3142,6 +3143,7 @@ "elb_ssl_listeners_use_acm_certificate", "elbv2_desync_mitigation_mode", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json index 40b338ce41..b99a36d38e 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -2066,6 +2066,7 @@ "elb_ssl_listeners", "elb_ssl_listeners_use_acm_certificate", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", @@ -3145,6 +3146,7 @@ "elb_ssl_listeners_use_acm_certificate", "elbv2_desync_mitigation_mode", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/nist_800_171_revision_2_aws.json b/prowler/compliance/aws/nist_800_171_revision_2_aws.json index 921bd33a53..31c45c6ae3 100644 --- a/prowler/compliance/aws/nist_800_171_revision_2_aws.json +++ b/prowler/compliance/aws/nist_800_171_revision_2_aws.json @@ -717,6 +717,7 @@ "apigateway_restapi_client_certificate_enabled", "ec2_ebs_volume_encryption", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/nist_800_53_revision_5_aws.json b/prowler/compliance/aws/nist_800_53_revision_5_aws.json index e0ef936229..70864860bf 100644 --- a/prowler/compliance/aws/nist_800_53_revision_5_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_5_aws.json @@ -5592,6 +5592,7 @@ "Checks": [ "apigateway_restapi_client_certificate_enabled", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/rbi_cyber_security_framework_aws.json b/prowler/compliance/aws/rbi_cyber_security_framework_aws.json index 5de1f5ca8a..554b40cdad 100644 --- a/prowler/compliance/aws/rbi_cyber_security_framework_aws.json +++ b/prowler/compliance/aws/rbi_cyber_security_framework_aws.json @@ -40,6 +40,7 @@ "ec2_instance_public_ip", "efs_encryption_at_rest_enabled", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/compliance/aws/secnumcloud_3.2_aws.json b/prowler/compliance/aws/secnumcloud_3.2_aws.json index 701f931b05..8075dc3610 100644 --- a/prowler/compliance/aws/secnumcloud_3.2_aws.json +++ b/prowler/compliance/aws/secnumcloud_3.2_aws.json @@ -490,6 +490,7 @@ "elbv2_ssl_listeners", "elb_insecure_ssl_ciphers", "elbv2_insecure_ssl_ciphers", + "elbv2_listener_pqc_tls_enabled", "cloudfront_distributions_pqc_tls_enabled", "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 6d2803be56..aba7fd6481 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -424,6 +424,21 @@ aws: - "SecurityPolicy_TLS13_1_2_PFS_PQ_2025_09" - "SecurityPolicy_TLS13_1_2_PQ_2025_09" + # aws.elbv2_listener_pqc_tls_enabled + # Allowed post-quantum TLS security policies for ELBv2 HTTPS/TLS listeners + elbv2_listener_pqc_tls_allowed_policies: + - "ELBSecurityPolicy-TLS13-1-2-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext1-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext2-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-3-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext0-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext1-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext2-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Res-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09" + # aws.rolesanywhere_trust_anchor_pqc_pki # Allowed post-quantum key algorithms for AWS Private CAs backing IAM Roles Anywhere trust anchors rolesanywhere_pqc_pca_key_algorithms: diff --git a/prowler/config/schema/aws.py b/prowler/config/schema/aws.py index 4e52093029..7ac12d60de 100644 --- a/prowler/config/schema/aws.py +++ b/prowler/config/schema/aws.py @@ -415,6 +415,10 @@ class AWSProviderConfig(ProviderConfigBase): le=6, description="Min AZs an Application/Network LB must span. Range: 1..6.", ) + elbv2_listener_pqc_tls_allowed_policies: Optional[list[str]] = Field( + default=None, + description="ELBv2 SSL policies that satisfy the PQ TLS listener check.", + ) # --- ElastiCache ----------------------------------------------------- minimum_snapshot_retention_period: Optional[int] = Field( diff --git a/prowler/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/__init__.py b/prowler/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled.metadata.json new file mode 100644 index 0000000000..d86778d5ae --- /dev/null +++ b/prowler/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "aws", + "CheckID": "elbv2_listener_pqc_tls_enabled", + "CheckTitle": "ELBv2 HTTPS/TLS listeners use a post-quantum TLS security policy", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "elbv2", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "AwsElbv2LoadBalancer", + "ResourceGroup": "network", + "Description": "**ELBv2 HTTPS and TLS listeners** are assessed for use of **post-quantum (PQ) TLS security policies**. Listeners whose `SslPolicy` is not in the approved PQ set lack hybrid key exchange (ML-KEM 768 + ECDHE), which can increase harvest-now-decrypt-later exposure for recorded traffic.", + "Risk": "Without PQ-ready TLS policies, encrypted traffic captured today may be stored for future cryptanalysis if a **cryptographically relevant quantum computer** becomes available (**harvest-now, decrypt-later** attack). PQ-ready TLS policies reduce this long-term confidentiality risk for sensitive data, credentials, and session tokens transmitted through the load balancer.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/describe-ssl-policies.html", + "https://aws.amazon.com/security/post-quantum-cryptography/", + "https://csrc.nist.gov/projects/post-quantum-cryptography" + ], + "Remediation": { + "Code": { + "CLI": "aws elbv2 modify-listener --listener-arn --ssl-policy ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::ElasticLoadBalancingV2::Listener\n Properties:\n LoadBalancerArn: \n Protocol: HTTPS\n Port: 443\n DefaultActions:\n - Type: forward\n TargetGroupArn: \n Certificates:\n - CertificateArn: \n SslPolicy: ELBSecurityPolicy-TLS13-1-2-PQ-2025-09 # FIX: uses a post-quantum TLS policy\n```", + "Other": "1. In the AWS Console, go to EC2 > Load Balancers\n2. Select the load balancer and open the Listeners tab\n3. Select the HTTPS/TLS listener and choose Edit\n4. Set Security policy to ELBSecurityPolicy-TLS13-1-2-PQ-2025-09 (or any approved PQ policy)\n5. Save changes", + "Terraform": "```hcl\nresource \"aws_lb_listener\" \"\" {\n load_balancer_arn = \"\"\n port = 443\n protocol = \"HTTPS\"\n ssl_policy = \"ELBSecurityPolicy-TLS13-1-2-PQ-2025-09\" # FIX: post-quantum TLS policy\n certificate_arn = \"\"\n\n default_action {\n type = \"forward\"\n target_group_arn = \"\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Migrate all ELBv2 HTTPS and TLS listeners to a **post-quantum TLS policy** (`ELBSecurityPolicy-TLS13-*-PQ-2025-09` family) to enable hybrid key exchange (ML-KEM + ECDHE). Periodically review and update policies as AWS publishes new PQ-ready options.", + "Url": "https://hub.prowler.com/check/elbv2_listener_pqc_tls_enabled" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [ + "elbv2_insecure_ssl_ciphers" + ], + "Notes": "" +} diff --git a/prowler/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled.py b/prowler/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled.py new file mode 100644 index 0000000000..270a2cc31b --- /dev/null +++ b/prowler/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled.py @@ -0,0 +1,73 @@ +"""Check that ELBv2 HTTPS/TLS listeners use post-quantum TLS policies.""" + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.elbv2.elbv2_client import elbv2_client + +PQ_TLS_POLICIES_DEFAULT = [ + "ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext1-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext2-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-3-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext0-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext1-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext2-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Res-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09", +] + + +class elbv2_listener_pqc_tls_enabled(Check): + """Verify that every ELBv2 HTTPS or TLS listener uses a post-quantum TLS policy. + + This check evaluates whether each HTTPS (ALB) or TLS (NLB) listener on an + ELBv2 load balancer terminates TLS with a security policy that offers + post-quantum (PQ) hybrid key exchange (ML-KEM 768 combined with ECDHE). + - PASS: All HTTPS/TLS listeners on the load balancer use a PQ TLS policy. + - FAIL: At least one HTTPS/TLS listener uses a non-PQ TLS policy. + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the PQ TLS policy check for every ELBv2 load balancer. + + Returns: + A list of reports for load balancers with discovered listeners. + """ + findings = [] + pq_tls_policies = elbv2_client.audit_config.get( + "elbv2_listener_pqc_tls_allowed_policies", PQ_TLS_POLICIES_DEFAULT + ) + for lb in elbv2_client.loadbalancersv2.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=lb) + + if lb.listener_discovery_failed: + continue + + has_tls_listeners = False + non_pq_listeners = [] + for listener_arn, listener in lb.listeners.items(): + if listener.protocol in ("HTTPS", "TLS"): + has_tls_listeners = True + if listener.ssl_policy not in pq_tls_policies: + ssl_policy = listener.ssl_policy or "" + non_pq_listeners.append( + f"{listener.protocol}:{listener.port} ({listener_arn}) uses {ssl_policy}" + ) + + if not has_tls_listeners: + report.status = "PASS" + report.status_extended = f"ELBv2 {lb.name} has no HTTPS/TLS listeners." + findings.append(report) + continue + + if non_pq_listeners: + report.status = "FAIL" + report.status_extended = f"ELBv2 {lb.name} has HTTPS/TLS listeners without post-quantum TLS policy: {', '.join(non_pq_listeners)}." + else: + report.status = "PASS" + report.status_extended = f"ELBv2 {lb.name} has all HTTPS/TLS listeners using a post-quantum TLS policy." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/elbv2/elbv2_service.py b/prowler/providers/aws/services/elbv2/elbv2_service.py index c52110869f..e2d9bd3682 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_service.py +++ b/prowler/providers/aws/services/elbv2/elbv2_service.py @@ -86,10 +86,14 @@ class ELBv2(AWSService): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) else: + load_balancer[1].listener_discovery_failed = True + load_balancer[1].listener_discovery_error = str(error) logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) except Exception as error: + load_balancer[1].listener_discovery_failed = True + load_balancer[1].listener_discovery_error = str(error) logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) @@ -209,6 +213,8 @@ class LoadBalancerv2(BaseModel): drop_invalid_header_fields: Optional[str] cross_zone_load_balancing: Optional[str] listeners: Dict[str, Listenerv2] = {} + listener_discovery_failed: bool = False + listener_discovery_error: Optional[str] = None scheme: Optional[str] security_groups: list[str] = [] # Key: ZoneName, Value: SubnetId diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 365efbc0c9..d10c384b0d 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -320,6 +320,19 @@ config_aws = { "minimum_snapshot_retention_period": 7, "elb_min_azs": 2, "elbv2_min_azs": 2, + "elbv2_listener_pqc_tls_allowed_policies": [ + "ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext1-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext2-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-3-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext0-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext1-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext2-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Res-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09", + ], "secrets_ignore_patterns": [], "max_days_secret_unused": 90, "max_days_secret_unrotated": 90, diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index 39cba5f27d..df663e923a 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -362,6 +362,21 @@ aws: # Minimum number of Availability Zones that an ELBv2 must be in elbv2_min_azs: 2 + # aws.elbv2_listener_pqc_tls_enabled + # Allowed post-quantum TLS security policies for ELBv2 HTTPS/TLS listeners + elbv2_listener_pqc_tls_allowed_policies: + - "ELBSecurityPolicy-TLS13-1-2-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext1-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext2-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-3-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext0-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext1-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Ext2-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-2-Res-FIPS-PQ-2025-09" + - "ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09" + # AWS Elasticache Configuration # aws.elasticache_redis_cluster_backup_enabled # Minimum number of days that a Redis cluster must have backups retention period diff --git a/tests/config/schema/aws_schema_test.py b/tests/config/schema/aws_schema_test.py index d37a2e0bf6..838853f52a 100644 --- a/tests/config/schema/aws_schema_test.py +++ b/tests/config/schema/aws_schema_test.py @@ -176,6 +176,33 @@ class Test_AWS_Enums: assert _validate({"ecr_repository_vulnerability_minimum_severity": level}) == {} +class TestAWSELBv2PQCTLSAllowedPolicies: + def test_valid_policy_list_round_trips(self): + policies = [ + "ELBSecurityPolicy-TLS13-1-2-Res-2021-06", + "ELBSecurityPolicy-TLS13-1-3-2021-06", + ] + + assert _validate({"elbv2_listener_pqc_tls_allowed_policies": policies}) == { + "elbv2_listener_pqc_tls_allowed_policies": policies + } + + def test_key_is_exposed_in_scan_config_schema(self): + aws_properties = SCAN_CONFIG_SCHEMA["properties"]["aws"]["properties"] + + assert "elbv2_listener_pqc_tls_allowed_policies" in aws_properties + + @pytest.mark.parametrize( + "value", + [ + "ELBSecurityPolicy-TLS13-1-2-Res-2021-06", + ["ELBSecurityPolicy-TLS13-1-2-Res-2021-06", 123], + ], + ) + def test_invalid_policy_values_are_dropped(self, value): + assert _validate({"elbv2_listener_pqc_tls_allowed_policies": value}) == {} + + class Test_AWS_Secrets_Ignore_Files: def test_valid_file_patterns_round_trip(self): files = ["*.deps.json", "vendor/*.js"] diff --git a/tests/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled_test.py b/tests/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled_test.py new file mode 100644 index 0000000000..12f4d5a4dd --- /dev/null +++ b/tests/providers/aws/services/elbv2/elbv2_listener_pqc_tls_enabled/elbv2_listener_pqc_tls_enabled_test.py @@ -0,0 +1,673 @@ +"""Tests for elbv2_listener_pqc_tls_enabled check.""" + +from unittest import mock + +import pytest +from boto3 import client, resource +from botocore.exceptions import ClientError +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_EU_WEST_1_AZA, + AWS_REGION_EU_WEST_1_AZB, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_elbv2_listener_pqc_tls_enabled: + """Test cases for the elbv2_listener_pqc_tls_enabled check.""" + + def _create_alb_infrastructure(self, region=AWS_REGION_EU_WEST_1): + """Helper to create VPC, subnets, security group, target group, and ALB. + + Returns a tuple of (elbv2_client, lb_response, target_group_arn). + """ + conn = client("elbv2", region_name=region) + ec2 = resource("ec2", region_name=region) + + security_group = ec2.create_security_group( + GroupName="a-security-group", Description="First One" + ) + vpc = ec2.create_vpc(CidrBlock="172.28.7.0/24", InstanceTenancy="default") + subnet1 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.192/26", + AvailabilityZone=f"{region}a", + ) + subnet2 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.0/26", + AvailabilityZone=f"{region}b", + ) + + lb = conn.create_load_balancer( + Name="my-lb", + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme="internal", + Type="application", + )["LoadBalancers"][0] + + response = conn.create_target_group( + Name="a-target", + Protocol="HTTP", + Port=8080, + VpcId=vpc.id, + HealthCheckProtocol="HTTP", + HealthCheckPort="8080", + HealthCheckPath="/", + HealthCheckIntervalSeconds=5, + HealthCheckTimeoutSeconds=3, + HealthyThresholdCount=5, + UnhealthyThresholdCount=2, + Matcher={"HttpCode": "200"}, + ) + target_group_arn = response["TargetGroups"][0]["TargetGroupArn"] + + return conn, lb, target_group_arn + + def _create_nlb_infrastructure(self, region=AWS_REGION_EU_WEST_1): + """Helper to create VPC, subnets, target group, and NLB. + + Returns a tuple of (elbv2_client, lb_response, target_group_arn). + """ + conn = client("elbv2", region_name=region) + ec2 = resource("ec2", region_name=region) + + vpc = ec2.create_vpc(CidrBlock="172.28.7.0/24", InstanceTenancy="default") + subnet1 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.192/26", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + ) + subnet2 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.0/26", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZB, + ) + + lb = conn.create_load_balancer( + Name="my-nlb", + Subnets=[subnet1.id, subnet2.id], + Scheme="internal", + Type="network", + )["LoadBalancers"][0] + + response = conn.create_target_group( + Name="a-target", + Protocol="TCP", + Port=8080, + VpcId=vpc.id, + ) + target_group_arn = response["TargetGroups"][0]["TargetGroupArn"] + + return conn, lb, target_group_arn + + def _mock_and_execute(self, audit_config=None): + """Helper to set up mocks and execute the check. + + Must be called inside a @mock_aws decorated method, after AWS + resources have been created with moto. + """ + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + audit_config = audit_config or {} + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1], + create_default_organization=False, + audit_config=audit_config, + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1], + audit_config=audit_config, + ), + ), + mock.patch( + "prowler.providers.aws.services.elbv2.elbv2_listener_pqc_tls_enabled.elbv2_listener_pqc_tls_enabled.elbv2_client", + new=ELBv2(aws_provider), + ), + ): + from prowler.providers.aws.services.elbv2.elbv2_listener_pqc_tls_enabled.elbv2_listener_pqc_tls_enabled import ( + elbv2_listener_pqc_tls_enabled, + ) + + check = elbv2_listener_pqc_tls_enabled() + return check.execute() + + def _assert_listener_arn_in_status(self, result, listener_arn): + """Assert that remediation details identify the affected listener ARN.""" + assert listener_arn in result[0].status_extended + + # ------------------------------------------------------------------ + # No-resource scenarios + # ------------------------------------------------------------------ + + @mock_aws + def test_no_load_balancers(self): + """Test that no findings are returned when there are no load balancers.""" + result = self._mock_and_execute() + assert len(result) == 0 + + @mock_aws + def test_lb_with_http_listener_only(self): + """Test PASS when a load balancer has no HTTPS/TLS listeners.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTP", + Port=80, + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "ELBv2 my-lb has no HTTPS/TLS listeners." + assert result[0].resource_id == "my-lb" + + # ------------------------------------------------------------------ + # PASS scenarios + # ------------------------------------------------------------------ + + @pytest.mark.parametrize( + "ssl_policy", + [ + "ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext1-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext2-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-3-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext0-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext1-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Ext2-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-2-Res-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09", + ], + ) + @mock_aws + def test_listener_with_pq_policy_pass(self, ssl_policy): + """Test PASS when HTTPS listener uses an allowed PQ TLS policy.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy=ssl_policy, + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "ELBv2 my-lb has all HTTPS/TLS listeners using a post-quantum TLS policy." + ) + assert result[0].resource_id == "my-lb" + assert result[0].resource_arn == lb["LoadBalancerArn"] + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + def test_multiple_https_listeners_all_pq_pass(self): + """Test PASS when a LB has multiple HTTPS listeners all using PQ policies.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=8443, + SslPolicy="ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "ELBv2 my-lb has all HTTPS/TLS listeners using a post-quantum TLS policy." + ) + assert result[0].resource_id == "my-lb" + + @mock_aws + def test_mixed_http_and_pq_https_listeners_pass(self): + """Test PASS when LB has both HTTP and HTTPS listeners, HTTPS using PQ policy.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + # HTTP listener (out of scope) + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTP", + Port=80, + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + # HTTPS listener with PQ policy + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == "my-lb" + + # ------------------------------------------------------------------ + # FAIL scenarios + # ------------------------------------------------------------------ + + @mock_aws + def test_listener_with_classical_tls_policy_fail(self): + """Test FAIL when HTTPS listener uses a classical (non-PQ) TLS policy.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + listener = conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-2021-06", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + )["Listeners"][0] + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ELBv2 my-lb has HTTPS/TLS listeners without post-quantum TLS policy: HTTPS:443 ({listener['ListenerArn']}) uses ELBSecurityPolicy-TLS13-1-2-2021-06." + ) + self._assert_listener_arn_in_status(result, listener["ListenerArn"]) + assert result[0].resource_id == "my-lb" + assert result[0].resource_arn == lb["LoadBalancerArn"] + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + def test_listener_with_tls_1_0_pq_policy_fails_by_default(self): + """Test FAIL when HTTPS listener uses a TLS 1.0-minimum PQ policy by default.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + listener = conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-0-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + )["Listeners"][0] + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ELBv2 my-lb has HTTPS/TLS listeners without post-quantum TLS policy: HTTPS:443 ({listener['ListenerArn']}) uses ELBSecurityPolicy-TLS13-1-0-PQ-2025-09." + ) + assert result[0].resource_id == "my-lb" + + @mock_aws + def test_listener_with_empty_ssl_policy_fail(self): + """Test FAIL when an HTTPS listener has no SSL policy value.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + listener = conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-2021-06", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + )["Listeners"][0] + + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1], + create_default_organization=False, + ) + service = ELBv2(aws_provider) + service.loadbalancersv2[lb["LoadBalancerArn"]].listeners[ + listener["ListenerArn"] + ].ssl_policy = "" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.elbv2.elbv2_listener_pqc_tls_enabled.elbv2_listener_pqc_tls_enabled.elbv2_client", + new=service, + ), + ): + from prowler.providers.aws.services.elbv2.elbv2_listener_pqc_tls_enabled.elbv2_listener_pqc_tls_enabled import ( + elbv2_listener_pqc_tls_enabled, + ) + + result = elbv2_listener_pqc_tls_enabled().execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ELBv2 my-lb has HTTPS/TLS listeners without post-quantum TLS policy: HTTPS:443 ({listener['ListenerArn']}) uses ." + ) + assert result[0].resource_id == "my-lb" + + @mock_aws + def test_listener_with_legacy_policy_fail(self): + """Test FAIL when HTTPS listener uses a legacy TLS policy.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + listener = conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS-1-1-2017-01", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + )["Listeners"][0] + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ELBv2 my-lb has HTTPS/TLS listeners without post-quantum TLS policy: HTTPS:443 ({listener['ListenerArn']}) uses ELBSecurityPolicy-TLS-1-1-2017-01." + ) + self._assert_listener_arn_in_status(result, listener["ListenerArn"]) + assert result[0].resource_id == "my-lb" + assert result[0].resource_arn == lb["LoadBalancerArn"] + + @mock_aws + def test_mixed_pq_and_non_pq_listeners_fail(self): + """Test FAIL when LB has one PQ listener and one non-PQ listener.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + # PQ listener + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + # Non-PQ listener + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=8443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-2021-06", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "ELBSecurityPolicy-TLS13-1-2-2021-06" in result[0].status_extended + assert result[0].resource_id == "my-lb" + + @mock_aws + def test_multiple_non_pq_listeners_lists_all_policies_fail(self): + """Test FAIL lists all non-PQ policies when multiple listeners are non-compliant.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS-1-1-2017-01", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=8443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-2021-06", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Both non-PQ policies should be mentioned + assert "ELBSecurityPolicy-TLS-1-1-2017-01" in result[0].status_extended + assert "ELBSecurityPolicy-TLS13-1-2-2021-06" in result[0].status_extended + assert result[0].resource_id == "my-lb" + + # ------------------------------------------------------------------ + # Custom audit_config scenario + # ------------------------------------------------------------------ + + @mock_aws + def test_custom_audit_config_narrows_allowlist(self): + """Test that a custom audit_config allowlist is honoured. + + When elbv2_listener_pqc_tls_allowed_policies is overridden to only + allow FIPS PQ policies, a non-FIPS PQ policy should FAIL. + """ + conn, lb, target_group_arn = self._create_alb_infrastructure() + # Use a PQ policy that is in the default list but NOT in our custom list + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + custom_config = { + "elbv2_listener_pqc_tls_allowed_policies": [ + "ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09", + ] + } + + result = self._mock_and_execute(audit_config=custom_config) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "ELBSecurityPolicy-TLS13-1-2-PQ-2025-09" in result[0].status_extended + + @mock_aws + def test_custom_audit_config_fips_policy_pass(self): + """Test PASS when listener uses a FIPS PQ policy and custom config allows it.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + custom_config = { + "elbv2_listener_pqc_tls_allowed_policies": [ + "ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09", + "ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09", + ] + } + + result = self._mock_and_execute(audit_config=custom_config) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "ELBv2 my-lb has all HTTPS/TLS listeners using a post-quantum TLS policy." + ) + + @mock_aws + def test_custom_audit_config_allows_tls_1_0_pq_policy(self): + """Test PASS when custom config explicitly allows a TLS 1.0-minimum PQ policy.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-0-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute( + audit_config={ + "elbv2_listener_pqc_tls_allowed_policies": [ + "ELBSecurityPolicy-TLS13-1-0-PQ-2025-09" + ] + } + ) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "ELBv2 my-lb has all HTTPS/TLS listeners using a post-quantum TLS policy." + ) + + @mock_aws + def test_empty_audit_config_allowlist_fails_tls_listener(self): + """Test an intentionally empty allowlist is honoured.""" + conn, lb, target_group_arn = self._create_alb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="HTTPS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute( + audit_config={"elbv2_listener_pqc_tls_allowed_policies": []} + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == "my-lb" + assert "ELBSecurityPolicy-TLS13-1-2-PQ-2025-09" in result[0].status_extended + + @mock_aws + def test_tls_listener_with_pq_policy_pass(self): + """Test PASS when a TLS listener uses an allowed PQ TLS policy.""" + conn, lb, target_group_arn = self._create_nlb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="TLS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-PQ-2025-09", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == "my-nlb" + assert result[0].resource_arn == lb["LoadBalancerArn"] + + @mock_aws + def test_nlb_with_tcp_listener_only(self): + """Test PASS when an NLB has no HTTPS/TLS listeners.""" + conn, lb, target_group_arn = self._create_nlb_infrastructure() + conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="TCP", + Port=80, + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + ) + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "ELBv2 my-nlb has no HTTPS/TLS listeners." + assert result[0].resource_id == "my-nlb" + assert result[0].resource_arn == lb["LoadBalancerArn"] + + @mock_aws + def test_tls_listener_with_non_pq_policy_fail(self): + """Test FAIL when a TLS listener uses a non-PQ TLS policy.""" + conn, lb, target_group_arn = self._create_nlb_infrastructure() + listener = conn.create_listener( + LoadBalancerArn=lb["LoadBalancerArn"], + Protocol="TLS", + Port=443, + SslPolicy="ELBSecurityPolicy-TLS13-1-2-2021-06", + DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}], + )["Listeners"][0] + + result = self._mock_and_execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ELBv2 my-nlb has HTTPS/TLS listeners without post-quantum TLS policy: TLS:443 ({listener['ListenerArn']}) uses ELBSecurityPolicy-TLS13-1-2-2021-06." + ) + self._assert_listener_arn_in_status(result, listener["ListenerArn"]) + assert result[0].resource_id == "my-nlb" + assert result[0].resource_arn == lb["LoadBalancerArn"] + + @mock_aws + def test_listener_discovery_failure_returns_no_findings(self): + """Test no findings when listeners cannot be retrieved for a load balancer.""" + conn, lb, _ = self._create_alb_infrastructure() + + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1], + create_default_organization=False, + ) + + service = ELBv2(aws_provider) + error = ClientError( + { + "Error": { + "Code": "AccessDenied", + "Message": "User is not authorized to perform: elasticloadbalancing:DescribeListeners", + } + }, + "DescribeListeners", + ) + service.loadbalancersv2[lb["LoadBalancerArn"]].listeners = {} + service.regional_clients[AWS_REGION_EU_WEST_1] = mock.MagicMock( + region=AWS_REGION_EU_WEST_1, + get_paginator=mock.MagicMock(side_effect=error), + ) + service._describe_listeners( + (lb["LoadBalancerArn"], service.loadbalancersv2[lb["LoadBalancerArn"]]) + ) + + assert service.loadbalancersv2[lb["LoadBalancerArn"]].listener_discovery_failed + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.elbv2.elbv2_listener_pqc_tls_enabled.elbv2_listener_pqc_tls_enabled.elbv2_client", + new=service, + ), + ): + from prowler.providers.aws.services.elbv2.elbv2_listener_pqc_tls_enabled.elbv2_listener_pqc_tls_enabled import ( + elbv2_listener_pqc_tls_enabled, + ) + + result = elbv2_listener_pqc_tls_enabled().execute() + + assert len(result) == 0