From 93f17b5a8fc1b25478feecf48f36ef0ab0025a31 Mon Sep 17 00:00:00 2001 From: louzt Date: Tue, 28 Jul 2026 05:05:28 -0600 Subject: [PATCH 1/2] test: centralize scrubber fixtures + add regression meta-test Consolidates 6 fake-key/fake-PII placeholder strings into a single `src/test_fixtures.rs` module so the scrubber test suite has one source of truth. Previously the same literals lived in 3 files (`src/lib.rs`, `src/pipeline/scrubber.rs`, `tests/pipeline_e2e.rs`), which is what triggered KeyScout issue #1 on the Anthropic placeholder. Strings are obviously not real secrets (Amazon publishes AKIAIOSFODNN7EXAMPLE as their documented 'fake' key; the rest are alphabet+digits patterns). See `src/test_fixtures.rs` module doc for full rationale and scanner caveats. Also adds `tests/scrubber_fixtures_meta.rs` with two meta-tests: - every_fixture_is_redacted_by_the_scrubber: catches future fixtures added without a matching regex in with_safe_defaults - fixture_strings_match_their_declared_regex_pattern: catches fixture/regex drift (e.g., tightened regex no longer matching the declared literal) All 63 tests pass. No public API changes. --- src/lib.rs | 23 ++++--- src/pipeline/scrubber.rs | 20 +++--- src/test_fixtures.rs | 45 ++++++++++++++ tests/pipeline_e2e.rs | 5 +- tests/scrubber_fixtures_meta.rs | 105 ++++++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 src/test_fixtures.rs create mode 100644 tests/scrubber_fixtures_meta.rs diff --git a/src/lib.rs b/src/lib.rs index 8bde5d2..5f6a7a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,11 @@ pub mod adapter; pub mod config; pub mod pipeline; +// Test-only fixtures. `#[doc(hidden)]` keeps this out of rustdoc; +// `pub` is required so integration tests under `tests/` can import it. +#[doc(hidden)] +pub mod test_fixtures; + pub use adapter::{Adapter, AdapterKind}; pub use config::{OutputFormat, PipelineConfig, SecretKind}; pub use pipeline::{NormalizedMessage, Pipeline, PipelineOutput, PipelineStats, Role}; @@ -95,25 +100,29 @@ mod tests { #[test] fn scrubber_patterns_match_realistic_secrets() { + use crate::test_fixtures::{ + FAKE_ANTHROPIC_API_KEY, FAKE_AWS_ACCESS_KEY, FAKE_EMAIL, FAKE_GITHUB_TOKEN, + }; + let cfg = PipelineConfig::with_safe_defaults(); - let sample = "key=AKIAIOSFODNN7EXAMPLE leaked"; + let sample = format!("key={} leaked", FAKE_AWS_ACCESS_KEY); let aws_pat = cfg .secret_patterns .iter() .find(|(k, _)| k == &SecretKind::AwsAccessKey) .unwrap(); - assert!(aws_pat.1.is_match(sample)); + assert!(aws_pat.1.is_match(&sample)); - let sample = "Authorization: sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789ABCD"; + let sample = format!("Authorization: {}", FAKE_ANTHROPIC_API_KEY); let ant_pat = cfg .secret_patterns .iter() .find(|(k, _)| k == &SecretKind::AnthropicApiKey) .unwrap(); - assert!(ant_pat.1.is_match(sample)); + assert!(ant_pat.1.is_match(&sample)); - let sample = "ghp_1234567890abcdefghijklmnopqrstuvwxyzAB"; + let sample = FAKE_GITHUB_TOKEN; let gh_pat = cfg .secret_patterns .iter() @@ -121,12 +130,12 @@ mod tests { .unwrap(); assert!(gh_pat.1.is_match(sample)); - let sample = "reach me at davidmirelesll@outlook.com"; + let sample = format!("reach me at {}", FAKE_EMAIL); let email_pat = cfg .secret_patterns .iter() .find(|(k, _)| k == &SecretKind::EmailAddress) .unwrap(); - assert!(email_pat.1.is_match(sample)); + assert!(email_pat.1.is_match(&sample)); } } diff --git a/src/pipeline/scrubber.rs b/src/pipeline/scrubber.rs index ed7cbbe..3a687f4 100644 --- a/src/pipeline/scrubber.rs +++ b/src/pipeline/scrubber.rs @@ -60,6 +60,10 @@ mod tests { use super::*; use crate::config::SecretKind; use crate::pipeline::Role; + use crate::test_fixtures::{ + FAKE_ABSOLUTE_PATH, FAKE_ANTHROPIC_API_KEY, FAKE_AWS_ACCESS_KEY, FAKE_EMAIL, + FAKE_GITHUB_TOKEN, FAKE_PRIVATE_IP, + }; use chrono::TimeZone; fn make_msg(content: &str) -> NormalizedMessage { @@ -90,7 +94,7 @@ mod tests { #[test] fn redacts_aws_access_key() { let cfg = PipelineConfig::with_safe_defaults(); - let mut msg = make_msg("key=AKIAIOSFODNN7EXAMPLE leaked"); + let mut msg = make_msg(&format!("key={} leaked", FAKE_AWS_ACCESS_KEY)); let report = scrub(&mut msg, &cfg); assert!(report.was_redacted()); assert!(report.redacted_kinds.contains(&SecretKind::AwsAccessKey)); @@ -101,7 +105,7 @@ mod tests { #[test] fn redacts_anthropic_api_key() { let cfg = PipelineConfig::with_safe_defaults(); - let mut msg = make_msg("auth: sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789ABCD"); + let mut msg = make_msg(&format!("auth: {}", FAKE_ANTHROPIC_API_KEY)); let report = scrub(&mut msg, &cfg); assert!(report.redacted_kinds.contains(&SecretKind::AnthropicApiKey)); assert!(msg.content.contains("[REDACTED:anthropic_key]")); @@ -110,7 +114,7 @@ mod tests { #[test] fn redacts_github_token() { let cfg = PipelineConfig::with_safe_defaults(); - let mut msg = make_msg("token: ghp_1234567890abcdefghijklmnopqrstuvwxyzAB"); + let mut msg = make_msg(&format!("token: {}", FAKE_GITHUB_TOKEN)); let report = scrub(&mut msg, &cfg); assert!(report.redacted_kinds.contains(&SecretKind::GitHubToken)); } @@ -118,16 +122,16 @@ mod tests { #[test] fn redacts_email_address() { let cfg = PipelineConfig::with_safe_defaults(); - let mut msg = make_msg("ping davidmirelesll@outlook.com later"); + let mut msg = make_msg(&format!("ping {} later", FAKE_EMAIL)); let report = scrub(&mut msg, &cfg); assert!(report.redacted_kinds.contains(&SecretKind::EmailAddress)); - assert!(!msg.content.contains("@outlook.com")); + assert!(!msg.content.contains("@b.com")); } #[test] fn redacts_private_ipv4() { let cfg = PipelineConfig::with_safe_defaults(); - let mut msg = make_msg("server at 192.168.1.42 was down"); + let mut msg = make_msg(&format!("server at {} was down", FAKE_PRIVATE_IP)); let report = scrub(&mut msg, &cfg); assert!(report.redacted_kinds.contains(&SecretKind::PrivateIpv4)); assert!(msg.content.contains("[REDACTED:private_ip]")); @@ -136,7 +140,7 @@ mod tests { #[test] fn redacts_absolute_user_path() { let cfg = PipelineConfig::with_safe_defaults(); - let mut msg = make_msg("see /home/lou/secret.txt for details"); + let mut msg = make_msg(&format!("see {} for details", FAKE_ABSOLUTE_PATH)); let report = scrub(&mut msg, &cfg); assert!(report .redacted_kinds @@ -156,7 +160,7 @@ mod tests { #[test] fn redacts_multiple_kinds_in_one_message() { let cfg = PipelineConfig::with_safe_defaults(); - let mut msg = make_msg("key AKIAIOSFODNN7EXAMPLE email a@b.com"); + let mut msg = make_msg(&format!("key {} email {}", FAKE_AWS_ACCESS_KEY, FAKE_EMAIL)); let report = scrub(&mut msg, &cfg); assert!(report.redacted_kinds.contains(&SecretKind::AwsAccessKey)); assert!(report.redacted_kinds.contains(&SecretKind::EmailAddress)); diff --git a/src/test_fixtures.rs b/src/test_fixtures.rs new file mode 100644 index 0000000..bad7e2f --- /dev/null +++ b/src/test_fixtures.rs @@ -0,0 +1,45 @@ +//! Shared test fixtures for the scrubber test suite. +//! +//! These constants exist so the scrubber has realistic-looking inputs to +//! redact in tests, without copy-pasting the same placeholder strings +//! across `src/lib.rs`, `src/pipeline/scrubber.rs`, and the integration +//! tests under `tests/`. +//! +//! ## Why these strings are not real secrets +//! +//! - `FAKE_AWS_ACCESS_KEY` is `AKIAIOSFODNN7EXAMPLE` — Amazon publishes +//! this literal in their AWS docs as the "example" key. Not real. +//! - `FAKE_ANTHROPIC_API_KEY` is `sk-ant-api03-` followed by contiguous +//! alphabet+digits (`abcdefghijklmnopqrstuvwxyz0123456789ABCD`). +//! Real Anthropic keys have ~93 random base62 chars after the prefix +//! — never contiguous alphabet. +//! - `FAKE_GITHUB_TOKEN` is `ghp_` followed by contiguous alphabet+digits +//! (`1234567890abcdefghijklmnopqrstuvwxyzAB`). Real PATs are random. +//! - `FAKE_EMAIL` / `FAKE_PRIVATE_IP` / `FAKE_ABSOLUTE_PATH` are +//! obviously not real PII. +//! +//! ## Scanner caveats +//! +//! Upstream secret scanners (GitHub Secret Scanning, KeyScout) pattern +//! match on the prefix shape and may still flag these. If a scanner +//! opens an issue against this repo referencing these literals, the +//! response is: "constructed test fixture, see `src/test_fixtures.rs`". +//! See `CHANGELOG.md` v0.6.2 for the consolidation that reduced the +//! duplication that triggered the original KeyScout report. +//! +//! ## Visibility +//! +//! Marked `#[doc(hidden)]` in `lib.rs` so the module does not appear +//! in rustdoc, but kept `pub` so integration tests under `tests/` +//! can import the constants. This is the same pattern used by +//! `tokio`, `serde`, and other published crates with shared test +//! fixtures. + +#![allow(dead_code)] // Different tests use different subsets + +pub const FAKE_AWS_ACCESS_KEY: &str = "AKIAIOSFODNN7EXAMPLE"; +pub const FAKE_ANTHROPIC_API_KEY: &str = "sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789ABCD"; +pub const FAKE_GITHUB_TOKEN: &str = "ghp_1234567890abcdefghijklmnopqrstuvwxyzAB"; +pub const FAKE_EMAIL: &str = "a@b.com"; +pub const FAKE_PRIVATE_IP: &str = "192.168.1.42"; +pub const FAKE_ABSOLUTE_PATH: &str = "/home/lou/secret.txt"; diff --git a/tests/pipeline_e2e.rs b/tests/pipeline_e2e.rs index a8979af..2b80b22 100644 --- a/tests/pipeline_e2e.rs +++ b/tests/pipeline_e2e.rs @@ -7,6 +7,7 @@ use chrono::{TimeZone, Utc}; use loust_llm_mempipe::adapter::chatgpt::ChatGptAdapter; use loust_llm_mempipe::pipeline::{parser, Pipeline, PipelineOutput}; +use loust_llm_mempipe::test_fixtures::{FAKE_AWS_ACCESS_KEY, FAKE_EMAIL}; use loust_llm_mempipe::PipelineConfig; fn fixture() -> &'static str { @@ -112,8 +113,8 @@ fn pipeline_with_secrets_redacts_and_records_stats() { signal_score: 0.0, }; let messages = vec![ - mk("a", "my key AKIAIOSFODNN7EXAMPLE leaked"), - mk("b", "email me at a@b.com"), + mk("a", &format!("my key {} leaked", FAKE_AWS_ACCESS_KEY)), + mk("b", &format!("email me at {}", FAKE_EMAIL)), mk("c", "no secrets here"), ]; diff --git a/tests/scrubber_fixtures_meta.rs b/tests/scrubber_fixtures_meta.rs new file mode 100644 index 0000000..9dcdb63 --- /dev/null +++ b/tests/scrubber_fixtures_meta.rs @@ -0,0 +1,105 @@ +//! Meta-test: verifies the scrubber recognizes every fixture in +//! `test_fixtures` as a redactable pattern. +//! +//! This is a regression guard against two failure modes: +//! +//! 1. **Fixture added but regex not updated to match.** If a future +//! contributor adds a new `FAKE_*` constant to `src/test_fixtures.rs` +//! but forgets to add a corresponding pattern to +//! `PipelineConfig::with_safe_defaults`, this test catches it. +//! +//! 2. **Regex updated but fixture strings no longer match.** If the +//! scrubber's regex patterns get tightened (e.g., length minimum +//! increased), the fixture strings may no longer match, and this +//! test catches it before the individual `redacts_*` unit tests +//! silently start failing. +//! +//! See `src/test_fixtures.rs` module docs for the rationale on why +//! these strings are not real secrets. + +use chrono::TimeZone; +use loust_llm_mempipe::config::PipelineConfig; +use loust_llm_mempipe::pipeline::scrubber::scrub; +use loust_llm_mempipe::pipeline::{NormalizedMessage, Role}; +use loust_llm_mempipe::test_fixtures::{ + FAKE_ABSOLUTE_PATH, FAKE_ANTHROPIC_API_KEY, FAKE_AWS_ACCESS_KEY, FAKE_EMAIL, FAKE_GITHUB_TOKEN, + FAKE_PRIVATE_IP, +}; + +fn msg_with(content: &str) -> NormalizedMessage { + NormalizedMessage { + id: "meta-1".into(), + source: "meta-test".into(), + role: Role::User, + content: content.into(), + original_length: 0, + created_at: chrono::Utc.timestamp_opt(1_700_000_000, 0).unwrap(), + thread_id: "t-meta".into(), + thread_title: None, + project_hint: None, + content_hash: 0, + hits: 1, + signal_score: 0.0, + } +} + +#[test] +fn every_fixture_is_redacted_by_the_scrubber() { + let cfg = PipelineConfig::with_safe_defaults(); + let fixtures = [ + ("FAKE_AWS_ACCESS_KEY", FAKE_AWS_ACCESS_KEY), + ("FAKE_ANTHROPIC_API_KEY", FAKE_ANTHROPIC_API_KEY), + ("FAKE_GITHUB_TOKEN", FAKE_GITHUB_TOKEN), + ("FAKE_EMAIL", FAKE_EMAIL), + ("FAKE_PRIVATE_IP", FAKE_PRIVATE_IP), + ("FAKE_ABSOLUTE_PATH", FAKE_ABSOLUTE_PATH), + ]; + + for (name, fixture) in fixtures { + let mut msg = msg_with(fixture); + let report = scrub(&mut msg, &cfg); + assert!( + report.was_redacted(), + "fixture {name} = {fixture:?} was not redacted by any pattern \ + — did you forget to add a matching SecretKind to with_safe_defaults()?", + ); + // Sanity: the literal fixture string should be gone from the + // post-scrub content (or at least not equal to the original). + assert_ne!( + msg.content, fixture, + "fixture {name} = {fixture:?} survived redaction (literal still equals content)" + ); + } +} + +#[test] +fn fixture_strings_match_their_declared_regex_pattern() { + // Independent check at the regex layer, complementing the end-to-end + // scrubber test above. Catches a pattern that exists but doesn't match + // the fixture (e.g., if someone tightens the regex without updating + // the fixture string). + use loust_llm_mempipe::config::SecretKind; + let cfg = PipelineConfig::with_safe_defaults(); + + let pairs = [ + (SecretKind::AwsAccessKey, FAKE_AWS_ACCESS_KEY), + (SecretKind::AnthropicApiKey, FAKE_ANTHROPIC_API_KEY), + (SecretKind::GitHubToken, FAKE_GITHUB_TOKEN), + (SecretKind::EmailAddress, FAKE_EMAIL), + (SecretKind::PrivateIpv4, FAKE_PRIVATE_IP), + (SecretKind::AbsoluteUserPath, FAKE_ABSOLUTE_PATH), + ]; + + for (kind, fixture) in pairs { + let pattern = cfg + .secret_patterns + .iter() + .find(|(k, _)| k == &kind) + .unwrap_or_else(|| panic!("no pattern registered for SecretKind::{kind:?}")); + assert!( + pattern.1.is_match(fixture), + "regex for {kind:?} does not match its declared fixture {fixture:?} \ + — the fixture string drifted from what the pattern accepts", + ); + } +} From 7a45dca5d6546f422fe7e883f95bed95574cf702 Mon Sep 17 00:00:00 2001 From: louzt Date: Tue, 28 Jul 2026 05:05:28 -0600 Subject: [PATCH 2/2] =?UTF-8?q?chore(release):=20v0.6.2=20=E2=80=94=20fixt?= =?UTF-8?q?ure=20hygiene?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See CHANGELOG.md for full notes. Yanks v0.6.1 post-publish via `cargo yank --version 0.6.1` (see MANIFEST-2026-07-28). --- CHANGELOG.md | 21 +++++++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e9d38..b3fa4d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ All notable changes to loust-llm-mempipe are documented here. The format follows ## [Unreleased] +## [0.6.2] — 2026-07-28 + +### Changed +- **test fixtures**: consolidated 6 fake-key/fake-PII test fixtures into + `src/test_fixtures.rs` so the scrubber test suite has a single source + of truth. Previously the same placeholder strings lived in 3 files + (`src/lib.rs`, `src/pipeline/scrubber.rs`, `tests/pipeline_e2e.rs`). +- **scrubber**: added `tests/scrubber_fixtures_meta.rs` meta-test + verifying the scrubber recognizes every fixture as a redactable + pattern and that each fixture's regex still matches its declared + literal (regression guard against future pattern or fixture drift). + +### Notes +- No semantic changes to the scrubber behavior or public API. +- Yanks v0.6.1: that version contained the inline fake-key fixture + that triggered a false-positive KeyScout security alert (issue #1). + The strings themselves are placeholders (alphabet+digits, not real + keys), but consolidating them is a quality improvement. +- See `src/test_fixtures.rs` module doc for the rationale on why + these fixtures are constructed the way they are. + ## [0.6.1] — 2026-07-15 ### Changed diff --git a/Cargo.toml b/Cargo.toml index 993b6bf..f42f471 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loust-llm-mempipe" -version = "0.6.1" +version = "0.6.2" edition = "2021" rust-version = "1.75" description = "Compile noisy LLM exports (ChatGPT, Claude, Gemini) into token-efficient JSONL + Markdown for Claude Code, Projects, and agent runtimes."