Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,25 @@ jobs:

# The '**' glob is required to descend into Stage-nested stacks. Without
# it, `cdk synth` only synthesizes the top-level App (which contains the
# Stage but no stacks directly), so cdk-nag never runs against the actual
# WAF/backend/frontend stacks and any unsuppressed findings silently pass
# CI. `make cdk-synth` already uses the same glob — this aligns the
# CI gate with what runs locally.
# Stage but no stacks directly), so asset bundling never runs against the
# actual WAF/backend/frontend stacks. `make cdk-synth` uses the same glob
# — this aligns the CI gate with what runs locally.
- name: Run cdk synth
env:
CDK_DEFAULT_ACCOUNT: "123456789012"
CDK_DEFAULT_REGION: us-east-1
AWS_DEFAULT_REGION: us-east-1
run: npx cdk synth '**' --quiet

# cdk-nag v3 hard gate: CDK signals a failed policy validation by setting
# process.exitCode in the NODE process — for a Python app that's jsii's
# throwaway kernel, so the synth above exits 0 even with findings
# (verified live). The checker fails on any violation AND on a missing
# report (packs not attached = broken gate, not a pass); see
# scripts/check_validation_report.py.
- name: Check cdk-nag validation report
run: uv run python scripts/check_validation_report.py cdk.out

- name: Run CDK stack assertion tests
env:
AWS_DEFAULT_REGION: us-east-1
Expand Down
16 changes: 9 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ CDK and Powertools require incompatible `attrs` versions (CDK pulls `attrs<26` v

Run `make doctor` after `make install` to verify both venvs picked up the expected groups, `npx cdk`/`drawio` resolve, and pre-commit is wired. `make clean-venvs && make install` is the recovery path for a corrupted venv. `make pr` runs every CI gate locally in CI's order.

## `cdk synth` must use `'**'`
## `cdk synth` must use `'**'` — and the nag gate is the report check, not the exit code

All five stacks live inside `AppStage` (a `cdk.Stage`). Bare `cdk synth` walks only the App's direct children, finds the Stage, doesn't recurse, and emits an empty synthesis that succeeds *without* running cdk-nag against the real stacks. `make cdk-synth` and the CI `cdk-check` job both invoke `cdk synth '**'`. If you run `cdk synth` directly during development, include the glob — otherwise the gate passes silently regardless of what cdk-nag would find.
All five stacks live inside `AppStage` (a `cdk.Stage`). Bare `cdk synth` walks only the App's direct children, finds the Stage, doesn't recurse, and emits an empty synthesis — asset bundling never runs against the real stacks. `make cdk-synth` and the CI `cdk-check` job both invoke `cdk synth '**'`.

## cdk-nag is a hard gate
**The CLI's exit code is NOT the cdk-nag gate.** cdk-nag v3 packs are policy-validation plugins, and CDK signals validation failure by setting `process.exitCode` in the **Node** process — for this Python app that's jsii's throwaway kernel, so `cdk synth` exits 0 even with findings (verified live). The hard gate is `scripts/check_validation_report.py`, run over `cdk.out/validation-report.json` by both `make cdk-synth` and the CI step right after synth; it fails on any violation and on a *missing* report (packs not attached = broken gate, not a pass).

Five rule packs run on every synth: AwsSolutions, Serverless, NIST 800-53 R5, HIPAA Security, PCI DSS 3.2.1. Findings fail CI. Resolve by:
## cdk-nag is a hard gate (v3: policy-validation plugins, not Aspects)

Five rule packs — AwsSolutions, Serverless, NIST 800-53 R5, HIPAA Security, PCI DSS 3.2.1 — run as cdk-nag **v3 policy-validation plugins**, attached ONCE at the App root (`attach_nag_packs` in `nag_utils.py`, called from `app.py` and the nag-gating test fixtures). They evaluate the synthesized assembly, not per-stack Aspects. Findings fail CI. Resolve by:

1. **Fix the underlying issue** (preferred). README "Design decisions and known limitations" documents recurring patterns.
2. **Suppress with rationale**. Every suppression carries a `reason=` string. For `AwsSolutions-IAM5` wildcards, scope with `applies_to=["Resource::*"]` or a specific pattern and explain *why* the wildcard is unavoidable.
2. **Acknowledge with rationale** via `acknowledge_rules(construct, [{"id": ..., "reason": ..., "applies_to": [...]}])` — the project-wide adapter in `nag_utils.py` that keeps the v2-era data shape and maps it onto v3's `Validations.of().acknowledge()`. Three v3 rules to know: **granular rules (IAM4/IAM5) match individual `Rule[Finding]` ids only** — a bare `AwsSolutions-IAM5` acknowledgment matches nothing, so every wildcard needs its exact `applies_to` finding (the gate's failure output prints them); **acknowledgments cover the whole construct subtree** (v2's `apply_to_children` is gone); and **finding ids containing more than one `::`** (IAM4's `Policy::arn:<AWS::Partition>:...`) are rejected by CDK's acknowledge API — `acknowledge_rules` routes those through the `aws:cdk:acknowledged-rules` metadata fallback (documented in its docstring; drop when fixed upstream).

