use crate::{Explanation, Remediation, RuleKind, Severity};
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum DetectionMode {
MatcherOnly,
Deterministic,
Contextual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RuleMetadata<'a> {
id: &'a str,
kind: RuleKind,
detection_mode: DetectionMode,
severity: Severity,
remediation: Option<Remediation>,
}
impl<'a> RuleMetadata<'a> {
#[must_use]
pub const fn new(
id: &'a str,
kind: RuleKind,
detection_mode: DetectionMode,
severity: Severity,
remediation: Option<Remediation>,
) -> Self {
Self {
id,
kind,
detection_mode,
severity,
remediation,
}
}
#[must_use]
pub const fn id(&self) -> &'a str {
self.id
}
#[must_use]
pub const fn kind(&self) -> RuleKind {
self.kind
}
#[must_use]
pub const fn detection_mode(&self) -> DetectionMode {
self.detection_mode
}
#[must_use]
pub const fn explanation(&self) -> Explanation {
Explanation::classified(self.detection_mode)
}
#[must_use]
pub const fn severity(&self) -> Severity {
self.severity
}
#[must_use]
pub const fn remediation(&self) -> Option<Remediation> {
self.remediation
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exposes_presentation_safe_rule_metadata() {
let metadata = RuleMetadata::new(
"github.token",
RuleKind::Prefix,
DetectionMode::Deterministic,
Severity::High,
Some(Remediation::RotateCredential),
);
assert_eq!(metadata.id(), "github.token");
assert_eq!(metadata.kind(), RuleKind::Prefix);
assert_eq!(metadata.detection_mode(), DetectionMode::Deterministic);
assert_eq!(
metadata.explanation(),
Explanation::Classified(DetectionMode::Deterministic)
);
assert_eq!(metadata.severity(), Severity::High);
assert_eq!(metadata.remediation(), Some(Remediation::RotateCredential));
}
#[test]
fn explanation_tracks_detection_mode_without_parallel_state() {
for mode in [
DetectionMode::MatcherOnly,
DetectionMode::Deterministic,
DetectionMode::Contextual,
] {
let metadata =
RuleMetadata::new("rule", RuleKind::Pattern, mode, Severity::Medium, None);
assert_eq!(metadata.explanation(), Explanation::classified(mode));
assert_eq!(metadata.explanation().detection_mode(), Some(mode));
assert!(metadata.explanation().is_classified());
assert!(!metadata.explanation().is_ambiguous());
}
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trip_preserves_metadata() {
let metadata = RuleMetadata::new(
"stripe.secret",
RuleKind::Prefix,
DetectionMode::Deterministic,
Severity::Critical,
Some(Remediation::RotateCredential),
);
let json = serde_json::to_string(&metadata).expect("serialize metadata");
let decoded: RuleMetadata<'_> = serde_json::from_str(&json).expect("deserialize metadata");
assert_eq!(decoded, metadata);
}
}