use serde::{Deserialize, Serialize};
pub const FINDING_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum OriginStage {
Scan,
Audit,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum DialTier {
Suspected,
Named,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Provenance {
Encountered,
Constructable,
Heuristic,
Imagined,
}
impl Provenance {
pub const DEFAULT: Self = Self::Imagined;
#[must_use]
pub fn from_variant_str(s: &str) -> Option<Self> {
match s {
"Encountered" => Some(Self::Encountered),
"Constructable" => Some(Self::Constructable),
"Heuristic" => Some(Self::Heuristic),
"Imagined" => Some(Self::Imagined),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Presentation {
Passive,
Active,
}
impl Presentation {
pub const DEFAULT: Self = Self::Passive;
#[must_use]
pub fn from_variant_str(s: &str) -> Option<Self> {
match s {
"Passive" => Some(Self::Passive),
"Active" => Some(Self::Active),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Severity {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Magnitude {
Smell,
Aura,
Dread,
}
impl Magnitude {
#[must_use]
pub fn from_variant_str(s: &str) -> Option<Self> {
match s {
"smell" => Some(Self::Smell),
"aura" => Some(Self::Aura),
"dread" => Some(Self::Dread),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum ExistenceCertainty {
Unsure,
Sure,
}
impl ExistenceCertainty {
#[must_use]
pub fn from_variant_str(s: &str) -> Option<Self> {
match s {
"unsure" => Some(Self::Unsure),
"sure" => Some(Self::Sure),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum FindingBody {
MarkedUnknown {
magnitude: Magnitude,
existence_certainty: ExistenceCertainty,
trigger: String,
},
DialVerdict {
class: String,
tier: DialTier,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Finding {
#[serde(default = "default_schema_version")]
pub schema_version: u32,
pub file: String,
pub line: usize,
#[serde(default)]
pub structural_digest: String,
pub cluster_key: String,
pub severity: Severity,
pub source: String,
pub class_provenance: Provenance,
pub presentation: Presentation,
pub timestamp: u64,
pub origin_stage: OriginStage,
pub body: FindingBody,
}
const fn default_schema_version() -> u32 {
FINDING_SCHEMA_VERSION
}
#[must_use]
pub fn cluster_key_of(structural_digest: &str, class: &str) -> String {
format!("{class}@{structural_digest}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finding_serializes_kebab_cased_and_round_trips() {
let f = Finding {
schema_version: FINDING_SCHEMA_VERSION,
file: "lib.rs".into(),
line: 42,
structural_digest: "fnv:drop-guard-before-flush".into(),
cluster_key: cluster_key_of("fnv:drop-guard-before-flush", "dread"),
severity: Severity::High,
source: "scan:declaration".into(),
class_provenance: Provenance::Encountered,
presentation: Presentation::Active,
timestamp: 500,
origin_stage: OriginStage::Scan,
body: FindingBody::MarkedUnknown {
magnitude: Magnitude::Dread,
existence_certainty: ExistenceCertainty::Unsure,
trigger: "the teardown drops the guard before the flush".into(),
},
};
let json = serde_json::to_string(&f).expect("Finding serializes");
assert!(json.contains("\"origin_stage\":\"scan\""));
assert!(json.contains("\"class_provenance\":\"encountered\""));
assert!(json.contains("\"presentation\":\"active\""));
assert!(json.contains("\"kind\":\"marked-unknown\""));
let back: Finding = serde_json::from_str(&json).expect("Finding round-trips");
assert_eq!(back, f);
}
#[test]
fn schema_version_defaults_on_older_record() {
let older = r#"{
"file": "a.rs", "line": 1, "cluster_key": "C@d", "severity": "low",
"source": "scan:imagined-repertoire", "class_provenance": "imagined",
"presentation": "passive", "timestamp": 0, "origin_stage": "scan",
"body": {"kind": "dial-verdict", "class": "C", "tier": "suspected"}
}"#;
let f: Finding = serde_json::from_str(older).expect("older record deserializes");
assert_eq!(f.schema_version, FINDING_SCHEMA_VERSION);
assert_eq!(f.structural_digest, ""); assert_eq!(f.class_provenance, Provenance::Imagined);
assert_eq!(f.presentation, Presentation::Passive);
}
#[test]
fn provenance_default_is_the_floor_not_the_ceiling() {
assert_eq!(Provenance::DEFAULT, Provenance::Imagined);
assert_ne!(Provenance::DEFAULT, Provenance::Encountered);
assert_ne!(Provenance::DEFAULT, Provenance::Constructable);
assert_eq!(Presentation::DEFAULT, Presentation::Passive);
}
#[test]
fn provenance_from_variant_str_round_trips_and_rejects_unknown() {
for (s, want) in [
("Encountered", Provenance::Encountered),
("Constructable", Provenance::Constructable),
("Heuristic", Provenance::Heuristic),
("Imagined", Provenance::Imagined),
] {
assert_eq!(Provenance::from_variant_str(s), Some(want));
}
assert_eq!(Provenance::from_variant_str("Bogus"), None);
assert_eq!(Provenance::from_variant_str("heuristic"), None);
assert_eq!(
Presentation::from_variant_str("Passive"),
Some(Presentation::Passive)
);
assert_eq!(
Presentation::from_variant_str("Active"),
Some(Presentation::Active)
);
assert_eq!(Presentation::from_variant_str("Bogus"), None);
}
#[test]
fn magnitude_and_existence_certainty_from_variant_str() {
assert_eq!(Magnitude::from_variant_str("smell"), Some(Magnitude::Smell));
assert_eq!(Magnitude::from_variant_str("aura"), Some(Magnitude::Aura));
assert_eq!(Magnitude::from_variant_str("dread"), Some(Magnitude::Dread));
assert_eq!(Magnitude::from_variant_str("bogus"), None);
assert_eq!(
ExistenceCertainty::from_variant_str("unsure"),
Some(ExistenceCertainty::Unsure)
);
assert_eq!(
ExistenceCertainty::from_variant_str("sure"),
Some(ExistenceCertainty::Sure)
);
assert_eq!(ExistenceCertainty::from_variant_str("bogus"), None);
}
}