From 11250895e7d16eb37468c6e7fa35a340fd07c6fe Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 11:19:07 -0400 Subject: [PATCH 01/27] test(sccm): require sealed client evidence admission --- .../src/sccm/client/admission_tests.rs | 203 ++++++++++++++++++ .../cmtraceopen-parser/src/sccm/client/mod.rs | 3 + 2 files changed, 206 insertions(+) create mode 100644 crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs new file mode 100644 index 000000000..86f75cff1 --- /dev/null +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -0,0 +1,203 @@ +use sha2::{Digest, Sha256}; + +use super::{ + admit_client_evidence, assess_client_intake, SccmClientCapturedPayload, + SccmClientIntakeArtifact, SccmClientIntakeBundle, +}; +use crate::sccm::{SccmArtifact, SccmCoverageState, SccmRole, SccmRotation}; + +fn digest(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn bundle() -> SccmClientIntakeBundle { + SccmClientIntakeBundle { + artifacts: vec![artifact("policy-agent", "PolicyAgent.log")], + capture_gaps: Vec::new(), + } +} + +fn artifact(identity: &str, basename: &str) -> SccmClientIntakeArtifact { + let group = match basename { + "PolicyAgent.log" => "client-policy-agent", + "CIAgent.log" => "client-policy-state", + _ => panic!("test artifact must be catalogued"), + }; + SccmClientIntakeArtifact { + artifact: SccmArtifact { + artifact_id: format!("fixture-admitted-{identity}"), + display_name: basename.to_owned(), + original_path: None, + host: None, + role: SccmRole::Client, + configmgr_version: Some("5.00.9128.1000".to_owned()), + collected_at_utc: Some("2026-07-30T00:00:00Z".to_owned()), + rotation: SccmRotation::Current, + coverage: SccmCoverageState::Captured, + encoding: Some("utf-8".to_owned()), + }, + path_fingerprint: Some(format!("synthetic-admitted-{identity}")), + rotation_lineage: None, + relative_path: Some(format!("evidence/{group}/current/{basename}")), + fragment_complete: Some(true), + } +} + +fn payload() -> SccmClientCapturedPayload { + payload_for("fixture-admitted-policy-agent", "+000") +} + +fn payload_for(artifact_id: &str, offset: &str) -> SccmClientCapturedPayload { + let bytes = concat!( + "", + "\n" + ); + let bytes = bytes.into_bytes(); + SccmClientCapturedPayload { + artifact_id: artifact_id.to_owned(), + byte_length: bytes.len() as u64, + expected_sha256: digest(&bytes), + bytes, + } +} + +#[test] +fn admission_seals_canonical_records_from_complete_captured_payloads() { + let bundle = bundle(); + let assessment = assess_client_intake(&bundle).expect("fixture assessment is canonical"); + + let admitted = admit_client_evidence(&bundle, &assessment, &[payload()]) + .expect("a complete payload with the registered profile is admitted"); + + assert_eq!(admitted.evidence().len(), 1); + assert!(admitted.verify_integrity().is_ok()); + assert_eq!( + admitted.source_coverage("client-policy-agent"), + Some(&SccmCoverageState::Captured) + ); +} + +#[test] +fn admission_rejects_missing_extra_duplicate_and_swapped_payloads() { + let mut bundle = bundle(); + bundle.artifacts.push(artifact("policy-state", "CIAgent.log")); + let assessment = assess_client_intake(&bundle).expect("two payload fixture is canonical"); + let agent = payload(); + let state = payload_for("fixture-admitted-policy-state", "+000"); + + assert!(admit_client_evidence(&bundle, &assessment, &[agent.clone()]).is_err()); + + let mut extra = vec![agent.clone(), state.clone()]; + extra.push(payload_for("fixture-not-in-bundle", "+000")); + assert!(admit_client_evidence(&bundle, &assessment, &extra).is_err()); + + assert!(admit_client_evidence(&bundle, &assessment, &[agent.clone(), agent]).is_err()); + + let mut swapped = state; + swapped.artifact_id = "fixture-admitted-policy-agent".to_owned(); + assert!(admit_client_evidence(&bundle, &assessment, &[payload(), swapped]).is_err()); +} + +#[test] +fn admission_rejects_payload_digest_and_length_mismatches() { + let bundle = bundle(); + let assessment = assess_client_intake(&bundle).expect("fixture assessment is canonical"); + + let mut bad_digest = payload(); + bad_digest.expected_sha256 = "0".repeat(64); + assert!(admit_client_evidence(&bundle, &assessment, &[bad_digest]).is_err()); + + let mut bad_length = payload(); + bad_length.byte_length += 1; + assert!(admit_client_evidence(&bundle, &assessment, &[bad_length]).is_err()); +} + +#[test] +fn admission_rejects_noncaptured_incomplete_malformed_and_invalid_offset_payloads() { + let mut capped = bundle(); + capped.artifacts[0].artifact.coverage = SccmCoverageState::Capped; + capped.artifacts[0].fragment_complete = Some(false); + let capped_assessment = assess_client_intake(&capped).expect("capped state is explicit"); + assert!(admit_client_evidence(&capped, &capped_assessment, &[payload()]).is_err()); + + let mut incomplete = bundle(); + incomplete.artifacts[0].fragment_complete = Some(false); + let incomplete_assessment = + assess_client_intake(&incomplete).expect("incomplete boundary is explicit"); + assert!(admit_client_evidence(&incomplete, &incomplete_assessment, &[payload()]).is_err()); + + let malformed = SccmClientCapturedPayload { + artifact_id: "fixture-admitted-policy-agent".to_owned(), + bytes: b"not a CCM logical record".to_vec(), + byte_length: 24, + expected_sha256: digest(b"not a CCM logical record"), + }; + let assessment = assess_client_intake(&bundle()).expect("fixture assessment is canonical"); + assert!(admit_client_evidence(&bundle(), &assessment, &[malformed]).is_err()); + + assert!(admit_client_evidence(&bundle(), &assessment, &[payload_for( + "fixture-admitted-policy-agent", + "+9999", + )]) + .is_err()); +} + +#[test] +fn admission_reassesses_bundle_and_is_deterministic_across_payload_order() { + let mut bundle = bundle(); + bundle.artifacts.push(artifact("policy-state", "CIAgent.log")); + let canonical = assess_client_intake(&bundle).expect("canonical assessment"); + let mut forged = canonical.clone(); + forged.groups[0].fragments.clear(); + assert!(admit_client_evidence(&bundle, &forged, &[payload(), payload_for( + "fixture-admitted-policy-state", + "+000", + )]) + .is_err()); + + let forward = admit_client_evidence( + &bundle, + &canonical, + &[payload(), payload_for("fixture-admitted-policy-state", "+000")], + ) + .expect("forward payload ordering is admitted"); + let reverse = admit_client_evidence( + &bundle, + &canonical, + &[payload_for("fixture-admitted-policy-state", "+000"), payload()], + ) + .expect("reverse payload ordering is admitted"); + assert_eq!(forward.evidence(), reverse.evidence()); + assert_eq!(forward.integrity_seal(), reverse.integrity_seal()); +} + +#[test] +fn admission_integrity_rejects_test_only_record_profile_and_identity_collisions() { + let bundle = bundle(); + let assessment = assess_client_intake(&bundle).expect("fixture assessment is canonical"); + + let mut record_mutation = + admit_client_evidence(&bundle, &assessment, &[payload()]).expect("admitted evidence"); + record_mutation.test_only_mutate_first_message(); + assert!(record_mutation.verify_integrity().is_err()); + + let mut profile_mutation = + admit_client_evidence(&bundle, &assessment, &[payload()]).expect("admitted evidence"); + profile_mutation.test_only_mutate_first_profile(); + assert!(profile_mutation.verify_integrity().is_err()); + + let mut collision = + admit_client_evidence(&bundle, &assessment, &[payload()]).expect("admitted evidence"); + collision.test_only_duplicate_first_evidence(); + assert!(collision.verify_integrity().is_err()); +} diff --git a/crates/cmtraceopen-parser/src/sccm/client/mod.rs b/crates/cmtraceopen-parser/src/sccm/client/mod.rs index dd43bd011..5af403fb4 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/mod.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/mod.rs @@ -5,4 +5,7 @@ mod intake; +#[cfg(test)] +mod admission_tests; + pub use intake::*; From c863233dac5434ab0e2e270fa764602864733f3c Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 11:29:21 -0400 Subject: [PATCH 02/27] feat(sccm): seal canonical client evidence admission --- .../src/sccm/client/admission.rs | 378 ++++++++++++++++++ .../src/sccm/client/admission_tests.rs | 150 +++++-- .../cmtraceopen-parser/src/sccm/client/mod.rs | 1 + 3 files changed, 497 insertions(+), 32 deletions(-) create mode 100644 crates/cmtraceopen-parser/src/sccm/client/admission.rs diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs new file mode 100644 index 000000000..64c6c0aae --- /dev/null +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -0,0 +1,378 @@ +//! Crate-private sealing of canonical SCCM client evidence. +//! +//! Raw payload bytes exist only while this constructor verifies their digest +//! and normalizes their CCM logical records. Reducers receive the resulting +//! immutable-by-API capability, never bytes or caller-supplied evidence. + +// This is deliberately a crate-private shared interface that lands before +// workflow reducers. No production reducer owns it in this slice, so rustc +// cannot yet observe a call site; retaining the lint allowance here avoids +// weakening workspace-wide warning policy while preserving the review gate. +#![allow(dead_code)] + +use std::collections::{BTreeMap, BTreeSet}; + +use encoding_rs::{UTF_16BE, UTF_16LE, UTF_8, WINDOWS_1252}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +use crate::parser::ccm::scan_logical_records; +use crate::sccm::evidence::SccmRawEvidenceSnapshot; +use crate::sccm::{ + SccmArtifact, SccmCoverageState, SccmEvidence, SccmExtractionProfile, + SccmExtractionProfileMaturity, SccmRole, SccmTimeOrderingState, + SCCM_EXPERIMENTAL_KEY_PROFILE_ID, +}; + +use super::{ + assess_client_intake, SccmClientIntakeAssessment, SccmClientIntakeBundle, + SccmClientIntakeError, SccmClientIntakeFragment, MAX_SCCM_CLIENT_INTAKE_ARTIFACTS, +}; + +/// Raw, already-captured bytes offered to the one-shot client evidence +/// admission boundary. This is an input only: the successful capability does +/// not retain this vector or any decoded raw text. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SccmClientCapturedPayload { + pub artifact_id: String, + pub bytes: Vec, + pub byte_length: u64, + pub expected_sha256: String, +} + +/// The bounded evidence authority internal client reducers consume. +/// +/// Its fields stay private and it deliberately implements neither serde nor a +/// public constructor. `verify_integrity` recomputes the deterministic seal so +/// a reducer can fail closed if future crate-internal code corrupts it. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct SccmClientAdmittedEvidence { + evidence: Vec, + source_coverage: BTreeMap, + profiles_by_artifact: BTreeMap, + integrity_seal: String, +} + +impl SccmClientAdmittedEvidence { + pub(crate) fn evidence(&self) -> Result<&[SccmEvidence], SccmClientEvidenceAdmissionError> { + self.verify_integrity()?; + Ok(&self.evidence) + } + + pub(crate) fn source_coverage( + &self, + logical_artifact_id: &str, + ) -> Result, SccmClientEvidenceAdmissionError> { + self.verify_integrity()?; + Ok(self.source_coverage.get(logical_artifact_id)) + } + + pub(crate) fn profile_for_artifact( + &self, + artifact_id: &str, + ) -> Result, SccmClientEvidenceAdmissionError> { + self.verify_integrity()?; + Ok(self.profiles_by_artifact.get(artifact_id)) + } + + pub(crate) fn integrity_seal(&self) -> &str { + &self.integrity_seal + } + + pub(crate) fn verify_integrity(&self) -> Result<(), SccmClientEvidenceAdmissionError> { + let recomputed = compute_integrity_seal( + &self.evidence, + &self.source_coverage, + &self.profiles_by_artifact, + )?; + (recomputed == self.integrity_seal) + .then_some(()) + .ok_or(SccmClientEvidenceAdmissionError::IntegrityViolation) + } + + /// Makes workflow handling of a missing, capped, malformed, or partial + /// source explicit. The authority never turns a coverage gap into success. + pub(crate) fn require_captured_source( + &self, + logical_artifact_id: &str, + ) -> Result<(), SccmClientEvidenceAdmissionError> { + match self.source_coverage(logical_artifact_id)? { + Some(SccmCoverageState::Captured) => Ok(()), + Some(_) => Err(SccmClientEvidenceAdmissionError::SourceCoverageUnavailable), + None => Err(SccmClientEvidenceAdmissionError::UnknownSourceGroup), + } + } + + #[cfg(test)] + pub(crate) fn test_only_mutate_first_message(&mut self) { + self.evidence[0].message.push_str(" forged"); + } + + #[cfg(test)] + pub(crate) fn test_only_mutate_first_profile(&mut self) { + self.profiles_by_artifact + .values_mut() + .next() + .expect("test admission has one selected profile") + .profile_id + .push_str("-forged"); + } + + #[cfg(test)] + pub(crate) fn test_only_duplicate_first_evidence(&mut self) { + self.evidence.push(self.evidence[0].clone()); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub(crate) enum SccmClientEvidenceAdmissionError { + #[error("client evidence admission bundle is invalid: {0}")] + InvalidBundle(SccmClientIntakeError), + #[error("client evidence admission assessment is not the canonical bundle projection")] + AssessmentMutation, + #[error("client evidence admission payload count exceeds the v1 bound")] + PayloadLimitExceeded, + #[error("client evidence admission contains duplicate payload artifact IDs")] + DuplicatePayload, + #[error("client evidence admission is missing a payload for a captured fragment")] + MissingPayload, + #[error("client evidence admission payload has no matching canonical captured fragment")] + ExtraPayload, + #[error("client evidence admission payload targets a noncaptured or incomplete fragment")] + NonAdmissibleFragment, + #[error("client evidence admission payload length or digest is invalid")] + PayloadIntegrityMismatch, + #[error("client evidence admission cannot decode the declared artifact encoding")] + InvalidEncoding, + #[error("client evidence admission payload has no complete CCM logical record")] + MalformedCcm, + #[error("client evidence admission record timestamp provenance is not comparable")] + InvalidTimestampProvenance, + #[error("client evidence admission selected an unregistered extraction profile")] + UnregisteredProfile, + #[error("client evidence admission produced colliding logical evidence identities")] + CollidingEvidenceIdentity, + #[error("client evidence admission integrity seal is invalid")] + IntegrityViolation, + #[error("client evidence admission source group is not declared")] + UnknownSourceGroup, + #[error("client evidence admission source coverage is not complete captured evidence")] + SourceCoverageUnavailable, +} + +/// Reassesses a canonical client bundle and seals the logical CCM evidence it +/// derives from each exact complete captured payload. +pub(crate) fn admit_client_evidence( + bundle: &SccmClientIntakeBundle, + assessment: &SccmClientIntakeAssessment, + payloads: &[SccmClientCapturedPayload], +) -> Result { + if payloads.len() > MAX_SCCM_CLIENT_INTAKE_ARTIFACTS { + return Err(SccmClientEvidenceAdmissionError::PayloadLimitExceeded); + } + + let canonical = + assess_client_intake(bundle).map_err(SccmClientEvidenceAdmissionError::InvalidBundle)?; + if canonical != *assessment { + return Err(SccmClientEvidenceAdmissionError::AssessmentMutation); + } + + let source_coverage = canonical + .groups + .iter() + .map(|group| (group.logical_artifact_id.clone(), group.coverage.clone())) + .collect::>(); + let eligible = canonical + .physical_artifacts + .iter() + .map(|fragment| { + (fragment.coverage == SccmCoverageState::Captured + && fragment.fragment_complete == Some(true)) + .then_some((fragment.artifact_id.clone(), fragment)) + .ok_or(SccmClientEvidenceAdmissionError::NonAdmissibleFragment) + }) + .collect::, _>>()?; + + if eligible.len() != canonical.physical_artifacts.len() { + return Err(SccmClientEvidenceAdmissionError::NonAdmissibleFragment); + } + if payloads.len() != eligible.len() { + return Err(SccmClientEvidenceAdmissionError::MissingPayload); + } + + let mut ordered_payloads = payloads.iter().collect::>(); + ordered_payloads.sort_by(|left, right| left.artifact_id.cmp(&right.artifact_id)); + let mut seen_payload_ids = BTreeSet::new(); + let mut evidence = Vec::new(); + let mut profiles_by_artifact = BTreeMap::new(); + let mut evidence_ids = BTreeSet::new(); + let mut evidence_references = BTreeSet::new(); + + for payload in ordered_payloads { + if !seen_payload_ids.insert(payload.artifact_id.as_str()) { + return Err(SccmClientEvidenceAdmissionError::DuplicatePayload); + } + let fragment = eligible + .get(&payload.artifact_id) + .copied() + .ok_or(SccmClientEvidenceAdmissionError::ExtraPayload)?; + validate_payload(payload)?; + + let profile = SccmExtractionProfile::for_version(fragment.configmgr_version.as_deref()); + if !is_registered_client_profile(&profile) { + return Err(SccmClientEvidenceAdmissionError::UnregisteredProfile); + } + let content = decode_payload(payload, fragment.encoding.as_deref())?; + let artifact = artifact_for_fragment(fragment); + let records = scan_logical_records(&content, &fragment.basename); + if records.is_empty() { + return Err(SccmClientEvidenceAdmissionError::MalformedCcm); + } + + for record in records { + let normalized = SccmRawEvidenceSnapshot::from_record(&artifact, record).export(); + if normalized.evidence_id != normalized.reference.entry_id + || normalized.reference.line_start.is_none() + || normalized.reference.line_end.is_none() + || normalized.timestamp.ordering_state != SccmTimeOrderingState::NormalizedUtc + || normalized.timestamp.offset_minutes.is_none() + || normalized.timestamp.utc_millis.is_none() + { + return Err(SccmClientEvidenceAdmissionError::InvalidTimestampProvenance); + } + let reference_identity = ( + normalized.reference.artifact_id.clone(), + normalized.reference.entry_id.clone(), + normalized.reference.line_start, + normalized.reference.line_end, + ); + if !evidence_ids.insert(normalized.evidence_id.clone()) + || !evidence_references.insert(reference_identity) + { + return Err(SccmClientEvidenceAdmissionError::CollidingEvidenceIdentity); + } + evidence.push(normalized); + } + profiles_by_artifact.insert(fragment.artifact_id.clone(), profile); + } + + evidence.sort_by(compare_evidence); + let integrity_seal = + compute_integrity_seal(&evidence, &source_coverage, &profiles_by_artifact)?; + Ok(SccmClientAdmittedEvidence { + evidence, + source_coverage, + profiles_by_artifact, + integrity_seal, + }) +} + +fn validate_payload( + payload: &SccmClientCapturedPayload, +) -> Result<(), SccmClientEvidenceAdmissionError> { + let length = u64::try_from(payload.bytes.len()) + .map_err(|_| SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch)?; + if length != payload.byte_length || !is_lowercase_sha256(&payload.expected_sha256) { + return Err(SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch); + } + (digest_hex(&payload.bytes) == payload.expected_sha256) + .then_some(()) + .ok_or(SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch) +} + +fn decode_payload( + payload: &SccmClientCapturedPayload, + encoding: Option<&str>, +) -> Result { + let encoding = match encoding { + Some("utf-8") => UTF_8, + Some("utf-16le") => UTF_16LE, + Some("utf-16be") => UTF_16BE, + Some("windows-1252") => WINDOWS_1252, + _ => return Err(SccmClientEvidenceAdmissionError::InvalidEncoding), + }; + let (decoded, _, had_errors) = encoding.decode(&payload.bytes); + (!had_errors) + .then_some(decoded.into_owned()) + .ok_or(SccmClientEvidenceAdmissionError::InvalidEncoding) +} + +fn artifact_for_fragment(fragment: &SccmClientIntakeFragment) -> SccmArtifact { + SccmArtifact { + artifact_id: fragment.artifact_id.clone(), + display_name: fragment.basename.clone(), + original_path: None, + host: None, + role: SccmRole::Client, + configmgr_version: fragment.configmgr_version.clone(), + collected_at_utc: fragment.collected_at_utc.clone(), + rotation: fragment.rotation.clone(), + coverage: fragment.coverage.clone(), + encoding: fragment.encoding.clone(), + } +} + +fn is_registered_client_profile(profile: &SccmExtractionProfile) -> bool { + profile.profile_id == SCCM_EXPERIMENTAL_KEY_PROFILE_ID + && profile.maturity == SccmExtractionProfileMaturity::Experimental + && profile.configmgr_version_prefixes == ["5.00.9128."] + && profile.validated_artifact_families.is_empty() + && profile + .selected_configmgr_version + .as_deref() + .is_some_and(|version| version.starts_with("5.00.9128.")) +} + +fn compare_evidence(left: &SccmEvidence, right: &SccmEvidence) -> std::cmp::Ordering { + ( + left.reference.artifact_id.as_str(), + left.reference.line_start, + left.reference.line_end, + left.reference.entry_id.as_str(), + left.evidence_id.as_str(), + ) + .cmp(&( + right.reference.artifact_id.as_str(), + right.reference.line_start, + right.reference.line_end, + right.reference.entry_id.as_str(), + right.evidence_id.as_str(), + )) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct IntegrityProjection<'a> { + evidence: &'a [SccmEvidence], + source_coverage: &'a BTreeMap, + profiles_by_artifact: &'a BTreeMap, +} + +fn compute_integrity_seal( + evidence: &[SccmEvidence], + source_coverage: &BTreeMap, + profiles_by_artifact: &BTreeMap, +) -> Result { + let bytes = serde_json::to_vec(&IntegrityProjection { + evidence, + source_coverage, + profiles_by_artifact, + }) + .map_err(|_| SccmClientEvidenceAdmissionError::IntegrityViolation)?; + Ok(digest_hex(&bytes)) +} + +fn digest_hex(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn is_lowercase_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) +} diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index 86f75cff1..5cdf3e4ea 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -1,9 +1,7 @@ use sha2::{Digest, Sha256}; -use super::{ - admit_client_evidence, assess_client_intake, SccmClientCapturedPayload, - SccmClientIntakeArtifact, SccmClientIntakeBundle, -}; +use super::admission::{admit_client_evidence, SccmClientCapturedPayload}; +use super::{assess_client_intake, SccmClientIntakeArtifact, SccmClientIntakeBundle}; use crate::sccm::{SccmArtifact, SccmCoverageState, SccmRole, SccmRotation}; fn digest(bytes: &[u8]) -> String { @@ -28,7 +26,7 @@ fn artifact(identity: &str, basename: &str) -> SccmClientIntakeArtifact { }; SccmClientIntakeArtifact { artifact: SccmArtifact { - artifact_id: format!("fixture-admitted-{identity}"), + artifact_id: format!("fixture-{identity}"), display_name: basename.to_owned(), original_path: None, host: None, @@ -39,7 +37,7 @@ fn artifact(identity: &str, basename: &str) -> SccmClientIntakeArtifact { coverage: SccmCoverageState::Captured, encoding: Some("utf-8".to_owned()), }, - path_fingerprint: Some(format!("synthetic-admitted-{identity}")), + path_fingerprint: Some(format!("synthetic-{identity}")), rotation_lineage: None, relative_path: Some(format!("evidence/{group}/current/{basename}")), fragment_complete: Some(true), @@ -47,7 +45,7 @@ fn artifact(identity: &str, basename: &str) -> SccmClientIntakeArtifact { } fn payload() -> SccmClientCapturedPayload { - payload_for("fixture-admitted-policy-agent", "+000") + payload_for("fixture-policy-agent", "+000") } fn payload_for(artifact_id: &str, offset: &str) -> SccmClientCapturedPayload { @@ -58,10 +56,10 @@ fn payload_for(artifact_id: &str, offset: &str) -> SccmClientCapturedPayload { .to_owned() + offset + concat!( - "\" date=\"7-30-2026\" ", - "component=\"Synthetic\" context=\"\" type=\"1\" ", - "thread=\"1\" file=\"synthetic.cc:1\">\n" - ); + "\" date=\"7-30-2026\" ", + "component=\"Synthetic\" context=\"\" type=\"1\" ", + "thread=\"1\" file=\"synthetic.cc:1\">\n" + ); let bytes = bytes.into_bytes(); SccmClientCapturedPayload { artifact_id: artifact_id.to_owned(), @@ -71,6 +69,31 @@ fn payload_for(artifact_id: &str, offset: &str) -> SccmClientCapturedPayload { } } +fn numbered_artifact(number: u32) -> SccmClientIntakeArtifact { + let artifact_id = format!("sccm-artifact:v1:sha256:{number:064x}"); + let basename = format!("PolicyAgent.log.{number}"); + SccmClientIntakeArtifact { + artifact: SccmArtifact { + artifact_id, + display_name: basename.clone(), + original_path: None, + host: None, + role: SccmRole::Client, + configmgr_version: Some("5.00.9128.1000".to_owned()), + collected_at_utc: Some("2026-07-30T00:00:00Z".to_owned()), + rotation: SccmRotation::Numbered(number), + coverage: SccmCoverageState::Captured, + encoding: Some("utf-8".to_owned()), + }, + path_fingerprint: Some(format!("sha256:{number:064x}")), + rotation_lineage: None, + relative_path: Some(format!( + "evidence/client-policy-agent/numbered-{number}/{basename}" + )), + fragment_complete: Some(true), + } +} + #[test] fn admission_seals_canonical_records_from_complete_captured_payloads() { let bundle = bundle(); @@ -79,32 +102,47 @@ fn admission_seals_canonical_records_from_complete_captured_payloads() { let admitted = admit_client_evidence(&bundle, &assessment, &[payload()]) .expect("a complete payload with the registered profile is admitted"); - assert_eq!(admitted.evidence().len(), 1); + assert_eq!(admitted.evidence().expect("valid seal").len(), 1); assert!(admitted.verify_integrity().is_ok()); assert_eq!( - admitted.source_coverage("client-policy-agent"), + admitted + .source_coverage("client-policy-agent") + .expect("valid seal"), Some(&SccmCoverageState::Captured) ); + assert!(admitted + .profile_for_artifact("fixture-policy-agent") + .expect("valid seal") + .is_some()); + assert!(admitted + .require_captured_source("client-policy-agent") + .is_ok()); + assert!(admitted + .require_captured_source("client-policy-state") + .is_err()); + assert!(admitted.require_captured_source("not-a-source").is_err()); } #[test] fn admission_rejects_missing_extra_duplicate_and_swapped_payloads() { let mut bundle = bundle(); - bundle.artifacts.push(artifact("policy-state", "CIAgent.log")); + bundle + .artifacts + .push(artifact("policy-state", "CIAgent.log")); let assessment = assess_client_intake(&bundle).expect("two payload fixture is canonical"); let agent = payload(); - let state = payload_for("fixture-admitted-policy-state", "+000"); + let state = payload_for("fixture-policy-state", "+000"); - assert!(admit_client_evidence(&bundle, &assessment, &[agent.clone()]).is_err()); + assert!(admit_client_evidence(&bundle, &assessment, std::slice::from_ref(&agent)).is_err()); let mut extra = vec![agent.clone(), state.clone()]; - extra.push(payload_for("fixture-not-in-bundle", "+000")); + extra.push(payload_for("fixture-unknown", "+000")); assert!(admit_client_evidence(&bundle, &assessment, &extra).is_err()); assert!(admit_client_evidence(&bundle, &assessment, &[agent.clone(), agent]).is_err()); let mut swapped = state; - swapped.artifact_id = "fixture-admitted-policy-agent".to_owned(); + swapped.artifact_id = "fixture-policy-agent".to_owned(); assert!(admit_client_evidence(&bundle, &assessment, &[payload(), swapped]).is_err()); } @@ -122,6 +160,28 @@ fn admission_rejects_payload_digest_and_length_mismatches() { assert!(admit_client_evidence(&bundle, &assessment, &[bad_length]).is_err()); } +#[test] +fn admission_accepts_the_exact_cap_and_rejects_payload_overflow_before_reassessment() { + let artifacts = (1..=super::MAX_SCCM_CLIENT_INTAKE_ARTIFACTS as u32) + .map(numbered_artifact) + .collect::>(); + let bundle = SccmClientIntakeBundle { + artifacts, + capture_gaps: Vec::new(), + }; + let assessment = assess_client_intake(&bundle).expect("exact artifact cap is canonical"); + let payloads = bundle + .artifacts + .iter() + .map(|artifact| payload_for(&artifact.artifact.artifact_id, "+000")) + .collect::>(); + assert!(admit_client_evidence(&bundle, &assessment, &payloads).is_ok()); + + let mut overflow = payloads; + overflow.push(payload_for("fixture-overflow", "+000")); + assert!(admit_client_evidence(&bundle, &assessment, &overflow).is_err()); +} + #[test] fn admission_rejects_noncaptured_incomplete_malformed_and_invalid_offset_payloads() { let mut capped = bundle(); @@ -136,8 +196,17 @@ fn admission_rejects_noncaptured_incomplete_malformed_and_invalid_offset_payload assess_client_intake(&incomplete).expect("incomplete boundary is explicit"); assert!(admit_client_evidence(&incomplete, &incomplete_assessment, &[payload()]).is_err()); + let mut unknown_profile = bundle(); + unknown_profile.artifacts[0].artifact.configmgr_version = Some("5.00.9999.1000".to_owned()); + let unknown_profile_assessment = + assess_client_intake(&unknown_profile).expect("unknown version remains canonical coverage"); + assert!( + admit_client_evidence(&unknown_profile, &unknown_profile_assessment, &[payload()],) + .is_err() + ); + let malformed = SccmClientCapturedPayload { - artifact_id: "fixture-admitted-policy-agent".to_owned(), + artifact_id: "fixture-policy-agent".to_owned(), bytes: b"not a CCM logical record".to_vec(), byte_length: 24, expected_sha256: digest(b"not a CCM logical record"), @@ -145,39 +214,52 @@ fn admission_rejects_noncaptured_incomplete_malformed_and_invalid_offset_payload let assessment = assess_client_intake(&bundle()).expect("fixture assessment is canonical"); assert!(admit_client_evidence(&bundle(), &assessment, &[malformed]).is_err()); - assert!(admit_client_evidence(&bundle(), &assessment, &[payload_for( - "fixture-admitted-policy-agent", - "+9999", - )]) + assert!(admit_client_evidence( + &bundle(), + &assessment, + &[payload_for("fixture-policy-agent", "+9999",)] + ) .is_err()); } #[test] fn admission_reassesses_bundle_and_is_deterministic_across_payload_order() { let mut bundle = bundle(); - bundle.artifacts.push(artifact("policy-state", "CIAgent.log")); + bundle + .artifacts + .push(artifact("policy-state", "CIAgent.log")); let canonical = assess_client_intake(&bundle).expect("canonical assessment"); let mut forged = canonical.clone(); - forged.groups[0].fragments.clear(); - assert!(admit_client_evidence(&bundle, &forged, &[payload(), payload_for( - "fixture-admitted-policy-state", - "+000", - )]) + forged + .groups + .iter_mut() + .find(|group| group.logical_artifact_id == "client-policy-agent") + .expect("fixture contains policy-agent group") + .fragments + .clear(); + assert!(admit_client_evidence( + &bundle, + &forged, + &[payload(), payload_for("fixture-policy-state", "+000",)] + ) .is_err()); let forward = admit_client_evidence( &bundle, &canonical, - &[payload(), payload_for("fixture-admitted-policy-state", "+000")], + &[payload(), payload_for("fixture-policy-state", "+000")], ) .expect("forward payload ordering is admitted"); let reverse = admit_client_evidence( &bundle, &canonical, - &[payload_for("fixture-admitted-policy-state", "+000"), payload()], + &[payload_for("fixture-policy-state", "+000"), payload()], ) .expect("reverse payload ordering is admitted"); - assert_eq!(forward.evidence(), reverse.evidence()); + assert_eq!( + forward.evidence().expect("forward seal"), + reverse.evidence().expect("reverse seal") + ); assert_eq!(forward.integrity_seal(), reverse.integrity_seal()); } @@ -190,11 +272,15 @@ fn admission_integrity_rejects_test_only_record_profile_and_identity_collisions( admit_client_evidence(&bundle, &assessment, &[payload()]).expect("admitted evidence"); record_mutation.test_only_mutate_first_message(); assert!(record_mutation.verify_integrity().is_err()); + assert!(record_mutation.evidence().is_err()); let mut profile_mutation = admit_client_evidence(&bundle, &assessment, &[payload()]).expect("admitted evidence"); profile_mutation.test_only_mutate_first_profile(); assert!(profile_mutation.verify_integrity().is_err()); + assert!(profile_mutation + .profile_for_artifact("fixture-policy-agent") + .is_err()); let mut collision = admit_client_evidence(&bundle, &assessment, &[payload()]).expect("admitted evidence"); diff --git a/crates/cmtraceopen-parser/src/sccm/client/mod.rs b/crates/cmtraceopen-parser/src/sccm/client/mod.rs index 5af403fb4..8af5acd94 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/mod.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/mod.rs @@ -3,6 +3,7 @@ //! This module accepts already-supplied metadata. Native discovery and capture //! remain outside `cmtraceopen-parser`. +pub(crate) mod admission; mod intake; #[cfg(test)] From 345422eafd6fe82aa0e0b997b5df9f7a315fa09b Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 11:45:44 -0400 Subject: [PATCH 03/27] test(sccm): expose client admission framing and cap gaps --- .../src/sccm/client/admission_tests.rs | 124 ++++++++++++++++-- 1 file changed, 113 insertions(+), 11 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index 5cdf3e4ea..ab10158a3 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -49,18 +49,48 @@ fn payload() -> SccmClientCapturedPayload { } fn payload_for(artifact_id: &str, offset: &str) -> SccmClientCapturedPayload { - let bytes = concat!( - "", - " String { + format!( + concat!( + "\n" + ), + message = message, + offset = offset, ) - .to_owned() - + offset - + concat!( - "\" date=\"7-30-2026\" ", - "component=\"Synthetic\" context=\"\" type=\"1\" ", - "thread=\"1\" file=\"synthetic.cc:1\">\n" - ); - let bytes = bytes.into_bytes(); +} + +fn refresh_payload_integrity(payload: &mut SccmClientCapturedPayload) { + payload.byte_length = payload.bytes.len() as u64; + payload.expected_sha256 = digest(&payload.bytes); +} + +fn payload_padded_to(artifact_id: &str, total_bytes: usize) -> SccmClientCapturedPayload { + let mut payload = payload_for(artifact_id, "+000"); + assert!(payload.bytes.len() <= total_bytes, "test payload remains valid"); + payload.bytes.resize(total_bytes, b' '); + refresh_payload_integrity(&mut payload); + payload +} + +fn payload_with_repeated_records( + artifact_id: &str, + record_count: usize, + message: &str, +) -> SccmClientCapturedPayload { + let bytes = ccm_record(message, "+000") + .repeat(record_count) + .into_bytes(); SccmClientCapturedPayload { artifact_id: artifact_id.to_owned(), byte_length: bytes.len() as u64, @@ -222,6 +252,78 @@ fn admission_rejects_noncaptured_incomplete_malformed_and_invalid_offset_payload .is_err()); } +#[test] +fn admission_rejects_unclosed_ccm_suffix_even_when_manifest_claims_complete() { + let bundle = bundle(); + let assessment = assess_client_intake(&bundle).expect("fixture assessment is canonical"); + let mut truncated = payload(); + truncated + .bytes + .extend_from_slice(b">(); + let error = admit_client_evidence(&aggregate_bundle, &aggregate_assessment, &aggregate_payloads) + .expect_err("aggregate parser work must be capped before hashing every payload"); + assert_eq!( + error.to_string(), + "client evidence admission aggregate payload bytes exceed the v1 cap" + ); + + let too_many_records = + payload_with_repeated_records("fixture-policy-agent", 4_097, "synthetic record"); + let error = admit_client_evidence(&bundle, &assessment, &[too_many_records]) + .expect_err("logical-record expansion must be capped before evidence retention"); + assert_eq!( + error.to_string(), + "client evidence admission logical record count exceeds the v1 cap" + ); + + let retained_message = "x".repeat(3_000); + let oversized_retained = payload_with_repeated_records( + "fixture-policy-agent", + 1_024, + &retained_message, + ); + let error = admit_client_evidence(&bundle, &assessment, &[oversized_retained]) + .expect_err("retained evidence and seal input must remain bounded"); + assert_eq!( + error.to_string(), + "client evidence admission retained evidence exceeds the v1 byte cap" + ); +} + #[test] fn admission_reassesses_bundle_and_is_deterministic_across_payload_order() { let mut bundle = bundle(); From 4c660f8d8bbd387e9fbbfa2a1c82b9c2141f16d1 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 11:55:53 -0400 Subject: [PATCH 04/27] fix(sccm): bound client evidence admission --- crates/cmtraceopen-parser/src/parser/ccm.rs | 146 ++++++++++---- .../src/sccm/client/admission.rs | 180 ++++++++++++++++-- .../src/sccm/client/admission_tests.rs | 20 +- 3 files changed, 293 insertions(+), 53 deletions(-) diff --git a/crates/cmtraceopen-parser/src/parser/ccm.rs b/crates/cmtraceopen-parser/src/parser/ccm.rs index ee9429b63..a2abbc176 100644 --- a/crates/cmtraceopen-parser/src/parser/ccm.rs +++ b/crates/cmtraceopen-parser/src/parser/ccm.rs @@ -344,7 +344,7 @@ pub fn parse_content( /// matched as a single logical record. Text between matched records is /// emitted as individual plain-text entries, preserving line numbers. fn parse_content_multiline(content: &str, file_path: &str) -> (Vec, u32) { - let scan = scan_ccm_content(content, file_path, CcmScanMode::PublicProjection); + let scan = scan_ccm_content(content, file_path, CcmScanMode::PublicProjection, None); ( scan.records .into_iter() @@ -355,13 +355,44 @@ fn parse_content_multiline(content: &str, file_path: &str) -> (Vec, u3 } pub(crate) fn scan_logical_records(content: &str, file_path: &str) -> Vec { - scan_ccm_content(content, file_path, CcmScanMode::SccmEvidence) + scan_ccm_content(content, file_path, CcmScanMode::SccmEvidence, None) .records .into_iter() .filter(|record| record.entry.format == LogFormat::Ccm) .collect() } +/// Bounded SCCM evidence framing projection. This preserves the raw CCM +/// grammar while exposing whether a complete claimed payload contained +/// unmatched or ambiguous input that cannot be promoted to evidence. +pub(crate) struct CcmLogicalRecordScan { + pub records: Vec, + pub complete: bool, + pub record_limit_exceeded: bool, +} + +pub(crate) fn scan_logical_records_bounded( + content: &str, + file_path: &str, + max_records: usize, +) -> CcmLogicalRecordScan { + let scan = scan_ccm_content( + content, + file_path, + CcmScanMode::SccmEvidence, + Some(max_records), + ); + CcmLogicalRecordScan { + records: scan + .records + .into_iter() + .filter(|record| record.entry.format == LogFormat::Ccm) + .collect(), + complete: scan.errors == 0 && !scan.record_limit_exceeded, + record_limit_exceeded: scan.record_limit_exceeded, + } +} + #[derive(Clone, Copy, PartialEq, Eq)] enum CcmScanMode { PublicProjection, @@ -371,13 +402,57 @@ enum CcmScanMode { struct CcmScan { records: Vec, errors: u32, + record_limit_exceeded: bool, +} + +struct CcmScanBuild { + records: Vec, + errors: u32, + id_counter: u64, + record_limit: Option, + record_limit_exceeded: bool, } -fn scan_ccm_content(content: &str, file_path: &str, mode: CcmScanMode) -> CcmScan { +impl CcmScanBuild { + fn new(record_limit: Option) -> Self { + Self { + records: Vec::new(), + errors: 0, + id_counter: 0, + record_limit, + record_limit_exceeded: false, + } + } + + fn record_limit_reached(&mut self) -> bool { + if self + .record_limit + .is_some_and(|limit| self.records.len() >= limit) + { + self.record_limit_exceeded = true; + true + } else { + false + } + } + + fn finish(self) -> CcmScan { + CcmScan { + records: self.records, + errors: self.errors, + record_limit_exceeded: self.record_limit_exceeded, + } + } +} + +fn scan_ccm_content( + content: &str, + file_path: &str, + mode: CcmScanMode, + record_limit: Option, +) -> CcmScan { let line_starts = build_line_starts(content); - let mut records = Vec::new(); - let mut errors = 0u32; - let mut id_counter = 0u64; + let mut build = CcmScanBuild::new(record_limit); let mut cursor = 0usize; let mut search_cursor = 0usize; let mut matched_any = false; @@ -404,9 +479,7 @@ fn scan_ccm_content(content: &str, file_path: &str, mode: CcmScanMode) -> CcmSca cursor, &line_starts, file_path, - &mut records, - &mut id_counter, - &mut errors, + &mut build, ); cursor = full_match.end(); search_cursor = full_match.end(); @@ -420,27 +493,29 @@ fn scan_ccm_content(content: &str, file_path: &str, mode: CcmScanMode) -> CcmSca cursor, &line_starts, file_path, - &mut records, - &mut id_counter, - &mut errors, + &mut build, ); let line_start = line_number_for_offset(&line_starts, full_match.start()); let line_end = line_number_for_offset(&line_starts, full_match.end().saturating_sub(1)); if let Some(parsed) = parse_captures(&caps) { if parsed.public_compatible || mode == CcmScanMode::SccmEvidence { - records - .push(parsed.into_logical_record(id_counter, line_start, line_end, file_path)); - id_counter += 1; + if !build.record_limit_reached() { + build.records.push(parsed.into_logical_record( + build.id_counter, + line_start, + line_end, + file_path, + )); + build.id_counter += 1; + } } else { push_unmatched_plain( full_match.as_str(), full_match.start(), &line_starts, file_path, - &mut records, - &mut id_counter, - &mut errors, + &mut build, ); } } else { @@ -449,9 +524,7 @@ fn scan_ccm_content(content: &str, file_path: &str, mode: CcmScanMode) -> CcmSca full_match.start(), &line_starts, file_path, - &mut records, - &mut id_counter, - &mut errors, + &mut build, ); } @@ -466,11 +539,13 @@ fn scan_ccm_content(content: &str, file_path: &str, mode: CcmScanMode) -> CcmSca cursor, &line_starts, file_path, - &mut records, - &mut id_counter, - &mut errors, + &mut build, ); + if !matched_any && record_limit.is_some() { + return build.finish(); + } + if !matched_any { let lines: Vec<&str> = content.lines().collect(); let (entries, errors) = parse_lines(&lines, file_path); @@ -487,10 +562,14 @@ fn scan_ccm_content(content: &str, file_path: &str, mode: CcmScanMode) -> CcmSca } }) .collect(); - return CcmScan { records, errors }; + return CcmScan { + records, + errors, + record_limit_exceeded: build.record_limit_exceeded, + }; } - CcmScan { records, errors } + build.finish() } fn newest_nested_opener( @@ -636,18 +715,19 @@ fn push_unmatched_plain( base_offset: usize, line_starts: &[usize], file_path: &str, - records: &mut Vec, - id_counter: &mut u64, - errors: &mut u32, + build: &mut CcmScanBuild, ) { let mut local_offset = 0usize; for piece in segment.split_inclusive('\n') { let line = piece.trim_end_matches(['\r', '\n']); let trimmed = line.trim(); if !trimmed.is_empty() { + if build.record_limit_reached() { + return; + } let line_number = line_number_for_offset(line_starts, base_offset + local_offset); let entry = LogEntry { - id: *id_counter, + id: build.id_counter, line_number, message: trimmed.to_string(), component: None, @@ -696,15 +776,15 @@ fn push_unmatched_plain( iteration: None, tags: None, }; - records.push(CcmLogicalRecord { + build.records.push(CcmLogicalRecord { entry, context: None, line_start: line_number, line_end: line_number, timestamp: CcmTimestampParse::missing(), }); - *id_counter += 1; - *errors += 1; + build.id_counter += 1; + build.errors += 1; } local_offset += piece.len(); } diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index 64c6c0aae..f5ce8fb3c 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -10,14 +10,17 @@ // weakening workspace-wide warning policy while preserving the review gate. #![allow(dead_code)] -use std::collections::{BTreeMap, BTreeSet}; +use std::{ + collections::{BTreeMap, BTreeSet}, + io::{self, Write}, +}; use encoding_rs::{UTF_16BE, UTF_16LE, UTF_8, WINDOWS_1252}; use serde::Serialize; use sha2::{Digest, Sha256}; use thiserror::Error; -use crate::parser::ccm::scan_logical_records; +use crate::parser::ccm::scan_logical_records_bounded; use crate::sccm::evidence::SccmRawEvidenceSnapshot; use crate::sccm::{ SccmArtifact, SccmCoverageState, SccmEvidence, SccmExtractionProfile, @@ -30,6 +33,19 @@ use super::{ SccmClientIntakeError, SccmClientIntakeFragment, MAX_SCCM_CLIENT_INTAKE_ARTIFACTS, }; +/// Maximum raw bytes decoded from one client payload at the parser admission +/// boundary. This is intentionally below the native physical-file cap: the +/// pure parser must remain safe for wasm and non-native callers too. +pub(crate) const MAX_SCCM_CLIENT_ADMISSION_PAYLOAD_BYTES: usize = 4 * 1024 * 1024; +/// Maximum raw bytes admitted across one client evidence bundle. +pub(crate) const MAX_SCCM_CLIENT_ADMISSION_TOTAL_PAYLOAD_BYTES: usize = 16 * 1024 * 1024; +/// Maximum logical CCM records retained from one admitted client bundle. +pub(crate) const MAX_SCCM_CLIENT_ADMISSION_LOGICAL_RECORDS: usize = 4_096; +/// Maximum projected evidence bytes retained for one admitted client bundle. +pub(crate) const MAX_SCCM_CLIENT_ADMISSION_RETAINED_EVIDENCE_BYTES: usize = 3 * 1024 * 1024; +/// Maximum bytes streamed into a deterministic client evidence integrity seal. +pub(crate) const MAX_SCCM_CLIENT_ADMISSION_SEAL_BYTES: usize = 4 * 1024 * 1024; + /// Raw, already-captured bytes offered to the one-shot client evidence /// admission boundary. This is an input only: the successful capability does /// not retain this vector or any decoded raw text. @@ -133,6 +149,10 @@ pub(crate) enum SccmClientEvidenceAdmissionError { AssessmentMutation, #[error("client evidence admission payload count exceeds the v1 bound")] PayloadLimitExceeded, + #[error("client evidence admission payload exceeds the v1 per-payload byte cap")] + PayloadByteLimitExceeded, + #[error("client evidence admission aggregate payload bytes exceed the v1 cap")] + AggregatePayloadByteLimitExceeded, #[error("client evidence admission contains duplicate payload artifact IDs")] DuplicatePayload, #[error("client evidence admission is missing a payload for a captured fragment")] @@ -147,6 +167,14 @@ pub(crate) enum SccmClientEvidenceAdmissionError { InvalidEncoding, #[error("client evidence admission payload has no complete CCM logical record")] MalformedCcm, + #[error("client evidence admission CCM framing is incomplete or ambiguous")] + IncompleteCcmFraming, + #[error("client evidence admission logical record count exceeds the v1 cap")] + LogicalRecordLimitExceeded, + #[error("client evidence admission retained evidence exceeds the v1 byte cap")] + RetainedEvidenceLimitExceeded, + #[error("client evidence admission integrity seal exceeds the v1 byte cap")] + IntegritySealLimitExceeded, #[error("client evidence admission record timestamp provenance is not comparable")] InvalidTimestampProvenance, #[error("client evidence admission selected an unregistered extraction profile")] @@ -171,6 +199,7 @@ pub(crate) fn admit_client_evidence( if payloads.len() > MAX_SCCM_CLIENT_INTAKE_ARTIFACTS { return Err(SccmClientEvidenceAdmissionError::PayloadLimitExceeded); } + validate_payload_budget(payloads)?; let canonical = assess_client_intake(bundle).map_err(SccmClientEvidenceAdmissionError::InvalidBundle)?; @@ -208,6 +237,7 @@ pub(crate) fn admit_client_evidence( let mut profiles_by_artifact = BTreeMap::new(); let mut evidence_ids = BTreeSet::new(); let mut evidence_references = BTreeSet::new(); + let mut retained_evidence_bytes = 0usize; for payload in ordered_payloads { if !seen_payload_ids.insert(payload.artifact_id.as_str()) { @@ -225,12 +255,22 @@ pub(crate) fn admit_client_evidence( } let content = decode_payload(payload, fragment.encoding.as_deref())?; let artifact = artifact_for_fragment(fragment); - let records = scan_logical_records(&content, &fragment.basename); - if records.is_empty() { + let scan = scan_logical_records_bounded( + &content, + &fragment.basename, + MAX_SCCM_CLIENT_ADMISSION_LOGICAL_RECORDS, + ); + if scan.record_limit_exceeded { + return Err(SccmClientEvidenceAdmissionError::LogicalRecordLimitExceeded); + } + if !scan.complete { + return Err(SccmClientEvidenceAdmissionError::IncompleteCcmFraming); + } + if scan.records.is_empty() { return Err(SccmClientEvidenceAdmissionError::MalformedCcm); } - for record in records { + for record in scan.records { let normalized = SccmRawEvidenceSnapshot::from_record(&artifact, record).export(); if normalized.evidence_id != normalized.reference.entry_id || normalized.reference.line_start.is_none() @@ -252,6 +292,12 @@ pub(crate) fn admit_client_evidence( { return Err(SccmClientEvidenceAdmissionError::CollidingEvidenceIdentity); } + retained_evidence_bytes = retained_evidence_bytes + .checked_add(retained_evidence_size(&normalized)?) + .ok_or(SccmClientEvidenceAdmissionError::RetainedEvidenceLimitExceeded)?; + if retained_evidence_bytes > MAX_SCCM_CLIENT_ADMISSION_RETAINED_EVIDENCE_BYTES { + return Err(SccmClientEvidenceAdmissionError::RetainedEvidenceLimitExceeded); + } evidence.push(normalized); } profiles_by_artifact.insert(fragment.artifact_id.clone(), profile); @@ -268,6 +314,24 @@ pub(crate) fn admit_client_evidence( }) } +fn validate_payload_budget( + payloads: &[SccmClientCapturedPayload], +) -> Result<(), SccmClientEvidenceAdmissionError> { + let mut total_payload_bytes = 0usize; + for payload in payloads { + if payload.bytes.len() > MAX_SCCM_CLIENT_ADMISSION_PAYLOAD_BYTES { + return Err(SccmClientEvidenceAdmissionError::PayloadByteLimitExceeded); + } + total_payload_bytes = total_payload_bytes + .checked_add(payload.bytes.len()) + .ok_or(SccmClientEvidenceAdmissionError::AggregatePayloadByteLimitExceeded)?; + if total_payload_bytes > MAX_SCCM_CLIENT_ADMISSION_TOTAL_PAYLOAD_BYTES { + return Err(SccmClientEvidenceAdmissionError::AggregatePayloadByteLimitExceeded); + } + } + Ok(()) +} + fn validate_payload( payload: &SccmClientCapturedPayload, ) -> Result<(), SccmClientEvidenceAdmissionError> { @@ -281,6 +345,42 @@ fn validate_payload( .ok_or(SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch) } +fn retained_evidence_size( + evidence: &SccmEvidence, +) -> Result { + let execution_context_bytes = match evidence.execution_context.as_ref() { + Some(handle) => handle + .scheme + .len() + .checked_add(handle.value.len()) + .ok_or(SccmClientEvidenceAdmissionError::RetainedEvidenceLimitExceeded)?, + None => 0, + }; + let mut total = 0usize; + for size in [ + evidence.evidence_id.len(), + evidence.reference.artifact_id.len(), + evidence.reference.entry_id.len(), + evidence.component.as_deref().map_or(0, str::len), + evidence.ccm_source_file.as_deref().map_or(0, str::len), + evidence.message.len(), + evidence + .timestamp + .original_display + .as_deref() + .map_or(0, str::len), + execution_context_bytes, + // Reserve fixed-field and container overhead so the retained-memory + // cap remains conservative without serializing a second copy. + 256, + ] { + total = total + .checked_add(size) + .ok_or(SccmClientEvidenceAdmissionError::RetainedEvidenceLimitExceeded)?; + } + Ok(total) +} + fn decode_payload( payload: &SccmClientCapturedPayload, encoding: Option<&str>, @@ -349,18 +449,74 @@ struct IntegrityProjection<'a> { profiles_by_artifact: &'a BTreeMap, } +struct BoundedIntegrityWriter { + hasher: Sha256, + byte_limit: usize, + bytes_written: usize, + limit_exceeded: bool, +} + +impl BoundedIntegrityWriter { + fn new(byte_limit: usize) -> Self { + Self { + hasher: Sha256::new(), + byte_limit, + bytes_written: 0, + limit_exceeded: false, + } + } + + fn finish(self) -> String { + self.hasher + .finalize() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() + } +} + +impl Write for BoundedIntegrityWriter { + fn write(&mut self, bytes: &[u8]) -> io::Result { + let Some(next_size) = self.bytes_written.checked_add(bytes.len()) else { + self.limit_exceeded = true; + return Err(io::Error::other("integrity seal byte limit exceeded")); + }; + if next_size > self.byte_limit { + self.limit_exceeded = true; + return Err(io::Error::other("integrity seal byte limit exceeded")); + } + self.hasher.update(bytes); + self.bytes_written = next_size; + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + fn compute_integrity_seal( evidence: &[SccmEvidence], source_coverage: &BTreeMap, profiles_by_artifact: &BTreeMap, ) -> Result { - let bytes = serde_json::to_vec(&IntegrityProjection { - evidence, - source_coverage, - profiles_by_artifact, - }) - .map_err(|_| SccmClientEvidenceAdmissionError::IntegrityViolation)?; - Ok(digest_hex(&bytes)) + let mut writer = BoundedIntegrityWriter::new(MAX_SCCM_CLIENT_ADMISSION_SEAL_BYTES); + let serialized = serde_json::to_writer( + &mut writer, + &IntegrityProjection { + evidence, + source_coverage, + profiles_by_artifact, + }, + ); + if serialized.is_err() { + return Err(if writer.limit_exceeded { + SccmClientEvidenceAdmissionError::IntegritySealLimitExceeded + } else { + SccmClientEvidenceAdmissionError::IntegrityViolation + }); + } + Ok(writer.finish()) } fn digest_hex(bytes: &[u8]) -> String { diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index ab10158a3..be2c5beec 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -77,7 +77,10 @@ fn refresh_payload_integrity(payload: &mut SccmClientCapturedPayload) { fn payload_padded_to(artifact_id: &str, total_bytes: usize) -> SccmClientCapturedPayload { let mut payload = payload_for(artifact_id, "+000"); - assert!(payload.bytes.len() <= total_bytes, "test payload remains valid"); + assert!( + payload.bytes.len() <= total_bytes, + "test payload remains valid" + ); payload.bytes.resize(total_bytes, b' '); refresh_payload_integrity(&mut payload); payload @@ -294,8 +297,12 @@ fn admission_rejects_oversized_payload_work_records_and_retained_evidence() { .iter() .map(|artifact| payload_padded_to(&artifact.artifact.artifact_id, 4 * 1024 * 1024)) .collect::>(); - let error = admit_client_evidence(&aggregate_bundle, &aggregate_assessment, &aggregate_payloads) - .expect_err("aggregate parser work must be capped before hashing every payload"); + let error = admit_client_evidence( + &aggregate_bundle, + &aggregate_assessment, + &aggregate_payloads, + ) + .expect_err("aggregate parser work must be capped before hashing every payload"); assert_eq!( error.to_string(), "client evidence admission aggregate payload bytes exceed the v1 cap" @@ -311,11 +318,8 @@ fn admission_rejects_oversized_payload_work_records_and_retained_evidence() { ); let retained_message = "x".repeat(3_000); - let oversized_retained = payload_with_repeated_records( - "fixture-policy-agent", - 1_024, - &retained_message, - ); + let oversized_retained = + payload_with_repeated_records("fixture-policy-agent", 1_024, &retained_message); let error = admit_client_evidence(&bundle, &assessment, &[oversized_retained]) .expect_err("retained evidence and seal input must remain bounded"); assert_eq!( From 30f85332ddd715360500a19b192467ea382455a4 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:05:46 -0400 Subject: [PATCH 05/27] test(sccm): expose aggregate client record cap bypass --- .../src/sccm/client/admission_tests.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index be2c5beec..77927038b 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -328,6 +328,32 @@ fn admission_rejects_oversized_payload_work_records_and_retained_evidence() { ); } +#[test] +fn admission_enforces_logical_record_cap_across_all_payloads() { + let bundle = SccmClientIntakeBundle { + artifacts: (1..=2).map(numbered_artifact).collect(), + capture_gaps: Vec::new(), + }; + let assessment = + assess_client_intake(&bundle).expect("two captured rotations are canonical intake"); + let payloads = bundle + .artifacts + .iter() + .map(|artifact| payload_with_repeated_records(&artifact.artifact.artifact_id, 2_049, "x")) + .collect::>(); + + let result = admit_client_evidence(&bundle, &assessment, &payloads); + assert!( + result.is_err(), + "two individually bounded rotations totaling 4,098 records were admitted" + ); + let error = result.expect_err("the logical-record cap must apply across the complete bundle"); + assert_eq!( + error.to_string(), + "client evidence admission logical record count exceeds the v1 cap" + ); +} + #[test] fn admission_reassesses_bundle_and_is_deterministic_across_payload_order() { let mut bundle = bundle(); From d7bb62987960fed3625d94cf2274a4959f0ae19e Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:06:26 -0400 Subject: [PATCH 06/27] fix(sccm): enforce aggregate client record cap --- crates/cmtraceopen-parser/src/sccm/client/admission.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index f5ce8fb3c..ca9d8eaa2 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -238,6 +238,7 @@ pub(crate) fn admit_client_evidence( let mut evidence_ids = BTreeSet::new(); let mut evidence_references = BTreeSet::new(); let mut retained_evidence_bytes = 0usize; + let mut remaining_logical_records = MAX_SCCM_CLIENT_ADMISSION_LOGICAL_RECORDS; for payload in ordered_payloads { if !seen_payload_ids.insert(payload.artifact_id.as_str()) { @@ -269,6 +270,9 @@ pub(crate) fn admit_client_evidence( if scan.records.is_empty() { return Err(SccmClientEvidenceAdmissionError::MalformedCcm); } + remaining_logical_records = remaining_logical_records + .checked_sub(scan.records.len()) + .ok_or(SccmClientEvidenceAdmissionError::LogicalRecordLimitExceeded)?; for record in scan.records { let normalized = SccmRawEvidenceSnapshot::from_record(&artifact, record).export(); From dc07cf9759b2a065282bd75f28506f03107a5c05 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:12:59 -0400 Subject: [PATCH 07/27] test(sccm): expose client evidence authority gaps --- .../sccm/client/authority_contract_tests.rs | 402 ++++++++++++++++++ .../cmtraceopen-parser/src/sccm/client/mod.rs | 2 + .../tests/sccm_client_admission_authority.rs | 75 ++++ 3 files changed, 479 insertions(+) create mode 100644 crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs create mode 100644 crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs diff --git a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs new file mode 100644 index 000000000..8718d93d9 --- /dev/null +++ b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs @@ -0,0 +1,402 @@ +use sha2::{Digest, Sha256}; + +use super::admission::{ + admit_client_evidence, SccmClientAdmittedEvidence, SccmClientCapturedPayload, + SccmClientEvidenceAdmissionError, +}; +use super::{assess_client_intake, SccmClientIntakeArtifact, SccmClientIntakeBundle}; +use crate::sccm::{SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmRole, SccmRotation}; + +fn digest(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn ccm_bytes(message: &str) -> Vec { + format!( + concat!( + "\n" + ), + message = message, + ) + .into_bytes() +} + +fn source_group(basename: &str) -> &'static str { + match basename { + "PolicyAgent.log" => "client-policy-agent", + "CAS.log" => "client-content", + "AppIntentEval.log" => "client-app-intent", + "CustomVendorHook.log" => "unknown", + _ => panic!("authority fixture basename must be declared here"), + } +} + +fn artifact( + identity: &str, + basename: &str, + coverage: SccmCoverageState, + fragment_complete: bool, + binding: Option<&[u8]>, +) -> SccmClientIntakeArtifact { + let physical = matches!( + coverage, + SccmCoverageState::Captured | SccmCoverageState::Capped | SccmCoverageState::ParseFailed + ); + let (declared_byte_length, content_sha256) = binding + .map(|bytes| (Some(bytes.len() as u64), Some(digest(bytes)))) + .unwrap_or((None, None)); + let group = source_group(basename); + let relative_path = (physical && group != "unknown") + .then(|| format!("evidence/{group}/current/{basename}")) + .or_else(|| { + (physical && group == "unknown").then(|| format!("evidence/{group}/{basename}")) + }); + + SccmClientIntakeArtifact { + artifact: SccmArtifact { + artifact_id: format!("fixture-{identity}"), + display_name: basename.to_owned(), + original_path: None, + host: None, + role: SccmRole::Client, + configmgr_version: Some("5.00.9128.1000".to_owned()), + collected_at_utc: Some("2026-07-30T00:00:00Z".to_owned()), + rotation: SccmRotation::Current, + coverage, + encoding: Some("utf-8".to_owned()), + }, + path_fingerprint: physical.then(|| format!("synthetic-{identity}")), + rotation_lineage: None, + relative_path, + fragment_complete: Some(fragment_complete), + declared_byte_length, + content_sha256, + } +} + +fn bundle_with(artifacts: Vec) -> SccmClientIntakeBundle { + SccmClientIntakeBundle { + artifacts, + capture_gaps: Vec::new(), + } +} + +fn payload(artifact_id: &str, bytes: Vec) -> SccmClientCapturedPayload { + match SccmClientCapturedPayload::new(artifact_id.to_owned(), bytes) { + Ok(payload) => payload, + Err(error) => panic!("authority fixture payload must be valid: {error}"), + } +} + +fn admission_error( + result: Result, + context: &str, +) -> SccmClientEvidenceAdmissionError { + match result { + Ok(_) => panic!("{context}"), + Err(error) => error, + } +} + +#[test] +fn admission_rejects_substituted_valid_ccm_bytes_against_the_intake_binding() { + let bound = ccm_bytes("bound policy evidence"); + let substituted = ccm_bytes("different but valid policy evidence"); + let bundle = bundle_with(vec![artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&bound), + )]); + let assessment = assess_client_intake(&bundle).expect("bound intake is canonical"); + + let error = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-policy", substituted)], + ), + "valid substituted bytes must not inherit the intake artifact's authority", + ); + assert_eq!( + error, + SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch + ); +} + +#[test] +fn intake_content_binding_is_paired_lowercase_and_capture_local() { + let bytes = ccm_bytes("bound policy evidence"); + let valid = artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&bytes), + ); + let assessment = assess_client_intake(&bundle_with(vec![valid.clone()])) + .expect("a recognized complete capture may carry a content binding"); + let projected = &assessment.physical_artifacts[0]; + assert_eq!(projected.declared_byte_length, Some(bytes.len() as u64)); + assert_eq!( + projected.content_sha256.as_deref(), + Some(digest(&bytes).as_str()) + ); + + let mut missing_digest = valid.clone(); + missing_digest.content_sha256 = None; + assert_eq!( + assess_client_intake(&bundle_with(vec![missing_digest])), + Err(super::SccmClientIntakeError::InvalidContentBinding) + ); + + let mut missing_length = valid.clone(); + missing_length.declared_byte_length = None; + assert_eq!( + assess_client_intake(&bundle_with(vec![missing_length])), + Err(super::SccmClientIntakeError::InvalidContentBinding) + ); + + let mut uppercase_digest = valid.clone(); + uppercase_digest.content_sha256 = uppercase_digest + .content_sha256 + .map(|value| value.to_uppercase()); + assert_eq!( + assess_client_intake(&bundle_with(vec![uppercase_digest])), + Err(super::SccmClientIntakeError::InvalidContentBinding) + ); + + for mut inadmissible in [ + artifact( + "denied", + "PolicyAgent.log", + SccmCoverageState::AccessDenied, + false, + Some(&bytes), + ), + artifact( + "capped", + "CAS.log", + SccmCoverageState::Capped, + false, + Some(&bytes), + ), + artifact( + "incomplete", + "AppIntentEval.log", + SccmCoverageState::Captured, + false, + Some(&bytes), + ), + artifact( + "custom", + "CustomVendorHook.log", + SccmCoverageState::Captured, + true, + Some(&bytes), + ), + ] { + assert_eq!( + assess_client_intake(&bundle_with(vec![inadmissible.clone()])), + Err(super::SccmClientIntakeError::InvalidContentBinding), + "content authority must be absent outside a recognized complete capture" + ); + inadmissible.declared_byte_length = None; + inadmissible.content_sha256 = None; + assess_client_intake(&bundle_with(vec![inadmissible])) + .expect("the same coverage remains representable without content authority"); + } +} + +#[test] +fn legacy_intake_remains_assessable_but_cannot_admit_bytes() { + let bytes = ccm_bytes("legacy policy evidence"); + let legacy = artifact( + "legacy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + None, + ); + let bundle = bundle_with(vec![legacy]); + let assessment = assess_client_intake(&bundle).expect("legacy intake remains assessment-only"); + let wire = serde_json::to_value(&bundle).expect("legacy intake remains serializable"); + assert!(wire["artifacts"][0].get("declaredByteLength").is_none()); + assert!(wire["artifacts"][0].get("contentSha256").is_none()); + + let error = admission_error( + admit_client_evidence(&bundle, &assessment, &[payload("fixture-legacy", bytes)]), + "legacy intake must not authorize caller-supplied bytes", + ); + assert_eq!( + error, + SccmClientEvidenceAdmissionError::MissingContentBinding + ); +} + +#[test] +fn admission_rejects_swapped_duplicate_extra_and_missing_payloads() { + let policy_bytes = ccm_bytes("policy evidence"); + let content_bytes = ccm_bytes("content evidence"); + let bundle = bundle_with(vec![ + artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&policy_bytes), + ), + artifact( + "content", + "CAS.log", + SccmCoverageState::Captured, + true, + Some(&content_bytes), + ), + ]); + let assessment = assess_client_intake(&bundle).expect("two bound sources are canonical"); + + let swapped = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[ + payload("fixture-policy", content_bytes.clone()), + payload("fixture-content", policy_bytes.clone()), + ], + ), + "swapped valid payloads must not be admitted", + ); + assert_eq!( + swapped, + SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch + ); + + let duplicate = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[ + payload("fixture-policy", policy_bytes.clone()), + payload("fixture-policy", policy_bytes.clone()), + ], + ), + "duplicate payload identities must fail closed", + ); + assert_eq!( + duplicate, + SccmClientEvidenceAdmissionError::DuplicatePayload + ); + + let extra = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[ + payload("fixture-policy", policy_bytes.clone()), + payload("fixture-extra", ccm_bytes("extra evidence")), + ], + ), + "an extra syntactically valid payload identity must fail closed", + ); + assert_eq!(extra, SccmClientEvidenceAdmissionError::ExtraPayload); + + let missing = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-policy", policy_bytes)], + ), + "a missing bound payload must fail closed", + ); + assert_eq!(missing, SccmClientEvidenceAdmissionError::MissingPayload); +} + +#[test] +fn incomplete_and_capped_sources_fail_locally_without_blocking_bound_policy() { + let policy_bytes = ccm_bytes("policy evidence"); + let bundle = bundle_with(vec![ + artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&policy_bytes), + ), + artifact( + "content-capped", + "CAS.log", + SccmCoverageState::Capped, + false, + None, + ), + artifact( + "intent-incomplete", + "AppIntentEval.log", + SccmCoverageState::Captured, + false, + None, + ), + ]); + let assessment = assess_client_intake(&bundle).expect("mixed source coverage is canonical"); + let admitted = match admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-policy", policy_bytes)], + ) { + Ok(admitted) => admitted, + Err(error) => panic!("unrelated source gaps must not block policy admission: {error}"), + }; + + assert!(admitted + .require_captured_source("client-policy-agent") + .is_ok()); + assert!(admitted.require_captured_source("client-content").is_err()); + assert!(admitted + .require_captured_source("client-app-intent") + .is_err()); + assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); +} + +#[test] +fn admitted_profile_is_bound_to_the_catalogued_source_family() { + let bytes = ccm_bytes("policy evidence"); + let bundle = bundle_with(vec![artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&bytes), + )]); + let assessment = assess_client_intake(&bundle).expect("bound policy intake is canonical"); + let admitted = + match admit_client_evidence(&bundle, &assessment, &[payload("fixture-policy", bytes)]) { + Ok(admitted) => admitted, + Err(error) => panic!("bound policy evidence must be admitted: {error}"), + }; + + let profile = admitted + .profile_for_artifact("fixture-policy") + .expect("valid authority seal") + .expect("admitted artifact has a profile"); + assert_eq!( + profile.validated_artifact_families, + [SccmArtifactFamily::ClientPolicy] + ); +} + +#[test] +fn captured_payload_constructor_rejects_noncanonical_identity() { + let result = SccmClientCapturedPayload::new("C:\\Users\\raw\\PolicyAgent.log", ccm_bytes("x")); + match result { + Err(SccmClientEvidenceAdmissionError::InvalidPayloadArtifactId) => {} + Err(error) => panic!("unexpected payload constructor error: {error}"), + Ok(_) => panic!("raw path identity must not enter the payload boundary"), + } +} diff --git a/crates/cmtraceopen-parser/src/sccm/client/mod.rs b/crates/cmtraceopen-parser/src/sccm/client/mod.rs index 8af5acd94..b2df4278e 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/mod.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/mod.rs @@ -8,5 +8,7 @@ mod intake; #[cfg(test)] mod admission_tests; +#[cfg(test)] +mod authority_contract_tests; pub use intake::*; diff --git a/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs b/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs new file mode 100644 index 000000000..124ac0277 --- /dev/null +++ b/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs @@ -0,0 +1,75 @@ +use cmtraceopen_parser::sccm::client::{ + admit_client_evidence, assess_client_intake, SccmClientCapturedPayload, + SccmClientIntakeArtifact, SccmClientIntakeBundle, +}; +use cmtraceopen_parser::sccm::{SccmArtifact, SccmCoverageState, SccmRole, SccmRotation}; +use sha2::{Digest, Sha256}; + +fn digest(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn bytes() -> Vec { + concat!( + "\n" + ) + .as_bytes() + .to_vec() +} + +#[test] +fn public_bytes_only_facade_uses_intake_bound_content_authority() { + let bytes = bytes(); + let bundle = SccmClientIntakeBundle { + artifacts: vec![SccmClientIntakeArtifact { + artifact: SccmArtifact { + artifact_id: "fixture-public-authority".to_owned(), + display_name: "PolicyAgent.log".to_owned(), + original_path: None, + host: None, + role: SccmRole::Client, + configmgr_version: Some("5.00.9128.1000".to_owned()), + collected_at_utc: Some("2026-07-30T00:00:00Z".to_owned()), + rotation: SccmRotation::Current, + coverage: SccmCoverageState::Captured, + encoding: Some("utf-8".to_owned()), + }, + path_fingerprint: Some("synthetic-public-authority".to_owned()), + rotation_lineage: None, + relative_path: Some("evidence/client-policy-agent/current/PolicyAgent.log".to_owned()), + fragment_complete: Some(true), + declared_byte_length: Some(bytes.len() as u64), + content_sha256: Some(digest(&bytes)), + }], + capture_gaps: Vec::new(), + }; + let assessment = assess_client_intake(&bundle).expect("public intake is canonical"); + let payload = match SccmClientCapturedPayload::new("fixture-public-authority", bytes) { + Ok(payload) => payload, + Err(error) => panic!("public payload constructor rejected canonical bytes: {error}"), + }; + + assert!( + admit_client_evidence(&bundle, &assessment, &[payload]).is_ok(), + "public callers can obtain opaque authority only for intake-bound bytes" + ); + + let wire = serde_json::to_value(&bundle).expect("bound intake serializes"); + assert!(wire["artifacts"][0]["declaredByteLength"].is_u64()); + assert_eq!( + wire["artifacts"][0]["contentSha256"].as_str().map(str::len), + Some(64) + ); + let round_trip: SccmClientIntakeBundle = + serde_json::from_value(wire).expect("bound intake round trips"); + let projected = assess_client_intake(&round_trip).expect("round trip remains canonical"); + assert_eq!( + projected.physical_artifacts[0].declared_byte_length, + Some(round_trip.artifacts[0].declared_byte_length.unwrap()) + ); +} From 32ee8f6f37c3dbef86f1ffa95e903092775d9bed Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:25:30 -0400 Subject: [PATCH 08/27] fix(sccm): bind client evidence to intake authority --- .../src/sccm/client/admission.rs | 179 +++++++--- .../src/sccm/client/admission_tests.rs | 309 ++++++++++++------ .../sccm/client/authority_contract_tests.rs | 41 ++- .../src/sccm/client/intake.rs | 56 +++- .../cmtraceopen-parser/src/sccm/client/mod.rs | 4 + .../tests/sccm_client_admission_authority.rs | 6 +- .../tests/sccm_client_intake.rs | 8 + .../preparation/issue-319-client-intake.md | 21 +- 8 files changed, 462 insertions(+), 162 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index ca9d8eaa2..5e96678e8 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -1,13 +1,13 @@ -//! Crate-private sealing of canonical SCCM client evidence. +//! Sealing of canonical SCCM client evidence behind an opaque capability. //! //! Raw payload bytes exist only while this constructor verifies their digest //! and normalizes their CCM logical records. Reducers receive the resulting //! immutable-by-API capability, never bytes or caller-supplied evidence. -// This is deliberately a crate-private shared interface that lands before -// workflow reducers. No production reducer owns it in this slice, so rustc -// cannot yet observe a call site; retaining the lint allowance here avoids -// weakening workspace-wide warning policy while preserving the review gate. +// The public facade lands before workflow reducers, while its capability +// accessors deliberately remain crate-private. No production reducer owns +// them in this slice, so rustc cannot yet observe those call sites; retaining +// the local lint allowance avoids weakening workspace-wide warning policy. #![allow(dead_code)] use std::{ @@ -21,16 +21,18 @@ use sha2::{Digest, Sha256}; use thiserror::Error; use crate::parser::ccm::scan_logical_records_bounded; +use crate::sccm::catalog::classify_artifact_name; use crate::sccm::evidence::SccmRawEvidenceSnapshot; use crate::sccm::{ - SccmArtifact, SccmCoverageState, SccmEvidence, SccmExtractionProfile, + SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmEvidence, SccmExtractionProfile, SccmExtractionProfileMaturity, SccmRole, SccmTimeOrderingState, SCCM_EXPERIMENTAL_KEY_PROFILE_ID, }; use super::{ - assess_client_intake, SccmClientIntakeAssessment, SccmClientIntakeBundle, - SccmClientIntakeError, SccmClientIntakeFragment, MAX_SCCM_CLIENT_INTAKE_ARTIFACTS, + assess_client_intake, intake::is_safe_artifact_id, SccmClientIntakeAssessment, + SccmClientIntakeBundle, SccmClientIntakeError, SccmClientIntakeFragment, + MAX_SCCM_CLIENT_INTAKE_ARTIFACTS, }; /// Maximum raw bytes decoded from one client payload at the parser admission @@ -49,12 +51,28 @@ pub(crate) const MAX_SCCM_CLIENT_ADMISSION_SEAL_BYTES: usize = 4 * 1024 * 1024; /// Raw, already-captured bytes offered to the one-shot client evidence /// admission boundary. This is an input only: the successful capability does /// not retain this vector or any decoded raw text. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct SccmClientCapturedPayload { - pub artifact_id: String, - pub bytes: Vec, - pub byte_length: u64, - pub expected_sha256: String, +pub struct SccmClientCapturedPayload { + artifact_id: String, + bytes: Vec, +} + +impl SccmClientCapturedPayload { + /// Constructs one bytes-only admission input. Artifact identity is only a + /// routing handle; byte length and digest authority remain exclusively on + /// the canonical intake fragment. + pub fn new( + artifact_id: impl Into, + bytes: Vec, + ) -> Result { + let artifact_id = artifact_id.into(); + if !is_safe_artifact_id(&artifact_id) { + return Err(SccmClientEvidenceAdmissionError::InvalidPayloadArtifactId); + } + if bytes.len() > MAX_SCCM_CLIENT_ADMISSION_PAYLOAD_BYTES { + return Err(SccmClientEvidenceAdmissionError::PayloadByteLimitExceeded); + } + Ok(Self { artifact_id, bytes }) + } } /// The bounded evidence authority internal client reducers consume. @@ -62,10 +80,10 @@ pub(crate) struct SccmClientCapturedPayload { /// Its fields stay private and it deliberately implements neither serde nor a /// public constructor. `verify_integrity` recomputes the deterministic seal so /// a reducer can fail closed if future crate-internal code corrupts it. -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct SccmClientAdmittedEvidence { +pub struct SccmClientAdmittedEvidence { evidence: Vec, source_coverage: BTreeMap, + admitted_source_groups: BTreeSet, profiles_by_artifact: BTreeMap, integrity_seal: String, } @@ -100,6 +118,7 @@ impl SccmClientAdmittedEvidence { let recomputed = compute_integrity_seal( &self.evidence, &self.source_coverage, + &self.admitted_source_groups, &self.profiles_by_artifact, )?; (recomputed == self.integrity_seal) @@ -114,7 +133,11 @@ impl SccmClientAdmittedEvidence { logical_artifact_id: &str, ) -> Result<(), SccmClientEvidenceAdmissionError> { match self.source_coverage(logical_artifact_id)? { - Some(SccmCoverageState::Captured) => Ok(()), + Some(SccmCoverageState::Captured) + if self.admitted_source_groups.contains(logical_artifact_id) => + { + Ok(()) + } Some(_) => Err(SccmClientEvidenceAdmissionError::SourceCoverageUnavailable), None => Err(SccmClientEvidenceAdmissionError::UnknownSourceGroup), } @@ -142,7 +165,7 @@ impl SccmClientAdmittedEvidence { } #[derive(Debug, Clone, PartialEq, Eq, Error)] -pub(crate) enum SccmClientEvidenceAdmissionError { +pub enum SccmClientEvidenceAdmissionError { #[error("client evidence admission bundle is invalid: {0}")] InvalidBundle(SccmClientIntakeError), #[error("client evidence admission assessment is not the canonical bundle projection")] @@ -153,16 +176,18 @@ pub(crate) enum SccmClientEvidenceAdmissionError { PayloadByteLimitExceeded, #[error("client evidence admission aggregate payload bytes exceed the v1 cap")] AggregatePayloadByteLimitExceeded, + #[error("client evidence admission payload artifact ID is invalid")] + InvalidPayloadArtifactId, #[error("client evidence admission contains duplicate payload artifact IDs")] DuplicatePayload, #[error("client evidence admission is missing a payload for a captured fragment")] MissingPayload, #[error("client evidence admission payload has no matching canonical captured fragment")] ExtraPayload, - #[error("client evidence admission payload targets a noncaptured or incomplete fragment")] - NonAdmissibleFragment, #[error("client evidence admission payload length or digest is invalid")] PayloadIntegrityMismatch, + #[error("client evidence admission captured fragment has no intake-bound length and digest")] + MissingContentBinding, #[error("client evidence admission cannot decode the declared artifact encoding")] InvalidEncoding, #[error("client evidence admission payload has no complete CCM logical record")] @@ -191,7 +216,7 @@ pub(crate) enum SccmClientEvidenceAdmissionError { /// Reassesses a canonical client bundle and seals the logical CCM evidence it /// derives from each exact complete captured payload. -pub(crate) fn admit_client_evidence( +pub fn admit_client_evidence( bundle: &SccmClientIntakeBundle, assessment: &SccmClientIntakeAssessment, payloads: &[SccmClientCapturedPayload], @@ -212,19 +237,40 @@ pub(crate) fn admit_client_evidence( .iter() .map(|group| (group.logical_artifact_id.clone(), group.coverage.clone())) .collect::>(); - let eligible = canonical - .physical_artifacts + let mut eligible = BTreeMap::new(); + let mut unbound_complete_captures = BTreeSet::new(); + for fragment in &canonical.physical_artifacts { + if fragment.coverage != SccmCoverageState::Captured + || fragment.fragment_complete != Some(true) + { + continue; + } + if fragment.declared_byte_length.is_none() || fragment.content_sha256.is_none() { + unbound_complete_captures.insert(fragment.artifact_id.as_str()); + continue; + } + eligible.insert(fragment.artifact_id.clone(), fragment); + } + let admitted_source_groups = canonical + .groups .iter() - .map(|fragment| { - (fragment.coverage == SccmCoverageState::Captured - && fragment.fragment_complete == Some(true)) - .then_some((fragment.artifact_id.clone(), fragment)) - .ok_or(SccmClientEvidenceAdmissionError::NonAdmissibleFragment) + .filter(|group| { + !group.fragments.is_empty() + && group.coverage == SccmCoverageState::Captured + && group.fragments.iter().all(|fragment| { + fragment.coverage == SccmCoverageState::Captured + && fragment.fragment_complete == Some(true) + && fragment.declared_byte_length.is_some() + && fragment.content_sha256.is_some() + }) }) - .collect::, _>>()?; - - if eligible.len() != canonical.physical_artifacts.len() { - return Err(SccmClientEvidenceAdmissionError::NonAdmissibleFragment); + .map(|group| group.logical_artifact_id.clone()) + .collect::>(); + if payloads + .iter() + .any(|payload| unbound_complete_captures.contains(payload.artifact_id.as_str())) + { + return Err(SccmClientEvidenceAdmissionError::MissingContentBinding); } if payloads.len() != eligible.len() { return Err(SccmClientEvidenceAdmissionError::MissingPayload); @@ -248,10 +294,13 @@ pub(crate) fn admit_client_evidence( .get(&payload.artifact_id) .copied() .ok_or(SccmClientEvidenceAdmissionError::ExtraPayload)?; - validate_payload(payload)?; + validate_payload(payload, fragment)?; - let profile = SccmExtractionProfile::for_version(fragment.configmgr_version.as_deref()); - if !is_registered_client_profile(&profile) { + let classified = classify_artifact_name(&fragment.basename, SccmRole::Client); + let family = classified.family; + let mut profile = SccmExtractionProfile::for_version(fragment.configmgr_version.as_deref()); + profile.validated_artifact_families = vec![family.clone()]; + if !classified.supported_for_diagnosis || !is_registered_client_profile(&profile, &family) { return Err(SccmClientEvidenceAdmissionError::UnregisteredProfile); } let content = decode_payload(payload, fragment.encoding.as_deref())?; @@ -308,11 +357,16 @@ pub(crate) fn admit_client_evidence( } evidence.sort_by(compare_evidence); - let integrity_seal = - compute_integrity_seal(&evidence, &source_coverage, &profiles_by_artifact)?; + let integrity_seal = compute_integrity_seal( + &evidence, + &source_coverage, + &admitted_source_groups, + &profiles_by_artifact, + )?; Ok(SccmClientAdmittedEvidence { evidence, source_coverage, + admitted_source_groups, profiles_by_artifact, integrity_seal, }) @@ -338,13 +392,21 @@ fn validate_payload_budget( fn validate_payload( payload: &SccmClientCapturedPayload, + fragment: &SccmClientIntakeFragment, ) -> Result<(), SccmClientEvidenceAdmissionError> { let length = u64::try_from(payload.bytes.len()) .map_err(|_| SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch)?; - if length != payload.byte_length || !is_lowercase_sha256(&payload.expected_sha256) { + let declared_length = fragment + .declared_byte_length + .ok_or(SccmClientEvidenceAdmissionError::MissingContentBinding)?; + let declared_digest = fragment + .content_sha256 + .as_deref() + .ok_or(SccmClientEvidenceAdmissionError::MissingContentBinding)?; + if length != declared_length || !is_lowercase_sha256(declared_digest) { return Err(SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch); } - (digest_hex(&payload.bytes) == payload.expected_sha256) + (digest_hex(&payload.bytes) == declared_digest) .then_some(()) .ok_or(SccmClientEvidenceAdmissionError::PayloadIntegrityMismatch) } @@ -417,11 +479,15 @@ fn artifact_for_fragment(fragment: &SccmClientIntakeFragment) -> SccmArtifact { } } -fn is_registered_client_profile(profile: &SccmExtractionProfile) -> bool { +fn is_registered_client_profile( + profile: &SccmExtractionProfile, + family: &SccmArtifactFamily, +) -> bool { profile.profile_id == SCCM_EXPERIMENTAL_KEY_PROFILE_ID && profile.maturity == SccmExtractionProfileMaturity::Experimental && profile.configmgr_version_prefixes == ["5.00.9128."] - && profile.validated_artifact_families.is_empty() + && profile.validated_artifact_families.first() == Some(family) + && profile.validated_artifact_families.len() == 1 && profile .selected_configmgr_version .as_deref() @@ -450,7 +516,9 @@ fn compare_evidence(left: &SccmEvidence, right: &SccmEvidence) -> std::cmp::Orde struct IntegrityProjection<'a> { evidence: &'a [SccmEvidence], source_coverage: &'a BTreeMap, - profiles_by_artifact: &'a BTreeMap, + admitted_source_groups: &'a BTreeSet, + profile_assignments: &'a BTreeMap<&'a str, usize>, + profiles: &'a [&'a SccmExtractionProfile], } struct BoundedIntegrityWriter { @@ -502,15 +570,40 @@ impl Write for BoundedIntegrityWriter { fn compute_integrity_seal( evidence: &[SccmEvidence], source_coverage: &BTreeMap, + admitted_source_groups: &BTreeSet, profiles_by_artifact: &BTreeMap, ) -> Result { + // Many rotations legitimately select the same profile. Seal the complete + // profile once and bind each artifact to its deterministic index so the + // intake artifact ceiling does not multiply identical profile metadata + // past the independent seal cap. + let mut profile_indices = BTreeMap::::new(); + let mut unique_profiles = Vec::new(); + let mut profile_assignments = BTreeMap::new(); + for (artifact_id, profile) in profiles_by_artifact { + let canonical_profile = serde_json::to_string(profile) + .map_err(|_| SccmClientEvidenceAdmissionError::IntegrityViolation)?; + let profile_index = match profile_indices.get(&canonical_profile) { + Some(index) => *index, + None => { + let index = unique_profiles.len(); + profile_indices.insert(canonical_profile, index); + unique_profiles.push(profile); + index + } + }; + profile_assignments.insert(artifact_id.as_str(), profile_index); + } + let mut writer = BoundedIntegrityWriter::new(MAX_SCCM_CLIENT_ADMISSION_SEAL_BYTES); let serialized = serde_json::to_writer( &mut writer, &IntegrityProjection { evidence, source_coverage, - profiles_by_artifact, + admitted_source_groups, + profile_assignments: &profile_assignments, + profiles: &unique_profiles, }, ); if serialized.is_err() { diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index 77927038b..a42269835 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -1,6 +1,9 @@ use sha2::{Digest, Sha256}; -use super::admission::{admit_client_evidence, SccmClientCapturedPayload}; +use super::admission::{ + admit_client_evidence, SccmClientAdmittedEvidence, SccmClientCapturedPayload, + SccmClientEvidenceAdmissionError, +}; use super::{assess_client_intake, SccmClientIntakeArtifact, SccmClientIntakeBundle}; use crate::sccm::{SccmArtifact, SccmCoverageState, SccmRole, SccmRotation}; @@ -18,7 +21,19 @@ fn bundle() -> SccmClientIntakeBundle { } } +fn bundle_with_bound_policy(bytes: &[u8]) -> SccmClientIntakeBundle { + let mut bundle = bundle(); + bind_artifact_to_bytes(&mut bundle.artifacts[0], bytes); + bundle +} + fn artifact(identity: &str, basename: &str) -> SccmClientIntakeArtifact { + let artifact_id = format!("fixture-{identity}"); + let bytes = payload_bytes_for(&artifact_id, "+000"); + artifact_bound_to(identity, basename, &bytes) +} + +fn artifact_bound_to(identity: &str, basename: &str, bytes: &[u8]) -> SccmClientIntakeArtifact { let group = match basename { "PolicyAgent.log" => "client-policy-agent", "CIAgent.log" => "client-policy-state", @@ -41,6 +56,8 @@ fn artifact(identity: &str, basename: &str) -> SccmClientIntakeArtifact { rotation_lineage: None, relative_path: Some(format!("evidence/{group}/current/{basename}")), fragment_complete: Some(true), + declared_byte_length: Some(bytes.len() as u64), + content_sha256: Some(digest(bytes)), } } @@ -49,12 +66,17 @@ fn payload() -> SccmClientCapturedPayload { } fn payload_for(artifact_id: &str, offset: &str) -> SccmClientCapturedPayload { - let bytes = ccm_record("SYNTHETIC FIXTURE admitted policy", offset).into_bytes(); - SccmClientCapturedPayload { - artifact_id: artifact_id.to_owned(), - byte_length: bytes.len() as u64, - expected_sha256: digest(&bytes), - bytes, + payload_from_bytes(artifact_id, payload_bytes_for(artifact_id, offset)) +} + +fn payload_bytes_for(artifact_id: &str, offset: &str) -> Vec { + ccm_record(&format!("SYNTHETIC FIXTURE {artifact_id}"), offset).into_bytes() +} + +fn payload_from_bytes(artifact_id: &str, bytes: Vec) -> SccmClientCapturedPayload { + match SccmClientCapturedPayload::new(artifact_id.to_owned(), bytes) { + Ok(payload) => payload, + Err(error) => panic!("test payload must satisfy its public boundary: {error}"), } } @@ -70,39 +92,31 @@ fn ccm_record(message: &str, offset: &str) -> String { ) } -fn refresh_payload_integrity(payload: &mut SccmClientCapturedPayload) { - payload.byte_length = payload.bytes.len() as u64; - payload.expected_sha256 = digest(&payload.bytes); +fn bind_artifact_to_bytes(artifact: &mut SccmClientIntakeArtifact, bytes: &[u8]) { + artifact.declared_byte_length = Some(bytes.len() as u64); + artifact.content_sha256 = Some(digest(bytes)); } -fn payload_padded_to(artifact_id: &str, total_bytes: usize) -> SccmClientCapturedPayload { - let mut payload = payload_for(artifact_id, "+000"); - assert!( - payload.bytes.len() <= total_bytes, - "test payload remains valid" - ); - payload.bytes.resize(total_bytes, b' '); - refresh_payload_integrity(&mut payload); - payload +fn payload_bytes_padded_to(artifact_id: &str, total_bytes: usize) -> Vec { + let mut bytes = payload_bytes_for(artifact_id, "+000"); + assert!(bytes.len() <= total_bytes, "test payload remains valid"); + bytes.resize(total_bytes, b' '); + bytes } -fn payload_with_repeated_records( - artifact_id: &str, - record_count: usize, - message: &str, -) -> SccmClientCapturedPayload { - let bytes = ccm_record(message, "+000") +fn repeated_record_bytes(record_count: usize, message: &str) -> Vec { + ccm_record(message, "+000") .repeat(record_count) - .into_bytes(); - SccmClientCapturedPayload { - artifact_id: artifact_id.to_owned(), - byte_length: bytes.len() as u64, - expected_sha256: digest(&bytes), - bytes, - } + .into_bytes() } fn numbered_artifact(number: u32) -> SccmClientIntakeArtifact { + let artifact_id = format!("sccm-artifact:v1:sha256:{number:064x}"); + let bytes = payload_bytes_for(&artifact_id, "+000"); + numbered_artifact_bound_to(number, &bytes) +} + +fn numbered_artifact_bound_to(number: u32, bytes: &[u8]) -> SccmClientIntakeArtifact { let artifact_id = format!("sccm-artifact:v1:sha256:{number:064x}"); let basename = format!("PolicyAgent.log.{number}"); SccmClientIntakeArtifact { @@ -124,6 +138,18 @@ fn numbered_artifact(number: u32) -> SccmClientIntakeArtifact { "evidence/client-policy-agent/numbered-{number}/{basename}" )), fragment_complete: Some(true), + declared_byte_length: Some(bytes.len() as u64), + content_sha256: Some(digest(bytes)), + } +} + +fn admission_error( + result: Result, + context: &str, +) -> SccmClientEvidenceAdmissionError { + match result { + Ok(_) => panic!("{context}"), + Err(error) => error, } } @@ -163,34 +189,54 @@ fn admission_rejects_missing_extra_duplicate_and_swapped_payloads() { .artifacts .push(artifact("policy-state", "CIAgent.log")); let assessment = assess_client_intake(&bundle).expect("two payload fixture is canonical"); - let agent = payload(); - let state = payload_for("fixture-policy-state", "+000"); - - assert!(admit_client_evidence(&bundle, &assessment, std::slice::from_ref(&agent)).is_err()); + assert!(admit_client_evidence(&bundle, &assessment, &[payload()]).is_err()); - let mut extra = vec![agent.clone(), state.clone()]; - extra.push(payload_for("fixture-unknown", "+000")); - assert!(admit_client_evidence(&bundle, &assessment, &extra).is_err()); + assert!(admit_client_evidence( + &bundle, + &assessment, + &[ + payload(), + payload_for("fixture-policy-state", "+000"), + payload_for("fixture-unknown", "+000"), + ] + ) + .is_err()); - assert!(admit_client_evidence(&bundle, &assessment, &[agent.clone(), agent]).is_err()); + assert!(admit_client_evidence(&bundle, &assessment, &[payload(), payload()]).is_err()); - let mut swapped = state; - swapped.artifact_id = "fixture-policy-agent".to_owned(); - assert!(admit_client_evidence(&bundle, &assessment, &[payload(), swapped]).is_err()); + assert!(admit_client_evidence( + &bundle, + &assessment, + &[ + payload(), + payload_from_bytes( + "fixture-policy-state", + payload_bytes_for("fixture-policy-agent", "+000"), + ), + ] + ) + .is_err()); } #[test] fn admission_rejects_payload_digest_and_length_mismatches() { - let bundle = bundle(); - let assessment = assess_client_intake(&bundle).expect("fixture assessment is canonical"); - - let mut bad_digest = payload(); - bad_digest.expected_sha256 = "0".repeat(64); - assert!(admit_client_evidence(&bundle, &assessment, &[bad_digest]).is_err()); + let mut bad_digest_bundle = bundle(); + bad_digest_bundle.artifacts[0].content_sha256 = Some("0".repeat(64)); + let bad_digest_assessment = + assess_client_intake(&bad_digest_bundle).expect("wrong digest remains canonical metadata"); + assert!( + admit_client_evidence(&bad_digest_bundle, &bad_digest_assessment, &[payload()]).is_err() + ); - let mut bad_length = payload(); - bad_length.byte_length += 1; - assert!(admit_client_evidence(&bundle, &assessment, &[bad_length]).is_err()); + let mut bad_length_bundle = bundle(); + bad_length_bundle.artifacts[0].declared_byte_length = bad_length_bundle.artifacts[0] + .declared_byte_length + .and_then(|length| length.checked_add(1)); + let bad_length_assessment = + assess_client_intake(&bad_length_bundle).expect("wrong length remains canonical metadata"); + assert!( + admit_client_evidence(&bad_length_bundle, &bad_length_assessment, &[payload()]).is_err() + ); } #[test] @@ -208,10 +254,12 @@ fn admission_accepts_the_exact_cap_and_rejects_payload_overflow_before_reassessm .iter() .map(|artifact| payload_for(&artifact.artifact.artifact_id, "+000")) .collect::>(); - assert!(admit_client_evidence(&bundle, &assessment, &payloads).is_ok()); + if let Err(error) = admit_client_evidence(&bundle, &assessment, &payloads) { + panic!("the exact payload and record cap must remain admissible: {error}"); + } let mut overflow = payloads; - overflow.push(payload_for("fixture-overflow", "+000")); + overflow.push(payload_for("fixture-policy-approved", "+000")); assert!(admit_client_evidence(&bundle, &assessment, &overflow).is_err()); } @@ -220,11 +268,15 @@ fn admission_rejects_noncaptured_incomplete_malformed_and_invalid_offset_payload let mut capped = bundle(); capped.artifacts[0].artifact.coverage = SccmCoverageState::Capped; capped.artifacts[0].fragment_complete = Some(false); + capped.artifacts[0].declared_byte_length = None; + capped.artifacts[0].content_sha256 = None; let capped_assessment = assess_client_intake(&capped).expect("capped state is explicit"); assert!(admit_client_evidence(&capped, &capped_assessment, &[payload()]).is_err()); let mut incomplete = bundle(); incomplete.artifacts[0].fragment_complete = Some(false); + incomplete.artifacts[0].declared_byte_length = None; + incomplete.artifacts[0].content_sha256 = None; let incomplete_assessment = assess_client_intake(&incomplete).expect("incomplete boundary is explicit"); assert!(admit_client_evidence(&incomplete, &incomplete_assessment, &[payload()]).is_err()); @@ -238,35 +290,46 @@ fn admission_rejects_noncaptured_incomplete_malformed_and_invalid_offset_payload .is_err() ); - let malformed = SccmClientCapturedPayload { - artifact_id: "fixture-policy-agent".to_owned(), - bytes: b"not a CCM logical record".to_vec(), - byte_length: 24, - expected_sha256: digest(b"not a CCM logical record"), - }; - let assessment = assess_client_intake(&bundle()).expect("fixture assessment is canonical"); - assert!(admit_client_evidence(&bundle(), &assessment, &[malformed]).is_err()); + let malformed_bytes = b"not a CCM logical record".to_vec(); + let malformed_bundle = bundle_with_bound_policy(&malformed_bytes); + let malformed_assessment = + assess_client_intake(&malformed_bundle).expect("malformed bytes remain bound intake"); + assert!(admit_client_evidence( + &malformed_bundle, + &malformed_assessment, + &[payload_from_bytes("fixture-policy-agent", malformed_bytes)] + ) + .is_err()); + let invalid_offset_bytes = payload_bytes_for("fixture-policy-agent", "+9999"); + let invalid_offset_bundle = bundle_with_bound_policy(&invalid_offset_bytes); + let invalid_offset_assessment = assess_client_intake(&invalid_offset_bundle) + .expect("invalid record time remains bound intake metadata"); assert!(admit_client_evidence( - &bundle(), - &assessment, - &[payload_for("fixture-policy-agent", "+9999",)] + &invalid_offset_bundle, + &invalid_offset_assessment, + &[payload_from_bytes( + "fixture-policy-agent", + invalid_offset_bytes, + )] ) .is_err()); } #[test] fn admission_rejects_unclosed_ccm_suffix_even_when_manifest_claims_complete() { - let bundle = bundle(); + let mut truncated_bytes = payload_bytes_for("fixture-policy-agent", "+000"); + truncated_bytes.extend_from_slice(b" error, + Ok(_) => panic!("the bytes-only constructor must cap per-payload parser work"), + }; assert_eq!( error.to_string(), "client evidence admission payload exceeds the v1 per-payload byte cap" ); + let mut aggregate_artifacts = Vec::new(); + let mut aggregate_payloads = Vec::new(); + for number in 1..=5 { + let artifact_id = format!("sccm-artifact:v1:sha256:{number:064x}"); + let bytes = payload_bytes_padded_to(&artifact_id, 4 * 1024 * 1024); + aggregate_artifacts.push(numbered_artifact_bound_to(number, &bytes)); + aggregate_payloads.push(payload_from_bytes(&artifact_id, bytes)); + } let aggregate_bundle = SccmClientIntakeBundle { - artifacts: (1..=5).map(numbered_artifact).collect(), + artifacts: aggregate_artifacts, capture_gaps: Vec::new(), }; let aggregate_assessment = assess_client_intake(&aggregate_bundle).expect("aggregate fixture assessment is canonical"); - let aggregate_payloads = aggregate_bundle - .artifacts - .iter() - .map(|artifact| payload_padded_to(&artifact.artifact.artifact_id, 4 * 1024 * 1024)) - .collect::>(); - let error = admit_client_evidence( - &aggregate_bundle, - &aggregate_assessment, - &aggregate_payloads, - ) - .expect_err("aggregate parser work must be capped before hashing every payload"); + let error = admission_error( + admit_client_evidence( + &aggregate_bundle, + &aggregate_assessment, + &aggregate_payloads, + ), + "aggregate parser work must be capped before hashing every payload", + ); assert_eq!( error.to_string(), "client evidence admission aggregate payload bytes exceed the v1 cap" ); - let too_many_records = - payload_with_repeated_records("fixture-policy-agent", 4_097, "synthetic record"); - let error = admit_client_evidence(&bundle, &assessment, &[too_many_records]) - .expect_err("logical-record expansion must be capped before evidence retention"); + let too_many_record_bytes = repeated_record_bytes(4_097, "synthetic record"); + let too_many_record_bundle = bundle_with_bound_policy(&too_many_record_bytes); + let too_many_record_assessment = assess_client_intake(&too_many_record_bundle) + .expect("record-limit fixture intake is canonical"); + let error = admission_error( + admit_client_evidence( + &too_many_record_bundle, + &too_many_record_assessment, + &[payload_from_bytes( + "fixture-policy-agent", + too_many_record_bytes, + )], + ), + "logical-record expansion must be capped before evidence retention", + ); assert_eq!( error.to_string(), "client evidence admission logical record count exceeds the v1 cap" ); let retained_message = "x".repeat(3_000); - let oversized_retained = - payload_with_repeated_records("fixture-policy-agent", 1_024, &retained_message); - let error = admit_client_evidence(&bundle, &assessment, &[oversized_retained]) - .expect_err("retained evidence and seal input must remain bounded"); + let oversized_retained_bytes = repeated_record_bytes(1_024, &retained_message); + let oversized_retained_bundle = bundle_with_bound_policy(&oversized_retained_bytes); + let oversized_retained_assessment = assess_client_intake(&oversized_retained_bundle) + .expect("retained-limit fixture intake is canonical"); + let error = admission_error( + admit_client_evidence( + &oversized_retained_bundle, + &oversized_retained_assessment, + &[payload_from_bytes( + "fixture-policy-agent", + oversized_retained_bytes, + )], + ), + "retained evidence and seal input must remain bounded", + ); assert_eq!( error.to_string(), "client evidence admission retained evidence exceeds the v1 byte cap" @@ -330,24 +419,30 @@ fn admission_rejects_oversized_payload_work_records_and_retained_evidence() { #[test] fn admission_enforces_logical_record_cap_across_all_payloads() { + let mut artifacts = Vec::new(); + let mut payloads = Vec::new(); + for number in 1..=2 { + let artifact_id = format!("sccm-artifact:v1:sha256:{number:064x}"); + let bytes = repeated_record_bytes(2_049, "x"); + artifacts.push(numbered_artifact_bound_to(number, &bytes)); + payloads.push(payload_from_bytes(&artifact_id, bytes)); + } let bundle = SccmClientIntakeBundle { - artifacts: (1..=2).map(numbered_artifact).collect(), + artifacts, capture_gaps: Vec::new(), }; let assessment = assess_client_intake(&bundle).expect("two captured rotations are canonical intake"); - let payloads = bundle - .artifacts - .iter() - .map(|artifact| payload_with_repeated_records(&artifact.artifact.artifact_id, 2_049, "x")) - .collect::>(); let result = admit_client_evidence(&bundle, &assessment, &payloads); assert!( result.is_err(), "two individually bounded rotations totaling 4,098 records were admitted" ); - let error = result.expect_err("the logical-record cap must apply across the complete bundle"); + let error = admission_error( + result, + "the logical-record cap must apply across the complete bundle", + ); assert_eq!( error.to_string(), "client evidence admission logical record count exceeds the v1 cap" diff --git a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs index 8718d93d9..332e127f6 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs @@ -31,6 +31,7 @@ fn source_group(basename: &str) -> &'static str { "PolicyAgent.log" => "client-policy-agent", "CAS.log" => "client-content", "AppIntentEval.log" => "client-app-intent", + "AppEnforce.log" => "client-app-enforce", "CustomVendorHook.log" => "unknown", _ => panic!("authority fixture basename must be declared here"), } @@ -130,6 +131,26 @@ fn admission_rejects_substituted_valid_ccm_bytes_against_the_intake_binding() { ); } +#[test] +fn admission_rejects_a_mutated_assessment_content_binding() { + let bytes = ccm_bytes("bound policy evidence"); + let bundle = bundle_with(vec![artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&bytes), + )]); + let mut assessment = assess_client_intake(&bundle).expect("bound intake is canonical"); + assessment.physical_artifacts[0].content_sha256 = Some("0".repeat(64)); + + let error = admission_error( + admit_client_evidence(&bundle, &assessment, &[payload("fixture-policy", bytes)]), + "a caller-mutated assessment binding must not become authority", + ); + assert_eq!(error, SccmClientEvidenceAdmissionError::AssessmentMutation); +} + #[test] fn intake_content_binding_is_paired_lowercase_and_capture_local() { let bytes = ccm_bytes("bound policy evidence"); @@ -218,7 +239,7 @@ fn intake_content_binding_is_paired_lowercase_and_capture_local() { fn legacy_intake_remains_assessable_but_cannot_admit_bytes() { let bytes = ccm_bytes("legacy policy evidence"); let legacy = artifact( - "legacy", + "policy-approved", "PolicyAgent.log", SccmCoverageState::Captured, true, @@ -231,7 +252,11 @@ fn legacy_intake_remains_assessable_but_cannot_admit_bytes() { assert!(wire["artifacts"][0].get("contentSha256").is_none()); let error = admission_error( - admit_client_evidence(&bundle, &assessment, &[payload("fixture-legacy", bytes)]), + admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-policy-approved", bytes)], + ), "legacy intake must not authorize caller-supplied bytes", ); assert_eq!( @@ -300,7 +325,7 @@ fn admission_rejects_swapped_duplicate_extra_and_missing_payloads() { &assessment, &[ payload("fixture-policy", policy_bytes.clone()), - payload("fixture-extra", ccm_bytes("extra evidence")), + payload("fixture-policy-two", ccm_bytes("extra evidence")), ], ), "an extra syntactically valid payload identity must fail closed", @@ -343,6 +368,13 @@ fn incomplete_and_capped_sources_fail_locally_without_blocking_bound_policy() { false, None, ), + artifact( + "enforce-missing", + "AppEnforce.log", + SccmCoverageState::Captured, + true, + None, + ), ]); let assessment = assess_client_intake(&bundle).expect("mixed source coverage is canonical"); let admitted = match admit_client_evidence( @@ -361,6 +393,9 @@ fn incomplete_and_capped_sources_fail_locally_without_blocking_bound_policy() { assert!(admitted .require_captured_source("client-app-intent") .is_err()); + assert!(admitted + .require_captured_source("client-app-enforce") + .is_err()); assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); } diff --git a/crates/cmtraceopen-parser/src/sccm/client/intake.rs b/crates/cmtraceopen-parser/src/sccm/client/intake.rs index fbe0135d1..8bbc01f3d 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/intake.rs @@ -185,6 +185,17 @@ pub struct SccmClientIntakeArtifact { /// `ParseFailed` fragment may be complete when all bytes were copied but /// their contents could not be normalized as CCM evidence. pub fragment_complete: Option, + /// Byte length declared by the capture authority for a recognized, + /// complete `Captured` fragment. This is optional so legacy intake stays + /// assessment-compatible, but admission requires it together with + /// `content_sha256` before caller-supplied bytes can become evidence. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub declared_byte_length: Option, + /// Lowercase SHA-256 declared by the capture authority. It is a pair with + /// `declared_byte_length` and is forbidden on noncaptured, incomplete, or + /// unsupported declarations. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content_sha256: Option, } /// A coverage-only declaration for a recognized client source rotation that @@ -480,6 +491,8 @@ pub struct SccmClientIntakeFragment { pub configmgr_version: Option, pub collected_at_utc: Option, pub encoding: Option, + pub declared_byte_length: Option, + pub content_sha256: Option, } #[derive(Debug, Clone, PartialEq)] @@ -544,6 +557,10 @@ struct SccmClientIntakeFragmentWire { configmgr_version: Option, collected_at_utc: Option, encoding: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + declared_byte_length: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + content_sha256: Option, } impl From for SccmClientIntakeFragment { @@ -560,6 +577,8 @@ impl From for SccmClientIntakeFragment { configmgr_version: wire.configmgr_version, collected_at_utc: wire.collected_at_utc, encoding: wire.encoding, + declared_byte_length: wire.declared_byte_length, + content_sha256: wire.content_sha256, } } } @@ -578,6 +597,8 @@ impl From<&SccmClientIntakeFragment> for SccmClientIntakeFragmentWire { configmgr_version: fragment.configmgr_version.clone(), collected_at_utc: fragment.collected_at_utc.clone(), encoding: fragment.encoding.clone(), + declared_byte_length: fragment.declared_byte_length, + content_sha256: fragment.content_sha256.clone(), } } } @@ -835,6 +856,8 @@ pub enum SccmClientIntakeError { MissingFragmentCompleteness, #[error("client intake fragment completeness contradicts its declared coverage state")] InvalidFragmentCompleteness, + #[error("client intake content length and digest binding is malformed or not capture-local")] + InvalidContentBinding, #[error("client intake capture gap is malformed, unsupported, or not coverage-only")] InvalidCaptureGap, } @@ -1125,6 +1148,8 @@ fn fragment_as_intake_artifact(fragment: &SccmClientIntakeFragment) -> SccmClien rotation_lineage: fragment.rotation_lineage.clone(), relative_path: fragment.relative_path.clone(), fragment_complete: fragment.fragment_complete, + declared_byte_length: fragment.declared_byte_length, + content_sha256: fragment.content_sha256.clone(), } } @@ -1148,6 +1173,8 @@ fn unsupported_as_intake_artifact( rotation_lineage: unsupported.rotation_lineage.clone(), relative_path: unsupported.relative_path.clone(), fragment_complete: unsupported.fragment_complete, + declared_byte_length: None, + content_sha256: None, } } @@ -1313,6 +1340,7 @@ fn validate_bundle(bundle: &SccmClientIntakeBundle) -> Result<(), SccmClientInta let fragment_complete = source .fragment_complete .ok_or(SccmClientIntakeError::MissingFragmentCompleteness)?; + validate_content_binding(source, fragment_complete)?; let source_identity = ( source.artifact.display_name.to_ascii_lowercase(), rotation_identity(&source.artifact.rotation), @@ -1484,9 +1512,35 @@ fn intake_fragment(source: &SccmClientIntakeArtifact) -> SccmClientIntakeFragmen configmgr_version: source.artifact.configmgr_version.clone(), collected_at_utc: normalized_collected_at(source.artifact.collected_at_utc.as_deref()), encoding: source.artifact.encoding.clone(), + declared_byte_length: source.declared_byte_length, + content_sha256: source.content_sha256.clone(), } } +fn validate_content_binding( + source: &SccmClientIntakeArtifact, + fragment_complete: bool, +) -> Result<(), SccmClientIntakeError> { + let has_binding = match ( + source.declared_byte_length, + source.content_sha256.as_deref(), + ) { + (None, None) => false, + (Some(_), Some(digest)) if is_sha256_digest(digest) => true, + _ => return Err(SccmClientIntakeError::InvalidContentBinding), + }; + + if has_binding + && (source.artifact.coverage != SccmCoverageState::Captured + || !fragment_complete + || matching_groups(&source.artifact.display_name, &source.artifact.rotation).is_empty()) + { + return Err(SccmClientIntakeError::InvalidContentBinding); + } + + Ok(()) +} + fn normalized_collected_at(value: Option<&str>) -> Option { value.map(|value| { DateTime::parse_from_rfc3339(value) @@ -1678,7 +1732,7 @@ fn is_physical_state(coverage: &SccmCoverageState) -> bool { ) } -fn is_safe_artifact_id(value: &str) -> bool { +pub(super) fn is_safe_artifact_id(value: &str) -> bool { if value.is_empty() || value.chars().count() > MAX_ARTIFACT_ID_CHARS { return false; } diff --git a/crates/cmtraceopen-parser/src/sccm/client/mod.rs b/crates/cmtraceopen-parser/src/sccm/client/mod.rs index b2df4278e..6622096f5 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/mod.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/mod.rs @@ -11,4 +11,8 @@ mod admission_tests; #[cfg(test)] mod authority_contract_tests; +pub use admission::{ + admit_client_evidence, SccmClientAdmittedEvidence, SccmClientCapturedPayload, + SccmClientEvidenceAdmissionError, +}; pub use intake::*; diff --git a/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs b/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs index 124ac0277..e7f556f4e 100644 --- a/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs +++ b/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs @@ -28,7 +28,7 @@ fn public_bytes_only_facade_uses_intake_bound_content_authority() { let bundle = SccmClientIntakeBundle { artifacts: vec![SccmClientIntakeArtifact { artifact: SccmArtifact { - artifact_id: "fixture-public-authority".to_owned(), + artifact_id: "fixture-policy-approved".to_owned(), display_name: "PolicyAgent.log".to_owned(), original_path: None, host: None, @@ -39,7 +39,7 @@ fn public_bytes_only_facade_uses_intake_bound_content_authority() { coverage: SccmCoverageState::Captured, encoding: Some("utf-8".to_owned()), }, - path_fingerprint: Some("synthetic-public-authority".to_owned()), + path_fingerprint: Some("synthetic-policy-approved".to_owned()), rotation_lineage: None, relative_path: Some("evidence/client-policy-agent/current/PolicyAgent.log".to_owned()), fragment_complete: Some(true), @@ -49,7 +49,7 @@ fn public_bytes_only_facade_uses_intake_bound_content_authority() { capture_gaps: Vec::new(), }; let assessment = assess_client_intake(&bundle).expect("public intake is canonical"); - let payload = match SccmClientCapturedPayload::new("fixture-public-authority", bytes) { + let payload = match SccmClientCapturedPayload::new("fixture-policy-approved", bytes) { Ok(payload) => payload, Err(error) => panic!("public payload constructor rejected canonical bytes: {error}"), }; diff --git a/crates/cmtraceopen-parser/tests/sccm_client_intake.rs b/crates/cmtraceopen-parser/tests/sccm_client_intake.rs index c77263fa5..2ebef4a15 100644 --- a/crates/cmtraceopen-parser/tests/sccm_client_intake.rs +++ b/crates/cmtraceopen-parser/tests/sccm_client_intake.rs @@ -224,6 +224,8 @@ fn load_bundle(scenario: &str) -> SccmClientIntakeBundle { rotation_lineage: fixture.rotation.lineage_id, relative_path: fixture.relative_path, fragment_complete: fixture.rotation.fragment_complete, + declared_byte_length: None, + content_sha256: None, } }) .collect(), @@ -412,6 +414,8 @@ fn synthetic_artifact(artifact_id: &str, display_name: &str) -> SccmClientIntake rotation_lineage: None, relative_path: Some(relative_path), fragment_complete: Some(true), + declared_byte_length: None, + content_sha256: None, } } @@ -449,6 +453,8 @@ fn opaque_numbered_artifact(number: usize) -> SccmClientIntakeArtifact { "evidence/client-policy-agent/numbered-{number}/PolicyAgent.log.{number}" )), fragment_complete: Some(true), + declared_byte_length: None, + content_sha256: None, } } @@ -1720,6 +1726,8 @@ fn capped_cas_fragment_cannot_claim_complete() { rotation_lineage: None, relative_path: Some("evidence/client-content/current/CAS.log".to_owned()), fragment_complete: Some(true), + declared_byte_length: None, + content_sha256: None, }; assert_eq!( diff --git a/docs/sccm/preparation/issue-319-client-intake.md b/docs/sccm/preparation/issue-319-client-intake.md index 40c870aca..bd741189c 100644 --- a/docs/sccm/preparation/issue-319-client-intake.md +++ b/docs/sccm/preparation/issue-319-client-intake.md @@ -136,6 +136,15 @@ root, use a testable access-status provider, and retain original paths only as privacy-classified provenance. There is no Tauri command, UI, direct parser filesystem access, globbing in the pure crate, or redefinition of CCM. +The later native binding must use one opened source handle for the entire +capture transaction: stream the bounded retained bytes, compute +`declaredByteLength` and lowercase `contentSha256` over exactly those bytes, +and persist/hand off that same byte sequence before closing the handle. It must +not stat, hash, or reopen the path in separate authority steps, because a file +replacement between those operations would bind the manifest to bytes that +were never supplied. This pure-parser slice validates and consumes that +binding but does not implement or claim the Windows handle workflow. + ## Determinism, collision, and rotation rules - Sort manifest artifacts by catalog entry ID, normalized path fingerprint, @@ -210,11 +219,13 @@ remains a separate acceptance gate. file format. - `expected.json` uses `contractState: pureIntakeImplementedNativePending`. `pureAssessment` is the complete typed - executable oracle. `nativeDesignPending` holds byte/limit/digest facts that - remain outside the pure projection, and `downstreamDesignPending` labels - request wording and prohibited claims that are not intake output. Native - manifest emission, discovery/capture, and Windows acceptance remain - design-only gates rather than delivered claims. + executable oracle. Legacy fixtures intentionally omit the additive + length/digest authority and therefore remain assessment-only; + `nativeDesignPending` holds the proposed byte/limit facts until the native + adapter can populate the new binding from one source handle. + `downstreamDesignPending` labels request wording and prohibited claims that + are not intake output. Native manifest emission, discovery/capture, and + Windows acceptance remain design-only gates rather than delivered claims. ## Remaining delivery blockers From 2b196e05aa7a1610f6f61f6729dbc77ec9110253 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:46:17 -0400 Subject: [PATCH 09/27] test(sccm): expose client admission authority gaps --- .../src/sccm/client/admission_tests.rs | 108 +++++++++- .../sccm/client/authority_contract_tests.rs | 188 +++++++++++++++++- .../tests/sccm_client_admission_authority.rs | 19 +- 3 files changed, 306 insertions(+), 9 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index a42269835..078f991fc 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -92,6 +92,23 @@ fn ccm_record(message: &str, offset: &str) -> String { ) } +fn utf16_with_bom(value: &str, little_endian: bool) -> Vec { + let mut bytes = if little_endian { + vec![0xff, 0xfe] + } else { + vec![0xfe, 0xff] + }; + for unit in value.encode_utf16() { + let encoded = if little_endian { + unit.to_le_bytes() + } else { + unit.to_be_bytes() + }; + bytes.extend_from_slice(&encoded); + } + bytes +} + fn bind_artifact_to_bytes(artifact: &mut SccmClientIntakeArtifact, bytes: &[u8]) { artifact.declared_byte_length = Some(bytes.len() as u64); artifact.content_sha256 = Some(digest(bytes)); @@ -218,6 +235,28 @@ fn admission_rejects_missing_extra_duplicate_and_swapped_payloads() { .is_err()); } +#[test] +fn admission_distinguishes_within_cap_extra_payloads_from_missing_payloads() { + let bundle = bundle(); + let assessment = assess_client_intake(&bundle).expect("one payload fixture is canonical"); + + let missing = admission_error( + admit_client_evidence(&bundle, &assessment, &[]), + "an omitted eligible payload must fail closed", + ); + assert_eq!(missing, SccmClientEvidenceAdmissionError::MissingPayload); + + let extra = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[payload(), payload_for("fixture-policy-approved", "+000")], + ), + "an additional within-cap payload must fail closed", + ); + assert_eq!(extra, SccmClientEvidenceAdmissionError::ExtraPayload); +} + #[test] fn admission_rejects_payload_digest_and_length_mismatches() { let mut bad_digest_bundle = bundle(); @@ -239,6 +278,66 @@ fn admission_rejects_payload_digest_and_length_mismatches() { ); } +#[test] +fn admission_rejects_boms_that_do_not_match_the_declared_encoding() { + let record = ccm_record("declared encoding authority", "+000"); + let mut utf8_bom = vec![0xef, 0xbb, 0xbf]; + utf8_bom.extend_from_slice(record.as_bytes()); + let cases = [ + ("utf-8", utf16_with_bom(&record, true)), + ("utf-16le", utf16_with_bom(&record, false)), + ("utf-16be", utf8_bom.clone()), + ("windows-1252", utf8_bom), + ]; + + for (declared_encoding, bytes) in cases { + let mut bundle = bundle_with_bound_policy(&bytes); + bundle.artifacts[0].artifact.encoding = Some(declared_encoding.to_owned()); + let assessment = assess_client_intake(&bundle) + .expect("a declared encoding and byte binding remain canonical intake metadata"); + let error = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[payload_from_bytes("fixture-policy-agent", bytes)], + ), + "a mismatched Unicode BOM must not override declared encoding authority", + ); + assert_eq!( + error, + SccmClientEvidenceAdmissionError::InvalidEncoding, + "declared {declared_encoding}" + ); + } +} + +#[test] +fn admission_accepts_only_matching_unicode_boms() { + let record = ccm_record("matching declared encoding", "+000"); + let mut utf8_bom = vec![0xef, 0xbb, 0xbf]; + utf8_bom.extend_from_slice(record.as_bytes()); + let cases = [ + ("utf-8", utf8_bom), + ("utf-16le", utf16_with_bom(&record, true)), + ("utf-16be", utf16_with_bom(&record, false)), + ]; + + for (declared_encoding, bytes) in cases { + let mut bundle = bundle_with_bound_policy(&bytes); + bundle.artifacts[0].artifact.encoding = Some(declared_encoding.to_owned()); + let assessment = assess_client_intake(&bundle) + .expect("a matching BOM remains canonical intake metadata"); + admit_client_evidence( + &bundle, + &assessment, + &[payload_from_bytes("fixture-policy-agent", bytes)], + ) + .unwrap_or_else(|error| { + panic!("matching {declared_encoding} BOM must be admitted: {error}") + }); + } +} + #[test] fn admission_accepts_the_exact_cap_and_rejects_payload_overflow_before_reassessment() { let artifacts = (1..=super::MAX_SCCM_CLIENT_INTAKE_ARTIFACTS as u32) @@ -260,7 +359,14 @@ fn admission_accepts_the_exact_cap_and_rejects_payload_overflow_before_reassessm let mut overflow = payloads; overflow.push(payload_for("fixture-policy-approved", "+000")); - assert!(admit_client_evidence(&bundle, &assessment, &overflow).is_err()); + let error = admission_error( + admit_client_evidence(&bundle, &assessment, &overflow), + "the global payload-count guard must reject 4,097 payloads", + ); + assert_eq!( + error, + SccmClientEvidenceAdmissionError::PayloadLimitExceeded + ); } #[test] diff --git a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs index 332e127f6..7bcaad39b 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs @@ -5,7 +5,11 @@ use super::admission::{ SccmClientEvidenceAdmissionError, }; use super::{assess_client_intake, SccmClientIntakeArtifact, SccmClientIntakeBundle}; -use crate::sccm::{SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmRole, SccmRotation}; +use crate::sccm::{ + extract_keys, SccmArtifact, SccmArtifactFamily, SccmCorrelationKeyKind, SccmCoverageState, + SccmExtractionGapKind, SccmExtractionProfile, SccmExtractionProfileMaturity, SccmKeyConfidence, + SccmRole, SccmRotation, SCCM_EXPERIMENTAL_KEY_PROFILE_ID, +}; fn digest(bytes: &[u8]) -> String { Sha256::digest(bytes) @@ -32,6 +36,8 @@ fn source_group(basename: &str) -> &'static str { "CAS.log" => "client-content", "AppIntentEval.log" => "client-app-intent", "AppEnforce.log" => "client-app-enforce", + "client.msi.log" => "client-ccmsetup", + "ReportingEvents.log" => "client-windows-update-supplemental", "CustomVendorHook.log" => "unknown", _ => panic!("authority fixture basename must be declared here"), } @@ -426,6 +432,186 @@ fn admitted_profile_is_bound_to_the_catalogued_source_family() { ); } +#[test] +fn recognized_non_ccm_sources_cannot_enter_raw_ccm_admission() { + for (identity, basename) in [ + ("client-setup", "client.msi.log"), + ("reporting-supplemental", "ReportingEvents.log"), + ] { + let bytes = ccm_bytes("CCM-shaped bytes from a non-CCM source"); + let bundle = bundle_with(vec![artifact( + identity, + basename, + SccmCoverageState::Captured, + true, + Some(&bytes), + )]); + let assessment = assess_client_intake(&bundle).expect("bound intake is canonical"); + + assert!( + admit_client_evidence( + &bundle, + &assessment, + &[payload(&format!("fixture-{identity}"), bytes)], + ) + .is_err(), + "{basename} must never authorize raw CCM evidence" + ); + } +} + +#[test] +fn captured_non_ccm_supplement_does_not_block_or_join_policy_admission() { + let policy_bytes = ccm_bytes("policy evidence"); + let supplemental_bytes = ccm_bytes("CCM-shaped supplemental text"); + let bundle = bundle_with(vec![ + artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&policy_bytes), + ), + artifact( + "reporting-supplemental", + "ReportingEvents.log", + SccmCoverageState::Captured, + true, + Some(&supplemental_bytes), + ), + ]); + let assessment = assess_client_intake(&bundle).expect("mixed intake is canonical"); + let admitted = admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-policy", policy_bytes)], + ) + .expect("non-CCM supplemental bytes must not be required for policy admission"); + + assert!(admitted + .require_captured_source("client-policy-agent") + .is_ok()); + assert!(admitted + .require_captured_source("client-windows-update-supplemental") + .is_err()); + assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); + assert!(admitted + .profile_for_artifact("fixture-reporting-supplemental") + .expect("valid authority seal") + .is_none()); +} + +#[test] +fn sealed_policy_profile_extracts_a_low_confidence_assignment_key() { + let assignment_id = "12345678-1234-1234-1234-123456789abc"; + let bytes = ccm_bytes(&format!("Assignment ID = {assignment_id}")); + let bundle = bundle_with(vec![artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&bytes), + )]); + let assessment = assess_client_intake(&bundle).expect("bound policy intake is canonical"); + let admitted = admit_client_evidence(&bundle, &assessment, &[payload("fixture-policy", bytes)]) + .expect("bound policy evidence must be admitted"); + let evidence = &admitted.evidence().expect("valid authority seal")[0]; + let profile = admitted + .profile_for_artifact("fixture-policy") + .expect("valid authority seal") + .expect("policy evidence has a sealed profile"); + + let result = extract_keys(evidence, profile); + + assert_eq!(result.keys.len(), 1); + assert_eq!(result.keys[0].kind, SccmCorrelationKeyKind::AssignmentId); + assert_eq!(result.keys[0].normalized, assignment_id); + assert_eq!(result.keys[0].confidence, SccmKeyConfidence::Low); + assert!(result + .gaps + .iter() + .any(|gap| gap.kind == SccmExtractionGapKind::ExperimentalProfile)); + assert!(result + .gaps + .iter() + .all(|gap| gap.kind != SccmExtractionGapKind::UnvalidatedProfile)); +} + +#[test] +fn caller_forged_family_profile_is_rejected_by_key_extraction() { + let assignment_id = "12345678-1234-1234-1234-123456789abc"; + let bytes = ccm_bytes(&format!("Assignment ID = {assignment_id}")); + let bundle = bundle_with(vec![artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&bytes), + )]); + let assessment = assess_client_intake(&bundle).expect("bound policy intake is canonical"); + let admitted = admit_client_evidence(&bundle, &assessment, &[payload("fixture-policy", bytes)]) + .expect("bound policy evidence must be admitted"); + let evidence = &admitted.evidence().expect("valid authority seal")[0]; + let forged = SccmExtractionProfile { + profile_id: SCCM_EXPERIMENTAL_KEY_PROFILE_ID.to_owned(), + configmgr_version_prefixes: vec!["5.00.9128.".to_owned()], + validated_artifact_families: vec![SccmArtifactFamily::Unknown("callerForged".to_owned())], + selected_configmgr_version: Some("5.00.9128.1000".to_owned()), + maturity: SccmExtractionProfileMaturity::Experimental, + }; + + let result = extract_keys(evidence, &forged); + + assert!(result.keys.is_empty()); + assert_eq!(result.gaps.len(), 1); + assert_eq!( + result.gaps[0].kind, + SccmExtractionGapKind::UnvalidatedProfile + ); + assert_eq!( + result.gaps[0].candidate_kind, + Some(SccmCorrelationKeyKind::AssignmentId) + ); +} + +#[test] +fn unregistered_ccm_family_is_admitted_with_an_unvalidated_profile_gap() { + let bytes = ccm_bytes("Package ID = LAB00001"); + let bundle = bundle_with(vec![artifact( + "content", + "CAS.log", + SccmCoverageState::Captured, + true, + Some(&bytes), + )]); + let assessment = assess_client_intake(&bundle).expect("bound content intake is canonical"); + let admitted = + admit_client_evidence(&bundle, &assessment, &[payload("fixture-content", bytes)]) + .expect("raw CCM evidence does not require a validated key-extraction family"); + let evidence = &admitted.evidence().expect("valid authority seal")[0]; + let profile = admitted + .profile_for_artifact("fixture-content") + .expect("valid authority seal") + .expect("content evidence has a sealed family-bound profile"); + + let result = extract_keys(evidence, profile); + + assert_eq!( + profile.validated_artifact_families, + [SccmArtifactFamily::ClientContent] + ); + assert!(result.keys.is_empty()); + assert_eq!(result.gaps.len(), 1); + assert_eq!( + result.gaps[0].kind, + SccmExtractionGapKind::UnvalidatedProfile + ); + assert_eq!( + result.gaps[0].candidate_kind, + Some(SccmCorrelationKeyKind::PackageId) + ); +} + #[test] fn captured_payload_constructor_rejects_noncanonical_identity() { let result = SccmClientCapturedPayload::new("C:\\Users\\raw\\PolicyAgent.log", ccm_bytes("x")); diff --git a/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs b/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs index e7f556f4e..3cc585940 100644 --- a/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs +++ b/crates/cmtraceopen-parser/tests/sccm_client_admission_authority.rs @@ -25,6 +25,8 @@ fn bytes() -> Vec { #[test] fn public_bytes_only_facade_uses_intake_bound_content_authority() { let bytes = bytes(); + let expected_length = bytes.len() as u64; + let expected_digest = digest(&bytes); let bundle = SccmClientIntakeBundle { artifacts: vec![SccmClientIntakeArtifact { artifact: SccmArtifact { @@ -43,8 +45,8 @@ fn public_bytes_only_facade_uses_intake_bound_content_authority() { rotation_lineage: None, relative_path: Some("evidence/client-policy-agent/current/PolicyAgent.log".to_owned()), fragment_complete: Some(true), - declared_byte_length: Some(bytes.len() as u64), - content_sha256: Some(digest(&bytes)), + declared_byte_length: Some(expected_length), + content_sha256: Some(expected_digest.clone()), }], capture_gaps: Vec::new(), }; @@ -54,10 +56,9 @@ fn public_bytes_only_facade_uses_intake_bound_content_authority() { Err(error) => panic!("public payload constructor rejected canonical bytes: {error}"), }; - assert!( - admit_client_evidence(&bundle, &assessment, &[payload]).is_ok(), - "public callers can obtain opaque authority only for intake-bound bytes" - ); + if let Err(error) = admit_client_evidence(&bundle, &assessment, &[payload]) { + panic!("public callers can obtain opaque authority only for intake-bound bytes: {error}"); + } let wire = serde_json::to_value(&bundle).expect("bound intake serializes"); assert!(wire["artifacts"][0]["declaredByteLength"].is_u64()); @@ -70,6 +71,10 @@ fn public_bytes_only_facade_uses_intake_bound_content_authority() { let projected = assess_client_intake(&round_trip).expect("round trip remains canonical"); assert_eq!( projected.physical_artifacts[0].declared_byte_length, - Some(round_trip.artifacts[0].declared_byte_length.unwrap()) + Some(expected_length) + ); + assert_eq!( + projected.physical_artifacts[0].content_sha256.as_deref(), + Some(expected_digest.as_str()) ); } From 2d18af848854ff9b7d7c9c8eae82ea2deee0a0a9 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:51:50 -0400 Subject: [PATCH 10/27] fix(sccm): enforce client evidence source authority --- .../src/sccm/client/admission.rs | 110 +++++++++++------- crates/cmtraceopen-parser/src/sccm/keys.rs | 39 ++++++- 2 files changed, 106 insertions(+), 43 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index 5e96678e8..a7602525b 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -24,9 +24,8 @@ use crate::parser::ccm::scan_logical_records_bounded; use crate::sccm::catalog::classify_artifact_name; use crate::sccm::evidence::SccmRawEvidenceSnapshot; use crate::sccm::{ - SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmEvidence, SccmExtractionProfile, - SccmExtractionProfileMaturity, SccmRole, SccmTimeOrderingState, - SCCM_EXPERIMENTAL_KEY_PROFILE_ID, + SccmArtifact, SccmCoverageState, SccmEvidence, SccmExtractionProfile, SccmRole, + SccmTimeOrderingState, }; use super::{ @@ -240,6 +239,10 @@ pub fn admit_client_evidence( let mut eligible = BTreeMap::new(); let mut unbound_complete_captures = BTreeSet::new(); for fragment in &canonical.physical_artifacts { + let classified = classify_artifact_name(&fragment.basename, SccmRole::Client); + if !classified.supported_for_diagnosis || !classified.uses_ccm_records { + continue; + } if fragment.coverage != SccmCoverageState::Captured || fragment.fragment_complete != Some(true) { @@ -249,20 +252,19 @@ pub fn admit_client_evidence( unbound_complete_captures.insert(fragment.artifact_id.as_str()); continue; } - eligible.insert(fragment.artifact_id.clone(), fragment); + eligible.insert(fragment.artifact_id.clone(), (fragment, classified.family)); } let admitted_source_groups = canonical .groups .iter() .filter(|group| { - !group.fragments.is_empty() + group.fragments.iter().any(is_supported_raw_ccm_fragment) && group.coverage == SccmCoverageState::Captured - && group.fragments.iter().all(|fragment| { - fragment.coverage == SccmCoverageState::Captured - && fragment.fragment_complete == Some(true) - && fragment.declared_byte_length.is_some() - && fragment.content_sha256.is_some() - }) + && group + .fragments + .iter() + .filter(|fragment| is_supported_raw_ccm_fragment(fragment)) + .all(is_bound_complete_capture) }) .map(|group| group.logical_artifact_id.clone()) .collect::>(); @@ -272,9 +274,12 @@ pub fn admit_client_evidence( { return Err(SccmClientEvidenceAdmissionError::MissingContentBinding); } - if payloads.len() != eligible.len() { + if payloads.len() < eligible.len() { return Err(SccmClientEvidenceAdmissionError::MissingPayload); } + if payloads.len() > eligible.len() { + return Err(SccmClientEvidenceAdmissionError::ExtraPayload); + } let mut ordered_payloads = payloads.iter().collect::>(); ordered_payloads.sort_by(|left, right| left.artifact_id.cmp(&right.artifact_id)); @@ -290,19 +295,17 @@ pub fn admit_client_evidence( if !seen_payload_ids.insert(payload.artifact_id.as_str()) { return Err(SccmClientEvidenceAdmissionError::DuplicatePayload); } - let fragment = eligible + let (fragment, family) = eligible .get(&payload.artifact_id) - .copied() .ok_or(SccmClientEvidenceAdmissionError::ExtraPayload)?; + let fragment = *fragment; validate_payload(payload, fragment)?; - let classified = classify_artifact_name(&fragment.basename, SccmRole::Client); - let family = classified.family; - let mut profile = SccmExtractionProfile::for_version(fragment.configmgr_version.as_deref()); - profile.validated_artifact_families = vec![family.clone()]; - if !classified.supported_for_diagnosis || !is_registered_client_profile(&profile, &family) { - return Err(SccmClientEvidenceAdmissionError::UnregisteredProfile); - } + let profile = SccmExtractionProfile::for_artifact_family( + fragment.configmgr_version.as_deref(), + family, + ) + .ok_or(SccmClientEvidenceAdmissionError::UnregisteredProfile)?; let content = decode_payload(payload, fragment.encoding.as_deref())?; let artifact = artifact_for_fragment(fragment); let scan = scan_logical_records_bounded( @@ -390,6 +393,18 @@ fn validate_payload_budget( Ok(()) } +fn is_supported_raw_ccm_fragment(fragment: &SccmClientIntakeFragment) -> bool { + let classified = classify_artifact_name(&fragment.basename, SccmRole::Client); + classified.supported_for_diagnosis && classified.uses_ccm_records +} + +fn is_bound_complete_capture(fragment: &SccmClientIntakeFragment) -> bool { + fragment.coverage == SccmCoverageState::Captured + && fragment.fragment_complete == Some(true) + && fragment.declared_byte_length.is_some() + && fragment.content_sha256.is_some() +} + fn validate_payload( payload: &SccmClientCapturedPayload, fragment: &SccmClientIntakeFragment, @@ -451,19 +466,45 @@ fn decode_payload( payload: &SccmClientCapturedPayload, encoding: Option<&str>, ) -> Result { - let encoding = match encoding { - Some("utf-8") => UTF_8, - Some("utf-16le") => UTF_16LE, - Some("utf-16be") => UTF_16BE, - Some("windows-1252") => WINDOWS_1252, + let (encoding, declared_bom) = match encoding { + Some("utf-8") => (UTF_8, Some(UnicodeBom::Utf8)), + Some("utf-16le") => (UTF_16LE, Some(UnicodeBom::Utf16Le)), + Some("utf-16be") => (UTF_16BE, Some(UnicodeBom::Utf16Be)), + Some("windows-1252") => (WINDOWS_1252, None), _ => return Err(SccmClientEvidenceAdmissionError::InvalidEncoding), }; - let (decoded, _, had_errors) = encoding.decode(&payload.bytes); + let bytes = match recognized_unicode_bom(&payload.bytes) { + Some((actual_bom, bom_len)) if Some(actual_bom) == declared_bom => { + &payload.bytes[bom_len..] + } + Some(_) => return Err(SccmClientEvidenceAdmissionError::InvalidEncoding), + None => payload.bytes.as_slice(), + }; + let (decoded, had_errors) = encoding.decode_without_bom_handling(bytes); (!had_errors) .then_some(decoded.into_owned()) .ok_or(SccmClientEvidenceAdmissionError::InvalidEncoding) } +#[derive(Clone, Copy, PartialEq, Eq)] +enum UnicodeBom { + Utf8, + Utf16Le, + Utf16Be, +} + +fn recognized_unicode_bom(bytes: &[u8]) -> Option<(UnicodeBom, usize)> { + if bytes.starts_with(&[0xef, 0xbb, 0xbf]) { + Some((UnicodeBom::Utf8, 3)) + } else if bytes.starts_with(&[0xff, 0xfe]) { + Some((UnicodeBom::Utf16Le, 2)) + } else if bytes.starts_with(&[0xfe, 0xff]) { + Some((UnicodeBom::Utf16Be, 2)) + } else { + None + } +} + fn artifact_for_fragment(fragment: &SccmClientIntakeFragment) -> SccmArtifact { SccmArtifact { artifact_id: fragment.artifact_id.clone(), @@ -479,21 +520,6 @@ fn artifact_for_fragment(fragment: &SccmClientIntakeFragment) -> SccmArtifact { } } -fn is_registered_client_profile( - profile: &SccmExtractionProfile, - family: &SccmArtifactFamily, -) -> bool { - profile.profile_id == SCCM_EXPERIMENTAL_KEY_PROFILE_ID - && profile.maturity == SccmExtractionProfileMaturity::Experimental - && profile.configmgr_version_prefixes == ["5.00.9128."] - && profile.validated_artifact_families.first() == Some(family) - && profile.validated_artifact_families.len() == 1 - && profile - .selected_configmgr_version - .as_deref() - .is_some_and(|version| version.starts_with("5.00.9128.")) -} - fn compare_evidence(left: &SccmEvidence, right: &SccmEvidence) -> std::cmp::Ordering { ( left.reference.artifact_id.as_str(), diff --git a/crates/cmtraceopen-parser/src/sccm/keys.rs b/crates/cmtraceopen-parser/src/sccm/keys.rs index c1821d450..dd43d444b 100644 --- a/crates/cmtraceopen-parser/src/sccm/keys.rs +++ b/crates/cmtraceopen-parser/src/sccm/keys.rs @@ -2,6 +2,7 @@ use std::sync::OnceLock; use regex::Regex; +use super::catalog::SccmArtifactFamily; use super::findings::{has_at_most_chars, MAX_SCCM_CORRELATION_KEY_VALUE_CHARS}; use super::models::{ SccmCorrelationKey, SccmCorrelationKeyKind, SccmEvidence, SccmExtractionGap, @@ -48,6 +49,24 @@ impl SccmExtractionProfile { }, } } + + /// Selects the centrally defined built-in version profile and binds it to + /// the catalog family that produced one admitted raw-CCM artifact. + /// + /// Family binding does not validate a new extractor family. `extract_keys` + /// independently recognizes only the executable-fixture registry below; + /// other families remain visible through `UnvalidatedProfile` gaps. + pub(crate) fn for_artifact_family( + configmgr_version: Option<&str>, + family: &SccmArtifactFamily, + ) -> Option { + let mut profile = Self::for_version(configmgr_version); + if !is_builtin_experimental_core(&profile) { + return None; + } + profile.validated_artifact_families = vec![family.clone()]; + Some(profile) + } } struct KeyPattern { @@ -231,9 +250,20 @@ fn profile_gap_kind(profile: &SccmExtractionProfile) -> Option bool { + is_builtin_experimental_core(profile) + && match profile.validated_artifact_families.as_slice() { + // Preserve the generic public `for_version` contract while the + // admitted-evidence path always seals a catalog family. + [] => true, + [family] => is_validated_builtin_experimental_family(family), + _ => false, + } +} + +fn is_builtin_experimental_core(profile: &SccmExtractionProfile) -> bool { profile.profile_id == SCCM_EXPERIMENTAL_KEY_PROFILE_ID + && profile.maturity == SccmExtractionProfileMaturity::Experimental && profile.configmgr_version_prefixes == [EXPERIMENTAL_VERSION_PREFIX] - && profile.validated_artifact_families.is_empty() && profile .selected_configmgr_version .as_deref() @@ -243,6 +273,13 @@ fn is_builtin_experimental(profile: &SccmExtractionProfile) -> bool { }) } +fn is_validated_builtin_experimental_family(family: &SccmArtifactFamily) -> bool { + // Only Policy has executable key-extraction fixtures in the first client + // implementation slice. Expanding this registry requires those fixtures + // and a review; catalog membership alone is never validation authority. + matches!(family, SccmArtifactFamily::ClientPolicy) +} + fn is_canonical_configmgr_version(version: &str) -> bool { let mut component_count = 0; for component in version.split('.') { From aebe81c3f050595be6ce043fa039f5a87043e121 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:55:15 -0400 Subject: [PATCH 11/27] test(sccm): expose UTF-32 BOM prefix confusion --- .../src/sccm/client/admission_tests.rs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index 078f991fc..871a9e64e 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -109,6 +109,23 @@ fn utf16_with_bom(value: &str, little_endian: bool) -> Vec { bytes } +fn utf32_with_bom(value: &str, little_endian: bool) -> Vec { + let mut bytes = if little_endian { + vec![0xff, 0xfe, 0x00, 0x00] + } else { + vec![0x00, 0x00, 0xfe, 0xff] + }; + for character in value.chars() { + let encoded = if little_endian { + u32::from(character).to_le_bytes() + } else { + u32::from(character).to_be_bytes() + }; + bytes.extend_from_slice(&encoded); + } + bytes +} + fn bind_artifact_to_bytes(artifact: &mut SccmClientIntakeArtifact, bytes: &[u8]) { artifact.declared_byte_length = Some(bytes.len() as u64); artifact.content_sha256 = Some(digest(bytes)); @@ -311,6 +328,35 @@ fn admission_rejects_boms_that_do_not_match_the_declared_encoding() { } } +#[test] +fn admission_rejects_utf32_boms_before_utf16_prefix_matching() { + let record = ccm_record("unsupported UTF-32 encoding", "+000"); + let cases = [ + ("utf-16le", utf32_with_bom(&record, true)), + ("windows-1252", utf32_with_bom(&record, false)), + ]; + + for (declared_encoding, bytes) in cases { + let mut bundle = bundle_with_bound_policy(&bytes); + bundle.artifacts[0].artifact.encoding = Some(declared_encoding.to_owned()); + let assessment = assess_client_intake(&bundle) + .expect("an unsupported BOM remains canonical intake metadata"); + let error = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[payload_from_bytes("fixture-policy-agent", bytes)], + ), + "UTF-32 BOMs must not inherit another declared encoding", + ); + assert_eq!( + error, + SccmClientEvidenceAdmissionError::InvalidEncoding, + "declared {declared_encoding}" + ); + } +} + #[test] fn admission_accepts_only_matching_unicode_boms() { let record = ccm_record("matching declared encoding", "+000"); From d2bcd121cee40108cb259f91c9674ac6f78cef72 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:56:04 -0400 Subject: [PATCH 12/27] fix(sccm): reject unsupported Unicode BOMs --- crates/cmtraceopen-parser/src/sccm/client/admission.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index a7602525b..22a8ca7c6 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -491,10 +491,16 @@ enum UnicodeBom { Utf8, Utf16Le, Utf16Be, + Utf32Le, + Utf32Be, } fn recognized_unicode_bom(bytes: &[u8]) -> Option<(UnicodeBom, usize)> { - if bytes.starts_with(&[0xef, 0xbb, 0xbf]) { + if bytes.starts_with(&[0xff, 0xfe, 0x00, 0x00]) { + Some((UnicodeBom::Utf32Le, 4)) + } else if bytes.starts_with(&[0x00, 0x00, 0xfe, 0xff]) { + Some((UnicodeBom::Utf32Be, 4)) + } else if bytes.starts_with(&[0xef, 0xbb, 0xbf]) { Some((UnicodeBom::Utf8, 3)) } else if bytes.starts_with(&[0xff, 0xfe]) { Some((UnicodeBom::Utf16Le, 2)) From 5e5b8ce877c341a5bb1ceb5b6d13ba236d7c5953 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:10:11 -0400 Subject: [PATCH 13/27] test(sccm): expose mixed setup coverage authority --- .../sccm/client/authority_contract_tests.rs | 108 +++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs index 7bcaad39b..c1efd36fb 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs @@ -4,7 +4,10 @@ use super::admission::{ admit_client_evidence, SccmClientAdmittedEvidence, SccmClientCapturedPayload, SccmClientEvidenceAdmissionError, }; -use super::{assess_client_intake, SccmClientIntakeArtifact, SccmClientIntakeBundle}; +use super::{ + assess_client_intake, SccmClientIntakeArtifact, SccmClientIntakeBundle, + SccmClientIntakeCaptureGap, +}; use crate::sccm::{ extract_keys, SccmArtifact, SccmArtifactFamily, SccmCorrelationKeyKind, SccmCoverageState, SccmExtractionGapKind, SccmExtractionProfile, SccmExtractionProfileMaturity, SccmKeyConfidence, @@ -36,6 +39,7 @@ fn source_group(basename: &str) -> &'static str { "CAS.log" => "client-content", "AppIntentEval.log" => "client-app-intent", "AppEnforce.log" => "client-app-enforce", + "ccmsetup.log" | "ccmsetup.lo_" => "client-ccmsetup", "client.msi.log" => "client-ccmsetup", "ReportingEvents.log" => "client-windows-update-supplemental", "CustomVendorHook.log" => "unknown", @@ -501,6 +505,108 @@ fn captured_non_ccm_supplement_does_not_block_or_join_policy_admission() { .is_none()); } +#[test] +fn non_ccm_sibling_coverage_does_not_block_bound_ccmsetup_admission() { + for (coverage, identity) in [ + (SccmCoverageState::Capped, "client-setup-capped"), + (SccmCoverageState::AccessDenied, "client-setup-denied"), + ] { + let setup_bytes = ccm_bytes("bound ccmsetup evidence"); + let bundle = bundle_with(vec![ + artifact( + "ccmsetup", + "ccmsetup.log", + SccmCoverageState::Captured, + true, + Some(&setup_bytes), + ), + artifact(identity, "client.msi.log", coverage.clone(), false, None), + ]); + let assessment = assess_client_intake(&bundle).expect("mixed setup intake is canonical"); + let admitted = admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-ccmsetup", setup_bytes)], + ) + .expect("a non-CCM sibling must not block exact bound ccmsetup evidence"); + + assert!(admitted.require_captured_source("client-ccmsetup").is_ok()); + assert_eq!( + admitted + .source_coverage("client-ccmsetup") + .expect("valid authority seal"), + Some(&coverage), + "canonical non-CCM coverage remains visible for evidence-first reporting" + ); + assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); + } +} + +#[test] +fn raw_ccm_sibling_gap_still_blocks_ccmsetup_group_readiness() { + let setup_bytes = ccm_bytes("bound ccmsetup evidence"); + let mut denied_rollback = artifact( + "ccmsetup-denied", + "ccmsetup.lo_", + SccmCoverageState::AccessDenied, + false, + None, + ); + denied_rollback.artifact.rotation = SccmRotation::LoUnderscore; + let bundle = bundle_with(vec![ + artifact( + "ccmsetup", + "ccmsetup.log", + SccmCoverageState::Captured, + true, + Some(&setup_bytes), + ), + denied_rollback, + ]); + let assessment = assess_client_intake(&bundle).expect("raw CCM gap intake is canonical"); + let admitted = admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-ccmsetup", setup_bytes)], + ) + .expect("a raw CCM gap is local readiness state, not global admission failure"); + + assert!(admitted.require_captured_source("client-ccmsetup").is_err()); + assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); +} + +#[test] +fn raw_ccm_capture_gap_still_blocks_ccmsetup_group_readiness() { + let setup_bytes = ccm_bytes("bound ccmsetup evidence"); + let bundle = SccmClientIntakeBundle { + artifacts: vec![artifact( + "ccmsetup", + "ccmsetup.log", + SccmCoverageState::Captured, + true, + Some(&setup_bytes), + )], + capture_gaps: vec![SccmClientIntakeCaptureGap { + artifact_id: "fixture-capped-rotation".to_owned(), + basename: "ccmsetup.log.1".to_owned(), + rotation: SccmRotation::Numbered(1), + coverage: SccmCoverageState::Capped, + path_fingerprint: "synthetic-capped-rotation".to_owned(), + rotation_lineage: "synthetic:capped-rotation".to_owned(), + }], + }; + let assessment = assess_client_intake(&bundle).expect("raw CCM capture gap is canonical"); + let admitted = admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-ccmsetup", setup_bytes)], + ) + .expect("a raw CCM capture gap is local readiness state, not admission failure"); + + assert!(admitted.require_captured_source("client-ccmsetup").is_err()); + assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); +} + #[test] fn sealed_policy_profile_extracts_a_low_confidence_assignment_key() { let assignment_id = "12345678-1234-1234-1234-123456789abc"; From 3d8584c626396e190e9a4df8db2af8e3f4fd5263 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:15:41 -0400 Subject: [PATCH 14/27] fix(sccm): isolate raw CCM group readiness --- .../src/sccm/client/admission.rs | 28 ++++++++++++------- .../src/sccm/client/intake.rs | 10 +++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index 22a8ca7c6..df320c8eb 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -29,9 +29,10 @@ use crate::sccm::{ }; use super::{ - assess_client_intake, intake::is_safe_artifact_id, SccmClientIntakeAssessment, - SccmClientIntakeBundle, SccmClientIntakeError, SccmClientIntakeFragment, - MAX_SCCM_CLIENT_INTAKE_ARTIFACTS, + assess_client_intake, + intake::{is_safe_artifact_id, source_matches_group}, + SccmClientIntakeAssessment, SccmClientIntakeBundle, SccmClientIntakeError, + SccmClientIntakeFragment, MAX_SCCM_CLIENT_INTAKE_ARTIFACTS, }; /// Maximum raw bytes decoded from one client payload at the parser admission @@ -132,11 +133,7 @@ impl SccmClientAdmittedEvidence { logical_artifact_id: &str, ) -> Result<(), SccmClientEvidenceAdmissionError> { match self.source_coverage(logical_artifact_id)? { - Some(SccmCoverageState::Captured) - if self.admitted_source_groups.contains(logical_artifact_id) => - { - Ok(()) - } + Some(_) if self.admitted_source_groups.contains(logical_artifact_id) => Ok(()), Some(_) => Err(SccmClientEvidenceAdmissionError::SourceCoverageUnavailable), None => Err(SccmClientEvidenceAdmissionError::UnknownSourceGroup), } @@ -259,12 +256,19 @@ pub fn admit_client_evidence( .iter() .filter(|group| { group.fragments.iter().any(is_supported_raw_ccm_fragment) - && group.coverage == SccmCoverageState::Captured && group .fragments .iter() .filter(|fragment| is_supported_raw_ccm_fragment(fragment)) .all(is_bound_complete_capture) + && !canonical.capture_gaps.iter().any(|capture_gap| { + is_supported_raw_ccm_source(&capture_gap.basename) + && source_matches_group( + &capture_gap.basename, + &capture_gap.rotation, + &group.logical_artifact_id, + ) + }) }) .map(|group| group.logical_artifact_id.clone()) .collect::>(); @@ -394,7 +398,11 @@ fn validate_payload_budget( } fn is_supported_raw_ccm_fragment(fragment: &SccmClientIntakeFragment) -> bool { - let classified = classify_artifact_name(&fragment.basename, SccmRole::Client); + is_supported_raw_ccm_source(&fragment.basename) +} + +fn is_supported_raw_ccm_source(basename: &str) -> bool { + let classified = classify_artifact_name(basename, SccmRole::Client); classified.supported_for_diagnosis && classified.uses_ccm_records } diff --git a/crates/cmtraceopen-parser/src/sccm/client/intake.rs b/crates/cmtraceopen-parser/src/sccm/client/intake.rs index 8bbc01f3d..ae4c116cd 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/intake.rs @@ -1464,6 +1464,16 @@ fn matching_groups( .collect() } +pub(super) fn source_matches_group( + display_name: &str, + rotation: &SccmRotation, + logical_artifact_id: &str, +) -> bool { + matching_groups(display_name, rotation) + .iter() + .any(|group| group.logical_artifact_id == logical_artifact_id) +} + fn catalogued_client_source( display_name: &str, rotation: &SccmRotation, From ec38be9aac786ad5e8e2149b9e639ac7ced032ea Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:19:52 -0400 Subject: [PATCH 15/27] test(sccm): expose forged policy extraction authority --- .../src/sccm/client/admission_tests.rs | 7 +- .../sccm/client/authority_contract_tests.rs | 95 +++++++++++-------- 2 files changed, 59 insertions(+), 43 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index 871a9e64e..117662de4 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -204,9 +204,8 @@ fn admission_seals_canonical_records_from_complete_captured_payloads() { Some(&SccmCoverageState::Captured) ); assert!(admitted - .profile_for_artifact("fixture-policy-agent") - .expect("valid seal") - .is_some()); + .extract_keys_for_artifact("fixture-policy-agent") + .is_ok()); assert!(admitted .require_captured_source("client-policy-agent") .is_ok()); @@ -658,7 +657,7 @@ fn admission_integrity_rejects_test_only_record_profile_and_identity_collisions( profile_mutation.test_only_mutate_first_profile(); assert!(profile_mutation.verify_integrity().is_err()); assert!(profile_mutation - .profile_for_artifact("fixture-policy-agent") + .extract_keys_for_artifact("fixture-policy-agent") .is_err()); let mut collision = diff --git a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs index c1efd36fb..b69208211 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs @@ -1,8 +1,8 @@ use sha2::{Digest, Sha256}; use super::admission::{ - admit_client_evidence, SccmClientAdmittedEvidence, SccmClientCapturedPayload, - SccmClientEvidenceAdmissionError, + admit_client_evidence, SccmClientAdmittedEvidence, SccmClientAdmittedKeyExtraction, + SccmClientCapturedPayload, SccmClientEvidenceAdmissionError, }; use super::{ assess_client_intake, SccmClientIntakeArtifact, SccmClientIntakeBundle, @@ -426,14 +426,14 @@ fn admitted_profile_is_bound_to_the_catalogued_source_family() { Err(error) => panic!("bound policy evidence must be admitted: {error}"), }; - let profile = admitted - .profile_for_artifact("fixture-policy") - .expect("valid authority seal") - .expect("admitted artifact has a profile"); + let extraction = admitted + .extract_keys_for_artifact("fixture-policy") + .expect("admitted artifact has sealed key-extraction authority"); assert_eq!( - profile.validated_artifact_families, - [SccmArtifactFamily::ClientPolicy] + extraction.artifact_family(), + &SccmArtifactFamily::ClientPolicy ); + assert_eq!(extraction.artifact_id(), "fixture-policy"); } #[test] @@ -500,9 +500,8 @@ fn captured_non_ccm_supplement_does_not_block_or_join_policy_admission() { .is_err()); assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); assert!(admitted - .profile_for_artifact("fixture-reporting-supplemental") - .expect("valid authority seal") - .is_none()); + .extract_keys_for_artifact("fixture-reporting-supplemental") + .is_err()); } #[test] @@ -608,7 +607,7 @@ fn raw_ccm_capture_gap_still_blocks_ccmsetup_group_readiness() { } #[test] -fn sealed_policy_profile_extracts_a_low_confidence_assignment_key() { +fn admitted_policy_extraction_is_sealed_to_the_exact_artifact_and_family() { let assignment_id = "12345678-1234-1234-1234-123456789abc"; let bytes = ccm_bytes(&format!("Assignment ID = {assignment_id}")); let bundle = bundle_with(vec![artifact( @@ -621,14 +620,17 @@ fn sealed_policy_profile_extracts_a_low_confidence_assignment_key() { let assessment = assess_client_intake(&bundle).expect("bound policy intake is canonical"); let admitted = admit_client_evidence(&bundle, &assessment, &[payload("fixture-policy", bytes)]) .expect("bound policy evidence must be admitted"); - let evidence = &admitted.evidence().expect("valid authority seal")[0]; - let profile = admitted - .profile_for_artifact("fixture-policy") - .expect("valid authority seal") - .expect("policy evidence has a sealed profile"); - - let result = extract_keys(evidence, profile); + let extraction: SccmClientAdmittedKeyExtraction = admitted + .extract_keys_for_artifact("fixture-policy") + .expect("policy evidence has sealed extraction authority"); + let result = &extraction.results()[0]; + assert_eq!(extraction.artifact_id(), "fixture-policy"); + assert_eq!( + extraction.artifact_family(), + &SccmArtifactFamily::ClientPolicy + ); + assert_eq!(extraction.results().len(), 1); assert_eq!(result.keys.len(), 1); assert_eq!(result.keys[0].kind, SccmCorrelationKeyKind::AssignmentId); assert_eq!(result.keys[0].normalized, assignment_id); @@ -644,7 +646,7 @@ fn sealed_policy_profile_extracts_a_low_confidence_assignment_key() { } #[test] -fn caller_forged_family_profile_is_rejected_by_key_extraction() { +fn caller_constructed_policy_profile_cannot_cross_the_admitted_boundary() { let assignment_id = "12345678-1234-1234-1234-123456789abc"; let bytes = ccm_bytes(&format!("Assignment ID = {assignment_id}")); let bundle = bundle_with(vec![artifact( @@ -658,26 +660,44 @@ fn caller_forged_family_profile_is_rejected_by_key_extraction() { let admitted = admit_client_evidence(&bundle, &assessment, &[payload("fixture-policy", bytes)]) .expect("bound policy evidence must be admitted"); let evidence = &admitted.evidence().expect("valid authority seal")[0]; - let forged = SccmExtractionProfile { + let caller_constructed = SccmExtractionProfile { profile_id: SCCM_EXPERIMENTAL_KEY_PROFILE_ID.to_owned(), configmgr_version_prefixes: vec!["5.00.9128.".to_owned()], - validated_artifact_families: vec![SccmArtifactFamily::Unknown("callerForged".to_owned())], + validated_artifact_families: vec![SccmArtifactFamily::ClientPolicy], selected_configmgr_version: Some("5.00.9128.1000".to_owned()), maturity: SccmExtractionProfileMaturity::Experimental, }; - let result = extract_keys(evidence, &forged); - - assert!(result.keys.is_empty()); - assert_eq!(result.gaps.len(), 1); + let generic_result = extract_keys(evidence, &caller_constructed); + assert_eq!(generic_result.keys.len(), 1); assert_eq!( - result.gaps[0].kind, - SccmExtractionGapKind::UnvalidatedProfile + generic_result.keys[0].kind, + SccmCorrelationKeyKind::AssignmentId ); + + let admitted_result: SccmClientAdmittedKeyExtraction = admitted + .extract_keys_for_artifact("fixture-policy") + .expect("only the admitted authority selects the sealed profile"); + let result = &admitted_result.results()[0]; + + assert_eq!(admitted_result.artifact_id(), "fixture-policy"); assert_eq!( - result.gaps[0].candidate_kind, - Some(SccmCorrelationKeyKind::AssignmentId) + admitted_result.artifact_family(), + &SccmArtifactFamily::ClientPolicy ); + assert_eq!(admitted_result.results().len(), 1); + assert_eq!(result.keys.len(), 1); + assert_eq!(result.keys[0].kind, SccmCorrelationKeyKind::AssignmentId); + assert_eq!(result.keys[0].normalized, assignment_id); + assert_eq!(result.keys[0].confidence, SccmKeyConfidence::Low); + assert!(result + .gaps + .iter() + .any(|gap| gap.kind == SccmExtractionGapKind::ExperimentalProfile)); + assert!(result + .gaps + .iter() + .all(|gap| gap.kind != SccmExtractionGapKind::UnvalidatedProfile)); } #[test] @@ -694,17 +714,14 @@ fn unregistered_ccm_family_is_admitted_with_an_unvalidated_profile_gap() { let admitted = admit_client_evidence(&bundle, &assessment, &[payload("fixture-content", bytes)]) .expect("raw CCM evidence does not require a validated key-extraction family"); - let evidence = &admitted.evidence().expect("valid authority seal")[0]; - let profile = admitted - .profile_for_artifact("fixture-content") - .expect("valid authority seal") - .expect("content evidence has a sealed family-bound profile"); - - let result = extract_keys(evidence, profile); + let extraction = admitted + .extract_keys_for_artifact("fixture-content") + .expect("content evidence has sealed family-bound extraction authority"); + let result = &extraction.results()[0]; assert_eq!( - profile.validated_artifact_families, - [SccmArtifactFamily::ClientContent] + extraction.artifact_family(), + &SccmArtifactFamily::ClientContent ); assert!(result.keys.is_empty()); assert_eq!(result.gaps.len(), 1); From 8d8af0a9b7bc78d95c2e1772bdd444ae8f475f45 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:23:35 -0400 Subject: [PATCH 16/27] test(sccm): bind extraction to exact artifact evidence --- .../sccm/client/authority_contract_tests.rs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs index b69208211..09dd96bf4 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs @@ -609,17 +609,34 @@ fn raw_ccm_capture_gap_still_blocks_ccmsetup_group_readiness() { #[test] fn admitted_policy_extraction_is_sealed_to_the_exact_artifact_and_family() { let assignment_id = "12345678-1234-1234-1234-123456789abc"; - let bytes = ccm_bytes(&format!("Assignment ID = {assignment_id}")); - let bundle = bundle_with(vec![artifact( - "policy", - "PolicyAgent.log", - SccmCoverageState::Captured, - true, - Some(&bytes), - )]); + let policy_bytes = ccm_bytes(&format!("Assignment ID = {assignment_id}")); + let content_bytes = ccm_bytes("Package ID = LAB00001"); + let bundle = bundle_with(vec![ + artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&policy_bytes), + ), + artifact( + "content", + "CAS.log", + SccmCoverageState::Captured, + true, + Some(&content_bytes), + ), + ]); let assessment = assess_client_intake(&bundle).expect("bound policy intake is canonical"); - let admitted = admit_client_evidence(&bundle, &assessment, &[payload("fixture-policy", bytes)]) - .expect("bound policy evidence must be admitted"); + let admitted = admit_client_evidence( + &bundle, + &assessment, + &[ + payload("fixture-content", content_bytes), + payload("fixture-policy", policy_bytes), + ], + ) + .expect("bound policy and content evidence must be admitted"); let extraction: SccmClientAdmittedKeyExtraction = admitted .extract_keys_for_artifact("fixture-policy") .expect("policy evidence has sealed extraction authority"); From 37309e9f2e0037e16510eec8c792646c2447d998 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:24:43 -0400 Subject: [PATCH 17/27] fix(sccm): seal client key extraction authority --- .../src/sccm/client/admission.rs | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index df320c8eb..646afd9f7 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -24,8 +24,8 @@ use crate::parser::ccm::scan_logical_records_bounded; use crate::sccm::catalog::classify_artifact_name; use crate::sccm::evidence::SccmRawEvidenceSnapshot; use crate::sccm::{ - SccmArtifact, SccmCoverageState, SccmEvidence, SccmExtractionProfile, SccmRole, - SccmTimeOrderingState, + extract_keys, SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmEvidence, + SccmExtractionProfile, SccmKeyExtractionResult, SccmRole, SccmTimeOrderingState, }; use super::{ @@ -88,6 +88,29 @@ pub struct SccmClientAdmittedEvidence { integrity_seal: String, } +/// Artifact-scoped key-extraction results selected only from sealed client +/// evidence authority. Its private fields and lack of a constructor prevent a +/// generic extraction result from being substituted for admitted authority. +pub(crate) struct SccmClientAdmittedKeyExtraction { + artifact_id: String, + artifact_family: SccmArtifactFamily, + results: Vec, +} + +impl SccmClientAdmittedKeyExtraction { + pub(crate) fn artifact_id(&self) -> &str { + &self.artifact_id + } + + pub(crate) fn artifact_family(&self) -> &SccmArtifactFamily { + &self.artifact_family + } + + pub(crate) fn results(&self) -> &[SccmKeyExtractionResult] { + &self.results + } +} + impl SccmClientAdmittedEvidence { pub(crate) fn evidence(&self) -> Result<&[SccmEvidence], SccmClientEvidenceAdmissionError> { self.verify_integrity()?; @@ -102,12 +125,33 @@ impl SccmClientAdmittedEvidence { Ok(self.source_coverage.get(logical_artifact_id)) } - pub(crate) fn profile_for_artifact( + pub(crate) fn extract_keys_for_artifact( &self, artifact_id: &str, - ) -> Result, SccmClientEvidenceAdmissionError> { + ) -> Result { self.verify_integrity()?; - Ok(self.profiles_by_artifact.get(artifact_id)) + let (sealed_artifact_id, profile) = self + .profiles_by_artifact + .get_key_value(artifact_id) + .ok_or(SccmClientEvidenceAdmissionError::MissingAdmittedExtractionProfile)?; + let [artifact_family] = profile.validated_artifact_families.as_slice() else { + return Err(SccmClientEvidenceAdmissionError::IntegrityViolation); + }; + let results = self + .evidence + .iter() + .filter(|evidence| evidence.reference.artifact_id == *sealed_artifact_id) + .map(|evidence| extract_keys(evidence, profile)) + .collect::>(); + if results.is_empty() { + return Err(SccmClientEvidenceAdmissionError::MissingAdmittedArtifactEvidence); + } + + Ok(SccmClientAdmittedKeyExtraction { + artifact_id: sealed_artifact_id.clone(), + artifact_family: artifact_family.clone(), + results, + }) } pub(crate) fn integrity_seal(&self) -> &str { @@ -200,6 +244,10 @@ pub enum SccmClientEvidenceAdmissionError { InvalidTimestampProvenance, #[error("client evidence admission selected an unregistered extraction profile")] UnregisteredProfile, + #[error("client evidence admission has no sealed extraction profile for the artifact")] + MissingAdmittedExtractionProfile, + #[error("client evidence admission has no sealed evidence for the artifact")] + MissingAdmittedArtifactEvidence, #[error("client evidence admission produced colliding logical evidence identities")] CollidingEvidenceIdentity, #[error("client evidence admission integrity seal is invalid")] From a6bdeeb985de3728ca677167cebd931d2470097f Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:44:57 -0400 Subject: [PATCH 18/27] test(sccm): pin unsealed native intake projection --- src-tauri/tests/sccm_client_manifest.rs | 30 ++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src-tauri/tests/sccm_client_manifest.rs b/src-tauri/tests/sccm_client_manifest.rs index 39303a1f2..72446a119 100644 --- a/src-tauri/tests/sccm_client_manifest.rs +++ b/src-tauri/tests/sccm_client_manifest.rs @@ -6,7 +6,10 @@ use app_lib::sccm::{ SccmManifestProvenance, SccmManifestSourceState, MAX_SCCM_MANIFEST_ARTIFACTS, SCCM_MANIFEST_FILE_NAME, }; -use cmtraceopen_parser::sccm::{assess_client_intake, SccmCoverageState, SccmRotation}; +use cmtraceopen_parser::sccm::{ + admit_client_evidence, assess_client_intake, SccmClientCapturedPayload, + SccmClientEvidenceAdmissionError, SccmCoverageState, SccmRotation, +}; use serde_json::{json, Value}; use sha2::{Digest, Sha256}; use tempfile::tempdir; @@ -272,15 +275,30 @@ fn validated_v1_reader_projects_one_physical_client_artifact() { } #[test] -fn validated_v1_reader_preserves_a_complete_fragment() { +fn native_projection_preserves_completeness_without_sealing_content_binding() { let temp = tempdir().expect("temporary root"); let bundle_root = temp.path().join("bundle"); - let mut current = physical_artifact(SccmRotation::Current, b"complete-policy-current"); + let exact_bytes = b"complete-policy-current"; + let mut current = physical_artifact(SccmRotation::Current, exact_bytes); current.value["fragmentComplete"] = json!(true); write_native_bundle(&bundle_root, &[¤t], &[]); let bundle = read_sccm_client_intake_bundle(&bundle_root).expect("verified pure projection"); - assert_eq!(bundle.artifacts[0].fragment_complete, Some(true)); + let projected = &bundle.artifacts[0]; + assert_eq!(projected.fragment_complete, Some(true)); + assert_eq!(projected.declared_byte_length, None); + assert_eq!(projected.content_sha256, None); + + let assessment = assess_client_intake(&bundle).expect("native projection remains canonical"); + let payload = SccmClientCapturedPayload::new( + projected.artifact.artifact_id.clone(), + exact_bytes.to_vec(), + ) + .expect("exact manifest-validated bytes form a bounded payload"); + assert!(matches!( + admit_client_evidence(&bundle, &assessment, &[payload]), + Err(SccmClientEvidenceAdmissionError::MissingContentBinding) + )); } #[test] @@ -378,7 +396,7 @@ fn reader_rejects_duplicate_artifact_ids() { } #[test] -fn legacy_projection_never_invents_native_capture_gaps() { +fn legacy_projection_never_invents_native_capture_gaps_or_content_binding() { let temp = tempdir().expect("temporary root"); let bundle_root = temp.path().join("legacy-bundle"); make_private_directory(&bundle_root); @@ -411,6 +429,8 @@ fn legacy_projection_never_invents_native_capture_gaps() { assert!(manifest.capture_gaps.is_empty()); assert!(bundle.capture_gaps.is_empty()); assert_eq!(bundle.artifacts.len(), 1); + assert_eq!(bundle.artifacts[0].declared_byte_length, None); + assert_eq!(bundle.artifacts[0].content_sha256, None); let expected_catalog_id = catalog_entry_id_for("ccmsetup.log"); let expected_artifact_id = format!( "sccm-artifact:v1:sha256:{}", From bb4571cc7f15380c5e2ebb39118c1af958af1c2f Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:45:24 -0400 Subject: [PATCH 19/27] fix(sccm): keep native intake projection unsealed --- src-tauri/src/sccm/manifest.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src-tauri/src/sccm/manifest.rs b/src-tauri/src/sccm/manifest.rs index 859c38ca3..c77ee7eb6 100644 --- a/src-tauri/src/sccm/manifest.rs +++ b/src-tauri/src/sccm/manifest.rs @@ -121,6 +121,8 @@ fn manifest_to_client_intake_bundle( rotation_lineage: source.rotation_lineage.clone(), relative_path: source.relative_path.clone(), fragment_complete: Some(source.fragment_complete), + declared_byte_length: None, + content_sha256: None, }) .collect(); let capture_gaps = manifest @@ -867,6 +869,8 @@ fn read_legacy_client_intake_bundle( rotation_lineage: None, relative_path: None, fragment_complete: Some(false), + declared_byte_length: None, + content_sha256: None, }); } let bundle = SccmClientIntakeBundle { From 7da7d8d015a774719e9c3aa2d5de0cd404d7b5c2 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 14:01:31 -0400 Subject: [PATCH 20/27] test(sccm): pin independent admission seal cap --- .../src/sccm/client/admission.rs | 3 +++ .../src/sccm/client/admission_tests.rs | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index 646afd9f7..4c9e0e05a 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -46,6 +46,9 @@ pub(crate) const MAX_SCCM_CLIENT_ADMISSION_LOGICAL_RECORDS: usize = 4_096; /// Maximum projected evidence bytes retained for one admitted client bundle. pub(crate) const MAX_SCCM_CLIENT_ADMISSION_RETAINED_EVIDENCE_BYTES: usize = 3 * 1024 * 1024; /// Maximum bytes streamed into a deterministic client evidence integrity seal. +/// This is an independent serialization-work cap: JSON escaping may reject a +/// retained-memory-bounded bundle once its escaped serialization exceeds this +/// separate hashing-work bound. pub(crate) const MAX_SCCM_CLIENT_ADMISSION_SEAL_BYTES: usize = 4 * 1024 * 1024; /// Raw, already-captured bytes offered to the one-shot client evidence diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index 117662de4..68779a490 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -568,6 +568,27 @@ fn admission_rejects_oversized_payload_work_records_and_retained_evidence() { ); } +#[test] +fn admission_applies_the_integrity_seal_cap_independently_of_retained_memory() { + let escaped_message = "\0".repeat(700_000); + let bytes = repeated_record_bytes(1, &escaped_message); + let bundle = bundle_with_bound_policy(&bytes); + let assessment = assess_client_intake(&bundle).expect("seal-cap fixture intake is canonical"); + + let error = admission_error( + admit_client_evidence( + &bundle, + &assessment, + &[payload_from_bytes("fixture-policy-agent", bytes)], + ), + "JSON escaping must not bypass the independent seal-work cap", + ); + assert_eq!( + error, + SccmClientEvidenceAdmissionError::IntegritySealLimitExceeded + ); +} + #[test] fn admission_enforces_logical_record_cap_across_all_payloads() { let mut artifacts = Vec::new(); From d705778e959bad23fa7d6b13361c03544731c068 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 14:24:58 -0400 Subject: [PATCH 21/27] test(sccm): expose admitted authority review gaps --- crates/cmtraceopen-parser/src/parser/ccm.rs | 51 ++++++++++++++++++- .../src/sccm/client/admission_tests.rs | 36 ++++++++++--- .../sccm/client/authority_contract_tests.rs | 10 ++-- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/crates/cmtraceopen-parser/src/parser/ccm.rs b/crates/cmtraceopen-parser/src/parser/ccm.rs index a2abbc176..1e2360472 100644 --- a/crates/cmtraceopen-parser/src/parser/ccm.rs +++ b/crates/cmtraceopen-parser/src/parser/ccm.rs @@ -12,6 +12,8 @@ use regex::Regex; use super::severity::detect_severity_from_text; use crate::models::log_entry::{LogEntry, LogFormat, ParserSpecialization, Severity}; +#[cfg(test)] +use std::cell::RefCell; use std::sync::OnceLock; const CCM_RECORD_OPENER: &str = ">> = + const { RefCell::new(None) }; +} + +#[cfg(test)] +pub(crate) fn observe_bounded_scans( + operation: impl FnOnce() -> T, +) -> (T, Vec) { + BOUNDED_SCAN_OBSERVATIONS.with(|observations| { + assert!( + observations.replace(Some(Vec::new())).is_none(), + "bounded scan observation cannot be nested" + ); + }); + let output = operation(); + let observations = BOUNDED_SCAN_OBSERVATIONS.with(|observations| { + observations + .borrow_mut() + .take() + .expect("bounded scan observation was installed") + }); + (output, observations) +} + pub(crate) fn scan_logical_records_bounded( content: &str, file_path: &str, @@ -382,7 +418,7 @@ pub(crate) fn scan_logical_records_bounded( CcmScanMode::SccmEvidence, Some(max_records), ); - CcmLogicalRecordScan { + let bounded = CcmLogicalRecordScan { records: scan .records .into_iter() @@ -390,7 +426,18 @@ pub(crate) fn scan_logical_records_bounded( .collect(), complete: scan.errors == 0 && !scan.record_limit_exceeded, record_limit_exceeded: scan.record_limit_exceeded, - } + }; + #[cfg(test)] + BOUNDED_SCAN_OBSERVATIONS.with(|observations| { + if let Some(observations) = observations.borrow_mut().as_mut() { + observations.push(CcmBoundedScanObservation { + record_limit: max_records, + retained_records: bounded.records.len(), + record_limit_exceeded: bounded.record_limit_exceeded, + }); + } + }); + bounded } #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs index 68779a490..4f50aa5d0 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission_tests.rs @@ -5,6 +5,7 @@ use super::admission::{ SccmClientEvidenceAdmissionError, }; use super::{assess_client_intake, SccmClientIntakeArtifact, SccmClientIntakeBundle}; +use crate::parser::ccm::{observe_bounded_scans, CcmBoundedScanObservation}; use crate::sccm::{SccmArtifact, SccmCoverageState, SccmRole, SccmRotation}; fn digest(bytes: &[u8]) -> String { @@ -593,9 +594,9 @@ fn admission_applies_the_integrity_seal_cap_independently_of_retained_memory() { fn admission_enforces_logical_record_cap_across_all_payloads() { let mut artifacts = Vec::new(); let mut payloads = Vec::new(); - for number in 1..=2 { + for (number, record_count) in [(1, 4_095), (2, 4_096)] { let artifact_id = format!("sccm-artifact:v1:sha256:{number:064x}"); - let bytes = repeated_record_bytes(2_049, "x"); + let bytes = repeated_record_bytes(record_count, "x"); artifacts.push(numbered_artifact_bound_to(number, &bytes)); payloads.push(payload_from_bytes(&artifact_id, bytes)); } @@ -606,11 +607,8 @@ fn admission_enforces_logical_record_cap_across_all_payloads() { let assessment = assess_client_intake(&bundle).expect("two captured rotations are canonical intake"); - let result = admit_client_evidence(&bundle, &assessment, &payloads); - assert!( - result.is_err(), - "two individually bounded rotations totaling 4,098 records were admitted" - ); + let (result, observations) = + observe_bounded_scans(|| admit_client_evidence(&bundle, &assessment, &payloads)); let error = admission_error( result, "the logical-record cap must apply across the complete bundle", @@ -619,6 +617,30 @@ fn admission_enforces_logical_record_cap_across_all_payloads() { error.to_string(), "client evidence admission logical record count exceeds the v1 cap" ); + assert_eq!( + observations, + vec![ + CcmBoundedScanObservation { + record_limit: 4_096, + retained_records: 4_095, + record_limit_exceeded: false, + }, + CcmBoundedScanObservation { + record_limit: 1, + retained_records: 1, + record_limit_exceeded: true, + }, + ], + "the second scanner must receive and retain only the aggregate remainder" + ); + assert_eq!( + observations + .iter() + .map(|observation| observation.retained_records) + .sum::(), + 4_096, + "admission must never materialize more than the bundle record cap" + ); } #[test] diff --git a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs index 09dd96bf4..c6328e4c6 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs @@ -686,11 +686,11 @@ fn caller_constructed_policy_profile_cannot_cross_the_admitted_boundary() { }; let generic_result = extract_keys(evidence, &caller_constructed); - assert_eq!(generic_result.keys.len(), 1); - assert_eq!( - generic_result.keys[0].kind, - SccmCorrelationKeyKind::AssignmentId - ); + assert!(generic_result.keys.is_empty()); + assert!(generic_result + .gaps + .iter() + .any(|gap| gap.kind == SccmExtractionGapKind::UnvalidatedProfile)); let admitted_result: SccmClientAdmittedKeyExtraction = admitted .extract_keys_for_artifact("fixture-policy") From 0ff8adf33a0234740123ae5d2450e252576d8a42 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 14:26:11 -0400 Subject: [PATCH 22/27] fix(sccm): reserve family trust for admitted profiles --- .../src/sccm/client/admission.rs | 7 +-- crates/cmtraceopen-parser/src/sccm/keys.rs | 51 +++++++++++++++---- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index 4c9e0e05a..e2a7eddf1 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -23,9 +23,10 @@ use thiserror::Error; use crate::parser::ccm::scan_logical_records_bounded; use crate::sccm::catalog::classify_artifact_name; use crate::sccm::evidence::SccmRawEvidenceSnapshot; +use crate::sccm::keys::extract_keys_from_admitted_profile; use crate::sccm::{ - extract_keys, SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmEvidence, - SccmExtractionProfile, SccmKeyExtractionResult, SccmRole, SccmTimeOrderingState, + SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmEvidence, SccmExtractionProfile, + SccmKeyExtractionResult, SccmRole, SccmTimeOrderingState, }; use super::{ @@ -144,7 +145,7 @@ impl SccmClientAdmittedEvidence { .evidence .iter() .filter(|evidence| evidence.reference.artifact_id == *sealed_artifact_id) - .map(|evidence| extract_keys(evidence, profile)) + .map(|evidence| extract_keys_from_admitted_profile(evidence, profile)) .collect::>(); if results.is_empty() { return Err(SccmClientEvidenceAdmissionError::MissingAdmittedArtifactEvidence); diff --git a/crates/cmtraceopen-parser/src/sccm/keys.rs b/crates/cmtraceopen-parser/src/sccm/keys.rs index dd43d444b..ac70c567b 100644 --- a/crates/cmtraceopen-parser/src/sccm/keys.rs +++ b/crates/cmtraceopen-parser/src/sccm/keys.rs @@ -169,6 +169,27 @@ pub fn normalize_key(kind: SccmCorrelationKeyKind, raw: &str) -> SccmCorrelation pub fn extract_keys( evidence: &SccmEvidence, profile: &SccmExtractionProfile, +) -> SccmKeyExtractionResult { + extract_keys_with_authority(evidence, profile, ExtractionProfileAuthority::Public) +} + +pub(crate) fn extract_keys_from_admitted_profile( + evidence: &SccmEvidence, + profile: &SccmExtractionProfile, +) -> SccmKeyExtractionResult { + extract_keys_with_authority(evidence, profile, ExtractionProfileAuthority::Admitted) +} + +#[derive(Clone, Copy)] +enum ExtractionProfileAuthority { + Public, + Admitted, +} + +fn extract_keys_with_authority( + evidence: &SccmEvidence, + profile: &SccmExtractionProfile, + authority: ExtractionProfileAuthority, ) -> SccmKeyExtractionResult { let candidates = find_candidates(&evidence.message); let mut result = SccmKeyExtractionResult { @@ -177,7 +198,7 @@ pub fn extract_keys( gaps: Vec::new(), }; - if let Some(kind) = profile_gap_kind(profile) { + if let Some(kind) = profile_gap_kind(profile, authority) { if candidates.is_empty() { result.gaps.push(gap_for(kind, profile, evidence, None)); } else { @@ -233,7 +254,10 @@ fn is_bounded_key_value(key: &SccmCorrelationKey) -> bool { && has_at_most_chars(&key.normalized, MAX_SCCM_CORRELATION_KEY_VALUE_CHARS) } -fn profile_gap_kind(profile: &SccmExtractionProfile) -> Option { +fn profile_gap_kind( + profile: &SccmExtractionProfile, + authority: ExtractionProfileAuthority, +) -> Option { if profile.selected_configmgr_version.is_none() { return Some(SccmExtractionGapKind::MissingVersion); } @@ -242,20 +266,29 @@ fn profile_gap_kind(profile: &SccmExtractionProfile) -> Option { Some(SccmExtractionGapKind::UnvalidatedVersion) } - SccmExtractionProfileMaturity::Experimental if is_builtin_experimental(profile) => None, + SccmExtractionProfileMaturity::Experimental + if is_builtin_experimental(profile, authority) => + { + None + } SccmExtractionProfileMaturity::Experimental | SccmExtractionProfileMaturity::Stable => { Some(SccmExtractionGapKind::UnvalidatedProfile) } } } -fn is_builtin_experimental(profile: &SccmExtractionProfile) -> bool { +fn is_builtin_experimental( + profile: &SccmExtractionProfile, + authority: ExtractionProfileAuthority, +) -> bool { is_builtin_experimental_core(profile) - && match profile.validated_artifact_families.as_slice() { - // Preserve the generic public `for_version` contract while the - // admitted-evidence path always seals a catalog family. - [] => true, - [family] => is_validated_builtin_experimental_family(family), + && match (authority, profile.validated_artifact_families.as_slice()) { + // Preserve the generic public `for_version` contract without + // treating a caller-populated family list as admission authority. + (ExtractionProfileAuthority::Public, []) => true, + (ExtractionProfileAuthority::Admitted, [family]) => { + is_validated_builtin_experimental_family(family) + } _ => false, } } From 7d63f88b585a49289dfecb6e2dbe6cfd517a2d90 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 14:26:55 -0400 Subject: [PATCH 23/27] fix(sccm): enforce aggregate CCM record budget --- crates/cmtraceopen-parser/src/parser/ccm.rs | 17 +++++++++-------- .../src/sccm/client/admission.rs | 7 ++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/cmtraceopen-parser/src/parser/ccm.rs b/crates/cmtraceopen-parser/src/parser/ccm.rs index 1e2360472..aa859c502 100644 --- a/crates/cmtraceopen-parser/src/parser/ccm.rs +++ b/crates/cmtraceopen-parser/src/parser/ccm.rs @@ -547,15 +547,16 @@ fn scan_ccm_content( let line_end = line_number_for_offset(&line_starts, full_match.end().saturating_sub(1)); if let Some(parsed) = parse_captures(&caps) { if parsed.public_compatible || mode == CcmScanMode::SccmEvidence { - if !build.record_limit_reached() { - build.records.push(parsed.into_logical_record( - build.id_counter, - line_start, - line_end, - file_path, - )); - build.id_counter += 1; + if build.record_limit_reached() { + return build.finish(); } + build.records.push(parsed.into_logical_record( + build.id_counter, + line_start, + line_end, + file_path, + )); + build.id_counter += 1; } else { push_unmatched_plain( full_match.as_str(), diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index e2a7eddf1..79b72d692 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -364,11 +364,8 @@ pub fn admit_client_evidence( .ok_or(SccmClientEvidenceAdmissionError::UnregisteredProfile)?; let content = decode_payload(payload, fragment.encoding.as_deref())?; let artifact = artifact_for_fragment(fragment); - let scan = scan_logical_records_bounded( - &content, - &fragment.basename, - MAX_SCCM_CLIENT_ADMISSION_LOGICAL_RECORDS, - ); + let scan = + scan_logical_records_bounded(&content, &fragment.basename, remaining_logical_records); if scan.record_limit_exceeded { return Err(SccmClientEvidenceAdmissionError::LogicalRecordLimitExceeded); } From f21ad297391f33217e7239459d64d4d055576229 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 14:31:23 -0400 Subject: [PATCH 24/27] refactor(sccm): confine family trust to sealed admission --- .../src/sccm/client/admission.rs | 16 +++-- crates/cmtraceopen-parser/src/sccm/keys.rs | 58 +++---------------- 2 files changed, 19 insertions(+), 55 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index 79b72d692..8cc34ecc0 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -23,10 +23,9 @@ use thiserror::Error; use crate::parser::ccm::scan_logical_records_bounded; use crate::sccm::catalog::classify_artifact_name; use crate::sccm::evidence::SccmRawEvidenceSnapshot; -use crate::sccm::keys::extract_keys_from_admitted_profile; use crate::sccm::{ - SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmEvidence, SccmExtractionProfile, - SccmKeyExtractionResult, SccmRole, SccmTimeOrderingState, + extract_keys, SccmArtifact, SccmArtifactFamily, SccmCoverageState, SccmEvidence, + SccmExtractionProfile, SccmKeyExtractionResult, SccmRole, SccmTimeOrderingState, }; use super::{ @@ -141,11 +140,20 @@ impl SccmClientAdmittedEvidence { let [artifact_family] = profile.validated_artifact_families.as_slice() else { return Err(SccmClientEvidenceAdmissionError::IntegrityViolation); }; + let mut extraction_profile = profile.clone(); + // Only Policy has executable key-extraction fixtures in the first + // client slice. Clearing this sealed family binding selects the public + // generic extractor without exposing a crate-wide privileged helper; + // the opaque result retains the verified family separately. All other + // families remain explicitly unvalidated. + if matches!(artifact_family, SccmArtifactFamily::ClientPolicy) { + extraction_profile.validated_artifact_families.clear(); + } let results = self .evidence .iter() .filter(|evidence| evidence.reference.artifact_id == *sealed_artifact_id) - .map(|evidence| extract_keys_from_admitted_profile(evidence, profile)) + .map(|evidence| extract_keys(evidence, &extraction_profile)) .collect::>(); if results.is_empty() { return Err(SccmClientEvidenceAdmissionError::MissingAdmittedArtifactEvidence); diff --git a/crates/cmtraceopen-parser/src/sccm/keys.rs b/crates/cmtraceopen-parser/src/sccm/keys.rs index ac70c567b..875c1cf03 100644 --- a/crates/cmtraceopen-parser/src/sccm/keys.rs +++ b/crates/cmtraceopen-parser/src/sccm/keys.rs @@ -169,27 +169,6 @@ pub fn normalize_key(kind: SccmCorrelationKeyKind, raw: &str) -> SccmCorrelation pub fn extract_keys( evidence: &SccmEvidence, profile: &SccmExtractionProfile, -) -> SccmKeyExtractionResult { - extract_keys_with_authority(evidence, profile, ExtractionProfileAuthority::Public) -} - -pub(crate) fn extract_keys_from_admitted_profile( - evidence: &SccmEvidence, - profile: &SccmExtractionProfile, -) -> SccmKeyExtractionResult { - extract_keys_with_authority(evidence, profile, ExtractionProfileAuthority::Admitted) -} - -#[derive(Clone, Copy)] -enum ExtractionProfileAuthority { - Public, - Admitted, -} - -fn extract_keys_with_authority( - evidence: &SccmEvidence, - profile: &SccmExtractionProfile, - authority: ExtractionProfileAuthority, ) -> SccmKeyExtractionResult { let candidates = find_candidates(&evidence.message); let mut result = SccmKeyExtractionResult { @@ -198,7 +177,7 @@ fn extract_keys_with_authority( gaps: Vec::new(), }; - if let Some(kind) = profile_gap_kind(profile, authority) { + if let Some(kind) = profile_gap_kind(profile) { if candidates.is_empty() { result.gaps.push(gap_for(kind, profile, evidence, None)); } else { @@ -254,10 +233,7 @@ fn is_bounded_key_value(key: &SccmCorrelationKey) -> bool { && has_at_most_chars(&key.normalized, MAX_SCCM_CORRELATION_KEY_VALUE_CHARS) } -fn profile_gap_kind( - profile: &SccmExtractionProfile, - authority: ExtractionProfileAuthority, -) -> Option { +fn profile_gap_kind(profile: &SccmExtractionProfile) -> Option { if profile.selected_configmgr_version.is_none() { return Some(SccmExtractionGapKind::MissingVersion); } @@ -266,31 +242,18 @@ fn profile_gap_kind( SccmExtractionProfileMaturity::Unvalidated => { Some(SccmExtractionGapKind::UnvalidatedVersion) } - SccmExtractionProfileMaturity::Experimental - if is_builtin_experimental(profile, authority) => - { - None - } + SccmExtractionProfileMaturity::Experimental if is_builtin_experimental(profile) => None, SccmExtractionProfileMaturity::Experimental | SccmExtractionProfileMaturity::Stable => { Some(SccmExtractionGapKind::UnvalidatedProfile) } } } -fn is_builtin_experimental( - profile: &SccmExtractionProfile, - authority: ExtractionProfileAuthority, -) -> bool { +fn is_builtin_experimental(profile: &SccmExtractionProfile) -> bool { is_builtin_experimental_core(profile) - && match (authority, profile.validated_artifact_families.as_slice()) { - // Preserve the generic public `for_version` contract without - // treating a caller-populated family list as admission authority. - (ExtractionProfileAuthority::Public, []) => true, - (ExtractionProfileAuthority::Admitted, [family]) => { - is_validated_builtin_experimental_family(family) - } - _ => false, - } + // Preserve the generic public `for_version` contract without treating + // a caller-populated family list as admission authority. + && profile.validated_artifact_families.is_empty() } fn is_builtin_experimental_core(profile: &SccmExtractionProfile) -> bool { @@ -306,13 +269,6 @@ fn is_builtin_experimental_core(profile: &SccmExtractionProfile) -> bool { }) } -fn is_validated_builtin_experimental_family(family: &SccmArtifactFamily) -> bool { - // Only Policy has executable key-extraction fixtures in the first client - // implementation slice. Expanding this registry requires those fixtures - // and a review; catalog membership alone is never validation authority. - matches!(family, SccmArtifactFamily::ClientPolicy) -} - fn is_canonical_configmgr_version(version: &str) -> bool { let mut component_count = 0; for component in version.split('.') { From b199a53d92b68b6701ab3e679856ebc9499a035b Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 14:32:01 -0400 Subject: [PATCH 25/27] test(ccm): expose observer cleanup after panic --- crates/cmtraceopen-parser/src/parser/ccm.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/cmtraceopen-parser/src/parser/ccm.rs b/crates/cmtraceopen-parser/src/parser/ccm.rs index aa859c502..2bf703dd6 100644 --- a/crates/cmtraceopen-parser/src/parser/ccm.rs +++ b/crates/cmtraceopen-parser/src/parser/ccm.rs @@ -935,6 +935,17 @@ pub fn parse_lines(lines: &[&str], file_path: &str) -> (Vec, u32) { mod tests { use super::*; + #[test] + fn bounded_scan_observer_clears_after_a_panicking_operation() { + let panic = std::panic::catch_unwind(|| { + let _ = observe_bounded_scans(|| panic!("expected observer probe panic")); + }); + assert!(panic.is_err()); + + let (_, observations) = observe_bounded_scans(|| ()); + assert!(observations.is_empty()); + } + #[test] fn test_parse_ccm_line() { let line = r#""#; From f6c458adba054bdba6e3923aae8526332fafa026 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 14:32:31 -0400 Subject: [PATCH 26/27] test(ccm): reset bounded scan observation on unwind --- crates/cmtraceopen-parser/src/parser/ccm.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/cmtraceopen-parser/src/parser/ccm.rs b/crates/cmtraceopen-parser/src/parser/ccm.rs index 2bf703dd6..20f6cbc02 100644 --- a/crates/cmtraceopen-parser/src/parser/ccm.rs +++ b/crates/cmtraceopen-parser/src/parser/ccm.rs @@ -387,6 +387,18 @@ thread_local! { const { RefCell::new(None) }; } +#[cfg(test)] +struct BoundedScanObservationGuard; + +#[cfg(test)] +impl Drop for BoundedScanObservationGuard { + fn drop(&mut self) { + BOUNDED_SCAN_OBSERVATIONS.with(|observations| { + observations.borrow_mut().take(); + }); + } +} + #[cfg(test)] pub(crate) fn observe_bounded_scans( operation: impl FnOnce() -> T, @@ -397,6 +409,7 @@ pub(crate) fn observe_bounded_scans( "bounded scan observation cannot be nested" ); }); + let _cleanup = BoundedScanObservationGuard; let output = operation(); let observations = BOUNDED_SCAN_OBSERVATIONS.with(|observations| { observations From 05f5d5ed2138042871f0bebc3b9f081ffb660e99 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 15:09:54 -0400 Subject: [PATCH 27/27] fix(sccm): isolate missing encoding admission gaps --- .../src/sccm/client/admission.rs | 13 +++++- .../sccm/client/authority_contract_tests.rs | 43 +++++++++++++++++++ .../src/sccm/client/intake.rs | 2 +- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/client/admission.rs b/crates/cmtraceopen-parser/src/sccm/client/admission.rs index 8cc34ecc0..b30ca9ae1 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/admission.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/admission.rs @@ -30,7 +30,7 @@ use crate::sccm::{ use super::{ assess_client_intake, - intake::{is_safe_artifact_id, source_matches_group}, + intake::{is_safe_artifact_id, is_supported_encoding, source_matches_group}, SccmClientIntakeAssessment, SccmClientIntakeBundle, SccmClientIntakeError, SccmClientIntakeFragment, MAX_SCCM_CLIENT_INTAKE_ARTIFACTS, }; @@ -309,6 +309,9 @@ pub fn admit_client_evidence( unbound_complete_captures.insert(fragment.artifact_id.as_str()); continue; } + if !has_supported_payload_encoding(fragment) { + continue; + } eligible.insert(fragment.artifact_id.clone(), (fragment, classified.family)); } let admitted_source_groups = canonical @@ -468,6 +471,14 @@ fn is_bound_complete_capture(fragment: &SccmClientIntakeFragment) -> bool { && fragment.fragment_complete == Some(true) && fragment.declared_byte_length.is_some() && fragment.content_sha256.is_some() + && has_supported_payload_encoding(fragment) +} + +fn has_supported_payload_encoding(fragment: &SccmClientIntakeFragment) -> bool { + fragment + .encoding + .as_deref() + .is_some_and(is_supported_encoding) } fn validate_payload( diff --git a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs index c6328e4c6..7e81399c2 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/authority_contract_tests.rs @@ -409,6 +409,49 @@ fn incomplete_and_capped_sources_fail_locally_without_blocking_bound_policy() { assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); } +#[test] +fn missing_fragment_encoding_is_a_local_admission_gap() { + let policy_bytes = ccm_bytes("policy evidence"); + let content_bytes = ccm_bytes("content evidence without decoding provenance"); + let mut content = artifact( + "content-missing", + "CAS.log", + SccmCoverageState::Captured, + true, + Some(&content_bytes), + ); + content.artifact.encoding = None; + let bundle = bundle_with(vec![ + artifact( + "policy", + "PolicyAgent.log", + SccmCoverageState::Captured, + true, + Some(&policy_bytes), + ), + content, + ]); + let assessment = assess_client_intake(&bundle) + .expect("missing decoding provenance remains an assessable local coverage gap"); + + let admitted = admit_client_evidence( + &bundle, + &assessment, + &[payload("fixture-policy", policy_bytes)], + ) + .expect("a fragment without decoding provenance must not block unrelated bound evidence"); + + assert!(admitted + .require_captured_source("client-policy-agent") + .is_ok()); + assert_eq!( + admitted.require_captured_source("client-content"), + Err(SccmClientEvidenceAdmissionError::SourceCoverageUnavailable), + "the un-decodable source remains a local admission gap" + ); + assert_eq!(admitted.evidence().expect("valid authority seal").len(), 1); +} + #[test] fn admitted_profile_is_bound_to_the_catalogued_source_family() { let bytes = ccm_bytes("policy evidence"); diff --git a/crates/cmtraceopen-parser/src/sccm/client/intake.rs b/crates/cmtraceopen-parser/src/sccm/client/intake.rs index ae4c116cd..749102e33 100644 --- a/crates/cmtraceopen-parser/src/sccm/client/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/client/intake.rs @@ -1829,7 +1829,7 @@ fn is_four_ascii_digits(value: &str) -> bool { value.len() == 4 && value.bytes().all(|byte| byte.is_ascii_digit()) } -fn is_supported_encoding(value: &str) -> bool { +pub(super) fn is_supported_encoding(value: &str) -> bool { matches!(value, "utf-8" | "utf-16le" | "utf-16be" | "windows-1252") }