A bespoke validation Aspect rides alongside the packs (also wired by `apply_compliance_aspects`): `TemplateConventionChecks` in `infrastructure/validation_aspects.py` enforces two project conventions no rule pack covers — every log group declares an explicit retention (never-expire is the CloudWatch default), and every stateful resource (`CfnBucket`, DynamoDB `CfnTable`/`CfnGlobalTable`, `CfnKey`) declares an explicit removal policy. Its violations are error-level annotations, so they fail the same gates as nag findings (and `TestNagCompliance`); it's unit-tested in `tests/cdk/test_validation_aspects.py`.
A bespoke validation Aspect stays per-stack (wired by `apply_compliance_aspects`): `TemplateConventionChecks` in `infrastructure/validation_aspects.py` enforces two project conventions no rule pack covers — every log group declares an explicit retention, and every stateful resource declares an explicit removal policy. Its violations are error-level annotations (asserted empty by `TestNagCompliance`); it's unit-tested in `tests/cdk/test_validation_aspects.py`. `apply_compliance_aspects` also registers cdk-nag's `WriteNagSuppressionsToCloudFormationAspect` per stack — the v2-style `cdk_nag` Metadata audit trail in templates — because Aspects don't cross `cdk.Stage` boundaries, so registering it at the App root would silently skip every Stage-nested stack (verified live).

**Local nag gate**: `Template.from_stack()` does **NOT** raise on cdk-nag (or validation-Aspect) errors, but they surface as error-level annotations — and `tests/cdk/test_stage.py::TestNagCompliance` asserts that list is empty for every stack (prod and ephemeral shapes). So `make test-cdk` catches unsuppressed findings locally, without Docker. The CLI `cdk synth '**'` in the CI `cdk-check` job remains the authoritative gate (it also exercises asset bundling); run `make cdk-synth` with Docker started for the full CI-equivalent check before pushing IAM-touching code.
**Local nag gate**: neither `app.synth()` nor `Template.from_stack()` raises on v3 findings (CDK sets `process.exitCode` in jsii's throwaway Node kernel). `tests/cdk/test_stage.py::TestNagCompliance` synthesizes every shipped shape (prod, dev, `appconfig_monitor`, `retain_data`) and parses each assembly's `validation-report.json`; `test_nag_gate_can_fail` is the canary proving the gate is not vacuous. So `make test-cdk` catches unacknowledged findings locally, without Docker. The CI `cdk-check` job pairs `cdk synth '**'` (asset bundling) with `scripts/check_validation_report.py` (the CLI-side nag gate); run `make cdk-synth` with Docker started for the full CI-equivalent check before pushing IAM-touching code.

## Encryption posture

Expand Down
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,16 @@ coverage-badge: ## Generate the shields-endpoint coverage badge JSON (whole repo
cdk-synth: ## Synthesize all CDK stacks and validate cdk-nag rules (CDK CLI via `npm ci` / `make install`)
# The '**' glob descends into Stage-nested stacks. Without it, `cdk synth`
# stops at the Stage manifest, the five nested stacks never synthesize,
# and cdk-nag rules silently don't fire on them — so a "passing" synth
# can mask findings that surface later in `cdk deploy`.
# and asset bundling silently doesn't run on them.
#
# The explicit report check is the cdk-nag v3 hard gate: CDK signals a
# failed policy validation by setting process.exitCode in the NODE process,
# which for a Python app is jsii's throwaway kernel — so `cdk synth` exits 0
# even with findings (verified live; see scripts/check_validation_report.py).
# The checker fails on any violation AND on a missing report (packs not
# attached = broken gate, not a pass).
$(CDK) synth '**' $(CDK_ENV_ARG)
uv run python scripts/check_validation_report.py cdk.out

cdk-notices: ## Show AWS-published CDK notices (CVEs, deprecated CDK versions, upcoming breaking changes)
$(CDK) notices
Expand Down
Loading
Loading