use std::collections::BTreeMap;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Critical,
High,
Medium,
Low,
Info,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Status {
Open,
InProgress,
Remediated,
Reopened,
AcceptedException,
FalsePositive,
}
impl Status {
pub fn is_open(self) -> bool {
match self {
Status::Open | Status::InProgress | Status::Reopened => true,
Status::Remediated | Status::AcceptedException | Status::FalsePositive => false,
}
}
pub fn is_closed(self) -> bool {
!self.is_open()
}
pub fn as_str(self) -> &'static str {
match self {
Status::Open => "open",
Status::InProgress => "in-progress",
Status::Remediated => "remediated",
Status::Reopened => "reopened",
Status::AcceptedException => "accepted-exception",
Status::FalsePositive => "false-positive",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Agent {
pub model: String,
pub skill: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FindingRef {
pub control_id: String,
pub run_id: String,
pub manifest_sha256: String,
pub finding_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body_path: Option<String>,
}
impl FindingRef {
pub fn fingerprint(&self) -> String {
format!("{}:{}", self.control_id, self.finding_id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalLink {
pub system: String,
pub external_id: String,
pub url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventEnvelope {
pub seq: u64,
pub ts: DateTime<Utc>,
pub actor: String,
#[serde(default)]
pub agent: Option<Agent>,
pub prev_sha256: Option<String>,
#[serde(flatten)]
pub data: EventData,
}
pub type RiskEvent = EventEnvelope;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "kebab-case")]
pub enum EventData {
Opened {
finding_ref: FindingRef,
title: String,
severity: Severity,
impact: u8,
likelihood: u8,
affected_systems: Vec<String>,
sla_days: u32,
due_at: NaiveDate,
},
OwnerAssigned {
owner: String,
},
ScoreChanged {
impact: u8,
likelihood: u8,
severity: Severity,
reason: String,
},
SlaSet {
due_at: NaiveDate,
basis: String,
},
StatusChanged {
from: Status,
to: Status,
reason: String,
},
EvidenceLinked {
finding_ref: FindingRef,
},
ExternalLinked {
system: String,
external_id: String,
url: String,
},
ExternalStatusObserved {
system: String,
status: String,
observed_at: DateTime<Utc>,
},
Note {
text: String,
},
Remediated {
#[serde(default, skip_serializing_if = "Option::is_none")]
resolved_run_ref: Option<FindingRef>,
note: String,
},
Reopened {
reason: String,
},
ExceptionDocumented {
rationale: String,
approved_by: String,
expires_at: NaiveDate,
},
}
impl EventData {
pub fn type_str(&self) -> &'static str {
match self {
EventData::Opened { .. } => "opened",
EventData::OwnerAssigned { .. } => "owner-assigned",
EventData::ScoreChanged { .. } => "score-changed",
EventData::SlaSet { .. } => "sla-set",
EventData::StatusChanged { .. } => "status-changed",
EventData::EvidenceLinked { .. } => "evidence-linked",
EventData::ExternalLinked { .. } => "external-linked",
EventData::ExternalStatusObserved { .. } => "external-status-observed",
EventData::Note { .. } => "note",
EventData::Remediated { .. } => "remediated",
EventData::Reopened { .. } => "reopened",
EventData::ExceptionDocumented { .. } => "exception-documented",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RiskState {
pub status: Status,
pub title: String,
pub severity: Severity,
pub impact: u8,
pub likelihood: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub due_at: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sla_days: Option<u32>,
pub affected_systems: Vec<String>,
pub finding_refs: Vec<FindingRef>,
pub external: Vec<ExternalLink>,
#[serde(default)]
pub external_status: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exception_expires_at: Option<NaiveDate>,
}
impl RiskState {
pub fn fingerprint(&self) -> Option<String> {
self.finding_refs.first().map(FindingRef::fingerprint)
}
pub fn source_control(&self) -> Option<&str> {
self.finding_refs.first().map(|f| f.control_id.as_str())
}
pub fn first_run_id(&self) -> Option<&str> {
self.finding_refs.first().map(|f| f.run_id.as_str())
}
}