use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArtifactState {
Discovered,
Acquired,
Analyzed,
Scanned,
Promoted,
Quarantined,
Deployed,
}
impl ArtifactState {
pub const fn name(self) -> &'static str {
match self {
ArtifactState::Discovered => "Discovered",
ArtifactState::Acquired => "Acquired",
ArtifactState::Analyzed => "Analyzed",
ArtifactState::Scanned => "Scanned",
ArtifactState::Promoted => "Promoted",
ArtifactState::Quarantined => "Quarantined",
ArtifactState::Deployed => "Deployed",
}
}
pub const fn can_transition_to(self, next: ArtifactState) -> bool {
use ArtifactState::*;
matches!(
(self, next),
(Discovered, Acquired)
| (Acquired, Analyzed)
| (Analyzed, Scanned)
| (Scanned, Promoted)
| (Scanned, Quarantined)
| (Quarantined, Scanned)
| (Promoted, Deployed)
)
}
pub fn transition(self, next: ArtifactState) -> Result<ArtifactState> {
if self.can_transition_to(next) {
Ok(next)
} else {
Err(Error::IllegalTransition {
from: self.name(),
to: next.name(),
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
pub id: String,
pub severity: Severity,
pub summary: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScanVerdict {
Clean,
Findings(Vec<Finding>),
}
impl ScanVerdict {
pub fn is_clean(&self) -> bool {
matches!(self, ScanVerdict::Clean)
}
}
#[cfg(test)]
mod tests {
use super::ArtifactState::*;
use super::*;
#[test]
fn happy_path_is_permitted() {
let path = [Discovered, Acquired, Analyzed, Scanned, Promoted, Deployed];
for pair in path.windows(2) {
assert!(
pair[0].can_transition_to(pair[1]),
"{:?} -> {:?}",
pair[0],
pair[1]
);
}
}
#[test]
fn quarantine_and_rescan_loop_is_permitted() {
assert!(Scanned.can_transition_to(Quarantined));
assert!(Quarantined.can_transition_to(Scanned));
}
#[test]
fn skipping_states_is_rejected() {
let err = Discovered.transition(Promoted).unwrap_err();
assert_eq!(
err,
Error::IllegalTransition {
from: "Discovered",
to: "Promoted"
}
);
}
#[test]
fn quarantined_cannot_deploy() {
assert!(!Quarantined.can_transition_to(Deployed));
assert!(!Quarantined.can_transition_to(Promoted));
}
#[test]
fn only_clean_verdict_is_clean() {
assert!(ScanVerdict::Clean.is_clean());
assert!(!ScanVerdict::Findings(vec![Finding {
id: "RUSTSEC-0000-0000".into(),
severity: Severity::High,
summary: "example".into(),
}])
.is_clean());
}
}