From ca48fd07199541bafb924d8b07ad39e844e0e921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Mon, 22 Jun 2026 17:12:01 +0200 Subject: [PATCH] feat(aws): add apigateway_domain_name_pqc_tls_enabled check (#11316) Co-authored-by: Hugo P.Brito --- contrib/k8s/helm/prowler-api/values.yaml | 7 + .../cli/tutorials/configuration_file.mdx | 1 + permissions/prowler-additions-policy.json | 4 +- .../cloudformation/prowler-scan-role.yml | 2 + prowler/CHANGELOG.md | 1 + ...itected_framework_security_pillar_aws.json | 1 + prowler/compliance/aws/ccc_aws.json | 1 + prowler/compliance/aws/ens_rd2022_aws.json | 2 + .../aws/fedramp_moderate_revision_4_aws.json | 2 + prowler/compliance/aws/ffiec_aws.json | 1 + .../aws/gxp_21_cfr_part_11_aws.json | 1 + prowler/compliance/aws/iso27001_2013_aws.json | 1 + .../compliance/aws/kisa_isms_p_2023_aws.json | 2 + .../aws/kisa_isms_p_2023_korean_aws.json | 2 + .../aws/nist_800_171_revision_2_aws.json | 1 + .../aws/nist_800_53_revision_5_aws.json | 2 + .../aws/rbi_cyber_security_framework_aws.json | 1 + .../compliance/aws/secnumcloud_3.2_aws.json | 1 + prowler/config/config.yaml | 8 + .../__init__.py | 0 ..._domain_name_pqc_tls_enabled.metadata.json | 41 +++ .../apigateway_domain_name_pqc_tls_enabled.py | 55 ++++ .../services/apigateway/apigateway_service.py | 53 +++- ...ateway_domain_name_pqc_tls_enabled_test.py | 243 ++++++++++++++++++ .../apigateway/apigateway_service_test.py | 23 ++ 25 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/__init__.py create mode 100644 prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled.metadata.json create mode 100644 prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled.py create mode 100644 tests/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled_test.py diff --git a/contrib/k8s/helm/prowler-api/values.yaml b/contrib/k8s/helm/prowler-api/values.yaml index 72c4b26088..db861f7b88 100644 --- a/contrib/k8s/helm/prowler-api/values.yaml +++ b/contrib/k8s/helm/prowler-api/values.yaml @@ -438,6 +438,13 @@ mainConfig: # Minimum number of Availability Zones that an ELBv2 must be in elbv2_min_azs: 2 + # AWS Post-Quantum TLS Configuration + # aws.apigateway_domain_name_pqc_tls_enabled + apigateway_pqc_tls_allowed_policies: + - "SecurityPolicy_TLS13_1_2_FIPS_PFS_PQ_2025_09" + - "SecurityPolicy_TLS13_1_2_PFS_PQ_2025_09" + - "SecurityPolicy_TLS13_1_2_PQ_2025_09" + # AWS Post-Quantum SSH Key Exchange Configuration # aws.transfer_server_pqc_ssh_kex_enabled transfer_pqc_ssh_allowed_policies: diff --git a/docs/user-guide/cli/tutorials/configuration_file.mdx b/docs/user-guide/cli/tutorials/configuration_file.mdx index 4b918c869f..58f30f413d 100644 --- a/docs/user-guide/cli/tutorials/configuration_file.mdx +++ b/docs/user-guide/cli/tutorials/configuration_file.mdx @@ -55,6 +55,7 @@ The following list includes all the AWS checks with configurable variables that | `elasticache_redis_cluster_backup_enabled` | `minimum_snapshot_retention_period` | Integer | | `elb_is_in_multiple_az` | `elb_min_azs` | Integer | | `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer | +| `apigateway_domain_name_pqc_tls_enabled` | `apigateway_pqc_tls_allowed_policies` | List of Strings | | `guardduty_is_enabled` | `mute_non_default_regions` | Boolean | | `iam_user_access_not_stale_to_sagemaker` | `max_unused_sagemaker_access_days` | Integer | | `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer | diff --git a/permissions/prowler-additions-policy.json b/permissions/prowler-additions-policy.json index 906f06fbfb..37579edb35 100644 --- a/permissions/prowler-additions-policy.json +++ b/permissions/prowler-additions-policy.json @@ -61,7 +61,9 @@ ], "Resource": [ "arn:*:apigateway:*::/restapis/*", - "arn:*:apigateway:*::/apis/*" + "arn:*:apigateway:*::/apis/*", + "arn:*:apigateway:*::/domainnames", + "arn:*:apigateway:*::/domainnames/*" ], "Sid": "AllowAPIGatewayReadOnly" } diff --git a/permissions/templates/cloudformation/prowler-scan-role.yml b/permissions/templates/cloudformation/prowler-scan-role.yml index 395fed6424..2ab6d41289 100644 --- a/permissions/templates/cloudformation/prowler-scan-role.yml +++ b/permissions/templates/cloudformation/prowler-scan-role.yml @@ -150,6 +150,8 @@ Resources: Resource: - "arn:*:apigateway:*::/restapis/*" - "arn:*:apigateway:*::/apis/*" + - "arn:*:apigateway:*::/domainnames" + - "arn:*:apigateway:*::/domainnames/*" - !If - OrganizationsEnabled - PolicyName: ProwlerOrganizations diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 7dd9d94d3f..003ed6b11f 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -43,6 +43,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checks across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the Cloudflare provider, mapping existing Cloudflare edge/network checks across the applicable DORA pillars [(#11645)](https://github.com/prowler-cloud/prowler/pull/11645) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the AlibabaCloud provider, mapping existing AlibabaCloud checks across the applicable DORA pillars [(#11646)](https://github.com/prowler-cloud/prowler/pull/11646) +- `apigateway_domain_name_pqc_tls_enabled` check for AWS provider to verify API Gateway custom domain names use a post-quantum TLS security policy [(#11316)](https://github.com/prowler-cloud/prowler/pull/11316) - `transfer_server_pqc_ssh_kex_enabled` check for AWS provider to verify Transfer Family servers use a post-quantum hybrid SSH key exchange security policy [(#11315)](https://github.com/prowler-cloud/prowler/pull/11315) ### 🔄 Changed 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 e99760d8be..8002f5d61c 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 @@ -1151,6 +1151,7 @@ "elb_insecure_ssl_ciphers", "elb_ssl_listeners", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elbv2_ssl_listeners", "s3_bucket_secure_transport_policy" diff --git a/prowler/compliance/aws/ccc_aws.json b/prowler/compliance/aws/ccc_aws.json index 298b803c1c..7fec0c7d71 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", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elbv2_ssl_listeners", "elbv2_nlb_tls_termination_enabled", diff --git a/prowler/compliance/aws/ens_rd2022_aws.json b/prowler/compliance/aws/ens_rd2022_aws.json index 850ffb6850..7b0658c040 100644 --- a/prowler/compliance/aws/ens_rd2022_aws.json +++ b/prowler/compliance/aws/ens_rd2022_aws.json @@ -2367,6 +2367,7 @@ ], "Checks": [ "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled" ] }, @@ -2391,6 +2392,7 @@ ], "Checks": [ "elbv2_insecure_ssl_ciphers", + "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 1090931bf5..11ce0fc381 100644 --- a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json @@ -1145,6 +1145,7 @@ "Checks": [ "apigateway_restapi_client_certificate_enabled", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elb_ssl_listeners", "opensearch_service_domains_node_to_node_encryption_enabled", @@ -1165,6 +1166,7 @@ "Checks": [ "apigateway_restapi_client_certificate_enabled", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elb_ssl_listeners", "opensearch_service_domains_node_to_node_encryption_enabled", diff --git a/prowler/compliance/aws/ffiec_aws.json b/prowler/compliance/aws/ffiec_aws.json index ad5ff3068f..5a7fa14bc3 100644 --- a/prowler/compliance/aws/ffiec_aws.json +++ b/prowler/compliance/aws/ffiec_aws.json @@ -487,6 +487,7 @@ "Checks": [ "apigateway_restapi_client_certificate_enabled", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elb_ssl_listeners", "s3_bucket_secure_transport_policy" 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 29b21c3a69..022a778bad 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", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elb_ssl_listeners", "opensearch_service_domains_encryption_at_rest_enabled", diff --git a/prowler/compliance/aws/iso27001_2013_aws.json b/prowler/compliance/aws/iso27001_2013_aws.json index 4709458f8b..791aea8993 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", + "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 526c656c5a..734a92e8d3 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -2040,6 +2040,7 @@ "elb_ssl_listeners", "elb_ssl_listeners_use_acm_certificate", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elbv2_nlb_tls_termination_enabled", "elbv2_ssl_listeners", @@ -3091,6 +3092,7 @@ "elb_ssl_listeners_use_acm_certificate", "elbv2_desync_mitigation_mode", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elbv2_internet_facing", "elbv2_listeners_underneath", 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 1cc50d15af..05ef1c0082 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -2042,6 +2042,7 @@ "elb_ssl_listeners", "elb_ssl_listeners_use_acm_certificate", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elbv2_nlb_tls_termination_enabled", "elbv2_ssl_listeners", @@ -3094,6 +3095,7 @@ "elb_ssl_listeners_use_acm_certificate", "elbv2_desync_mitigation_mode", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elbv2_internet_facing", "elbv2_listeners_underneath", 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 35b67d75b0..d5a55398ce 100644 --- a/prowler/compliance/aws/nist_800_171_revision_2_aws.json +++ b/prowler/compliance/aws/nist_800_171_revision_2_aws.json @@ -653,6 +653,7 @@ "apigateway_restapi_client_certificate_enabled", "ec2_ebs_volume_encryption", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "opensearch_service_domains_node_to_node_encryption_enabled", "s3_bucket_default_encryption", 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 ac25515f93..a1cf795225 100644 --- a/prowler/compliance/aws/nist_800_53_revision_5_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_5_aws.json @@ -5262,6 +5262,7 @@ "Checks": [ "apigateway_restapi_client_certificate_enabled", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elb_ssl_listeners", "opensearch_service_domains_node_to_node_encryption_enabled", @@ -5550,6 +5551,7 @@ ], "Checks": [ "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elb_ssl_listeners" ] diff --git a/prowler/compliance/aws/rbi_cyber_security_framework_aws.json b/prowler/compliance/aws/rbi_cyber_security_framework_aws.json index 22c3774959..46d2411902 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", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "elb_ssl_listeners", "ec2_ebs_default_encryption", diff --git a/prowler/compliance/aws/secnumcloud_3.2_aws.json b/prowler/compliance/aws/secnumcloud_3.2_aws.json index 794de281de..c49e7c201a 100644 --- a/prowler/compliance/aws/secnumcloud_3.2_aws.json +++ b/prowler/compliance/aws/secnumcloud_3.2_aws.json @@ -474,6 +474,7 @@ "elbv2_ssl_listeners", "elb_insecure_ssl_ciphers", "elbv2_insecure_ssl_ciphers", + "apigateway_domain_name_pqc_tls_enabled", "transfer_server_pqc_ssh_kex_enabled", "redshift_cluster_in_transit_encryption_enabled", "elasticache_redis_cluster_in_transit_encryption_enabled", diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index f9dda5dc92..004fa1207a 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -380,6 +380,14 @@ aws: # Minimum number of Availability Zones that an ELBv2 must be in elbv2_min_azs: 2 + # AWS Post-Quantum TLS Configuration + # aws.apigateway_domain_name_pqc_tls_enabled + # Allowed post-quantum TLS security policies for API Gateway custom domain names + apigateway_pqc_tls_allowed_policies: + - "SecurityPolicy_TLS13_1_2_FIPS_PFS_PQ_2025_09" + - "SecurityPolicy_TLS13_1_2_PFS_PQ_2025_09" + - "SecurityPolicy_TLS13_1_2_PQ_2025_09" + # AWS Post-Quantum SSH Key Exchange Configuration # aws.transfer_server_pqc_ssh_kex_enabled # Allowed AWS Transfer Family security policies with post-quantum SSH key exchange diff --git a/prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/__init__.py b/prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled.metadata.json b/prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled.metadata.json new file mode 100644 index 0000000000..276f6e1741 --- /dev/null +++ b/prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "aws", + "CheckID": "apigateway_domain_name_pqc_tls_enabled", + "CheckTitle": "API Gateway custom domain names use a post-quantum TLS security policy", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "apigateway", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "AwsApiGatewayDomainName", + "ResourceGroup": "network", + "Description": "**API Gateway custom domain names** for REST APIs are assessed for use of a **post-quantum (PQ) TLS security policy** such as `SecurityPolicy_TLS13_1_2_PQ_2025_09`. Custom domains with legacy policies such as `TLS_1_0` or `TLS_1_2` lack hybrid ML-KEM key exchange, leaving captured traffic vulnerable to future quantum decryption.", + "Risk": "Without a PQ-ready TLS policy, traffic to API Gateway custom domains captured today can be decrypted once a **cryptographically relevant quantum computer** exists (**harvest-now, decrypt-later** attack). This threatens long-term **confidentiality** of API payloads, credentials, and bearer tokens.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-custom-domain-tls-version.html", + "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-security-policies-list.html", + "https://aws.amazon.com/security/post-quantum-cryptography/", + "https://csrc.nist.gov/projects/post-quantum-cryptography" + ], + "Remediation": { + "Code": { + "CLI": "aws apigateway update-domain-name --domain-name --patch-operations op=replace,path=/securityPolicy,value=SecurityPolicy_TLS13_1_2_PQ_2025_09", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::ApiGateway::DomainName\n Properties:\n DomainName: api.example.com\n RegionalCertificateArn: \n SecurityPolicy: SecurityPolicy_TLS13_1_2_PQ_2025_09 # FIX: enhanced post-quantum security policy\n EndpointConfiguration:\n Types:\n - REGIONAL\n```", + "Other": "1. In the AWS Console, go to API Gateway > Custom domain names\n2. Select the custom domain and choose Edit on Domain name configurations\n3. Set Security policy to SecurityPolicy_TLS13_1_2_PQ_2025_09 (post-quantum) and Endpoint access mode to Strict\n4. Save the changes", + "Terraform": "```hcl\nresource \"aws_api_gateway_domain_name\" \"\" {\n domain_name = \"api.example.com\"\n regional_certificate_arn = \"\"\n security_policy = \"SecurityPolicy_TLS13_1_2_PQ_2025_09\" # FIX: enhanced post-quantum security policy\n endpoint_configuration {\n types = [\"REGIONAL\"]\n }\n}\n```" + }, + "Recommendation": { + "Text": "Migrate every API Gateway custom domain name to an **enhanced post-quantum TLS policy** such as `SecurityPolicy_TLS13_1_2_PQ_2025_09` that enables hybrid ML-KEM key exchange. Note that you must also enable Strict endpoint access mode and that mutual TLS is not supported on enhanced policies. Review allowed policies regularly as AWS publishes new PQ-ready options.", + "Url": "https://hub.prowler.com/check/apigateway_domain_name_pqc_tls_enabled" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "API Gateway HTTP and WebSocket APIs only support the legacy TLS_1_2 security policy and therefore cannot use post-quantum TLS today; this check evaluates REST API custom domain names only." +} diff --git a/prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled.py b/prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled.py new file mode 100644 index 0000000000..d9c2d071f9 --- /dev/null +++ b/prowler/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled.py @@ -0,0 +1,55 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.apigateway.apigateway_client import ( + apigateway_client, +) + +PQC_APIGATEWAY_POLICIES_DEFAULT = [ + "SecurityPolicy_TLS13_1_2_FIPS_PFS_PQ_2025_09", + "SecurityPolicy_TLS13_1_2_PFS_PQ_2025_09", + "SecurityPolicy_TLS13_1_2_PQ_2025_09", +] + + +def _get_allowed_policies(configured_policies: object) -> list[str]: + if not isinstance(configured_policies, list): + return PQC_APIGATEWAY_POLICIES_DEFAULT + + return configured_policies + + +class apigateway_domain_name_pqc_tls_enabled(Check): + """Verify that every API Gateway custom domain name uses a post-quantum TLS policy. + + A custom domain name PASSES when its ``securityPolicy`` belongs to the + configured allowlist of enhanced post-quantum policies. + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the API Gateway custom domain post-quantum TLS check. + + Returns: + A list of reports for API Gateway custom domain names and their + post-quantum TLS policy compliance status. + """ + findings = [] + pqc_policies = _get_allowed_policies( + apigateway_client.audit_config.get("apigateway_pqc_tls_allowed_policies") + ) + for domain in apigateway_client.domain_names: + report = Check_Report_AWS(metadata=self.metadata(), resource=domain) + policy = domain.security_policy or "" + if domain.security_policy in pqc_policies: + report.status = "PASS" + report.status_extended = ( + f"API Gateway custom domain {domain.name} uses post-quantum " + f"TLS policy {policy}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"API Gateway custom domain {domain.name} uses TLS policy " + f"{policy}, which is not in the post-quantum allowlist." + ) + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/apigateway/apigateway_service.py b/prowler/providers/aws/services/apigateway/apigateway_service.py index 4f887a2da8..099551f440 100644 --- a/prowler/providers/aws/services/apigateway/apigateway_service.py +++ b/prowler/providers/aws/services/apigateway/apigateway_service.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Optional from botocore.exceptions import ClientError from pydantic.v1 import BaseModel @@ -13,12 +13,45 @@ class APIGateway(AWSService): # Call AWSService's __init__ super().__init__(__class__.__name__, provider) self.rest_apis = [] + self.domain_names = [] self.__threading_call__(self._get_rest_apis) + self.__threading_call__(self._get_domain_names) self._get_authorizers() self._get_rest_api() self._get_stages() self._get_resources() + def _get_domain_names(self, regional_client: Any) -> None: + """Get API Gateway custom domain names for a regional client. + + Args: + regional_client: Regional API Gateway boto3 client used to list + custom domain names. + """ + logger.info("APIGateway - Getting custom domain names...") + try: + paginator = regional_client.get_paginator("get_domain_names") + for page in paginator.paginate(): + for item in page.get("items", []): + domain_name = item.get("domainName", "") + arn = f"arn:{self.audited_partition}:apigateway:{regional_client.region}::/domainnames/{domain_name}" + if not self.audit_resources or ( + is_resource_filtered(arn, self.audit_resources) + ): + self.domain_names.append( + DomainName( + name=domain_name, + arn=arn, + region=regional_client.region, + security_policy=item.get("securityPolicy", ""), + tags=[item.get("tags", {})], + ) + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + def _get_rest_apis(self, regional_client): logger.info("APIGateway - Getting Rest APIs...") try: @@ -249,3 +282,21 @@ class RestAPI(BaseModel): stages: list[Stage] = [] tags: Optional[list] = [] resources: list[PathResourceMethods] = [] + + +class DomainName(BaseModel): + """API Gateway custom domain name metadata. + + Attributes: + name: Custom domain name. + arn: Custom domain name ARN. + region: AWS region where the custom domain name exists. + security_policy: TLS security policy configured for the custom domain. + tags: Custom domain tags. + """ + + name: str + arn: str + region: str + security_policy: str = "" + tags: Optional[list] = [] diff --git a/tests/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled_test.py b/tests/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled_test.py new file mode 100644 index 0000000000..6d460be32e --- /dev/null +++ b/tests/providers/aws/services/apigateway/apigateway_domain_name_pqc_tls_enabled/apigateway_domain_name_pqc_tls_enabled_test.py @@ -0,0 +1,243 @@ +from unittest import mock + +from moto import mock_aws + +from prowler.providers.aws.services.apigateway.apigateway_service import DomainName +from tests.providers.aws.utils import ( + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +DOMAIN_NAME = "api.example.com" +DOMAIN_ARN = f"arn:aws:apigateway:{AWS_REGION_US_EAST_1}::/domainnames/{DOMAIN_NAME}" + + +def _build_client(security_policy: str): + apigw_client = mock.MagicMock() + apigw_client.audit_config = {} + apigw_client.domain_names = [ + DomainName( + name=DOMAIN_NAME, + arn=DOMAIN_ARN, + region=AWS_REGION_US_EAST_1, + security_policy=security_policy, + ) + ] + return apigw_client + + +class Test_apigateway_domain_name_pqc_tls_enabled: + @mock_aws + def test_no_domains(self): + apigw_client = mock.MagicMock() + apigw_client.audit_config = {} + apigw_client.domain_names = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled.apigateway_client", + new=apigw_client, + ): + from prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled import ( + apigateway_domain_name_pqc_tls_enabled, + ) + + check = apigateway_domain_name_pqc_tls_enabled() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_tls13_only_policy_fails_by_default(self): + apigw_client = _build_client("SecurityPolicy_TLS13_1_3_2025_09") + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled.apigateway_client", + new=apigw_client, + ): + from prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled import ( + apigateway_domain_name_pqc_tls_enabled, + ) + + check = apigateway_domain_name_pqc_tls_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "SecurityPolicy_TLS13_1_3_2025_09" in result[0].status_extended + assert "not in the post-quantum allowlist" in result[0].status_extended + assert result[0].resource_id == DOMAIN_NAME + assert result[0].resource_arn == DOMAIN_ARN + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_alternate_pq_policy(self): + apigw_client = _build_client("SecurityPolicy_TLS13_1_2_PQ_2025_09") + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled.apigateway_client", + new=apigw_client, + ): + from prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled import ( + apigateway_domain_name_pqc_tls_enabled, + ) + + check = apigateway_domain_name_pqc_tls_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + "SecurityPolicy_TLS13_1_2_PQ_2025_09" in result[0].status_extended + ) + + @mock_aws + def test_legacy_tls_1_2(self): + apigw_client = _build_client("TLS_1_2") + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled.apigateway_client", + new=apigw_client, + ): + from prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled import ( + apigateway_domain_name_pqc_tls_enabled, + ) + + check = apigateway_domain_name_pqc_tls_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "TLS_1_2" in result[0].status_extended + assert "not in the post-quantum allowlist" in result[0].status_extended + + @mock_aws + def test_missing_security_policy(self): + apigw_client = _build_client("") + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled.apigateway_client", + new=apigw_client, + ): + from prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled import ( + apigateway_domain_name_pqc_tls_enabled, + ) + + check = apigateway_domain_name_pqc_tls_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "" in result[0].status_extended + + @mock_aws + def test_configurable_allowlist(self): + apigw_client = _build_client("TLS_1_2") + apigw_client.audit_config = { + "apigateway_pqc_tls_allowed_policies": [ + "TLS_1_2", + ] + } + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled.apigateway_client", + new=apigw_client, + ): + from prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled import ( + apigateway_domain_name_pqc_tls_enabled, + ) + + check = apigateway_domain_name_pqc_tls_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + + @mock_aws + def test_null_config_uses_default_allowlist(self): + apigw_client = _build_client("SecurityPolicy_TLS13_1_2_PQ_2025_09") + apigw_client.audit_config = { + "apigateway_pqc_tls_allowed_policies": None, + } + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled.apigateway_client", + new=apigw_client, + ): + from prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled import ( + apigateway_domain_name_pqc_tls_enabled, + ) + + check = apigateway_domain_name_pqc_tls_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + + @mock_aws + def test_non_iterable_config_uses_default_allowlist(self): + apigw_client = _build_client("SecurityPolicy_TLS13_1_2_PQ_2025_09") + apigw_client.audit_config = { + "apigateway_pqc_tls_allowed_policies": 123, + } + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled.apigateway_client", + new=apigw_client, + ): + from prowler.providers.aws.services.apigateway.apigateway_domain_name_pqc_tls_enabled.apigateway_domain_name_pqc_tls_enabled import ( + apigateway_domain_name_pqc_tls_enabled, + ) + + check = apigateway_domain_name_pqc_tls_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" diff --git a/tests/providers/aws/services/apigateway/apigateway_service_test.py b/tests/providers/aws/services/apigateway/apigateway_service_test.py index 9396693ec3..c153af2871 100644 --- a/tests/providers/aws/services/apigateway/apigateway_service_test.py +++ b/tests/providers/aws/services/apigateway/apigateway_service_test.py @@ -206,3 +206,26 @@ class Test_APIGateway_Service: assert list(apigateway.rest_apis[0].resources[1].resource_methods.values()) == [ "AWS_IAM" ] + + # Test APIGateway _get_domain_names + @mock_aws + def test_get_domain_names(self): + apigateway_client = client("apigateway", region_name=AWS_REGION_US_EAST_1) + + apigateway_client.create_domain_name( + domainName="api.example.com", + securityPolicy="SecurityPolicy_TLS13_1_3_2025_09", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + apigateway = APIGateway(aws_provider) + + assert len(apigateway.domain_names) == 1 + domain = apigateway.domain_names[0] + assert domain.name == "api.example.com" + assert domain.region == AWS_REGION_US_EAST_1 + assert domain.security_policy == "SecurityPolicy_TLS13_1_3_2025_09" + assert ( + domain.arn + == f"arn:aws:apigateway:{AWS_REGION_US_EAST_1}::/domainnames/api.example.com" + )