From 5b4ad15ce5a327998176dd86055967c17ff391e6 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 15:59:14 -0400 Subject: [PATCH 1/9] test(sccm): define healthy DP transaction intake contract --- .../tests/sccm_server_distribution_point.rs | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs index 3bd61d887..b6962a682 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs @@ -1,8 +1,9 @@ use std::path::{Path, PathBuf}; use cmtraceopen_parser::sccm::server::windows::{ - analyze_distribution_point, assess_server_intake, SccmServerArtifactPayload, - SccmServerIntakeAssessment, SccmServerIntakeError, + analyze_distribution_point, analyze_distribution_point_content_from_server_intake, + assess_server_intake, SccmServerArtifactPayload, SccmServerIntakeAssessment, + SccmServerIntakeError, }; use cmtraceopen_parser::sccm::{SccmCoverageState, SccmRole, SccmRotation, SccmTimeOrderingState}; use serde_json::Value; @@ -57,6 +58,33 @@ fn load_assessment(scenario: &str) -> SccmServerIntakeAssessment { assess_manifest(&manifest, &payloads).expect("fixture intake is accepted") } +fn load_distribution_point_assessment(scenario: &str) -> SccmServerIntakeAssessment { + let scenario_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/sccm/server/distribution_point") + .join(scenario); + let manifest_json = + std::fs::read_to_string(scenario_root.join("manifest.json")).expect("manifest is readable"); + let manifest: Value = serde_json::from_str(&manifest_json).expect("manifest is valid JSON"); + let payloads = manifest["artifacts"] + .as_array() + .expect("artifacts are an array") + .iter() + .filter_map(|artifact| { + let relative_path = artifact["relativePath"].as_str()?; + Some(SccmServerArtifactPayload { + manifest_artifact_id: artifact["artifactId"] + .as_str() + .expect("artifact id is a string") + .to_owned(), + bytes: std::fs::read(scenario_root.join(relative_path)) + .expect("captured evidence is readable"), + }) + }) + .collect::>(); + assess_server_intake(&manifest_json, &payloads) + .expect("distribution point fixture is canonical server intake") +} + fn dp_manifest_artifact_mut(manifest: &mut Value) -> &mut Value { manifest["artifacts"] .as_array_mut() @@ -240,6 +268,30 @@ fn distribution_point_adapter_projects_only_canonical_intake_observations_determ ); } +#[test] +fn healthy_package_reduces_a_sealed_role_local_transaction() { + let assessment = load_distribution_point_assessment("healthy-package"); + let bounded = analyze_distribution_point(&assessment); + assert_eq!(bounded.source_observations.len(), 6); + + let analysis = analyze_distribution_point_content_from_server_intake(&assessment) + .expect("healthy canonical intake must enter the DP semantic reducer"); + + assert!(!analysis.cross_side_correlation_performed); + assert_eq!(analysis.transactions.len(), 1); + assert_eq!(analysis.transactions[0].key.package_id, "LAB00001"); + assert_eq!(analysis.transactions[0].key.content_id, "content-alpha"); + assert_eq!(analysis.transactions[0].key.content_version, 1); + assert_eq!( + analysis.transactions[0] + .key + .distribution_point_handle + .as_str(), + "safe:dp:lab-dp-01" + ); + assert_eq!(analysis.transactions[0].evidence.len(), 6); +} + #[test] fn sealed_intake_without_dp_source_version_reaches_profile_eligibility_guard() { let assessment = assess_complete_manifest_after(|manifest| { From 037baa618a5fd82e26bd14bcb21a001d3b41d9a4 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 16:04:19 -0400 Subject: [PATCH 2/9] fix(sccm): align DP intake profile guards --- .../sccm/server/windows/distribution_point.rs | 31 ++------ .../src/sccm/server/windows/intake.rs | 59 ++++++++++++--- .../tests/sccm_server_distribution_point.rs | 72 +++++++++++++++++-- 3 files changed, 121 insertions(+), 41 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs index 7f97ae727..d9fdb6bc4 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs @@ -220,7 +220,7 @@ fn is_dp_distribution_artifact(artifact: &SccmServerArtifactAssessment) -> bool fn admitted_for_source_observation( artifact: &SccmServerArtifactAssessment, - evidence: &crate::sccm::SccmEvidence, + evidence: &SccmEvidence, ) -> bool { artifact.state == SccmCoverageState::Captured && artifact.profile_eligible @@ -239,7 +239,7 @@ fn artifact_metadata_is_congruent( && artifact.profile_eligible && artifact.parser_eligible && artifact.fragment_complete != Some(false) - && supported_source_version(artifact.source_version.as_deref()) + && intake.source_version_is_profile_eligible(artifact.source_version.as_deref()) && rotation_is_canonical_for_artifact(artifact) && safe_assessed_handle(&intake.topology.capture_host_handle) && safe_assessed_handle(&intake.topology.site_handle) @@ -252,30 +252,6 @@ fn artifact_metadata_is_congruent( && coverage_is_congruent(intake, artifact, artifact_id_counts) } -fn supported_source_version(value: Option<&str>) -> bool { - let Some(value) = value else { - return false; - }; - if value == "5.00.TEST" { - return true; - } - let mut parts = value.split('.'); - matches!( - ( - parts.next(), - parts.next(), - parts.next(), - parts.next(), - parts.next(), - ), - (Some("5"), Some("00"), Some(build), Some(revision), None) - if build.len() == 4 - && revision.len() == 4 - && build.bytes().all(|byte| byte.is_ascii_digit()) - && revision.bytes().all(|byte| byte.is_ascii_digit()) - ) -} - fn rotation_is_canonical_for_artifact(artifact: &SccmServerArtifactAssessment) -> bool { let Some(basename) = artifact.original_basename.as_deref() else { return false; @@ -493,6 +469,9 @@ fn artifact_requests(gaps: &[SccmDistributionPointCoverageGap]) -> Vec, pub findings: Vec, pub next_artifact_requests: Vec, + /// Private fixture provenance retained so downstream test-only profiles + /// reuse the canonical source-version predicate rather than copying it. + synthetic_fixture: bool, /// Private integrity binding for the canonical projection that server-role /// reducers consume. It is sequence-independent, but every authoritative /// schema, topology, artifact, coverage, and evidence field remains bound @@ -234,6 +237,10 @@ impl SccmServerIntakeAssessment { .as_ref() .is_some_and(|integrity| integrity == &self.intake_integrity) } + + pub(crate) fn source_version_is_profile_eligible(&self, value: Option<&str>) -> bool { + value.is_some_and(|value| source_version_is_profile_eligible(value, self.synthetic_fixture)) + } } fn normalized_topology_or_none( @@ -782,6 +789,7 @@ pub fn assess_server_intake( evidence, findings: Vec::new(), next_artifact_requests, + synthetic_fixture: manifest.synthetic_fixture, intake_integrity, extensions: normalize_opaque_extensions( &manifest.extensions, @@ -1737,6 +1745,20 @@ mod intake_integrity_tests { use super::*; + #[test] + fn source_version_profile_predicate_keeps_synthetic_test_scope_exact() { + assert!(source_version_is_profile_eligible("5.00.TEST", true)); + assert!(source_version_is_profile_eligible("5.00.TEST.0001", true)); + assert!(!source_version_is_profile_eligible("5.00.TEST.1", true)); + assert!(!source_version_is_profile_eligible( + "5.00.TEST.0001.extra", + true + )); + assert!(!source_version_is_profile_eligible("5.00.9128.1000", true)); + assert!(source_version_is_profile_eligible("5.00.9128.1000", false)); + assert!(!source_version_is_profile_eligible("5.00.TEST.0001", false)); + } + fn canonical_assessment() -> SccmServerIntakeAssessment { let directory = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/sccm/server/management-point/canonical-intake-policy-scope"); @@ -2283,12 +2305,8 @@ fn normalize_source_version( let Some(value) = value else { return Ok(None); }; - let safe = if synthetic_fixture { - value == "5.00.TEST" - } else { - source_version_is_profile_eligible(value, false) - || opaque_sha256_handle(value, "cmtraceopen.version.sha256.v1:") - }; + let safe = source_version_is_profile_eligible(value, synthetic_fixture) + || (!synthetic_fixture && opaque_sha256_handle(value, "cmtraceopen.version.sha256.v1:")); if !safe { return Err(SccmServerIntakeError::InvalidArtifact); } @@ -2362,9 +2380,17 @@ fn validate_artifact_annotations( Ok(()) } -fn source_version_is_profile_eligible(value: &str, synthetic_fixture: bool) -> bool { - if synthetic_fixture && value == "5.00.TEST" { - return true; +pub(crate) fn source_version_is_profile_eligible(value: &str, synthetic_fixture: bool) -> bool { + if synthetic_fixture { + if value == "5.00.TEST" { + return true; + } + let mut parts = value.split('.'); + return matches!( + (parts.next(), parts.next(), parts.next(), parts.next(), parts.next()), + (Some("5"), Some("00"), Some("TEST"), Some(revision), None) + if revision.len() == 4 && revision.bytes().all(|byte| byte.is_ascii_digit()) + ); } let mut parts = value.split('.'); matches!( @@ -2393,6 +2419,9 @@ fn safe_manifest_artifact_id(value: &str, synthetic_fixture: bool) -> bool { | "b-sitecomp" | "dp-dist-current" | "dp-distribution-absent-candidate" + | "dp-healthy-01-distmgr" + | "dp-healthy-02-pkgxfer" + | "dp-healthy-03-provider" | "mp-iis-skipped" | "mp-policy-access-denied" | "mp-policy-configured" @@ -2452,6 +2481,9 @@ fn safe_lineage_id(value: &str, synthetic_fixture: bool) -> bool { value, "dp-dist-lab" | "dp-distribution-default" + | "healthy-distmgr" + | "healthy-pkgxfer" + | "healthy-provider" | "mp-iis-supplement" | "mp-policy-a" | "mp-policy-access" @@ -2479,6 +2511,9 @@ fn safe_path_fingerprint(value: &str, synthetic_fixture: bool) -> bool { "synthetic:path:a-mp" | "synthetic:path:a-site" | "synthetic:path:dp-default" + | "synthetic:healthy-distmgr" + | "synthetic:healthy-pkgxfer" + | "synthetic:healthy-provider" | "synthetic:path:iis-not-requested" | "synthetic:path:mp-configured-a" | "synthetic:path:mp-default" @@ -2612,13 +2647,17 @@ fn safe_optional_handle(value: Option<&str>, synthetic_fixture: bool, domain: &s }; if synthetic_fixture { return match domain { - "host" => matches!(value, "synthetic:host:mp-01" | "synthetic:host:site-01"), + "host" => matches!( + value, + "synthetic:host:mp-01" | "synthetic:host:site-01" | "safe:server:lab-pri-01" + ), "subject" => { matches!( value, "synthetic:subject:dp-01" | "synthetic:subject:dp-02" | "synthetic:subject:sup-01" + | "safe:dp:lab-dp-01" ) } _ => false, diff --git a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs index b6962a682..1c08d5986 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs @@ -3,10 +3,11 @@ use std::path::{Path, PathBuf}; use cmtraceopen_parser::sccm::server::windows::{ analyze_distribution_point, analyze_distribution_point_content_from_server_intake, assess_server_intake, SccmServerArtifactPayload, SccmServerIntakeAssessment, - SccmServerIntakeError, + SccmServerIntakeError, SCCM_DISTRIBUTION_POINT_ANALYSIS_SCHEMA_VERSION, + SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_ID, SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_VERSION, }; use cmtraceopen_parser::sccm::{SccmCoverageState, SccmRole, SccmRotation, SccmTimeOrderingState}; -use serde_json::Value; +use serde_json::{json, Value}; fn intake_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/sccm/server/intake") @@ -62,9 +63,10 @@ fn load_distribution_point_assessment(scenario: &str) -> SccmServerIntakeAssessm let scenario_root = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/sccm/server/distribution_point") .join(scenario); - let manifest_json = + let fixture_manifest_json = std::fs::read_to_string(scenario_root.join("manifest.json")).expect("manifest is readable"); - let manifest: Value = serde_json::from_str(&manifest_json).expect("manifest is valid JSON"); + let manifest: Value = + serde_json::from_str(&fixture_manifest_json).expect("manifest is valid JSON"); let payloads = manifest["artifacts"] .as_array() .expect("artifacts are an array") @@ -81,7 +83,54 @@ fn load_distribution_point_assessment(scenario: &str) -> SccmServerIntakeAssessm }) }) .collect::>(); - assess_server_intake(&manifest_json, &payloads) + let canonical_manifest = json!({ + "sccmManifestVersion": 1, + "syntheticFixture": true, + "proposalOnly": true, + "privacy": {"synthetic": true, "rawPaths": "redacted"}, + "bundleRole": "server", + "topology": { + "captureHost": "LAB-CM01", + "siteCode": manifest["topology"]["siteCode"], + "rolesObserved": manifest["topology"]["rolesObserved"], + }, + "artifacts": manifest["artifacts"] + .as_array() + .expect("artifacts are an array") + .iter() + .map(|artifact| json!({ + "artifactId": artifact["artifactId"], + "producerRole": artifact["producerRole"], + "producerHostHandle": artifact["producerHostHandle"], + "workflowSubject": { + "role": artifact["workflowSubjectRole"], + "instanceHandle": artifact["workflowSubjectHandle"], + }, + "sourceId": artifact["sourceId"], + "sourceKind": artifact["sourceKind"], + "sourceVersion": artifact["sourceVersion"], + "originalPath": "REDACTED_DP_SOURCE_ROOT", + "originalBasename": artifact["originalBasename"], + "configuredPathProvenance": { + "state": "configured", + "pathFingerprint": artifact["pathFingerprint"], + }, + "rotation": { + "kind": artifact["rotation"]["kind"], + "lineageId": artifact["rotation"]["lineageId"], + }, + "captureState": artifact["captureState"], + "encoding": artifact["encoding"], + "collectionLimit": artifact["collectionLimit"], + "collectedUtc": artifact["collectedUtc"], + "relativePath": artifact["relativePath"], + "bytesCopied": artifact["bytesCopied"], + })) + .collect::>(), + }); + let canonical_manifest_json = + serde_json::to_string(&canonical_manifest).expect("canonical manifest serializes"); + assess_server_intake(&canonical_manifest_json, &payloads) .expect("distribution point fixture is canonical server intake") } @@ -229,6 +278,19 @@ fn distribution_point_adapter_projects_only_canonical_intake_observations_determ let analysis = analyze_distribution_point(&assessment); assert!(!analysis.cross_side_correlation_performed); + assert_eq!( + analysis.schema_version, + SCCM_DISTRIBUTION_POINT_ANALYSIS_SCHEMA_VERSION + ); + assert_eq!( + analysis.profile.id, + SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_ID + ); + assert_eq!( + analysis.profile.version, + SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_VERSION + ); + assert_eq!(analysis.profile.stability, "experimental"); assert!(analysis.coverage_gaps.is_empty()); assert!(analysis.artifact_requests.is_empty()); assert_eq!(analysis.source_observations.len(), 1); From 43d665483bcfe7fdb4cbf33c296a4e329e97d6b9 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 16:11:44 -0400 Subject: [PATCH 3/9] feat(sccm): reduce healthy DP content transaction --- .../sccm/server/windows/distribution_point.rs | 394 ++++++++++++++++++ .../src/sccm/server/windows/intake.rs | 5 +- .../tests/sccm_server_distribution_point.rs | 31 +- 3 files changed, 425 insertions(+), 5 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs index d9fdb6bc4..022170880 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs @@ -9,6 +9,7 @@ use std::collections::{BTreeMap, BTreeSet}; use serde::Serialize; +use thiserror::Error; use crate::sccm::{ classify_artifact_name, SccmArtifactFamily, SccmArtifactRequest, SccmCoverageState, @@ -24,6 +25,9 @@ pub const SCCM_DISTRIBUTION_POINT_ANALYSIS_SCHEMA_VERSION: u32 = 1; pub const SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_ID: &str = "sccm-dp-intake-envelope"; pub const SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_VERSION: u32 = 1; pub const SCCM_DISTRIBUTION_POINT_SOURCE_ID: &str = "server-dp-distribution"; +pub const SCCM_DISTRIBUTION_POINT_CONTENT_ANALYSIS_SCHEMA_VERSION: u32 = 1; +pub const SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_ID: &str = "dp-server-5.00.test-v1"; +pub const SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_VERSION: u32 = 1; const SCCM_DISTRIBUTION_POINT_INTAKE_AUTHORITY_REASON: &str = "Canonical server intake authority could not be verified."; @@ -80,6 +84,113 @@ pub struct SccmDistributionPointAnalysis { pub cross_side_correlation_performed: bool, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum SccmDistributionPointContentPhase { + ReceiveContent, + Distribute, + Transfer, + Validate, + MakeAvailable, + ServeOrReport, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum SccmDistributionPointContentState { + Succeeded, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum SccmDistributionPointContentClassification { + Success, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum SccmDistributionPointContentConfidence { + High, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SccmDistributionPointContentKey { + pub package_id: String, + pub content_id: String, + pub content_version: u32, + pub site_code: String, + pub distribution_point_handle: String, + pub extraction_profile_id: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SccmDistributionPointContentObservation { + pub phase: SccmDistributionPointContentPhase, + pub terminal: bool, + pub timestamp: SccmTimestamp, + pub evidence: SccmEvidenceRef, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SccmDistributionPointContentTransaction { + pub transaction_id: String, + pub key: SccmDistributionPointContentKey, + pub state: SccmDistributionPointContentState, + pub classification: SccmDistributionPointContentClassification, + pub confidence: SccmDistributionPointContentConfidence, + pub last_successful_phase: SccmDistributionPointContentPhase, + pub evidence: Vec, + pub observations: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SccmDistributionPointContentAnalysis { + pub schema_version: u32, + pub workflow: SccmDistributionPointWorkflow, + pub profile: SccmDistributionPointProfile, + pub transactions: Vec, + pub cross_side_correlation_performed: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum SccmDistributionPointContentIntakeError { + #[error("canonical server intake authority could not be verified")] + IntakeAuthority, + #[error("Distribution Point topology is not compatible with the admitted profile")] + Topology, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct DistributionPointFactKey { + package_id: String, + content_id: String, + content_version: u32, + site_code: String, + distribution_point_handle: String, +} + +#[derive(Debug, Clone)] +struct DistributionPointFact { + key: DistributionPointFactKey, + phase: SccmDistributionPointContentPhase, + terminal: bool, + reference: SccmEvidenceRef, + timestamp: SccmTimestamp, +} + +/// Private canonical transaction envelope. Facts only originate from an +/// integrity-bound server intake assessment; callers cannot construct or +/// submit source facts directly. +#[derive(Debug)] +struct DistributionPointTransactionEnvelope { + key: DistributionPointFactKey, + facts: Vec, +} + /// Project only complete, profile-eligible logical CCM records from the /// canonical server intake. The output is source-local and intentionally does /// not interpret message text as a package/content success or failure. @@ -170,6 +281,289 @@ pub fn analyze_distribution_point( } } +/// Reduce the approved synthetic DP package profile from canonical server +/// intake. This first slice recognizes only a complete, source-local healthy +/// transaction; incomplete, retry, and terminal-failure facts intentionally +/// remain outside this API until their dedicated contracts are reviewed. +pub fn analyze_distribution_point_content_from_server_intake( + intake: &SccmServerIntakeAssessment, +) -> Result { + if !intake.adapter_authority_is_intake_bound() || !intake.topology_authority_is_intake_bound() { + return Err(SccmDistributionPointContentIntakeError::IntakeAuthority); + } + if !intake + .topology + .roles_observed + .contains(&SccmRole::DistributionPoint) + { + return Err(SccmDistributionPointContentIntakeError::Topology); + } + + let bounded = analyze_distribution_point(intake); + let evidence_by_entry_id = intake + .evidence + .iter() + .map(|evidence| (evidence.reference.entry_id.as_str(), evidence)) + .collect::>(); + let artifacts_by_id = intake + .artifacts + .iter() + .map(|artifact| (artifact.artifact_id.as_str(), artifact)) + .collect::>(); + let mut facts_by_key = BTreeMap::>::new(); + + for observation in &bounded.source_observations { + let Some(evidence) = evidence_by_entry_id.get(observation.evidence.entry_id.as_str()) + else { + continue; + }; + let Some(artifact) = artifacts_by_id.get(observation.artifact_id.as_str()) else { + continue; + }; + let Some(fact) = parse_healthy_distribution_point_fact(observation, artifact, evidence) + else { + continue; + }; + facts_by_key.entry(fact.key.clone()).or_default().push(fact); + } + + let mut transactions = facts_by_key + .into_iter() + .filter_map(|(key, facts)| { + reduce_healthy_transaction(DistributionPointTransactionEnvelope { key, facts }) + }) + .collect::>(); + transactions.sort_by(|left, right| left.transaction_id.cmp(&right.transaction_id)); + + Ok(SccmDistributionPointContentAnalysis { + schema_version: SCCM_DISTRIBUTION_POINT_CONTENT_ANALYSIS_SCHEMA_VERSION, + workflow: SccmDistributionPointWorkflow::DistributionPointContent, + profile: SccmDistributionPointProfile { + id: SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_ID.to_owned(), + version: SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_VERSION, + stability: "experimental".to_owned(), + }, + transactions, + cross_side_correlation_performed: false, + }) +} + +fn parse_healthy_distribution_point_fact( + observation: &SccmDistributionPointSourceObservation, + artifact: &SccmServerArtifactAssessment, + evidence: &SccmEvidence, +) -> Option { + let phase = exact_message_token(&evidence.message, "Phase").and_then(content_phase)?; + let disposition = exact_message_token(&evidence.message, "Disposition")?; + let terminal = exact_message_token(&evidence.message, "Terminal")?; + let package_id = exact_message_token(&evidence.message, "PackageId")?; + let content_id = exact_message_token(&evidence.message, "ContentId")?; + let content_version = exact_message_token(&evidence.message, "ContentVersion")? + .parse::() + .ok() + .filter(|version| *version > 0)?; + let site_code = exact_message_token(&evidence.message, "SiteCode")?; + let distribution_point_handle = exact_message_token(&evidence.message, "DpHandle")?; + let profile_id = exact_message_token(&evidence.message, "ProfileId")?; + + if disposition != "succeeded" + || profile_id != SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_ID + || !safe_package_id(package_id) + || !safe_content_id(content_id) + || !safe_site_code(site_code) + || !safe_distribution_point_handle(distribution_point_handle) + || !matches!( + evidence.timestamp.ordering_state, + SccmTimeOrderingState::NormalizedUtc + ) + || evidence.timestamp.offset_minutes.is_none() + || evidence.timestamp.utc_millis.is_none() + { + return None; + } + + let expected_source = match phase { + SccmDistributionPointContentPhase::ReceiveContent + | SccmDistributionPointContentPhase::Distribute => { + observation.producer_role == SccmRole::SiteServer + && artifact.original_basename.as_deref() == Some("distmgr.log") + } + SccmDistributionPointContentPhase::Transfer => { + observation.producer_role == SccmRole::SiteServer + && artifact.original_basename.as_deref() == Some("PkgXferMgr.log") + } + SccmDistributionPointContentPhase::Validate + | SccmDistributionPointContentPhase::MakeAvailable + | SccmDistributionPointContentPhase::ServeOrReport => { + observation.producer_role == SccmRole::DistributionPoint + && artifact.original_basename.as_deref() == Some("SMSDPProv.log") + } + }; + let observed_distribution_point = match observation.producer_role { + SccmRole::SiteServer => observation.workflow_subject_handle.as_deref(), + SccmRole::DistributionPoint => observation.producer_host_handle.as_deref(), + _ => None, + }; + let expected_terminal = if phase == SccmDistributionPointContentPhase::ServeOrReport { + "true" + } else { + "false" + }; + if !expected_source + || observed_distribution_point != Some(distribution_point_handle) + || terminal != expected_terminal + { + return None; + } + + Some(DistributionPointFact { + key: DistributionPointFactKey { + package_id: package_id.to_owned(), + content_id: content_id.to_owned(), + content_version, + site_code: site_code.to_owned(), + distribution_point_handle: distribution_point_handle.to_owned(), + }, + phase, + terminal: terminal == "true", + reference: evidence.reference.clone(), + timestamp: evidence.timestamp.clone(), + }) +} + +fn reduce_healthy_transaction( + mut envelope: DistributionPointTransactionEnvelope, +) -> Option { + envelope.facts.sort_by(|left, right| { + ( + left.phase, + left.timestamp.utc_millis, + left.reference.entry_id.as_str(), + ) + .cmp(&( + right.phase, + right.timestamp.utc_millis, + right.reference.entry_id.as_str(), + )) + }); + let expected_phases = [ + SccmDistributionPointContentPhase::ReceiveContent, + SccmDistributionPointContentPhase::Distribute, + SccmDistributionPointContentPhase::Transfer, + SccmDistributionPointContentPhase::Validate, + SccmDistributionPointContentPhase::MakeAvailable, + SccmDistributionPointContentPhase::ServeOrReport, + ]; + if envelope.facts.len() != expected_phases.len() + || envelope + .facts + .iter() + .zip(expected_phases) + .any(|(fact, expected)| fact.phase != expected) + { + return None; + } + + let mut previous_timestamp = None; + for fact in &envelope.facts { + let timestamp = fact.timestamp.utc_millis?; + if previous_timestamp.is_some_and(|previous| timestamp <= previous) { + return None; + } + previous_timestamp = Some(timestamp); + } + if envelope.facts.iter().any(|fact| { + fact.terminal != (fact.phase == SccmDistributionPointContentPhase::ServeOrReport) + }) { + return None; + } + + let key = SccmDistributionPointContentKey { + package_id: envelope.key.package_id, + content_id: envelope.key.content_id, + content_version: envelope.key.content_version, + site_code: envelope.key.site_code, + distribution_point_handle: envelope.key.distribution_point_handle, + extraction_profile_id: SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_ID.to_owned(), + }; + let transaction_id = format!( + "dp:{}:{}:v{}:{}", + key.package_id, key.content_id, key.content_version, key.distribution_point_handle + ); + let evidence = envelope + .facts + .iter() + .map(|fact| fact.reference.clone()) + .collect::>(); + let observations = envelope + .facts + .into_iter() + .map(|fact| SccmDistributionPointContentObservation { + phase: fact.phase, + terminal: fact.terminal, + timestamp: fact.timestamp, + evidence: fact.reference, + }) + .collect::>(); + + Some(SccmDistributionPointContentTransaction { + transaction_id, + key, + state: SccmDistributionPointContentState::Succeeded, + classification: SccmDistributionPointContentClassification::Success, + confidence: SccmDistributionPointContentConfidence::High, + last_successful_phase: SccmDistributionPointContentPhase::ServeOrReport, + evidence, + observations, + }) +} + +fn exact_message_token<'a>(message: &'a str, label: &str) -> Option<&'a str> { + let prefix = format!("{label}="); + let mut values = message + .split(';') + .map(str::trim) + .filter_map(|segment| segment.strip_prefix(&prefix)); + let value = values.next()?; + values.next().is_none().then_some(value) +} + +fn content_phase(value: &str) -> Option { + match value { + "receiveContent" => Some(SccmDistributionPointContentPhase::ReceiveContent), + "distribute" => Some(SccmDistributionPointContentPhase::Distribute), + "transfer" => Some(SccmDistributionPointContentPhase::Transfer), + "validate" => Some(SccmDistributionPointContentPhase::Validate), + "makeAvailable" => Some(SccmDistributionPointContentPhase::MakeAvailable), + "serveOrReport" => Some(SccmDistributionPointContentPhase::ServeOrReport), + _ => None, + } +} + +fn safe_package_id(value: &str) -> bool { + (3..=32).contains(&value.len()) + && value + .bytes() + .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit()) +} + +fn safe_content_id(value: &str) -> bool { + (3..=128).contains(&value.len()) + && value + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') +} + +fn safe_site_code(value: &str) -> bool { + value.len() == 3 && value.bytes().all(|byte| byte.is_ascii_alphanumeric()) +} + +fn safe_distribution_point_handle(value: &str) -> bool { + value + .strip_prefix("safe:dp:") + .is_some_and(|handle| !handle.is_empty() && handle.len() <= 128 && !handle.contains("..")) +} + fn intake_authority_invalid_analysis() -> SccmDistributionPointAnalysis { let coverage_gaps = vec![SccmDistributionPointCoverageGap { source_id: SCCM_DISTRIBUTION_POINT_SOURCE_ID.to_owned(), diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs index 6c0da7057..7eaca5caf 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/intake.rs @@ -2649,7 +2649,10 @@ fn safe_optional_handle(value: Option<&str>, synthetic_fixture: bool, domain: &s return match domain { "host" => matches!( value, - "synthetic:host:mp-01" | "synthetic:host:site-01" | "safe:server:lab-pri-01" + "synthetic:host:mp-01" + | "synthetic:host:site-01" + | "safe:server:lab-pri-01" + | "safe:dp:lab-dp-01" ), "subject" => { matches!( diff --git a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs index 1c08d5986..6605e7ffc 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs @@ -102,9 +102,13 @@ fn load_distribution_point_assessment(scenario: &str) -> SccmServerIntakeAssessm "artifactId": artifact["artifactId"], "producerRole": artifact["producerRole"], "producerHostHandle": artifact["producerHostHandle"], - "workflowSubject": { - "role": artifact["workflowSubjectRole"], - "instanceHandle": artifact["workflowSubjectHandle"], + "workflowSubject": if artifact["producerRole"] == "distributionPoint" { + Value::Null + } else { + json!({ + "role": artifact["workflowSubjectRole"], + "instanceHandle": artifact["workflowSubjectHandle"], + }) }, "sourceId": artifact["sourceId"], "sourceKind": artifact["sourceKind"], @@ -123,7 +127,7 @@ fn load_distribution_point_assessment(scenario: &str) -> SccmServerIntakeAssessm "encoding": artifact["encoding"], "collectionLimit": artifact["collectionLimit"], "collectedUtc": artifact["collectedUtc"], - "relativePath": artifact["relativePath"], + "relativePath": canonical_distribution_point_relative_path(artifact), "bytesCopied": artifact["bytesCopied"], })) .collect::>(), @@ -134,6 +138,25 @@ fn load_distribution_point_assessment(scenario: &str) -> SccmServerIntakeAssessm .expect("distribution point fixture is canonical server intake") } +fn canonical_distribution_point_relative_path(artifact: &Value) -> String { + let role_segment = match artifact["producerRole"].as_str() { + Some("siteServer") => "site-server", + Some("distributionPoint") => "distribution-point", + _ => panic!("DP fixture has a supported producer role"), + }; + let basename = artifact["originalBasename"] + .as_str() + .expect("DP fixture has a source basename"); + let subject_segment = if artifact["producerRole"] == "distributionPoint" { + "" + } else { + "subject-distribution-point/" + }; + format!( + "evidence/sccm/server/{role_segment}/server-dp-distribution/{subject_segment}current/{basename}" + ) +} + fn dp_manifest_artifact_mut(manifest: &mut Value) -> &mut Value { manifest["artifacts"] .as_array_mut() From 374a6ce304d20826ff753f5140babee6289acd80 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 16:25:25 -0400 Subject: [PATCH 4/9] fix(sccm): bind DP profile site to topology --- .../sccm/server/windows/distribution_point.rs | 19 ++++++++-- .../tests/sccm_server_distribution_point.rs | 36 +++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs index 022170880..146bb55c6 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs @@ -310,6 +310,8 @@ pub fn analyze_distribution_point_content_from_server_intake( .iter() .map(|artifact| (artifact.artifact_id.as_str(), artifact)) .collect::>(); + let expected_site_code = + selected_content_profile_site_code(intake.topology.site_handle.as_str()); let mut facts_by_key = BTreeMap::>::new(); for observation in &bounded.source_observations { @@ -320,8 +322,12 @@ pub fn analyze_distribution_point_content_from_server_intake( let Some(artifact) = artifacts_by_id.get(observation.artifact_id.as_str()) else { continue; }; - let Some(fact) = parse_healthy_distribution_point_fact(observation, artifact, evidence) - else { + let Some(fact) = parse_healthy_distribution_point_fact( + observation, + artifact, + evidence, + expected_site_code, + ) else { continue; }; facts_by_key.entry(fact.key.clone()).or_default().push(fact); @@ -352,6 +358,7 @@ fn parse_healthy_distribution_point_fact( observation: &SccmDistributionPointSourceObservation, artifact: &SccmServerArtifactAssessment, evidence: &SccmEvidence, + expected_site_code: Option<&str>, ) -> Option { let phase = exact_message_token(&evidence.message, "Phase").and_then(content_phase)?; let disposition = exact_message_token(&evidence.message, "Disposition")?; @@ -371,6 +378,7 @@ fn parse_healthy_distribution_point_fact( || !safe_package_id(package_id) || !safe_content_id(content_id) || !safe_site_code(site_code) + || expected_site_code != Some(site_code) || !safe_distribution_point_handle(distribution_point_handle) || !matches!( evidence.timestamp.ordering_state, @@ -431,6 +439,13 @@ fn parse_healthy_distribution_point_fact( }) } +fn selected_content_profile_site_code(topology_site_handle: &str) -> Option<&'static str> { + match topology_site_handle { + "synthetic:site:lab" => Some("LAB"), + _ => None, + } +} + fn reduce_healthy_transaction( mut envelope: DistributionPointTransactionEnvelope, ) -> Option { diff --git a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs index 6605e7ffc..a546b1b88 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs @@ -60,14 +60,21 @@ fn load_assessment(scenario: &str) -> SccmServerIntakeAssessment { } fn load_distribution_point_assessment(scenario: &str) -> SccmServerIntakeAssessment { + load_distribution_point_assessment_after(scenario, |_, _| {}) +} + +fn load_distribution_point_assessment_after( + scenario: &str, + mutate: impl FnOnce(&mut Value, &mut Vec), +) -> SccmServerIntakeAssessment { let scenario_root = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/sccm/server/distribution_point") .join(scenario); let fixture_manifest_json = std::fs::read_to_string(scenario_root.join("manifest.json")).expect("manifest is readable"); - let manifest: Value = + let mut manifest: Value = serde_json::from_str(&fixture_manifest_json).expect("manifest is valid JSON"); - let payloads = manifest["artifacts"] + let mut payloads = manifest["artifacts"] .as_array() .expect("artifacts are an array") .iter() @@ -83,6 +90,7 @@ fn load_distribution_point_assessment(scenario: &str) -> SccmServerIntakeAssessm }) }) .collect::>(); + mutate(&mut manifest, &mut payloads); let canonical_manifest = json!({ "sccmManifestVersion": 1, "syntheticFixture": true, @@ -377,6 +385,30 @@ fn healthy_package_reduces_a_sealed_role_local_transaction() { assert_eq!(analysis.transactions[0].evidence.len(), 6); } +#[test] +fn healthy_package_requires_profile_site_token_to_match_sealed_topology() { + let assessment = load_distribution_point_assessment_after("healthy-package", |_, payloads| { + for payload in payloads { + let content = + std::str::from_utf8(&payload.bytes).expect("synthetic DP evidence is UTF-8"); + assert!( + content.contains("SiteCode=LAB"), + "every healthy source must carry the profile site token" + ); + payload.bytes = content.replace("SiteCode=LAB", "SiteCode=ABC").into_bytes(); + } + }); + assert_eq!(assessment.topology.site_handle, "synthetic:site:lab"); + + let analysis = analyze_distribution_point_content_from_server_intake(&assessment) + .expect("site-token mismatch remains sealed evidence, not forged authority"); + + assert!( + analysis.transactions.is_empty(), + "valid-looking evidence for another site cannot become a healthy transaction" + ); +} + #[test] fn sealed_intake_without_dp_source_version_reaches_profile_eligibility_guard() { let assessment = assess_complete_manifest_after(|manifest| { From e667bf1ed4ead4f8d90747ef00742fae79cb4a7e Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 16:31:41 -0400 Subject: [PATCH 5/9] fix(sccm): preserve DP semantic coverage gaps --- .../sccm/server/windows/distribution_point.rs | 134 +++++++++- .../tests/sccm_server_distribution_point.rs | 239 +++++++++++++++++- 2 files changed, 362 insertions(+), 11 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs index 146bb55c6..23b3bb757 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs @@ -110,6 +110,7 @@ pub enum SccmDistributionPointContentClassification { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub enum SccmDistributionPointContentConfidence { + Medium, High, } @@ -153,6 +154,8 @@ pub struct SccmDistributionPointContentAnalysis { pub workflow: SccmDistributionPointWorkflow, pub profile: SccmDistributionPointProfile, pub transactions: Vec, + pub coverage_gaps: Vec, + pub artifact_requests: Vec, pub cross_side_correlation_performed: bool, } @@ -313,6 +316,8 @@ pub fn analyze_distribution_point_content_from_server_intake( let expected_site_code = selected_content_profile_site_code(intake.topology.site_handle.as_str()); let mut facts_by_key = BTreeMap::>::new(); + let mut semantic_gaps = + BTreeMap::<(String, String, String), SccmDistributionPointCoverageGap>::new(); for observation in &bounded.source_observations { let Some(evidence) = evidence_by_entry_id.get(observation.evidence.entry_id.as_str()) @@ -328,19 +333,70 @@ pub fn analyze_distribution_point_content_from_server_intake( evidence, expected_site_code, ) else { + note_semantic_gap( + &mut semantic_gaps, + artifacts_by_id + .get(observation.artifact_id.as_str()) + .copied(), + ); continue; }; facts_by_key.entry(fact.key.clone()).or_default().push(fact); } - let mut transactions = facts_by_key - .into_iter() - .filter_map(|(key, facts)| { + let mut transactions = Vec::new(); + for (key, facts) in facts_by_key { + let missing_roles = missing_healthy_phase_roles(&facts); + let gap_facts = facts.clone(); + if let Some(transaction) = reduce_healthy_transaction(DistributionPointTransactionEnvelope { key, facts }) - }) - .collect::>(); + { + transactions.push(transaction); + continue; + } + + let mut matched_missing_role = false; + for fact in &gap_facts { + let artifact = artifacts_by_id + .get(fact.reference.artifact_id.as_str()) + .copied(); + if artifact.is_some_and(|artifact| missing_roles.contains(&artifact.producer_role)) { + note_semantic_gap(&mut semantic_gaps, artifact); + matched_missing_role = true; + } + } + if !matched_missing_role { + for fact in &gap_facts { + note_semantic_gap( + &mut semantic_gaps, + artifacts_by_id + .get(fact.reference.artifact_id.as_str()) + .copied(), + ); + } + } + } transactions.sort_by(|left, right| left.transaction_id.cmp(&right.transaction_id)); + let mut coverage_gaps = bounded.coverage_gaps; + coverage_gaps.extend(semantic_gaps.into_values().map(|mut gap| { + gap.artifact_ids.sort(); + gap.artifact_ids.dedup(); + gap + })); + coverage_gaps.sort_by(|left, right| { + coverage_gap_sort_key(left) + .cmp(&coverage_gap_sort_key(right)) + .then_with(|| left.reason.cmp(&right.reason)) + }); + coverage_gaps.dedup(); + let artifact_requests = artifact_requests(&coverage_gaps); + if !coverage_gaps.is_empty() { + for transaction in &mut transactions { + transaction.confidence = SccmDistributionPointContentConfidence::Medium; + } + } + Ok(SccmDistributionPointContentAnalysis { schema_version: SCCM_DISTRIBUTION_POINT_CONTENT_ANALYSIS_SCHEMA_VERSION, workflow: SccmDistributionPointWorkflow::DistributionPointContent, @@ -350,10 +406,78 @@ pub fn analyze_distribution_point_content_from_server_intake( stability: "experimental".to_owned(), }, transactions, + coverage_gaps, + artifact_requests, cross_side_correlation_performed: false, }) } +fn note_semantic_gap( + gaps: &mut BTreeMap<(String, String, String), SccmDistributionPointCoverageGap>, + artifact: Option<&SccmServerArtifactAssessment>, +) { + let Some(artifact) = artifact else { + return; + }; + let key = ( + artifact.source_id.clone(), + role_sort_key(&artifact.producer_role).to_owned(), + artifact + .workflow_subject_role + .as_ref() + .map(role_sort_key) + .unwrap_or_default() + .to_owned(), + ); + gaps.entry(key) + .and_modify(|gap| gap.artifact_ids.push(artifact.artifact_id.clone())) + .or_insert_with(|| SccmDistributionPointCoverageGap { + source_id: artifact.source_id.clone(), + producer_role: Some(artifact.producer_role.clone()), + workflow_subject_role: artifact.workflow_subject_role.clone(), + state: Some(SccmCoverageState::Captured), + artifact_ids: vec![artifact.artifact_id.clone()], + reason: "Captured Distribution Point evidence did not match the selected content profile or complete a supported transaction." + .to_owned(), + }); +} + +fn missing_healthy_phase_roles(facts: &[DistributionPointFact]) -> Vec { + let expected = [ + ( + SccmDistributionPointContentPhase::ReceiveContent, + SccmRole::SiteServer, + ), + ( + SccmDistributionPointContentPhase::Distribute, + SccmRole::SiteServer, + ), + ( + SccmDistributionPointContentPhase::Transfer, + SccmRole::SiteServer, + ), + ( + SccmDistributionPointContentPhase::Validate, + SccmRole::DistributionPoint, + ), + ( + SccmDistributionPointContentPhase::MakeAvailable, + SccmRole::DistributionPoint, + ), + ( + SccmDistributionPointContentPhase::ServeOrReport, + SccmRole::DistributionPoint, + ), + ]; + let mut roles = Vec::new(); + for (phase, role) in expected { + if !facts.iter().any(|fact| fact.phase == phase) && !roles.contains(&role) { + roles.push(role); + } + } + roles +} + fn parse_healthy_distribution_point_fact( observation: &SccmDistributionPointSourceObservation, artifact: &SccmServerArtifactAssessment, diff --git a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs index a546b1b88..9b63700d5 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs @@ -2,9 +2,10 @@ use std::path::{Path, PathBuf}; use cmtraceopen_parser::sccm::server::windows::{ analyze_distribution_point, analyze_distribution_point_content_from_server_intake, - assess_server_intake, SccmServerArtifactPayload, SccmServerIntakeAssessment, - SccmServerIntakeError, SCCM_DISTRIBUTION_POINT_ANALYSIS_SCHEMA_VERSION, - SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_ID, SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_VERSION, + assess_server_intake, SccmDistributionPointContentConfidence, SccmServerArtifactPayload, + SccmServerIntakeAssessment, SccmServerIntakeError, + SCCM_DISTRIBUTION_POINT_ANALYSIS_SCHEMA_VERSION, SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_ID, + SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_VERSION, }; use cmtraceopen_parser::sccm::{SccmCoverageState, SccmRole, SccmRotation, SccmTimeOrderingState}; use serde_json::{json, Value}; @@ -48,9 +49,16 @@ fn assess_manifest( fn assess_complete_manifest_after( mutate: impl FnOnce(&mut Value), +) -> Result { + assess_complete_manifest_and_payloads_after(|manifest, _| mutate(manifest)) +} + +fn assess_complete_manifest_and_payloads_after( + mutate: impl FnOnce(&mut Value, &mut Vec), ) -> Result { let (mut manifest, payloads) = load_manifest_and_payloads("complete-multi-role"); - mutate(&mut manifest); + let mut payloads = payloads; + mutate(&mut manifest, &mut payloads); assess_manifest(&manifest, &payloads) } @@ -135,8 +143,15 @@ fn load_distribution_point_assessment_after( "encoding": artifact["encoding"], "collectionLimit": artifact["collectionLimit"], "collectedUtc": artifact["collectedUtc"], - "relativePath": canonical_distribution_point_relative_path(artifact), - "bytesCopied": artifact["bytesCopied"], + "relativePath": if matches!( + artifact["captureState"].as_str(), + Some("captured" | "capped" | "parseFailed") + ) { + json!(canonical_distribution_point_relative_path(artifact)) + } else { + Value::Null + }, + "bytesCopied": artifact["bytesCopied"].as_u64().unwrap_or(0), })) .collect::>(), }); @@ -165,6 +180,64 @@ fn canonical_distribution_point_relative_path(artifact: &Value) -> String { ) } +fn dp_payload_mut(payloads: &mut [SccmServerArtifactPayload]) -> &mut SccmServerArtifactPayload { + payloads + .iter_mut() + .find(|payload| payload.manifest_artifact_id == "dp-dist-current") + .expect("fixture contains the DP payload") +} + +fn set_dp_nonphysical_coverage( + manifest: &mut Value, + payloads: &mut Vec, + state: &str, +) { + let artifact = dp_manifest_artifact_mut(manifest); + artifact["captureState"] = json!(state); + artifact["relativePath"] = Value::Null; + artifact["bytesCopied"] = json!(0); + artifact["encoding"] = Value::Null; + artifact["collectionLimit"] = Value::Null; + artifact["collectionDetail"] = Value::Null; + artifact["skipReason"] = Value::Null; + artifact["unsupportedReason"] = Value::Null; + match state { + "accessDenied" => artifact["collectionDetail"] = json!("synthetic permission denial"), + "skipped" => artifact["skipReason"] = json!("optional supplemental source not requested"), + "unsupported" => { + artifact["unsupportedReason"] = json!("no approved server source contract") + } + _ => {} + } + payloads.retain(|payload| payload.manifest_artifact_id != "dp-dist-current"); +} + +fn dp_coverage_assessment(case: &str) -> SccmServerIntakeAssessment { + assess_complete_manifest_and_payloads_after(|manifest, payloads| match case { + "absent" | "accessDenied" | "skipped" | "unsupported" => { + set_dp_nonphysical_coverage(manifest, payloads, case); + } + "capped" => { + let payload = dp_payload_mut(payloads); + payload.bytes.truncate(64); + let artifact = dp_manifest_artifact_mut(manifest); + artifact["captureState"] = json!("capped"); + artifact["bytesCopied"] = json!(64); + artifact["collectionLimit"] = json!({"byteLimit": 64, "limitApplied": true}); + artifact["truncated"] = json!(true); + artifact["fragmentComplete"] = json!(false); + } + "malformed" => { + let payload = dp_payload_mut(payloads); + payload.bytes = b"not a complete CCM logical record".to_vec(); + let bytes_copied = payload.bytes.len() as u64; + dp_manifest_artifact_mut(manifest)["bytesCopied"] = json!(bytes_copied); + } + _ => panic!("declared DP coverage case"), + }) + .unwrap_or_else(|error| panic!("{case} DP coverage is sealed: {error}")) +} + fn dp_manifest_artifact_mut(manifest: &mut Value) -> &mut Value { manifest["artifacts"] .as_array_mut() @@ -409,6 +482,160 @@ fn healthy_package_requires_profile_site_token_to_match_sealed_topology() { ); } +#[test] +fn semantic_analysis_preserves_conservative_source_coverage_and_bounded_requests() { + for (case, expected_state) in [ + ("absent", SccmCoverageState::Absent), + ("accessDenied", SccmCoverageState::AccessDenied), + ("capped", SccmCoverageState::Capped), + ("skipped", SccmCoverageState::Skipped), + ("unsupported", SccmCoverageState::Unsupported), + ("malformed", SccmCoverageState::ParseFailed), + ] { + let assessment = dp_coverage_assessment(case); + let analysis = analyze_distribution_point_content_from_server_intake(&assessment) + .unwrap_or_else(|error| panic!("{case} coverage remains analyzable: {error}")); + + assert!(analysis.transactions.is_empty(), "{case}"); + assert_eq!(analysis.coverage_gaps.len(), 1, "{case}"); + assert_eq!( + analysis.coverage_gaps[0].state, + Some(expected_state), + "{case}" + ); + assert_eq!( + analysis + .artifact_requests + .iter() + .map(|request| (request.logical_id.as_str(), &request.role)) + .collect::>(), + vec![("distmgr", &SccmRole::SiteServer)], + "{case}" + ); + + let repeated = + analyze_distribution_point_content_from_server_intake(&dp_coverage_assessment(case)) + .expect("repeated coverage remains analyzable"); + assert_eq!( + serde_json::to_value(&analysis).expect("analysis serializes"), + serde_json::to_value(repeated).expect("repeated analysis serializes"), + "{case} output is deterministic" + ); + } +} + +#[test] +fn incomplete_and_unknown_profile_evidence_remain_explicit_semantic_gaps() { + let incomplete = + load_distribution_point_assessment_after("healthy-package", |manifest, payloads| { + let provider = payloads + .iter_mut() + .find(|payload| payload.manifest_artifact_id == "dp-healthy-03-provider") + .expect("fixture contains provider payload"); + let content = + std::str::from_utf8(&provider.bytes).expect("synthetic provider evidence is UTF-8"); + let retained = content + .lines() + .filter(|line| !line.contains("Phase=serveOrReport")) + .collect::>() + .join("\n") + + "\n"; + provider.bytes = retained.into_bytes(); + manifest["artifacts"] + .as_array_mut() + .expect("artifacts are an array") + .iter_mut() + .find(|artifact| artifact["artifactId"] == "dp-healthy-03-provider") + .expect("fixture contains provider artifact")["bytesCopied"] = + json!(provider.bytes.len() as u64); + }); + let incomplete_analysis = analyze_distribution_point_content_from_server_intake(&incomplete) + .expect("incomplete semantic evidence remains analyzable"); + assert!(incomplete_analysis.transactions.is_empty()); + assert_eq!(incomplete_analysis.coverage_gaps.len(), 1); + assert_eq!( + incomplete_analysis.coverage_gaps[0].state, + Some(SccmCoverageState::Captured) + ); + assert_eq!( + incomplete_analysis.artifact_requests[0].logical_id, + "smsDpProv" + ); + + let unknown_profile = + load_distribution_point_assessment_after("healthy-package", |_, payloads| { + for payload in payloads { + let content = + std::str::from_utf8(&payload.bytes).expect("synthetic DP evidence is UTF-8"); + payload.bytes = content + .replace( + "ProfileId=dp-server-5.00.test-v1", + "ProfileId=dp-server-5.00.test-v2", + ) + .into_bytes(); + } + }); + let unknown_analysis = analyze_distribution_point_content_from_server_intake(&unknown_profile) + .expect("unknown semantic profile remains analyzable"); + assert!(unknown_analysis.transactions.is_empty()); + assert_eq!(unknown_analysis.coverage_gaps.len(), 2); + assert!(unknown_analysis + .coverage_gaps + .iter() + .all(|gap| gap.state == Some(SccmCoverageState::Captured))); + assert_eq!( + unknown_analysis + .artifact_requests + .iter() + .map(|request| request.logical_id.as_str()) + .collect::>(), + vec!["distmgr", "smsDpProv"] + ); +} + +#[test] +fn healthy_transaction_cannot_hide_a_sealed_coverage_gap_or_keep_high_confidence() { + let assessment = load_distribution_point_assessment_after("healthy-package", |manifest, _| { + manifest["artifacts"] + .as_array_mut() + .expect("artifacts are an array") + .push(json!({ + "artifactId": "dp-distribution-absent-candidate", + "sourceId": "server-dp-distribution", + "producerRole": "siteServer", + "producerHostHandle": "safe:server:lab-pri-01", + "workflowSubjectRole": "distributionPoint", + "workflowSubjectHandle": "safe:dp:lab-dp-01", + "sourceKind": "ccmLog", + "sourceVersion": "5.00.TEST.0001", + "originalBasename": "distmgr.log", + "pathFingerprint": "synthetic:path:dp-default", + "rotation": {"kind": "current", "lineageId": "dp-distribution-default"}, + "captureState": "absent", + "encoding": null, + "collectionLimit": null, + "collectedUtc": "2026-07-30T12:20:00Z", + "relativePath": null, + "bytesCopied": 0 + })); + }); + + let analysis = analyze_distribution_point_content_from_server_intake(&assessment) + .expect("mixed healthy and missing coverage remains analyzable"); + + assert_eq!(analysis.transactions.len(), 1); + assert_eq!(analysis.coverage_gaps.len(), 1); + assert_eq!( + analysis.coverage_gaps[0].state, + Some(SccmCoverageState::Absent) + ); + assert_eq!(analysis.artifact_requests[0].logical_id, "distmgr"); + assert_eq!( + analysis.transactions[0].confidence, + SccmDistributionPointContentConfidence::Medium + ); +} + #[test] fn sealed_intake_without_dp_source_version_reaches_profile_eligibility_guard() { let assessment = assess_complete_manifest_after(|manifest| { From 95765d77ca49cf98cb8db918491bf10d6458353c Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 16:32:52 -0400 Subject: [PATCH 6/9] fix(sccm): frame complete DP transaction identity --- .../sccm/server/windows/distribution_point.rs | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs index 23b3bb757..a79ff18cd 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs @@ -123,6 +123,7 @@ pub struct SccmDistributionPointContentKey { pub site_code: String, pub distribution_point_handle: String, pub extraction_profile_id: String, + pub extraction_profile_version: u32, } #[derive(Debug, Clone, PartialEq, Serialize)] @@ -624,11 +625,9 @@ fn reduce_healthy_transaction( site_code: envelope.key.site_code, distribution_point_handle: envelope.key.distribution_point_handle, extraction_profile_id: SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_ID.to_owned(), + extraction_profile_version: SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_VERSION, }; - let transaction_id = format!( - "dp:{}:{}:v{}:{}", - key.package_id, key.content_id, key.content_version, key.distribution_point_handle - ); + let transaction_id = distribution_point_transaction_id(&key); let evidence = envelope .facts .iter() @@ -657,6 +656,19 @@ fn reduce_healthy_transaction( }) } +fn distribution_point_transaction_id(key: &SccmDistributionPointContentKey) -> String { + format!( + "dp:site={}:package={}:content={}:content-version={}:dp={}:profile={}:profile-version={}", + key.site_code, + key.package_id, + key.content_id, + key.content_version, + key.distribution_point_handle, + key.extraction_profile_id, + key.extraction_profile_version, + ) +} + fn exact_message_token<'a>(message: &'a str, label: &str) -> Option<&'a str> { let prefix = format!("{label}="); let mut values = message @@ -1109,3 +1121,56 @@ fn role_sort_key(role: &SccmRole) -> &str { SccmRole::Unknown(value) => value, } } + +#[cfg(test)] +mod content_identity_tests { + use super::*; + + fn key( + site_code: &str, + profile_id: &str, + profile_version: u32, + ) -> SccmDistributionPointContentKey { + SccmDistributionPointContentKey { + package_id: "LAB00001".to_owned(), + content_id: "content-alpha".to_owned(), + content_version: 1, + site_code: site_code.to_owned(), + distribution_point_handle: "safe:dp:lab-dp-01".to_owned(), + extraction_profile_id: profile_id.to_owned(), + extraction_profile_version: profile_version, + } + } + + #[test] + fn transaction_identity_includes_site_topology_and_profile_version_deterministically() { + let lab = key("LAB", "dp-server-5.00.test-v1", 1); + let abc = key("ABC", "dp-server-5.00.test-v1", 1); + let profile_v2 = key("LAB", "dp-server-5.00.test-v2", 2); + + assert_eq!( + distribution_point_transaction_id(&lab), + "dp:site=LAB:package=LAB00001:content=content-alpha:content-version=1:dp=safe:dp:lab-dp-01:profile=dp-server-5.00.test-v1:profile-version=1" + ); + assert_ne!( + distribution_point_transaction_id(&lab), + distribution_point_transaction_id(&abc) + ); + assert_ne!( + distribution_point_transaction_id(&lab), + distribution_point_transaction_id(&profile_v2) + ); + + let mut forward = [&lab, &abc, &profile_v2] + .into_iter() + .map(distribution_point_transaction_id) + .collect::>(); + let mut reversed = [&profile_v2, &abc, &lab] + .into_iter() + .map(distribution_point_transaction_id) + .collect::>(); + forward.sort(); + reversed.sort(); + assert_eq!(forward, reversed); + } +} From 68ea767013c00d72e94c43dadc50733e279e74c7 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 16:35:02 -0400 Subject: [PATCH 7/9] test(sccm): pin six-record DP site mismatch --- .../tests/sccm_server_distribution_point.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs index 9b63700d5..78a888f5e 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs @@ -460,6 +460,7 @@ fn healthy_package_reduces_a_sealed_role_local_transaction() { #[test] fn healthy_package_requires_profile_site_token_to_match_sealed_topology() { + let mut mutated_records = 0usize; let assessment = load_distribution_point_assessment_after("healthy-package", |_, payloads| { for payload in payloads { let content = @@ -468,9 +469,15 @@ fn healthy_package_requires_profile_site_token_to_match_sealed_topology() { content.contains("SiteCode=LAB"), "every healthy source must carry the profile site token" ); + mutated_records = + mutated_records.saturating_add(content.matches("SiteCode=LAB").count()); payload.bytes = content.replace("SiteCode=LAB", "SiteCode=ABC").into_bytes(); } }); + assert_eq!( + mutated_records, 6, + "the full healthy phase chain is mutated" + ); assert_eq!(assessment.topology.site_handle, "synthetic:site:lab"); let analysis = analyze_distribution_point_content_from_server_intake(&assessment) From 1ccfa109cea8440dab45d1ca7226083193496378 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 16:45:41 -0400 Subject: [PATCH 8/9] fix(sccm): retain DP topology identity --- .../sccm/server/windows/distribution_point.rs | 44 +++++++++++++++++-- .../tests/sccm_server_distribution_point.rs | 4 ++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs index a79ff18cd..3c6c8cf3d 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs @@ -120,6 +120,7 @@ pub struct SccmDistributionPointContentKey { pub package_id: String, pub content_id: String, pub content_version: u32, + pub topology_site_handle: String, pub site_code: String, pub distribution_point_handle: String, pub extraction_profile_id: String, @@ -191,6 +192,7 @@ struct DistributionPointFact { /// submit source facts directly. #[derive(Debug)] struct DistributionPointTransactionEnvelope { + topology_site_handle: String, key: DistributionPointFactKey, facts: Vec, } @@ -350,7 +352,11 @@ pub fn analyze_distribution_point_content_from_server_intake( let missing_roles = missing_healthy_phase_roles(&facts); let gap_facts = facts.clone(); if let Some(transaction) = - reduce_healthy_transaction(DistributionPointTransactionEnvelope { key, facts }) + reduce_healthy_transaction(DistributionPointTransactionEnvelope { + topology_site_handle: intake.topology.site_handle.clone(), + key, + facts, + }) { transactions.push(transaction); continue; @@ -622,6 +628,7 @@ fn reduce_healthy_transaction( package_id: envelope.key.package_id, content_id: envelope.key.content_id, content_version: envelope.key.content_version, + topology_site_handle: envelope.topology_site_handle, site_code: envelope.key.site_code, distribution_point_handle: envelope.key.distribution_point_handle, extraction_profile_id: SCCM_DISTRIBUTION_POINT_CONTENT_PROFILE_ID.to_owned(), @@ -658,7 +665,8 @@ fn reduce_healthy_transaction( fn distribution_point_transaction_id(key: &SccmDistributionPointContentKey) -> String { format!( - "dp:site={}:package={}:content={}:content-version={}:dp={}:profile={}:profile-version={}", + "dp:topology-site={}:site={}:package={}:content={}:content-version={}:dp={}:profile={}:profile-version={}", + key.topology_site_handle, key.site_code, key.package_id, key.content_id, @@ -1135,6 +1143,7 @@ mod content_identity_tests { package_id: "LAB00001".to_owned(), content_id: "content-alpha".to_owned(), content_version: 1, + topology_site_handle: "synthetic:site:lab".to_owned(), site_code: site_code.to_owned(), distribution_point_handle: "safe:dp:lab-dp-01".to_owned(), extraction_profile_id: profile_id.to_owned(), @@ -1150,7 +1159,7 @@ mod content_identity_tests { assert_eq!( distribution_point_transaction_id(&lab), - "dp:site=LAB:package=LAB00001:content=content-alpha:content-version=1:dp=safe:dp:lab-dp-01:profile=dp-server-5.00.test-v1:profile-version=1" + "dp:topology-site=synthetic:site:lab:site=LAB:package=LAB00001:content=content-alpha:content-version=1:dp=safe:dp:lab-dp-01:profile=dp-server-5.00.test-v1:profile-version=1" ); assert_ne!( distribution_point_transaction_id(&lab), @@ -1173,4 +1182,33 @@ mod content_identity_tests { reversed.sort(); assert_eq!(forward, reversed); } + + #[test] + fn transaction_identity_distinguishes_canonical_topology_with_identical_message_keys() { + let lab = SccmDistributionPointContentKey { + package_id: "LAB00001".to_owned(), + content_id: "content-alpha".to_owned(), + content_version: 1, + topology_site_handle: "synthetic:site:lab".to_owned(), + site_code: "LAB".to_owned(), + distribution_point_handle: "safe:dp:lab-dp-01".to_owned(), + extraction_profile_id: "dp-server-5.00.test-v1".to_owned(), + extraction_profile_version: 1, + }; + let peer = SccmDistributionPointContentKey { + topology_site_handle: "synthetic:site:lab-peer".to_owned(), + ..lab.clone() + }; + + let lab_id = distribution_point_transaction_id(&lab); + let peer_id = distribution_point_transaction_id(&peer); + assert_ne!(lab, peer); + assert_ne!(lab_id, peer_id); + + let mut forward = [lab_id.as_str(), peer_id.as_str()]; + let mut reversed = [peer_id.as_str(), lab_id.as_str()]; + forward.sort(); + reversed.sort(); + assert_eq!(forward, reversed); + } } diff --git a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs index 78a888f5e..bb670cdbc 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs @@ -448,6 +448,10 @@ fn healthy_package_reduces_a_sealed_role_local_transaction() { assert_eq!(analysis.transactions[0].key.package_id, "LAB00001"); assert_eq!(analysis.transactions[0].key.content_id, "content-alpha"); assert_eq!(analysis.transactions[0].key.content_version, 1); + assert_eq!( + analysis.transactions[0].key.topology_site_handle, + assessment.topology.site_handle + ); assert_eq!( analysis.transactions[0] .key From 42a7e69c5540e8f1e2206801d7b52d7c43f4e5c0 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Aug 2026 17:05:24 -0400 Subject: [PATCH 9/9] fix(sccm): request complete DP source set --- .../sccm/server/windows/distribution_point.rs | 94 +++++++++++++- .../tests/sccm_server_distribution_point.rs | 120 ++++++++++-------- 2 files changed, 161 insertions(+), 53 deletions(-) diff --git a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs index 3c6c8cf3d..72b1fd5e4 100644 --- a/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs +++ b/crates/cmtraceopen-parser/src/sccm/server/windows/distribution_point.rs @@ -1023,13 +1023,17 @@ fn artifact_requests(gaps: &[SccmDistributionPointCoverageGap]) -> Vec Vec &str { } } +#[cfg(test)] +mod artifact_request_tests { + use super::*; + + fn gap(producer_role: Option) -> SccmDistributionPointCoverageGap { + SccmDistributionPointCoverageGap { + source_id: SCCM_DISTRIBUTION_POINT_SOURCE_ID.to_owned(), + producer_role, + workflow_subject_role: Some(SccmRole::DistributionPoint), + state: Some(SccmCoverageState::Absent), + artifact_ids: Vec::new(), + reason: "controlled coverage gap".to_owned(), + } + } + + fn contracts(requests: &[SccmArtifactRequest]) -> Vec<(&str, SccmRole, &str)> { + requests + .iter() + .map(|request| { + ( + request.logical_id.as_str(), + request.role.clone(), + request.reason.as_str(), + ) + }) + .collect() + } + + fn expected_site_requests() -> Vec<(&'static str, SccmRole, &'static str)> { + vec![ + ( + "distmgr", + SccmRole::SiteServer, + "Collect the complete distmgr.log file.", + ), + ( + "pkgXferMgr", + SccmRole::SiteServer, + "Collect the complete PkgXferMgr.log file.", + ), + ] + } + + fn expected_all_requests() -> Vec<(&'static str, SccmRole, &'static str)> { + let mut requests = expected_site_requests(); + requests.push(( + "smsDpProv", + SccmRole::DistributionPoint, + "Collect the complete SMSDPProv.log file.", + )); + requests + } + + fn expected_dp_requests() -> Vec<(&'static str, SccmRole, &'static str)> { + vec![( + "smsDpProv", + SccmRole::DistributionPoint, + "Collect the complete SMSDPProv.log file.", + )] + } + + #[test] + fn artifact_requests_cover_required_sources_by_gap_scope_deterministically() { + let site_gap = gap(Some(SccmRole::SiteServer)); + let site_requests = artifact_requests(&[site_gap.clone(), site_gap.clone()]); + assert_eq!(contracts(&site_requests), expected_site_requests()); + + let unscoped_gap = gap(None); + let unscoped_requests = artifact_requests(&[unscoped_gap.clone(), unscoped_gap.clone()]); + assert_eq!(contracts(&unscoped_requests), expected_all_requests()); + + let dp_gap = gap(Some(SccmRole::DistributionPoint)); + let dp_requests = artifact_requests(&[dp_gap.clone(), dp_gap.clone()]); + assert_eq!(contracts(&dp_requests), expected_dp_requests()); + + let forward = artifact_requests(&[site_gap.clone(), unscoped_gap.clone(), dp_gap.clone()]); + let reversed = artifact_requests(&[dp_gap, unscoped_gap, site_gap]); + assert_eq!(forward, reversed); + } +} + #[cfg(test)] mod content_identity_tests { use super::*; diff --git a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs index bb670cdbc..b99cd9cb5 100644 --- a/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs +++ b/crates/cmtraceopen-parser/tests/sccm_server_distribution_point.rs @@ -7,9 +7,57 @@ use cmtraceopen_parser::sccm::server::windows::{ SCCM_DISTRIBUTION_POINT_ANALYSIS_SCHEMA_VERSION, SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_ID, SCCM_DISTRIBUTION_POINT_INTAKE_PROFILE_VERSION, }; -use cmtraceopen_parser::sccm::{SccmCoverageState, SccmRole, SccmRotation, SccmTimeOrderingState}; +use cmtraceopen_parser::sccm::{ + SccmArtifactRequest, SccmCoverageState, SccmRole, SccmRotation, SccmTimeOrderingState, +}; use serde_json::{json, Value}; +fn artifact_request_contracts(requests: &[SccmArtifactRequest]) -> Vec<(&str, SccmRole, &str)> { + requests + .iter() + .map(|request| { + ( + request.logical_id.as_str(), + request.role.clone(), + request.reason.as_str(), + ) + }) + .collect() +} + +fn expected_site_server_artifact_requests() -> Vec<(&'static str, SccmRole, &'static str)> { + vec![ + ( + "distmgr", + SccmRole::SiteServer, + "Collect the complete distmgr.log file.", + ), + ( + "pkgXferMgr", + SccmRole::SiteServer, + "Collect the complete PkgXferMgr.log file.", + ), + ] +} + +fn expected_all_dp_profile_artifact_requests() -> Vec<(&'static str, SccmRole, &'static str)> { + let mut requests = expected_site_server_artifact_requests(); + requests.push(( + "smsDpProv", + SccmRole::DistributionPoint, + "Collect the complete SMSDPProv.log file.", + )); + requests +} + +fn expected_dp_artifact_requests() -> Vec<(&'static str, SccmRole, &'static str)> { + vec![( + "smsDpProv", + SccmRole::DistributionPoint, + "Collect the complete SMSDPProv.log file.", + )] +} + fn intake_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/sccm/server/intake") } @@ -314,14 +362,9 @@ fn assert_dp_sealed_guard_rejection( "Captured Distribution Point evidence is incomplete or outside the supported intake profile.", "{context}" ); - assert_eq!(analysis.artifact_requests.len(), 1, "{context}"); assert_eq!( - analysis.artifact_requests[0].logical_id, "distmgr", - "{context}" - ); - assert_eq!( - analysis.artifact_requests[0].role, - SccmRole::SiteServer, + artifact_request_contracts(&analysis.artifact_requests), + expected_site_server_artifact_requests(), "{context}" ); assert!(!analysis.cross_side_correlation_performed, "{context}"); @@ -352,23 +395,9 @@ fn assert_dp_intake_authority_invalid( gap.reason, "Canonical server intake authority could not be verified.", "{context}" ); - assert_eq!(analysis.artifact_requests.len(), 2, "{context}"); - assert_eq!( - analysis.artifact_requests[0].logical_id, "distmgr", - "{context}" - ); - assert_eq!( - analysis.artifact_requests[0].role, - SccmRole::SiteServer, - "{context}" - ); assert_eq!( - analysis.artifact_requests[1].logical_id, "smsDpProv", - "{context}" - ); - assert_eq!( - analysis.artifact_requests[1].role, - SccmRole::DistributionPoint, + artifact_request_contracts(&analysis.artifact_requests), + expected_all_dp_profile_artifact_requests(), "{context}" ); assert!(!analysis.cross_side_correlation_performed, "{context}"); @@ -515,12 +544,8 @@ fn semantic_analysis_preserves_conservative_source_coverage_and_bounded_requests "{case}" ); assert_eq!( - analysis - .artifact_requests - .iter() - .map(|request| (request.logical_id.as_str(), &request.role)) - .collect::>(), - vec![("distmgr", &SccmRole::SiteServer)], + artifact_request_contracts(&analysis.artifact_requests), + expected_site_server_artifact_requests(), "{case}" ); @@ -569,8 +594,8 @@ fn incomplete_and_unknown_profile_evidence_remain_explicit_semantic_gaps() { Some(SccmCoverageState::Captured) ); assert_eq!( - incomplete_analysis.artifact_requests[0].logical_id, - "smsDpProv" + artifact_request_contracts(&incomplete_analysis.artifact_requests), + expected_dp_artifact_requests() ); let unknown_profile = @@ -595,12 +620,8 @@ fn incomplete_and_unknown_profile_evidence_remain_explicit_semantic_gaps() { .iter() .all(|gap| gap.state == Some(SccmCoverageState::Captured))); assert_eq!( - unknown_analysis - .artifact_requests - .iter() - .map(|request| request.logical_id.as_str()) - .collect::>(), - vec!["distmgr", "smsDpProv"] + artifact_request_contracts(&unknown_analysis.artifact_requests), + expected_all_dp_profile_artifact_requests() ); } @@ -640,7 +661,10 @@ fn healthy_transaction_cannot_hide_a_sealed_coverage_gap_or_keep_high_confidence analysis.coverage_gaps[0].state, Some(SccmCoverageState::Absent) ); - assert_eq!(analysis.artifact_requests[0].logical_id, "distmgr"); + assert_eq!( + artifact_request_contracts(&analysis.artifact_requests), + expected_site_server_artifact_requests() + ); assert_eq!( analysis.transactions[0].confidence, SccmDistributionPointContentConfidence::Medium @@ -881,9 +905,10 @@ fn absent_dp_candidate_is_coverage_not_a_role_diagnosis() { analysis.coverage_gaps[0].reason, "Distribution Point source coverage is absent; recollect the declared source without changing its state." ); - assert_eq!(analysis.artifact_requests.len(), 1); - assert_eq!(analysis.artifact_requests[0].logical_id, "distmgr"); - assert_eq!(analysis.artifact_requests[0].role, SccmRole::SiteServer); + assert_eq!( + artifact_request_contracts(&analysis.artifact_requests), + expected_site_server_artifact_requests() + ); serde_json::to_value(&analysis).expect("coverage-only analysis serializes"); } @@ -897,15 +922,8 @@ fn no_declared_dp_source_requests_both_bounded_sides_without_correlation() { assert_eq!(analysis.coverage_gaps.len(), 1); assert_eq!(analysis.coverage_gaps[0].producer_role, None); assert_eq!( - analysis - .artifact_requests - .iter() - .map(|request| (request.logical_id.as_str(), &request.role)) - .collect::>(), - vec![ - ("distmgr", &SccmRole::SiteServer), - ("smsDpProv", &SccmRole::DistributionPoint), - ] + artifact_request_contracts(&analysis.artifact_requests), + expected_all_dp_profile_artifact_requests() ); assert!(!analysis.cross_side_correlation_performed); }