From 236f142e330ea8147ff93135ee6dfb652956ec4a Mon Sep 17 00:00:00 2001 From: Brian Dwyer <2973273+bdwyertech@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:56:46 -0400 Subject: [PATCH 1/2] aws - wafv2 - fix wafv2-enabled filter ignoring CLOUDFRONT scope (#11039) --- c7n/filters/waf.py | 10 ++- c7n/resources/waf.py | 9 +++ .../cloudfront.ListDistributions_1.json | 72 +++++++++++++++++++ .../tagging.GetResources_1.json | 13 ++++ .../wafv2.ListWebACLs_1.json | 16 +++++ tests/test_cloudfront.py | 42 +++++++++++ 6 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 tests/data/placebo/test_distribution_wafv2_filter_scope/cloudfront.ListDistributions_1.json create mode 100644 tests/data/placebo/test_distribution_wafv2_filter_scope/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_distribution_wafv2_filter_scope/wafv2.ListWebACLs_1.json diff --git a/c7n/filters/waf.py b/c7n/filters/waf.py index 076be6f4b2c..dec64a67456 100644 --- a/c7n/filters/waf.py +++ b/c7n/filters/waf.py @@ -223,8 +223,14 @@ def _legacy_match(self, r_acl): # based on the scope def _get_web_acls(self, scope): if self._cached_web_acls is None: - self._cached_web_acls = self.manager.get_resource_manager('wafv2').resources( - query=dict(Scope=scope), + # Scope must be supplied as resource manager data rather than a query + # kwarg. The wafv2 manager derives both the API Scope and the region + # used for the call from its own data (CLOUDFRONT web acls are global + # and must be addressed via us-east-1), and a Scope passed via + # resources(query=...) is overwritten by the manager's own value. + wafv2 = self.manager.get_resource_manager( + 'wafv2', {'query': [{'Scope': scope}]}) + self._cached_web_acls = wafv2.resources( # required to get the additional detail needed for this filter (e.g. Rules), but # the legacy mode does not require additional detail augment=(not self._is_legacy) diff --git a/c7n/resources/waf.py b/c7n/resources/waf.py index eb586f474bd..0580a078f08 100644 --- a/c7n/resources/waf.py +++ b/c7n/resources/waf.py @@ -249,6 +249,15 @@ def get_permissions(self): # set REGIONAL for Scope as default def get_query_params(self, query_params): query_params = query_params or {} + # Scope is owned by the resource manager, since it also determines which + # region the wafv2 API is called in. Fail loudly rather than silently + # discarding a conflicting Scope supplied by the caller. + requested_scope = query_params.get('Scope') + if requested_scope is not None and requested_scope != self.manager.scope: + raise PolicyValidationError( + "wafv2 Scope conflict: %s was requested but the resource manager " + "scope is %s. Pass Scope via the resource manager's 'query' data." + % (requested_scope, self.manager.scope)) # Parse query from policy data queries = self.manager.data.get('query', []) for q in queries: diff --git a/tests/data/placebo/test_distribution_wafv2_filter_scope/cloudfront.ListDistributions_1.json b/tests/data/placebo/test_distribution_wafv2_filter_scope/cloudfront.ListDistributions_1.json new file mode 100644 index 00000000000..594e78a7bdc --- /dev/null +++ b/tests/data/placebo/test_distribution_wafv2_filter_scope/cloudfront.ListDistributions_1.json @@ -0,0 +1,72 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "DistributionList": { + "Marker": "", + "MaxItems": 100, + "IsTruncated": false, + "Quantity": 1, + "Items": [ + { + "Id": "E1EXAMPLE1234", + "ARN": "arn:aws:cloudfront::123456789012:distribution/E1EXAMPLE1234", + "Status": "Deployed", + "LastModifiedTime": { + "hour": 0, + "microsecond": 0, + "second": 0, + "year": 2024, + "month": 1, + "day": 1, + "minute": 0 + }, + "DomainName": "d111111abcdef8.cloudfront.net", + "Aliases": { + "Quantity": 0, + "Items": [] + }, + "Origins": { + "Quantity": 1, + "Items": [ + { + "Id": "S3-example", + "DomainName": "example-bucket.s3.amazonaws.com", + "OriginPath": "" + } + ] + }, + "DefaultCacheBehavior": { + "TargetOriginId": "S3-example", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "CacheBehaviors": { + "Quantity": 0, + "Items": [] + }, + "CustomErrorResponses": { + "Quantity": 0, + "Items": [] + }, + "Comment": "", + "PriceClass": "PriceClass_All", + "Enabled": true, + "ViewerCertificate": { + "CloudFrontDefaultCertificate": true, + "MinimumProtocolVersion": "TLSv1", + "CertificateSource": "cloudfront" + }, + "Restrictions": { + "GeoRestriction": { + "RestrictionType": "none", + "Quantity": 0 + } + }, + "WebACLId": "arn:aws:wafv2:us-east-1:123456789012:global/webacl/example-cloudfront-acl/11111111-2222-3333-4444-555555555555", + "HttpVersion": "http2", + "IsIPV6Enabled": true + } + ] + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_distribution_wafv2_filter_scope/tagging.GetResources_1.json b/tests/data/placebo/test_distribution_wafv2_filter_scope/tagging.GetResources_1.json new file mode 100644 index 00000000000..e20312f86da --- /dev/null +++ b/tests/data/placebo/test_distribution_wafv2_filter_scope/tagging.GetResources_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:cloudfront::123456789012:distribution/E1EXAMPLE1234", + "Tags": [] + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_distribution_wafv2_filter_scope/wafv2.ListWebACLs_1.json b/tests/data/placebo/test_distribution_wafv2_filter_scope/wafv2.ListWebACLs_1.json new file mode 100644 index 00000000000..6baff824a4b --- /dev/null +++ b/tests/data/placebo/test_distribution_wafv2_filter_scope/wafv2.ListWebACLs_1.json @@ -0,0 +1,16 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "NextMarker": "", + "WebACLs": [ + { + "Name": "example-cloudfront-acl", + "Id": "11111111-2222-3333-4444-555555555555", + "Description": "", + "LockToken": "22222222-3333-4444-5555-666666666666", + "ARN": "arn:aws:wafv2:us-east-1:123456789012:global/webacl/example-cloudfront-acl/11111111-2222-3333-4444-555555555555" + } + ] + } +} \ No newline at end of file diff --git a/tests/test_cloudfront.py b/tests/test_cloudfront.py index 9ab169b3563..c87bc827fc4 100644 --- a/tests/test_cloudfront.py +++ b/tests/test_cloudfront.py @@ -1,7 +1,9 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 from .common import BaseTest, event_data +from c7n.exceptions import PolicyValidationError from c7n.resources.aws import shape_validate +from c7n.resources.waf import WAFV2, DescribeWafV2 from c7n.utils import local_session, jmespath_compile from unittest.mock import MagicMock, patch from botocore.exceptions import ClientError @@ -1157,3 +1159,43 @@ def test_wafv2_cloudfront_scope_uses_us_east_1(self): manager = WAFV2(ctx, {}) manager.get_client() mock_session.client.assert_called_once_with('wafv2', region_name='us-west-2') + + def test_wafv2_filter_uses_cloudfront_scope(self): + """The wafv2-enabled filter must list CLOUDFRONT-scoped web acls. + + A distribution with a CLOUDFRONT WebACL attached must not be reported by + `state: false`. If the filter's requested scope is dropped in favor of the + REGIONAL default, the distribution's CLOUDFRONT WebACL ARN is never found + and the distribution is incorrectly reported as unprotected. + """ + factory = self.replay_flight_data("test_distribution_wafv2_filter_scope") + p = self.load_policy( + { + "name": "wafv2-cloudfront-scope", + "resource": "distribution", + "filters": [{"type": "wafv2-enabled", "state": False}], + }, + session_factory=factory, + ) + + scopes = [] + original = DescribeWafV2.get_query_params + + def record(self, query): + params = original(self, query) + scopes.append(params.get('Scope')) + return params + + with patch.object(DescribeWafV2, 'get_query_params', record): + resources = p.run() + + self.assertEqual(scopes, ['CLOUDFRONT']) + self.assertEqual(len(resources), 0) + + def test_wafv2_filter_scope_conflict_raises(self): + """A Scope passed via resources(query=) that conflicts with the resource + manager's own scope is an error rather than being silently ignored.""" + p = self.load_policy({"name": "wafv2", "resource": "distribution"}) + mgr = WAFV2(p.ctx, {'query': [{'Scope': 'REGIONAL'}]}) + with self.assertRaises(PolicyValidationError): + mgr.source.get_query_params({'Scope': 'CLOUDFRONT'}) From 05f6b10da89039a2766f5917454d9de310a45b41 Mon Sep 17 00:00:00 2001 From: James Troup Date: Fri, 4 Sep 2026 14:18:42 +0100 Subject: [PATCH 2/2] fix: aws - cloudfront - wafv2-enabled scope, stamp .post1 --- .github/workflows/stacklet-release.yml | 7 ++++--- c7n/version.py | 2 +- pyproject.toml | 2 +- tools/c7n_awscc/pyproject.toml | 2 +- tools/c7n_azure/pyproject.toml | 2 +- tools/c7n_gcp/pyproject.toml | 2 +- tools/c7n_kube/pyproject.toml | 2 +- tools/c7n_left/pyproject.toml | 2 +- tools/c7n_mailer/pyproject.toml | 2 +- tools/c7n_oci/pyproject.toml | 2 +- tools/c7n_openstack/pyproject.toml | 2 +- tools/c7n_org/pyproject.toml | 2 +- tools/c7n_policystream/pyproject.toml | 2 +- tools/c7n_sphinxext/pyproject.toml | 2 +- tools/c7n_tencentcloud/pyproject.toml | 2 +- uv.lock | 26 +++++++++++++------------- 16 files changed, 31 insertions(+), 30 deletions(-) diff --git a/.github/workflows/stacklet-release.yml b/.github/workflows/stacklet-release.yml index 94085d65580..8a1bd80cb87 100644 --- a/.github/workflows/stacklet-release.yml +++ b/.github/workflows/stacklet-release.yml @@ -26,15 +26,16 @@ jobs: persist-credentials: false # This repo only ever ships prereleases stamped with a +HASH local - # version segment (…aN+HASH). A version without both is a misconfiguration + # version segment (…aN+HASH, or …aN.postM+HASH for a hotfix on a + # release branch). A version without both is a misconfiguration # — fail loudly before building or publishing anything. - name: Assert prerelease version shell: bash run: | set -euo pipefail version=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2) - if [[ ! "$version" =~ a[0-9]+[+][0-9a-zA-Z.]+$ ]]; then - echo "::error::cloud-custodian-prerelease must ship a stamped prerelease version (…aN+HASH); got '$version'" + if [[ ! "$version" =~ a[0-9]+(\.post[0-9]+)?[+][0-9a-zA-Z.]+$ ]]; then + echo "::error::cloud-custodian-prerelease must ship a stamped prerelease version (…aN+HASH or …aN.postM+HASH); got '$version'" exit 1 fi echo "Releasing prerelease version $version" diff --git a/c7n/version.py b/c7n/version.py index dbfa72e41c2..e04cbc94a77 100644 --- a/c7n/version.py +++ b/c7n/version.py @@ -1,2 +1,2 @@ # Generated via tools/dev/devpkg.py -version = "0.9.52a5+a83dda5c9" +version = "0.9.52a5.post1+a83dda5c9" diff --git a/pyproject.toml b/pyproject.toml index 74928da9cf4..43b6d397fed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "urllib3>2", ] name = "c7n" -version = "0.9.52a5+a83dda5c9" +version = "0.9.52a5.post1+a83dda5c9" description = "Cloud Custodian - Policy Rules Engine" readme = "README.md" classifiers = [ diff --git a/tools/c7n_awscc/pyproject.toml b/tools/c7n_awscc/pyproject.toml index 66d0a7008e5..65f3ae02a7a 100644 --- a/tools/c7n_awscc/pyproject.toml +++ b/tools/c7n_awscc/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "jsonpatch<2.0,>=1.32", ] name = "c7n_awscc" -version = "0.1.37a5+a83dda5c9" +version = "0.1.37a5.post1+a83dda5c9" readme = "readme.md" description = "Cloud Custodian - AWS Cloud Control Provider" classifiers = [ diff --git a/tools/c7n_azure/pyproject.toml b/tools/c7n_azure/pyproject.toml index 4f0df08b402..b088403cf44 100644 --- a/tools/c7n_azure/pyproject.toml +++ b/tools/c7n_azure/pyproject.toml @@ -87,7 +87,7 @@ dependencies = [ "requests<3.0.0,>=2.22.0", ] name = "c7n_azure" -version = "0.7.51a5+a83dda5c9" +version = "0.7.51a5.post1+a83dda5c9" description = "Cloud Custodian - Azure Support" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_gcp/pyproject.toml b/tools/c7n_gcp/pyproject.toml index 2886dc30f38..e9c2c6d2cbe 100644 --- a/tools/c7n_gcp/pyproject.toml +++ b/tools/c7n_gcp/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "retrying<2.0.0,>=1.3.3", ] name = "c7n_gcp" -version = "0.4.51a5+a83dda5c9" +version = "0.4.51a5.post1+a83dda5c9" description = "Cloud Custodian - Google Cloud Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_kube/pyproject.toml b/tools/c7n_kube/pyproject.toml index 5b509e656ac..744c9beb1b0 100644 --- a/tools/c7n_kube/pyproject.toml +++ b/tools/c7n_kube/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "kubernetes>=10.0.1", ] name = "c7n_kube" -version = "0.2.51a5+a83dda5c9" +version = "0.2.51a5.post1+a83dda5c9" description = "Cloud Custodian - Kubernetes Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_left/pyproject.toml b/tools/c7n_left/pyproject.toml index 4c222699bb9..f5a2e88ddff 100644 --- a/tools/c7n_left/pyproject.toml +++ b/tools/c7n_left/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "tfparse<1.0,>=0.6", ] name = "c7n_left" -version = "0.3.38a5+a83dda5c9" +version = "0.3.38a5.post1+a83dda5c9" description = "Custodian policies for IAAC definitions" readme = "README.md" classifiers = [ diff --git a/tools/c7n_mailer/pyproject.toml b/tools/c7n_mailer/pyproject.toml index de29ab063bf..4f63b5407b7 100644 --- a/tools/c7n_mailer/pyproject.toml +++ b/tools/c7n_mailer/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "types-six<2.0.0,>=1.16.10", ] name = "c7n_mailer" -version = "0.6.51a5+a83dda5c9" +version = "0.6.51a5.post1+a83dda5c9" description = "Cloud Custodian - Reference Mailer" readme = "README.md" classifiers = [ diff --git a/tools/c7n_oci/pyproject.toml b/tools/c7n_oci/pyproject.toml index 50d7344a951..fb7f8e8ecd8 100644 --- a/tools/c7n_oci/pyproject.toml +++ b/tools/c7n_oci/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "pyopenssl>=24" ] name = "c7n_oci" -version = "0.1.26a5+a83dda5c9" +version = "0.1.26a5.post1+a83dda5c9" description = "Cloud Custodian - OCI Support" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_openstack/pyproject.toml b/tools/c7n_openstack/pyproject.toml index ffc769cc465..ac1e7c6f42e 100644 --- a/tools/c7n_openstack/pyproject.toml +++ b/tools/c7n_openstack/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "openstacksdk==0.52.0", ] name = "c7n_openstack" -version = "0.1.42a5+a83dda5c9" +version = "0.1.42a5.post1+a83dda5c9" description = "Cloud Custodian - OpenStack Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_org/pyproject.toml b/tools/c7n_org/pyproject.toml index df70022ec5d..4b22cb92c9e 100644 --- a/tools/c7n_org/pyproject.toml +++ b/tools/c7n_org/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "click>=8.0", ] name = "c7n_org" -version = "0.6.51a5+a83dda5c9" +version = "0.6.51a5.post1+a83dda5c9" description = "Cloud Custodian - Parallel Execution" readme = "README.md" classifiers = [ diff --git a/tools/c7n_policystream/pyproject.toml b/tools/c7n_policystream/pyproject.toml index 8f556f3226a..c155712dcbe 100644 --- a/tools/c7n_policystream/pyproject.toml +++ b/tools/c7n_policystream/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "requests<3.0.0,>=2.22.0", ] name = "c7n_policystream" -version = "0.4.51a5+a83dda5c9" +version = "0.4.51a5.post1+a83dda5c9" description = "Cloud Custodian - Git Commits as Logical Policy Changes" readme = "README.md" classifiers = [ diff --git a/tools/c7n_sphinxext/pyproject.toml b/tools/c7n_sphinxext/pyproject.toml index bc195381bc4..19e304aad68 100644 --- a/tools/c7n_sphinxext/pyproject.toml +++ b/tools/c7n_sphinxext/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "sphinx-rtd-theme>=1.0.0", ] name = "c7n_sphinxext" -version = "1.1.51a5+a83dda5c9" +version = "1.1.51a5.post1+a83dda5c9" description = "Cloud Custodian - Sphinx Extensions" classifiers = [ "License :: OSI Approved :: Apache Software License", diff --git a/tools/c7n_tencentcloud/pyproject.toml b/tools/c7n_tencentcloud/pyproject.toml index e65bf880ae3..152bbdf15f1 100644 --- a/tools/c7n_tencentcloud/pyproject.toml +++ b/tools/c7n_tencentcloud/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "tencentcloud-sdk-python<4.0.0,>=3.0.783", ] name = "c7n_tencentcloud" -version = "0.1.34a5+a83dda5c9" +version = "0.1.34a5.post1+a83dda5c9" description = "Cloud Custodian - Tencent Cloud Provider" readme = "readme.md" diff --git a/uv.lock b/uv.lock index 991222ccf38..e095edb72f4 100644 --- a/uv.lock +++ b/uv.lock @@ -1488,7 +1488,7 @@ wheels = [ [[package]] name = "c7n" -version = "0.9.52a5+a83dda5c9" +version = "0.9.52a5.post1+a83dda5c9" source = { editable = "." } dependencies = [ { name = "argcomplete" }, @@ -1577,7 +1577,7 @@ lint = [ [[package]] name = "c7n-awscc" -version = "0.1.37a5+a83dda5c9" +version = "0.1.37a5.post1+a83dda5c9" source = { editable = "tools/c7n_awscc" } dependencies = [ { name = "boto3" }, @@ -1602,7 +1602,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-azure" -version = "0.7.51a5+a83dda5c9" +version = "0.7.51a5.post1+a83dda5c9" source = { editable = "tools/c7n_azure" } dependencies = [ { name = "adal" }, @@ -1785,7 +1785,7 @@ dev = [ [[package]] name = "c7n-gcp" -version = "0.4.51a5+a83dda5c9" +version = "0.4.51a5.post1+a83dda5c9" source = { editable = "tools/c7n_gcp" } dependencies = [ { name = "c7n" }, @@ -1826,7 +1826,7 @@ dev = [ [[package]] name = "c7n-kube" -version = "0.2.51a5+a83dda5c9" +version = "0.2.51a5.post1+a83dda5c9" source = { editable = "tools/c7n_kube" } dependencies = [ { name = "c7n" }, @@ -1855,7 +1855,7 @@ dev = [ [[package]] name = "c7n-left" -version = "0.3.38a5+a83dda5c9" +version = "0.3.38a5.post1+a83dda5c9" source = { editable = "tools/c7n_left" } dependencies = [ { name = "c7n" }, @@ -1888,7 +1888,7 @@ dev = [ [[package]] name = "c7n-mailer" -version = "0.6.51a5+a83dda5c9" +version = "0.6.51a5.post1+a83dda5c9" source = { editable = "tools/c7n_mailer" } dependencies = [ { name = "boto3" }, @@ -1954,7 +1954,7 @@ dev = [ [[package]] name = "c7n-oci" -version = "0.1.26a5+a83dda5c9" +version = "0.1.26a5.post1+a83dda5c9" source = { editable = "tools/c7n_oci" } dependencies = [ { name = "c7n" }, @@ -1979,7 +1979,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-openstack" -version = "0.1.42a5+a83dda5c9" +version = "0.1.42a5.post1+a83dda5c9" source = { editable = "tools/c7n_openstack" } dependencies = [ { name = "c7n" }, @@ -2006,7 +2006,7 @@ dev = [ [[package]] name = "c7n-org" -version = "0.6.51a5+a83dda5c9" +version = "0.6.51a5.post1+a83dda5c9" source = { editable = "tools/c7n_org" } dependencies = [ { name = "c7n" }, @@ -2029,7 +2029,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-policystream" -version = "0.4.51a5+a83dda5c9" +version = "0.4.51a5.post1+a83dda5c9" source = { editable = "tools/c7n_policystream" } dependencies = [ { name = "boto3" }, @@ -2065,7 +2065,7 @@ dev = [ [[package]] name = "c7n-sphinxext" -version = "1.1.51a5+a83dda5c9" +version = "1.1.51a5.post1+a83dda5c9" source = { editable = "tools/c7n_sphinxext" } dependencies = [ { name = "c7n" }, @@ -2092,7 +2092,7 @@ requires-dist = [ [[package]] name = "c7n-tencentcloud" -version = "0.1.34a5+a83dda5c9" +version = "0.1.34a5.post1+a83dda5c9" source = { editable = "tools/c7n_tencentcloud" } dependencies = [ { name = "c7n" },