diff --git a/lib/components/fabro-workflow/src/pipeline/persist.rs b/lib/components/fabro-workflow/src/pipeline/persist.rs index cc12c3cba..846f9b853 100644 --- a/lib/components/fabro-workflow/src/pipeline/persist.rs +++ b/lib/components/fabro-workflow/src/pipeline/persist.rs @@ -272,6 +272,50 @@ mod tests { assert!(loaded.diagnostics().is_empty()); } + #[tokio::test] + async fn load_from_store_preserves_high_entropy_dockerfile_content() { + // The spec the worker executes must survive the store byte-identical. + // Event redaction is a storage/display concern; when it reaches the + // spec that `load_from_store` rehydrates, the sandbox builds a + // corrupted Dockerfile: `ARG NAME=` pairs come back as + // `ARG REDACTED`, the build's `set -eu` step fails on the unset + // variable, and the environment's snapshot identity silently changes. + let temp = tempfile::tempdir().unwrap(); + let run_dir = temp.path().join("run"); + std::fs::create_dir_all(&run_dir).unwrap(); + let (graph, source) = graph_and_source(); + + // Two shapes that must both survive: the hex pins that triggered the + // production failure, and a token high-entropy enough that any + // detector will keep flagging it in stored events. The second keeps + // this test red until execution stops reading redacted content, + // independent of how the entropy heuristic evolves. + let dockerfile = "FROM buildpack-deps:noble\n\ + ARG DOCKER_INSTALL_COMMIT=5ce20f2eef3615d08fea941eda5a109e949e8ebf\n\ + ARG DOCKER_INSTALL_SHA256=b991f2806186f7287bb9e53362060c382e906d154599b2fb0982f34246bacfd4\n\ + ENV CACHE_SALT=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p\n\ + RUN install-docker \"${DOCKER_INSTALL_COMMIT}\" \"${DOCKER_INSTALL_SHA256}\"\n"; + + let mut record = sample_record(different_graph()); + record.graph = graph; + record.settings.run.environment.image.dockerfile = Some( + fabro_types::settings::run::DockerfileSource::Inline(dockerfile.to_string()), + ); + + let run_store = seeded_store(&record, Some(&source)).await; + let loaded = load_from_store(&run_store.clone().into(), &run_dir) + .await + .unwrap(); + + assert_eq!( + loaded.run_spec().settings.run.environment.image.dockerfile, + Some(fabro_types::settings::run::DockerfileSource::Inline( + dockerfile.to_string() + )), + "the executable run spec must round-trip through the store unredacted" + ); + } + #[test] fn persist_returns_error_on_io_failure() { let temp = tempfile::tempdir().unwrap(); diff --git a/lib/foundation/fabro-redact/src/lib.rs b/lib/foundation/fabro-redact/src/lib.rs index 8ed562e53..4a7efa45b 100644 --- a/lib/foundation/fabro-redact/src/lib.rs +++ b/lib/foundation/fabro-redact/src/lib.rs @@ -111,6 +111,27 @@ mod tests { assert_eq!(result, "key=REDACTED"); } + #[test] + fn redact_string_keeps_assignment_with_low_entropy_value() { + // A pinned git SHA is pure hex, so the value alone can never exceed + // 4.0 bits of entropy. Only the merged NAME=value token crosses the + // 4.5-bit threshold, because the uppercase name widens the charset. + // Measuring the name together with the value redacts innocuous + // pins; the pair must survive. + let input = "ARG DOCKER_INSTALL_COMMIT=5ce20f2eef3615d08fea941eda5a109e949e8ebf"; + assert_eq!(redact_string(input), input); + } + + #[test] + fn redact_string_keeps_assignment_key_for_high_entropy_value() { + // The value alone is above the entropy threshold, so it is + // redacted either way — but the name says which setting was + // redacted and must survive, as the gitleaks layer already + // does for `key=REDACTED`. + let result = redact_string("BUILD_STAMP=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p"); + assert_eq!(result, "BUILD_STAMP=REDACTED"); + } + #[test] fn redact_string_overlapping_detections_produce_single_redacted() { // A high-entropy string that also matches a gitleaks pattern