diff --git a/engine/examples/dashboard_preview.rs b/engine/examples/dashboard_preview.rs index f7967b5..92bbb97 100644 --- a/engine/examples/dashboard_preview.rs +++ b/engine/examples/dashboard_preview.rs @@ -378,6 +378,21 @@ fn record_signing_inventory(log: &PolicyDecisionLog) { "checking", "signing posture not yet known (registry/log unreachable)", ); + // A signing-regression finding (JEF-264): the api-gateway repo — with an established signed + // history — is now signed by a NEW identity (the push-access-compromise signal). Audit-only: + // the image is still admitted; the loud banner surfaces before→after in full. + log.record(PolicyDecisionRecord::now( + "signing-regression", + "allow", + "SigningRegression/ghcr.io/acme/api-gateway", + "ghcr.io/acme/api-gateway:v1.9.0", + "regression-identity-established", + "", + "", + "signed by https://github.com/acme-forks/api-gateway/.github/workflows/build.yaml@refs/heads/main \ + via https://token.actions.githubusercontent.com | before: \ + https://github.com/acme/api-gateway/.github/workflows/release.yaml@refs/tags/v1.8.2", + )); } /// A representative bake/coverage summary used by the covered scenarios. diff --git a/engine/src/engine/dashboard/admission_tests.rs b/engine/src/engine/dashboard/admission_tests.rs index 78c47c8..57224cf 100644 --- a/engine/src/engine/dashboard/admission_tests.rs +++ b/engine/src/engine/dashboard/admission_tests.rs @@ -303,6 +303,90 @@ fn signing_inventory_escapes_an_attacker_chosen_identity() { assert!(html.contains("<script>"), "the identity is escaped"); } +// ---- signing-regression banner (JEF-264) render tests -------------------------------------- + +/// A signing-regression finding row (JEF-264 shape): `SigningRegression/` subject, the drift +/// token in `signature`, the before→after prose in `reason`. +fn regression_rec(repo: &str, image: &str, signature: &str, reason: &str) -> PolicyDecisionRecord { + PolicyDecisionRecord::now( + "signing-regression", + "allow", + format!("SigningRegression/{repo}"), + image, + signature, + "", + "", + reason, + ) +} + +#[test] +fn signing_regression_renders_loud_word_and_before_after_in_full() { + let rows = vec![regression_rec( + "ghcr.io/acme/app", + "ghcr.io/acme/app:2", + "regression-identity-established", + "signed by https://github.com/evil/app/.github/workflows/pwn.yaml@refs/heads/main via \ + https://token.actions.githubusercontent.com | before: \ + https://github.com/acme/app/.github/workflows/r.yaml@refs/tags/v1", + )]; + let html = render(&rows); + // The loud, lexically-distinct posture word (not the calm "not signed"). + assert!( + html.contains("signing regression"), + "the loud regression word is rendered" + ); + // Both identities in FULL — the before (old signer) and the after (new signer). + assert!( + html.contains("https://github.com/acme/app/.github/workflows/r.yaml@refs/tags/v1"), + "the before signer is shown in full" + ); + assert!( + html.contains("https://github.com/evil/app/.github/workflows/pwn.yaml@refs/heads/main"), + "the after signer is shown in full" + ); +} + +#[test] +fn signing_regression_cold_baseline_reads_as_a_weak_lead() { + let rows = vec![regression_rec( + "ghcr.io/acme/app", + "ghcr.io/acme/app:2", + "regression-unsigned-cold", + "now not signed (was signed) | before: releng@acme.example", + )]; + let html = render(&rows); + assert!(html.contains("signing regression")); + assert!( + html.contains("treat as a lead"), + "a cold-baseline regression is honestly flagged a weak lead" + ); +} + +#[test] +fn signing_regression_escapes_an_attacker_chosen_identity() { + // The before/after signer identities are attacker-influenceable Fulcio SANs — a crafted SAN in a + // regression row must not inject markup (maud auto-escape; never PreEscaped). + let evil = ""; + let rows = vec![regression_rec( + "ghcr.io/acme/app", + "ghcr.io/acme/app:2", + "regression-identity-established", + &format!( + "signed by {evil} via https://token.actions.githubusercontent.com | before: {evil}" + ), + )]; + let html = render(&rows); + assert!( + !html.contains("