Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub fn derive_findings(snapshot: &StoreAnalysis) -> Vec<IntuneFinding> {
push_license_failure(snapshot, &mut findings);
push_download_failure(snapshot, &mut findings);
push_registration_failure(snapshot, &mut findings);
push_installer_failure(snapshot, &mut findings);
push_provisioning_failure(snapshot, &mut findings);
push_uninstall_failure(snapshot, &mut findings);
push_no_interactive_user(snapshot, &mut findings);
Expand All @@ -40,6 +41,7 @@ pub fn derive_findings(snapshot: &StoreAnalysis) -> Vec<IntuneFinding> {
push_device_failure_without_intune_intent(snapshot, &mut findings);
push_ambiguous_display_name(snapshot, &mut findings);
push_unknown_event_version(snapshot, &mut findings);
push_event_level_mismatch(snapshot, &mut findings);
push_malformed_source(snapshot, &mut findings);
push_evidence_coverage_gap(snapshot, &mut findings);
push_install_completed(snapshot, &mut findings);
Expand Down Expand Up @@ -129,6 +131,32 @@ fn transactions_in_state(
.collect()
}

/// The weakest confidence among the affected transactions.
///
/// An outcome-asserting finding (a failure, a completion, a scheduling
/// condition) claims exactly what its transactions claim, so it may not claim
/// it more strongly than the reducer did: a transaction capped at `Low` for an
/// unknown dialect, a level mismatch, or a malformed contributor caps every
/// finding built on it (ADR-001). Rules that instead describe an evidence gap
/// or an attribution boundary keep their own deliberate confidence, because
/// their claim is about what is missing, not about an outcome.
fn weakest_confidence(transactions: &[&StoreTransaction]) -> IntuneFindingConfidence {
fn rank(confidence: &IntuneFindingConfidence) -> u8 {
match confidence {
IntuneFindingConfidence::Low => 0,
IntuneFindingConfidence::Medium => 1,
IntuneFindingConfidence::High => 2,
}
}
let mut weakest = IntuneFindingConfidence::High;
for transaction in transactions {
if rank(&transaction.confidence) < rank(&weakest) {
weakest = transaction.confidence.clone();
}
}
weakest
}

#[allow(clippy::too_many_arguments)]
fn push_finding(
findings: &mut Vec<IntuneFinding>,
Expand Down Expand Up @@ -179,7 +207,7 @@ fn push_license_failure(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFindi
findings,
"store-license-acquisition-failed",
IntuneFindingSeverity::Error,
IntuneFindingConfidence::High,
weakest_confidence(&affected),
"Store license or acquisition failed",
format!(
"The Store could not acquire a license or entitlement for {}.{}",
Expand All @@ -204,7 +232,7 @@ fn push_download_failure(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFind
findings,
"store-download-staging-failed",
IntuneFindingSeverity::Error,
IntuneFindingConfidence::High,
weakest_confidence(&affected),
"Package download or staging failed",
format!(
"The package payload for {} was never staged on disk, so no registration was attempted.{}",
Expand Down Expand Up @@ -232,7 +260,7 @@ fn push_registration_failure(snapshot: &StoreAnalysis, findings: &mut Vec<Intune
findings,
"store-registration-failed",
IntuneFindingSeverity::Error,
IntuneFindingConfidence::High,
weakest_confidence(&affected),
if win32 {
"Store-delivered installer reported failure"
} else {
Expand All @@ -252,6 +280,37 @@ fn push_registration_failure(snapshot: &StoreAnalysis, findings: &mut Vec<Intune
);
}

/// A Store-delivered Win32 package whose own installer reported failure.
///
/// Kept apart from the registration rule because the failing grammar is a
/// plain Windows installer: AppX dependency/framework remediation does not
/// apply, and suggesting it here would be the cross-family collapse this
/// module exists to prevent.
fn push_installer_failure(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFinding>) {
let affected = transactions_in_state(snapshot, StoreTransactionState::InstallerFailure);
if affected.is_empty() {
return;
}
push_finding(
findings,
"store-win32-installer-failed",
IntuneFindingSeverity::Error,
weakest_confidence(&affected),
"Store-delivered Win32 installer reported failure",
format!(
"The installer for {} ran and reported failure. This is the package's own Windows installer, not an AppX deployment stage.{}",
labels(&affected),
error_suffix(&affected)
),
&[
"Read the cited exit code against the installer's own documentation (MSI or setup engine), not against AppX deployment errors",
"Collect the installer's own log for the failing run",
],
evidence_of(&affected),
Vec::new(),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

fn push_provisioning_failure(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFinding>) {
let affected = transactions_in_state(snapshot, StoreTransactionState::ProvisioningFailure);
if affected.is_empty() {
Expand All @@ -261,7 +320,7 @@ fn push_provisioning_failure(snapshot: &StoreAnalysis, findings: &mut Vec<Intune
findings,
"store-provisioning-failed",
IntuneFindingSeverity::Error,
IntuneFindingConfidence::High,
weakest_confidence(&affected),
"Device provisioning of the package failed",
format!(
"{} failed while being provisioned for all users on the device. This is a machine-wide operation and does not imply any per-user registration failed.{}",
Expand All @@ -286,7 +345,7 @@ fn push_uninstall_failure(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFin
findings,
"store-uninstall-failed",
IntuneFindingSeverity::Error,
IntuneFindingConfidence::High,
weakest_confidence(&affected),
"Package removal failed",
format!(
"Removal of {} did not complete.{}",
Expand All @@ -310,7 +369,7 @@ fn push_no_interactive_user(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneF
findings,
"store-no-interactive-user",
IntuneFindingSeverity::Warning,
IntuneFindingConfidence::High,
weakest_confidence(&affected),
"A user-context package had no user to install for",
format!(
"{} requires a user context and no interactive user was signed in when it was evaluated. This is a scheduling condition, not an installation failure.",
Expand Down Expand Up @@ -499,13 +558,24 @@ fn push_ambiguous_display_name(snapshot: &StoreAnalysis, findings: &mut Vec<Intu

// ── Coverage rules ──────────────────────────────────────────────────────────

/// Canonical citation list: sorted and de-duplicated, so a record supplied
/// twice is one citation and neither the rendered count nor the citation
/// order depends on artifact permutation or duplication.
fn normalized_refs(refs: impl Iterator<Item = IntuneEvidenceRef>) -> Vec<IntuneEvidenceRef> {
let mut evidence: Vec<IntuneEvidenceRef> = refs.collect();
evidence.sort();
evidence.dedup();
evidence
}

fn push_unknown_event_version(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFinding>) {
let evidence = snapshot
.observations
.iter()
.filter(|observation| observation.unknown_version)
.map(|observation| observation.context.evidence_ref.clone())
.collect::<Vec<_>>();
let evidence = normalized_refs(
snapshot
.observations
.iter()
.filter(|observation| observation.unknown_version)
.map(|observation| observation.context.evidence_ref.clone()),
);
if evidence.is_empty() {
return;
}
Expand All @@ -525,6 +595,40 @@ fn push_unknown_event_version(snapshot: &StoreAnalysis, findings: &mut Vec<Intun
);
}

/// A *known* event whose level contradicts the outcome its event id states.
///
/// Deliberately a separate finding from `store-unknown-event-version`: there
/// the dialect is unrecognized and nothing was interpreted; here the dialect is
/// fully understood and the record contradicts itself. Both degrade the
/// touched transaction's confidence, but for different reasons and with
/// different remediations, so conflating them would hide which one happened.
fn push_event_level_mismatch(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFinding>) {
let evidence = normalized_refs(
snapshot
.observations
.iter()
.filter(|observation| observation.level_mismatch)
.map(|observation| observation.context.evidence_ref.clone()),
);
if evidence.is_empty() {
return;
}
push_finding(
findings,
"store-event-level-mismatch",
IntuneFindingSeverity::Info,
IntuneFindingConfidence::High,
"A known event's level contradicts its stated outcome",
format!(
"{} record(s) carry a recognized failure event id but were logged at Information level. The stated outcome was kept and the level was not promoted into evidence; any transaction they touch is reported at reduced confidence.",
evidence.len()
),
&["Compare the rendered event text with the event id's documented meaning; the export may have altered the level"],
evidence,
Vec::new(),
);
}

fn push_malformed_source(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFinding>) {
let gaps = snapshot
.coverage
Expand Down Expand Up @@ -593,7 +697,7 @@ fn push_install_completed(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneFin
findings,
"store-install-completed",
IntuneFindingSeverity::Info,
IntuneFindingConfidence::High,
weakest_confidence(&affected),
"A Store app install completed",
format!("{} completed installation.", labels(&affected)),
&[],
Expand All @@ -611,7 +715,7 @@ fn push_uninstall_completed(snapshot: &StoreAnalysis, findings: &mut Vec<IntuneF
findings,
"store-uninstall-completed",
IntuneFindingSeverity::Info,
IntuneFindingConfidence::High,
weakest_confidence(&affected),
"A Store app uninstall completed",
format!("{} was removed.", labels(&affected)),
&[],
Expand Down Expand Up @@ -647,6 +751,7 @@ mod tests {
has_intune_intent: true,
has_device_evidence: true,
unknown_version_observed: false,
level_mismatch_observed: false,
observations: vec!["appx:1".to_owned()],
evidence: vec![IntuneEvidenceRef {
evidence_id: "appx:1".to_owned(),
Expand Down Expand Up @@ -696,6 +801,87 @@ mod tests {
assert_eq!(ids.len(), count, "duplicate finding ids: {ids:?}");
}

/// ADR-001: a finding that asserts an outcome may not claim stronger
/// confidence than the evidence behind it. Every outcome-asserting rule
/// must inherit the weakest confidence among its affected transactions,
/// and must still report `High` when nothing was degraded.
#[test]
fn outcome_findings_inherit_the_weakest_affected_transaction_confidence() {
let cases = [
(
StoreTransactionState::LicenseFailure,
"store-license-acquisition-failed",
),
(
StoreTransactionState::DownloadFailure,
"store-download-staging-failed",
),
(
StoreTransactionState::RegistrationFailure,
"store-registration-failed",
),
(
StoreTransactionState::InstallerFailure,
"store-win32-installer-failed",
),
(
StoreTransactionState::ProvisioningFailure,
"store-provisioning-failed",
),
(
StoreTransactionState::UninstallFailure,
"store-uninstall-failed",
),
(
StoreTransactionState::NoInteractiveUser,
"store-no-interactive-user",
),
(
StoreTransactionState::InstallCompleted,
"store-install-completed",
),
(
StoreTransactionState::UninstallCompleted,
"store-uninstall-completed",
),
];
for (state, finding_id) in cases {
let mut degraded = transaction(state);
degraded.confidence = IntuneFindingConfidence::Low;
let snapshot = StoreAnalysis {
// One intact transaction and one degraded one: the weakest
// member decides, not the first or the strongest.
transactions: vec![transaction(state), degraded],
..StoreAnalysis::default()
};
let findings = derive_findings(&snapshot);
let finding = findings
.iter()
.find(|finding| finding.finding_id == finding_id)
.unwrap_or_else(|| panic!("{finding_id} must be emitted"));
assert_eq!(
finding.confidence,
IntuneFindingConfidence::Low,
"{finding_id} must not overstate degraded evidence"
);

let intact_snapshot = StoreAnalysis {
transactions: vec![transaction(state)],
..StoreAnalysis::default()
};
let intact = derive_findings(&intact_snapshot);
let finding = intact
.iter()
.find(|finding| finding.finding_id == finding_id)
.unwrap_or_else(|| panic!("{finding_id} must be emitted"));
assert_eq!(
finding.confidence,
IntuneFindingConfidence::High,
"{finding_id} keeps full confidence on intact evidence"
);
}
}

#[test]
fn an_os_failure_without_intune_intent_is_not_blamed_on_intune() {
let mut orphan = transaction(StoreTransactionState::RegistrationFailure);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,32 @@ pub struct StoreObservation {
pub app_id: Option<String>,
pub execution_context: StoreExecutionContext,
pub action: StoreDeploymentAction,
/// The source's own operation-correlation token — for a Windows event, the
/// ETW activity id. This is the only linkage that can tie a success record
/// to an earlier failure as one operation: record order alone proves
/// chronology, not linkage (ADR-003), so the reducer requires a shared
/// activity id before a later success may supersede a failure. Absent for
/// sources whose grammar carries no such token (IME text, supplied facts).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub activity_id: Option<String>,
/// Intune's typed assignment intent, present only when this observation is
/// a typed assignment. This is the authoritative statement of intent: the
/// reducer reads intent from here and never from caller-writable
/// `named_data` (ADR-001).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub typed_intent: Option<StoreAssignmentIntent>,
pub error: Option<IntuneErrorCode>,
/// True when the record came from a recognized provider but an event id or
/// schema version this build has no rule for.
pub unknown_version: bool,
/// True when a *known* event's level contradicts the outcome its event id
/// states (a failure id logged at Information level). Deliberately a
/// separate flag from [`Self::unknown_version`]: an unrecognized dialect
/// and a self-contradictory known record degrade confidence for distinct
/// reasons, and conflating them would hide which one happened. Serialized
/// unconditionally, exactly like [`Self::unknown_version`], so `false` is
/// a statement rather than an absence.
pub level_mismatch: bool,
#[serde(default)]
pub named_data: Vec<IntuneNamedValue>,
/// Verbatim record or rendered event text. Redacted by the export
Expand Down Expand Up @@ -467,6 +489,13 @@ pub struct StoreTransaction {
/// True when device-side OS evidence (event, inventory fact, installer) is present.
pub has_device_evidence: bool,
pub unknown_version_observed: bool,
/// True when any contributing observation carried
/// [`StoreObservation::level_mismatch`]. Kept beside
/// [`Self::unknown_version_observed`] with the same always-present shape:
/// the two degradations cap confidence identically but for distinct
/// reasons, and a consumer reading a `Low` transaction must be able to
/// tell which one happened without replaying the observations.
pub level_mismatch_observed: bool,
pub observations: Vec<String>,
pub evidence: Vec<IntuneEvidenceRef>,
/// The smallest artifact that would advance this diagnosis.
Expand Down
Loading