From 6134c623f7fb5f2a7a3c5c24fc8346da8cd9955f Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 09:50:09 -0400 Subject: [PATCH 01/18] test(parser): cover server coverage topology handles --- .../tests/sccm_server_intake.rs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_intake.rs b/crates/cmtraceopen-parser/tests/sccm_server_intake.rs index cb0f5aaa9..874b9127c 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_intake.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_intake.rs @@ -1057,6 +1057,56 @@ fn server_intake_scopes_canonical_identity_to_producer_host() { ); } +#[test] +fn server_intake_coverage_binds_each_row_to_its_producer_host() { + let (manifest_json, payloads) = load_bundle("collision-same-basename-configured-roots"); + let mut manifest = manifest_value(&manifest_json); + let fingerprint = + manifest["artifacts"][0]["configuredPathProvenance"]["pathFingerprint"].clone(); + let lineage = manifest["artifacts"][0]["rotation"]["lineageId"].clone(); + manifest["artifacts"][0]["producerHostHandle"] = + Value::String("synthetic:host:site-01".to_owned()); + manifest["artifacts"][1]["configuredPathProvenance"]["pathFingerprint"] = fingerprint; + manifest["artifacts"][1]["rotation"]["lineageId"] = lineage; + + let assessment = assess_server_intake(&serialize_manifest(&manifest), &payloads) + .expect("the same source captured on distinct producer hosts is assessed"); + let serialized = serde_json::to_value(&assessment).expect("assessment serializes"); + assert_eq!( + serialized["coverage"], + json!([ + { + "producerRole": "managementPoint", + "producerHostHandle": "synthetic:host:mp-01", + "sourceId": "server-mp-policy", + "state": "captured", + "artifactIds": ["mp-policy-root-b-current"], + }, + { + "producerRole": "managementPoint", + "producerHostHandle": "synthetic:host:site-01", + "sourceId": "server-mp-policy", + "state": "captured", + "artifactIds": ["mp-policy-root-a-current"], + }, + ]), + "coverage membership must retain the physical producer that supplied each artifact", + ); + + let mut reordered_manifest = manifest.clone(); + reordered_manifest["artifacts"] + .as_array_mut() + .expect("artifacts are an array") + .reverse(); + let reordered = assess_server_intake(&serialize_manifest(&reordered_manifest), &payloads) + .expect("reordered distinct-host artifacts are assessed"); + assert_eq!( + serde_json::to_vec(&assessment).expect("assessment serializes"), + serde_json::to_vec(&reordered).expect("reordered assessment serializes"), + "topology-bound coverage must be byte-stable across manifest ordering", + ); +} + #[test] fn server_intake_scopes_path_fingerprint_lineage_to_producer_host() { let (manifest_json, payloads) = load_bundle("collision-same-basename-configured-roots"); @@ -1137,6 +1187,92 @@ fn server_intake_scopes_canonical_identity_to_workflow_subject() { ); } +#[test] +fn server_intake_coverage_binds_each_row_to_its_workflow_subject() { + let (manifest_json, payloads) = load_bundle("complete-multi-role"); + let mut manifest = manifest_value(&manifest_json); + configure_second_artifact_as_dp_identity(&mut manifest, "synthetic:subject:dp-02", false); + + let assessment = assess_server_intake(&serialize_manifest(&manifest), &payloads) + .expect("the same source captured for distinct workflow subjects is assessed"); + let serialized = serde_json::to_value(&assessment).expect("assessment serializes"); + assert_eq!( + serialized["coverage"], + json!([ + { + "producerRole": "managementPoint", + "producerHostHandle": "synthetic:host:mp-01", + "sourceId": "server-mp-policy", + "state": "captured", + "artifactIds": ["mp-policy-current"], + }, + { + "producerRole": "siteServer", + "producerHostHandle": "synthetic:host:site-01", + "workflowSubjectRole": "distributionPoint", + "workflowSubjectHandle": "synthetic:subject:dp-01", + "sourceId": "server-dp-distribution", + "state": "captured", + "artifactIds": ["dp-dist-current"], + }, + { + "producerRole": "siteServer", + "producerHostHandle": "synthetic:host:site-01", + "workflowSubjectRole": "distributionPoint", + "workflowSubjectHandle": "synthetic:subject:dp-02", + "sourceId": "server-dp-distribution", + "state": "captured", + "artifactIds": ["sup-sync-current"], + }, + { + "producerRole": "siteServer", + "producerHostHandle": "synthetic:host:site-01", + "sourceId": "server-sitecomp", + "state": "captured", + "artifactIds": ["sitecomp-current"], + }, + ]), + "coverage membership must retain the exact workflow subject for each DP artifact", + ); + + let mut reordered_manifest = manifest.clone(); + reordered_manifest["artifacts"] + .as_array_mut() + .expect("artifacts are an array") + .reverse(); + let reordered = assess_server_intake(&serialize_manifest(&reordered_manifest), &payloads) + .expect("reordered distinct-subject artifacts are assessed"); + assert_eq!( + serde_json::to_vec(&assessment).expect("assessment serializes"), + serde_json::to_vec(&reordered).expect("reordered assessment serializes"), + "topology-bound coverage must be byte-stable across manifest ordering", + ); +} + +#[test] +fn server_intake_coverage_omits_absent_optional_topology_handles() { + let (manifest_json, payloads) = load_bundle("complete-multi-role"); + let assessment = assess_server_intake(&manifest_json, &payloads) + .expect("complete bundle is assessed"); + let serialized = serde_json::to_value(&assessment).expect("assessment serializes"); + let management_point = serialized["coverage"] + .as_array() + .expect("coverage is an array") + .iter() + .find(|row| row["sourceId"] == "server-mp-policy") + .expect("management-point coverage is present"); + + assert_eq!( + management_point["producerHostHandle"], + Value::String("synthetic:host:mp-01".to_owned()), + "present producer topology stays additive in coverage JSON", + ); + assert!( + management_point.get("workflowSubjectHandle").is_none(), + "an absent optional workflow handle must not alter legacy coverage JSON", + ); +} + #[test] fn server_intake_scopes_path_fingerprint_lineage_to_workflow_subject() { let (manifest_json, payloads) = load_bundle("complete-multi-role"); From 0fa1aeb8eafb8bec8b3263deffa07e73cf1ec508 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 09:51:55 -0400 Subject: [PATCH 02/18] fix(parser): bind server coverage to topology handles --- .../src/sccm/server/windows/intake.rs | 14 ++++++++++++-- .../cmtraceopen-parser/tests/sccm_server_intake.rs | 8 ++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs index 08bcb3d42..304e88bd5 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs @@ -478,7 +478,11 @@ pub enum SccmServerConfiguredPathClass { #[serde(rename_all = "camelCase")] pub struct SccmServerCoverage { pub producer_role: SccmRole, + #[serde(skip_serializing_if = "Option::is_none")] + pub producer_host_handle: Option, pub workflow_subject_role: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub workflow_subject_handle: Option, pub source_id: String, pub state: SccmCoverageState, pub artifact_ids: Vec, @@ -592,8 +596,10 @@ pub fn assess_server_intake( let mut artifacts = Vec::with_capacity(prepared.len()); let mut evidence = Vec::new(); - let mut coverage_by_key: BTreeMap<(String, String, String, String), SccmServerCoverage> = - BTreeMap::new(); + let mut coverage_by_key: BTreeMap< + (String, String, String, String, String, String), + SccmServerCoverage, + > = BTreeMap::new(); let mut request_keys = BTreeSet::new(); let mut next_artifact_requests = Vec::new(); let usable_source_keys = prepared @@ -608,6 +614,7 @@ pub fn assess_server_intake( let artifact = prepared_artifact.assessment; let coverage_key = ( role_sort_key(&artifact.producer_role).to_owned(), + artifact.producer_host_handle.clone().unwrap_or_default(), artifact.source_id.clone(), artifact .workflow_subject_role @@ -615,6 +622,7 @@ pub fn assess_server_intake( .map(role_sort_key) .unwrap_or_default() .to_owned(), + artifact.workflow_subject_handle.clone().unwrap_or_default(), coverage_sort_key(&artifact.state).to_owned(), ); coverage_by_key @@ -622,7 +630,9 @@ pub fn assess_server_intake( .and_modify(|row| row.artifact_ids.push(artifact.artifact_id.clone())) .or_insert_with(|| SccmServerCoverage { producer_role: artifact.producer_role.clone(), + producer_host_handle: artifact.producer_host_handle.clone(), workflow_subject_role: artifact.workflow_subject_role.clone(), + workflow_subject_handle: artifact.workflow_subject_handle.clone(), source_id: artifact.source_id.clone(), state: artifact.state.clone(), artifact_ids: vec![artifact.artifact_id.clone()], diff --git a/crates/cmtraceopen-parser/tests/sccm_server_intake.rs b/crates/cmtraceopen-parser/tests/sccm_server_intake.rs index 874b9127c..ba12f3517 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_intake.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_intake.rs @@ -1078,6 +1078,7 @@ fn server_intake_coverage_binds_each_row_to_its_producer_host() { { "producerRole": "managementPoint", "producerHostHandle": "synthetic:host:mp-01", + "workflowSubjectRole": null, "sourceId": "server-mp-policy", "state": "captured", "artifactIds": ["mp-policy-root-b-current"], @@ -1085,6 +1086,7 @@ fn server_intake_coverage_binds_each_row_to_its_producer_host() { { "producerRole": "managementPoint", "producerHostHandle": "synthetic:host:site-01", + "workflowSubjectRole": null, "sourceId": "server-mp-policy", "state": "captured", "artifactIds": ["mp-policy-root-a-current"], @@ -1202,6 +1204,7 @@ fn server_intake_coverage_binds_each_row_to_its_workflow_subject() { { "producerRole": "managementPoint", "producerHostHandle": "synthetic:host:mp-01", + "workflowSubjectRole": null, "sourceId": "server-mp-policy", "state": "captured", "artifactIds": ["mp-policy-current"], @@ -1227,6 +1230,7 @@ fn server_intake_coverage_binds_each_row_to_its_workflow_subject() { { "producerRole": "siteServer", "producerHostHandle": "synthetic:host:site-01", + "workflowSubjectRole": null, "sourceId": "server-sitecomp", "state": "captured", "artifactIds": ["sitecomp-current"], @@ -1252,8 +1256,8 @@ fn server_intake_coverage_binds_each_row_to_its_workflow_subject() { #[test] fn server_intake_coverage_omits_absent_optional_topology_handles() { let (manifest_json, payloads) = load_bundle("complete-multi-role"); - let assessment = assess_server_intake(&manifest_json, &payloads) - .expect("complete bundle is assessed"); + let assessment = + assess_server_intake(&manifest_json, &payloads).expect("complete bundle is assessed"); let serialized = serde_json::to_value(&assessment).expect("assessment serializes"); let management_point = serialized["coverage"] .as_array() From ef31f38b6ddecf0397469d122b712c0f51822ee1 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 10:01:14 -0400 Subject: [PATCH 03/18] test(parser): reject incongruent server coverage topology --- .../tests/sccm_server_site_core.rs | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index 4cfaaeb88..23efe14bd 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -1850,3 +1850,138 @@ fn intake_coverage_must_be_congruent_before_facts_can_shape_results() { assert!(!analysis.unlinked_observations.is_empty()); assert!(!analysis.artifact_requests.is_empty()); } + +fn assert_topology_incongruence_fails_closed( + analysis: &SccmSiteCoreAnalysis, + expected_gaps: &[(&str, &str, &str)], +) { + assert!( + analysis.results.is_empty(), + "incongruent topology-bound coverage must not shape a result" + ); + assert_eq!(analysis.coverage_gaps.len(), expected_gaps.len()); + assert_eq!( + analysis.unlinked_observations.len(), + expected_gaps.len(), + "every topology mismatch stays visible as a coverage observation" + ); + assert_eq!( + analysis.findings.len(), + expected_gaps.len(), + "every topology mismatch stays visible as a validated finding" + ); + + for (artifact_id, source_id, producer_host_handle) in expected_gaps { + let gap = analysis + .coverage_gaps + .iter() + .find(|gap| gap.artifact_id == *artifact_id) + .expect("topology-specific coverage gap"); + assert_eq!(gap.source_id, *source_id); + assert_eq!(gap.state, SccmCoverageState::ParseFailed); + assert_eq!(gap.reason_code, "intake-coverage-incongruent"); + + let observation = analysis + .unlinked_observations + .iter() + .find(|observation| observation.coverage_gap_artifact_ids == [artifact_id.to_string()]) + .expect("coverage gap has an explicit observation"); + assert_eq!( + observation.finding_class, + SccmFindingClass::InsufficientEvidence + ); + + let finding = analysis + .findings + .iter() + .find(|finding| finding.subject_id == observation.observation_id) + .expect("coverage observation has a validated finding"); + assert_eq!( + finding.finding.class, + SccmFindingClass::InsufficientEvidence + ); + assert!(finding + .finding + .coverage_gaps + .iter() + .any(|finding_gap| finding_gap.artifact_id == *artifact_id)); + + let request = analysis + .artifact_requests + .iter() + .find(|request| request.logical_name == *source_id) + .expect("topology-specific gap has a bounded request"); + assert_eq!( + request.scope.producer_host_handle.as_deref(), + Some(*producer_host_handle), + "the request stays scoped to artifact topology, not mutated coverage topology" + ); + assert_bounded_request_has_specific_scope(request); + } +} + +#[test] +fn swapped_coverage_producer_hosts_fail_site_core_congruence_closed() { + let mut assessment = assess(&[ + Source::sitecomp(HEALTHY_SITECOMP), + Source::status(HEALTHY_STATUS), + ]); + assessment + .artifacts + .iter_mut() + .find(|artifact| artifact.source_id == "server-status") + .expect("status artifact") + .producer_host_handle = Some("synthetic:host:site-02".to_owned()); + assessment + .coverage + .iter_mut() + .find(|coverage| coverage.source_id == "server-sitecomp") + .expect("sitecomp coverage") + .producer_host_handle = Some("synthetic:host:site-02".to_owned()); + assessment + .coverage + .iter_mut() + .find(|coverage| coverage.source_id == "server-status") + .expect("status coverage") + .producer_host_handle = Some("synthetic:host:site-01".to_owned()); + + let analysis = analyze_site_core(&assessment); + assert_topology_incongruence_fails_closed( + &analysis, + &[ + ( + "sitecomp-current", + "server-sitecomp", + "synthetic:host:site-01", + ), + ("z-site-status", "server-status", "synthetic:host:site-02"), + ], + ); +} + +#[test] +fn changed_coverage_workflow_subject_handle_fails_site_core_congruence_closed() { + let mut assessment = assess(&[ + Source::sitecomp(HEALTHY_SITECOMP), + Source::status(HEALTHY_STATUS), + ]); + assessment + .coverage + .iter_mut() + .find(|coverage| coverage.source_id == "server-sitecomp") + .expect("sitecomp coverage") + .workflow_subject_handle = Some("synthetic:subject:site-core-01".to_owned()); + + let analysis = analyze_site_core(&assessment); + assert_topology_incongruence_fails_closed( + &analysis, + &[ + ( + "sitecomp-current", + "server-sitecomp", + "synthetic:host:site-01", + ), + ("z-site-status", "server-status", "synthetic:host:site-01"), + ], + ); +} From f6d9e9f39a541051a73bf2d51ff23c61eff83f73 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 10:04:21 -0400 Subject: [PATCH 04/18] fix(parser): enforce server coverage topology congruence --- CHANGELOG.md | 10 ++++ .../src/sccm/server/windows/intake.rs | 10 ++++ .../src/sccm/server/windows/site_core.rs | 17 +++++-- .../tests/sccm_server_site_core.rs | 47 +++++++++---------- .../preparation/issue-335-server-intake.md | 13 +++++ 5 files changed, 70 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dca96054..fd21abafb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Fixed + +- **SCCM server coverage topology (#335)**: Normalized server coverage rows now + retain optional opaque producer-host and workflow-subject handles, preventing + artifacts from distinct physical producers or workflow subjects from + collapsing into one row. Site-core analysis also rejects coverage whose + topology does not match its artifact membership and emits explicit coverage + gaps instead of shaping results from incongruent input. The additive fields + remain schema v1 and are omitted from JSON when absent. + ## [1.5.0] - 2026-07-27 ### Added diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs index 304e88bd5..db92ee225 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs @@ -474,6 +474,16 @@ pub enum SccmServerConfiguredPathClass { NonDefault, } +/// A deterministic artifact-membership row in server intake assessment schema v1. +/// +/// Rows emitted by [`assess_server_intake`] bind membership to the validated, +/// privacy-safe producer and workflow-subject topology. The two handle fields +/// are optional additive schema-v1 JSON fields and are omitted when absent; +/// neither contains a raw host name or path. +/// +/// Adding these public fields is not Rust struct-literal source compatible, +/// and strict JSON consumers that reject unknown fields must recognize them +/// before consuming rows where they are present. #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SccmServerCoverage { diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs index 7347eeb3c..9683b829d 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs @@ -1934,7 +1934,14 @@ fn evidence_collision_artifact_ids( } fn site_core_coverage_is_congruent(intake: &SccmServerIntakeAssessment) -> bool { - type CoverageKey = (String, String, String, String); + type CoverageKey = ( + String, + Option, + String, + String, + Option, + String, + ); let mut expected = BTreeMap::>::new(); for artifact in &intake.artifacts { @@ -1944,13 +1951,15 @@ fn site_core_coverage_is_congruent(intake: &SccmServerIntakeAssessment) -> bool expected .entry(( role_sort_key(&artifact.producer_role).to_owned(), + artifact.producer_host_handle.clone(), + artifact.source_id.clone(), artifact .workflow_subject_role .as_ref() .map(role_sort_key) .unwrap_or_default() .to_owned(), - artifact.source_id.clone(), + artifact.workflow_subject_handle.clone(), coverage_sort_key(&artifact.state).to_owned(), )) .or_default() @@ -1965,13 +1974,15 @@ fn site_core_coverage_is_congruent(intake: &SccmServerIntakeAssessment) -> bool observed .entry(( role_sort_key(&coverage.producer_role).to_owned(), + coverage.producer_host_handle.clone(), + coverage.source_id.clone(), coverage .workflow_subject_role .as_ref() .map(role_sort_key) .unwrap_or_default() .to_owned(), - coverage.source_id.clone(), + coverage.workflow_subject_handle.clone(), coverage_sort_key(&coverage.state).to_owned(), )) .or_default() diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index 23efe14bd..8d0a4e9e0 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -319,6 +319,25 @@ fn replace_source_artifact_id( } } +fn replace_source_producer_host( + assessment: &mut SccmServerIntakeAssessment, + source_id: &str, + replacement: &str, +) { + let artifact = assessment + .artifacts + .iter_mut() + .find(|artifact| artifact.source_id == source_id) + .expect("source artifact"); + artifact.producer_host_handle = Some(replacement.to_owned()); + let coverage = assessment + .coverage + .iter_mut() + .find(|coverage| coverage.artifact_ids.contains(&artifact.artifact_id)) + .expect("source coverage"); + coverage.producer_host_handle = Some(replacement.to_owned()); +} + fn assert_bounded_request_has_specific_scope(request: &SccmSiteCoreArtifactRequest) { assert!((1..=2).contains(&request.max_artifacts)); assert!(!request.candidates.is_empty()); @@ -797,12 +816,7 @@ fn unrelated_same_minute_components_and_producer_hosts_never_merge() { Source::sitecomp(HEALTHY_SITECOMP), Source::status(HEALTHY_STATUS), ]); - split_hosts - .artifacts - .iter_mut() - .find(|artifact| artifact.source_id == "server-status") - .expect("status artifact") - .producer_host_handle = Some("synthetic:host:site-02".to_owned()); + replace_source_producer_host(&mut split_hosts, "server-status", "synthetic:host:site-02"); let split = analyze_site_core(&split_hosts); assert_eq!(split.results.len(), 2); assert_ne!( @@ -823,12 +837,7 @@ fn unrelated_same_minute_components_and_producer_hosts_never_merge() { .any(|result| { result.transaction_key.producer_host_handle == "synthetic:host:site-02" })); let mut foreign_gap = assess(&[Source::sitecomp(HEALTHY_SITECOMP), Source::absent_status()]); - foreign_gap - .artifacts - .iter_mut() - .find(|artifact| artifact.source_id == "server-status") - .expect("status artifact") - .producer_host_handle = Some("synthetic:host:site-02".to_owned()); + replace_source_producer_host(&mut foreign_gap, "server-status", "synthetic:host:site-02"); let foreign_gap_analysis = analyze_site_core(&foreign_gap); assert_eq!(foreign_gap_analysis.results.len(), 1); assert_eq!( @@ -1210,12 +1219,7 @@ fn undeclared_component_gap_does_not_attach_across_producer_hosts() { Source::status(HEALTHY_STATUS), Source::sitecomp(HEALTHY_SITECOMP), ]); - assessment - .artifacts - .iter_mut() - .find(|artifact| artifact.source_id == "server-sitecomp") - .expect("component artifact") - .producer_host_handle = Some("synthetic:host:site-02".to_owned()); + replace_source_producer_host(&mut assessment, "server-sitecomp", "synthetic:host:site-02"); let analysis = analyze_site_core(&assessment); let status_only_result = analysis @@ -1926,12 +1930,7 @@ fn swapped_coverage_producer_hosts_fail_site_core_congruence_closed() { Source::sitecomp(HEALTHY_SITECOMP), Source::status(HEALTHY_STATUS), ]); - assessment - .artifacts - .iter_mut() - .find(|artifact| artifact.source_id == "server-status") - .expect("status artifact") - .producer_host_handle = Some("synthetic:host:site-02".to_owned()); + replace_source_producer_host(&mut assessment, "server-status", "synthetic:host:site-02"); assessment .coverage .iter_mut() diff --git a/docs/sccm/preparation/issue-335-server-intake.md b/docs/sccm/preparation/issue-335-server-intake.md index 4c2551352..66ca37c08 100644 --- a/docs/sccm/preparation/issue-335-server-intake.md +++ b/docs/sccm/preparation/issue-335-server-intake.md @@ -158,6 +158,19 @@ Deferred native tests must make the write/privacy boundaries observable: ## Intake assessment rules +- Normalized schema-v1 coverage rows group by producer role, optional opaque + producer-host handle, source ID, optional workflow-subject role and opaque + instance handle, and capture state. Every `artifactId` in a row is therefore + bound to the exact topology retained by its normalized artifact; a role, + source, and state match alone cannot merge physical producers or workflow + subjects. +- `producerHostHandle` and `workflowSubjectHandle` are additive optional fields + on normalized schema-v1 coverage JSON and are omitted when absent. They retain + only intake-validated opaque handles, never raw host names or paths. This + additive change does not silently advance `schemaVersion`: Rust consumers + constructing `SccmServerCoverage` with struct literals must supply the new + fields, and strict JSON readers that reject unknown fields must add them to + their accepted schema before reading rows where they are present. - Classify by `(producer role/topology, source ID/basename, supported rotation, provenance)`, not filename, workflow subject, or default path alone. - Stable-normalize artifacts by producer role/host handle, source ID, From 912dc1d787e5d8e309ba86497feda0462782eae0 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 12:28:01 -0400 Subject: [PATCH 05/18] test(sccm): expose MP coverage handle seal gaps --- .../server/windows/management_point_tests.rs | 212 +++++++++++++++++- 1 file changed, 205 insertions(+), 7 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs index 7cbb63b5f..ba4de6f8f 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs @@ -188,11 +188,27 @@ fn load_bundle(scenario: &str) -> SccmManagementPointBundle { fn load_server_intake_fixture( directory: &Path, ) -> cmtraceopen_parser::sccm::server::windows::SccmServerIntakeAssessment { - let manifest_json = fs::read_to_string(directory.join("manifest.json")) - .expect("server intake fixture manifest must be readable"); - let manifest: Value = serde_json::from_str(&manifest_json) - .expect("server intake fixture manifest must be valid JSON"); - let payloads = manifest["artifacts"] + let manifest: Value = serde_json::from_str( + &fs::read_to_string(directory.join("manifest.json")) + .expect("server intake fixture manifest must be readable"), + ) + .expect("server intake fixture manifest must be valid JSON"); + assess_server_intake_manifest(directory, &manifest) +} + +fn assess_server_intake_manifest( + directory: &Path, + manifest: &Value, +) -> cmtraceopen_parser::sccm::server::windows::SccmServerIntakeAssessment { + assess_server_intake_manifest_with_payload_manifest(directory, manifest, manifest) +} + +fn assess_server_intake_manifest_with_payload_manifest( + directory: &Path, + manifest: &Value, + payload_manifest: &Value, +) -> cmtraceopen_parser::sccm::server::windows::SccmServerIntakeAssessment { + let payloads = payload_manifest["artifacts"] .as_array() .expect("canonical MP fixture artifacts") .iter() @@ -208,8 +224,11 @@ fn load_server_intake_fixture( }) }) .collect::>(); - assess_server_intake(&manifest_json, &payloads) - .expect("fixture must satisfy canonical server intake") + assess_server_intake( + &serde_json::to_string(manifest).expect("server intake fixture manifest serializes"), + &payloads, + ) + .expect("fixture must satisfy canonical server intake") } fn load_canonical_intake( @@ -822,6 +841,18 @@ fn canonical_intake_adapter_accepts_reordered_authoritative_records() { assessment.evidence.len() > 1, "fixture must exercise evidence reordering" ); + assert!( + assessment.coverage.iter().any(|record| { + record.producer_host_handle.is_some() && record.workflow_subject_handle.is_none() + }), + "fixture must retain rows with an absent optional workflow handle" + ); + assert!( + assessment.coverage.iter().any(|record| { + record.producer_host_handle.is_some() && record.workflow_subject_handle.is_some() + }), + "fixture must retain rows with both optional topology handles present" + ); let expected = analyze_management_point_from_server_intake(&assessment) .expect("canonical multi-role intake must enter the adapter"); @@ -836,6 +867,173 @@ fn canonical_intake_adapter_accepts_reordered_authoritative_records() { ); } +#[test] +fn canonical_intake_adapter_accepts_coverage_rows_distinguished_by_producer_host() { + let directory = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/sccm/server/intake/collision-same-basename-configured-roots"); + let mut manifest = load_json(&directory.join("manifest.json")); + let fingerprint = + manifest["artifacts"][0]["configuredPathProvenance"]["pathFingerprint"].clone(); + let lineage = manifest["artifacts"][0]["rotation"]["lineageId"].clone(); + manifest["artifacts"][1]["producerHostHandle"] = + Value::String("synthetic:host:site-01".to_owned()); + manifest["artifacts"][1]["configuredPathProvenance"]["pathFingerprint"] = fingerprint; + manifest["artifacts"][1]["rotation"]["lineageId"] = lineage; + + let assessment = assess_server_intake_manifest(&directory, &manifest); + let coverage = assessment + .coverage + .iter() + .filter(|record| record.source_id == "server-mp-policy") + .collect::>(); + assert_eq!( + coverage.len(), + 2, + "both physical producer rows are retained" + ); + assert_eq!( + coverage + .iter() + .map(|record| record.producer_host_handle.as_deref()) + .collect::>(), + vec![Some("synthetic:host:mp-01"), Some("synthetic:host:site-01")], + ); + + assert!( + matches!( + analyze_management_point_from_server_intake(&assessment), + Err(SccmManagementPointIntakeError::TopologyMismatch) + ), + "distinct producer-host coverage reaches MP topology validation rather than failing as an unbound intake projection" + ); + let mut reordered = assessment; + reordered.artifacts.reverse(); + reordered.coverage.reverse(); + reordered.evidence.reverse(); + assert!( + matches!( + analyze_management_point_from_server_intake(&reordered), + Err(SccmManagementPointIntakeError::TopologyMismatch) + ), + "producer-host-bound coverage order is not authority" + ); +} + +#[test] +fn canonical_intake_adapter_accepts_coverage_rows_distinguished_by_workflow_subject() { + let directory = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/sccm/server/intake/complete-multi-role"); + let mut manifest = load_json(&directory.join("manifest.json")); + let payload_manifest = manifest.clone(); + let fingerprint = + manifest["artifacts"][2]["configuredPathProvenance"]["pathFingerprint"].clone(); + let artifact = &mut manifest["artifacts"][3]; + artifact["workflowSubject"] = json!({ + "role": "distributionPoint", + "instanceHandle": "synthetic:subject:dp-02", + }); + artifact["sourceId"] = Value::String("server-dp-distribution".to_owned()); + artifact["originalPath"] = Value::String("REDACTED_SITE_DP_CONTROL_ROOT_COPY".to_owned()); + artifact["originalBasename"] = Value::String("distmgr.log".to_owned()); + artifact["configuredPathProvenance"]["pathFingerprint"] = fingerprint; + artifact["relativePath"] = Value::String( + "evidence/sccm/server/site-server/server-dp-distribution/subject-distribution-point/instance-bbbbbbbb/current/distmgr.log" + .to_owned(), + ); + + let assessment = assess_server_intake_manifest_with_payload_manifest( + &directory, + &manifest, + &payload_manifest, + ); + let coverage = assessment + .coverage + .iter() + .filter(|record| record.source_id == "server-dp-distribution") + .collect::>(); + assert_eq!(coverage.len(), 2, "both workflow-subject rows are retained"); + assert_eq!( + coverage + .iter() + .map(|record| record.workflow_subject_handle.as_deref()) + .collect::>(), + vec![ + Some("synthetic:subject:dp-01"), + Some("synthetic:subject:dp-02"), + ], + ); + + let expected = analyze_management_point_from_server_intake(&assessment) + .expect("distinct workflow-subject coverage remains adapter-authoritative"); + let mut reordered = assessment; + reordered.artifacts.reverse(); + reordered.coverage.reverse(); + reordered.evidence.reverse(); + assert_eq!( + analyze_management_point_from_server_intake(&reordered) + .expect("workflow-subject-bound coverage order is not authority"), + expected + ); +} + +#[test] +fn canonical_intake_adapter_rejects_post_intake_coverage_handle_mutations() { + let assessment = load_server_intake_scenario("complete-multi-role"); + let management_point_index = assessment + .coverage + .iter() + .position(|record| record.source_id == "server-mp-policy") + .expect("fixture has management-point coverage"); + let distribution_point_index = assessment + .coverage + .iter() + .position(|record| record.source_id == "server-dp-distribution") + .expect("fixture has distribution-point coverage"); + let software_update_point_index = assessment + .coverage + .iter() + .position(|record| record.source_id == "server-sup-sync") + .expect("fixture has software-update-point coverage"); + + let mut added = assessment.clone(); + added.coverage[management_point_index].workflow_subject_handle = + Some("synthetic:subject:mp-added".to_owned()); + assert_unbound_intake_projection(&added, "coverage handle addition mutation"); + + let mut removed_producer = assessment.clone(); + removed_producer.coverage[management_point_index].producer_host_handle = None; + assert_unbound_intake_projection( + &removed_producer, + "coverage producer-handle removal mutation", + ); + + let mut removed_subject = assessment.clone(); + removed_subject.coverage[distribution_point_index].workflow_subject_handle = None; + assert_unbound_intake_projection(&removed_subject, "coverage subject-handle removal mutation"); + + let mut swapped_producers = assessment.clone(); + let producer_handle = swapped_producers.coverage[management_point_index] + .producer_host_handle + .clone(); + swapped_producers.coverage[management_point_index].producer_host_handle = swapped_producers + .coverage[software_update_point_index] + .producer_host_handle + .clone(); + swapped_producers.coverage[software_update_point_index].producer_host_handle = producer_handle; + assert_unbound_intake_projection(&swapped_producers, "coverage producer-handle swap mutation"); + + let mut swapped_subjects = assessment; + let subject_handle = swapped_subjects.coverage[distribution_point_index] + .workflow_subject_handle + .clone(); + swapped_subjects.coverage[distribution_point_index].workflow_subject_handle = swapped_subjects + .coverage[software_update_point_index] + .workflow_subject_handle + .clone(); + swapped_subjects.coverage[software_update_point_index].workflow_subject_handle = subject_handle; + assert_unbound_intake_projection(&swapped_subjects, "coverage subject-handle swap mutation"); +} + #[test] fn canonical_intake_adapter_rejects_promoted_capped_profile_ineligible_metadata() { let directory = From eec87212288bb79c9b11a665c56e6b89077f55cf Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 2 Aug 2026 12:29:05 -0400 Subject: [PATCH 06/18] fix(sccm): seal MP coverage topology handles --- .../cmtraceopen-parser/src/sccm/server/windows/intake.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs index db92ee225..5e3b45f86 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs @@ -256,7 +256,9 @@ struct ArtifactIntegrityIdentity(String); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct CoverageIntegrityIdentity { producer_role: String, + producer_host_handle: String, workflow_subject_role: String, + workflow_subject_handle: String, source_id: String, state: String, } @@ -298,7 +300,9 @@ impl SccmServerIntakeIntegrity { .iter() .map(|(identity, record)| { identity.producer_role.len() + + identity.producer_host_handle.len() + identity.workflow_subject_role.len() + + identity.workflow_subject_handle.len() + identity.source_id.len() + identity.state.len() + std::mem::size_of_val(&record.payload_len) @@ -1330,10 +1334,12 @@ fn coverage_string_bytes(coverage: &[SccmServerCoverage]) -> Option { let mut total = 0usize; for record in coverage { checked_add_string_bytes(&mut total, role_sort_key(&record.producer_role))?; + checked_add_optional_string_bytes(&mut total, record.producer_host_handle.as_deref())?; checked_add_optional_string_bytes( &mut total, record.workflow_subject_role.as_ref().map(role_sort_key), )?; + checked_add_optional_string_bytes(&mut total, record.workflow_subject_handle.as_deref())?; checked_add_string_bytes(&mut total, &record.source_id)?; checked_add_string_bytes(&mut total, coverage_sort_key(&record.state))?; for artifact_id in &record.artifact_ids { @@ -1506,12 +1512,14 @@ fn canonical_intake_integrity_with_structure( for record in &normalized_coverage { let identity = CoverageIntegrityIdentity { producer_role: role_sort_key(&record.producer_role).to_owned(), + producer_host_handle: record.producer_host_handle.clone().unwrap_or_default(), workflow_subject_role: record .workflow_subject_role .as_ref() .map(role_sort_key) .unwrap_or_default() .to_owned(), + workflow_subject_handle: record.workflow_subject_handle.clone().unwrap_or_default(), source_id: record.source_id.clone(), state: coverage_sort_key(&record.state).to_owned(), }; From 8bbf15c66b3a67ef87675b2e1144e77008bb3480 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 10:39:26 -0400 Subject: [PATCH 07/18] test(sccm): reject mutated site-core topology authority --- .../tests/sccm_server_site_core.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index 8d0a4e9e0..0e833a2e4 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -1984,3 +1984,67 @@ fn changed_coverage_workflow_subject_handle_fails_site_core_congruence_closed() ], ); } + +#[test] +fn post_intake_topology_mutations_fail_site_core_authority_closed() { + let assessment = assess(&[ + Source::sitecomp(HEALTHY_SITECOMP), + Source::status(HEALTHY_STATUS), + ]); + assert_eq!( + analyze_site_core(&assessment).results[0].state, + SccmSiteCoreState::Healthy, + "the canonical control assessment must exercise normal fact reduction" + ); + + let mut changed_site_handle = assessment.clone(); + changed_site_handle.topology.site_handle = "synthetic:site:other".to_owned(); + + let mut changed_capture_host = assessment.clone(); + changed_capture_host.topology.capture_host_handle = "synthetic:host:site-02".to_owned(); + + let mut changed_observed_roles = assessment; + changed_observed_roles + .topology + .roles_observed + .push(SccmRole::ManagementPoint); + + let analyses = [ + ("site handle", analyze_site_core(&changed_site_handle)), + ("capture host", analyze_site_core(&changed_capture_host)), + ("observed roles", analyze_site_core(&changed_observed_roles)), + ]; + assert_eq!( + analyses + .iter() + .map(|(name, analysis)| ( + *name, + analysis.results.len(), + analysis.coverage_gaps.len(), + analysis.unlinked_observations.len(), + analysis.findings.len(), + analysis.artifact_requests.len(), + )) + .collect::>(), + vec![ + ("site handle", 0, 2, 2, 2, 2), + ("capture host", 0, 2, 2, 2, 2), + ("observed roles", 0, 2, 2, 2, 2), + ], + "no caller-mutated topology may retain normal site-core facts" + ); + + for (_, analysis) in &analyses { + assert_topology_incongruence_fails_closed( + analysis, + &[ + ( + "sitecomp-current", + "server-sitecomp", + "synthetic:host:site-01", + ), + ("z-site-status", "server-status", "synthetic:host:site-01"), + ], + ); + } +} From e4378f88eedca4d2450da90180a5454e86dd740d Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 10:41:07 -0400 Subject: [PATCH 08/18] fix(sccm): bind site-core to intake topology authority --- .../src/sccm/server/windows/intake.rs | 28 +++++++++++++++++++ .../src/sccm/server/windows/site_core.rs | 3 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs index 5e3b45f86..e23ffc0dc 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs @@ -194,6 +194,34 @@ impl SccmServerIntakeAssessment { &self.privacy_extensions } + pub(crate) fn topology_authority_is_intake_bound(&self) -> bool { + if self.schema_version != self.intake_integrity.schema_version + || self.topology.roles_observed.len() != self.intake_integrity.topology_role_count + || topology_string_bytes(&self.topology) + != Some(self.intake_integrity.structure.topology_string_bytes) + { + return false; + } + let mut normalized_topology = self.topology.clone(); + normalized_topology + .roles_observed + .sort_by(|left, right| role_sort_key(left).cmp(role_sort_key(right))); + if normalized_topology + .roles_observed + .windows(2) + .any(|roles| roles[0] == roles[1]) + { + return false; + } + canonical_record_digest_bounded( + b"topology", + &normalized_topology, + Some(self.intake_integrity.topology.payload_len), + ) + .as_ref() + .is_some_and(|topology| topology == &self.intake_integrity.topology) + } + pub(crate) fn adapter_authority_is_intake_bound(&self) -> bool { if self.schema_version != self.intake_integrity.schema_version || self.topology.roles_observed.len() != self.intake_integrity.topology_role_count diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs index 9683b829d..c4d8b57ed 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs @@ -308,7 +308,8 @@ impl<'a> SiteCoreContext<'a> { evidence_collision_artifact_ids(&intake.evidence, &evidence_identity_is_unique); let (evidence_source_rejections, unresolved_evidence_gaps) = evidence_source_rejections(intake); - let coverage_congruent = site_core_coverage_is_congruent(intake); + let coverage_congruent = + intake.topology_authority_is_intake_bound() && site_core_coverage_is_congruent(intake); let sources = admitted_sources( intake, &collision_artifact_ids, From ab5fe5363bcccc36c66ad44b5d15bc03196e8479 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 11:41:52 -0400 Subject: [PATCH 09/18] test(sccm): reject forged site-core producer authority --- .../tests/sccm_server_site_core.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index 0e833a2e4..bf03d3a80 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -1855,6 +1855,29 @@ fn intake_coverage_must_be_congruent_before_facts_can_shape_results() { assert!(!analysis.artifact_requests.is_empty()); } +#[test] +fn coordinated_post_intake_producer_host_mutation_fails_site_core_authority_closed() { + let mut assessment = assess(&[ + Source::sitecomp(HEALTHY_SITECOMP), + Source::status(HEALTHY_STATUS), + ]); + replace_source_producer_host(&mut assessment, "server-sitecomp", "synthetic:host:forged"); + replace_source_producer_host(&mut assessment, "server-status", "synthetic:host:forged"); + + let analysis = analyze_site_core(&assessment); + assert_topology_incongruence_fails_closed( + &analysis, + &[ + ( + "sitecomp-current", + "server-sitecomp", + "synthetic:host:forged", + ), + ("z-site-status", "server-status", "synthetic:host:forged"), + ], + ); +} + fn assert_topology_incongruence_fails_closed( analysis: &SccmSiteCoreAnalysis, expected_gaps: &[(&str, &str, &str)], From 7390faa67a4bed9c972d7f3d2acaf3fe29bd7f10 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 11:51:31 -0400 Subject: [PATCH 10/18] fix(sccm): seal site-core intake authority --- .../src/sccm/server/windows/site_core.rs | 31 +- .../tests/sccm_server_site_core.rs | 335 ++++++------------ 2 files changed, 125 insertions(+), 241 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs index c4d8b57ed..7cc2c8a68 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs @@ -296,6 +296,7 @@ struct AdmittedSource<'a> { struct SiteCoreContext<'a> { artifacts: &'a [SccmServerArtifactAssessment], sources: BTreeMap<&'a str, AdmittedSource<'a>>, + intake_authority_is_bound: bool, evidence_identity_is_unique: Vec, coverage_gaps: Vec, coverage_gap_producer_hosts: BTreeMap, @@ -303,13 +304,25 @@ struct SiteCoreContext<'a> { impl<'a> SiteCoreContext<'a> { fn new(intake: &'a SccmServerIntakeAssessment) -> Self { - let evidence_identity_is_unique = unique_evidence_identities(&intake.evidence); - let collision_artifact_ids = - evidence_collision_artifact_ids(&intake.evidence, &evidence_identity_is_unique); - let (evidence_source_rejections, unresolved_evidence_gaps) = - evidence_source_rejections(intake); - let coverage_congruent = - intake.topology_authority_is_intake_bound() && site_core_coverage_is_congruent(intake); + let intake_authority_is_bound = intake.adapter_authority_is_intake_bound(); + let evidence_identity_is_unique = if intake_authority_is_bound { + unique_evidence_identities(&intake.evidence) + } else { + vec![false; intake.evidence.len()] + }; + let collision_artifact_ids = if intake_authority_is_bound { + evidence_collision_artifact_ids(&intake.evidence, &evidence_identity_is_unique) + } else { + BTreeSet::new() + }; + let (evidence_source_rejections, unresolved_evidence_gaps) = if intake_authority_is_bound { + evidence_source_rejections(intake) + } else { + (BTreeMap::new(), Vec::new()) + }; + let coverage_congruent = intake_authority_is_bound + && intake.topology_authority_is_intake_bound() + && site_core_coverage_is_congruent(intake); let sources = admitted_sources( intake, &collision_artifact_ids, @@ -322,6 +335,7 @@ impl<'a> SiteCoreContext<'a> { Self { artifacts: &intake.artifacts, sources, + intake_authority_is_bound, evidence_identity_is_unique, coverage_gaps, coverage_gap_producer_hosts: BTreeMap::new(), @@ -390,6 +404,9 @@ pub fn analyze_site_core(intake: &SccmServerIntakeAssessment) -> SccmSiteCoreAna let mut grouped = BTreeMap::>::new(); let mut record_observations = Vec::new(); for (position, evidence) in intake.evidence.iter().enumerate() { + if !context.intake_authority_is_bound { + break; + } let Some(source) = context.sources.get(evidence.reference.artifact_id.as_str()) else { continue; }; diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index bf03d3a80..974c2bad3 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -267,6 +267,26 @@ impl<'a> Source<'a> { } fn assess(sources: &[Source<'_>]) -> SccmServerIntakeAssessment { + assess_with_producer_hosts(sources, &[]) +} + +fn assess_with_producer_hosts( + sources: &[Source<'_>], + producer_hosts: &[(&str, &str)], +) -> SccmServerIntakeAssessment { + let artifacts = sources + .iter() + .map(|source| { + let mut artifact = source.manifest_artifact(); + if let Some((_, producer_host)) = producer_hosts + .iter() + .find(|(source_id, _)| *source_id == source.source_id) + { + artifact["producerHostHandle"] = Value::String((*producer_host).to_owned()); + } + artifact + }) + .collect::>(); let manifest = json!({ "sccmManifestVersion": 1, "syntheticFixture": true, @@ -278,7 +298,7 @@ fn assess(sources: &[Source<'_>]) -> SccmServerIntakeAssessment { "siteCode": "LAB", "rolesObserved": ["siteServer"], }, - "artifacts": sources.iter().map(Source::manifest_artifact).collect::>(), + "artifacts": artifacts, }); let payloads = sources .iter() @@ -371,51 +391,9 @@ fn assert_bounded_request_has_specific_scope(request: &SccmSiteCoreArtifactReque ); } -fn assert_malformed_peer_source_fails_closed( - analysis: &SccmSiteCoreAnalysis, - malformed_id: &str, - required_source_id: &str, - required_reason_code: &str, -) { - assert_eq!(analysis.results.len(), 1); - let result = &analysis.results[0]; - assert_eq!(result.state, SccmSiteCoreState::Incomplete); - assert_eq!( - result.finding_class, - Some(SccmFindingClass::InsufficientEvidence) - ); - assert!(!result.evidence.is_empty()); - - let synthetic_gap = analysis - .coverage_gaps - .iter() - .find(|gap| { - gap.source_id == required_source_id - && gap.state == SccmCoverageState::Absent - && gap.reason_code == required_reason_code - }) - .expect("ineligible peer leaves a synthetic missing-source gap"); - assert!(synthetic_gap - .artifact_id - .starts_with("site-core:missing-source:v1:")); - assert_eq!( - result.coverage_gap_artifact_ids, - vec![synthetic_gap.artifact_id.clone()] - ); - - let rejected_gap = analysis - .coverage_gaps - .iter() - .find(|gap| { - gap.source_id == required_source_id - && gap.state == SccmCoverageState::ParseFailed - && gap.reason_code == "evidence-reference-rejected" - }) - .expect("malformed peer remains explicit rejected coverage"); - assert!(rejected_gap - .artifact_id - .starts_with("site-core:rejected-artifact:v1:")); - assert_ne!(rejected_gap.artifact_id, malformed_id); +fn assert_malformed_peer_source_fails_closed(analysis: &SccmSiteCoreAnalysis, malformed_id: &str) { + assert!(analysis.results.is_empty()); + assert_eq!(analysis.coverage_gaps.len(), 2); assert!(analysis.coverage_gaps.iter().all(|gap| { gap.artifact_id != malformed_id && !gap.artifact_id.is_empty() @@ -424,22 +402,11 @@ fn assert_malformed_peer_source_fails_closed( && gap.artifact_id.bytes().all(|byte| { byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b':' | b'_' | b'-') }) + && gap.state == SccmCoverageState::ParseFailed + && gap.reason_code == "intake-coverage-incongruent" })); - - let result_finding = analysis - .findings - .iter() - .find(|finding| finding.subject_id == result.result_id) - .expect("malformed peer retains a validated result finding"); - assert_eq!( - result_finding.finding.class, - SccmFindingClass::InsufficientEvidence - ); - assert_eq!(result_finding.finding.coverage_gaps.len(), 1); - assert_eq!( - result_finding.finding.coverage_gaps[0].artifact_id, - synthetic_gap.artifact_id - ); + assert_eq!(analysis.unlinked_observations.len(), 2); + assert_eq!(analysis.findings.len(), 2); for gap in &analysis.coverage_gaps { let observation = analysis @@ -463,13 +430,8 @@ fn assert_malformed_peer_source_fails_closed( .any(|finding_gap| finding_gap.artifact_id == gap.artifact_id)); } - let requests = analysis - .artifact_requests - .iter() - .filter(|request| request.logical_name == required_source_id) - .collect::>(); - assert!(!requests.is_empty()); - for request in requests { + assert!(!analysis.artifact_requests.is_empty()); + for request in &analysis.artifact_requests { assert_bounded_request_has_specific_scope(request); assert_eq!( request.scope.producer_host_handle.as_deref(), @@ -505,12 +467,18 @@ fn assert_explicit_gap_and_request( } } -fn assert_gap_reason(analysis: &SccmSiteCoreAnalysis, artifact_id: &str, reason_code: &str) { - assert!(analysis.coverage_gaps.iter().any(|gap| { - gap.artifact_id == artifact_id - && gap.state == SccmCoverageState::ParseFailed - && gap.reason_code == reason_code - })); +fn assert_intake_authority_mutation_fails_closed(analysis: &SccmSiteCoreAnalysis) { + assert_topology_incongruence_fails_closed( + analysis, + &[ + ( + "sitecomp-current", + "server-sitecomp", + "synthetic:host:site-01", + ), + ("z-site-status", "server-status", "synthetic:host:site-01"), + ], + ); } fn assert_delimiter_attached_unknown_label_fails_closed(delimiter: char) { @@ -812,11 +780,13 @@ fn unrelated_same_minute_components_and_producer_hosts_never_merge() { .iter() .any(|result| result.state == SccmSiteCoreState::TerminalFailure)); - let mut split_hosts = assess(&[ - Source::sitecomp(HEALTHY_SITECOMP), - Source::status(HEALTHY_STATUS), - ]); - replace_source_producer_host(&mut split_hosts, "server-status", "synthetic:host:site-02"); + let split_hosts = assess_with_producer_hosts( + &[ + Source::sitecomp(HEALTHY_SITECOMP), + Source::status(HEALTHY_STATUS), + ], + &[("server-status", "synthetic:host:mp-01")], + ); let split = analyze_site_core(&split_hosts); assert_eq!(split.results.len(), 2); assert_ne!( @@ -834,10 +804,12 @@ fn unrelated_same_minute_components_and_producer_hosts_never_merge() { assert!(split .results .iter() - .any(|result| { result.transaction_key.producer_host_handle == "synthetic:host:site-02" })); + .any(|result| { result.transaction_key.producer_host_handle == "synthetic:host:mp-01" })); - let mut foreign_gap = assess(&[Source::sitecomp(HEALTHY_SITECOMP), Source::absent_status()]); - replace_source_producer_host(&mut foreign_gap, "server-status", "synthetic:host:site-02"); + let foreign_gap = assess_with_producer_hosts( + &[Source::sitecomp(HEALTHY_SITECOMP), Source::absent_status()], + &[("server-status", "synthetic:host:mp-01")], + ); let foreign_gap_analysis = analyze_site_core(&foreign_gap); assert_eq!(foreign_gap_analysis.results.len(), 1); assert_eq!( @@ -1161,12 +1133,7 @@ fn malformed_status_peer_cannot_hide_required_status_coverage() { ]); replace_source_artifact_id(&mut assessment, "server-status", &malformed_id); - assert_malformed_peer_source_fails_closed( - &analyze_site_core(&assessment), - &malformed_id, - "server-status", - "required-status-source-not-declared", - ); + assert_malformed_peer_source_fails_closed(&analyze_site_core(&assessment), &malformed_id); } } @@ -1179,12 +1146,7 @@ fn malformed_component_peer_cannot_hide_required_component_coverage() { ]); replace_source_artifact_id(&mut assessment, "server-sitecomp", &malformed_id); - assert_malformed_peer_source_fails_closed( - &analyze_site_core(&assessment), - &malformed_id, - "server-sitecomp", - "required-component-source-not-declared", - ); + assert_malformed_peer_source_fails_closed(&analyze_site_core(&assessment), &malformed_id); } } @@ -1215,11 +1177,13 @@ fn undeclared_component_gap_is_deterministic_under_status_only_assessment_permut #[test] fn undeclared_component_gap_does_not_attach_across_producer_hosts() { - let mut assessment = assess(&[ - Source::status(HEALTHY_STATUS), - Source::sitecomp(HEALTHY_SITECOMP), - ]); - replace_source_producer_host(&mut assessment, "server-sitecomp", "synthetic:host:site-02"); + let assessment = assess_with_producer_hosts( + &[ + Source::status(HEALTHY_STATUS), + Source::sitecomp(HEALTHY_SITECOMP), + ], + &[("server-sitecomp", "synthetic:host:mp-01")], + ); let analysis = analyze_site_core(&assessment); let status_only_result = analysis @@ -1246,7 +1210,7 @@ fn undeclared_component_gap_does_not_attach_across_producer_hosts() { let foreign_component_result = analysis .results .iter() - .find(|result| result.transaction_key.producer_host_handle == "synthetic:host:site-02") + .find(|result| result.transaction_key.producer_host_handle == "synthetic:host:mp-01") .expect("foreign component host result"); assert!(foreign_component_result .coverage_gap_artifact_ids @@ -1365,7 +1329,7 @@ fn rejected_role_subject_and_duplicate_sources_become_explicit_parse_gaps() { } #[test] -fn rejected_evidence_contracts_become_source_gaps_with_scoped_requests() { +fn post_intake_evidence_mutations_fail_sealed_authority_closed() { let healthy = assess(&[ Source::sitecomp(HEALTHY_SITECOMP), Source::status(HEALTHY_STATUS), @@ -1373,78 +1337,25 @@ fn rejected_evidence_contracts_become_source_gaps_with_scoped_requests() { let mut wrong_role = healthy.clone(); wrong_role.evidence[0].role = SccmRole::ManagementPoint; - let analysis = analyze_site_core(&wrong_role); - assert_explicit_gap_and_request(&analysis, "sitecomp-current", "server-sitecomp"); - assert_gap_reason(&analysis, "sitecomp-current", "evidence-role-rejected"); - assert!(analysis.unlinked_observations.iter().any(|observation| { - observation.finding_class == SccmFindingClass::Symptom - && observation - .evidence - .iter() - .any(|evidence| evidence.entry_id == wrong_role.evidence[0].evidence_id) - })); - assert_eq!(analysis.results.len(), 1); - assert!(analysis.results.iter().all(|result| { - result.state != SccmSiteCoreState::Healthy - || result.confidence != SccmSiteCoreConfidence::High - })); + assert_intake_authority_mutation_fails_closed(&analyze_site_core(&wrong_role)); let mut incomplete_reference = healthy.clone(); incomplete_reference.evidence[0].reference.line_end = None; - let analysis = analyze_site_core(&incomplete_reference); - assert_explicit_gap_and_request(&analysis, "sitecomp-current", "server-sitecomp"); - assert_gap_reason(&analysis, "sitecomp-current", "evidence-reference-rejected"); - assert!(analysis.unlinked_observations.iter().any(|observation| { - observation.finding_class == SccmFindingClass::Symptom && observation.evidence.is_empty() - })); - assert_eq!(analysis.results.len(), 1); - assert!(analysis.results.iter().all(|result| { - result.state != SccmSiteCoreState::Healthy - || result.confidence != SccmSiteCoreConfidence::High - })); + assert_intake_authority_mutation_fails_closed(&analyze_site_core(&incomplete_reference)); let mut cross_source_reference = healthy.clone(); cross_source_reference.evidence[0].reference.artifact_id = "z-site-status".to_owned(); cross_source_reference.evidence[0].reference.line_start = Some(10_001); cross_source_reference.evidence[0].reference.line_end = Some(10_001); - let analysis = analyze_site_core(&cross_source_reference); - assert_explicit_gap_and_request(&analysis, "z-site-status", "server-status"); - assert_gap_reason( - &analysis, - "z-site-status", - "evidence-source-attribution-rejected", - ); - assert!(analysis.unlinked_observations.iter().any(|observation| { - observation.finding_class == SccmFindingClass::Symptom - && observation - .evidence - .iter() - .any(|evidence| evidence.entry_id == cross_source_reference.evidence[0].evidence_id) - })); - assert_eq!(analysis.results.len(), 1); - assert!(analysis.results.iter().all(|result| { - result.state != SccmSiteCoreState::Healthy - || result.confidence != SccmSiteCoreConfidence::High - })); + assert_intake_authority_mutation_fails_closed(&analyze_site_core(&cross_source_reference)); let mut unresolved_reference = healthy; unresolved_reference.evidence[0].reference.artifact_id = "orphan-sitecomp-record".to_owned(); - let analysis = analyze_site_core(&unresolved_reference); - assert_explicit_gap_and_request(&analysis, "orphan-sitecomp-record", "server-sitecomp"); - assert_gap_reason( - &analysis, - "orphan-sitecomp-record", - "evidence-source-unresolved", - ); - assert_eq!(analysis.results.len(), 1); - assert!(analysis.results.iter().all(|result| { - result.state != SccmSiteCoreState::Healthy - || result.confidence != SccmSiteCoreConfidence::High - })); + assert_intake_authority_mutation_fails_closed(&analyze_site_core(&unresolved_reference)); } #[test] -fn foreign_artifact_identity_cannot_scope_an_unresolved_site_core_request() { +fn foreign_post_intake_artifact_identity_cannot_scope_a_site_core_request() { let mut assessment = assess(&[ Source::sitecomp(HEALTHY_SITECOMP), Source::status(HEALTHY_STATUS), @@ -1458,35 +1369,17 @@ fn foreign_artifact_identity_cannot_scope_an_unresolved_site_core_request() { assessment.evidence[0].reference.artifact_id = "foreign-artifact".to_owned(); let analysis = analyze_site_core(&assessment); - let gap = analysis - .coverage_gaps - .iter() - .find(|gap| { - gap.source_id == "server-sitecomp" && gap.reason_code == "evidence-source-unresolved" - }) - .expect("foreign attribution becomes a site-core coverage gap"); - assert_ne!(gap.artifact_id, "foreign-artifact"); - assert!(gap - .artifact_id - .starts_with("site-core:rejected-artifact:v1:")); - let request = analysis - .artifact_requests - .iter() - .find(|request| request.logical_name == "server-sitecomp") - .expect("unresolved site-core evidence has a bounded request"); - assert_bounded_request_has_specific_scope(request); - assert_eq!( - request.scope.producer_host_handle.as_deref(), - Some("synthetic:host:site-01") - ); - assert_ne!( - request.scope.rotation_lineage_handle.as_deref(), - Some("foreign-lineage") - ); + assert_intake_authority_mutation_fails_closed(&analysis); + assert!(analysis.artifact_requests.iter().all(|request| request + .scope + .producer_host_handle + .as_deref() + != Some("synthetic:host:foreign") + && request.scope.rotation_lineage_handle.as_deref() != Some("foreign-lineage"))); } #[test] -fn rejected_nonprofile_prose_is_coverage_not_a_profile_symptom() { +fn post_intake_nonprofile_role_mutation_fails_sealed_authority_closed() { let mut assessment = assess(&[ Source::sitecomp(HEALTHY_SITECOMP), Source::status(HEALTHY_STATUS), @@ -1494,15 +1387,7 @@ fn rejected_nonprofile_prose_is_coverage_not_a_profile_symptom() { assessment.evidence[0].message = "ordinary non-profile source prose".to_owned(); assessment.evidence[0].role = SccmRole::ManagementPoint; - let analysis = analyze_site_core(&assessment); - assert_explicit_gap_and_request(&analysis, "sitecomp-current", "server-sitecomp"); - assert_gap_reason(&analysis, "sitecomp-current", "evidence-role-rejected"); - assert_eq!(analysis.unlinked_observations.len(), 1); - assert_eq!( - analysis.unlinked_observations[0].finding_class, - SccmFindingClass::InsufficientEvidence - ); - assert!(analysis.unlinked_observations[0].evidence.is_empty()); + assert_intake_authority_mutation_fails_closed(&analyze_site_core(&assessment)); } #[test] @@ -1518,15 +1403,14 @@ fn colliding_evidence_identities_are_parse_gaps_not_silent_drops() { #[test] fn closed_profile_schema_rejects_arbitrary_keys_and_retains_safe_unknown_facts() { - let mut arbitrary_work = assess(&[ - Source::sitecomp(HEALTHY_SITECOMP), - Source::status(HEALTHY_STATUS), + let arbitrary_sitecomp = + HEALTHY_SITECOMP.replace("workItemId=SC-HEALTH-001", "workItemId=ARBITRARY-001"); + let arbitrary_status = + HEALTHY_STATUS.replace("workItemId=SC-HEALTH-001", "workItemId=ARBITRARY-001"); + let arbitrary_work = assess(&[ + Source::sitecomp(&arbitrary_sitecomp), + Source::status(&arbitrary_status), ]); - for evidence in &mut arbitrary_work.evidence { - evidence.message = evidence - .message - .replace("workItemId=SC-HEALTH-001", "workItemId=ARBITRARY-001"); - } let arbitrary = analyze_site_core(&arbitrary_work); assert!(arbitrary.results.is_empty()); assert_eq!( @@ -1539,14 +1423,13 @@ fn closed_profile_schema_rejects_arbitrary_keys_and_retains_safe_unknown_facts() .iter() .all(|evidence| arbitrary_wire.contains(&evidence.evidence_id))); - let mut unknown_status = assess(&[ - Source::sitecomp(HEALTHY_SITECOMP), + let unknown_sitecomp = + HEALTHY_SITECOMP.replace("SC_COMPONENT_START_OK", "SC_UNREVIEWED_STATUS"); + let unknown_status = assess(&[ + Source::sitecomp(&unknown_sitecomp), Source::status(HEALTHY_STATUS), ]); let rejected_id = unknown_status.evidence[0].evidence_id.clone(); - unknown_status.evidence[0].message = unknown_status.evidence[0] - .message - .replace("SC_COMPONENT_START_OK", "SC_UNREVIEWED_STATUS"); let unknown = analyze_site_core(&unknown_status); let unknown_wire = serde_json::to_string(&unknown).expect("analysis serializes"); assert!(unknown_wire.contains(&rejected_id)); @@ -1626,28 +1509,17 @@ fn delimiter_separated_known_profile_labels_and_safe_prose_remain_accepted() { #[test] fn every_required_source_coverage_state_emits_insufficient_evidence_and_a_request() { - for state in [ - SccmCoverageState::Absent, - SccmCoverageState::AccessDenied, - SccmCoverageState::Skipped, - SccmCoverageState::Unsupported, + for (state_token, state) in [ + ("absent", SccmCoverageState::Absent), + ("accessDenied", SccmCoverageState::AccessDenied), + ("skipped", SccmCoverageState::Skipped), + ("unsupported", SccmCoverageState::Unsupported), ] { - let mut assessment = assess(&[ - Source::sitecomp(HEALTHY_SITECOMP), - Source::status(HEALTHY_STATUS), - ]); - assessment - .artifacts - .iter_mut() - .find(|artifact| artifact.source_id == "server-sitecomp") - .expect("sitecomp artifact") - .state = state.clone(); - assessment - .coverage - .iter_mut() - .find(|coverage| coverage.source_id == "server-sitecomp") - .expect("sitecomp coverage") - .state = state.clone(); + let mut sitecomp = Source::sitecomp(HEALTHY_SITECOMP); + sitecomp.content = None; + sitecomp.capture_state = state_token; + sitecomp.encoding = None; + let assessment = assess(&[sitecomp, Source::status(HEALTHY_STATUS)]); let analysis = analyze_site_core(&assessment); assert!(analysis @@ -1793,12 +1665,7 @@ fn rotation_provenance_must_match_classification_and_requests_use_exact_pairs() .expect("sitecomp artifact") .rotation = Some(SccmRotation::LoUnderscore); let rejected = analyze_site_core(&mismatch); - assert_explicit_gap_and_request(&rejected, "sitecomp-current", "server-sitecomp"); - assert_eq!(rejected.results.len(), 1); - assert!(rejected.results.iter().all(|result| { - result.state != SccmSiteCoreState::Healthy - || result.confidence != SccmSiteCoreConfidence::High - })); + assert_intake_authority_mutation_fails_closed(&rejected); let backlog = analyze_site_core(&assess(&[ Source::sitecomp(INBOX_BACKLOG), From 39e5828fa476ddaa427da3c659bf93b0b91c226f Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 11:52:30 -0400 Subject: [PATCH 11/18] refactor(sccm): share topology authority normalization --- .../src/sccm/server/windows/intake.rs | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs index e23ffc0dc..d5609ddae 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs @@ -202,17 +202,9 @@ impl SccmServerIntakeAssessment { { return false; } - let mut normalized_topology = self.topology.clone(); - normalized_topology - .roles_observed - .sort_by(|left, right| role_sort_key(left).cmp(role_sort_key(right))); - if normalized_topology - .roles_observed - .windows(2) - .any(|roles| roles[0] == roles[1]) - { + let Some(normalized_topology) = normalized_topology_or_none(&self.topology) else { return false; - } + }; canonical_record_digest_bounded( b"topology", &normalized_topology, @@ -244,6 +236,23 @@ impl SccmServerIntakeAssessment { } } +fn normalized_topology_or_none( + topology: &SccmServerTopologyAssessment, +) -> Option { + let mut normalized = topology.clone(); + normalized + .roles_observed + .sort_by(|left, right| role_sort_key(left).cmp(role_sort_key(right))); + if normalized + .roles_observed + .windows(2) + .any(|roles| roles[0] == roles[1]) + { + return None; + } + Some(normalized) +} + /// Nonserialized canonical-input binding for downstream server-role adapters. /// Collection order is not authority: the normalized records are serialized /// independently and compared as duplicate-free sets. @@ -1487,17 +1496,7 @@ fn canonical_intake_integrity_with_structure( #[cfg(test)] INTAKE_CANONICALIZATION_CALLS.with(|calls| calls.set(calls.get().saturating_add(1))); - let mut normalized_topology = topology.clone(); - normalized_topology - .roles_observed - .sort_by(|left, right| role_sort_key(left).cmp(role_sort_key(right))); - if normalized_topology - .roles_observed - .windows(2) - .any(|roles| roles[0] == roles[1]) - { - return None; - } + let normalized_topology = normalized_topology_or_none(topology)?; let mut normalized_coverage = coverage.to_vec(); for record in &mut normalized_coverage { From d569b42be8d0ea00ee067127b27d870d1959bae2 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 11:54:44 -0400 Subject: [PATCH 12/18] refactor(sccm): centralize coverage identity keys --- .../src/sccm/server/windows/intake.rs | 97 +++++++++++-------- .../src/sccm/server/windows/site_core.rs | 42 +------- 2 files changed, 64 insertions(+), 75 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs index d5609ddae..d47ca5b92 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs @@ -263,7 +263,7 @@ struct SccmServerIntakeIntegrity { structure: IntakeIntegrityStructure, topology: IntakeIntegrityRecord, artifacts: BTreeMap, - coverage: BTreeMap, + coverage: BTreeMap, evidence: BTreeMap, } @@ -291,15 +291,57 @@ struct IntakeIntegrityStructure { struct ArtifactIntegrityIdentity(String); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct CoverageIntegrityIdentity { +pub(super) struct CoverageIdentityKey { producer_role: String, - producer_host_handle: String, - workflow_subject_role: String, - workflow_subject_handle: String, + producer_host_handle: Option, source_id: String, + workflow_subject_role: Option, + workflow_subject_handle: Option, state: String, } +impl CoverageIdentityKey { + fn new( + producer_role: &SccmRole, + producer_host_handle: Option<&str>, + source_id: &str, + workflow_subject_role: Option<&SccmRole>, + workflow_subject_handle: Option<&str>, + state: &SccmCoverageState, + ) -> Self { + Self { + producer_role: role_sort_key(producer_role).to_owned(), + producer_host_handle: producer_host_handle.map(str::to_owned), + source_id: source_id.to_owned(), + workflow_subject_role: workflow_subject_role.map(|role| role_sort_key(role).to_owned()), + workflow_subject_handle: workflow_subject_handle.map(str::to_owned), + state: coverage_sort_key(state).to_owned(), + } + } + + pub(super) fn from_artifact(artifact: &SccmServerArtifactAssessment) -> Self { + Self::new( + &artifact.producer_role, + artifact.producer_host_handle.as_deref(), + &artifact.source_id, + artifact.workflow_subject_role.as_ref(), + artifact.workflow_subject_handle.as_deref(), + &artifact.state, + ) + } + + pub(super) fn from_coverage(coverage: &SccmServerCoverage) -> Self { + Self::new( + &coverage.producer_role, + coverage.producer_host_handle.as_deref(), + &coverage.source_id, + coverage.workflow_subject_role.as_ref(), + coverage.workflow_subject_handle.as_deref(), + &coverage.state, + ) + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct EvidenceIntegrityIdentity(String); @@ -337,10 +379,16 @@ impl SccmServerIntakeIntegrity { .iter() .map(|(identity, record)| { identity.producer_role.len() - + identity.producer_host_handle.len() - + identity.workflow_subject_role.len() - + identity.workflow_subject_handle.len() + + identity.producer_host_handle.as_deref().map_or(0, str::len) + identity.source_id.len() + + identity + .workflow_subject_role + .as_deref() + .map_or(0, str::len) + + identity + .workflow_subject_handle + .as_deref() + .map_or(0, str::len) + identity.state.len() + std::mem::size_of_val(&record.payload_len) + record.digest.len() @@ -647,10 +695,7 @@ pub fn assess_server_intake( let mut artifacts = Vec::with_capacity(prepared.len()); let mut evidence = Vec::new(); - let mut coverage_by_key: BTreeMap< - (String, String, String, String, String, String), - SccmServerCoverage, - > = BTreeMap::new(); + let mut coverage_by_key = BTreeMap::::new(); let mut request_keys = BTreeSet::new(); let mut next_artifact_requests = Vec::new(); let usable_source_keys = prepared @@ -663,19 +708,7 @@ pub fn assess_server_intake( for prepared_artifact in prepared { let artifact = prepared_artifact.assessment; - let coverage_key = ( - role_sort_key(&artifact.producer_role).to_owned(), - artifact.producer_host_handle.clone().unwrap_or_default(), - artifact.source_id.clone(), - artifact - .workflow_subject_role - .as_ref() - .map(role_sort_key) - .unwrap_or_default() - .to_owned(), - artifact.workflow_subject_handle.clone().unwrap_or_default(), - coverage_sort_key(&artifact.state).to_owned(), - ); + let coverage_key = CoverageIdentityKey::from_artifact(&artifact); coverage_by_key .entry(coverage_key) .and_modify(|row| row.artifact_ids.push(artifact.artifact_id.clone())) @@ -1537,19 +1570,7 @@ fn canonical_intake_integrity_with_structure( let mut coverage_integrity = BTreeMap::new(); for record in &normalized_coverage { - let identity = CoverageIntegrityIdentity { - producer_role: role_sort_key(&record.producer_role).to_owned(), - producer_host_handle: record.producer_host_handle.clone().unwrap_or_default(), - workflow_subject_role: record - .workflow_subject_role - .as_ref() - .map(role_sort_key) - .unwrap_or_default() - .to_owned(), - workflow_subject_handle: record.workflow_subject_handle.clone().unwrap_or_default(), - source_id: record.source_id.clone(), - state: coverage_sort_key(&record.state).to_owned(), - }; + let identity = CoverageIdentityKey::from_coverage(record); let max_payload_len = match expected { Some(expected) => Some(expected.coverage.get(&identity)?.payload_len), None => None, diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs index 7cc2c8a68..da184ebfd 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs @@ -18,6 +18,7 @@ use crate::sccm::{ SccmTimeOrderingState, SccmTimestamp, }; +use super::intake::CoverageIdentityKey; use super::{ SccmServerArtifactAssessment, SccmServerConfiguredPathState, SccmServerIntakeAssessment, }; @@ -1952,57 +1953,24 @@ fn evidence_collision_artifact_ids( } fn site_core_coverage_is_congruent(intake: &SccmServerIntakeAssessment) -> bool { - type CoverageKey = ( - String, - Option, - String, - String, - Option, - String, - ); - - let mut expected = BTreeMap::>::new(); + let mut expected = BTreeMap::>::new(); for artifact in &intake.artifacts { if SiteCoreGroup::from_source_id(&artifact.source_id).is_none() { continue; } expected - .entry(( - role_sort_key(&artifact.producer_role).to_owned(), - artifact.producer_host_handle.clone(), - artifact.source_id.clone(), - artifact - .workflow_subject_role - .as_ref() - .map(role_sort_key) - .unwrap_or_default() - .to_owned(), - artifact.workflow_subject_handle.clone(), - coverage_sort_key(&artifact.state).to_owned(), - )) + .entry(CoverageIdentityKey::from_artifact(artifact)) .or_default() .push(artifact.artifact_id.clone()); } - let mut observed = BTreeMap::>::new(); + let mut observed = BTreeMap::>::new(); for coverage in &intake.coverage { if SiteCoreGroup::from_source_id(&coverage.source_id).is_none() { continue; } observed - .entry(( - role_sort_key(&coverage.producer_role).to_owned(), - coverage.producer_host_handle.clone(), - coverage.source_id.clone(), - coverage - .workflow_subject_role - .as_ref() - .map(role_sort_key) - .unwrap_or_default() - .to_owned(), - coverage.workflow_subject_handle.clone(), - coverage_sort_key(&coverage.state).to_owned(), - )) + .entry(CoverageIdentityKey::from_coverage(coverage)) .or_default() .extend(coverage.artifact_ids.iter().cloned()); } From 749ebc9459ff1d16c8fa04fe3e2fbeef64258e91 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:07:27 -0400 Subject: [PATCH 13/18] test(sccm): quarantine invalid intake authority --- .../tests/sccm_server_site_core.rs | 215 +++++++----------- 1 file changed, 84 insertions(+), 131 deletions(-) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index 974c2bad3..12e603cad 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -392,53 +392,9 @@ fn assert_bounded_request_has_specific_scope(request: &SccmSiteCoreArtifactReque } fn assert_malformed_peer_source_fails_closed(analysis: &SccmSiteCoreAnalysis, malformed_id: &str) { - assert!(analysis.results.is_empty()); - assert_eq!(analysis.coverage_gaps.len(), 2); - assert!(analysis.coverage_gaps.iter().all(|gap| { - gap.artifact_id != malformed_id - && !gap.artifact_id.is_empty() - && gap.artifact_id.len() <= 256 - && gap.artifact_id.trim() == gap.artifact_id - && gap.artifact_id.bytes().all(|byte| { - byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b':' | b'_' | b'-') - }) - && gap.state == SccmCoverageState::ParseFailed - && gap.reason_code == "intake-coverage-incongruent" - })); - assert_eq!(analysis.unlinked_observations.len(), 2); - assert_eq!(analysis.findings.len(), 2); - - for gap in &analysis.coverage_gaps { - let observation = analysis - .unlinked_observations - .iter() - .find(|observation| observation.coverage_gap_artifact_ids == [gap.artifact_id.clone()]) - .expect("each gap has an explicit coverage observation"); - let finding = analysis - .findings - .iter() - .find(|finding| finding.subject_id == observation.observation_id) - .expect("each gap has a validated coverage finding"); - assert_eq!( - finding.finding.class, - SccmFindingClass::InsufficientEvidence - ); - assert!(finding - .finding - .coverage_gaps - .iter() - .any(|finding_gap| finding_gap.artifact_id == gap.artifact_id)); - } - - assert!(!analysis.artifact_requests.is_empty()); - for request in &analysis.artifact_requests { - assert_bounded_request_has_specific_scope(request); - assert_eq!( - request.scope.producer_host_handle.as_deref(), - Some("synthetic:host:site-01") - ); - } - assert!(!analysis.cross_side_correlation_performed); + assert_authority_invalid_analysis(analysis); + let wire = serde_json::to_string(analysis).expect("analysis serializes"); + assert!(!wire.contains(malformed_id)); } fn assert_explicit_gap_and_request( @@ -481,6 +437,33 @@ fn assert_intake_authority_mutation_fails_closed(analysis: &SccmSiteCoreAnalysis ); } +fn assert_authority_invalid_analysis(analysis: &SccmSiteCoreAnalysis) { + assert!(analysis.results.is_empty()); + assert_eq!(analysis.coverage_gaps.len(), 1); + let gap = &analysis.coverage_gaps[0]; + assert_eq!(gap.artifact_id, "site-core-intake-authority"); + assert_eq!(gap.source_id, "server-site-core-intake"); + assert_eq!(gap.state, SccmCoverageState::ParseFailed); + assert_eq!(gap.reason_code, "intake-authority-invalid"); + + assert_eq!(analysis.unlinked_observations.len(), 1); + let observation = &analysis.unlinked_observations[0]; + assert_eq!( + observation.finding_class, + SccmFindingClass::InsufficientEvidence + ); + assert_eq!( + observation.coverage_gap_artifact_ids, + ["site-core-intake-authority"] + ); + assert!(observation.evidence.is_empty()); + assert!(observation.next_artifacts.is_empty()); + + assert!(analysis.findings.is_empty()); + assert!(analysis.artifact_requests.is_empty()); + assert!(!analysis.cross_side_correlation_performed); +} + fn assert_delimiter_attached_unknown_label_fails_closed(delimiter: char) { for (position, outcome_field) in [ ( @@ -1716,10 +1699,7 @@ fn intake_coverage_must_be_congruent_before_facts_can_shape_results() { assessment.coverage.clear(); let analysis = analyze_site_core(&assessment); - assert!(analysis.results.is_empty()); - assert!(!analysis.coverage_gaps.is_empty()); - assert!(!analysis.unlinked_observations.is_empty()); - assert!(!analysis.artifact_requests.is_empty()); + assert_authority_invalid_analysis(&analysis); } #[test] @@ -1745,72 +1725,64 @@ fn coordinated_post_intake_producer_host_mutation_fails_site_core_authority_clos ); } +#[test] +fn invalid_intake_authority_never_exports_forged_scope_or_identity() { + let healthy = assess(&[ + Source::sitecomp(HEALTHY_SITECOMP), + Source::status(HEALTHY_STATUS), + ]); + + let forged_host = "synthetic:host:forged-scope"; + let mut host_mutation = healthy.clone(); + replace_source_producer_host(&mut host_mutation, "server-sitecomp", forged_host); + replace_source_producer_host(&mut host_mutation, "server-status", forged_host); + + let forged_lineage = "synthetic:lineage:forged-scope"; + let mut lineage_mutation = healthy.clone(); + for artifact in &mut lineage_mutation.artifacts { + artifact.rotation_lineage_handle = forged_lineage.to_owned(); + } + + let forged_artifact_id = "synthetic:artifact:forged-scope"; + let mut artifact_id_mutation = healthy; + replace_source_artifact_id( + &mut artifact_id_mutation, + "server-sitecomp", + forged_artifact_id, + ); + + let analyses = [ + (forged_host, analyze_site_core(&host_mutation)), + (forged_lineage, analyze_site_core(&lineage_mutation)), + (forged_artifact_id, analyze_site_core(&artifact_id_mutation)), + ]; + for (forged_value, analysis) in &analyses { + assert_authority_invalid_analysis(analysis); + let wire = serde_json::to_string(analysis).expect("analysis serializes"); + assert!( + !wire.contains(forged_value), + "invalid authority exported forged value {forged_value}" + ); + } + assert!(analyses.windows(2).all(|pair| { + serde_json::to_vec(&pair[0].1).expect("analysis serializes") + == serde_json::to_vec(&pair[1].1).expect("analysis serializes") + })); +} + fn assert_topology_incongruence_fails_closed( analysis: &SccmSiteCoreAnalysis, expected_gaps: &[(&str, &str, &str)], ) { - assert!( - analysis.results.is_empty(), - "incongruent topology-bound coverage must not shape a result" - ); - assert_eq!(analysis.coverage_gaps.len(), expected_gaps.len()); - assert_eq!( - analysis.unlinked_observations.len(), - expected_gaps.len(), - "every topology mismatch stays visible as a coverage observation" - ); - assert_eq!( - analysis.findings.len(), - expected_gaps.len(), - "every topology mismatch stays visible as a validated finding" - ); - + assert_authority_invalid_analysis(analysis); + let wire = serde_json::to_string(analysis).expect("analysis serializes"); for (artifact_id, source_id, producer_host_handle) in expected_gaps { - let gap = analysis - .coverage_gaps - .iter() - .find(|gap| gap.artifact_id == *artifact_id) - .expect("topology-specific coverage gap"); - assert_eq!(gap.source_id, *source_id); - assert_eq!(gap.state, SccmCoverageState::ParseFailed); - assert_eq!(gap.reason_code, "intake-coverage-incongruent"); - - let observation = analysis - .unlinked_observations - .iter() - .find(|observation| observation.coverage_gap_artifact_ids == [artifact_id.to_string()]) - .expect("coverage gap has an explicit observation"); - assert_eq!( - observation.finding_class, - SccmFindingClass::InsufficientEvidence - ); - - let finding = analysis - .findings - .iter() - .find(|finding| finding.subject_id == observation.observation_id) - .expect("coverage observation has a validated finding"); - assert_eq!( - finding.finding.class, - SccmFindingClass::InsufficientEvidence - ); - assert!(finding - .finding - .coverage_gaps - .iter() - .any(|finding_gap| finding_gap.artifact_id == *artifact_id)); - - let request = analysis - .artifact_requests - .iter() - .find(|request| request.logical_name == *source_id) - .expect("topology-specific gap has a bounded request"); - assert_eq!( - request.scope.producer_host_handle.as_deref(), - Some(*producer_host_handle), - "the request stays scoped to artifact topology, not mutated coverage topology" - ); - assert_bounded_request_has_specific_scope(request); + for forged_or_untrusted_value in [artifact_id, source_id, producer_host_handle] { + assert!( + !wire.contains(forged_or_untrusted_value), + "invalid authority exported untrusted value {forged_or_untrusted_value}" + ); + } } } @@ -1904,25 +1876,6 @@ fn post_intake_topology_mutations_fail_site_core_authority_closed() { ("capture host", analyze_site_core(&changed_capture_host)), ("observed roles", analyze_site_core(&changed_observed_roles)), ]; - assert_eq!( - analyses - .iter() - .map(|(name, analysis)| ( - *name, - analysis.results.len(), - analysis.coverage_gaps.len(), - analysis.unlinked_observations.len(), - analysis.findings.len(), - analysis.artifact_requests.len(), - )) - .collect::>(), - vec![ - ("site handle", 0, 2, 2, 2, 2), - ("capture host", 0, 2, 2, 2, 2), - ("observed roles", 0, 2, 2, 2, 2), - ], - "no caller-mutated topology may retain normal site-core facts" - ); for (_, analysis) in &analyses { assert_topology_incongruence_fails_closed( From b21419a4b6cbe2c531ed46cc5a88f2561b3a4650 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:09:52 -0400 Subject: [PATCH 14/18] fix(sccm): quarantine invalid intake authority --- .../src/sccm/server/windows/site_core.rs | 49 ++++++--- .../tests/sccm_server_site_core.rs | 104 ++++-------------- 2 files changed, 55 insertions(+), 98 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs index da184ebfd..195511aa6 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs @@ -33,6 +33,9 @@ pub const SCCM_SITE_CORE_STATUS_GROUP: &str = "server-status"; const SITE_CORE_PROFILE_VERSION_TOKEN: &str = "5.00.TEST"; const RECAPTURE_FLOOR_BYTES: u64 = 4096; const MAX_SITE_CORE_REQUEST_ARTIFACTS: usize = 2; +const INTAKE_AUTHORITY_ARTIFACT_ID: &str = "site-core-intake-authority"; +const INTAKE_AUTHORITY_SOURCE_ID: &str = "server-site-core-intake"; +const INTAKE_AUTHORITY_REASON_CODE: &str = "intake-authority-invalid"; const STATE_CHAIN: [SccmSiteCorePhase; 5] = [ SccmSiteCorePhase::ComponentStart, @@ -306,24 +309,34 @@ struct SiteCoreContext<'a> { impl<'a> SiteCoreContext<'a> { fn new(intake: &'a SccmServerIntakeAssessment) -> Self { let intake_authority_is_bound = intake.adapter_authority_is_intake_bound(); - let evidence_identity_is_unique = if intake_authority_is_bound { - unique_evidence_identities(&intake.evidence) - } else { - vec![false; intake.evidence.len()] - }; - let collision_artifact_ids = if intake_authority_is_bound { - evidence_collision_artifact_ids(&intake.evidence, &evidence_identity_is_unique) - } else { - BTreeSet::new() - }; - let (evidence_source_rejections, unresolved_evidence_gaps) = if intake_authority_is_bound { - evidence_source_rejections(intake) - } else { - (BTreeMap::new(), Vec::new()) - }; - let coverage_congruent = intake_authority_is_bound - && intake.topology_authority_is_intake_bound() - && site_core_coverage_is_congruent(intake); + if !intake_authority_is_bound { + // The public assessment fields are no longer authoritative once the + // private intake seal fails. Keep the coverage failure explicit, but + // do not use caller-mutable artifact identities or topology to scope + // a collection request. + return Self { + artifacts: &[], + sources: BTreeMap::new(), + intake_authority_is_bound, + evidence_identity_is_unique: Vec::new(), + coverage_gaps: vec![SccmSiteCoreCoverageGap { + artifact_id: INTAKE_AUTHORITY_ARTIFACT_ID.to_owned(), + source_id: INTAKE_AUTHORITY_SOURCE_ID.to_owned(), + state: SccmCoverageState::ParseFailed, + reason_code: INTAKE_AUTHORITY_REASON_CODE.to_owned(), + diagnostic_meaning: SccmSiteCoreDiagnosticMeaning::CoverageOnly, + }], + coverage_gap_producer_hosts: BTreeMap::new(), + }; + } + + let evidence_identity_is_unique = unique_evidence_identities(&intake.evidence); + let collision_artifact_ids = + evidence_collision_artifact_ids(&intake.evidence, &evidence_identity_is_unique); + let (evidence_source_rejections, unresolved_evidence_gaps) = + evidence_source_rejections(intake); + let coverage_congruent = + intake.topology_authority_is_intake_bound() && site_core_coverage_is_congruent(intake); let sources = admitted_sources( intake, &collision_artifact_ids, diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index 12e603cad..0949f88a7 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -397,32 +397,6 @@ fn assert_malformed_peer_source_fails_closed(analysis: &SccmSiteCoreAnalysis, ma assert!(!wire.contains(malformed_id)); } -fn assert_explicit_gap_and_request( - analysis: &SccmSiteCoreAnalysis, - artifact_id: &str, - source_id: &str, -) { - assert!(analysis.coverage_gaps.iter().any(|gap| { - gap.artifact_id == artifact_id && gap.state == SccmCoverageState::ParseFailed - })); - assert!(analysis.unlinked_observations.iter().any(|observation| { - observation.finding_class == SccmFindingClass::InsufficientEvidence - && observation - .coverage_gap_artifact_ids - .iter() - .any(|candidate| candidate == artifact_id) - })); - let request = analysis - .artifact_requests - .iter() - .find(|request| request.logical_name == source_id) - .expect("coverage gap has a source-specific artifact request"); - assert_bounded_request_has_specific_scope(request); - for request in &analysis.artifact_requests { - assert_bounded_request_has_specific_scope(request); - } -} - fn assert_intake_authority_mutation_fails_closed(analysis: &SccmSiteCoreAnalysis) { assert_topology_incongruence_fails_closed( analysis, @@ -879,15 +853,7 @@ fn encoding_profile_coverage_fragment_cap_and_time_provenance_fail_closed() { evidence.timestamp.ordering_state = SccmTimeOrderingState::OffsetInvalid; } - for (name, assessment) in [ - ("encoding", encoding), - ("profile", unknown_profile), - ("coverage", denied), - ("fragment", incomplete_fragment), - ("content", missing_content_provenance), - ("cap", capped), - ("time", invalid_time), - ] { + for (name, assessment) in [("encoding", encoding), ("cap", capped)] { let analysis = analyze_site_core(&assessment); assert!( !analysis.coverage_gaps.is_empty(), @@ -917,6 +883,16 @@ fn encoding_profile_coverage_fragment_cap_and_time_provenance_fail_closed() { || finding.finding.confidence != cmtraceopen_parser::sccm::SccmConfidence::High })); } + + for assessment in [ + unknown_profile, + denied, + incomplete_fragment, + missing_content_provenance, + invalid_time, + ] { + assert_authority_invalid_analysis(&analyze_site_core(&assessment)); + } } #[test] @@ -1232,22 +1208,11 @@ fn no_provenance_mutation_can_reintroduce_a_confirmed_failure() { .expect("captured source provenance") .limit_applied = true; - let analysis = analyze_site_core(&assessment); - assert!(analysis.results.is_empty()); - assert!(analysis - .coverage_gaps - .iter() - .any(|gap| gap.artifact_id == "sitecomp-current")); - let request = analysis - .artifact_requests - .iter() - .find(|request| request.logical_name == "server-sitecomp") - .expect("provenance coverage gap has a request"); - assert_bounded_request_has_specific_scope(request); + assert_authority_invalid_analysis(&analyze_site_core(&assessment)); } #[test] -fn rejected_role_subject_and_duplicate_sources_become_explicit_parse_gaps() { +fn post_intake_source_contract_mutations_fail_sealed_authority_closed() { let healthy = assess(&[ Source::sitecomp(HEALTHY_SITECOMP), Source::status(HEALTHY_STATUS), @@ -1260,11 +1225,7 @@ fn rejected_role_subject_and_duplicate_sources_become_explicit_parse_gaps() { .find(|artifact| artifact.source_id == "server-status") .expect("status artifact") .producer_role = SccmRole::ManagementPoint; - assert_explicit_gap_and_request( - &analyze_site_core(&wrong_role), - "z-site-status", - "server-status", - ); + assert_authority_invalid_analysis(&analyze_site_core(&wrong_role)); let mut wrong_subject = healthy.clone(); let sitecomp = wrong_subject @@ -1274,11 +1235,7 @@ fn rejected_role_subject_and_duplicate_sources_become_explicit_parse_gaps() { .expect("sitecomp artifact"); sitecomp.workflow_subject_role = Some(SccmRole::Client); sitecomp.workflow_subject_handle = Some("synthetic:subject:client-01".to_owned()); - assert_explicit_gap_and_request( - &analyze_site_core(&wrong_subject), - "sitecomp-current", - "server-sitecomp", - ); + assert_authority_invalid_analysis(&analyze_site_core(&wrong_subject)); let mut duplicate = healthy; let duplicate_sitecomp = duplicate @@ -1288,11 +1245,7 @@ fn rejected_role_subject_and_duplicate_sources_become_explicit_parse_gaps() { .expect("sitecomp artifact") .clone(); duplicate.artifacts.push(duplicate_sitecomp); - assert_explicit_gap_and_request( - &analyze_site_core(&duplicate), - "sitecomp-current", - "server-sitecomp", - ); + assert_authority_invalid_analysis(&analyze_site_core(&duplicate)); let mut rejected_shape = assess(&[ Source::sitecomp(HEALTHY_SITECOMP), @@ -1304,11 +1257,7 @@ fn rejected_role_subject_and_duplicate_sources_become_explicit_parse_gaps() { .find(|artifact| artifact.source_id == "server-status") .expect("status artifact") .original_basename = Some("future-status.bin".to_owned()); - assert_explicit_gap_and_request( - &analyze_site_core(&rejected_shape), - "z-site-status", - "server-status", - ); + assert_authority_invalid_analysis(&analyze_site_core(&rejected_shape)); } #[test] @@ -1374,14 +1323,12 @@ fn post_intake_nonprofile_role_mutation_fails_sealed_authority_closed() { } #[test] -fn colliding_evidence_identities_are_parse_gaps_not_silent_drops() { +fn post_intake_evidence_identity_collision_fails_sealed_authority_closed() { let mut assessment = assess(&[Source::sitecomp(HEALTHY_SITECOMP), Source::absent_status()]); let duplicate = assessment.evidence[0].clone(); assessment.evidence.push(duplicate); - let analysis = analyze_site_core(&assessment); - assert_explicit_gap_and_request(&analysis, "sitecomp-current", "server-sitecomp"); - assert!(analysis.results.is_empty()); + assert_authority_invalid_analysis(&analyze_site_core(&assessment)); } #[test] @@ -1579,9 +1526,10 @@ fn invalid_finding_inputs_become_explicit_gaps_instead_of_clearing_class() { } let analysis = analyze_site_core(&assessment); - assert!(analysis.results.is_empty()); - assert!(!analysis.unlinked_observations.is_empty()); - assert!(!analysis.artifact_requests.is_empty()); + assert_authority_invalid_analysis(&analysis); + assert!(!serde_json::to_string(&analysis) + .expect("analysis serializes") + .contains(&oversized_id)); } #[test] @@ -1683,11 +1631,7 @@ fn rotation_provenance_must_match_classification_and_requests_use_exact_pairs() kind: "future".to_owned(), value: None, })); - let unknown = analyze_site_core(&unknown_rotation); - assert!(!unknown.artifact_requests.is_empty()); - for request in &unknown.artifact_requests { - assert_bounded_request_has_specific_scope(request); - } + assert_authority_invalid_analysis(&analyze_site_core(&unknown_rotation)); } #[test] From 670954e2ec3eee09743ac552acac36322422daae Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:43:12 -0400 Subject: [PATCH 15/18] refactor(sccm): clarify server authority quarantine --- .../server/windows/management_point_tests.rs | 6 +- .../src/sccm/server/windows/site_core.rs | 60 ++++++++++--------- .../tests/sccm_server_site_core.rs | 6 +- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs index ba4de6f8f..7d55babfd 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs @@ -188,11 +188,7 @@ fn load_bundle(scenario: &str) -> SccmManagementPointBundle { fn load_server_intake_fixture( directory: &Path, ) -> cmtraceopen_parser::sccm::server::windows::SccmServerIntakeAssessment { - let manifest: Value = serde_json::from_str( - &fs::read_to_string(directory.join("manifest.json")) - .expect("server intake fixture manifest must be readable"), - ) - .expect("server intake fixture manifest must be valid JSON"); + let manifest = load_json(&directory.join("manifest.json")); assess_server_intake_manifest(directory, &manifest) } diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs index 195511aa6..d7b9b3513 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/site_core.rs @@ -335,6 +335,9 @@ impl<'a> SiteCoreContext<'a> { evidence_collision_artifact_ids(&intake.evidence, &evidence_identity_is_unique); let (evidence_source_rejections, unresolved_evidence_gaps) = evidence_source_rejections(intake); + // Deliberate defense in depth: the complete adapter seal currently + // includes topology, while Site Core also keeps its topology-specific + // authority contract explicit at the point that topology scopes facts. let coverage_congruent = intake.topology_authority_is_intake_bound() && site_core_coverage_is_congruent(intake); let sources = admitted_sources( @@ -417,38 +420,37 @@ pub fn analyze_site_core(intake: &SccmServerIntakeAssessment) -> SccmSiteCoreAna let mut context = SiteCoreContext::new(intake); let mut grouped = BTreeMap::>::new(); let mut record_observations = Vec::new(); - for (position, evidence) in intake.evidence.iter().enumerate() { - if !context.intake_authority_is_bound { - break; - } - let Some(source) = context.sources.get(evidence.reference.artifact_id.as_str()) else { - continue; - }; - if let Some(reason_code) = evidence_record_rejection_reason(evidence, source.group) { - if is_profile_record_candidate(&evidence.message) { - record_observations.push(rejected_record_observation( - evidence, - source, - reason_code, - )); + if context.intake_authority_is_bound { + for (position, evidence) in intake.evidence.iter().enumerate() { + let Some(source) = context.sources.get(evidence.reference.artifact_id.as_str()) else { + continue; + }; + if let Some(reason_code) = evidence_record_rejection_reason(evidence, source.group) { + if is_profile_record_candidate(&evidence.message) { + record_observations.push(rejected_record_observation( + evidence, + source, + reason_code, + )); + } + continue; } - continue; - } - if !source.fact_eligible || !context.evidence_identity_is_unique[position] { - continue; - } - match parse_fact(evidence, source, &intake.topology.site_handle) { - ProfileRecordParse::Accepted(fact) => { - grouped.entry(fact.key.clone()).or_default().push(*fact); + if !source.fact_eligible || !context.evidence_identity_is_unique[position] { + continue; } - ProfileRecordParse::Rejected(reason_code) => { - record_observations.push(rejected_record_observation( - evidence, - source, - reason_code, - )); + match parse_fact(evidence, source, &intake.topology.site_handle) { + ProfileRecordParse::Accepted(fact) => { + grouped.entry(fact.key.clone()).or_default().push(*fact); + } + ProfileRecordParse::Rejected(reason_code) => { + record_observations.push(rejected_record_observation( + evidence, + source, + reason_code, + )); + } + ProfileRecordParse::NotCandidate => {} } - ProfileRecordParse::NotCandidate => {} } } context.add_undeclared_peer_source_gaps(&grouped); diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index 0949f88a7..755ab59ce 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -398,6 +398,8 @@ fn assert_malformed_peer_source_fails_closed(analysis: &SccmSiteCoreAnalysis, ma } fn assert_intake_authority_mutation_fails_closed(analysis: &SccmSiteCoreAnalysis) { + // Once the intake seal fails, even the original canonical source values + // are no longer authority and must not survive the constant quarantine. assert_topology_incongruence_fails_closed( analysis, &[ @@ -1716,11 +1718,11 @@ fn invalid_intake_authority_never_exports_forged_scope_or_identity() { fn assert_topology_incongruence_fails_closed( analysis: &SccmSiteCoreAnalysis, - expected_gaps: &[(&str, &str, &str)], + forbidden_source_triples: &[(&str, &str, &str)], ) { assert_authority_invalid_analysis(analysis); let wire = serde_json::to_string(analysis).expect("analysis serializes"); - for (artifact_id, source_id, producer_host_handle) in expected_gaps { + for (artifact_id, source_id, producer_host_handle) in forbidden_source_triples { for forged_or_untrusted_value in [artifact_id, source_id, producer_host_handle] { assert!( !wire.contains(forged_or_untrusted_value), From efbbb46e6568a1a77076dca6b2a116b703d2434a Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 12:58:41 -0400 Subject: [PATCH 16/18] test(sccm): clarify server authority mutations --- .../tests/sccm_server_site_core.rs | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index 755ab59ce..fcc6d897d 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -1780,17 +1780,7 @@ fn changed_coverage_workflow_subject_handle_fails_site_core_congruence_closed() .workflow_subject_handle = Some("synthetic:subject:site-core-01".to_owned()); let analysis = analyze_site_core(&assessment); - assert_topology_incongruence_fails_closed( - &analysis, - &[ - ( - "sitecomp-current", - "server-sitecomp", - "synthetic:host:site-01", - ), - ("z-site-status", "server-status", "synthetic:host:site-01"), - ], - ); + assert_intake_authority_mutation_fails_closed(&analysis); } #[test] @@ -1823,17 +1813,11 @@ fn post_intake_topology_mutations_fail_site_core_authority_closed() { ("observed roles", analyze_site_core(&changed_observed_roles)), ]; - for (_, analysis) in &analyses { - assert_topology_incongruence_fails_closed( - analysis, - &[ - ( - "sitecomp-current", - "server-sitecomp", - "synthetic:host:site-01", - ), - ("z-site-status", "server-status", "synthetic:host:site-01"), - ], + for (mutation, analysis) in &analyses { + assert!( + analysis.results.is_empty(), + "{mutation} mutation still produced site-core results" ); + assert_intake_authority_mutation_fails_closed(analysis); } } From 686934834ee590362441de543ecfb0f46b409a03 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:17:31 -0400 Subject: [PATCH 17/18] test(sccm): bind authority assertions to intake --- .../server/windows/management_point_tests.rs | 2 +- .../tests/sccm_server_site_core.rs | 82 +++++++++++++------ 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs index 7d55babfd..5f301867f 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/management_point_tests.rs @@ -864,7 +864,7 @@ fn canonical_intake_adapter_accepts_reordered_authoritative_records() { } #[test] -fn canonical_intake_adapter_accepts_coverage_rows_distinguished_by_producer_host() { +fn canonical_intake_coverage_rows_distinguished_by_producer_host_reach_topology_validation() { let directory = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/sccm/server/intake/collision-same-basename-configured-roots"); let mut manifest = load_json(&directory.join("manifest.json")); diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index fcc6d897d..ced8ab4f9 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -397,20 +397,24 @@ fn assert_malformed_peer_source_fails_closed(analysis: &SccmSiteCoreAnalysis, ma assert!(!wire.contains(malformed_id)); } -fn assert_intake_authority_mutation_fails_closed(analysis: &SccmSiteCoreAnalysis) { +fn assert_intake_authority_mutation_fails_closed( + analysis: &SccmSiteCoreAnalysis, + intake: &SccmServerIntakeAssessment, +) { // Once the intake seal fails, even the original canonical source values // are no longer authority and must not survive the constant quarantine. - assert_topology_incongruence_fails_closed( - analysis, - &[ + let source_triples = intake + .artifacts + .iter() + .map(|artifact| { ( - "sitecomp-current", - "server-sitecomp", - "synthetic:host:site-01", - ), - ("z-site-status", "server-status", "synthetic:host:site-01"), - ], - ); + artifact.artifact_id.as_str(), + artifact.source_id.as_str(), + artifact.producer_host_handle.as_deref().unwrap_or_default(), + ) + }) + .collect::>(); + assert_invalid_authority_excludes_source_triples(analysis, &source_triples); } fn assert_authority_invalid_analysis(analysis: &SccmSiteCoreAnalysis) { @@ -1271,21 +1275,30 @@ fn post_intake_evidence_mutations_fail_sealed_authority_closed() { let mut wrong_role = healthy.clone(); wrong_role.evidence[0].role = SccmRole::ManagementPoint; - assert_intake_authority_mutation_fails_closed(&analyze_site_core(&wrong_role)); + assert_intake_authority_mutation_fails_closed(&analyze_site_core(&wrong_role), &wrong_role); let mut incomplete_reference = healthy.clone(); incomplete_reference.evidence[0].reference.line_end = None; - assert_intake_authority_mutation_fails_closed(&analyze_site_core(&incomplete_reference)); + assert_intake_authority_mutation_fails_closed( + &analyze_site_core(&incomplete_reference), + &incomplete_reference, + ); let mut cross_source_reference = healthy.clone(); cross_source_reference.evidence[0].reference.artifact_id = "z-site-status".to_owned(); cross_source_reference.evidence[0].reference.line_start = Some(10_001); cross_source_reference.evidence[0].reference.line_end = Some(10_001); - assert_intake_authority_mutation_fails_closed(&analyze_site_core(&cross_source_reference)); + assert_intake_authority_mutation_fails_closed( + &analyze_site_core(&cross_source_reference), + &cross_source_reference, + ); let mut unresolved_reference = healthy; unresolved_reference.evidence[0].reference.artifact_id = "orphan-sitecomp-record".to_owned(); - assert_intake_authority_mutation_fails_closed(&analyze_site_core(&unresolved_reference)); + assert_intake_authority_mutation_fails_closed( + &analyze_site_core(&unresolved_reference), + &unresolved_reference, + ); } #[test] @@ -1303,7 +1316,7 @@ fn foreign_post_intake_artifact_identity_cannot_scope_a_site_core_request() { assessment.evidence[0].reference.artifact_id = "foreign-artifact".to_owned(); let analysis = analyze_site_core(&assessment); - assert_intake_authority_mutation_fails_closed(&analysis); + assert_intake_authority_mutation_fails_closed(&analysis, &assessment); assert!(analysis.artifact_requests.iter().all(|request| request .scope .producer_host_handle @@ -1321,7 +1334,7 @@ fn post_intake_nonprofile_role_mutation_fails_sealed_authority_closed() { assessment.evidence[0].message = "ordinary non-profile source prose".to_owned(); assessment.evidence[0].role = SccmRole::ManagementPoint; - assert_intake_authority_mutation_fails_closed(&analyze_site_core(&assessment)); + assert_intake_authority_mutation_fails_closed(&analyze_site_core(&assessment), &assessment); } #[test] @@ -1598,7 +1611,7 @@ fn rotation_provenance_must_match_classification_and_requests_use_exact_pairs() .expect("sitecomp artifact") .rotation = Some(SccmRotation::LoUnderscore); let rejected = analyze_site_core(&mismatch); - assert_intake_authority_mutation_fails_closed(&rejected); + assert_intake_authority_mutation_fails_closed(&rejected, &mismatch); let backlog = analyze_site_core(&assess(&[ Source::sitecomp(INBOX_BACKLOG), @@ -1658,7 +1671,7 @@ fn coordinated_post_intake_producer_host_mutation_fails_site_core_authority_clos replace_source_producer_host(&mut assessment, "server-status", "synthetic:host:forged"); let analysis = analyze_site_core(&assessment); - assert_topology_incongruence_fails_closed( + assert_invalid_authority_excludes_source_triples( &analysis, &[ ( @@ -1716,7 +1729,7 @@ fn invalid_intake_authority_never_exports_forged_scope_or_identity() { })); } -fn assert_topology_incongruence_fails_closed( +fn assert_invalid_authority_excludes_source_triples( analysis: &SccmSiteCoreAnalysis, forbidden_source_triples: &[(&str, &str, &str)], ) { @@ -1724,6 +1737,9 @@ fn assert_topology_incongruence_fails_closed( let wire = serde_json::to_string(analysis).expect("analysis serializes"); for (artifact_id, source_id, producer_host_handle) in forbidden_source_triples { for forged_or_untrusted_value in [artifact_id, source_id, producer_host_handle] { + if forged_or_untrusted_value.is_empty() { + continue; + } assert!( !wire.contains(forged_or_untrusted_value), "invalid authority exported untrusted value {forged_or_untrusted_value}" @@ -1753,7 +1769,7 @@ fn swapped_coverage_producer_hosts_fail_site_core_congruence_closed() { .producer_host_handle = Some("synthetic:host:site-01".to_owned()); let analysis = analyze_site_core(&assessment); - assert_topology_incongruence_fails_closed( + assert_invalid_authority_excludes_source_triples( &analysis, &[ ( @@ -1780,7 +1796,7 @@ fn changed_coverage_workflow_subject_handle_fails_site_core_congruence_closed() .workflow_subject_handle = Some("synthetic:subject:site-core-01".to_owned()); let analysis = analyze_site_core(&assessment); - assert_intake_authority_mutation_fails_closed(&analysis); + assert_intake_authority_mutation_fails_closed(&analysis, &assessment); } #[test] @@ -1808,16 +1824,28 @@ fn post_intake_topology_mutations_fail_site_core_authority_closed() { .push(SccmRole::ManagementPoint); let analyses = [ - ("site handle", analyze_site_core(&changed_site_handle)), - ("capture host", analyze_site_core(&changed_capture_host)), - ("observed roles", analyze_site_core(&changed_observed_roles)), + ( + "site handle", + &changed_site_handle, + analyze_site_core(&changed_site_handle), + ), + ( + "capture host", + &changed_capture_host, + analyze_site_core(&changed_capture_host), + ), + ( + "observed roles", + &changed_observed_roles, + analyze_site_core(&changed_observed_roles), + ), ]; - for (mutation, analysis) in &analyses { + for (mutation, mutated_assessment, analysis) in &analyses { assert!( analysis.results.is_empty(), "{mutation} mutation still produced site-core results" ); - assert_intake_authority_mutation_fails_closed(analysis); + assert_intake_authority_mutation_fails_closed(analysis, mutated_assessment); } } From 5bd5768e5e276228b3d243ca07e87563784da832 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 13:22:12 -0400 Subject: [PATCH 18/18] test(sccm): keep authority exclusions nonvacuous --- .../tests/sccm_server_site_core.rs | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs index ced8ab4f9..98081b87e 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_site_core.rs @@ -410,7 +410,7 @@ fn assert_intake_authority_mutation_fails_closed( ( artifact.artifact_id.as_str(), artifact.source_id.as_str(), - artifact.producer_host_handle.as_deref().unwrap_or_default(), + artifact.producer_host_handle.as_deref(), ) }) .collect::>(); @@ -1243,6 +1243,18 @@ fn post_intake_source_contract_mutations_fail_sealed_authority_closed() { sitecomp.workflow_subject_handle = Some("synthetic:subject:client-01".to_owned()); assert_authority_invalid_analysis(&analyze_site_core(&wrong_subject)); + let mut missing_producer_host = healthy.clone(); + missing_producer_host + .artifacts + .iter_mut() + .find(|artifact| artifact.source_id == "server-sitecomp") + .expect("sitecomp artifact") + .producer_host_handle = None; + assert_intake_authority_mutation_fails_closed( + &analyze_site_core(&missing_producer_host), + &missing_producer_host, + ); + let mut duplicate = healthy; let duplicate_sitecomp = duplicate .artifacts @@ -1677,9 +1689,13 @@ fn coordinated_post_intake_producer_host_mutation_fails_site_core_authority_clos ( "sitecomp-current", "server-sitecomp", - "synthetic:host:forged", + Some("synthetic:host:forged"), + ), + ( + "z-site-status", + "server-status", + Some("synthetic:host:forged"), ), - ("z-site-status", "server-status", "synthetic:host:forged"), ], ); } @@ -1731,15 +1747,23 @@ fn invalid_intake_authority_never_exports_forged_scope_or_identity() { fn assert_invalid_authority_excludes_source_triples( analysis: &SccmSiteCoreAnalysis, - forbidden_source_triples: &[(&str, &str, &str)], + forbidden_source_triples: &[(&str, &str, Option<&str>)], ) { assert_authority_invalid_analysis(analysis); + assert!( + !forbidden_source_triples.is_empty(), + "authority assertion requires at least one source identity" + ); let wire = serde_json::to_string(analysis).expect("analysis serializes"); - for (artifact_id, source_id, producer_host_handle) in forbidden_source_triples { - for forged_or_untrusted_value in [artifact_id, source_id, producer_host_handle] { - if forged_or_untrusted_value.is_empty() { - continue; - } + for &(artifact_id, source_id, producer_host_handle) in forbidden_source_triples { + for forged_or_untrusted_value in [Some(artifact_id), Some(source_id), producer_host_handle] + .into_iter() + .flatten() + { + assert!( + !forged_or_untrusted_value.trim().is_empty(), + "authority assertion received a blank source identity" + ); assert!( !wire.contains(forged_or_untrusted_value), "invalid authority exported untrusted value {forged_or_untrusted_value}" @@ -1775,9 +1799,13 @@ fn swapped_coverage_producer_hosts_fail_site_core_congruence_closed() { ( "sitecomp-current", "server-sitecomp", - "synthetic:host:site-01", + Some("synthetic:host:site-01"), + ), + ( + "z-site-status", + "server-status", + Some("synthetic:host:site-02"), ), - ("z-site-status", "server-status", "synthetic:host:site-02"), ], ); }