From c935e19ae8afe4d551b8f73c0ae689f93525c17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCller?= <7556827+max06@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:17:11 +0000 Subject: [PATCH] scrub-baseline: skip already-redacted Secret markers (idempotency) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Baselines rendered by a post-#59 ATLAS already carry REDACTED:sha256: markers; the structural pass re-hashed them into sha256("REDACTED:...") so every Secret showed as changed in review comments — ghost diffs on PRs touching nothing render-relevant. Values already in marker form are now left untouched in both the collection and replacement expressions, making replay over an already-redacted tree a no-op while keeping the leak-closing behavior for pre-redaction baselines. Fixes #70 Co-Authored-By: Claude Fable 5 --- .github/actions/atlas-diff/scrub-baseline.sh | 8 +++ tests/bats/redaction/redaction-replay.bats | 70 ++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/.github/actions/atlas-diff/scrub-baseline.sh b/.github/actions/atlas-diff/scrub-baseline.sh index 4eb8425..d583af3 100755 --- a/.github/actions/atlas-diff/scrub-baseline.sh +++ b/.github/actions/atlas-diff/scrub-baseline.sh @@ -100,10 +100,17 @@ while IFS= read -r templates_dir; do # current-side render — unchanged Secrets vanish from the diff, rotated # ones show a marker change. Running the map replay first would hash # already-redacted shapes instead and break that alignment. + # + # Idempotency guard: baselines rendered by a post-Secret-redaction + # ATLAS already carry REDACTED:sha256: markers — re-hashing those + # produces sha256("REDACTED:sha256:") and every Secret shows as + # changed (#70). Values already in marker form are left untouched, so + # replaying over an already-redacted tree is a no-op. secret_vals="$(yq eval -N ' [ select(.kind == "Secret" and .apiVersion == "v1") | (.data[]?, .stringData[]?) | select(tag != "!!null") + | select((tostring | test("^REDACTED:sha256:")) | not) | tostring | @base64 ] | .[]' "$yf" 2>/dev/null | sort -u)" || secret_vals="" if [ -n "$secret_vals" ]; then @@ -122,6 +129,7 @@ while IFS= read -r templates_dir; do (select(.kind == "Secret" and .apiVersion == "v1") | (.data[]?, .stringData[]?) | select(tag != "!!null") + | select((tostring | test("^REDACTED:sha256:")) | not) ) |= ($m[(. | tostring)] // "REDACTED:sha256:unmapped") ' "$yf" > "$tmp_out" 2>/dev/null; then mv "$tmp_out" "$yf" diff --git a/tests/bats/redaction/redaction-replay.bats b/tests/bats/redaction/redaction-replay.bats index 179035b..740b406 100644 --- a/tests/bats/redaction/redaction-replay.bats +++ b/tests/bats/redaction/redaction-replay.bats @@ -284,3 +284,73 @@ YAML run yq '.data.accessKeyId' "$release_dir/secret.yaml" [ "$output" = "REDACTED" ] } + +# --- Idempotency: already-redacted baselines must pass through (issue #70) --- + +@test "scrubber: already-redacted Secret markers are left untouched" { + local scratch="${BATS_TEST_TMPDIR}/scrub-idempotent" + local baseline="${scratch}/baseline" + local mapdir="${scratch}/maps" + local release_dir="${baseline}/cluster1/dep/rel/chart/templates" + mkdir -p "$release_dir" "${mapdir}/cluster1/dep" + + # Baseline as a post-Secret-redaction ATLAS renders it: markers already + # in place, plus one raw value (the class the structural pass is for). + cat > "$release_dir/secret.yaml" <<'YAML' +apiVersion: v1 +kind: Secret +metadata: + name: already-done +stringData: + fromSops: REDACTED:sha256:9fab3a1ed1c3 + unmapped: REDACTED:sha256:unmapped + stillRaw: leaked-cleartext +YAML + + cat > "${mapdir}/cluster1/dep/rel.json" <<'JSON' +{"never-matches":"REDACTED"} +JSON + + run bash "${SCRUB_SCRIPT}" "$baseline" "$mapdir" + [ "$status" -eq 0 ] + + # Existing markers unchanged — NOT re-hashed into sha256("REDACTED:..."). + run yq '.stringData.fromSops' "$release_dir/secret.yaml" + [ "$output" = "REDACTED:sha256:9fab3a1ed1c3" ] + run yq '.stringData.unmapped' "$release_dir/secret.yaml" + [ "$output" = "REDACTED:sha256:unmapped" ] + # The raw value still gets its structural marker. + run yq '.stringData.stillRaw' "$release_dir/secret.yaml" + [[ "$output" == REDACTED:sha256:* ]] + [ "$output" != "REDACTED:sha256:unmapped" ] +} + +@test "scrubber: running twice produces identical output (no-op replay)" { + local scratch="${BATS_TEST_TMPDIR}/scrub-twice" + local baseline="${scratch}/baseline" + local mapdir="${scratch}/maps" + local release_dir="${baseline}/cluster1/dep/rel/chart/templates" + mkdir -p "$release_dir" "${mapdir}/cluster1/dep" + + cat > "$release_dir/secret.yaml" <<'YAML' +apiVersion: v1 +kind: Secret +metadata: + name: fresh +stringData: + token: s3cretvalue +YAML + + cat > "${mapdir}/cluster1/dep/rel.json" <<'JSON' +{"s3cretvalue":"REDACTED"} +JSON + + run bash "${SCRUB_SCRIPT}" "$baseline" "$mapdir" + [ "$status" -eq 0 ] + local first_pass + first_pass="$(cat "$release_dir/secret.yaml")" + + run bash "${SCRUB_SCRIPT}" "$baseline" "$mapdir" + [ "$status" -eq 0 ] + [ "$(cat "$release_dir/secret.yaml")" = "$first_pass" ] +}