use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::AuditHint;
use crate::scan::ScanReport;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupplyChainAudit {
pub antigen_type: String,
pub file: PathBuf,
pub line: usize,
pub crate_name: String,
pub version: String,
pub hint: AuditHint,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SupplyChainAuditReport {
pub audits: Vec<SupplyChainAudit>,
pub pass_count: usize,
pub fail_count: usize,
}
impl SupplyChainAuditReport {
#[must_use]
pub const fn all_pass(&self) -> bool {
self.fail_count == 0
}
}
#[must_use]
pub fn audit_supply_chain(report: &ScanReport, workspace_root: &Path) -> SupplyChainAuditReport {
let mut audits: Vec<SupplyChainAudit> = Vec::new();
for immunity in &report.immunities {
let Some(json) = &immunity.requires_predicate else {
continue;
};
let Ok(predicate) = serde_json::from_str::<antigen_attestation::Predicate>(json) else {
continue;
};
if predicate.validate().is_err() {
audits.push(SupplyChainAudit {
antigen_type: immunity.antigen_type.clone(),
hint: AuditHint::MalformedRequiresPredicate,
file: immunity.file.clone(),
line: immunity.line,
crate_name: String::new(),
version: String::new(),
detail: Some(
"requires_predicate deserialized but failed structural validation \
(e.g., empty combinator — vacuous pass with no leaves)"
.to_string(),
),
});
continue;
}
let entries = eval_supply_chain_predicate(
&predicate,
workspace_root,
&immunity.antigen_type,
&immunity.file,
immunity.line,
);
audits.extend(entries.entries);
}
let mut pass_count = 0usize;
let mut fail_count = 0usize;
for a in &audits {
if is_supply_chain_pass_hint(&a.hint) {
pass_count += 1;
} else {
fail_count += 1;
}
}
SupplyChainAuditReport {
audits,
pass_count,
fail_count,
}
}
struct SupplyChainEval {
passed: bool,
entries: Vec<SupplyChainAudit>,
}
fn eval_supply_chain_predicate(
pred: &antigen_attestation::Predicate,
workspace_root: &Path,
antigen_type: &str,
file: &Path,
line: usize,
) -> SupplyChainEval {
use antigen_attestation::Predicate;
match pred {
Predicate::Leaf(l) => {
let entry = audit_supply_chain_leaf(l, workspace_root, antigen_type, file, line);
let passed = entry
.as_ref()
.is_some_and(|e| is_supply_chain_pass_hint(&e.hint));
let logical_passed = entry.as_ref().is_none_or(|_| passed);
SupplyChainEval {
passed: logical_passed,
entries: entry.into_iter().collect(),
}
}
Predicate::AllOf { children } => {
let mut entries = Vec::new();
let mut all_pass = true;
for c in children {
let sub = eval_supply_chain_predicate(c, workspace_root, antigen_type, file, line);
if !sub.passed {
all_pass = false;
}
entries.extend(sub.entries);
}
SupplyChainEval {
passed: all_pass,
entries,
}
}
Predicate::AnyOf { children } => {
let mut pass_entries = Vec::new();
let mut fail_entries = Vec::new();
let mut any_pass = false;
for c in children {
let sub = eval_supply_chain_predicate(c, workspace_root, antigen_type, file, line);
if sub.passed {
any_pass = true;
pass_entries.extend(sub.entries);
} else {
fail_entries.extend(sub.entries);
}
}
let entries = if any_pass { pass_entries } else { fail_entries };
SupplyChainEval {
passed: any_pass,
entries,
}
}
Predicate::Not { child } => {
let sub = eval_supply_chain_predicate(child, workspace_root, antigen_type, file, line);
SupplyChainEval {
passed: !sub.passed,
entries: Vec::new(),
}
}
}
}
fn audit_supply_chain_leaf(
leaf: &antigen_attestation::Leaf,
workspace_root: &Path,
antigen_type: &str,
file: &Path,
line: usize,
) -> Option<SupplyChainAudit> {
use antigen_attestation::Leaf;
let (crate_name, version, hint, detail) = match leaf {
Leaf::DepPinned { crate_name } => {
eval_dep_pinned_to_hint(workspace_root, crate_name.as_deref())
}
Leaf::DepAttested {
crate_name,
version,
exact_version,
..
} => eval_dep_attested_to_hint(workspace_root, crate_name, version, *exact_version),
Leaf::MaintainerUnchanged {
crate_name,
since_version,
} => eval_maintainer_unchanged_to_hint(workspace_root, crate_name, since_version),
Leaf::ContentHashMatches {
crate_name,
version,
} => eval_content_hash_matches_to_hint(workspace_root, crate_name, version),
Leaf::SandboxClean {
crate_name,
sandbox_kind,
} => eval_sandbox_clean_to_hint(crate_name, sandbox_kind),
Leaf::RatifiedDoc { .. }
| Leaf::Signers { .. }
| Leaf::SignedTrailer { .. }
| Leaf::OraclesComplete { .. }
| Leaf::FreshWithinDays { .. } => return None,
};
Some(SupplyChainAudit {
antigen_type: antigen_type.to_string(),
file: file.to_path_buf(),
line,
crate_name,
version,
hint,
detail,
})
}
fn eval_dep_pinned_to_hint(
workspace_root: &Path,
crate_name: Option<&str>,
) -> (String, String, AuditHint, Option<String>) {
use crate::supply_chain::{evaluate, witness::DepPinnedState};
let state = evaluate::evaluate_dep_pinned(workspace_root, crate_name);
let (hint, detail) = match &state {
DepPinnedState::AllPinned => (AuditHint::FunctionResolves, None),
DepPinnedState::Unpinned { unpinned_deps } => (
AuditHint::UnpinnedDependency,
Some(format!("unpinned: {unpinned_deps:?}")),
),
DepPinnedState::NotInManifest { crate_name: cn } => (
AuditHint::UnpinnedDependency,
Some(format!("crate not in manifest: {cn}")),
),
};
(
crate_name.map_or_else(|| "*".to_string(), str::to_string),
String::new(),
hint,
detail,
)
}
fn eval_dep_attested_to_hint(
workspace_root: &Path,
crate_name: &str,
version: &str,
exact_version: bool,
) -> (String, String, AuditHint, Option<String>) {
use crate::supply_chain::{evaluate, witness::DepAttestedState};
let state = evaluate::evaluate_dep_attested(workspace_root, crate_name, version, exact_version);
let (hint, detail) = match &state {
DepAttestedState::Attested { .. } => (AuditHint::FunctionResolves, None),
DepAttestedState::AttestedWithoutReviewableArtifact => {
(AuditHint::DepAttestWithoutReviewableArtifact, None)
}
DepAttestedState::SidecarMissing => (AuditHint::UnattestedDependencyInclusion, None),
DepAttestedState::SidecarMalformed { error } => (
AuditHint::UnattestedDependencyInclusion,
Some(format!("sidecar malformed: {error}")),
),
DepAttestedState::AttestationStale {
attested_version,
requested_version,
} => (
AuditHint::DepAttestationStale,
Some(format!(
"attested: {attested_version}; requested: {requested_version}"
)),
),
};
(crate_name.to_string(), version.to_string(), hint, detail)
}
fn eval_maintainer_unchanged_to_hint(
workspace_root: &Path,
crate_name: &str,
since_version: &str,
) -> (String, String, AuditHint, Option<String>) {
use crate::supply_chain::{evaluate, witness::MaintainerState};
let state = evaluate::evaluate_maintainer_unchanged(workspace_root, crate_name, since_version);
let (hint, detail) = match &state {
MaintainerState::Unchanged => (AuditHint::FunctionResolves, None),
MaintainerState::Changed { added, removed } => (
AuditHint::MaintainerChangeWithoutReattestation,
Some(format!("added: {added:?}; removed: {removed:?}")),
),
MaintainerState::SnapshotMissing => (
AuditHint::MaintainerChangeWithoutReattestation,
Some("snapshot missing".to_string()),
),
MaintainerState::CratesIoQueryUnavailable => (AuditHint::CratesIoMetadataQueryFailed, None),
};
(
crate_name.to_string(),
since_version.to_string(),
hint,
detail,
)
}
fn eval_content_hash_matches_to_hint(
workspace_root: &Path,
crate_name: &str,
version: &str,
) -> (String, String, AuditHint, Option<String>) {
use crate::supply_chain::{evaluate, witness::ContentHashState};
let state = evaluate::evaluate_content_hash_matches(workspace_root, crate_name, version);
let (hint, detail) = match &state {
ContentHashState::Matches => (AuditHint::FunctionResolves, None),
ContentHashState::Mismatch { recorded, current } => (
AuditHint::ContentHashMismatch,
Some(format!("recorded: {recorded}; current: {current}")),
),
ContentHashState::NoAttestation => (AuditHint::ContentHashNoAttestation, None),
ContentHashState::CrateNotInLockfile { crate_name: cn } => (
AuditHint::ContentHashNoAttestation,
Some(format!("crate not in Cargo.lock: {cn}")),
),
ContentHashState::SidecarMalformed { error } => (
AuditHint::ContentHashSidecarMalformed,
Some(format!("malformed: {error}")),
),
};
(crate_name.to_string(), version.to_string(), hint, detail)
}
fn eval_sandbox_clean_to_hint(
crate_name: &str,
sandbox_kind: &str,
) -> (String, String, AuditHint, Option<String>) {
let hint = if sandbox_kind == "proc-macro" {
AuditHint::UnsandboxedProcMacro
} else {
AuditHint::UnsandboxedBuildScript
};
(
crate_name.to_string(),
String::new(),
hint,
Some(format!(
"v0.2 sandbox tooling not yet available; kind={sandbox_kind}"
)),
)
}
const fn is_supply_chain_pass_hint(hint: &AuditHint) -> bool {
matches!(hint, AuditHint::FunctionResolves)
}