aws / aws/aws-cdk

core (Validations): acknowledge() suppresses violations globally by rule ID across the entire App, not scoped to the given construct

Open
#38,495 6 comments 0 reactions 0 assignees View on GitHub
@aws-cdk/core bug effort/medium p1
Dominant language
TypeScript
Stars
12.9k
Forks
4.6k
Avg merge
2d 3h
Merged PRs (30d)
83

Description

### Describe the bug

`Validations.of(scope).acknowledge({ id, reason })` is documented as scope-based:

> Suppression is scope-based, and applies to all constructs under the given scope.
> — https://docs.aws.amazon.com/cdk/v2/guide/policy-validation-synthesis.html#acknowledging-warnings

In practice, the acknowledgment is **not** scoped to the construct (or its descendants) that `Validations.of()` was called on. It is matched purely by rule ID (e.g. `PluginName::RuleId`) against a single flat `Map` that is built by walking the *entire App tree* (`collectAcknowledgedRuleIds` in [`aws-cdk-lib/core/lib/private/collect-acknowledged-rule-ids.ts`](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/private/collect-acknowledged-rule-ids.ts)), and that same map is then used to filter violations from *every* stack in the app (`collectSuppressions` in [`aws-cdk-lib/core/lib/private/synthesis-validation.ts`](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/private/synthesis-validation.ts)).

Concretely:
- `collectAcknowledgedRuleIds(root)` walks the whole `App` construct tree and does `rules.set(ruleId, { reason, constructPath, ... })`, keyed **only by the rule ID string**. `constructPath` is recorded for reporting purposes only.
- `collectSuppressions(root, reports)` then does, for every violation `v` in every plugin report across every stack: `acknowledgedRules.get(ackIdOf(v))`. There is no comparison between the violation's `constructPath`/resource and the construct that acknowledgment was declared on.

The result: acknowledging rule `X` on one construct in Stack A silently suppresses **every** violation of rule `X` in **every other stack** in the same `App`, even for completely unrelated resources that were never acknowledged.

This is the opposite of what "scope-based" suppression implies, and it's a behavior change from the legacy [cdk-nag](https://github.com/cdklabs/cdk-nag) `NagSuppressions` API (construct-path scoped) that the `Validations` mechanism is meant to replace (cdk-nag v3 integrates with `Validations` per the docs).

### Regression Issue

- [ ] Select this option if this issue appears to be a regression.

### Last Known Working CDK Library Version

_No response_

### Expected Behavior

Calling `Validations.of(resourceA).acknowledge({ id: 'Plugin::MyRule-001', reason: '...' })` should only suppress violations of `MyRule-001` for `resourceA` (and its descendants, per the "scope-based" documentation). Violations of the same rule ID reported against unrelated resources/stacks that were never acknowledged should remain active and fail synthesis.

### Current Behavior

Acknowledging a rule on one resource in `StackA` also suppresses the identical rule reported against a completely unrelated resource in `StackB`. `cdk synth` succeeds with no error, and the emitted validation report shows `StackB`'s violation as suppressed, even though `Validations.of()` was never called anywhere near `StackB`'s resource.

### Reproduction Steps

```ts
import { App, CfnResource, Stack, Validations } from 'aws-cdk-lib';
import type { IPolicyValidationContext, IPolicyValidationPlugin, PolicyValidationPluginReport } from 'aws-cdk-lib';

// A trivial plugin that reports the SAME rule ID as a violation for whatever
// resources exist in each template it's given.
class ValidationPlugin implements IPolicyValidationPlugin {
public readonly name = 'ValidationPlugin';

public validate(context: IPolicyValidationContext): PolicyValidationPluginReport {
return {
success: false,
violations: context.stackTemplates.map((s) => ({
ruleName: 'MyRule-001',
description: 'dummy violation for demonstration',
violatingResources: [{
resourceName: 'Resource',
templatePath: s.templatePath,
locations: ['/'],
}],
})),
};
}
}

const app = new App({ context: { '@aws-cdk/core:validationReportJson': true } });
Validations.of(app).addPlugins(new ValidationPlugin());

const stackA = new Stack(app, 'StackA');
const resourceA = new CfnResource(stackA, 'Resource', { type: 'AWS::S3::Bucket' });
resourceA.overrideLogicalId('Resource');

const stackB = new Stack(app, 'StackB');
const resourceB = new CfnResource(stackB, 'Resource', { type: 'AWS::S3::Bucket' });
resourceB.overrideLogicalId('Resource');

// Only acknowledge the rule on StackA's resource.
Validations.of(resourceA).acknowledge({
id: 'ValidationPlugin::MyRule-001',
reason: 'Acceptable for StackA only',
});

app.synth();
console.log('SYNTH SUCCEEDED (no exception thrown)');
```

Run with `node app.ts`. Then inspect `cdk.out/validation-report.json` as shown above. Removing the `Validations.of(resourceA).acknowledge(...)` block reproduces the expected baseline (both violations active, synth throws).

### Possible Solution

`collectAcknowledgedRuleIds` / `collectSuppressions` (in [`aws-cdk-lib/core/lib/private/collect-acknowledged-rule-ids.ts`](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/private/collect-acknowledged-rule-ids.ts) and [`aws-cdk-lib/core/lib/private/synthesis-validation.ts`](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/private/synthesis-validation.ts)) need to take the acknowledging construct's path into account when matching a violation, not just the rule ID string. For example, the acknowledgment map could be keyed by `(ruleId)` -> list of `{ constructPath, reason }`, and a violation should only be considered suppressed if the violation's `constructPath` (from `violatingResources[].resourceName`/logical ID, mapped back to the originating construct) is equal to, or a descendant of, the construct path the acknowledgment was recorded on — matching the "applies to all constructs under the given scope" documentation.

### Additional Information/Context

We discovered this while migrating from `cdk-nag`'s aspect-based `NagSuppressions` to the new `Validations`-based integration in cdk-nag v3. Suppressing `AwsSolutions-ECS2` on a single `FargateTaskDefinition` in one stack (because it intentionally uses a non-secret env var alongside `secrets`) unexpectedly suppressed `AwsSolutions-ECS2` for unrelated Fargate task definitions in several other, unrelated stacks in the same CDK App. This makes `Validations.acknowledge()` unsafe to use for anything other than "acknowledge this rule for the entire App," which contradicts the documented scope-based behavior and removes the main advantage `NagSuppressions` had (precise, per-resource suppression).

### AWS CDK Library version (aws-cdk-lib)

2.262.2

### AWS CDK CLI version

2.1134.0 (build d87457e)

### Node.js Version

v24.18.0

### OS

macOS 26.5.2 (Darwin 25.5.0)

### Language

TypeScript

### Language Version

6.0.3

### Other information

_No response_

Contributor guide

Open the contributing guide

Research direction

Start with collectAcknowledgedRuleIds in aws-cdk-lib/core/lib/private/collect-acknowledged-rule-ids.ts and collectSuppressions in aws-cdk-lib/core/lib/private/synthesis-validation.ts, then run the TypeScript reproduction and inspect cdk.out/validation-report.json. Trace how acknowledgment and violation construct paths are represented. Done means a rule acknowledged on resourceA suppresses that resource and descendants, while the same rule on unrelated resources or stacks remains active.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, typescript
Domain
infrastructure, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.