Skip to main content

libverify_core/
profile.rs

1use std::collections::BTreeMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6use crate::control::{ControlFinding, ControlId};
7
8/// Policy-specific display labels for severity levels.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct SeverityLabels {
11    pub info: String,
12    pub warning: String,
13    pub error: String,
14}
15
16impl Default for SeverityLabels {
17    fn default() -> Self {
18        Self {
19            info: "compliant".to_string(),
20            warning: "observation".to_string(),
21            error: "exception".to_string(),
22        }
23    }
24}
25
26impl SeverityLabels {
27    pub fn label_for(&self, severity: FindingSeverity) -> &str {
28        match severity {
29            FindingSeverity::Info => &self.info,
30            FindingSeverity::Warning => &self.warning,
31            FindingSeverity::Error => &self.error,
32        }
33    }
34}
35
36/// Severity level assigned to a control finding by a profile.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum FindingSeverity {
40    Info,
41    Warning,
42    Error,
43}
44
45/// Gate outcome that determines whether a pipeline stage may proceed.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum GateDecision {
49    Pass,
50    Review,
51    Fail,
52}
53
54impl GateDecision {
55    pub fn as_str(&self) -> &'static str {
56        match self {
57            Self::Pass => "pass",
58            Self::Review => "review",
59            Self::Fail => "fail",
60        }
61    }
62}
63
64impl fmt::Display for GateDecision {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(self.as_str())
67    }
68}
69
70/// The profile-mapped result for a single control finding.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct ProfileOutcome {
73    pub control_id: ControlId,
74    pub severity: FindingSeverity,
75    pub decision: GateDecision,
76    pub rationale: String,
77    /// Policy-supplied key-value annotations (e.g. framework_ref, recommendation).
78    /// Empty by default for backward compatibility with existing Rego presets.
79    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
80    pub annotations: BTreeMap<String, String>,
81}
82
83/// Maps raw control findings to severity and gate decisions for a given policy.
84pub trait ControlProfile {
85    fn name(&self) -> &str;
86    fn map(&self, finding: &ControlFinding) -> ProfileOutcome;
87    fn severity_labels(&self) -> SeverityLabels {
88        SeverityLabels::default()
89    }
90}
91
92/// Applies a profile to all findings and returns the mapped outcomes.
93pub fn apply_profile(
94    profile: &dyn ControlProfile,
95    findings: &[ControlFinding],
96) -> Vec<ProfileOutcome> {
97    findings
98        .iter()
99        .map(|finding| profile.map(finding))
100        .collect()
101}