use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Category {
Documentation,
LocalDevelopment,
Testing,
Security,
Delivery,
Collaboration,
}
impl Category {
pub fn display_name(self) -> &'static str {
match self {
Self::Documentation => "Documentation & Onboarding",
Self::LocalDevelopment => "Development Hygiene",
Self::Testing => "Quality Assurance",
Self::Security => "Security & Supply Chain",
Self::Delivery => "Automation & Delivery",
Self::Collaboration => "Project Management & Collaboration",
}
}
pub fn id(self) -> &'static str {
match self {
Self::Documentation => "documentation",
Self::LocalDevelopment => "local_development",
Self::Testing => "testing",
Self::Security => "security",
Self::Delivery => "delivery",
Self::Collaboration => "collaboration",
}
}
pub fn weight(self) -> f64 {
match self {
Self::Documentation => 0.20,
Self::LocalDevelopment => 0.15,
Self::Testing => 0.20,
Self::Security => 0.15,
Self::Delivery => 0.15,
Self::Collaboration => 0.15,
}
}
pub fn rubric_note(self) -> &'static str {
match self {
Self::Documentation => "Foundation",
Self::LocalDevelopment => "Daily developer experience",
Self::Testing => "Code health",
Self::Security => "Risk reduction",
Self::Delivery => "Velocity + reliability",
Self::Collaboration => "Process maturity",
}
}
pub fn all() -> &'static [Category] {
&[
Self::Documentation,
Self::LocalDevelopment,
Self::Testing,
Self::Security,
Self::Delivery,
Self::Collaboration,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum Status {
Enforced,
Present,
Partial,
Missing,
NotApplicable,
Unknown,
}
impl Status {
pub fn is_gap(self) -> bool {
matches!(self, Self::Missing | Self::Partial | Self::Unknown)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum Confidence {
High,
Medium,
Low,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
}
impl EvidenceItem {
pub fn path(path: impl Into<String>) -> Self {
Self {
path: Some(path.into()),
detail: None,
line: None,
}
}
pub fn path_detail(path: impl Into<String>, detail: impl Into<String>) -> Self {
Self {
path: Some(path.into()),
detail: Some(detail.into()),
line: None,
}
}
pub fn detail(detail: impl Into<String>) -> Self {
Self {
path: None,
detail: Some(detail.into()),
line: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
pub rule: String,
pub category: Category,
pub status: Status,
pub confidence: Confidence,
pub summary: String,
pub evidence: Vec<EvidenceItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remediation: Option<String>,
}
impl Finding {
pub fn builder(rule: impl Into<String>, category: Category) -> FindingBuilder {
FindingBuilder {
rule: rule.into(),
category,
status: Status::Unknown,
confidence: Confidence::Medium,
summary: String::new(),
evidence: Vec::new(),
remediation: None,
}
}
}
#[derive(Debug)]
pub struct FindingBuilder {
rule: String,
category: Category,
status: Status,
confidence: Confidence,
summary: String,
evidence: Vec<EvidenceItem>,
remediation: Option<String>,
}
impl FindingBuilder {
pub fn status(mut self, status: Status) -> Self {
self.status = status;
self
}
pub fn confidence(mut self, confidence: Confidence) -> Self {
self.confidence = confidence;
self
}
pub fn summary(mut self, summary: impl Into<String>) -> Self {
self.summary = summary.into();
self
}
pub fn evidence(mut self, items: Vec<EvidenceItem>) -> Self {
self.evidence = items;
self
}
pub fn push_evidence(mut self, item: EvidenceItem) -> Self {
self.evidence.push(item);
self
}
pub fn remediation(mut self, remediation: impl Into<String>) -> Self {
self.remediation = Some(remediation.into());
self
}
pub fn build(self) -> Finding {
Finding {
rule: self.rule,
category: self.category,
status: self.status,
confidence: self.confidence,
summary: self.summary,
evidence: self.evidence,
remediation: self.remediation,
}
}
}