skill_veil_core/policy/
baseline.rs1use super::default_policy_schema_version;
2use crate::findings::OperationalContext;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct BaselineFile {
9 #[serde(default = "default_policy_schema_version")]
10 pub schema_version: String,
11 #[serde(default)]
18 pub entries: Vec<BaselineEntry>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct BaselineEntry {
24 pub fingerprint: String,
25 pub rule_id: String,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub artifact_path: Option<String>,
28 pub reason: String,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct WaiverFile {
34 #[serde(default = "default_policy_schema_version")]
35 pub schema_version: String,
36 #[serde(default)]
40 pub waivers: Vec<WaiverEntry>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct WaiverEntry {
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub rule_id: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub artifact_path: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub context: Option<OperationalContext>,
52 pub reason: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub expires_at: Option<DateTime<Utc>>,
55}
56
57#[cfg(test)]
58mod serde_default_tests {
59 use super::*;
60 use crate::policy::POLICY_SCHEMA_VERSION;
61
62 #[test]
73 fn baseline_file_deserializes_without_entries_key() {
74 let yaml = format!("schema_version: {POLICY_SCHEMA_VERSION}\n");
75 let parsed: BaselineFile =
76 serde_yaml::from_str(&yaml).expect("BaselineFile MUST load when `entries:` is omitted");
77 assert!(
78 parsed.entries.is_empty(),
79 "missing `entries:` key MUST default to an empty Vec"
80 );
81 assert_eq!(parsed.schema_version, POLICY_SCHEMA_VERSION);
82 }
83
84 #[test]
93 fn waiver_file_deserializes_without_waivers_key() {
94 let yaml = format!("schema_version: {POLICY_SCHEMA_VERSION}\n");
95 let parsed: WaiverFile =
96 serde_yaml::from_str(&yaml).expect("WaiverFile MUST load when `waivers:` is omitted");
97 assert!(
98 parsed.waivers.is_empty(),
99 "missing `waivers:` key MUST default to an empty Vec"
100 );
101 assert_eq!(parsed.schema_version, POLICY_SCHEMA_VERSION);
102 }
103
104 #[test]
111 fn baseline_file_still_loads_explicit_entries() {
112 let yaml = format!(
113 "schema_version: {POLICY_SCHEMA_VERSION}\n\
114 entries:\n \
115 - fingerprint: deadbeef\n \
116 rule_id: RULE_A\n \
117 reason: pre-existing\n"
118 );
119 let parsed: BaselineFile = serde_yaml::from_str(&yaml).expect("explicit entries must load");
120 assert_eq!(parsed.entries.len(), 1);
121 assert_eq!(parsed.entries[0].fingerprint, "deadbeef");
122 assert_eq!(parsed.entries[0].rule_id, "RULE_A");
123 }
124
125 #[test]
132 fn baseline_entry_rejects_unknown_fields() {
133 let yaml = format!(
134 "schema_version: {POLICY_SCHEMA_VERSION}\n\
135 entries:\n \
136 - fingerprintt: deadbeef\n \
137 rule_id: RULE_A\n \
138 reason: pre-existing\n"
139 );
140 let result: Result<BaselineFile, _> = serde_yaml::from_str(&yaml);
141 assert!(
142 result.is_err(),
143 "BaselineEntry MUST reject unknown field 'fingerprintt'; \
144 pre-fix, this was silently accepted and fingerprint was missing"
145 );
146 }
147
148 #[test]
155 fn waiver_entry_rejects_unknown_fields() {
156 let yaml = format!(
157 "schema_version: {POLICY_SCHEMA_VERSION}\n\
158 waivers:\n \
159 - rule_ld: RULE_A\n \
160 reason: approved exception\n"
161 );
162 let result: Result<WaiverFile, _> = serde_yaml::from_str(&yaml);
163 assert!(
164 result.is_err(),
165 "WaiverEntry MUST reject unknown field 'rule_ld'; \
166 pre-fix, this was silently accepted and rule_id defaulted to None"
167 );
168 }
169
170 #[test]
175 fn baseline_file_rejects_unknown_top_level_fields() {
176 let yaml = format!("schema_version: {POLICY_SCHEMA_VERSION}\nentires: []\n");
177 let result: Result<BaselineFile, _> = serde_yaml::from_str(&yaml);
178 assert!(
179 result.is_err(),
180 "BaselineFile MUST reject unknown field 'entires'; \
181 pre-fix, this was silently accepted and entries defaulted to empty"
182 );
183 }
184
185 #[test]
190 fn waiver_file_rejects_unknown_top_level_fields() {
191 let yaml = format!("schema_version: {POLICY_SCHEMA_VERSION}\nwavers: []\n");
192 let result: Result<WaiverFile, _> = serde_yaml::from_str(&yaml);
193 assert!(
194 result.is_err(),
195 "WaiverFile MUST reject unknown field 'wavers'; \
196 pre-fix, this was silently accepted and waivers defaulted to empty"
197 );
198 }
199}