Skip to content

chore: merge upstream cloud-custodian and stamp 0.9.54a1+0fa7d19f5 (ENG-9376) - #27

Merged
kevin-birkett merged 66 commits into
stacklet/integrationfrom
kevin/chore/c7n-0.9.54a1
Oct 23, 2026
Merged

kevin-birkett merged 66 commits into
stacklet/integrationfrom
kevin/chore/c7n-0.9.54a1

Conversation

@kevin-birkett

@kevin-birkett kevin-birkett commented Oct 22, 2026 •

Copy link
Copy Markdown

ENG-9376

what

Merges upstream cloud-custodian bf0b50dbc..0fa7d19f5 into
stacklet/integration and stamps a new alpha series: c7n becomes
0.9.54a1+0fa7d19f5, and each workspace tool ticks one patch (e.g.
c7n-azure 0.7.53a1, c7n-left 0.3.40a1).

Two commits: the upstream merge on its own, then the stamp. The merge's
combined diff (git show --cc) is empty, so there is no hand-written
content in it to review.

Conflicts were the version fields in pyproject.toml, the twelve
tools/*/pyproject.toml and c7n/version.py, the root [tool.uv]
cooldown block, and uv.lock. The versions and the 7-day cooldown keep
the branch's side, and uv.lock takes upstream's. Upstream's other
edits to those files (an azure-mgmt-eventgrid floor bump and new
GOOGLE_WORKSPACE_* test env unsets) merged cleanly and are kept. No
source file conflicted: #24 landing as a merge commit left the merge
base at bf0b50dbc.

The stamp commit also drops the tfparse exclude-newer-package
override added in #3. It admitted tfparse 0.6.20 past the 7-day
cooldown, but as a fixed date it now capped tfparse at releases before
2026-06-02. Without it the lock resolves tfparse 0.6.22, matching
upstream. Apart from that, the lock differs from upstream's only in the
cooldown span and the workspace versions.

Upstream content picked up:

  • core: a duration value_type; resolver URI caching keys on request
    headers and skips blank CSV rows.
  • aws: new resources for s3tables (table-bucket, table), s3files
    (file-system, mount-target, access-point), Q Business, AIOps
    investigation groups, rds-global-cluster, eventbridge event-connection
    and kafka-broker, plus expanded IoT support; new actions including
    account set-access-analyzer, ami set-disabled, cloudtrail
    set-event-selectors and sagemaker-space update; SSM tagging for
    managed instances, ops items, patch baselines, maintenance windows and
    associations; security-group listing now paginates; fixes for
    invoke-lambda qualifiers, secrets-manager ARN lookup, and tag actions
    on CFN ROLLBACK_COMPLETE stacks and CloudTrail shadow trails.
  • azure: machine learning compute cluster, environment version and
    schedule resources, and ML job cancel / data container archive
    actions; disk snapshot and NSG remove-rules actions; set-firewall-rules
    no longer corrupts rules across resources; API call stats.
  • gcp: workspace-user (MFA reporting) and Vertex AI metadata store
    artifact resources; set-public-access on buckets, set-ip-forwarding on
    instances, set-iam-policy on KMS keys with auditConfig support; project
    metrics scoped to one project at a time; API call stats.
  • k8s: resource cache keys include the resource type; API call stats.
  • releng: upstream's 0.9.53 release prep.

No Stacklet code changed.

why

The October c7n upgrade (ENG-9376) needs upstream's latest changes.
Upstream released 0.9.53.0 on 2026-10-01, so per the versioning
convention the base moves to the next unreleased version, 0.9.54, and
the alpha restarts at a1.

This should land as a merge commit, not a squash, so that 0fa7d19f5
stays in the ancestry and the next upstream merge has the right merge
base (see #24).

testing

The upstream commits bring their own tests, which arrive with the
merge. No Stacklet code changed, so Stacklet CI and upstream CI running
on this PR is the verification of the code.

For the stamp: all 13 workspace packages are stamped uniformly,
c7n/version.py is regenerated, no reference to the old hash remains,
and the cumulative diff against upstream main contains only the known
Stacklet deltas (versions, cooldown, CODEOWNERS, stacklet-*.yml, the
ci.yml trigger retarget, justfile, release tooling).
make pkg-build-wheel builds all 13 wheels and passes
twine check --strict, tools/dev/check_release_wheels.py accepts
them, and make lint passes.

docs

No docs needed.


🤖 Generated with Claude Code

rondodev and others added 30 commits September 15, 2026 18:35
cloud-custodian#10972)

* aws - add s3files file-system, mount-target and access-point resources

* exempt s3files-mount-target from arn info meta lint

* address review feedback: drop redundant policy_attribute init, remove access-point-account filter, align parent annotation

* drop redundant permission_prefix matching service default

* use universal tagging for file-system and access-point resources

---------

Co-authored-by: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com>
…#10973)

* aws - add s3tables table-bucket and table resources

* Using resource_type arn for tag augment

* drop redundant policy_attribute

* drop redundant permission_prefix matching service default

* add replication filter for table buckets

* "use universal tagging for table-bucket and table resources

---------

Co-authored-by: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com>
…#11007)

* adding event-connection resource

* Update test_policy.py

---------

Co-authored-by: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com>
…ch baselines, maintenance windows, and associations (cloud-custodian#11093)

* ssm - add tagging support for managed instances, ops items, patch baselines, maintenance windows, and associations

* Update ssm.py
…ud-custodian#10883)

The ssm-compliance filter advertises a schema default of ['NON_COMPLIANT']
for the optional 'states' field, but process() read self.data['states']
with a hard subscript. Custodian's Draft7Validator does not inject schema
defaults into the policy data, so a policy that omits 'states' passed
validation and then raised KeyError: 'states' at runtime.

Read the value defensively so the advertised default is applied instead
of crashing.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Co-authored-by: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com>
…#11015)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resources

*  qbusiness-application (top-level)
*  qbusiness-index, qbusiness-plugin, qbusiness-data-accessor (children of application)
*  qbusiness-data-source (enumerated across each application/index pair)

Filters / Actions

* cross-account filter on qbusiness-application
* Universal tagging on all resources
- iot-policy: parses policyDocument in augment, has-statement and
  attached filters, delete action
- iot-certificate: describe_certificate detail with creationDate for age
  based filtering, set-inactive action
- iot-ota-update: unsigned filter over otaUpdateFiles codeSigning
- iot: tag support for ownership attribution
- account: iot-logging filter for GetV2LoggingOptions
…todian#11063)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…todian#11116)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dian#11137)

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d-custodian#11168)

QueryResourceManager.get_cache_key keyed the resource cache on the source
type and query alone, so every Kubernetes resource type mapped to the same
cache entry. With caching enabled, the first policy to run populated it and
every later policy within the cache period was handed that policy's resources
instead of querying its own: a k8s.deployment policy run after a k8s.pod
policy filtered the cluster's pods, matched nothing, and reported an empty
result without error.

Key on the resource manager's class as well, as c7n.query's
QueryResourceManager does. Custom resources were already distinct, since their
group, version and plural are part of the query.

---------

Co-authored-by: AJ Kerrigan <aj@stacklet.io>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
KodyMullins and others added 24 commits October 14, 2026 14:03
---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
---------

Co-authored-by: Caleb Syring <caleb.syring@sixfeetup.com>
Co-authored-by: Kody Mullins <kody.mullins@sixfeetup.com>
---------

Co-authored-by: AJ Kerrigan <aj@stacklet.io>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dian#11215)

---------

Co-authored-by: Sayak Satpathi <170233065+sayaksatpathi@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#11187)

Co-authored-by: Usman Qureshi <usman.nja.14@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: AJ Kerrigan <aj@stacklet.io>
Co-authored-by: AJ Kerrigan <aj@speckledmonkey.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…an#11194)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: AJ Kerrigan <aj@speckledmonkey.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…ains (cloud-custodian#11226)

Signed-off-by: Srinivas Chippagiri <cvas22@gmail.com>
…sources (cloud-custodian#11144)

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…logs (cloud-custodian#10650)

---------

Co-authored-by: Daniel Lindsley <daniel@sixfeetup.com>
Co-authored-by: Kody Mullins <kody.mullins@sixfeetup.com>
…an#10642)

---------

Co-authored-by: Daniel Lindsley <daniel@sixfeetup.com>
Co-authored-by: Caleb Syring <caleb.syring@sixfeetup.com>
Co-authored-by: Kody Mullins <kody.mullins@sixfeetup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* k8s - add api call stats and metrics

Kubernetes policies fell back to the null api stats collector, so
metadata.json carried no api-stats and no ApiCalls metric was emitted.

Count each kubernetes client call by method name for the duration of a
policy execution, recording the counts in the api-stats metadata in the
same shape as aws, and emit ApiCalls and ApiTime metrics. Failed calls
are counted too.

* core - share api call stats between providers, add aws call timing

Move api call counting, timing, metadata and metrics into a core base
class, so a provider only needs to hook its client to record calls.
Providers whose clients have no event hooks of their own record calls
through a shared function.

Move k8s and aws onto it. aws keeps its call names, metadata and
interface, and now times each call between botocore's before-call and
after-call events, emitting an ApiTime metric alongside ApiCalls.

* core - lock api call stats updates, check aws stats cleanup

Filters and actions make api calls from thread pools, so serialise
updates to the call counts and total time.

Check that an aws execution removes its event handlers using a client
created from the cached session afterwards; a client keeps its own copy
of the session's handlers, so one created during the execution can't
show it.

* core - round ApiTime metric, record aws transport errors

Round the ApiTime metric to milliseconds, as full float precision is
noise in metric output.

Record aws calls that fail in transport, such as connection errors.
botocore emits after-call-error rather than after-call for those, without
the operation model, so before-call now keeps the call's name alongside
its start time in the request context.
* gcp - add api call stats and metrics

gcp policies fell back to the null api stats collector, so metadata.json
carried no api-stats and no ApiCalls metric was emitted.

Record each gcp api call by its method id, such as iam.roles.list, with
its duration, on the shared api call stats base. A call is recorded once
however many attempts its retries take, and failed calls are counted
too.

* gcp - rename api stats test module to a unique name

pytest can't import two test modules with the same basename in one run,
and c7n_kube already has a test_api_stats.py.
azure policies fell back to the null api stats collector, so
metadata.json carried no api-stats and no ApiCalls metric was emitted.

Record each azure sdk request attempt, on the shared api call stats base,
named by http method and resource type with resource names left out, such
as GET Microsoft.Compute/virtualMachines; requests outside resource manager
are named by host, leaving out the account or vault name. Clients get a
custom hook policy that times each attempt, including ones that fail
without a response, and still runs the existing hooks. The send override
for legacy clients records each attempt in its retry loop.
# Conflicts:
#	c7n/version.py
#	pyproject.toml
#	tools/c7n_awscc/pyproject.toml
#	tools/c7n_azure/pyproject.toml
#	tools/c7n_gcp/pyproject.toml
#	tools/c7n_kube/pyproject.toml
#	tools/c7n_left/pyproject.toml
#	tools/c7n_mailer/pyproject.toml
#	tools/c7n_oci/pyproject.toml
#	tools/c7n_openstack/pyproject.toml
#	tools/c7n_org/pyproject.toml
#	tools/c7n_policystream/pyproject.toml
#	tools/c7n_sphinxext/pyproject.toml
#	tools/c7n_tencentcloud/pyproject.toml
#	uv.lock
Upstream released 0.9.53.0, so the base moves to 0.9.54 and the
alpha series restarts at a1; the workspace tools each tick one patch.

Drop the tfparse exclude-newer-package override. It was added in #3
to admit tfparse 0.6.20 past the 7-day cooldown, but as a fixed date
it now caps tfparse at releases before 2026-06-02. Without it the
lock resolves tfparse 0.6.22, matching upstream.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kevin-birkett
kevin-birkett marked this pull request as ready for review October 22, 2026 20:38
@kevin-birkett
kevin-birkett requested a review from a team as a code owner October 22, 2026 20:38

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

21 issues found across 961 files

Confidence score: 2/5

  • tools/c7n_azure/c7n_azure/utils.py can send its bearer token to an http:// endpoint when the Run History URL comes from discoveryUrl. Require HTTPS before sending authorization headers.
  • tools/c7n_gcp/c7n_gcp/actions/iampolicy.py can remove IAM Conditions when audit-config and binding updates are combined. Request policy version 3 before fetching bindings to write.
  • tools/c7n_gcp/c7n_gcp/resources/workspace.py checks only the first assignment page, so it can miss users on later pages and report MFA fields that may not protect them. Use execute_paged_query for the check.
  • tools/c7n_azure/c7n_azure/filters.py appends self.filter without grouping, so an or branch can bypass the generated dimension clauses. Parenthesize self.filter before joining it with and.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tools/c7n_azure/c7n_azure/utils.py">

<violation number="1" location="tools/c7n_azure/c7n_azure/utils.py:635">
P2: This session bypasses the API statistics collector, so ML history requests are silently omitted from `api-stats` and `ApiTime`. Instrument each adapter attempt, including retry and exception paths, or record these requests at the caller.</violation>

<violation number="2" location="tools/c7n_azure/c7n_azure/utils.py:636">
P1: Custom agent: **Flag Security Vulnerabilities**

This session sends its default `Authorization` bearer token to `http://` URLs as well as HTTPS. The Run History caller derives its endpoint from `discoveryUrl` without checking the scheme; reject non-HTTPS endpoints before posting.</violation>
</file>

<file name="tools/c7n_gcp/c7n_gcp/utils.py">

<violation number="1" location="tools/c7n_gcp/c7n_gcp/utils.py:330">
P1: `_canonicalize_value` turns Cloud Logging typed and wildcard values into quoted strings, so filters with different routing semantics can be grouped as identical and reported as overlapping sinks. Preserve these literals with operator-aware canonicalization instead of quoting every unquoted word.</violation>
</file>

<file name="docs/source/azure/examples/metrics-general.rst">

<violation number="1" location="docs/source/azure/examples/metrics-general.rst:71">
P2: The prose overstates parent-scope selection: resources without `c7n:parent-id` fall back to their own ID even when `dimensions` is set. Qualify this sentence with the annotation requirement so users do not assume every dimensions query targets a parent.</violation>
</file>

<file name="tests/data/placebo/test_iot_certificate_age/iot.DescribeCertificate_1.json">

<violation number="1" location="tests/data/placebo/test_iot_certificate_age/iot.DescribeCertificate_1.json:13">
P2: This response is inconsistent with the paired list response and certificate validity, causing the age test to select a certificate that the list data says is only weeks old. Synchronize both placebo responses and the PEM/validity dates, or use a consistently old certificate fixture for the age case.</violation>
</file>

<file name="tools/c7n_azure/c7n_azure/resources/network_security_group.py">

<violation number="1" location="tools/c7n_azure/c7n_azure/resources/network_security_group.py:504">
P2: Omitting a direction currently defaults it to `matched`, so `ingress: all` also deletes any egress rules annotated by an egress filter on the same policy. Treat an omitted direction as a no-op (or add an explicit opt-in) so the action cannot remove rules outside the requested direction.</violation>
</file>

<file name="c7n/resources/s3files.py">

<violation number="1" location="c7n/resources/s3files.py:48">
P2: This resource declares `tag:GetResources` as mandatory even though `DescribeFileSystem` reads tags from the S3 Files response directly and does not call the tagging API. Least-privilege roles running any FileSystem policy will therefore require an unused permission; separate universal tag registration from source permission discovery or omit this permission for the custom source.</violation>
</file>

<file name="tools/c7n_gcp/c7n_gcp/resources/logging.py">

<violation number="1" location="tools/c7n_gcp/c7n_gcp/resources/logging.py:182">
P2: This truthiness check drops enabled exclusions with an empty filter, even though an empty Cloud Logging filter matches every entry. Such a sink should have no effective routing, but the filter currently treats it as routing its base filter and can report false overlap or duplicate matches; handle empty/comment-only exclusions as match-all exclusions instead of skipping them.</violation>
</file>

<file name="tools/c7n_gcp/c7n_gcp/resources/workspace.py">

<violation number="1" location="tools/c7n_gcp/c7n_gcp/resources/workspace.py:79">
P1: This guard checks only the first assignment page, so an assignment returned on a later page is missed and the policy reports MFA fields that may not protect those users. Iterate with `execute_paged_query` and reject any page containing assignments.</violation>
</file>

<file name="tools/c7n_azure/c7n_azure/filters.py">

<violation number="1" location="tools/c7n_azure/c7n_azure/filters.py:218">
P2: `dimensions` is now accepted for Cosmos database and collection metric filters, but both subclasses discard it in their `get_filter` overrides. Such policies validate while silently ignoring the requested dimension restriction; compose dimensions in those overrides or reject the option in their schemas.</violation>

<violation number="2" location="tools/c7n_azure/c7n_azure/filters.py:341">
P1: Appending `self.filter` without grouping lets an `or` branch bypass every generated dimension clause. Wrap `self.filter` in parentheses before joining it with `and`.</violation>
</file>

<file name="tools/c7n_azure/c7n_azure/actions/firewall.py">

<violation number="1" location="tools/c7n_azure/c7n_azure/actions/firewall.py:45">
P2: Sorting append-mode bypass rules discards the policy’s explicit order. Preserve first-seen order while deduplicating, otherwise `test_bypass_network_rules_action` and consumers relying on stable serialization receive `AzureServices,Metrics` instead of `Metrics,AzureServices`.</violation>
</file>

<file name="c7n/resources/kafka.py">

<violation number="1" location="c7n/resources/kafka.py:77">
P2: The unconditional append exposes non-broker `ListNodes` entries as brokers. Append only entries containing `BrokerNodeInfo`, so controller or other node records do not produce resources with a missing ID and invalid metrics dimensions.</violation>

<violation number="2" location="c7n/resources/kafka.py:91">
P1: This child query calls `ListNodes` for every Kafka cluster, including serverless clusters. Skip serverless parents before enumeration; otherwise an account containing one can make the entire `kafka-broker` policy fail instead of returning only provisioned brokers.</violation>

<violation number="3" location="c7n/resources/kafka.py:94">
P1: Using `BrokerId` as the resource ID breaks identity-based retrieval because the child query filters raw nodes before augmentation, where that key does not exist. Use the raw unique `NodeARN` as `id` or override the child lookup to augment before filtering.</violation>
</file>

<file name="tools/c7n_azure/c7n_azure/resources/machine_learning_compute_cluster.py">

<violation number="1" location="tools/c7n_azure/c7n_azure/resources/machine_learning_compute_cluster.py:274">
P2: The filter ignores `uniqueChildRunComputeTargets` and `compute.target` when `run.target` is null. Include those fields so active pipeline parents cannot cause their compute cluster to be selected as inactive.</violation>
</file>

<file name="Makefile">

<violation number="1" location="Makefile:53">
P3: The no-test branch points to nonexistent `make diff-coverage-full` and `TEST_PATH` interfaces, so developers cannot follow its recovery instructions. Point to an existing command or implement those interfaces.</violation>
</file>

<file name="c7n/output.py">

<violation number="1" location="c7n/output.py:221">
P2: The process-global recorder list mixes API calls between overlapping policy executions. Scope the active recorder to its execution while preserving propagation to the worker threads used by filters and actions.</violation>
</file>

<file name="c7n/resources/aiops.py">

<violation number="1" location="c7n/resources/aiops.py:45">
P2: AIOps queries use `DescribeWithResourceTags`, which calls the Resource Groups Tagging API, but this resource does not declare `tag:GetResources`; generated execution roles can therefore fail tag augmentation with `AccessDenied`. Set `universal_taggable = object()` on the resource type or use a source that fetches tags through `aiops:ListTagsForResource`.</violation>
</file>

<file name="c7n/resources/vpc.py">

<violation number="1" location="c7n/resources/vpc.py:898">
P2: This unconditionally adds `MaxResults` to queries containing `GroupIds`, which EC2 rejects. Only set the pagination parameter when `GroupIds` is absent.</violation>
</file>

<file name="tools/c7n_gcp/c7n_gcp/actions/iampolicy.py">

<violation number="1" location="tools/c7n_gcp/c7n_gcp/actions/iampolicy.py:305">
P1: Combined audit-config and binding updates can remove IAM Conditions because the action writes bindings fetched without requesting policy version 3. Request version 3 from `getIamPolicy` before writing bindings, including for the resource-specific argument overrides.</violation>
</file>

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Re-trigger cubic

token = azure_session.credentials.get_token(token_scope)

session = requests.Session()
session.headers['Authorization'] = f'Bearer {token.token}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

This session sends its default Authorization bearer token to http:// URLs as well as HTTPS. The Run History caller derives its endpoint from discoveryUrl without checking the scheme; reject non-HTTPS endpoints before posting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tools/c7n_azure/c7n_azure/utils.py, line 636:

<comment>This session sends its default `Authorization` bearer token to `http://` URLs as well as HTTPS. The Run History caller derives its endpoint from `discoveryUrl` without checking the scheme; reject non-HTTPS endpoints before posting.</comment>

<file context>
@@ -608,6 +617,42 @@ def get_keyvault_auth_endpoint(cloud_endpoints):
+    token = azure_session.credentials.get_token(token_scope)
+
+    session = requests.Session()
+    session.headers['Authorization'] = f'Bearer {token.token}'
+
+    if max_retries:
</file context>

token_type, value = token
if token_type == "STRING" and value[0] == "'" and '"' not in value:
value = f'"{value[1:-1]}"'
elif token_type == "WORD" and not set(value) & set("\"'\\"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: _canonicalize_value turns Cloud Logging typed and wildcard values into quoted strings, so filters with different routing semantics can be grouped as identical and reported as overlapping sinks. Preserve these literals with operator-aware canonicalization instead of quoting every unquoted word.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tools/c7n_gcp/c7n_gcp/utils.py, line 330:

<comment>`_canonicalize_value` turns Cloud Logging typed and wildcard values into quoted strings, so filters with different routing semantics can be grouped as identical and reported as overlapping sinks. Preserve these literals with operator-aware canonicalization instead of quoting every unquoted word.</comment>

<file context>
@@ -1,3 +1,684 @@
+        token_type, value = token
+        if token_type == "STRING" and value[0] == "'" and '"' not in value:
+            value = f'"{value[1:-1]}"'
+        elif token_type == "WORD" and not set(value) & set("\"'\\"):
+            value = f'"{value}"'
+        return value.lower() if lower else value
</file context>

Comment on lines +79 to +86
assignments = client.execute_query(
'list',
{'filter': 'customer=="customers/%s"' % get_workspace_customer()})
if assignments.get('inboundSsoAssignments'):
raise PolicyExecutionError(
"Organizations with SSO assignments aren't currently"
" supported by gcp.workspace-user.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This guard checks only the first assignment page, so an assignment returned on a later page is missed and the policy reports MFA fields that may not protect those users. Iterate with execute_paged_query and reject any page containing assignments.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tools/c7n_gcp/c7n_gcp/resources/workspace.py, line 79:

<comment>This guard checks only the first assignment page, so an assignment returned on a later page is missed and the policy reports MFA fields that may not protect those users. Iterate with `execute_paged_query` and reject any page containing assignments.</comment>

<file context>
@@ -0,0 +1,103 @@
+            'cloudidentity', 'v1beta1', 'inboundSsoAssignments',
+            scopes=SSO_ASSIGNMENT_SCOPES)
+        # The quoting is required, though the api's own example omits it.
+        assignments = client.execute_query(
+            'list',
+            {'filter': 'customer=="customers/%s"' % get_workspace_customer()})
</file context>
Suggested change
assignments = client.execute_query(
'list',
{'filter': 'customer=="customers/%s"' % get_workspace_customer()})
if assignments.get('inboundSsoAssignments'):
raise PolicyExecutionError(
"Organizations with SSO assignments aren't currently"
" supported by gcp.workspace-user.")
for page in client.execute_paged_query(
'list',
{'filter': 'customer=="customers/%s"' % get_workspace_customer()}):
if page.get('inboundSsoAssignments'):
raise PolicyExecutionError(
"Organizations with SSO assignments aren't currently"
" supported by gcp.workspace-user.")

self.manager.ctx.policy.name, d['name'], resource.get('id')))
clauses.append("%s eq '%s'" % (d['name'], str(value).replace("'", "''")))
dim_filter = " and ".join(clauses)
return "%s and %s" % (self.filter, dim_filter) if self.filter else dim_filter

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Appending self.filter without grouping lets an or branch bypass every generated dimension clause. Wrap self.filter in parentheses before joining it with and.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tools/c7n_azure/c7n_azure/filters.py, line 341:

<comment>Appending `self.filter` without grouping lets an `or` branch bypass every generated dimension clause. Wrap `self.filter` in parentheses before joining it with `and`.</comment>

<file context>
@@ -262,11 +312,33 @@ def get_metric_data(self, resource):
+                        self.manager.ctx.policy.name, d['name'], resource.get('id')))
+            clauses.append("%s eq '%s'" % (d['name'], str(value).replace("'", "''")))
+        dim_filter = " and ".join(clauses)
+        return "%s and %s" % (self.filter, dim_filter) if self.filter else dim_filter
 
     def _write_metric_to_resource(self, resource, metrics_data, m):
</file context>
Suggested change
return "%s and %s" % (self.filter, dim_filter) if self.filter else dim_filter
return "(%s) and %s" % (self.filter, dim_filter) if self.filter else dim_filter

Comment thread c7n/resources/kafka.py
parent_spec = ('kafka', 'ClusterArn', True)
enum_spec = ('list_nodes', 'NodeInfoList', None)
permissions_enum = ('kafka:ListClustersV2', 'kafka:ListNodes')
name = id = 'BrokerId'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Using BrokerId as the resource ID breaks identity-based retrieval because the child query filters raw nodes before augmentation, where that key does not exist. Use the raw unique NodeARN as id or override the child lookup to augment before filtering.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At c7n/resources/kafka.py, line 94:

<comment>Using `BrokerId` as the resource ID breaks identity-based retrieval because the child query filters raw nodes before augmentation, where that key does not exist. Use the raw unique `NodeARN` as `id` or override the child lookup to augment before filtering.</comment>

<file context>
@@ -54,6 +58,85 @@ class resource_type(TypeInfo):
+        parent_spec = ('kafka', 'ClusterArn', True)
+        enum_spec = ('list_nodes', 'NodeInfoList', None)
+        permissions_enum = ('kafka:ListClustersV2', 'kafka:ListNodes')
+        name = id = 'BrokerId'
+        arn = False
+        dimension = 'Broker ID'
</file context>
Suggested change
name = id = 'BrokerId'
name = 'BrokerId'
id = 'NodeARN'

Comment on lines +274 to +277
target = run.get('target')
if isinstance(target, str) and target:
targets.add(target.lower())
return targets

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The filter ignores uniqueChildRunComputeTargets and compute.target when run.target is null. Include those fields so active pipeline parents cannot cause their compute cluster to be selected as inactive.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tools/c7n_azure/c7n_azure/resources/machine_learning_compute_cluster.py, line 274:

<comment>The filter ignores `uniqueChildRunComputeTargets` and `compute.target` when `run.target` is null. Include those fields so active pipeline parents cannot cause their compute cluster to be selected as inactive.</comment>

<file context>
@@ -0,0 +1,357 @@
+                {'filter': f'endTimeUtc ge {cutoff_text}'},
+            ))
+            for run in runs:
+                target = run.get('target')
+                if isinstance(target, str) and target:
+                    targets.add(target.lower())
</file context>
Suggested change
target = run.get('target')
if isinstance(target, str) and target:
targets.add(target.lower())
return targets
run_targets = [run.get('target')]
run_targets.extend(run.get('uniqueChildRunComputeTargets') or [])
compute = run.get('compute') or {}
if isinstance(compute, dict):
run_targets.append(compute.get('target'))
targets.update(
target.lower()
for target in run_targets
if isinstance(target, str) and target
)

Comment thread c7n/output.py
self.api_time += elapsed

def __enter__(self):
_api_call_recorders.append(self)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The process-global recorder list mixes API calls between overlapping policy executions. Scope the active recorder to its execution while preserving propagation to the worker threads used by filters and actions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At c7n/output.py, line 221:

<comment>The process-global recorder list mixes API calls between overlapping policy executions. Scope the active recorder to its execution while preserving propagation to the worker threads used by filters and actions.</comment>

<file context>
@@ -177,6 +179,55 @@ def __exit__(self, exc_type=None, exc_value=None, exc_traceback=None):
+            self.api_time += elapsed
+
+    def __enter__(self):
+        _api_call_recorders.append(self)
+        self.push_snapshot()
+
</file context>

Comment thread c7n/resources/aiops.py
'InternalServerException',
)))

source_mapping = {'describe': DescribeWithResourceTags}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: AIOps queries use DescribeWithResourceTags, which calls the Resource Groups Tagging API, but this resource does not declare tag:GetResources; generated execution roles can therefore fail tag augmentation with AccessDenied. Set universal_taggable = object() on the resource type or use a source that fetches tags through aiops:ListTagsForResource.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At c7n/resources/aiops.py, line 45:

<comment>AIOps queries use `DescribeWithResourceTags`, which calls the Resource Groups Tagging API, but this resource does not declare `tag:GetResources`; generated execution roles can therefore fail tag augmentation with `AccessDenied`. Set `universal_taggable = object()` on the resource type or use a source that fetches tags through `aiops:ListTagsForResource`.</comment>

<file context>
@@ -0,0 +1,130 @@
+        'InternalServerException',
+    )))
+
+    source_mapping = {'describe': DescribeWithResourceTags}
+
+
</file context>

Comment thread c7n/resources/vpc.py
# and leaves by-id lookups alone since EC2 rejects MaxResults alongside
# GroupIds.
query = dict(query or {})
query.setdefault('MaxResults', 1000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This unconditionally adds MaxResults to queries containing GroupIds, which EC2 rejects. Only set the pagination parameter when GroupIds is absent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At c7n/resources/vpc.py, line 898:

<comment>This unconditionally adds `MaxResults` to queries containing `GroupIds`, which EC2 rejects. Only set the pagination parameter when `GroupIds` is absent.</comment>

<file context>
@@ -886,6 +886,19 @@ def load_resource(self, item):
+        # and leaves by-id lookups alone since EC2 rejects MaxResults alongside
+        # GroupIds.
+        query = dict(query or {})
+        query.setdefault('MaxResults', 1000)
+        return super().resources(query)
+
</file context>
Suggested change
query.setdefault('MaxResults', 1000)
if 'GroupIds' not in query:
query.setdefault('MaxResults', 1000)

Comment thread Makefile
@CHANGED_TEST_FILES=$$(git diff --name-only origin/main | grep -E "test_.*\.py$$" || true); \
if [ -z "$$CHANGED_TEST_FILES" ]; then \
echo "Error: No test files changed compared to origin/main"; \
echo "Use 'make diff-coverage-full' to run all tests for a provider, or specify TEST_PATH=<path>"; \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The no-test branch points to nonexistent make diff-coverage-full and TEST_PATH interfaces, so developers cannot follow its recovery instructions. Point to an existing command or implement those interfaces.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Makefile, line 53:

<comment>The no-test branch points to nonexistent `make diff-coverage-full` and `TEST_PATH` interfaces, so developers cannot follow its recovery instructions. Point to an existing command or implement those interfaces.</comment>

<file context>
@@ -45,6 +45,49 @@ test-coverage:
+	@CHANGED_TEST_FILES=$$(git diff --name-only origin/main | grep -E "test_.*\.py$$" || true); \
+	if [ -z "$$CHANGED_TEST_FILES" ]; then \
+		echo "Error: No test files changed compared to origin/main"; \
+		echo "Use 'make diff-coverage-full' to run all tests for a provider, or specify TEST_PATH=<path>"; \
+		exit 1; \
+	fi; \
</file context>
Suggested change
echo "Use 'make diff-coverage-full' to run all tests for a provider, or specify TEST_PATH=<path>"; \
echo "Run 'make test-coverage' for full coverage, or invoke 'uv run pytest <path>' directly"; \

@jtroup jtroup left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevin-birkett
kevin-birkett merged commit 82c72cf into stacklet/integration Oct 23, 2026
3 checks passed
@kevin-birkett
kevin-birkett deleted the kevin/chore/c7n-0.9.54a1 branch October 23, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.