1use std::collections::BTreeMap;
9use std::path::PathBuf;
10
11use chrono::NaiveDate;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case")]
18pub enum Cadence {
19 Continuous,
20 Weekly,
21 Monthly,
22 Quarterly,
23 #[serde(rename = "semi-annual")]
24 SemiAnnual,
25 Annual,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum Weekday {
31 Monday,
32 Tuesday,
33 Wednesday,
34 Thursday,
35 Friday,
36 Saturday,
37 Sunday,
38}
39
40impl Weekday {
41 pub fn to_chrono(self) -> chrono::Weekday {
42 match self {
43 Weekday::Monday => chrono::Weekday::Mon,
44 Weekday::Tuesday => chrono::Weekday::Tue,
45 Weekday::Wednesday => chrono::Weekday::Wed,
46 Weekday::Thursday => chrono::Weekday::Thu,
47 Weekday::Friday => chrono::Weekday::Fri,
48 Weekday::Saturday => chrono::Weekday::Sat,
49 Weekday::Sunday => chrono::Weekday::Sun,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Control {
58 pub id: String,
59 pub title: String,
60 pub policy: String,
61 #[serde(default)]
62 pub nist: Vec<String>,
63 pub owner: String,
64 pub cadence: Cadence,
65 #[serde(default)]
66 pub weekday: Option<Weekday>,
67 #[serde(default)]
68 pub due_by: Option<String>,
69 pub skill: String,
70 #[serde(default)]
71 pub skill_args: Option<serde_json::Value>,
72 #[serde(default)]
73 pub scope: Option<Scope>,
74 #[serde(default)]
75 pub evidence_required: Vec<EvidenceRequirement>,
76 #[serde(default)]
77 pub remediation_thresholds: BTreeMap<String, u32>,
78 #[serde(default)]
79 pub outputs: Option<serde_json::Value>,
80 #[serde(default)]
81 pub references: Vec<Reference>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Reference {
86 pub title: String,
87 #[serde(default)]
88 pub path: Option<String>,
89 #[serde(default)]
90 pub url: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct EvidenceRequirement {
95 pub kind: String,
96 #[serde(default)]
97 pub description: Option<String>,
98 #[serde(default)]
99 pub prompt: Option<String>,
100 #[serde(default)]
101 pub path: Option<String>,
102 #[serde(default)]
103 pub cmd: Option<String>,
104 #[serde(default)]
105 pub per_system: Option<bool>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(untagged)]
112pub enum Scope {
113 Inventory(InventoryScope),
114 Inline(InlineScope),
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct InventoryScope {
119 pub kind: String,
120 #[serde(default)]
121 pub all: Option<bool>,
122 #[serde(default)]
123 pub has_tags: Vec<String>,
124 #[serde(default)]
125 pub excludes: Vec<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct InlineScope {
130 pub inline: Vec<InlineEntry>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct InlineEntry {
135 pub name: String,
136 pub kind: String,
137 #[serde(default)]
138 pub tags: Vec<String>,
139}
140
141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
146#[serde(transparent)]
147pub struct Inventory {
148 pub kinds: BTreeMap<String, Vec<InventoryEntry>>,
149}
150
151impl Inventory {
152 pub fn entries(&self, kind: &str) -> &[InventoryEntry] {
157 if let Some(v) = self.kinds.get(kind) {
158 return v;
159 }
160 let plural = format!("{kind}s");
161 if let Some(v) = self.kinds.get(&plural) {
162 return v;
163 }
164 &[]
165 }
166
167 pub fn section_for(&self, kind: &str) -> Option<&str> {
171 if let Some((k, _)) = self.kinds.get_key_value(kind) {
172 return Some(k.as_str());
173 }
174 let plural = format!("{kind}s");
175 self.kinds.get_key_value(&plural).map(|(k, _)| k.as_str())
176 }
177
178 pub fn iter(&self) -> impl Iterator<Item = (&String, &InventoryEntry)> {
179 self.kinds
180 .iter()
181 .flat_map(|(k, v)| v.iter().map(move |e| (k, e)))
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct InventoryEntry {
187 pub name: String,
188 #[serde(default)]
189 pub tags: Vec<String>,
190 #[serde(default)]
191 pub in_scope_since: Option<NaiveDate>,
192 #[serde(default)]
193 pub retired_on: Option<NaiveDate>,
194 #[serde(default)]
195 pub aliases: Vec<String>,
196 #[serde(default)]
198 pub excludes: Vec<String>,
199 #[serde(flatten)]
202 pub extras: BTreeMap<String, serde_json::Value>,
203}
204
205impl InventoryEntry {
206 pub fn is_active_on(&self, date: NaiveDate) -> bool {
209 if let Some(start) = self.in_scope_since {
210 if date < start {
211 return false;
212 }
213 }
214 if let Some(end) = self.retired_on {
215 if date >= end {
216 return false;
217 }
218 }
219 true
220 }
221}
222
223#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226pub struct Schedule {
227 #[serde(default)]
228 pub overrides: Vec<ScheduleEntry>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct ScheduleEntry {
233 pub control_id: String,
234 #[serde(default)]
235 pub due: Option<NaiveDate>,
236 #[serde(default)]
237 pub weekday: Option<Weekday>,
238 #[serde(default)]
239 pub note: Option<String>,
240 #[serde(default)]
241 pub reason: Option<String>,
242 #[serde(default)]
243 pub skip: Option<ScheduleSkip>,
244 #[serde(default)]
245 pub insert: Option<ScheduleInsert>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct ScheduleSkip {
250 #[serde(default)]
251 pub quarter: Option<String>,
252 #[serde(default)]
253 pub year: Option<i32>,
254 #[serde(default)]
255 pub reason: Option<String>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct ScheduleInsert {
260 pub run_at: NaiveDate,
261 #[serde(default)]
262 pub reason: Option<String>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct State {
269 pub schema_version: u32,
270 #[serde(default)]
271 pub controls: BTreeMap<String, StateEntry>,
272 #[serde(default)]
273 pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
274}
275
276impl Default for State {
277 fn default() -> Self {
278 Self {
279 schema_version: crate::SCHEMA_VERSION,
280 controls: BTreeMap::new(),
281 updated_at: None,
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct StateEntry {
288 pub last_run_id: Option<String>,
289 pub last_run_path: Option<String>,
290 pub last_run_at: Option<chrono::DateTime<chrono::Utc>>,
291 pub last_status: RunStatus,
292 pub next_due: Option<NaiveDate>,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "kebab-case")]
297pub enum RunStatus {
298 Complete,
299 InProgress,
300 Failed,
301 NeverRun,
302}
303
304#[derive(Debug, Clone, Default, Serialize, Deserialize)]
307pub struct Config {
308 #[serde(default)]
309 pub schema_version: Option<u32>,
310 #[serde(default)]
311 pub org: Option<OrgConfig>,
312 #[serde(default)]
313 pub owners: BTreeMap<String, String>,
314 #[serde(default)]
315 pub weekly_default_weekday: Option<Weekday>,
316 #[serde(default)]
317 pub thresholds: BTreeMap<String, serde_json::Value>,
318 #[serde(default)]
319 pub integrations: BTreeMap<String, serde_json::Value>,
320 #[serde(flatten)]
322 pub extras: BTreeMap<String, serde_json::Value>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct OrgConfig {
327 #[serde(default)]
328 pub name: Option<String>,
329 #[serde(default)]
330 pub wisp_repo: Option<String>,
331 #[serde(flatten)]
332 pub extras: BTreeMap<String, serde_json::Value>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
340pub struct ResolvedSystem {
341 pub name: String,
342 pub kind: String,
343 #[serde(default)]
344 pub tags: Vec<String>,
345 #[serde(flatten)]
348 pub extras: BTreeMap<String, serde_json::Value>,
349}
350
351#[derive(Debug, Clone)]
355pub struct LoadedRegistry {
356 pub root: PathBuf,
357 pub controls: BTreeMap<String, Control>,
358 pub inventory: Inventory,
359 pub schedule: Schedule,
360 pub state: State,
361 pub config: Config,
362}