From e3020c0798a730da554ff5ab74f5a86d2f39b492 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 07:27:03 -0400 Subject: [PATCH 01/18] test(sccm): define bounded client discovery contract --- src-tauri/Cargo.toml | 4 + src-tauri/tests/sccm_client_discovery.rs | 166 +++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 src-tauri/tests/sccm_client_discovery.rs diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cc40f7e80..7be2c6242 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -135,6 +135,10 @@ required-features = ["sysmon"] name = "sccm_client_manifest" required-features = ["sccm-diagnostics"] +[[test]] +name = "sccm_client_discovery" +required-features = ["sccm-diagnostics"] + [[bench]] name = "intune_pipeline" harness = false diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs new file mode 100644 index 000000000..cbf5785aa --- /dev/null +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -0,0 +1,166 @@ +use app_lib::sccm::{ + discover_client_sources, SccmClientDiscoveryInput, SccmClientDiscoveryObservation, + SccmClientDiscoveryObservationState, SccmClientDiscoveryState, + MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, +}; +use cmtraceopen_parser::sccm::SccmRotation; + +const ROOT_A: &str = "root-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ROOT_B: &str = "root-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + +fn observation( + root_handle: &str, + basename: &str, + rotation: SccmRotation, + state: SccmClientDiscoveryObservationState, +) -> SccmClientDiscoveryObservation { + SccmClientDiscoveryObservation { + root_handle: root_handle.to_owned(), + basename: basename.to_owned(), + rotation, + state, + } +} + +#[test] +fn discovery_uses_one_global_declaration_budget_and_marks_the_first_omitted_rotation() { + let mut observations = Vec::new(); + for number in 1..=2_048 { + observations.push(observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Numbered(number), + SccmClientDiscoveryObservationState::Found, + )); + observations.push(observation( + ROOT_B, + "PolicyAgent.log", + SccmRotation::Numbered(number), + SccmClientDiscoveryObservationState::Found, + )); + } + + let result = discover_client_sources(&SccmClientDiscoveryInput { + max_declarations_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + observations, + }); + + assert_eq!( + result.declarations.len(), + MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + "the 4096 declaration budget must be shared by all roots and sources" + ); + let gap = result + .declarations + .last() + .expect("the globally capped result retains the first omitted declaration"); + assert_eq!(gap.basename, "PolicyAgent.log"); + assert_eq!(gap.rotation, SccmRotation::Numbered(2_048)); + assert_eq!(gap.state, SccmClientDiscoveryState::Capped); +} + +#[test] +fn discovery_enforces_each_source_cap_and_retains_the_first_omitted_rotation_gap() { + let result = discover_client_sources(&SccmClientDiscoveryInput { + max_declarations_per_source: 2, + observations: vec![ + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Numbered(2), + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "AppEnforce.lo_", + SccmRotation::LoUnderscore, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + ], + }); + + assert_eq!( + result + .declarations + .iter() + .map(|declaration| (&declaration.rotation, declaration.state)) + .collect::>(), + vec![ + (&SccmRotation::Current, SccmClientDiscoveryState::Discovered), + (&SccmRotation::LoUnderscore, SccmClientDiscoveryState::Discovered), + (&SccmRotation::Numbered(2), SccmClientDiscoveryState::Capped), + ] + ); +} + +#[test] +fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_identities() { + let input = SccmClientDiscoveryInput { + max_declarations_per_source: 8, + observations: vec![ + observation( + ROOT_B, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "CIAgent.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::AccessDenied, + ), + observation( + ROOT_B, + "ScanAgent.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::NotFound, + ), + ], + }; + let result = discover_client_sources(&input); + let reversed = discover_client_sources(&SccmClientDiscoveryInput { + max_declarations_per_source: input.max_declarations_per_source, + observations: input.observations.into_iter().rev().collect(), + }); + + assert_eq!(result.declarations, reversed.declarations); + assert!(result + .declarations + .iter() + .any(|declaration| declaration.state == SccmClientDiscoveryState::AccessDenied)); + assert!(result + .declarations + .iter() + .any(|declaration| declaration.state == SccmClientDiscoveryState::NotFound)); + + let collisions = result + .declarations + .iter() + .filter(|declaration| { + declaration.basename == "AppEnforce.log" + && declaration.rotation == SccmRotation::Current + }) + .collect::>(); + assert_eq!(collisions.len(), 2); + assert_ne!(collisions[0].artifact_id, collisions[1].artifact_id); + assert_ne!(collisions[0].evidence_identity, collisions[1].evidence_identity); + assert_ne!(collisions[0].path_fingerprint, collisions[1].path_fingerprint); + assert!(result.declarations.iter().all(|declaration| { + !declaration.artifact_id.contains("C:\\\\") + && !declaration.evidence_identity.contains("C:\\\\") + && !declaration.path_fingerprint.contains("C:\\\\") + })); +} From 9944d9a4309e3074760bb9cd5f92abb80a3ce1bf Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 07:29:52 -0400 Subject: [PATCH 02/18] test(sccm): pin discovery budget and privacy boundaries --- src-tauri/tests/sccm_client_discovery.rs | 201 +++++++++++++++++++++-- 1 file changed, 187 insertions(+), 14 deletions(-) diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index cbf5785aa..9913b4d43 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -4,6 +4,7 @@ use app_lib::sccm::{ MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, }; use cmtraceopen_parser::sccm::SccmRotation; +use sha2::{Digest, Sha256}; const ROOT_A: &str = "root-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const ROOT_B: &str = "root-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; @@ -22,26 +23,95 @@ fn observation( } } +fn sha256(value: impl AsRef<[u8]>) -> String { + Sha256::digest(value) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn path_fingerprint(root_handle: &str, canonical_basename: &str) -> String { + let root_digest = root_handle + .strip_prefix("root-") + .expect("synthetic root handle has the required prefix"); + format!( + "sha256:{}", + sha256(format!( + "cmtraceopen.sccm.source.v1\0{root_digest}\0{canonical_basename}" + )) + ) +} + +fn rotation_segment(rotation: &SccmRotation) -> String { + match rotation { + SccmRotation::Current => "current".to_owned(), + SccmRotation::LoUnderscore => "lo".to_owned(), + SccmRotation::Numbered(number) => format!("numbered-{number}"), + SccmRotation::Timestamped(timestamp) => format!("timestamped-{timestamp}"), + SccmRotation::Unknown(_) => panic!("synthetic observations use known rotations"), + } +} + +fn expected_physical_artifact_id( + fingerprint: &str, + rotation: &SccmRotation, + basename: &str, +) -> String { + format!( + "sccm-artifact:v1:sha256:{}", + sha256(format!( + "artifact:v1:{fingerprint}:{}:{basename}", + rotation_segment(rotation) + )) + ) +} + +fn expected_marker_artifact_id( + canonical_basename: &str, + state: &str, + fingerprint: &str, + rotation: &SccmRotation, + basename: &str, +) -> String { + let catalog_entry_id = format!( + "sccm-client-source:v1:sha256:{}", + sha256(canonical_basename) + ); + format!( + "sccm-artifact:v1:sha256:{}", + sha256(format!( + "marker:v1:{catalog_entry_id}:{state}:{}:{basename}:{fingerprint}", + rotation_segment(rotation) + )) + ) +} + #[test] fn discovery_uses_one_global_declaration_budget_and_marks_the_first_omitted_rotation() { let mut observations = Vec::new(); for number in 1..=2_048 { observations.push(observation( ROOT_A, - "AppEnforce.log", + &format!("AppEnforce.log.{number}"), SccmRotation::Numbered(number), SccmClientDiscoveryObservationState::Found, )); observations.push(observation( ROOT_B, - "PolicyAgent.log", + &format!("PolicyAgent.log.{number}"), SccmRotation::Numbered(number), SccmClientDiscoveryObservationState::Found, )); } + observations.push(observation( + ROOT_B, + "PolicyAgent.log.2049", + SccmRotation::Numbered(2_049), + SccmClientDiscoveryObservationState::Found, + )); let result = discover_client_sources(&SccmClientDiscoveryInput { - max_declarations_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, observations, }); @@ -54,19 +124,55 @@ fn discovery_uses_one_global_declaration_budget_and_marks_the_first_omitted_rota .declarations .last() .expect("the globally capped result retains the first omitted declaration"); - assert_eq!(gap.basename, "PolicyAgent.log"); + assert_eq!(gap.basename, "PolicyAgent.log.2048"); assert_eq!(gap.rotation, SccmRotation::Numbered(2_048)); assert_eq!(gap.state, SccmClientDiscoveryState::Capped); + assert_eq!( + result + .declarations + .iter() + .filter(|declaration| declaration.state == SccmClientDiscoveryState::Discovered) + .count(), + MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS - 1 + ); +} + +#[test] +fn discovery_at_the_exact_global_boundary_does_not_manufacture_a_gap() { + let observations = (1..=MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS as u32) + .map(|number| { + observation( + ROOT_A, + &format!("AppEnforce.log.{number}"), + SccmRotation::Numbered(number), + SccmClientDiscoveryObservationState::Found, + ) + }) + .collect(); + + let result = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + observations, + }); + + assert_eq!( + result.declarations.len(), + MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS + ); + assert!(result + .declarations + .iter() + .all(|declaration| declaration.state == SccmClientDiscoveryState::Discovered)); } #[test] fn discovery_enforces_each_source_cap_and_retains_the_first_omitted_rotation_gap() { let result = discover_client_sources(&SccmClientDiscoveryInput { - max_declarations_per_source: 2, + max_found_fragments_per_source: 2, observations: vec![ observation( ROOT_A, - "AppEnforce.log", + "AppEnforce.log.2", SccmRotation::Numbered(2), SccmClientDiscoveryObservationState::Found, ), @@ -93,16 +199,34 @@ fn discovery_enforces_each_source_cap_and_retains_the_first_omitted_rotation_gap .collect::>(), vec![ (&SccmRotation::Current, SccmClientDiscoveryState::Discovered), - (&SccmRotation::LoUnderscore, SccmClientDiscoveryState::Discovered), + ( + &SccmRotation::LoUnderscore, + SccmClientDiscoveryState::Discovered + ), (&SccmRotation::Numbered(2), SccmClientDiscoveryState::Capped), ] ); + let fingerprint = path_fingerprint(ROOT_A, "AppEnforce.log"); + assert_eq!( + result.declarations[0].artifact_id, + expected_physical_artifact_id(&fingerprint, &SccmRotation::Current, "AppEnforce.log") + ); + assert_eq!( + result.declarations[2].artifact_id, + expected_marker_artifact_id( + "AppEnforce.log", + "capped", + &fingerprint, + &SccmRotation::Numbered(2), + "AppEnforce.log.2", + ) + ); } #[test] fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_identities() { let input = SccmClientDiscoveryInput { - max_declarations_per_source: 8, + max_found_fragments_per_source: 8, observations: vec![ observation( ROOT_B, @@ -132,7 +256,7 @@ fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_ }; let result = discover_client_sources(&input); let reversed = discover_client_sources(&SccmClientDiscoveryInput { - max_declarations_per_source: input.max_declarations_per_source, + max_found_fragments_per_source: input.max_found_fragments_per_source, observations: input.observations.into_iter().rev().collect(), }); @@ -156,11 +280,60 @@ fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_ .collect::>(); assert_eq!(collisions.len(), 2); assert_ne!(collisions[0].artifact_id, collisions[1].artifact_id); - assert_ne!(collisions[0].evidence_identity, collisions[1].evidence_identity); - assert_ne!(collisions[0].path_fingerprint, collisions[1].path_fingerprint); + assert_ne!( + collisions[0].evidence_identity, + collisions[1].evidence_identity + ); + assert_ne!( + collisions[0].path_fingerprint, + collisions[1].path_fingerprint + ); + for collision in collisions { + assert_eq!( + collision.artifact_id, + expected_physical_artifact_id( + &path_fingerprint(&collision.root_handle, "AppEnforce.log"), + &SccmRotation::Current, + "AppEnforce.log", + ) + ); + } + let denied = result + .declarations + .iter() + .find(|declaration| declaration.state == SccmClientDiscoveryState::AccessDenied) + .expect("access-denied observation remains explicit"); + assert_eq!( + denied.artifact_id, + expected_marker_artifact_id( + "CIAgent.log", + "accessDenied", + &path_fingerprint(ROOT_A, "CIAgent.log"), + &SccmRotation::Current, + "CIAgent.log", + ) + ); + let missing = result + .declarations + .iter() + .find(|declaration| declaration.state == SccmClientDiscoveryState::NotFound) + .expect("not-found observation remains explicit"); + assert_eq!( + missing.artifact_id, + expected_marker_artifact_id( + "ScanAgent.log", + "absent", + &path_fingerprint(ROOT_B, "ScanAgent.log"), + &SccmRotation::Current, + "ScanAgent.log", + ) + ); assert!(result.declarations.iter().all(|declaration| { - !declaration.artifact_id.contains("C:\\\\") - && !declaration.evidence_identity.contains("C:\\\\") - && !declaration.path_fingerprint.contains("C:\\\\") + !declaration.artifact_id.contains(ROOT_A) + && !declaration.artifact_id.contains(ROOT_B) + && !declaration.evidence_identity.contains(ROOT_A) + && !declaration.evidence_identity.contains(ROOT_B) + && !declaration.path_fingerprint.contains(ROOT_A) + && !declaration.path_fingerprint.contains(ROOT_B) })); } From 52e075f54d8f6725c87ac5ad32889c142d3ac65d Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 07:41:46 -0400 Subject: [PATCH 03/18] feat(sccm): normalize bounded client discovery --- src-tauri/src/sccm/discovery.rs | 244 ++++++++++++++++++++++++++++++++ src-tauri/src/sccm/mod.rs | 2 + 2 files changed, 246 insertions(+) create mode 100644 src-tauri/src/sccm/discovery.rs diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs new file mode 100644 index 000000000..cedbb917e --- /dev/null +++ b/src-tauri/src/sccm/discovery.rs @@ -0,0 +1,244 @@ +//! Read-only normalization of already-observed SCCM client source candidates. + +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet}; + +use cmtraceopen_parser::sccm::SccmRotation; +use sha2::{Digest, Sha256}; + +use super::contract::{ + canonical_client_source, catalog_entry_id, expected_marker_artifact_id, + expected_physical_artifact_id, logical_artifact_ids_for_basename, rotation_order, + rotation_segment, source_identity_digest, SccmManifestSourceState, +}; + +pub const MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS: usize = 4_096; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SccmClientDiscoveryObservationState { + Found, + AccessDenied, + NotFound, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SccmClientDiscoveryState { + Discovered, + AccessDenied, + NotFound, + Capped, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SccmClientDiscoveryObservation { + /// A privacy-classified root handle, never a native path. + pub root_handle: String, + pub basename: String, + pub rotation: SccmRotation, + pub state: SccmClientDiscoveryObservationState, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SccmClientDiscoveryInput { + /// Physical found-fragment cap for one root/source lineage. + pub max_found_fragments_per_source: usize, + pub observations: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SccmClientDiscoveryDeclaration { + pub catalog_entry_id: String, + pub logical_artifact_ids: Vec, + pub artifact_id: String, + pub evidence_identity: String, + pub path_fingerprint: String, + pub root_handle: String, + pub basename: String, + pub rotation: SccmRotation, + pub state: SccmClientDiscoveryState, +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct SccmClientDiscoveryResult { + pub declarations: Vec, +} + +struct Candidate { + observation: SccmClientDiscoveryObservation, + catalog_entry_id: String, + logical_artifact_ids: Vec, + source_digest: String, +} + +pub fn discover_client_sources(input: &SccmClientDiscoveryInput) -> SccmClientDiscoveryResult { + let mut candidates = input + .observations + .iter() + .filter_map(candidate_from_observation) + .collect::>(); + candidates.sort_by(compare_candidates); + + let mut found_per_source = BTreeMap::::new(); + let mut capped_sources = BTreeSet::::new(); + let mut declarations = Vec::new(); + for candidate in candidates { + let source_key = format!( + "{}:{}", + candidate.observation.root_handle, candidate.source_digest + ); + let state = match candidate.observation.state { + SccmClientDiscoveryObservationState::Found => { + let count = found_per_source.entry(source_key.clone()).or_default(); + if *count < input.max_found_fragments_per_source { + *count += 1; + SccmClientDiscoveryState::Discovered + } else if capped_sources.insert(source_key) { + SccmClientDiscoveryState::Capped + } else { + continue; + } + } + SccmClientDiscoveryObservationState::AccessDenied => { + SccmClientDiscoveryState::AccessDenied + } + SccmClientDiscoveryObservationState::NotFound => SccmClientDiscoveryState::NotFound, + }; + declarations.push(declaration_from_candidate(candidate, state)); + } + + if declarations.len() > MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS { + let first_omitted = declarations[MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS - 1].clone(); + declarations.truncate(MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS - 1); + declarations.push(SccmClientDiscoveryDeclaration { + state: SccmClientDiscoveryState::Capped, + artifact_id: marker_id( + &first_omitted.catalog_entry_id, + SccmManifestSourceState::Capped, + &first_omitted.rotation, + &first_omitted.basename, + &first_omitted.path_fingerprint, + ), + ..first_omitted + }); + } + SccmClientDiscoveryResult { declarations } +} + +fn candidate_from_observation(observation: &SccmClientDiscoveryObservation) -> Option { + let canonical = canonical_client_source(&observation.basename, &observation.rotation)?; + let source_digest = source_identity_digest(&observation.root_handle, &canonical)?; + Some(Candidate { + observation: observation.clone(), + catalog_entry_id: catalog_entry_id(&canonical), + logical_artifact_ids: logical_artifact_ids_for_basename(&canonical), + source_digest, + }) +} + +fn declaration_from_candidate( + candidate: Candidate, + state: SccmClientDiscoveryState, +) -> SccmClientDiscoveryDeclaration { + let path_fingerprint = format!("sha256:{}", candidate.source_digest); + let artifact_id = match state { + SccmClientDiscoveryState::Discovered => expected_physical_artifact_id( + &path_fingerprint, + &candidate.observation.rotation, + &candidate.observation.basename, + ), + SccmClientDiscoveryState::AccessDenied => marker_id( + &candidate.catalog_entry_id, + SccmManifestSourceState::AccessDenied, + &candidate.observation.rotation, + &candidate.observation.basename, + &path_fingerprint, + ), + SccmClientDiscoveryState::NotFound => marker_id( + &candidate.catalog_entry_id, + SccmManifestSourceState::Absent, + &candidate.observation.rotation, + &candidate.observation.basename, + &path_fingerprint, + ), + SccmClientDiscoveryState::Capped => marker_id( + &candidate.catalog_entry_id, + SccmManifestSourceState::Capped, + &candidate.observation.rotation, + &candidate.observation.basename, + &path_fingerprint, + ), + }; + SccmClientDiscoveryDeclaration { + evidence_identity: evidence_id( + &candidate.catalog_entry_id, + &candidate.source_digest, + &candidate.observation.rotation, + &candidate.observation.basename, + ), + catalog_entry_id: candidate.catalog_entry_id, + logical_artifact_ids: candidate.logical_artifact_ids, + artifact_id, + path_fingerprint, + root_handle: candidate.observation.root_handle, + basename: candidate.observation.basename, + rotation: candidate.observation.rotation, + state, + } +} + +fn marker_id( + catalog_entry_id: &str, + state: SccmManifestSourceState, + rotation: &SccmRotation, + basename: &str, + path_fingerprint: &str, +) -> String { + expected_marker_artifact_id( + catalog_entry_id, + state, + rotation, + basename, + Some(path_fingerprint), + ) +} + +fn evidence_id( + catalog_entry_id: &str, + source_digest: &str, + rotation: &SccmRotation, + basename: &str, +) -> String { + let value = format!( + "cmtraceopen.sccm.evidence.v1\0{catalog_entry_id}\0{source_digest}\0{}\0{basename}", + rotation_segment(rotation) + ); + let digest = Sha256::digest(value.as_bytes()); + format!( + "sccm-evidence:v1:sha256:{}", + digest + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() + ) +} + +fn compare_candidates(left: &Candidate, right: &Candidate) -> Ordering { + left.logical_artifact_ids + .cmp(&right.logical_artifact_ids) + .then_with(|| { + left.observation + .root_handle + .cmp(&right.observation.root_handle) + }) + .then_with(|| rotation_order(&left.observation.rotation, &right.observation.rotation)) + .then_with(|| left.observation.basename.cmp(&right.observation.basename)) + .then_with(|| state_rank(left.observation.state).cmp(&state_rank(right.observation.state))) +} + +fn state_rank(state: SccmClientDiscoveryObservationState) -> u8 { + match state { + SccmClientDiscoveryObservationState::Found => 0, + SccmClientDiscoveryObservationState::AccessDenied => 1, + SccmClientDiscoveryObservationState::NotFound => 2, + } +} diff --git a/src-tauri/src/sccm/mod.rs b/src-tauri/src/sccm/mod.rs index 9cd43a25a..335a74986 100644 --- a/src-tauri/src/sccm/mod.rs +++ b/src-tauri/src/sccm/mod.rs @@ -5,9 +5,11 @@ //! pure contracts without changing the generic collection manifest. mod contract; +mod discovery; mod manifest; mod private_fs; pub use cmtraceopen_parser::sccm::{SccmCoverageState, SccmRole, SccmRotation}; pub use contract::*; +pub use discovery::*; pub use manifest::*; From 3991611507c14d907e53e18037ca94a841820467 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 07:56:36 -0400 Subject: [PATCH 04/18] test(sccm): expose discovery bound and conflict gaps --- src-tauri/src/sccm/discovery.rs | 98 ++++++++++++++++++++++++ src-tauri/tests/sccm_client_discovery.rs | 56 ++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index cedbb917e..8abd3211c 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -3,6 +3,9 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet}; +#[cfg(test)] +use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; + use cmtraceopen_parser::sccm::SccmRotation; use sha2::{Digest, Sha256}; @@ -70,6 +73,12 @@ struct Candidate { source_digest: String, } +#[cfg(test)] +static CANDIDATE_CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); + +#[cfg(test)] +static DECLARATION_CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); + pub fn discover_client_sources(input: &SccmClientDiscoveryInput) -> SccmClientDiscoveryResult { let mut candidates = input .observations @@ -127,6 +136,9 @@ pub fn discover_client_sources(input: &SccmClientDiscoveryInput) -> SccmClientDi fn candidate_from_observation(observation: &SccmClientDiscoveryObservation) -> Option { let canonical = canonical_client_source(&observation.basename, &observation.rotation)?; let source_digest = source_identity_digest(&observation.root_handle, &canonical)?; + #[cfg(test)] + CANDIDATE_CONSTRUCTIONS.fetch_add(1, AtomicOrdering::Relaxed); + Some(Candidate { observation: observation.clone(), catalog_entry_id: catalog_entry_id(&canonical), @@ -139,6 +151,9 @@ fn declaration_from_candidate( candidate: Candidate, state: SccmClientDiscoveryState, ) -> SccmClientDiscoveryDeclaration { + #[cfg(test)] + DECLARATION_CONSTRUCTIONS.fetch_add(1, AtomicOrdering::Relaxed); + let path_fingerprint = format!("sha256:{}", candidate.source_digest); let artifact_id = match state { SccmClientDiscoveryState::Discovered => expected_physical_artifact_id( @@ -242,3 +257,86 @@ fn state_rank(state: SccmClientDiscoveryObservationState) -> u8 { SccmClientDiscoveryObservationState::NotFound => 2, } } + +#[cfg(test)] +mod tests { + use super::*; + + const ROOT_A: &str = "root-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const ROOT_B: &str = "root-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + + fn observation( + root_handle: &str, + basename: String, + rotation: SccmRotation, + ) -> SccmClientDiscoveryObservation { + SccmClientDiscoveryObservation { + root_handle: root_handle.to_owned(), + basename, + rotation, + state: SccmClientDiscoveryObservationState::Found, + } + } + + fn construction_counts() -> (usize, usize) { + ( + CANDIDATE_CONSTRUCTIONS.load(AtomicOrdering::Relaxed), + DECLARATION_CONSTRUCTIONS.load(AtomicOrdering::Relaxed), + ) + } + + fn reset_construction_counts() { + CANDIDATE_CONSTRUCTIONS.store(0, AtomicOrdering::Relaxed); + DECLARATION_CONSTRUCTIONS.store(0, AtomicOrdering::Relaxed); + } + + #[test] + fn oversized_discovery_constructs_only_the_bounded_retained_candidates_and_declarations() { + let mut observations = Vec::new(); + for number in 1..=6_000 { + observations.push(observation( + ROOT_A, + format!("AppEnforce.log.{number}"), + SccmRotation::Numbered(number), + )); + observations.push(observation( + ROOT_B, + format!("PolicyAgent.log.{number}"), + SccmRotation::Numbered(number), + )); + } + let input = SccmClientDiscoveryInput { + max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + observations, + }; + + reset_construction_counts(); + let result = discover_client_sources(&input); + let counts = construction_counts(); + + let mut reversed = input.clone(); + reversed.observations.reverse(); + reset_construction_counts(); + let reversed_result = discover_client_sources(&reversed); + let reversed_counts = construction_counts(); + + assert_eq!( + result, reversed_result, + "input order must not change the result" + ); + assert!( + result.declarations.len() <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + "the global result remains bounded" + ); + for (candidate_constructions, declaration_constructions) in [counts, reversed_counts] { + assert!( + candidate_constructions <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + "candidate construction must stop at the retained global budget" + ); + assert!( + declaration_constructions <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + "declaration and identity construction must stop at the retained global budget" + ); + } + } +} diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index 9913b4d43..c757907f2 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -337,3 +337,59 @@ fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_ && !declaration.path_fingerprint.contains(ROOT_B) })); } + +#[test] +fn discovery_coalesces_exact_duplicate_observations_without_spending_global_quota() { + let duplicate = observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ); + let result = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: 1, + observations: vec![duplicate.clone(), duplicate], + }); + + assert_eq!(result.declarations.len(), 1); + assert_eq!( + result.declarations[0].state, + SccmClientDiscoveryState::Discovered + ); +} + +#[test] +fn discovery_rejects_conflicting_states_for_one_canonical_physical_source() { + let input = SccmClientDiscoveryInput { + max_found_fragments_per_source: 8, + observations: vec![ + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::AccessDenied, + ), + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::NotFound, + ), + ], + }; + + let error = + discover_client_sources(&input).expect_err("contradictory physical evidence fails closed"); + let mut reversed = input; + reversed.observations.reverse(); + let reversed_error = discover_client_sources(&reversed) + .expect_err("contradictory physical evidence fails closed regardless of order"); + + assert_eq!(error, reversed_error); +} From a213e8a827a9f47f9800aea169c09dfedcae2a6b Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:02:43 -0400 Subject: [PATCH 05/18] test(sccm): isolate discovery construction probes --- src-tauri/src/sccm/discovery.rs | 28 ++++++++++++------------ src-tauri/tests/sccm_client_discovery.rs | 17 +++++++++----- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 8abd3211c..fde961356 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -4,7 +4,7 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet}; #[cfg(test)] -use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; +use std::cell::Cell; use cmtraceopen_parser::sccm::SccmRotation; use sha2::{Digest, Sha256}; @@ -74,10 +74,10 @@ struct Candidate { } #[cfg(test)] -static CANDIDATE_CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); - -#[cfg(test)] -static DECLARATION_CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); +thread_local! { + static CANDIDATE_CONSTRUCTIONS: Cell = const { Cell::new(0) }; + static DECLARATION_CONSTRUCTIONS: Cell = const { Cell::new(0) }; +} pub fn discover_client_sources(input: &SccmClientDiscoveryInput) -> SccmClientDiscoveryResult { let mut candidates = input @@ -135,9 +135,9 @@ pub fn discover_client_sources(input: &SccmClientDiscoveryInput) -> SccmClientDi fn candidate_from_observation(observation: &SccmClientDiscoveryObservation) -> Option { let canonical = canonical_client_source(&observation.basename, &observation.rotation)?; - let source_digest = source_identity_digest(&observation.root_handle, &canonical)?; #[cfg(test)] - CANDIDATE_CONSTRUCTIONS.fetch_add(1, AtomicOrdering::Relaxed); + CANDIDATE_CONSTRUCTIONS.with(|count| count.set(count.get() + 1)); + let source_digest = source_identity_digest(&observation.root_handle, &canonical)?; Some(Candidate { observation: observation.clone(), @@ -152,7 +152,7 @@ fn declaration_from_candidate( state: SccmClientDiscoveryState, ) -> SccmClientDiscoveryDeclaration { #[cfg(test)] - DECLARATION_CONSTRUCTIONS.fetch_add(1, AtomicOrdering::Relaxed); + DECLARATION_CONSTRUCTIONS.with(|count| count.set(count.get() + 1)); let path_fingerprint = format!("sha256:{}", candidate.source_digest); let artifact_id = match state { @@ -280,14 +280,14 @@ mod tests { fn construction_counts() -> (usize, usize) { ( - CANDIDATE_CONSTRUCTIONS.load(AtomicOrdering::Relaxed), - DECLARATION_CONSTRUCTIONS.load(AtomicOrdering::Relaxed), + CANDIDATE_CONSTRUCTIONS.with(Cell::get), + DECLARATION_CONSTRUCTIONS.with(Cell::get), ) } fn reset_construction_counts() { - CANDIDATE_CONSTRUCTIONS.store(0, AtomicOrdering::Relaxed); - DECLARATION_CONSTRUCTIONS.store(0, AtomicOrdering::Relaxed); + CANDIDATE_CONSTRUCTIONS.with(|count| count.set(0)); + DECLARATION_CONSTRUCTIONS.with(|count| count.set(0)); } #[test] @@ -311,13 +311,13 @@ mod tests { }; reset_construction_counts(); - let result = discover_client_sources(&input); + let result = discover_client_sources(&input).expect("valid observations"); let counts = construction_counts(); let mut reversed = input.clone(); reversed.observations.reverse(); reset_construction_counts(); - let reversed_result = discover_client_sources(&reversed); + let reversed_result = discover_client_sources(&reversed).expect("valid observations"); let reversed_counts = construction_counts(); assert_eq!( diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index c757907f2..716cf62c4 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -113,7 +113,8 @@ fn discovery_uses_one_global_declaration_budget_and_marks_the_first_omitted_rota let result = discover_client_sources(&SccmClientDiscoveryInput { max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, observations, - }); + }) + .expect("valid observations"); assert_eq!( result.declarations.len(), @@ -153,7 +154,8 @@ fn discovery_at_the_exact_global_boundary_does_not_manufacture_a_gap() { let result = discover_client_sources(&SccmClientDiscoveryInput { max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, observations, - }); + }) + .expect("valid observations"); assert_eq!( result.declarations.len(), @@ -189,7 +191,8 @@ fn discovery_enforces_each_source_cap_and_retains_the_first_omitted_rotation_gap SccmClientDiscoveryObservationState::Found, ), ], - }); + }) + .expect("valid observations"); assert_eq!( result @@ -254,11 +257,12 @@ fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_ ), ], }; - let result = discover_client_sources(&input); + let result = discover_client_sources(&input).expect("valid observations"); let reversed = discover_client_sources(&SccmClientDiscoveryInput { max_found_fragments_per_source: input.max_found_fragments_per_source, observations: input.observations.into_iter().rev().collect(), - }); + }) + .expect("valid observations"); assert_eq!(result.declarations, reversed.declarations); assert!(result @@ -349,7 +353,8 @@ fn discovery_coalesces_exact_duplicate_observations_without_spending_global_quot let result = discover_client_sources(&SccmClientDiscoveryInput { max_found_fragments_per_source: 1, observations: vec![duplicate.clone(), duplicate], - }); + }) + .expect("exact duplicates are valid"); assert_eq!(result.declarations.len(), 1); assert_eq!( From efa87309653792777f9c2dd6593f7a1a15bea1e8 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:04:22 -0400 Subject: [PATCH 06/18] test(sccm): pin bounded discovery source fairness --- src-tauri/src/sccm/discovery.rs | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index fde961356..1157dc496 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -339,4 +339,73 @@ mod tests { ); } } + + #[test] + fn per_source_cap_does_not_let_an_early_noisy_source_starve_later_sources() { + let mut observations = (1..=6_000) + .map(|number| { + observation( + ROOT_A, + format!("AppEnforce.log.{number}"), + SccmRotation::Numbered(number), + ) + }) + .collect::>(); + observations.splice( + 0..0, + [ + observation(ROOT_A, "AppEnforce.log".to_owned(), SccmRotation::Current), + observation( + ROOT_A, + "AppEnforce.lo_".to_owned(), + SccmRotation::LoUnderscore, + ), + ], + ); + observations.push(observation( + ROOT_B, + "PolicyAgent.log".to_owned(), + SccmRotation::Current, + )); + observations.push(observation( + ROOT_B, + "PolicyAgent.lo_".to_owned(), + SccmRotation::LoUnderscore, + )); + + reset_construction_counts(); + let result = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: 2, + observations, + }) + .expect("valid observations"); + let (candidate_constructions, declaration_constructions) = construction_counts(); + + assert_eq!( + result + .declarations + .iter() + .filter(|declaration| declaration.root_handle == ROOT_A) + .map(|declaration| (&declaration.rotation, declaration.state)) + .collect::>(), + vec![ + (&SccmRotation::Current, SccmClientDiscoveryState::Discovered), + ( + &SccmRotation::LoUnderscore, + SccmClientDiscoveryState::Discovered + ), + (&SccmRotation::Numbered(1), SccmClientDiscoveryState::Capped), + ] + ); + assert!(result.declarations.iter().any(|declaration| { + declaration.root_handle == ROOT_B + && declaration.rotation == SccmRotation::Current + && declaration.state == SccmClientDiscoveryState::Discovered + })); + assert!( + result.declarations.len() <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS + && candidate_constructions <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS + && declaration_constructions <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS + ); + } } From 7926a2e1e855da1573451e720ca93c666b37bb16 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:13:15 -0400 Subject: [PATCH 07/18] fix(sccm): bound deterministic client discovery --- src-tauri/src/sccm/discovery.rs | 262 ++++++++++++++++++++++++-------- 1 file changed, 202 insertions(+), 60 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 1157dc496..198106dcf 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -1,7 +1,8 @@ //! Read-only normalization of already-observed SCCM client source candidates. use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::fmt; #[cfg(test)] use std::cell::Cell; @@ -66,6 +67,23 @@ pub struct SccmClientDiscoveryResult { pub declarations: Vec, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SccmClientDiscoveryError { + ConflictingObservation, +} + +impl fmt::Display for SccmClientDiscoveryError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ConflictingObservation => { + formatter.write_str("conflicting SCCM client discovery observations") + } + } + } +} + +impl std::error::Error for SccmClientDiscoveryError {} + struct Candidate { observation: SccmClientDiscoveryObservation, catalog_entry_id: String, @@ -73,64 +91,185 @@ struct Candidate { source_digest: String, } +#[derive(Clone, Copy)] +struct ObservationRef<'a>(&'a SccmClientDiscoveryObservation); + +impl PartialEq for ObservationRef<'_> { + fn eq(&self, other: &Self) -> bool { + compare_observation_order(self.0, other.0) == Ordering::Equal + } +} + +impl Eq for ObservationRef<'_> {} + +impl PartialOrd for ObservationRef<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ObservationRef<'_> { + fn cmp(&self, other: &Self) -> Ordering { + compare_observation_order(self.0, other.0) + } +} + #[cfg(test)] thread_local! { static CANDIDATE_CONSTRUCTIONS: Cell = const { Cell::new(0) }; static DECLARATION_CONSTRUCTIONS: Cell = const { Cell::new(0) }; } -pub fn discover_client_sources(input: &SccmClientDiscoveryInput) -> SccmClientDiscoveryResult { - let mut candidates = input - .observations - .iter() - .filter_map(candidate_from_observation) - .collect::>(); - candidates.sort_by(compare_candidates); - - let mut found_per_source = BTreeMap::::new(); - let mut capped_sources = BTreeSet::::new(); - let mut declarations = Vec::new(); - for candidate in candidates { - let source_key = format!( - "{}:{}", - candidate.observation.root_handle, candidate.source_digest - ); - let state = match candidate.observation.state { - SccmClientDiscoveryObservationState::Found => { - let count = found_per_source.entry(source_key.clone()).or_default(); - if *count < input.max_found_fragments_per_source { - *count += 1; - SccmClientDiscoveryState::Discovered - } else if capped_sources.insert(source_key) { - SccmClientDiscoveryState::Capped - } else { - continue; - } - } - SccmClientDiscoveryObservationState::AccessDenied => { - SccmClientDiscoveryState::AccessDenied - } - SccmClientDiscoveryObservationState::NotFound => SccmClientDiscoveryState::NotFound, +pub fn discover_client_sources( + input: &SccmClientDiscoveryInput, +) -> Result { + let mut found_per_source = BTreeMap::<(String, String), usize>::new(); + let mut capped_sources = BTreeSet::<(String, String)>::new(); + let mut declarations = Vec::with_capacity(MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS); + let mut cursor = None; + let mut first_omitted: Option<(ObservationRef<'_>, SccmClientDiscoveryState)> = None; + let mut initial = initial_observations(input) + .into_iter() + .collect::>(); + + while let Some(observation) = initial + .pop_front() + .or_else(|| next_observation(input, cursor, &capped_sources)) + { + validate_observation_conflict(input, observation.0)?; + cursor = Some(observation); + + let Some(state) = selection_state( + observation.0, + input.max_found_fragments_per_source, + &mut found_per_source, + &mut capped_sources, + ) else { + continue; + }; + + if declarations.len() < MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS - 1 { + declarations.push(declaration_from_candidate( + candidate_from_observation(observation.0).expect("prevalidated observation"), + state, + )); + } else if let Some((first_omitted, _)) = first_omitted { + declarations.push(declaration_from_candidate( + candidate_from_observation(first_omitted.0).expect("prevalidated observation"), + SccmClientDiscoveryState::Capped, + )); + return Ok(SccmClientDiscoveryResult { declarations }); + } else { + first_omitted = Some((observation, state)); + } + } + + if let Some((last, state)) = first_omitted { + declarations.push(declaration_from_candidate( + candidate_from_observation(last.0).expect("prevalidated observation"), + state, + )); + } + + Ok(SccmClientDiscoveryResult { declarations }) +} + +fn initial_observations(input: &SccmClientDiscoveryInput) -> BTreeSet> { + let mut observations = BTreeSet::new(); + for observation in &input.observations { + if canonical_client_source(&observation.basename, &observation.rotation).is_none() { + continue; + } + let candidate = ObservationRef(observation); + if observations.len() < MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS + 1 { + observations.insert(candidate); + } else if candidate + < *observations + .iter() + .next_back() + .expect("nonempty candidate set") + { + observations.insert(candidate); + observations.pop_last(); + } + } + observations +} + +fn next_observation<'a>( + input: &'a SccmClientDiscoveryInput, + cursor: Option>, + capped_sources: &BTreeSet<(String, String)>, +) -> Option> { + let mut next = None; + for observation in &input.observations { + let Some(canonical) = canonical_client_source(&observation.basename, &observation.rotation) + else { + continue; }; - declarations.push(declaration_from_candidate(candidate, state)); + if observation.state == SccmClientDiscoveryObservationState::Found + && capped_sources.contains(&(observation.root_handle.clone(), canonical)) + { + continue; + } + + let candidate = ObservationRef(observation); + if cursor.is_some_and(|last| candidate <= last) { + continue; + } + if next.is_none_or(|current| candidate < current) { + next = Some(candidate); + } } + next +} - if declarations.len() > MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS { - let first_omitted = declarations[MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS - 1].clone(); - declarations.truncate(MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS - 1); - declarations.push(SccmClientDiscoveryDeclaration { - state: SccmClientDiscoveryState::Capped, - artifact_id: marker_id( - &first_omitted.catalog_entry_id, - SccmManifestSourceState::Capped, - &first_omitted.rotation, - &first_omitted.basename, - &first_omitted.path_fingerprint, - ), - ..first_omitted - }); +fn validate_observation_conflict( + input: &SccmClientDiscoveryInput, + selected: &SccmClientDiscoveryObservation, +) -> Result<(), SccmClientDiscoveryError> { + if input.observations.iter().any(|observation| { + observation.state != selected.state && same_physical_observation(observation, selected) + }) { + return Err(SccmClientDiscoveryError::ConflictingObservation); } - SccmClientDiscoveryResult { declarations } + Ok(()) +} + +fn same_physical_observation( + left: &SccmClientDiscoveryObservation, + right: &SccmClientDiscoveryObservation, +) -> bool { + left.root_handle == right.root_handle + && left.basename == right.basename + && left.rotation == right.rotation + && canonical_client_source(&left.basename, &left.rotation) + == canonical_client_source(&right.basename, &right.rotation) +} + +fn selection_state( + observation: &SccmClientDiscoveryObservation, + max_found_fragments_per_source: usize, + found_per_source: &mut BTreeMap<(String, String), usize>, + capped_sources: &mut BTreeSet<(String, String)>, +) -> Option { + let canonical = canonical_client_source(&observation.basename, &observation.rotation)?; + let source_key = (observation.root_handle.clone(), canonical); + Some(match observation.state { + SccmClientDiscoveryObservationState::Found => { + let count = found_per_source.entry(source_key.clone()).or_default(); + if *count < max_found_fragments_per_source { + *count += 1; + SccmClientDiscoveryState::Discovered + } else if capped_sources.insert(source_key) { + SccmClientDiscoveryState::Capped + } else { + return None; + } + } + SccmClientDiscoveryObservationState::AccessDenied => SccmClientDiscoveryState::AccessDenied, + SccmClientDiscoveryObservationState::NotFound => SccmClientDiscoveryState::NotFound, + }) } fn candidate_from_observation(observation: &SccmClientDiscoveryObservation) -> Option { @@ -237,17 +376,20 @@ fn evidence_id( ) } -fn compare_candidates(left: &Candidate, right: &Candidate) -> Ordering { - left.logical_artifact_ids - .cmp(&right.logical_artifact_ids) - .then_with(|| { - left.observation - .root_handle - .cmp(&right.observation.root_handle) - }) - .then_with(|| rotation_order(&left.observation.rotation, &right.observation.rotation)) - .then_with(|| left.observation.basename.cmp(&right.observation.basename)) - .then_with(|| state_rank(left.observation.state).cmp(&state_rank(right.observation.state))) +fn compare_observation_order( + left: &SccmClientDiscoveryObservation, + right: &SccmClientDiscoveryObservation, +) -> Ordering { + let left_canonical = + canonical_client_source(&left.basename, &left.rotation).expect("prevalidated observation"); + let right_canonical = canonical_client_source(&right.basename, &right.rotation) + .expect("prevalidated observation"); + logical_artifact_ids_for_basename(&left_canonical) + .cmp(&logical_artifact_ids_for_basename(&right_canonical)) + .then_with(|| left.root_handle.cmp(&right.root_handle)) + .then_with(|| rotation_order(&left.rotation, &right.rotation)) + .then_with(|| left.basename.cmp(&right.basename)) + .then_with(|| state_rank(left.state).cmp(&state_rank(right.state))) } fn state_rank(state: SccmClientDiscoveryObservationState) -> u8 { From b937456ffae434daa41ca945518755cfacc2869e Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:14:41 -0400 Subject: [PATCH 08/18] test(sccm): expose late discovery conflicts --- src-tauri/tests/sccm_client_discovery.rs | 78 ++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index 716cf62c4..e18b57bd2 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -398,3 +398,81 @@ fn discovery_rejects_conflicting_states_for_one_canonical_physical_source() { assert_eq!(error, reversed_error); } + +#[test] +fn discovery_rejects_late_conflicts_after_the_global_declaration_frontier() { + let mut observations = Vec::new(); + for number in 1..=2_049 { + observations.push(observation( + ROOT_A, + &format!("AppEnforce.log.{number}"), + SccmRotation::Numbered(number), + SccmClientDiscoveryObservationState::Found, + )); + observations.push(observation( + ROOT_B, + &format!("PolicyAgent.log.{number}"), + SccmRotation::Numbered(number), + SccmClientDiscoveryObservationState::Found, + )); + } + observations.extend([ + observation( + ROOT_A, + "CIAgent.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "CIAgent.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::AccessDenied, + ), + ]); + let input = SccmClientDiscoveryInput { + max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + observations, + }; + + let error = discover_client_sources(&input) + .expect_err("a conflict beyond the output frontier fails closed"); + let mut reversed = input; + reversed.observations.reverse(); + assert_eq!( + error, + discover_client_sources(&reversed) + .expect_err("a late conflict fails closed regardless of input order") + ); +} + +#[test] +fn discovery_rejects_conflicting_states_for_canonical_basename_aliases() { + let input = SccmClientDiscoveryInput { + max_found_fragments_per_source: 8, + observations: vec![ + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "appenforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::AccessDenied, + ), + ], + }; + + let error = discover_client_sources(&input) + .expect_err("canonical aliases with conflicting state fail closed"); + let mut reversed = input; + reversed.observations.reverse(); + assert_eq!( + error, + discover_client_sources(&reversed) + .expect_err("canonical alias conflict fails closed regardless of input order") + ); +} From cb630140d9e87140abf2391dc4b3cc23e24f76f5 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:16:07 -0400 Subject: [PATCH 09/18] fix(sccm): reject all conflicting discovery observations --- src-tauri/src/sccm/discovery.rs | 37 ++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 198106dcf..1eac8bd78 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -123,6 +123,7 @@ thread_local! { pub fn discover_client_sources( input: &SccmClientDiscoveryInput, ) -> Result { + validate_all_observation_conflicts(input)?; let mut found_per_source = BTreeMap::<(String, String), usize>::new(); let mut capped_sources = BTreeSet::<(String, String)>::new(); let mut declarations = Vec::with_capacity(MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS); @@ -136,8 +137,10 @@ pub fn discover_client_sources( .pop_front() .or_else(|| next_observation(input, cursor, &capped_sources)) { - validate_observation_conflict(input, observation.0)?; cursor = Some(observation); + if !is_physical_representative(input, observation.0) { + continue; + } let Some(state) = selection_state( observation.0, @@ -189,8 +192,9 @@ fn initial_observations(input: &SccmClientDiscoveryInput) -> BTreeSet( next } -fn validate_observation_conflict( +fn validate_all_observation_conflicts( input: &SccmClientDiscoveryInput, - selected: &SccmClientDiscoveryObservation, ) -> Result<(), SccmClientDiscoveryError> { - if input.observations.iter().any(|observation| { - observation.state != selected.state && same_physical_observation(observation, selected) - }) { - return Err(SccmClientDiscoveryError::ConflictingObservation); + for (index, left) in input.observations.iter().enumerate() { + if canonical_client_source(&left.basename, &left.rotation).is_none() { + continue; + } + for right in input.observations.iter().skip(index + 1) { + if left.state != right.state && same_physical_observation(left, right) { + return Err(SccmClientDiscoveryError::ConflictingObservation); + } + } } Ok(()) } @@ -241,12 +249,21 @@ fn same_physical_observation( right: &SccmClientDiscoveryObservation, ) -> bool { left.root_handle == right.root_handle - && left.basename == right.basename && left.rotation == right.rotation && canonical_client_source(&left.basename, &left.rotation) == canonical_client_source(&right.basename, &right.rotation) } +fn is_physical_representative( + input: &SccmClientDiscoveryInput, + selected: &SccmClientDiscoveryObservation, +) -> bool { + !input.observations.iter().any(|observation| { + same_physical_observation(observation, selected) + && ObservationRef(observation) < ObservationRef(selected) + }) +} + fn selection_state( observation: &SccmClientDiscoveryObservation, max_found_fragments_per_source: usize, From cf6299f5ed06c04c1c6e333638cf3a81b0cc0443 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:17:24 -0400 Subject: [PATCH 10/18] test(sccm): preserve malformed root discovery skip --- src-tauri/tests/sccm_client_discovery.rs | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index e18b57bd2..db9259b94 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -476,3 +476,29 @@ fn discovery_rejects_conflicting_states_for_canonical_basename_aliases() { .expect_err("canonical alias conflict fails closed regardless of input order") ); } + +#[test] +fn discovery_skips_supported_observations_with_malformed_root_handles() { + let result = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: 8, + observations: vec![ + observation( + "root-not-a-sha256-handle", + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_B, + "PolicyAgent.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + ], + }) + .expect("malformed roots are skipped rather than becoming a discovery failure"); + + assert_eq!(result.declarations.len(), 1); + assert_eq!(result.declarations[0].root_handle, ROOT_B); + assert_eq!(result.declarations[0].basename, "PolicyAgent.log"); +} From 0e85cdff6b92b8bf9b51e9fb3c8b737a9cbeb39b Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:17:43 -0400 Subject: [PATCH 11/18] fix(sccm): skip malformed discovery root handles --- src-tauri/src/sccm/discovery.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 1eac8bd78..1eb8a2c4e 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -12,8 +12,8 @@ use sha2::{Digest, Sha256}; use super::contract::{ canonical_client_source, catalog_entry_id, expected_marker_artifact_id, - expected_physical_artifact_id, logical_artifact_ids_for_basename, rotation_order, - rotation_segment, source_identity_digest, SccmManifestSourceState, + expected_physical_artifact_id, logical_artifact_ids_for_basename, root_handle_digest, + rotation_order, rotation_segment, source_identity_digest, SccmManifestSourceState, }; pub const MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS: usize = 4_096; @@ -180,7 +180,7 @@ pub fn discover_client_sources( fn initial_observations(input: &SccmClientDiscoveryInput) -> BTreeSet> { let mut observations = BTreeSet::new(); for observation in &input.observations { - if canonical_client_source(&observation.basename, &observation.rotation).is_none() { + if !is_supported_observation(observation) { continue; } let candidate = ObservationRef(observation); @@ -207,10 +207,11 @@ fn next_observation<'a>( ) -> Option> { let mut next = None; for observation in &input.observations { - let Some(canonical) = canonical_client_source(&observation.basename, &observation.rotation) - else { + if !is_supported_observation(observation) { continue; - }; + } + let canonical = canonical_client_source(&observation.basename, &observation.rotation) + .expect("supported observation has a canonical source"); if observation.state == SccmClientDiscoveryObservationState::Found && capped_sources.contains(&(observation.root_handle.clone(), canonical)) { @@ -232,11 +233,14 @@ fn validate_all_observation_conflicts( input: &SccmClientDiscoveryInput, ) -> Result<(), SccmClientDiscoveryError> { for (index, left) in input.observations.iter().enumerate() { - if canonical_client_source(&left.basename, &left.rotation).is_none() { + if !is_supported_observation(left) { continue; } for right in input.observations.iter().skip(index + 1) { - if left.state != right.state && same_physical_observation(left, right) { + if is_supported_observation(right) + && left.state != right.state + && same_physical_observation(left, right) + { return Err(SccmClientDiscoveryError::ConflictingObservation); } } @@ -244,6 +248,11 @@ fn validate_all_observation_conflicts( Ok(()) } +fn is_supported_observation(observation: &SccmClientDiscoveryObservation) -> bool { + root_handle_digest(&observation.root_handle).is_some() + && canonical_client_source(&observation.basename, &observation.rotation).is_some() +} + fn same_physical_observation( left: &SccmClientDiscoveryObservation, right: &SccmClientDiscoveryObservation, From 199ea45356a20b89ed8166001c1da12c94f4636e Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:19:04 -0400 Subject: [PATCH 12/18] perf(sccm): short-circuit discovery conflict checks --- src-tauri/src/sccm/discovery.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 1eb8a2c4e..324ee6755 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -237,10 +237,7 @@ fn validate_all_observation_conflicts( continue; } for right in input.observations.iter().skip(index + 1) { - if is_supported_observation(right) - && left.state != right.state - && same_physical_observation(left, right) - { + if left.state != right.state && same_physical_observation(left, right) { return Err(SccmClientDiscoveryError::ConflictingObservation); } } From 8a2d9d243d76c24d353897ee0ca4c8550868132a Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:19:47 -0400 Subject: [PATCH 13/18] test(sccm): pin conflict error privacy --- src-tauri/tests/sccm_client_discovery.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index db9259b94..d087a0018 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -391,6 +391,12 @@ fn discovery_rejects_conflicting_states_for_one_canonical_physical_source() { let error = discover_client_sources(&input).expect_err("contradictory physical evidence fails closed"); + assert_eq!( + error.to_string(), + "conflicting SCCM client discovery observations" + ); + assert!(!error.to_string().contains(ROOT_A)); + assert!(!error.to_string().contains("AppEnforce.log")); let mut reversed = input; reversed.observations.reverse(); let reversed_error = discover_client_sources(&reversed) From ee42e3a0462ca4e1364eadbca3291bda4ec0a71a Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:20:12 -0400 Subject: [PATCH 14/18] style(sccm): satisfy bounded discovery lint --- src-tauri/src/sccm/discovery.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 324ee6755..1a69cb986 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -191,10 +191,9 @@ fn initial_observations(input: &SccmClientDiscoveryInput) -> BTreeSet Date: Sun, 2 Aug 2026 08:36:50 -0400 Subject: [PATCH 15/18] test(sccm): expose discovery work bound --- src-tauri/src/sccm/discovery.rs | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 1a69cb986..6874d2878 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -454,6 +454,86 @@ mod tests { DECLARATION_CONSTRUCTIONS.with(|count| count.set(0)); } + #[test] + fn defensive_observation_limit_rejects_before_any_normalization_or_construction() { + let observations = (1..=MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS + 1) + .map(|number| { + observation( + ROOT_A, + format!("AppEnforce.log.{number}"), + SccmRotation::Numbered(number as u32), + ) + }) + .collect(); + let input = SccmClientDiscoveryInput { + max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + observations, + }; + + reset_construction_counts(); + reset_normalization_count(); + assert_eq!( + discover_client_sources(&input), + Err(SccmClientDiscoveryError::ObservationLimitExceeded), + "input beyond the defensive discovery contract must fail conservatively" + ); + assert_eq!( + normalization_count(), + 0, + "the defensive limit rejects before any observation is normalized" + ); + assert_eq!( + construction_counts(), + (0, 0), + "the defensive limit rejects before candidates or declarations are built" + ); + } + + #[test] + fn defensive_observation_bound_normalizes_each_all_found_or_mixed_state_input_once() { + let all_found = (1..=MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS) + .map(|number| { + observation( + ROOT_A, + format!("AppEnforce.log.{number}"), + SccmRotation::Numbered(number as u32), + ) + }) + .collect::>(); + let mixed_states = (1..=MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS) + .map(|number| SccmClientDiscoveryObservation { + root_handle: if number % 2 == 0 { ROOT_A } else { ROOT_B }.to_owned(), + basename: format!("PolicyAgent.log.{number}"), + rotation: SccmRotation::Numbered(number as u32), + state: match number % 3 { + 0 => SccmClientDiscoveryObservationState::Found, + 1 => SccmClientDiscoveryObservationState::AccessDenied, + _ => SccmClientDiscoveryObservationState::NotFound, + }, + }) + .collect::>(); + + for observations in [all_found, mixed_states] { + reset_construction_counts(); + reset_normalization_count(); + let result = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + observations, + }) + .expect("the defensive boundary itself remains processable"); + + assert_eq!( + normalization_count(), + MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS, + "each accepted observation is normalized exactly once" + ); + assert!( + result.declarations.len() <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + "the declaration output remains globally bounded" + ); + } + } + #[test] fn oversized_discovery_constructs_only_the_bounded_retained_candidates_and_declarations() { let mut observations = Vec::new(); From c778b3cd66d3969eb5f8f87c407f8964f7d520e4 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 08:43:11 -0400 Subject: [PATCH 16/18] fix(sccm): bound client discovery normalization work --- src-tauri/src/sccm/discovery.rs | 277 ++++++++++------------- src-tauri/tests/sccm_client_discovery.rs | 77 ++++++- 2 files changed, 189 insertions(+), 165 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 6874d2878..225afbf53 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -1,7 +1,7 @@ //! Read-only normalization of already-observed SCCM client source candidates. use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt; #[cfg(test)] @@ -17,6 +17,11 @@ use super::contract::{ }; pub const MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS: usize = 4_096; +/// Defensive bound for supplied observations. Native enumeration must report +/// its own truncation as SCCM coverage; this pure normalizer does not silently +/// discard observations beyond the contract. +pub const MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS: usize = + MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS + 1; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SccmClientDiscoveryObservationState { @@ -70,6 +75,7 @@ pub struct SccmClientDiscoveryResult { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SccmClientDiscoveryError { ConflictingObservation, + ObservationLimitExceeded, } impl fmt::Display for SccmClientDiscoveryError { @@ -78,6 +84,9 @@ impl fmt::Display for SccmClientDiscoveryError { Self::ConflictingObservation => { formatter.write_str("conflicting SCCM client discovery observations") } + Self::ObservationLimitExceeded => { + formatter.write_str("SCCM client discovery observation limit exceeded") + } } } } @@ -91,59 +100,41 @@ struct Candidate { source_digest: String, } -#[derive(Clone, Copy)] -struct ObservationRef<'a>(&'a SccmClientDiscoveryObservation); - -impl PartialEq for ObservationRef<'_> { - fn eq(&self, other: &Self) -> bool { - compare_observation_order(self.0, other.0) == Ordering::Equal - } -} - -impl Eq for ObservationRef<'_> {} - -impl PartialOrd for ObservationRef<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct PhysicalObservationKey { + root_handle: String, + canonical_basename: String, + rotation: String, } -impl Ord for ObservationRef<'_> { - fn cmp(&self, other: &Self) -> Ordering { - compare_observation_order(self.0, other.0) - } +struct NormalizedObservation<'a> { + observation: &'a SccmClientDiscoveryObservation, + canonical_basename: String, } #[cfg(test)] thread_local! { static CANDIDATE_CONSTRUCTIONS: Cell = const { Cell::new(0) }; static DECLARATION_CONSTRUCTIONS: Cell = const { Cell::new(0) }; + static NORMALIZATION_OPERATIONS: Cell = const { Cell::new(0) }; } pub fn discover_client_sources( input: &SccmClientDiscoveryInput, ) -> Result { - validate_all_observation_conflicts(input)?; + if input.observations.len() > MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS { + return Err(SccmClientDiscoveryError::ObservationLimitExceeded); + } + + let observations = normalize_observations(input)?; let mut found_per_source = BTreeMap::<(String, String), usize>::new(); let mut capped_sources = BTreeSet::<(String, String)>::new(); let mut declarations = Vec::with_capacity(MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS); - let mut cursor = None; - let mut first_omitted: Option<(ObservationRef<'_>, SccmClientDiscoveryState)> = None; - let mut initial = initial_observations(input) - .into_iter() - .collect::>(); - - while let Some(observation) = initial - .pop_front() - .or_else(|| next_observation(input, cursor, &capped_sources)) - { - cursor = Some(observation); - if !is_physical_representative(input, observation.0) { - continue; - } + let mut first_omitted: Option<(NormalizedObservation<'_>, SccmClientDiscoveryState)> = None; + for observation in observations { let Some(state) = selection_state( - observation.0, + &observation, input.max_found_fragments_per_source, &mut found_per_source, &mut capped_sources, @@ -153,12 +144,12 @@ pub fn discover_client_sources( if declarations.len() < MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS - 1 { declarations.push(declaration_from_candidate( - candidate_from_observation(observation.0).expect("prevalidated observation"), + candidate_from_observation(&observation).expect("prevalidated observation"), state, )); } else if let Some((first_omitted, _)) = first_omitted { declarations.push(declaration_from_candidate( - candidate_from_observation(first_omitted.0).expect("prevalidated observation"), + candidate_from_observation(&first_omitted).expect("prevalidated observation"), SccmClientDiscoveryState::Capped, )); return Ok(SccmClientDiscoveryResult { declarations }); @@ -169,7 +160,7 @@ pub fn discover_client_sources( if let Some((last, state)) = first_omitted { declarations.push(declaration_from_candidate( - candidate_from_observation(last.0).expect("prevalidated observation"), + candidate_from_observation(&last).expect("prevalidated observation"), state, )); } @@ -177,107 +168,62 @@ pub fn discover_client_sources( Ok(SccmClientDiscoveryResult { declarations }) } -fn initial_observations(input: &SccmClientDiscoveryInput) -> BTreeSet> { - let mut observations = BTreeSet::new(); - for observation in &input.observations { - if !is_supported_observation(observation) { - continue; - } - let candidate = ObservationRef(observation); - if observations.len() < MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS + 1 { - observations.insert(candidate); - } else if candidate - < *observations - .iter() - .next_back() - .expect("nonempty candidate set") - && observations.insert(candidate) - { - observations.pop_last(); - } - } - observations -} - -fn next_observation<'a>( - input: &'a SccmClientDiscoveryInput, - cursor: Option>, - capped_sources: &BTreeSet<(String, String)>, -) -> Option> { - let mut next = None; - for observation in &input.observations { - if !is_supported_observation(observation) { - continue; - } - let canonical = canonical_client_source(&observation.basename, &observation.rotation) - .expect("supported observation has a canonical source"); - if observation.state == SccmClientDiscoveryObservationState::Found - && capped_sources.contains(&(observation.root_handle.clone(), canonical)) - { - continue; - } - - let candidate = ObservationRef(observation); - if cursor.is_some_and(|last| candidate <= last) { - continue; - } - if next.is_none_or(|current| candidate < current) { - next = Some(candidate); - } - } - next -} - -fn validate_all_observation_conflicts( +fn normalize_observations( input: &SccmClientDiscoveryInput, -) -> Result<(), SccmClientDiscoveryError> { - for (index, left) in input.observations.iter().enumerate() { - if !is_supported_observation(left) { +) -> Result>, SccmClientDiscoveryError> { + let mut observations = BTreeMap::>::new(); + for observation in &input.observations { + let Some(normalized) = normalize_observation(observation) else { continue; - } - for right in input.observations.iter().skip(index + 1) { - if left.state != right.state && same_physical_observation(left, right) { - return Err(SccmClientDiscoveryError::ConflictingObservation); + }; + let key = PhysicalObservationKey { + root_handle: observation.root_handle.clone(), + canonical_basename: normalized.canonical_basename.clone(), + rotation: rotation_segment(&observation.rotation), + }; + match observations.entry(key) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(normalized); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + if entry.get().observation.state != observation.state { + return Err(SccmClientDiscoveryError::ConflictingObservation); + } + if compare_observation_order(&normalized, entry.get()) == Ordering::Less { + entry.insert(normalized); + } } } } - Ok(()) + let mut observations = observations.into_values().collect::>(); + observations.sort_by(compare_observation_order); + Ok(observations) } -fn is_supported_observation(observation: &SccmClientDiscoveryObservation) -> bool { - root_handle_digest(&observation.root_handle).is_some() - && canonical_client_source(&observation.basename, &observation.rotation).is_some() -} - -fn same_physical_observation( - left: &SccmClientDiscoveryObservation, - right: &SccmClientDiscoveryObservation, -) -> bool { - left.root_handle == right.root_handle - && left.rotation == right.rotation - && canonical_client_source(&left.basename, &left.rotation) - == canonical_client_source(&right.basename, &right.rotation) -} - -fn is_physical_representative( - input: &SccmClientDiscoveryInput, - selected: &SccmClientDiscoveryObservation, -) -> bool { - !input.observations.iter().any(|observation| { - same_physical_observation(observation, selected) - && ObservationRef(observation) < ObservationRef(selected) +fn normalize_observation( + observation: &SccmClientDiscoveryObservation, +) -> Option> { + #[cfg(test)] + NORMALIZATION_OPERATIONS.with(|count| count.set(count.get() + 1)); + root_handle_digest(&observation.root_handle)?; + let canonical_basename = canonical_client_source(&observation.basename, &observation.rotation)?; + Some(NormalizedObservation { + observation, + canonical_basename, }) } fn selection_state( - observation: &SccmClientDiscoveryObservation, + observation: &NormalizedObservation<'_>, max_found_fragments_per_source: usize, found_per_source: &mut BTreeMap<(String, String), usize>, capped_sources: &mut BTreeSet<(String, String)>, ) -> Option { - let canonical = canonical_client_source(&observation.basename, &observation.rotation)?; - let source_key = (observation.root_handle.clone(), canonical); - Some(match observation.state { + let source_key = ( + observation.observation.root_handle.clone(), + observation.canonical_basename.clone(), + ); + Some(match observation.observation.state { SccmClientDiscoveryObservationState::Found => { let count = found_per_source.entry(source_key.clone()).or_default(); if *count < max_found_fragments_per_source { @@ -294,16 +240,18 @@ fn selection_state( }) } -fn candidate_from_observation(observation: &SccmClientDiscoveryObservation) -> Option { - let canonical = canonical_client_source(&observation.basename, &observation.rotation)?; +fn candidate_from_observation(observation: &NormalizedObservation<'_>) -> Option { #[cfg(test)] CANDIDATE_CONSTRUCTIONS.with(|count| count.set(count.get() + 1)); - let source_digest = source_identity_digest(&observation.root_handle, &canonical)?; + let source_digest = source_identity_digest( + &observation.observation.root_handle, + &observation.canonical_basename, + )?; Some(Candidate { - observation: observation.clone(), - catalog_entry_id: catalog_entry_id(&canonical), - logical_artifact_ids: logical_artifact_ids_for_basename(&canonical), + observation: observation.observation.clone(), + catalog_entry_id: catalog_entry_id(&observation.canonical_basename), + logical_artifact_ids: logical_artifact_ids_for_basename(&observation.canonical_basename), source_digest, }) } @@ -399,19 +347,21 @@ fn evidence_id( } fn compare_observation_order( - left: &SccmClientDiscoveryObservation, - right: &SccmClientDiscoveryObservation, + left: &NormalizedObservation<'_>, + right: &NormalizedObservation<'_>, ) -> Ordering { - let left_canonical = - canonical_client_source(&left.basename, &left.rotation).expect("prevalidated observation"); - let right_canonical = canonical_client_source(&right.basename, &right.rotation) - .expect("prevalidated observation"); - logical_artifact_ids_for_basename(&left_canonical) - .cmp(&logical_artifact_ids_for_basename(&right_canonical)) - .then_with(|| left.root_handle.cmp(&right.root_handle)) - .then_with(|| rotation_order(&left.rotation, &right.rotation)) - .then_with(|| left.basename.cmp(&right.basename)) - .then_with(|| state_rank(left.state).cmp(&state_rank(right.state))) + logical_artifact_ids_for_basename(&left.canonical_basename) + .cmp(&logical_artifact_ids_for_basename( + &right.canonical_basename, + )) + .then_with(|| { + left.observation + .root_handle + .cmp(&right.observation.root_handle) + }) + .then_with(|| rotation_order(&left.observation.rotation, &right.observation.rotation)) + .then_with(|| left.observation.basename.cmp(&right.observation.basename)) + .then_with(|| state_rank(left.observation.state).cmp(&state_rank(right.observation.state))) } fn state_rank(state: SccmClientDiscoveryObservationState) -> u8 { @@ -454,6 +404,14 @@ mod tests { DECLARATION_CONSTRUCTIONS.with(|count| count.set(0)); } + fn normalization_count() -> usize { + NORMALIZATION_OPERATIONS.with(Cell::get) + } + + fn reset_normalization_count() { + NORMALIZATION_OPERATIONS.with(|count| count.set(0)); + } + #[test] fn defensive_observation_limit_rejects_before_any_normalization_or_construction() { let observations = (1..=MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS + 1) @@ -535,7 +493,7 @@ mod tests { } #[test] - fn oversized_discovery_constructs_only_the_bounded_retained_candidates_and_declarations() { + fn oversized_discovery_is_rejected_without_constructing_candidates_or_declarations() { let mut observations = Vec::new(); for number in 1..=6_000 { observations.push(observation( @@ -555,43 +513,40 @@ mod tests { }; reset_construction_counts(); - let result = discover_client_sources(&input).expect("valid observations"); + reset_normalization_count(); + let error = discover_client_sources(&input) + .expect_err("inputs outside the defensive contract must be rejected"); let counts = construction_counts(); let mut reversed = input.clone(); reversed.observations.reverse(); reset_construction_counts(); - let reversed_result = discover_client_sources(&reversed).expect("valid observations"); + reset_normalization_count(); + let reversed_error = discover_client_sources(&reversed) + .expect_err("input order does not weaken the defensive limit"); let reversed_counts = construction_counts(); assert_eq!( - result, reversed_result, - "input order must not change the result" - ); - assert!( - result.declarations.len() <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, - "the global result remains bounded" + error, reversed_error, + "input order must not change conservative overflow behavior" ); for (candidate_constructions, declaration_constructions) in [counts, reversed_counts] { - assert!( - candidate_constructions <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, - "candidate construction must stop at the retained global budget" - ); - assert!( - declaration_constructions <= MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, - "declaration and identity construction must stop at the retained global budget" + assert_eq!( + (candidate_constructions, declaration_constructions), + (0, 0), + "the defensive limit rejects before candidates or declarations are built" ); } } #[test] fn per_source_cap_does_not_let_an_early_noisy_source_starve_later_sources() { - let mut observations = (1..=6_000) + let mut observations = (1..=MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS - 4) .map(|number| { observation( ROOT_A, format!("AppEnforce.log.{number}"), - SccmRotation::Numbered(number), + SccmRotation::Numbered(number as u32), ) }) .collect::>(); diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index d087a0018..0bc93b959 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -1,7 +1,7 @@ use app_lib::sccm::{ - discover_client_sources, SccmClientDiscoveryInput, SccmClientDiscoveryObservation, - SccmClientDiscoveryObservationState, SccmClientDiscoveryState, - MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + discover_client_sources, SccmClientDiscoveryError, SccmClientDiscoveryInput, + SccmClientDiscoveryObservation, SccmClientDiscoveryObservationState, SccmClientDiscoveryState, + MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS, }; use cmtraceopen_parser::sccm::SccmRotation; use sha2::{Digest, Sha256}; @@ -408,7 +408,7 @@ fn discovery_rejects_conflicting_states_for_one_canonical_physical_source() { #[test] fn discovery_rejects_late_conflicts_after_the_global_declaration_frontier() { let mut observations = Vec::new(); - for number in 1..=2_049 { + for number in 1..=2_047 { observations.push(observation( ROOT_A, &format!("AppEnforce.log.{number}"), @@ -422,6 +422,12 @@ fn discovery_rejects_late_conflicts_after_the_global_declaration_frontier() { SccmClientDiscoveryObservationState::Found, )); } + observations.push(observation( + ROOT_A, + "AppEnforce.log.2048", + SccmRotation::Numbered(2_048), + SccmClientDiscoveryObservationState::Found, + )); observations.extend([ observation( ROOT_A, @@ -443,6 +449,7 @@ fn discovery_rejects_late_conflicts_after_the_global_declaration_frontier() { let error = discover_client_sources(&input) .expect_err("a conflict beyond the output frontier fails closed"); + assert_eq!(error, SccmClientDiscoveryError::ConflictingObservation); let mut reversed = input; reversed.observations.reverse(); assert_eq!( @@ -483,6 +490,68 @@ fn discovery_rejects_conflicting_states_for_canonical_basename_aliases() { ); } +#[test] +fn discovery_coalesces_same_state_canonical_aliases_to_the_stable_basename() { + let input = SccmClientDiscoveryInput { + max_found_fragments_per_source: 8, + observations: vec![ + observation( + ROOT_A, + "appenforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + ], + }; + + let result = discover_client_sources(&input).expect("same-state aliases are one source"); + let reversed = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: input.max_found_fragments_per_source, + observations: input.observations.into_iter().rev().collect(), + }) + .expect("same-state aliases remain order independent"); + + assert_eq!(result, reversed); + assert_eq!(result.declarations.len(), 1); + assert_eq!(result.declarations[0].basename, "AppEnforce.log"); + assert_eq!( + result.declarations[0].state, + SccmClientDiscoveryState::Discovered + ); +} + +#[test] +fn discovery_rejects_observations_beyond_its_defensive_contract() { + let observations = (1..=MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS + 1) + .map(|number| { + observation( + ROOT_A, + &format!("AppEnforce.log.{number}"), + SccmRotation::Numbered(number as u32), + SccmClientDiscoveryObservationState::Found, + ) + }) + .collect(); + let error = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, + observations, + }) + .expect_err("the API must not silently ignore input beyond its defensive contract"); + + assert_eq!(error, SccmClientDiscoveryError::ObservationLimitExceeded); + assert_eq!( + error.to_string(), + "SCCM client discovery observation limit exceeded" + ); + assert!(!error.to_string().contains(ROOT_A)); +} + #[test] fn discovery_skips_supported_observations_with_malformed_root_handles() { let result = discover_client_sources(&SccmClientDiscoveryInput { From 2804a5d414dc03e5f4e96c634c7e415a13caa198 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 10:33:24 -0400 Subject: [PATCH 17/18] test(sccm): expose discovery review regressions --- src-tauri/src/sccm/discovery.rs | 55 +++++- src-tauri/tests/sccm_client_discovery.rs | 204 ++++++++++++++++++----- 2 files changed, 208 insertions(+), 51 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index 225afbf53..ff2395d04 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -11,9 +11,9 @@ use cmtraceopen_parser::sccm::SccmRotation; use sha2::{Digest, Sha256}; use super::contract::{ - canonical_client_source, catalog_entry_id, expected_marker_artifact_id, - expected_physical_artifact_id, logical_artifact_ids_for_basename, root_handle_digest, - rotation_order, rotation_segment, source_identity_digest, SccmManifestSourceState, + SccmManifestSourceState, canonical_client_source, catalog_entry_id, + expected_marker_artifact_id, expected_physical_artifact_id, logical_artifact_ids_for_basename, + root_handle_digest, rotation_order, rotation_segment, source_identity_digest, }; pub const MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS: usize = 4_096; @@ -117,6 +117,7 @@ thread_local! { static CANDIDATE_CONSTRUCTIONS: Cell = const { Cell::new(0) }; static DECLARATION_CONSTRUCTIONS: Cell = const { Cell::new(0) }; static NORMALIZATION_OPERATIONS: Cell = const { Cell::new(0) }; + static LOGICAL_ARTIFACT_ID_LOOKUPS: Cell = const { Cell::new(0) }; } pub fn discover_client_sources( @@ -251,7 +252,7 @@ fn candidate_from_observation(observation: &NormalizedObservation<'_>) -> Option Some(Candidate { observation: observation.observation.clone(), catalog_entry_id: catalog_entry_id(&observation.canonical_basename), - logical_artifact_ids: logical_artifact_ids_for_basename(&observation.canonical_basename), + logical_artifact_ids: logical_artifact_ids(&observation.canonical_basename), source_digest, }) } @@ -350,10 +351,8 @@ fn compare_observation_order( left: &NormalizedObservation<'_>, right: &NormalizedObservation<'_>, ) -> Ordering { - logical_artifact_ids_for_basename(&left.canonical_basename) - .cmp(&logical_artifact_ids_for_basename( - &right.canonical_basename, - )) + logical_artifact_ids(&left.canonical_basename) + .cmp(&logical_artifact_ids(&right.canonical_basename)) .then_with(|| { left.observation .root_handle @@ -364,6 +363,12 @@ fn compare_observation_order( .then_with(|| state_rank(left.observation.state).cmp(&state_rank(right.observation.state))) } +fn logical_artifact_ids(canonical_basename: &str) -> Vec { + #[cfg(test)] + LOGICAL_ARTIFACT_ID_LOOKUPS.with(|count| count.set(count.get() + 1)); + logical_artifact_ids_for_basename(canonical_basename) +} + fn state_rank(state: SccmClientDiscoveryObservationState) -> u8 { match state { SccmClientDiscoveryObservationState::Found => 0, @@ -412,6 +417,14 @@ mod tests { NORMALIZATION_OPERATIONS.with(|count| count.set(0)); } + fn logical_artifact_id_lookup_count() -> usize { + LOGICAL_ARTIFACT_ID_LOOKUPS.with(Cell::get) + } + + fn reset_logical_artifact_id_lookup_count() { + LOGICAL_ARTIFACT_ID_LOOKUPS.with(|count| count.set(0)); + } + #[test] fn defensive_observation_limit_rejects_before_any_normalization_or_construction() { let observations = (1..=MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS + 1) @@ -492,6 +505,32 @@ mod tests { } } + #[test] + fn normalization_caches_logical_artifact_ids_once_per_observation_despite_sorting() { + let observations = (1..=64) + .map(|number| { + observation( + ROOT_A, + format!("AppEnforce.log.{number}"), + SccmRotation::Numbered(number), + ) + }) + .collect::>(); + + reset_logical_artifact_id_lookup_count(); + discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: 64, + observations, + }) + .expect("accepted observations normalize deterministically"); + + assert_eq!( + logical_artifact_id_lookup_count(), + 64, + "sorting and declaration construction must reuse each normalized observation's cached logical IDs" + ); + } + #[test] fn oversized_discovery_is_rejected_without_constructing_candidates_or_declarations() { let mut observations = Vec::new(); diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index 0bc93b959..98580c3e0 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -1,7 +1,7 @@ use app_lib::sccm::{ - discover_client_sources, SccmClientDiscoveryError, SccmClientDiscoveryInput, - SccmClientDiscoveryObservation, SccmClientDiscoveryObservationState, SccmClientDiscoveryState, MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS, + SccmClientDiscoveryError, SccmClientDiscoveryInput, SccmClientDiscoveryObservation, + SccmClientDiscoveryObservationState, SccmClientDiscoveryState, discover_client_sources, }; use cmtraceopen_parser::sccm::SccmRotation; use sha2::{Digest, Sha256}; @@ -86,6 +86,29 @@ fn expected_marker_artifact_id( ) } +fn expected_evidence_identity( + canonical_basename: &str, + root_handle: &str, + rotation: &SccmRotation, + physical_basename: &str, +) -> String { + let catalog_entry_id = format!( + "sccm-client-source:v1:sha256:{}", + sha256(canonical_basename) + ); + let fingerprint = path_fingerprint(root_handle, canonical_basename); + let source_digest = fingerprint + .strip_prefix("sha256:") + .expect("path fingerprint has the expected versioned prefix"); + format!( + "sccm-evidence:v1:sha256:{}", + sha256(format!( + "cmtraceopen.sccm.evidence.v1\0{catalog_entry_id}\0{source_digest}\0{}\0{physical_basename}", + rotation_segment(rotation) + )) + ) +} + #[test] fn discovery_uses_one_global_declaration_budget_and_marks_the_first_omitted_rotation() { let mut observations = Vec::new(); @@ -161,10 +184,12 @@ fn discovery_at_the_exact_global_boundary_does_not_manufacture_a_gap() { result.declarations.len(), MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS ); - assert!(result - .declarations - .iter() - .all(|declaration| declaration.state == SccmClientDiscoveryState::Discovered)); + assert!( + result + .declarations + .iter() + .all(|declaration| declaration.state == SccmClientDiscoveryState::Discovered) + ); } #[test] @@ -226,6 +251,58 @@ fn discovery_enforces_each_source_cap_and_retains_the_first_omitted_rotation_gap ); } +#[test] +fn discovery_marks_only_the_first_found_fragment_per_source_when_the_cap_is_zero() { + let result = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: 0, + observations: vec![ + observation( + ROOT_A, + "AppEnforce.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_A, + "AppEnforce.lo_", + SccmRotation::LoUnderscore, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_B, + "PolicyAgent.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), + observation( + ROOT_B, + "PolicyAgent.lo_", + SccmRotation::LoUnderscore, + SccmClientDiscoveryObservationState::Found, + ), + ], + }) + .expect("zero cap is an explicit per-source coverage boundary"); + + assert_eq!( + result + .declarations + .iter() + .map(|declaration| { + ( + declaration.root_handle.as_str(), + declaration.basename.as_str(), + declaration.state, + ) + }) + .collect::>(), + vec![ + (ROOT_A, "AppEnforce.log", SccmClientDiscoveryState::Capped), + (ROOT_B, "PolicyAgent.log", SccmClientDiscoveryState::Capped), + ] + ); +} + #[test] fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_identities() { let input = SccmClientDiscoveryInput { @@ -265,14 +342,18 @@ fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_ .expect("valid observations"); assert_eq!(result.declarations, reversed.declarations); - assert!(result - .declarations - .iter() - .any(|declaration| declaration.state == SccmClientDiscoveryState::AccessDenied)); - assert!(result - .declarations - .iter() - .any(|declaration| declaration.state == SccmClientDiscoveryState::NotFound)); + assert!( + result + .declarations + .iter() + .any(|declaration| declaration.state == SccmClientDiscoveryState::AccessDenied) + ); + assert!( + result + .declarations + .iter() + .any(|declaration| declaration.state == SccmClientDiscoveryState::NotFound) + ); let collisions = result .declarations @@ -481,6 +562,7 @@ fn discovery_rejects_conflicting_states_for_canonical_basename_aliases() { let error = discover_client_sources(&input) .expect_err("canonical aliases with conflicting state fail closed"); + assert_eq!(error, SccmClientDiscoveryError::ConflictingObservation); let mut reversed = input; reversed.observations.reverse(); assert_eq!( @@ -491,39 +573,69 @@ fn discovery_rejects_conflicting_states_for_canonical_basename_aliases() { } #[test] -fn discovery_coalesces_same_state_canonical_aliases_to_the_stable_basename() { - let input = SccmClientDiscoveryInput { - max_found_fragments_per_source: 8, - observations: vec![ - observation( +fn discovery_canonicalizes_supported_aliases_into_stable_physical_declarations() { + for (canonical_basename, alias, rotation, physical_basename) in [ + ( + "AppEnforce.log", + "appenforce.log", + SccmRotation::Current, + "AppEnforce.log", + ), + ( + "AppEnforce.log", + "appenforce.lo_", + SccmRotation::LoUnderscore, + "AppEnforce.lo_", + ), + ( + "AppEnforce.log", + "appenforce.log.7", + SccmRotation::Numbered(7), + "AppEnforce.log.7", + ), + ] { + let canonical = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: 8, + observations: vec![observation( ROOT_A, - "appenforce.log", - SccmRotation::Current, + physical_basename, + rotation.clone(), SccmClientDiscoveryObservationState::Found, - ), - observation( + )], + }) + .expect("canonical observation is supported"); + let alias = discover_client_sources(&SccmClientDiscoveryInput { + max_found_fragments_per_source: 8, + observations: vec![observation( ROOT_A, - "AppEnforce.log", - SccmRotation::Current, + alias, + rotation.clone(), SccmClientDiscoveryObservationState::Found, - ), - ], - }; - - let result = discover_client_sources(&input).expect("same-state aliases are one source"); - let reversed = discover_client_sources(&SccmClientDiscoveryInput { - max_found_fragments_per_source: input.max_found_fragments_per_source, - observations: input.observations.into_iter().rev().collect(), - }) - .expect("same-state aliases remain order independent"); + )], + }) + .expect("case-equivalent observation is supported"); - assert_eq!(result, reversed); - assert_eq!(result.declarations.len(), 1); - assert_eq!(result.declarations[0].basename, "AppEnforce.log"); - assert_eq!( - result.declarations[0].state, - SccmClientDiscoveryState::Discovered - ); + assert_eq!(alias.declarations, canonical.declarations); + let declaration = &alias.declarations[0]; + assert_eq!(declaration.basename, physical_basename); + assert_eq!( + declaration.artifact_id, + expected_physical_artifact_id( + &path_fingerprint(ROOT_A, canonical_basename), + &rotation, + physical_basename, + ) + ); + assert_eq!( + declaration.evidence_identity, + expected_evidence_identity(canonical_basename, ROOT_A, &rotation, physical_basename,) + ); + assert_eq!( + declaration.evidence_identity.as_bytes(), + canonical.declarations[0].evidence_identity.as_bytes(), + "equivalent aliases preserve byte-identical evidence IDs" + ); + } } #[test] @@ -553,7 +665,7 @@ fn discovery_rejects_observations_beyond_its_defensive_contract() { } #[test] -fn discovery_skips_supported_observations_with_malformed_root_handles() { +fn discovery_skips_malformed_roots_and_unsupported_basenames() { let result = discover_client_sources(&SccmClientDiscoveryInput { max_found_fragments_per_source: 8, observations: vec![ @@ -563,6 +675,12 @@ fn discovery_skips_supported_observations_with_malformed_root_handles() { SccmRotation::Current, SccmClientDiscoveryObservationState::Found, ), + observation( + ROOT_B, + "Unrelated.log", + SccmRotation::Current, + SccmClientDiscoveryObservationState::Found, + ), observation( ROOT_B, "PolicyAgent.log", From de0957d48905d453aa0f0accb03d901a0363d5af Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 10:34:17 -0400 Subject: [PATCH 18/18] fix(sccm): stabilize discovery normalization identities --- src-tauri/src/sccm/discovery.rs | 58 ++++++++++++++++-------- src-tauri/tests/sccm_client_discovery.rs | 34 ++++++-------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src-tauri/src/sccm/discovery.rs b/src-tauri/src/sccm/discovery.rs index ff2395d04..8786ba43b 100644 --- a/src-tauri/src/sccm/discovery.rs +++ b/src-tauri/src/sccm/discovery.rs @@ -7,14 +7,13 @@ use std::fmt; #[cfg(test)] use std::cell::Cell; -use cmtraceopen_parser::sccm::SccmRotation; -use sha2::{Digest, Sha256}; - use super::contract::{ - SccmManifestSourceState, canonical_client_source, catalog_entry_id, - expected_marker_artifact_id, expected_physical_artifact_id, logical_artifact_ids_for_basename, - root_handle_digest, rotation_order, rotation_segment, source_identity_digest, + canonical_client_source, catalog_entry_id, expected_marker_artifact_id, + expected_physical_artifact_id, logical_artifact_ids_for_basename, root_handle_digest, + rotation_order, rotation_segment, sha256_bytes, source_identity_digest, + SccmManifestSourceState, }; +use cmtraceopen_parser::sccm::SccmRotation; pub const MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS: usize = 4_096; /// Defensive bound for supplied observations. Native enumeration must report @@ -110,6 +109,7 @@ struct PhysicalObservationKey { struct NormalizedObservation<'a> { observation: &'a SccmClientDiscoveryObservation, canonical_basename: String, + logical_artifact_ids: Vec, } #[cfg(test)] @@ -130,7 +130,12 @@ pub fn discover_client_sources( let observations = normalize_observations(input)?; let mut found_per_source = BTreeMap::<(String, String), usize>::new(); let mut capped_sources = BTreeSet::<(String, String)>::new(); - let mut declarations = Vec::with_capacity(MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS); + let mut declarations = Vec::with_capacity( + input + .observations + .len() + .min(MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS), + ); let mut first_omitted: Option<(NormalizedObservation<'_>, SccmClientDiscoveryState)> = None; for observation in observations { @@ -208,9 +213,11 @@ fn normalize_observation( NORMALIZATION_OPERATIONS.with(|count| count.set(count.get() + 1)); root_handle_digest(&observation.root_handle)?; let canonical_basename = canonical_client_source(&observation.basename, &observation.rotation)?; + let logical_artifact_ids = logical_artifact_ids(&canonical_basename); Some(NormalizedObservation { observation, canonical_basename, + logical_artifact_ids, }) } @@ -249,10 +256,15 @@ fn candidate_from_observation(observation: &NormalizedObservation<'_>) -> Option &observation.canonical_basename, )?; + let mut physical_observation = observation.observation.clone(); + physical_observation.basename = physical_basename( + &observation.canonical_basename, + &physical_observation.rotation, + ); Some(Candidate { - observation: observation.observation.clone(), + observation: physical_observation, catalog_entry_id: catalog_entry_id(&observation.canonical_basename), - logical_artifact_ids: logical_artifact_ids(&observation.canonical_basename), + logical_artifact_ids: observation.logical_artifact_ids.clone(), source_digest, }) } @@ -337,22 +349,15 @@ fn evidence_id( "cmtraceopen.sccm.evidence.v1\0{catalog_entry_id}\0{source_digest}\0{}\0{basename}", rotation_segment(rotation) ); - let digest = Sha256::digest(value.as_bytes()); - format!( - "sccm-evidence:v1:sha256:{}", - digest - .iter() - .map(|byte| format!("{byte:02x}")) - .collect::() - ) + format!("sccm-evidence:v1:sha256:{}", sha256_bytes(value.as_bytes())) } fn compare_observation_order( left: &NormalizedObservation<'_>, right: &NormalizedObservation<'_>, ) -> Ordering { - logical_artifact_ids(&left.canonical_basename) - .cmp(&logical_artifact_ids(&right.canonical_basename)) + left.logical_artifact_ids + .cmp(&right.logical_artifact_ids) .then_with(|| { left.observation .root_handle @@ -369,6 +374,21 @@ fn logical_artifact_ids(canonical_basename: &str) -> Vec { logical_artifact_ids_for_basename(canonical_basename) } +fn physical_basename(canonical_basename: &str, rotation: &SccmRotation) -> String { + match rotation { + SccmRotation::Current => canonical_basename.to_owned(), + SccmRotation::LoUnderscore => { + let stem = canonical_basename + .strip_suffix(".log") + .expect("prevalidated lo_ rotation has a canonical log basename"); + format!("{stem}.lo_") + } + SccmRotation::Numbered(number) => format!("{canonical_basename}.{number}"), + SccmRotation::Timestamped(timestamp) => format!("{canonical_basename}.{timestamp}"), + SccmRotation::Unknown(_) => unreachable!("supported observation has a known rotation"), + } +} + fn state_rank(state: SccmClientDiscoveryObservationState) -> u8 { match state { SccmClientDiscoveryObservationState::Found => 0, diff --git a/src-tauri/tests/sccm_client_discovery.rs b/src-tauri/tests/sccm_client_discovery.rs index 98580c3e0..35993df0d 100644 --- a/src-tauri/tests/sccm_client_discovery.rs +++ b/src-tauri/tests/sccm_client_discovery.rs @@ -1,7 +1,7 @@ use app_lib::sccm::{ + discover_client_sources, SccmClientDiscoveryError, SccmClientDiscoveryInput, + SccmClientDiscoveryObservation, SccmClientDiscoveryObservationState, SccmClientDiscoveryState, MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS, MAX_SCCM_CLIENT_DISCOVERY_OBSERVATIONS, - SccmClientDiscoveryError, SccmClientDiscoveryInput, SccmClientDiscoveryObservation, - SccmClientDiscoveryObservationState, SccmClientDiscoveryState, discover_client_sources, }; use cmtraceopen_parser::sccm::SccmRotation; use sha2::{Digest, Sha256}; @@ -184,12 +184,10 @@ fn discovery_at_the_exact_global_boundary_does_not_manufacture_a_gap() { result.declarations.len(), MAX_SCCM_CLIENT_DISCOVERY_DECLARATIONS ); - assert!( - result - .declarations - .iter() - .all(|declaration| declaration.state == SccmClientDiscoveryState::Discovered) - ); + assert!(result + .declarations + .iter() + .all(|declaration| declaration.state == SccmClientDiscoveryState::Discovered)); } #[test] @@ -342,18 +340,14 @@ fn discovery_preserves_denied_and_not_found_coverage_with_stable_collision_safe_ .expect("valid observations"); assert_eq!(result.declarations, reversed.declarations); - assert!( - result - .declarations - .iter() - .any(|declaration| declaration.state == SccmClientDiscoveryState::AccessDenied) - ); - assert!( - result - .declarations - .iter() - .any(|declaration| declaration.state == SccmClientDiscoveryState::NotFound) - ); + assert!(result + .declarations + .iter() + .any(|declaration| declaration.state == SccmClientDiscoveryState::AccessDenied)); + assert!(result + .declarations + .iter() + .any(|declaration| declaration.state == SccmClientDiscoveryState::NotFound)); let collisions = result .declarations