use crate::Severity;
use x509_cert::Certificate;
#[cfg(feature = "serde")]
use crate::de_cow_static;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum DeviationAddError {
DuplicateId(String),
EmptyField(String),
}
impl std::fmt::Display for DeviationAddError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DuplicateId(id) => {
write!(f, "deviation id '{id}' already exists in the store")
}
Self::EmptyField(field) => {
write!(f, "deviation field '{field}' must not be empty")
}
}
}
}
impl std::error::Error for DeviationAddError {}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Clone, Debug)]
pub struct Deviation {
pub id: String,
pub target_lint: String,
pub scope: DeviationScope,
pub effective_start: Option<u64>,
pub effective_end: Option<u64>,
pub action: DeviationAction,
pub justification: String,
pub authorized_by: String,
pub evidence_uri: Option<String>,
}
impl Deviation {
#[must_use]
pub fn is_active_at(&self, now_unix: u64) -> bool {
let after_start = self.effective_start.map_or(true, |start| now_unix >= start);
let before_end = self.effective_end.map_or(true, |end| now_unix < end);
after_start && before_end
}
#[must_use]
pub fn applies_to(&self, cert: &Certificate, now_unix: u64) -> bool {
if !self.is_active_at(now_unix) {
return false;
}
self.scope.matches(cert)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeviationScope {
Any,
IssuerDnContains(String),
IssuerDnExact(x509_cert::name::Name),
SerialRange {
issuer: x509_cert::name::Name,
start: Vec<u8>,
end: Vec<u8>,
},
}
impl DeviationScope {
#[must_use]
pub fn matches(&self, cert: &Certificate) -> bool {
match self {
Self::Any => true,
Self::IssuerDnContains(substring) => {
let mut issuer_str = cert.tbs_certificate.issuer.to_string();
issuer_str.make_ascii_lowercase();
issuer_str.contains(substring.as_str())
}
Self::IssuerDnExact(name) => {
pkix_path::names_match(name, &cert.tbs_certificate.issuer)
}
Self::SerialRange { issuer, start, end } => {
if !pkix_path::names_match(issuer, &cert.tbs_certificate.issuer) {
return false;
}
let serial = cert.tbs_certificate.serial_number.as_bytes();
let cmp_start = serial_cmp(serial, start);
let cmp_end = serial_cmp(serial, end);
cmp_start.is_ge() && cmp_end.is_le()
}
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for DeviationScope {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStructVariant as _;
match self {
Self::Any => serializer.serialize_unit_variant("DeviationScope", 0, "Any"),
Self::IssuerDnContains(s) => {
serializer.serialize_newtype_variant("DeviationScope", 1, "IssuerDnContains", s)
}
Self::IssuerDnExact(name) => serializer.serialize_newtype_variant(
"DeviationScope",
2,
"IssuerDnExact",
&name.to_string(),
),
Self::SerialRange { issuer, start, end } => {
let mut sv =
serializer.serialize_struct_variant("DeviationScope", 3, "SerialRange", 3)?;
sv.serialize_field("issuer", &issuer.to_string())?;
sv.serialize_field("start", start)?;
sv.serialize_field("end", end)?;
sv.end()
}
}
}
}
fn serial_cmp(a: &[u8], b: &[u8]) -> core::cmp::Ordering {
let a = strip_leading_zeros(a);
let b = strip_leading_zeros(b);
a.len().cmp(&b.len()).then_with(|| a.cmp(b))
}
fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
let first_nonzero = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len());
&bytes[first_nonzero..]
}
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeviationAction {
DowngradeSeverityTo(Severity),
Suppress,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'static")))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeviatedFinding {
#[cfg_attr(feature = "serde", serde(deserialize_with = "de_cow_static"))]
pub lint_id: std::borrow::Cow<'static, str>,
#[cfg_attr(feature = "serde", serde(deserialize_with = "de_cow_static"))]
pub citation: std::borrow::Cow<'static, str>,
pub original_result: crate::LintResult,
pub deviation_id: String,
pub action: DeviationAction,
pub justification: String,
pub evidence_uri: Option<String>,
pub cert_index: Option<usize>,
pub evaluated_at_unix: u64,
}
impl DeviatedFinding {
#[must_use]
pub const fn effective_severity(&self) -> Option<Severity> {
match &self.action {
DeviationAction::DowngradeSeverityTo(s) => Some(*s),
DeviationAction::Suppress => None,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Clone, Debug, Default)]
pub struct DeviationStore {
deviations: Vec<Deviation>,
}
impl DeviationStore {
#[must_use]
pub const fn new() -> Self {
Self {
deviations: Vec::new(),
}
}
pub fn add(&mut self, mut deviation: Deviation) -> Result<(), DeviationAddError> {
if deviation.justification.is_empty() {
return Err(DeviationAddError::EmptyField("justification".into()));
}
if deviation.authorized_by.is_empty() {
return Err(DeviationAddError::EmptyField("authorized_by".into()));
}
if self.deviations.iter().any(|d| d.id == deviation.id) {
return Err(DeviationAddError::DuplicateId(deviation.id.clone()));
}
if let DeviationScope::IssuerDnContains(s) = &mut deviation.scope {
s.make_ascii_lowercase();
}
self.deviations.push(deviation);
Ok(())
}
#[must_use]
pub fn all(&self) -> &[Deviation] {
&self.deviations
}
#[must_use = "iterator is lazy; collect or iterate to use results"]
pub fn active_at(&self, now_unix: u64) -> impl Iterator<Item = &Deviation> {
self.deviations
.iter()
.filter(move |d| d.is_active_at(now_unix))
}
#[must_use = "iterator is lazy; collect or iterate to use results"]
pub fn active_for_lint<'a>(
&'a self,
lint_id: &'a str,
now_unix: u64,
) -> impl Iterator<Item = &'a Deviation> {
self.deviations
.iter()
.filter(move |d| d.target_lint.as_str() == lint_id && d.is_active_at(now_unix))
}
#[must_use = "iterator is lazy; collect or iterate to use results"]
pub fn expired_at(&self, now_unix: u64) -> impl Iterator<Item = &Deviation> {
self.deviations
.iter()
.filter(move |d| d.effective_end.is_some_and(|end| now_unix >= end))
}
#[must_use]
pub fn find_deviation(
&self,
lint_id: &str,
cert: &Certificate,
now_unix: u64,
) -> Option<&Deviation> {
self.deviations
.iter()
.find(|d| d.target_lint.as_str() == lint_id && d.applies_to(cert, now_unix))
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DeviationRunResult {
pub findings: Vec<crate::Finding>,
pub deviated: Vec<DeviatedFinding>,
}
pub struct DeviationRunner {
runner: crate::LintRunner,
store: DeviationStore,
}
impl DeviationRunner {
#[must_use]
pub const fn new(runner: crate::LintRunner, store: DeviationStore) -> Self {
Self { runner, store }
}
#[must_use]
pub const fn lint_runner(&self) -> &crate::LintRunner {
&self.runner
}
#[must_use]
pub const fn deviation_store(&self) -> &DeviationStore {
&self.store
}
#[must_use]
pub fn run_cert(
&self,
cert: &Certificate,
kind: crate::SubjectKind,
cert_index: usize,
now_unix: u64,
) -> DeviationRunResult {
let raw = self.runner.run_cert(cert, kind, cert_index, now_unix);
self.apply_deviations(raw, cert, now_unix)
}
#[must_use]
pub fn run_cert_at_issuance(
&self,
cert: &Certificate,
kind: crate::SubjectKind,
cert_index: usize,
) -> DeviationRunResult {
let issuance_unix = cert
.tbs_certificate
.validity
.not_before
.to_unix_duration()
.as_secs();
self.run_cert(cert, kind, cert_index, issuance_unix)
}
#[must_use]
pub fn run_chain(
&self,
chain: &[Certificate],
kinds: &[crate::SubjectKind],
now_unix: u64,
) -> DeviationRunResult {
let mut result = DeviationRunResult::default();
for (i, cert) in chain.iter().enumerate() {
let kind = kinds
.get(i)
.copied()
.unwrap_or(crate::SubjectKind::IntermediateCa);
let raw = self.runner.run_cert(cert, kind, i, now_unix);
let partial = self.apply_deviations(raw, cert, now_unix);
result.findings.extend(partial.findings);
result.deviated.extend(partial.deviated);
}
result
}
#[must_use]
pub fn run_path(
&self,
chain: &[Certificate],
path: &crate::ValidatedPath,
now_unix: u64,
) -> DeviationRunResult {
let raw = self.runner.run_path(chain, path, now_unix);
match chain.first() {
Some(leaf) => self.apply_deviations(raw, leaf, now_unix),
None => DeviationRunResult {
findings: raw,
deviated: vec![],
},
}
}
fn apply_deviations(
&self,
raw: Vec<crate::Finding>,
cert: &Certificate,
now_unix: u64,
) -> DeviationRunResult {
let mut result = DeviationRunResult::default();
for finding in raw {
if !finding.result.is_finding() {
result.findings.push(finding);
continue;
}
match self.store.find_deviation(&finding.lint_id, cert, now_unix) {
None => {
result.findings.push(finding);
}
Some(dev) => {
result.deviated.push(DeviatedFinding {
lint_id: finding.lint_id,
citation: finding.citation,
original_result: finding.result,
deviation_id: dev.id.clone(),
action: dev.action.clone(),
justification: dev.justification.clone(),
evidence_uri: dev.evidence_uri.clone(),
cert_index: finding.cert_index,
evaluated_at_unix: finding.evaluated_at_unix,
});
}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LintResult;
fn make_deviation(id: &str, lint_id: &str) -> Deviation {
Deviation {
id: id.to_string(),
target_lint: lint_id.to_string(),
scope: DeviationScope::Any,
effective_start: None,
effective_end: None,
action: DeviationAction::DowngradeSeverityTo(Severity::Info),
justification: "test justification".to_string(),
authorized_by: "test-author@example.com".to_string(),
evidence_uri: None,
}
}
fn load_cert() -> Certificate {
use der::Decode as _;
Certificate::from_der(include_bytes!(
"../../pkix-path/tests/fixtures/policy-checks/webpki-self-signed-365d.der"
))
.expect("fixture is valid DER")
}
#[test]
fn deviation_active_at_no_bounds() {
let d = make_deviation("d1", "test.lint");
assert!(d.is_active_at(0));
assert!(d.is_active_at(u64::MAX));
}
#[test]
fn deviation_active_after_start() {
let d = Deviation {
effective_start: Some(100),
effective_end: None,
..make_deviation("d2", "test.lint")
};
assert!(!d.is_active_at(99), "before start must not be active");
assert!(d.is_active_at(100), "at start must be active");
assert!(d.is_active_at(200), "after start must be active");
}
#[test]
fn deviation_expires_at_end() {
let d = Deviation {
effective_start: None,
effective_end: Some(200),
..make_deviation("d3", "test.lint")
};
assert!(d.is_active_at(199), "before end must be active");
assert!(
!d.is_active_at(200),
"at end must NOT be active (exclusive)"
);
assert!(!d.is_active_at(201), "after end must not be active");
}
#[test]
fn deviation_active_within_range() {
let d = Deviation {
effective_start: Some(100),
effective_end: Some(200),
..make_deviation("d4", "test.lint")
};
assert!(!d.is_active_at(99));
assert!(d.is_active_at(100));
assert!(d.is_active_at(150));
assert!(d.is_active_at(199));
assert!(!d.is_active_at(200));
}
#[test]
fn scope_any_matches_any_cert() {
let cert = load_cert();
assert!(DeviationScope::Any.matches(&cert));
}
#[test]
fn scope_issuer_dn_contains_case_insensitive() {
let cert = load_cert();
let issuer = cert.tbs_certificate.issuer.to_string();
let word = issuer.split_whitespace().next().unwrap_or("cert");
let scope_lower = DeviationScope::IssuerDnContains(word.to_lowercase());
let scope_upper = DeviationScope::IssuerDnContains(word.to_uppercase().to_lowercase());
assert!(scope_lower.matches(&cert), "lowercase match must succeed");
assert!(
scope_upper.matches(&cert),
"lowercased-at-construction match must succeed"
);
}
#[test]
fn scope_issuer_dn_contains_no_match() {
let cert = load_cert();
let scope = DeviationScope::IssuerDnContains("XYZ_NONEXISTENT_ISSUER_9999".to_string());
assert!(!scope.matches(&cert));
}
#[test]
fn deviation_store_add_normalizes_issuer_dn_contains_to_lowercase() {
let cert = load_cert();
let issuer = cert.tbs_certificate.issuer.to_string();
let word = issuer
.split(|c: char| !c.is_alphanumeric())
.find(|w| !w.is_empty())
.unwrap_or("test");
let uppercase_word = word.to_uppercase();
if uppercase_word == word.to_lowercase() {
return;
}
let mut store = DeviationStore::new();
let deviation = Deviation {
scope: DeviationScope::IssuerDnContains(uppercase_word.clone()),
..make_deviation("norm-test", "test.lint")
};
store.add(deviation).expect("add must succeed");
match &store.all()[0].scope {
DeviationScope::IssuerDnContains(s) => {
assert_eq!(
*s,
uppercase_word.to_lowercase(),
"DeviationStore::add must lowercase IssuerDnContains substring"
);
}
other => panic!("expected IssuerDnContains, got {other:?}"),
}
assert!(
store.all()[0].scope.matches(&cert),
"normalized IssuerDnContains must match cert"
);
}
#[test]
fn scope_issuer_dn_exact_matches_cert_issuer() {
let cert = load_cert();
let scope = DeviationScope::IssuerDnExact(cert.tbs_certificate.issuer.clone());
assert!(
scope.matches(&cert),
"IssuerDnExact with cert's own issuer must match"
);
}
#[test]
fn scope_issuer_dn_exact_does_not_match_different_dn() {
use der::Decode as _;
let cert = load_cert();
let other_cert = Certificate::from_der(include_bytes!(
"../../pkix-path/tests/fixtures/policy-checks/smime-self-signed-365d.der"
))
.expect("fixture is valid DER");
let scope = DeviationScope::IssuerDnExact(other_cert.tbs_certificate.issuer.clone());
let same = pkix_path::names_match(
&cert.tbs_certificate.issuer,
&other_cert.tbs_certificate.issuer,
);
if !same {
assert!(
!scope.matches(&cert),
"IssuerDnExact with different issuer must not match"
);
}
}
#[test]
fn serial_cmp_greater() {
use core::cmp::Ordering;
assert_eq!(serial_cmp(&[0x02], &[0x01]), Ordering::Greater);
assert_eq!(serial_cmp(&[0x01, 0x00], &[0xFF]), Ordering::Greater);
}
#[test]
fn serial_cmp_less() {
use core::cmp::Ordering;
assert_eq!(serial_cmp(&[0x01], &[0x02]), Ordering::Less);
assert_eq!(serial_cmp(&[0xFF], &[0x01, 0x00]), Ordering::Less);
}
#[test]
fn serial_cmp_equal() {
use core::cmp::Ordering;
assert_eq!(serial_cmp(&[0x05], &[0x05]), Ordering::Equal);
}
#[test]
fn serial_cmp_leading_zeros_stripped() {
use core::cmp::Ordering;
assert_eq!(serial_cmp(&[0x00, 0x01], &[0x01]), Ordering::Equal);
assert!(serial_cmp(&[0x00, 0x01], &[0x01]).is_ge());
assert!(serial_cmp(&[0x00, 0x01], &[0x01]).is_le());
}
#[test]
fn scope_serial_range_matches_cert_in_range() {
let cert = load_cert();
let serial = cert.tbs_certificate.serial_number.as_bytes().to_vec();
let scope = DeviationScope::SerialRange {
issuer: cert.tbs_certificate.issuer.clone(),
start: serial.clone(),
end: serial,
};
assert!(
scope.matches(&cert),
"cert's own serial must be within [serial, serial]"
);
}
#[test]
fn scope_serial_range_excludes_cert_outside_range() {
let cert = load_cert();
let serial = cert.tbs_certificate.serial_number.as_bytes();
let start = vec![0xFF; serial.len() + 1]; let end = vec![0xFF; serial.len() + 2];
let scope = DeviationScope::SerialRange {
issuer: cert.tbs_certificate.issuer.clone(),
start,
end,
};
assert!(
!scope.matches(&cert),
"cert serial below range start must not match"
);
}
#[test]
fn scope_serial_range_wrong_issuer_no_match() {
use der::Decode as _;
let cert = load_cert();
let other_cert = Certificate::from_der(include_bytes!(
"../../pkix-path/tests/fixtures/policy-checks/smime-self-signed-365d.der"
))
.expect("fixture is valid DER");
let serial = cert.tbs_certificate.serial_number.as_bytes().to_vec();
let scope = DeviationScope::SerialRange {
issuer: other_cert.tbs_certificate.issuer.clone(),
start: vec![0x00],
end: vec![0xFF; serial.len() + 2], };
let same_issuer = pkix_path::names_match(
&cert.tbs_certificate.issuer,
&other_cert.tbs_certificate.issuer,
);
if !same_issuer {
assert!(
!scope.matches(&cert),
"wrong issuer in SerialRange must not match"
);
}
}
#[test]
fn store_add_and_retrieve() {
let mut store = DeviationStore::new();
store
.add(make_deviation("d1", "test.lint.a"))
.expect("add should succeed");
store
.add(make_deviation("d2", "test.lint.b"))
.expect("add should succeed");
assert_eq!(store.all().len(), 2);
}
#[test]
fn store_rejects_empty_justification() {
let mut store = DeviationStore::new();
let result = store.add(Deviation {
justification: String::new(),
..make_deviation("d1", "test.lint")
});
assert_eq!(
result,
Err(DeviationAddError::EmptyField("justification".into())),
"empty justification must return EmptyField error"
);
}
#[test]
fn store_rejects_empty_authorized_by() {
let mut store = DeviationStore::new();
let result = store.add(Deviation {
authorized_by: String::new(),
..make_deviation("d1", "test.lint")
});
assert_eq!(
result,
Err(DeviationAddError::EmptyField("authorized_by".into())),
"empty authorized_by must return EmptyField error"
);
}
#[test]
fn store_rejects_duplicate_id() {
let mut store = DeviationStore::new();
store
.add(make_deviation("d1", "test.lint.a"))
.expect("first add should succeed");
let result = store.add(make_deviation("d1", "test.lint.b")); assert!(result.is_err(), "duplicate id must return Err");
assert_eq!(
result.unwrap_err(),
DeviationAddError::DuplicateId("d1".to_string())
);
}
#[test]
fn store_find_deviation_matches() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(Deviation {
effective_start: None,
effective_end: None,
..make_deviation("d1", "test.lint.a")
})
.expect("add should succeed");
let found = store.find_deviation("test.lint.a", &cert, now);
assert!(found.is_some());
assert_eq!(found.unwrap().id, "d1");
}
#[test]
fn store_find_deviation_no_match_wrong_lint() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(make_deviation("d1", "test.lint.a"))
.expect("add should succeed");
assert!(store.find_deviation("test.lint.b", &cert, now).is_none());
}
#[test]
fn store_find_deviation_expired_not_matched() {
let cert = load_cert();
let now: u64 = 1_000;
let mut store = DeviationStore::new();
store
.add(Deviation {
effective_end: Some(500), ..make_deviation("d1", "test.lint.a")
})
.expect("add should succeed");
assert!(store.find_deviation("test.lint.a", &cert, now).is_none());
}
#[test]
fn store_expired_at_reports_expired_deviations() {
let mut store = DeviationStore::new();
store
.add(Deviation {
effective_end: Some(500),
..make_deviation("d1", "test.lint.a")
})
.expect("add should succeed");
store
.add(Deviation {
effective_end: None, ..make_deviation("d2", "test.lint.b")
})
.expect("add should succeed");
let expired: Vec<_> = store.expired_at(1000).collect();
assert_eq!(expired.len(), 1);
assert_eq!(expired[0].id, "d1");
}
#[test]
fn deviated_finding_effective_severity() {
let f = DeviatedFinding {
lint_id: std::borrow::Cow::Borrowed("test.lint"),
citation: std::borrow::Cow::Borrowed("test citation"),
original_result: LintResult::Error("original"),
deviation_id: "d1".to_string(),
action: DeviationAction::DowngradeSeverityTo(Severity::Info),
justification: "test justification".to_string(),
evidence_uri: None,
cert_index: None,
evaluated_at_unix: 0,
};
assert_eq!(f.effective_severity(), Some(Severity::Info));
let f2 = DeviatedFinding {
action: DeviationAction::Suppress,
..f
};
assert_eq!(f2.effective_severity(), None);
}
struct AlwaysError;
impl crate::Lint for AlwaysError {
fn id(&self) -> &'static str {
"test.always_error"
}
fn citation(&self) -> &'static str {
"test"
}
fn severity(&self) -> crate::Severity {
crate::Severity::Error
}
fn scope(&self) -> crate::Scope {
crate::Scope::Certificate
}
fn applies_to(&self) -> crate::SubjectKind {
crate::SubjectKind::Any
}
fn check_cert(
&self,
_cert: &Certificate,
_kind: crate::SubjectKind,
_now: u64,
) -> crate::LintResult {
crate::LintResult::Error("always errors")
}
}
struct AlwaysPass;
impl crate::Lint for AlwaysPass {
fn id(&self) -> &'static str {
"test.always_pass"
}
fn citation(&self) -> &'static str {
"test"
}
fn severity(&self) -> crate::Severity {
crate::Severity::Info
}
fn scope(&self) -> crate::Scope {
crate::Scope::Certificate
}
fn applies_to(&self) -> crate::SubjectKind {
crate::SubjectKind::Any
}
fn check_cert(
&self,
_cert: &Certificate,
_kind: crate::SubjectKind,
_now: u64,
) -> crate::LintResult {
crate::LintResult::Pass
}
}
#[test]
fn deviation_runner_moves_deviated_finding_to_deviated() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(Deviation {
target_lint: "test.always_error".to_string(),
..make_deviation("d1", "test.always_error")
})
.expect("add should succeed");
let runner = crate::LintRunner::new(vec![Box::new(AlwaysError)]);
let dev_runner = DeviationRunner::new(runner, store);
let result = dev_runner.run_cert(&cert, crate::SubjectKind::Leaf, 0, now);
assert!(
result.findings.is_empty(),
"deviated finding must not be in findings"
);
assert_eq!(
result.deviated.len(),
1,
"deviated finding must be in deviated"
);
assert_eq!(result.deviated[0].lint_id, "test.always_error");
assert_eq!(result.deviated[0].deviation_id, "d1");
assert!(matches!(
result.deviated[0].original_result,
crate::LintResult::Error(_)
));
}
#[test]
fn deviation_runner_non_deviated_finding_stays_in_findings() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(make_deviation("d1", "test.different_lint"))
.expect("add should succeed");
let runner = crate::LintRunner::new(vec![Box::new(AlwaysPass)]);
let dev_runner = DeviationRunner::new(runner, store);
let result = dev_runner.run_cert(&cert, crate::SubjectKind::Leaf, 0, now);
assert_eq!(result.findings.len(), 1);
assert!(result.deviated.is_empty());
}
#[test]
fn deviation_runner_expired_deviation_does_not_apply() {
let cert = load_cert();
let now: u64 = 2_000_000;
let mut store = DeviationStore::new();
store
.add(Deviation {
effective_end: Some(1_000_000), ..make_deviation("d1", "test.always_error")
})
.expect("add should succeed");
let runner = crate::LintRunner::new(vec![Box::new(AlwaysError)]);
let dev_runner = DeviationRunner::new(runner, store);
let result = dev_runner.run_cert(&cert, crate::SubjectKind::Leaf, 0, now);
assert_eq!(result.findings.len(), 1);
assert!(result.deviated.is_empty());
}
#[test]
fn deviation_runner_suppress_action_sets_effective_severity_none() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(Deviation {
action: DeviationAction::Suppress,
..make_deviation("d1", "test.always_error")
})
.expect("add should succeed");
let runner = crate::LintRunner::new(vec![Box::new(AlwaysError)]);
let dev_runner = DeviationRunner::new(runner, store);
let result = dev_runner.run_cert(&cert, crate::SubjectKind::Leaf, 0, now);
assert!(result.findings.is_empty());
assert_eq!(result.deviated.len(), 1);
assert_eq!(result.deviated[0].effective_severity(), None);
}
#[test]
fn evidence_uri_flows_to_deviated_finding() {
let cert = load_cert();
let now: u64 = 1_000_000;
let uri = "https://pkipolicy.agency.gov/waivers/2025-11-03";
let mut store = DeviationStore::new();
store
.add(Deviation {
evidence_uri: Some(uri.to_string()),
..make_deviation("d1", "test.always_error")
})
.expect("add should succeed");
let runner = crate::LintRunner::new(vec![Box::new(AlwaysError)]);
let dev_runner = DeviationRunner::new(runner, store);
let result = dev_runner.run_cert(&cert, crate::SubjectKind::Leaf, 0, now);
assert_eq!(result.deviated.len(), 1);
assert_eq!(
result.deviated[0].evidence_uri.as_deref(),
Some(uri),
"evidence_uri must flow from Deviation to DeviatedFinding"
);
assert_eq!(result.deviated[0].justification, "test justification");
}
#[test]
fn evidence_uri_none_when_deviation_has_no_uri() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(make_deviation("d1", "test.always_error"))
.expect("add should succeed");
let runner = crate::LintRunner::new(vec![Box::new(AlwaysError)]);
let dev_runner = DeviationRunner::new(runner, store);
let result = dev_runner.run_cert(&cert, crate::SubjectKind::Leaf, 0, now);
assert_eq!(result.deviated.len(), 1);
assert_eq!(result.deviated[0].evidence_uri, None);
}
}