1use crate::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum ArtifactState {
9 Discovered,
11 Acquired,
13 Analyzed,
15 Scanned,
17 Promoted,
19 Quarantined,
21 Deployed,
23}
24
25impl ArtifactState {
26 pub const fn name(self) -> &'static str {
27 match self {
28 ArtifactState::Discovered => "Discovered",
29 ArtifactState::Acquired => "Acquired",
30 ArtifactState::Analyzed => "Analyzed",
31 ArtifactState::Scanned => "Scanned",
32 ArtifactState::Promoted => "Promoted",
33 ArtifactState::Quarantined => "Quarantined",
34 ArtifactState::Deployed => "Deployed",
35 }
36 }
37
38 pub const fn can_transition_to(self, next: ArtifactState) -> bool {
40 use ArtifactState::*;
41 matches!(
42 (self, next),
43 (Discovered, Acquired)
44 | (Acquired, Analyzed)
45 | (Analyzed, Scanned)
46 | (Scanned, Promoted)
47 | (Scanned, Quarantined)
48 | (Quarantined, Scanned)
49 | (Promoted, Deployed)
50 )
51 }
52
53 pub fn transition(self, next: ArtifactState) -> Result<ArtifactState> {
55 if self.can_transition_to(next) {
56 Ok(next)
57 } else {
58 Err(Error::IllegalTransition {
59 from: self.name(),
60 to: next.name(),
61 })
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
68pub enum Severity {
69 Low,
70 Medium,
71 High,
72 Critical,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct Finding {
78 pub id: String,
79 pub severity: Severity,
80 pub summary: String,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub enum ScanVerdict {
87 Clean,
88 Findings(Vec<Finding>),
89}
90
91impl ScanVerdict {
92 pub fn is_clean(&self) -> bool {
93 matches!(self, ScanVerdict::Clean)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::ArtifactState::*;
100 use super::*;
101
102 #[test]
103 fn happy_path_is_permitted() {
104 let path = [Discovered, Acquired, Analyzed, Scanned, Promoted, Deployed];
105 for pair in path.windows(2) {
106 assert!(
107 pair[0].can_transition_to(pair[1]),
108 "{:?} -> {:?}",
109 pair[0],
110 pair[1]
111 );
112 }
113 }
114
115 #[test]
116 fn quarantine_and_rescan_loop_is_permitted() {
117 assert!(Scanned.can_transition_to(Quarantined));
118 assert!(Quarantined.can_transition_to(Scanned));
119 }
120
121 #[test]
122 fn skipping_states_is_rejected() {
123 let err = Discovered.transition(Promoted).unwrap_err();
124 assert_eq!(
125 err,
126 Error::IllegalTransition {
127 from: "Discovered",
128 to: "Promoted"
129 }
130 );
131 }
132
133 #[test]
134 fn quarantined_cannot_deploy() {
135 assert!(!Quarantined.can_transition_to(Deployed));
136 assert!(!Quarantined.can_transition_to(Promoted));
137 }
138
139 #[test]
140 fn only_clean_verdict_is_clean() {
141 assert!(ScanVerdict::Clean.is_clean());
142 assert!(!ScanVerdict::Findings(vec![Finding {
143 id: "RUSTSEC-0000-0000".into(),
144 severity: Severity::High,
145 summary: "example".into(),
146 }])
147 .is_clean());
148 }
149}