Proof-of-concept proving the diagnostics-redaction exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
"Password and identity-secret samples are redacted."
Full coverage of the audit-report test matrix in
docs/security/diagnostic-redaction-audit-report.md §4
(REDACT-TC-001..010), plus two sanity tests.
The spike ships:
- RedactionPolicy: typed catalogue of regex rules
(identity-base64-blob, password-kv, ts3server-url-password,
authorization-bearer, linux/windows/macos user-path) with
optional capture-group narrowing.
- Structured-field redaction keyed on case-insensitive name
substrings (password, secret, token, ...).
- Bundle-level switches: chat and channel tree excluded by
default per audit-report §5.
- KnownSecretRegistry: literal-substring scrub for secrets the
host application has already loaded into memory (defense in
depth that regexes alone cannot guarantee — closes the gap
behind REDACT-TC-002).
- Length cap (MAX_PROTOCOL_STRING_LEN = 256) with truncation
marker for REDACT-TC-009.
- UTF-8 preserved in non-sensitive fields per REDACT-TC-010 /
ADR-008.
Test suite (12/12 PASS on 2026-05-13):
REDACT-TC-001 server password in connection data
REDACT-TC-002 identity secret in storage error (via KnownSecretRegistry)
REDACT-TC-003 server URL with password field
REDACT-TC-004 chat text excluded by default
REDACT-TC-005 channel name with Unicode excluded by default
REDACT-TC-006 nickname with Unicode preserved in safe field
REDACT-TC-007 local file path user segment minimized
REDACT-TC-008 mixed sensitive bundle (whole-bundle JSON scan)
REDACT-TC-009 long hostile protocol string truncated
REDACT-TC-010 multilingual safe text preserved
+ known-secret literal scrub
+ empty registered secret ignored
Out of scope: tracing-subscriber integration, diagnostic export
file format, memory/core dumps, performance, adversarial regex
evasion beyond trivial cases. These belong to chanora_diagnostics.
Authority: PoC plan §2, docs/security/diagnostic-redaction-audit-report.md,
SRS-093, SysRS-152/154/155.
Not product code; not promoted into chanora_diagnostics.
154 lines
4.9 KiB
Rust
154 lines
4.9 KiB
Rust
//! Redaction policy: the catalogue of patterns and structural rules
|
|
//! the redactor applies.
|
|
|
|
use once_cell::sync::Lazy;
|
|
use regex::Regex;
|
|
|
|
/// A single regex-driven rule.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RedactionRule {
|
|
pub id: &'static str,
|
|
pub description: &'static str,
|
|
pub pattern: Regex,
|
|
/// If `Some(n)`, the rule replaces capture-group `n` rather than
|
|
/// the whole match. Useful for "ts3server://host?password=XXXX"
|
|
/// where the host should be preserved but `XXXX` redacted.
|
|
pub redact_group: Option<usize>,
|
|
}
|
|
|
|
impl RedactionRule {
|
|
pub fn new(
|
|
id: &'static str,
|
|
description: &'static str,
|
|
pattern: &str,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
description,
|
|
pattern: Regex::new(pattern).expect("static rule regex must compile"),
|
|
redact_group: None,
|
|
}
|
|
}
|
|
|
|
pub fn new_group(
|
|
id: &'static str,
|
|
description: &'static str,
|
|
pattern: &str,
|
|
group: usize,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
description,
|
|
pattern: Regex::new(pattern).expect("static rule regex must compile"),
|
|
redact_group: Some(group),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The full redaction policy.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RedactionPolicy {
|
|
pub rules: Vec<RedactionRule>,
|
|
|
|
/// Structured-field redaction: any key whose lower-cased name
|
|
/// contains one of these substrings is fully redacted in
|
|
/// diagnostic bundles. Mirrors the bundle policy in
|
|
/// `diagnostic-redaction-audit-report.md` §5.
|
|
pub redact_field_substrings: Vec<&'static str>,
|
|
|
|
/// Bundle-level: include chat messages? Audit-report default = No.
|
|
pub include_chat: bool,
|
|
|
|
/// Bundle-level: include channel tree details?
|
|
pub include_channel_tree: bool,
|
|
}
|
|
|
|
/// Hard upper bound on protocol-string lengths kept in diagnostics.
|
|
///
|
|
/// REDACT-TC-009 ("long hostile protocol string"): truncate or safely
|
|
/// escape. The PoC truncates with a clear marker.
|
|
pub const MAX_PROTOCOL_STRING_LEN: usize = 256;
|
|
|
|
impl RedactionPolicy {
|
|
/// The default policy used by the PoC. Mirrors the audit-report
|
|
/// catalogue. Production code (`chanora_diagnostics`) will own the
|
|
/// canonical version of this list.
|
|
pub fn default_policy() -> Self {
|
|
Self {
|
|
rules: DEFAULT_RULES.clone(),
|
|
redact_field_substrings: vec![
|
|
"password",
|
|
"secret",
|
|
"private_key",
|
|
"private-key",
|
|
"identity_key",
|
|
"identity-key",
|
|
"token",
|
|
"credential",
|
|
"passphrase",
|
|
],
|
|
include_chat: false,
|
|
include_channel_tree: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
static DEFAULT_RULES: Lazy<Vec<RedactionRule>> = Lazy::new(|| {
|
|
vec![
|
|
// Identity secret as it appears in `tsclientlib` style:
|
|
// long base64-ish run preceded by a known marker.
|
|
// We err on the side of catching base64 blobs >= 64 chars.
|
|
RedactionRule::new(
|
|
"identity-base64-blob",
|
|
"Long base64-like run; matches identity secrets and tokens.",
|
|
r"\b[A-Za-z0-9+/]{64,}={0,2}\b",
|
|
),
|
|
// `password = "..."` / `password: "..."` / `password=...`
|
|
// Captures the value in group 1 so the *key* name is kept.
|
|
// The value excludes separators commonly found in URL query
|
|
// strings and config lines (`& , ; " whitespace`).
|
|
RedactionRule::new_group(
|
|
"password-kv",
|
|
"key=value style password assignment.",
|
|
r#"(?i)\b(?:password|passwd|pass|pwd)\s*[:=]\s*"?([^"\s,;&]+)"?"#,
|
|
1,
|
|
),
|
|
// ts3server://host?password=XXXX&...
|
|
RedactionRule::new_group(
|
|
"ts3server-url-password",
|
|
"Password embedded in a ts3server:// URL query string.",
|
|
r"(?i)(?:[?&])password=([^&\s]+)",
|
|
1,
|
|
),
|
|
// Generic Authorization: Bearer ...
|
|
RedactionRule::new_group(
|
|
"authorization-bearer",
|
|
"Authorization header bearer token.",
|
|
r"(?i)Authorization:\s*Bearer\s+(\S+)",
|
|
1,
|
|
),
|
|
// Linux user home: /home/<user>/... → minimize the username
|
|
// segment. Captures group 1 = username.
|
|
RedactionRule::new_group(
|
|
"linux-home-path",
|
|
"Linux home-directory path; minimizes the username segment.",
|
|
r"(/home/)([^/\s]+)",
|
|
2,
|
|
),
|
|
// Windows user: C:\Users\<user>\...
|
|
RedactionRule::new_group(
|
|
"windows-user-path",
|
|
"Windows user-profile path; minimizes the username segment.",
|
|
r"(?i)([A-Z]:\\Users\\)([^\\\s]+)",
|
|
2,
|
|
),
|
|
// macOS user: /Users/<user>/...
|
|
RedactionRule::new_group(
|
|
"macos-user-path",
|
|
"macOS user-profile path; minimizes the username segment.",
|
|
r"(/Users/)([^/\s]+)",
|
|
2,
|
|
),
|
|
]
|
|
});
|