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),
MalformedScope {
kind: String,
reason: 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")
}
Self::MalformedScope { kind, reason } => {
write!(f, "deviation scope kind '{kind}' is malformed: {reason}")
}
}
}
}
impl std::error::Error for DeviationAddError {}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
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>,
pub priority: i32,
}
impl Deviation {
pub fn new(
id: impl Into<String>,
target_lint: impl Into<String>,
scope: DeviationScope,
action: DeviationAction,
justification: impl Into<String>,
authorized_by: impl Into<String>,
) -> Result<Self, DeviationAddError> {
let id: String = id.into();
let target_lint: String = target_lint.into();
let justification: String = justification.into();
let authorized_by: String = authorized_by.into();
if id.is_empty() {
return Err(DeviationAddError::EmptyField("id".into()));
}
if target_lint.is_empty() {
return Err(DeviationAddError::EmptyField("target_lint".into()));
}
if justification.is_empty() {
return Err(DeviationAddError::EmptyField("justification".into()));
}
if authorized_by.is_empty() {
return Err(DeviationAddError::EmptyField("authorized_by".into()));
}
Ok(Self {
id,
target_lint,
scope,
effective_start: None,
effective_end: None,
action,
justification,
authorized_by,
evidence_uri: None,
priority: 0,
})
}
#[must_use]
pub fn with_effective_start(mut self, unix_seconds: u64) -> Self {
self.effective_start = Some(unix_seconds);
self
}
#[must_use]
pub fn with_effective_end(mut self, unix_seconds: u64) -> Self {
self.effective_end = Some(unix_seconds);
self
}
#[must_use]
pub fn with_evidence_uri(mut self, uri: impl Into<String>) -> Self {
self.evidence_uri = Some(uri.into());
self
}
#[must_use]
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
#[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)
}
}
pub const SCOPE_KIND_ANY: &str = "pkix-lint.scope.any";
pub const SCOPE_KIND_ISSUER_DN_CONTAINS: &str = "pkix-lint.scope.issuer-dn-contains";
pub const SCOPE_KIND_ISSUER_DN_EXACT: &str = "pkix-lint.scope.issuer-dn-exact";
pub const SCOPE_KIND_SERIAL_RANGE: &str = "pkix-lint.scope.serial-range";
pub const PROP_ISSUER_DN_SUBSTRING: &str = "pkix-lint.issuer-dn-substring";
pub const PROP_ISSUER_DN_DER: &str = "pkix-lint.issuer-dn-der";
pub const PROP_SERIAL_START: &str = "pkix-lint.serial-start";
pub const PROP_SERIAL_END: &str = "pkix-lint.serial-end";
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ScopePropValue {
Text(String),
Bytes(Vec<u8>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct DeviationScope {
pub kind: String,
pub props: Vec<(String, ScopePropValue)>,
}
impl DeviationScope {
#[must_use]
pub fn any() -> Self {
Self {
kind: SCOPE_KIND_ANY.to_string(),
props: Vec::new(),
}
}
#[must_use]
pub fn issuer_dn_contains(substring: impl Into<String>) -> Self {
Self {
kind: SCOPE_KIND_ISSUER_DN_CONTAINS.to_string(),
props: vec![(
PROP_ISSUER_DN_SUBSTRING.to_string(),
ScopePropValue::Text(substring.into()),
)],
}
}
#[must_use]
pub fn issuer_dn_exact(issuer_der: impl Into<Vec<u8>>) -> Self {
Self {
kind: SCOPE_KIND_ISSUER_DN_EXACT.to_string(),
props: vec![(
PROP_ISSUER_DN_DER.to_string(),
ScopePropValue::Bytes(issuer_der.into()),
)],
}
}
#[must_use]
pub fn serial_range(
issuer_der: impl Into<Vec<u8>>,
start: Vec<u8>,
end: Vec<u8>,
) -> Self {
Self {
kind: SCOPE_KIND_SERIAL_RANGE.to_string(),
props: vec![
(
PROP_ISSUER_DN_DER.to_string(),
ScopePropValue::Bytes(issuer_der.into()),
),
(PROP_SERIAL_START.to_string(), ScopePropValue::Bytes(start)),
(PROP_SERIAL_END.to_string(), ScopePropValue::Bytes(end)),
],
}
}
#[must_use]
pub fn get_prop(&self, name: &str) -> Option<&ScopePropValue> {
self.props
.iter()
.find_map(|(k, v)| (k == name).then_some(v))
}
fn get_text(&self, name: &str) -> Option<&str> {
match self.get_prop(name)? {
ScopePropValue::Text(s) => Some(s.as_str()),
ScopePropValue::Bytes(_) => None,
}
}
fn get_bytes(&self, name: &str) -> Option<&[u8]> {
match self.get_prop(name)? {
ScopePropValue::Bytes(b) => Some(b.as_slice()),
ScopePropValue::Text(_) => None,
}
}
#[must_use]
pub fn matches(&self, cert: &Certificate) -> bool {
match self.kind.as_str() {
SCOPE_KIND_ANY => true,
SCOPE_KIND_ISSUER_DN_CONTAINS => {
let Some(substring) = self.get_text(PROP_ISSUER_DN_SUBSTRING) else {
return false;
};
let issuer_str = cert.tbs_certificate.issuer.to_string().to_lowercase();
issuer_str.contains(substring)
}
SCOPE_KIND_ISSUER_DN_EXACT => {
let Some(der) = self.get_bytes(PROP_ISSUER_DN_DER) else {
return false;
};
use der::Decode as _;
let Ok(name) = x509_cert::name::Name::from_der(der) else {
return false;
};
pkix_path::names_match(&name, &cert.tbs_certificate.issuer)
}
SCOPE_KIND_SERIAL_RANGE => {
let Some(der) = self.get_bytes(PROP_ISSUER_DN_DER) else {
return false;
};
let Some(start) = self.get_bytes(PROP_SERIAL_START) else {
return false;
};
let Some(end) = self.get_bytes(PROP_SERIAL_END) else {
return false;
};
use der::Decode as _;
let Ok(issuer) = x509_cert::name::Name::from_der(der) else {
return false;
};
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()
}
_ => false,
}
}
}
fn validate_scope(scope: &DeviationScope) -> Result<(), DeviationAddError> {
let missing = |prop: &str| -> DeviationAddError {
DeviationAddError::MalformedScope {
kind: scope.kind.clone(),
reason: format!("missing required prop '{prop}'"),
}
};
let wrong_type = |prop: &str, expected: &str| -> DeviationAddError {
DeviationAddError::MalformedScope {
kind: scope.kind.clone(),
reason: format!("prop '{prop}' has wrong type (expected {expected})"),
}
};
match scope.kind.as_str() {
SCOPE_KIND_ANY => Ok(()),
SCOPE_KIND_ISSUER_DN_CONTAINS => match scope.get_prop(PROP_ISSUER_DN_SUBSTRING) {
None => Err(missing(PROP_ISSUER_DN_SUBSTRING)),
Some(ScopePropValue::Text(_)) => Ok(()),
Some(_) => Err(wrong_type(PROP_ISSUER_DN_SUBSTRING, "Text")),
},
SCOPE_KIND_ISSUER_DN_EXACT => match scope.get_prop(PROP_ISSUER_DN_DER) {
None => Err(missing(PROP_ISSUER_DN_DER)),
Some(ScopePropValue::Bytes(_)) => Ok(()),
Some(_) => Err(wrong_type(PROP_ISSUER_DN_DER, "Bytes")),
},
SCOPE_KIND_SERIAL_RANGE => {
for prop in [PROP_ISSUER_DN_DER, PROP_SERIAL_START, PROP_SERIAL_END] {
match scope.get_prop(prop) {
None => return Err(missing(prop)),
Some(ScopePropValue::Bytes(_)) => {}
Some(_) => return Err(wrong_type(prop, "Bytes")),
}
}
Ok(())
}
_ => Ok(()),
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ScopePropValue {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStructVariant as _;
match self {
Self::Text(s) => serializer.serialize_newtype_variant("ScopePropValue", 0, "Text", s),
Self::Bytes(b) => {
let mut sv =
serializer.serialize_struct_variant("ScopePropValue", 1, "Bytes", 1)?;
sv.serialize_field("hex", &hex_encode(b))?;
sv.end()
}
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for DeviationScope {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct as _;
let mut st = serializer.serialize_struct("DeviationScope", 2)?;
st.serialize_field("kind", &self.kind)?;
st.serialize_field("props", &self.props)?;
st.end()
}
}
#[cfg(feature = "serde")]
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
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))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
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, PartialEq, Eq)]
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()));
}
validate_scope(&deviation.scope)?;
if deviation.scope.kind == SCOPE_KIND_ISSUER_DN_CONTAINS {
for (name, value) in &mut deviation.scope.props {
if name == PROP_ISSUER_DN_SUBSTRING {
if let ScopePropValue::Text(s) = value {
*s = s.to_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> {
let mut best: Option<&Deviation> = None;
for d in &self.deviations {
if d.target_lint.as_str() == lint_id && d.applies_to(cert, now_unix) {
match best {
None => best = Some(d),
Some(prev) if d.priority > prev.priority => best = Some(d),
Some(_) => {}
}
}
}
best
}
#[must_use]
pub fn find_deviation_for_chain(
&self,
lint_id: &str,
chain: &[Certificate],
now_unix: u64,
) -> Option<&Deviation> {
let mut best: Option<&Deviation> = None;
for d in &self.deviations {
if d.target_lint.as_str() == lint_id
&& chain.iter().any(|cert| d.applies_to(cert, now_unix))
{
match best {
None => best = Some(d),
Some(prev) if d.priority > prev.priority => best = Some(d),
Some(_) => {}
}
}
}
best
}
}
#[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 {
assert_eq!(
kinds.len(),
chain.len(),
"DeviationRunner::run_chain requires kinds.len() == chain.len() \
(got kinds={}, chain={}); see PKIX-7f92.9.",
kinds.len(),
chain.len(),
);
let mut result = DeviationRunResult::default();
for (i, cert) in chain.iter().enumerate() {
let kind = kinds[i];
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);
self.apply_deviations_for_chain(raw, chain, now_unix)
}
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(make_deviated(finding, dev));
}
}
}
result
}
fn apply_deviations_for_chain(
&self,
raw: Vec<crate::Finding>,
chain: &[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_for_chain(&finding.lint_id, chain, now_unix)
{
None => result.findings.push(finding),
Some(dev) => result.deviated.push(make_deviated(finding, dev)),
}
}
result
}
}
fn make_deviated(finding: crate::Finding, dev: &Deviation) -> DeviatedFinding {
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,
}
}
#[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,
priority: 0,
}
}
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::issuer_dn_contains(word.to_lowercase());
let scope_upper = DeviationScope::issuer_dn_contains(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_pinned_multi_rdn_behavior() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../pkix-path/tests/pkits/certs/GoodCACert.crt");
let Ok(bytes) = std::fs::read(&path) else {
eprintln!("PKITS GoodCACert not available — skipping multi-RDN regression test");
return;
};
use der::Decode as _;
let cert = Certificate::from_der(&bytes).expect("decode PKITS GoodCACert");
let rendered = cert.tbs_certificate.issuer.to_string();
assert_eq!(
rendered, "CN=Trust Anchor,O=Test Certificates 2011,C=US",
"test oracle: x509-cert renders the PKITS GoodCACert issuer in CN-first RFC 4514 form"
);
let lower = rendered.to_lowercase();
for substring in ["trust anchor", "test certificates 2011", "c=us"] {
assert!(
lower.contains(substring),
"rendered lower must contain {substring:?} (oracle)"
);
let scope = DeviationScope::issuer_dn_contains(substring);
assert!(
scope.matches(&cert),
"single-value substring {substring:?} must match"
);
}
let cross = "trust anchor,o=test certificates";
assert!(
lower.contains(cross),
"rendered lower must literally contain {cross:?}"
);
let scope_cross = DeviationScope::issuer_dn_contains(cross);
assert!(
scope_cross.matches(&cert),
"cross-RDN substring matches when it tracks the renderer literally"
);
let cross_with_space = "trust anchor, o=test certificates";
assert!(
!lower.contains(cross_with_space),
"rendered lower does NOT contain {cross_with_space:?} — renderer omits space after comma"
);
let scope_space = DeviationScope::issuer_dn_contains(cross_with_space);
assert!(
!scope_space.matches(&cert),
"space-after-comma substring must not match (renderer-coupling hazard)"
);
let reordered = "c=us,o=test certificates 2011,cn=trust anchor";
let scope_reordered = DeviationScope::issuer_dn_contains(reordered);
assert!(
!scope_reordered.matches(&cert),
"C-first-ordered substring must not match CN-first rendering"
);
let long_name = "countryname=us";
let scope_long = DeviationScope::issuer_dn_contains(long_name);
assert!(
!scope_long.matches(&cert),
"long attribute name must not match x509-cert's short-name rendering"
);
}
#[test]
fn scope_issuer_dn_contains_no_match() {
let cert = load_cert();
let scope = DeviationScope::issuer_dn_contains("XYZ_NONEXISTENT_ISSUER_9999");
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::issuer_dn_contains(uppercase_word.clone()),
..make_deviation("norm-test", "test.lint")
};
store.add(deviation).expect("add must succeed");
let stored = &store.all()[0].scope;
assert_eq!(stored.kind, SCOPE_KIND_ISSUER_DN_CONTAINS);
let stored_substring = match stored.get_prop(PROP_ISSUER_DN_SUBSTRING) {
Some(ScopePropValue::Text(s)) => s,
other => panic!("expected Text substring prop, got {other:?}"),
};
assert_eq!(
*stored_substring,
uppercase_word.to_lowercase(),
"DeviationStore::add must lowercase issuer-dn-substring prop"
);
assert!(
stored.matches(&cert),
"normalized issuer-dn-contains scope must match cert"
);
}
#[test]
fn case_folding_is_unicode_aware_for_store_normalization() {
let mut store = DeviationStore::new();
let deviation = Deviation {
scope: DeviationScope::issuer_dn_contains("MÜLLER"),
..make_deviation("muller-test", "test.lint")
};
store.add(deviation).expect("add must succeed");
let stored = &store.all()[0].scope;
let stored_substring = match stored.get_prop(PROP_ISSUER_DN_SUBSTRING) {
Some(ScopePropValue::Text(s)) => s,
other => panic!("expected Text substring prop, got {other:?}"),
};
assert_eq!(
*stored_substring, "müller",
"DeviationStore::add must Unicode-lowercase the issuer-dn-substring; \
pre-fix make_ascii_lowercase produced \"mÜller\" (Ü untouched)"
);
}
#[test]
fn case_folding_is_unicode_aware_via_to_lowercase() {
assert_eq!("Müller".to_lowercase(), "müller");
assert_eq!("MÜLLER".to_lowercase(), "müller");
let mut s = String::from("MÜLLER");
s.make_ascii_lowercase();
assert_eq!(s, "mÜller", "ASCII-only fold is documented to leave non-ASCII alone");
}
#[test]
fn scope_issuer_dn_exact_matches_cert_issuer() {
use der::Encode as _;
let cert = load_cert();
let issuer_der = cert
.tbs_certificate
.issuer
.to_der()
.expect("Name::to_der is infallible for a parsed Name");
let scope = DeviationScope::issuer_dn_exact(issuer_der);
assert!(
scope.matches(&cert),
"issuer_dn_exact with cert's own issuer must match"
);
}
#[test]
fn scope_issuer_dn_exact_does_not_match_different_dn() {
use der::{Decode as _, Encode 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 other_issuer_der = other_cert
.tbs_certificate
.issuer
.to_der()
.expect("Name::to_der is infallible for a parsed Name");
let scope = DeviationScope::issuer_dn_exact(other_issuer_der);
let same = pkix_path::names_match(
&cert.tbs_certificate.issuer,
&other_cert.tbs_certificate.issuer,
);
if !same {
assert!(
!scope.matches(&cert),
"issuer_dn_exact 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() {
use der::Encode as _;
let cert = load_cert();
let serial = cert.tbs_certificate.serial_number.as_bytes().to_vec();
let issuer_der = cert
.tbs_certificate
.issuer
.to_der()
.expect("Name::to_der is infallible for a parsed Name");
let scope = DeviationScope::serial_range(issuer_der, serial.clone(), serial);
assert!(
scope.matches(&cert),
"cert's own serial must be within [serial, serial]"
);
}
#[test]
fn scope_serial_range_excludes_cert_outside_range() {
use der::Encode as _;
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 issuer_der = cert
.tbs_certificate
.issuer
.to_der()
.expect("Name::to_der is infallible for a parsed Name");
let scope = DeviationScope::serial_range(issuer_der, 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 _, Encode 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 other_issuer_der = other_cert
.tbs_certificate
.issuer
.to_der()
.expect("Name::to_der is infallible for a parsed Name");
let scope = DeviationScope::serial_range(
other_issuer_der,
vec![0x00],
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 serial_range must not match"
);
}
}
#[test]
fn scope_unknown_kind_fails_closed() {
let cert = load_cert();
let scope = DeviationScope {
kind: "pkix-lint.scope.future-axis-not-yet-defined".to_string(),
props: vec![],
};
assert!(
!scope.matches(&cert),
"unknown kind must fail-closed (return false)"
);
}
#[test]
fn scope_issuer_dn_contains_missing_prop_fails_closed() {
let cert = load_cert();
let scope = DeviationScope {
kind: SCOPE_KIND_ISSUER_DN_CONTAINS.to_string(),
props: vec![],
};
assert!(
!scope.matches(&cert),
"missing substring prop must fail-closed"
);
}
#[test]
fn scope_issuer_dn_exact_wrong_typed_prop_fails_closed() {
let cert = load_cert();
let scope = DeviationScope {
kind: SCOPE_KIND_ISSUER_DN_EXACT.to_string(),
props: vec![(
PROP_ISSUER_DN_DER.to_string(),
ScopePropValue::Text("not bytes".to_string()),
)],
};
assert!(
!scope.matches(&cert),
"wrong-typed issuer-dn-der prop must fail-closed"
);
}
#[test]
fn scope_issuer_dn_exact_malformed_der_fails_closed() {
let cert = load_cert();
let scope = DeviationScope {
kind: SCOPE_KIND_ISSUER_DN_EXACT.to_string(),
props: vec![(
PROP_ISSUER_DN_DER.to_string(),
ScopePropValue::Bytes(vec![0xFF, 0xFE, 0xFD]),
)],
};
assert!(
!scope.matches(&cert),
"malformed issuer-dn-der bytes must fail-closed"
);
}
#[test]
fn scope_constructor_kinds_match_constants() {
use der::Encode as _;
assert_eq!(DeviationScope::any().kind, SCOPE_KIND_ANY);
assert_eq!(
DeviationScope::issuer_dn_contains("x").kind,
SCOPE_KIND_ISSUER_DN_CONTAINS
);
let cert = load_cert();
let issuer_der = cert
.tbs_certificate
.issuer
.to_der()
.expect("Name::to_der is infallible for a parsed Name");
let exact = DeviationScope::issuer_dn_exact(issuer_der.clone());
assert_eq!(exact.kind, SCOPE_KIND_ISSUER_DN_EXACT);
let range = DeviationScope::serial_range(issuer_der, vec![0x01], vec![0x02]);
assert_eq!(range.kind, SCOPE_KIND_SERIAL_RANGE);
}
#[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 new_rejects_empty_justification() {
let err = Deviation::new(
"d1",
"test.lint",
DeviationScope::any(),
DeviationAction::Suppress,
"",
"ops@example.com",
)
.expect_err("empty justification must fail at construction");
assert_eq!(err, DeviationAddError::EmptyField("justification".into()));
}
#[test]
fn new_rejects_empty_authorized_by() {
let err = Deviation::new(
"d1",
"test.lint",
DeviationScope::any(),
DeviationAction::Suppress,
"valid justification",
"",
)
.expect_err("empty authorized_by must fail at construction");
assert_eq!(err, DeviationAddError::EmptyField("authorized_by".into()));
}
#[test]
fn new_accepts_non_empty_fields_and_add_succeeds() {
let dev = Deviation::new(
"d1",
"test.lint",
DeviationScope::any(),
DeviationAction::Suppress,
"valid justification",
"ops@example.com",
)
.expect("non-empty fields");
let mut store = DeviationStore::new();
store.add(dev).expect("add must succeed");
}
#[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_rejects_issuer_dn_contains_missing_substring_prop() {
let mut store = DeviationStore::new();
let bad = Deviation {
scope: DeviationScope {
kind: SCOPE_KIND_ISSUER_DN_CONTAINS.to_string(),
props: vec![], },
..make_deviation("malformed", "test.lint")
};
let err = store.add(bad).expect_err("malformed scope must be rejected");
match err {
DeviationAddError::MalformedScope { kind, reason } => {
assert_eq!(kind, SCOPE_KIND_ISSUER_DN_CONTAINS);
assert!(
reason.contains(PROP_ISSUER_DN_SUBSTRING),
"reason must name the missing prop; got: {reason}"
);
}
other => panic!("expected MalformedScope, got: {other:?}"),
}
}
#[test]
fn store_rejects_issuer_dn_contains_wrong_typed_substring_prop() {
let mut store = DeviationStore::new();
let bad = Deviation {
scope: DeviationScope {
kind: SCOPE_KIND_ISSUER_DN_CONTAINS.to_string(),
props: vec![(
PROP_ISSUER_DN_SUBSTRING.to_string(),
ScopePropValue::Bytes(vec![0x00]), )],
},
..make_deviation("malformed", "test.lint")
};
let err = store.add(bad).expect_err("wrong-typed prop must be rejected");
match err {
DeviationAddError::MalformedScope { kind, reason } => {
assert_eq!(kind, SCOPE_KIND_ISSUER_DN_CONTAINS);
assert!(
reason.contains("Text"),
"reason must name the expected type; got: {reason}"
);
}
other => panic!("expected MalformedScope, got: {other:?}"),
}
}
#[test]
fn store_rejects_issuer_dn_exact_missing_der_prop() {
let mut store = DeviationStore::new();
let bad = Deviation {
scope: DeviationScope {
kind: SCOPE_KIND_ISSUER_DN_EXACT.to_string(),
props: vec![], },
..make_deviation("malformed", "test.lint")
};
let err = store.add(bad).expect_err("malformed scope must be rejected");
assert!(matches!(err, DeviationAddError::MalformedScope { .. }));
}
#[test]
fn store_rejects_serial_range_missing_serial_end_prop() {
let mut store = DeviationStore::new();
let bad = Deviation {
scope: DeviationScope {
kind: SCOPE_KIND_SERIAL_RANGE.to_string(),
props: vec![
(
PROP_ISSUER_DN_DER.to_string(),
ScopePropValue::Bytes(vec![0x30, 0x00]),
),
(
PROP_SERIAL_START.to_string(),
ScopePropValue::Bytes(vec![0x01]),
),
],
},
..make_deviation("malformed", "test.lint")
};
let err = store.add(bad).expect_err("malformed scope must be rejected");
match err {
DeviationAddError::MalformedScope { reason, .. } => {
assert!(
reason.contains(PROP_SERIAL_END),
"reason must name the missing prop; got: {reason}"
);
}
other => panic!("expected MalformedScope, got: {other:?}"),
}
}
#[test]
fn store_accepts_well_formed_scopes_from_constructors() {
let mut store = DeviationStore::new();
store
.add(Deviation {
scope: DeviationScope::any(),
..make_deviation("d-any", "lint.a")
})
.expect("any() scope must be well-formed");
store
.add(Deviation {
scope: DeviationScope::issuer_dn_contains("foo"),
..make_deviation("d-contains", "lint.b")
})
.expect("issuer_dn_contains() scope must be well-formed");
}
#[test]
fn store_accepts_custom_scope_kind_without_inspection() {
let mut store = DeviationStore::new();
let custom = Deviation {
scope: DeviationScope {
kind: "custom.policy-bundle.org/some-axis".to_string(),
props: vec![],
},
..make_deviation("d-custom", "lint.c")
};
store.add(custom).expect(
"custom scope kinds are caller-defined extensibility; \
validate_scope must not reject them",
);
}
#[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());
}
fn load_cert_at(path: &str) -> Certificate {
use der::Decode as _;
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {path}: {e}"));
Certificate::from_der(&bytes).unwrap_or_else(|e| panic!("decode {path}: {e}"))
}
fn cert_webpki() -> Certificate {
load_cert_at("../pkix-path/tests/fixtures/policy-checks/webpki-self-signed-365d.der")
}
fn cert_smime() -> Certificate {
load_cert_at("../pkix-path/tests/fixtures/policy-checks/smime-self-signed-365d.der")
}
#[test]
fn find_deviation_for_chain_matches_when_intermediate_in_scope() {
let leaf = cert_webpki();
let intermediate = cert_smime();
let chain = [leaf, intermediate];
let mut store = DeviationStore::new();
store
.add(Deviation {
scope: DeviationScope::issuer_dn_contains("smime"),
..make_deviation("dev-intermediate-scope", "test.path.lint")
})
.expect("add must succeed");
let found = store.find_deviation_for_chain("test.path.lint", &chain, 1_000_000);
let dev = found.expect(
"deviation scoped to intermediate-cert issuer DN must match via chain[1]; \
pre-fix run_path / find_deviation only on chain[0] would miss this",
);
assert_eq!(dev.id, "dev-intermediate-scope");
}
#[test]
fn find_deviation_for_chain_returns_none_when_no_cert_in_scope() {
let leaf = cert_webpki();
let intermediate = cert_smime();
let chain = [leaf, intermediate];
let mut store = DeviationStore::new();
store
.add(Deviation {
scope: DeviationScope::issuer_dn_contains("xyz-nonexistent-dn"),
..make_deviation("dev-no-match", "test.path.lint")
})
.expect("add must succeed");
assert!(
store
.find_deviation_for_chain("test.path.lint", &chain, 1_000_000)
.is_none(),
"deviation scoped to a non-matching substring must not fire on any chain cert"
);
}
#[test]
fn find_deviation_higher_priority_wins() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(
Deviation::new(
"wildcard",
"test.lint",
DeviationScope::any(),
DeviationAction::Suppress,
"wildcard waiver",
"ops@example.com",
)
.expect("non-empty fields")
.with_priority(0),
)
.expect("add wildcard");
store
.add(
Deviation::new(
"specific",
"test.lint",
DeviationScope::any(),
DeviationAction::DowngradeSeverityTo(Severity::Info),
"lab-specific waiver",
"lab-lead@example.com",
)
.expect("non-empty fields")
.with_priority(100),
)
.expect("add specific");
let found = store.find_deviation("test.lint", &cert, now);
let dev = found.expect("at least one deviation must match");
assert_eq!(
dev.id, "specific",
"higher priority must win regardless of insertion order; \
got dev.id={} priority={}",
dev.id, dev.priority
);
}
#[test]
fn find_deviation_priority_tie_breaks_by_insertion_order() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(Deviation {
id: "first".to_string(),
priority: 50,
..make_deviation("first", "test.lint")
})
.expect("add first");
store
.add(Deviation {
id: "second".to_string(),
priority: 50,
..make_deviation("second", "test.lint")
})
.expect("add second");
let found = store.find_deviation("test.lint", &cert, now);
assert_eq!(
found.expect("at least one match").id,
"first",
"insertion order is the documented tie-breaker for equal priority"
);
}
#[test]
fn find_deviation_negative_priority_loses_to_default() {
let cert = load_cert();
let now: u64 = 1_000_000;
let mut store = DeviationStore::new();
store
.add(Deviation {
id: "fallback".to_string(),
priority: -100,
..make_deviation("fallback", "test.lint")
})
.expect("add fallback");
store
.add(Deviation {
id: "normal".to_string(),
priority: 0,
..make_deviation("normal", "test.lint")
})
.expect("add normal");
let found = store.find_deviation("test.lint", &cert, now);
assert_eq!(
found.expect("at least one match").id,
"normal",
"default priority 0 must outrank negative -100"
);
}
#[test]
fn find_deviation_for_chain_first_match_wins_in_store_order() {
let leaf = cert_webpki();
let intermediate = cert_smime();
let chain = [leaf, intermediate];
let mut store = DeviationStore::new();
store
.add(Deviation {
id: "dev-first".to_string(),
scope: DeviationScope::issuer_dn_contains("smime"),
..make_deviation("dev-first", "test.path.lint")
})
.expect("add must succeed");
store
.add(Deviation {
id: "dev-second".to_string(),
scope: DeviationScope::issuer_dn_contains("webpki"),
..make_deviation("dev-second", "test.path.lint")
})
.expect("add must succeed");
let found = store
.find_deviation_for_chain("test.path.lint", &chain, 1_000_000)
.expect("at least one deviation must match");
assert_eq!(
found.id, "dev-first",
"store-insertion order is the tie-breaker, not chain-iteration order"
);
}
#[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);
}
#[derive(Clone)]
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")
}
}
#[derive(Clone)]
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);
}
}