core (Validations): acknowledge() suppresses violations globally by rule ID across the entire App, not scoped to the given construct
- 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
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