use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const GUARD_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum GitHashAlgorithm {
Sha1,
Sha256,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GuardRootMode {
Repo,
Filesystem,
}
impl GuardRootMode {
pub fn label(self) -> &'static str {
match self {
GuardRootMode::Repo => "repo",
GuardRootMode::Filesystem => "filesystem",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum GuardRootState {
Indexing,
Current,
Dirty,
Blocked,
Degraded,
StalePolicy,
Stopped,
}
impl GuardRootState {
pub fn may_authorize_commit(self) -> bool {
false
}
pub fn needs_repair(self) -> bool {
matches!(self, GuardRootState::Degraded | GuardRootState::StalePolicy)
}
pub fn all() -> &'static [GuardRootState] {
&[
GuardRootState::Indexing,
GuardRootState::Current,
GuardRootState::Dirty,
GuardRootState::Blocked,
GuardRootState::Degraded,
GuardRootState::StalePolicy,
GuardRootState::Stopped,
]
}
pub fn label(self) -> &'static str {
match self {
GuardRootState::Indexing => "indexing",
GuardRootState::Current => "current",
GuardRootState::Dirty => "dirty",
GuardRootState::Blocked => "blocked",
GuardRootState::Degraded => "degraded",
GuardRootState::StalePolicy => "stale-policy",
GuardRootState::Stopped => "stopped",
}
}
}
impl std::fmt::Display for GuardRootState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GuardTransition {
ReconciliationStarted,
ReconciliationClean,
ReconciliationFindings,
ReconciliationDegraded,
EventAccepted,
EventsClean,
EventsFindings,
EventsDegraded,
CoverageLost,
PolicyChanged,
RepairStarted,
Stopped,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TransitionError {
#[error("illegal guard transition: {event} from state {from}")]
Illegal {
event: GuardTransition,
from: GuardRootState,
},
}
impl std::fmt::Display for GuardTransition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GuardTransition::ReconciliationStarted => write!(f, "reconciliation-started"),
GuardTransition::ReconciliationClean => write!(f, "reconciliation-clean"),
GuardTransition::ReconciliationFindings => write!(f, "reconciliation-findings"),
GuardTransition::ReconciliationDegraded => write!(f, "reconciliation-degraded"),
GuardTransition::EventAccepted => write!(f, "event-accepted"),
GuardTransition::EventsClean => write!(f, "events-clean"),
GuardTransition::EventsFindings => write!(f, "events-findings"),
GuardTransition::EventsDegraded => write!(f, "events-degraded"),
GuardTransition::CoverageLost => write!(f, "coverage-lost"),
GuardTransition::PolicyChanged => write!(f, "policy-changed"),
GuardTransition::RepairStarted => write!(f, "repair-started"),
GuardTransition::Stopped => write!(f, "stopped"),
}
}
}
impl GuardRootState {
pub fn transition(self, event: &GuardTransition) -> Result<GuardRootState, TransitionError> {
use GuardRootState as S;
use GuardTransition as T;
let result = match (self, event) {
(S::Stopped, T::ReconciliationStarted) => Some(S::Indexing),
(S::Indexing, T::ReconciliationClean) => Some(S::Current),
(S::Indexing, T::ReconciliationFindings) => Some(S::Blocked),
(S::Indexing, T::ReconciliationDegraded) => Some(S::Degraded),
(S::Current, T::EventAccepted) => Some(S::Dirty),
(S::Blocked, T::EventAccepted) => Some(S::Dirty),
(S::Dirty, T::EventsClean) => Some(S::Current),
(S::Dirty, T::EventsFindings) => Some(S::Blocked),
(S::Dirty, T::EventsDegraded) => Some(S::Degraded),
(S::Indexing, T::CoverageLost) => Some(S::Degraded),
(S::Current, T::CoverageLost) => Some(S::Degraded),
(S::Dirty, T::CoverageLost) => Some(S::Degraded),
(S::Blocked, T::CoverageLost) => Some(S::Degraded),
(S::Indexing, T::PolicyChanged) => Some(S::StalePolicy),
(S::Current, T::PolicyChanged) => Some(S::StalePolicy),
(S::Dirty, T::PolicyChanged) => Some(S::StalePolicy),
(S::Blocked, T::PolicyChanged) => Some(S::StalePolicy),
(S::Degraded, T::RepairStarted) => Some(S::Indexing),
(S::StalePolicy, T::RepairStarted) => Some(S::Indexing),
(S::Stopped, T::Stopped) => Some(S::Stopped),
(S::Indexing, T::Stopped) => Some(S::Stopped),
(S::Current, T::Stopped) => Some(S::Stopped),
(S::Dirty, T::Stopped) => Some(S::Stopped),
(S::Blocked, T::Stopped) => Some(S::Stopped),
(S::Degraded, T::Stopped) => Some(S::Stopped),
(S::StalePolicy, T::Stopped) => Some(S::Stopped),
(S::Degraded, T::CoverageLost) => Some(S::Degraded),
(S::StalePolicy, T::PolicyChanged) => Some(S::StalePolicy),
_ => None,
};
result.ok_or(TransitionError::Illegal {
event: event.clone(),
from: self,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GuardPolicyIdentity {
pub build_identity: String,
pub detector_digest: String,
pub suppression_digest: String,
pub keyhogignore_digest: String,
pub config_digest: String,
pub decode_policy_version: u32,
pub source_policy_digest: String,
pub guard_schema_version: u32,
pub report_semantics_version: u32,
}
impl GuardPolicyIdentity {
pub fn short_digest(&self) -> Result<String, serde_json::Error> {
let bytes = serde_json::to_vec(self)?;
let hash = blake3::hash(&bytes);
Ok(hex::encode(&hash.as_bytes()[..6]))
}
pub fn is_compatible_with(&self, other: &GuardPolicyIdentity) -> bool {
self == other
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GitCleanAttestation {
pub hash_algorithm: GitHashAlgorithm,
pub blob_oid: String,
pub object_size: u64,
pub policy_identity: GuardPolicyIdentity,
pub last_seen_sequence: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GitCleanAttestationKey<'a> {
pub hash_algorithm: GitHashAlgorithm,
pub blob_oid: &'a str,
pub policy_short_digest: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuardReceipt {
pub objects_requested: u64,
pub objects_hit: u64,
pub objects_scanned: u64,
pub objects_skipped: u64,
pub bytes_requested: u64,
pub bytes_hit: u64,
pub bytes_scanned: u64,
pub findings_count: u64,
pub coverage_gaps: u64,
pub terminal_state: GuardRootState,
pub policy_identity: GuardPolicyIdentity,
pub terminal_sequence: u64,
}
impl GuardReceipt {
pub fn validate_conservation(&self) -> Result<(), ReceiptError> {
let obj_sum = self.objects_hit + self.objects_scanned + self.objects_skipped;
if obj_sum != self.objects_requested {
return Err(ReceiptError::ObjectMismatch {
requested: self.objects_requested,
accounted: obj_sum,
});
}
let byte_sum = self.bytes_hit + self.bytes_scanned;
if byte_sum != self.bytes_requested {
return Err(ReceiptError::ByteMismatch {
requested: self.bytes_requested,
accounted: byte_sum,
});
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ReceiptError {
#[error("receipt object mismatch: requested {requested}, accounted {accounted}")]
ObjectMismatch {
requested: u64,
accounted: u64,
},
#[error("receipt byte mismatch: requested {requested}, accounted {accounted}")]
ByteMismatch {
requested: u64,
accounted: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuardRootRecord {
pub canonical_path: Vec<u8>,
pub filesystem_identity: FilesystemIdentity,
pub mode: GuardRootMode,
pub state: GuardRootState,
pub terminal_sequence: u64,
pub accepted_event_sequence: u64,
pub completed_event_sequence: u64,
pub initial_reconciliation_time: Option<u64>,
pub last_reconciliation_time: Option<u64>,
pub backend_route_label: String,
pub last_receipt: Option<GuardReceipt>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FilesystemIdentity {
pub device: u64,
pub inode: u64,
}