use crate::findings::{
ArtifactScope, Finding, FindingSummary, HygieneSummary, PackageHealth, RecommendedAction,
RootCauseGroup, SignalClass, ThreatCategory, Verdict, VerdictCalibrationNote,
RISK_THRESHOLD_APPROVAL, RISK_THRESHOLD_BLOCK,
};
pub(super) struct VerdictInputs<'a> {
pub(super) findings: &'a [Finding],
pub(super) root_cause_groups: &'a [RootCauseGroup],
pub(super) raw_root_cause_groups: &'a [RootCauseGroup],
pub(super) compound_reasons: &'a [crate::findings::VerdictReason],
pub(super) primary_summary: &'a FindingSummary,
pub(super) supporting_summary: &'a FindingSummary,
}
pub(super) const CONCLUSIVE_SINGLE_RULE_IDS: &[&str] = &[
"SKILL_MALICIOUS_PUBLISHER",
"SKILL_MACOS_BASE64_RCE",
"SKILL_FAKE_DEPENDENCY_DROPPER",
"SKILL_TELEGRAM_BOT_TOKEN_HARDCODED",
"SKILL_ECHO_WRAPPED_BASE64_EXEC",
"SKILL_HARDCODED_MESSAGING_EXFIL",
"SKILL_BASE64_PIPE_EXEC",
"SKILL_SSH_KEY_INJECTION",
"SKILL_MALICIOUS_DOMAIN",
"SKILL_REVERSE_SHELL_BASH",
"OFFICIAL_BACKDOOR_REMOTE_INSTRUCTION_HOST",
"SKILL_EXEC_CRON_INJECTION",
"OFFICIAL_APPROVAL_BYPASS_WITH_EXECUTION",
"SKILL_SUPPLY_CHAIN_TYPOSQUATTING",
"OFFICIAL_REMOTE_FETCH_EXEC_POLYGLOT",
"SKILL_CREDENTIAL_HARVESTING_ACTIVE",
"SKILL_REMOTE_EXEC_POWERSHELL",
"SKILL_REMOTE_EXEC_POWERSHELL_IEX",
"SKILL_PUMP_DUMP",
"SKILL_SMS_DATABASE_MULTI",
];
pub(super) struct VerdictPredicates {
pub(super) has_malicious_behavior: bool,
pub(super) has_compound_malicious: bool,
pub(super) has_primary_block: bool,
pub(super) has_supporting_block: bool,
pub(super) has_non_hygiene_signal: bool,
pub(super) calibration_weakened_non_hygiene: bool,
pub(super) has_actionable_non_package_root: bool,
pub(super) severe_hygiene_only: bool,
pub(super) has_conclusive_supporting_malicious: bool,
pub(super) has_conclusive_single_rule: bool,
pub(super) isolated_weak_package_root_signal: bool,
pub(super) has_non_hygiene_primary_block: bool,
pub(super) isolated_weak_signal_key: Option<(ArtifactScope, ThreatCategory, SignalClass)>,
pub(super) has_independent_malicious_corroboration: bool,
}
impl VerdictPredicates {
pub(super) fn compute(inputs: &VerdictInputs<'_>) -> Self {
let VerdictInputs {
findings,
root_cause_groups,
raw_root_cause_groups,
compound_reasons,
primary_summary,
supporting_summary,
} = inputs;
let has_malicious_behavior = root_cause_groups.iter().any(|group| {
group.signal_class == SignalClass::MaliciousBehavior
&& group.strongest_action == RecommendedAction::Block
});
let has_compound_malicious = compound_reasons
.iter()
.any(|r| r.signal_class == SignalClass::MaliciousBehavior);
let has_primary_block = primary_summary.recommended_action == RecommendedAction::Block;
let has_supporting_block =
supporting_summary.recommended_action == RecommendedAction::Block;
let has_non_hygiene_signal = root_cause_groups.iter().any(|group| {
matches!(
group.signal_class,
SignalClass::MaliciousBehavior
| SignalClass::SuspiciousPackageBehavior
| SignalClass::ReviewSignal
) && group.strongest_action != RecommendedAction::Log
});
let calibration_weakened_non_hygiene = raw_root_cause_groups.iter().any(|raw_group| {
let is_non_hygiene = matches!(
raw_group.signal_class,
SignalClass::MaliciousBehavior
| SignalClass::SuspiciousPackageBehavior
| SignalClass::ReviewSignal
) && raw_group.strongest_action != RecommendedAction::Log;
if !is_non_hygiene {
return false;
}
let calibrated = root_cause_groups
.iter()
.find(|cal| {
cal.scope == raw_group.scope
&& cal.category == raw_group.category
&& cal.signal_class == raw_group.signal_class
})
.or_else(|| {
root_cause_groups.iter().find(|cal| {
cal.scope == raw_group.scope
&& cal.category == raw_group.category
&& cal.signal_class == SignalClass::ReviewSignal
})
});
let Some(calibrated) = calibrated else {
return true;
};
calibrated.strongest_action < raw_group.strongest_action
|| calibrated.signal_class != raw_group.signal_class
});
let has_actionable_non_package_root = root_cause_groups.iter().any(|group| {
group.scope != ArtifactScope::PackageRootArtifact
&& group.strongest_action != RecommendedAction::Log
&& group.signal_class != SignalClass::Hygiene
});
let severe_hygiene_only = !has_non_hygiene_signal
&& !calibration_weakened_non_hygiene
&& root_cause_groups.iter().any(|group| {
group.signal_class == SignalClass::Hygiene
&& group.strongest_action == RecommendedAction::Block
});
let has_conclusive_supporting_malicious = findings
.iter()
.any(Finding::is_conclusive_malicious_evidence);
let has_conclusive_single_rule = findings.iter().any(|f| {
CONCLUSIVE_SINGLE_RULE_IDS.contains(&f.rule_id.as_str())
&& f.recommended_action == RecommendedAction::Block
});
let isolated_weak_signal_key = isolated_weak_package_root_group(root_cause_groups)
.map(|group| (group.scope, group.category, group.signal_class));
let isolated_weak_package_root_signal = isolated_weak_signal_key.is_some();
let has_non_hygiene_primary_block = root_cause_groups.iter().any(|group| {
group.scope == ArtifactScope::AgentEntrypoint
&& group.strongest_action == RecommendedAction::Block
&& group.signal_class != SignalClass::Hygiene
});
let mut malicious_block_rule_ids: std::collections::BTreeSet<&str> =
std::collections::BTreeSet::new();
let mut malicious_block_keys: std::collections::BTreeSet<(ThreatCategory, ArtifactScope)> =
std::collections::BTreeSet::new();
for f in *findings {
if f.signal_class == SignalClass::MaliciousBehavior
&& f.recommended_action == RecommendedAction::Block
{
malicious_block_rule_ids.insert(f.rule_id.as_str());
malicious_block_keys.insert((f.category, f.artifact_scope));
}
}
let has_independent_malicious_corroboration =
malicious_block_rule_ids.len() >= 2 || malicious_block_keys.len() >= 2;
Self {
has_malicious_behavior,
has_compound_malicious,
has_primary_block,
has_supporting_block,
has_non_hygiene_signal,
calibration_weakened_non_hygiene,
has_actionable_non_package_root,
severe_hygiene_only,
has_conclusive_supporting_malicious,
has_conclusive_single_rule,
isolated_weak_package_root_signal,
has_non_hygiene_primary_block,
isolated_weak_signal_key,
has_independent_malicious_corroboration,
}
}
pub(super) fn verdict(
&self,
calibration_notes: &[VerdictCalibrationNote],
primary_summary: &FindingSummary,
package_summary: &FindingSummary,
) -> Verdict {
let unconditional_escalation = (self.has_malicious_behavior
|| self.has_non_hygiene_primary_block)
&& self.has_independent_malicious_corroboration;
if unconditional_escalation
|| self.has_conclusive_single_rule
|| self.has_compound_malicious
|| (self.has_supporting_block && self.has_conclusive_supporting_malicious)
{
return Verdict::Malicious;
}
let risk_gated_high = primary_summary.risk_score >= RISK_THRESHOLD_BLOCK
|| package_summary.risk_score >= RISK_THRESHOLD_BLOCK;
let calibration_left_isolated_group_intact = self
.isolated_weak_signal_key
.map(|(scope, category, signal_class)| {
calibration_notes
.iter()
.filter(|n| {
n.scope == scope && n.category == category && n.signal_class == signal_class
})
.all(|n| n.effect.starts_with("remains_") || n.effect == "reclassified_only")
})
.unwrap_or(true);
if self.isolated_weak_package_root_signal
&& !self.has_actionable_non_package_root
&& !self.has_primary_block
&& !self.calibration_weakened_non_hygiene
&& !risk_gated_high
&& calibration_left_isolated_group_intact
&& package_summary.risk_score < RISK_THRESHOLD_APPROVAL
&& primary_summary.risk_score < RISK_THRESHOLD_APPROVAL
{
return Verdict::Benign;
}
if self.has_non_hygiene_signal
|| self.has_actionable_non_package_root
|| self.severe_hygiene_only
|| self.calibration_weakened_non_hygiene
|| risk_gated_high
{
Verdict::Suspicious
} else {
Verdict::Benign
}
}
pub(super) fn package_health(
&self,
hygiene_summary: &HygieneSummary,
verdict: Verdict,
) -> PackageHealth {
let base_health = if hygiene_summary.package_root_findings == 0
&& hygiene_summary.entrypoint_findings == 0
&& hygiene_summary.supporting_findings == 0
{
PackageHealth::Healthy
} else if self.severe_hygiene_only {
PackageHealth::NeedsReview
} else if self.has_non_hygiene_signal || self.calibration_weakened_non_hygiene {
PackageHealth::Elevated
} else {
PackageHealth::NeedsReview
};
if verdict == Verdict::Benign && base_health == PackageHealth::Elevated {
PackageHealth::NeedsReview
} else {
base_health
}
}
}
fn isolated_weak_package_root_group(
root_cause_groups: &[RootCauseGroup],
) -> Option<&RootCauseGroup> {
let actionable_groups: Vec<&RootCauseGroup> = root_cause_groups
.iter()
.filter(|group| group.strongest_action != RecommendedAction::Log)
.collect();
if actionable_groups.len() == 1
&& actionable_groups[0].scope == ArtifactScope::PackageRootArtifact
&& actionable_groups[0].strongest_action == RecommendedAction::RequireApproval
&& matches!(
actionable_groups[0].signal_class,
SignalClass::ReviewSignal | SignalClass::SuspiciousPackageBehavior
)
{
Some(actionable_groups[0])
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::findings::VerdictReason;
fn malicious_reason() -> VerdictReason {
VerdictReason {
scope: ArtifactScope::AgentEntrypoint,
category: ThreatCategory::DataExfiltration,
signal_class: SignalClass::MaliciousBehavior,
rationale: "compound chain at full strength".to_string(),
}
}
fn downgraded_reason() -> VerdictReason {
VerdictReason {
scope: ArtifactScope::AgentEntrypoint,
category: ThreatCategory::DataExfiltration,
signal_class: SignalClass::ReviewSignal,
rationale: "compound chain downgraded to review".to_string(),
}
}
#[test]
fn has_compound_malicious_only_counts_malicious_signal_class() {
let reasons_only_review = [downgraded_reason()];
assert!(
!reasons_only_review
.iter()
.any(|r| r.signal_class == SignalClass::MaliciousBehavior),
"review-signal-only compound reasons must NOT trip has_compound_malicious",
);
let reasons_with_malicious = [downgraded_reason(), malicious_reason()];
assert!(
reasons_with_malicious
.iter()
.any(|r| r.signal_class == SignalClass::MaliciousBehavior),
"any malicious-behavior compound reason MUST trip has_compound_malicious",
);
}
fn finding(
rule_id: &str,
signal_class: SignalClass,
action: RecommendedAction,
) -> crate::findings::Finding {
crate::findings::Finding::builder(rule_id, ThreatCategory::RemoteExec)
.severity(crate::findings::Severity::Critical)
.confidence(0.99)
.action(action)
.evidence_kind(crate::findings::EvidenceKind::Behavior)
.artifact(
crate::findings::ArtifactKind::SkillDocument,
Some("SKILL.md".to_string()),
)
.matched_on(crate::findings::MatchTarget::Document)
.signal_class(signal_class)
.build()
}
fn predicates_for(findings: &[crate::findings::Finding]) -> VerdictPredicates {
let primary = FindingSummary::from_findings(findings);
let supporting = FindingSummary::from_findings(&[]);
let groups = super::super::root_causes::build_root_cause_groups(findings);
VerdictPredicates::compute(&VerdictInputs {
findings,
root_cause_groups: &groups,
raw_root_cause_groups: &groups,
compound_reasons: &[],
primary_summary: &primary,
supporting_summary: &supporting,
})
}
#[test]
fn conclusive_single_rule_escalates_without_corroboration() {
for rule in CONCLUSIVE_SINGLE_RULE_IDS {
let findings = [finding(
rule,
SignalClass::MaliciousBehavior,
RecommendedAction::Block,
)];
let p = predicates_for(&findings);
assert!(
p.has_conclusive_single_rule,
"{rule} alone must set has_conclusive_single_rule",
);
assert!(
!p.has_independent_malicious_corroboration,
"{rule} fires once — corroboration must be absent (proves the \
escalation is via the conclusive path, not corroboration)",
);
assert_eq!(
p.verdict(
&[],
&FindingSummary::from_findings(&findings),
&FindingSummary::from_findings(&findings),
),
Verdict::Malicious,
"{rule} alone must yield Malicious",
);
}
}
#[test]
fn downgraded_conclusive_rule_does_not_escalate() {
let findings = [finding(
"SKILL_MACOS_BASE64_RCE",
SignalClass::ReviewSignal,
RecommendedAction::RequireApproval,
)];
let p = predicates_for(&findings);
assert!(
!p.has_conclusive_single_rule,
"a downgraded conclusive finding must not set the flag",
);
}
#[test]
fn conclusive_rule_escalates_regardless_of_signal_class() {
let findings = [finding(
"SKILL_MALICIOUS_PUBLISHER",
SignalClass::SuspiciousPackageBehavior,
RecommendedAction::Block,
)];
let p = predicates_for(&findings);
assert!(
p.has_conclusive_single_rule,
"a curated IOC rule at Block must escalate even as \
SuspiciousPackageBehavior",
);
assert_eq!(
p.verdict(
&[],
&FindingSummary::from_findings(&findings),
&FindingSummary::from_findings(&findings),
),
Verdict::Malicious,
"known-bad-publisher IOC alone must yield Malicious",
);
}
#[test]
fn non_curated_rule_still_needs_corroboration() {
let findings = [finding(
"SKILL_CRED_HARDCODED_KEY",
SignalClass::MaliciousBehavior,
RecommendedAction::Block,
)];
let p = predicates_for(&findings);
assert!(
!p.has_conclusive_single_rule,
"a non-curated rule must NOT get the conclusive bypass",
);
assert_eq!(
p.verdict(
&[],
&FindingSummary::from_findings(&findings),
&FindingSummary::from_findings(&findings),
),
Verdict::Suspicious,
"single non-curated MaliciousBehavior must stay Suspicious (corroboration gate)",
);
}
}