use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum EvidenceState<T> {
Complete { value: T },
Partial { value: T, gaps: Vec<EvidenceGap> },
Missing { gaps: Vec<EvidenceGap> },
#[default]
NotApplicable,
}
impl<T> EvidenceState<T> {
pub fn complete(value: T) -> Self {
Self::Complete { value }
}
pub fn partial(value: T, gaps: Vec<EvidenceGap>) -> Self {
Self::Partial { value, gaps }
}
pub fn missing(gaps: Vec<EvidenceGap>) -> Self {
Self::Missing { gaps }
}
pub fn not_applicable() -> Self {
Self::NotApplicable
}
pub fn value(&self) -> Option<&T> {
match self {
Self::Complete { value } | Self::Partial { value, .. } => Some(value),
Self::Missing { .. } | Self::NotApplicable => None,
}
}
pub fn gaps(&self) -> &[EvidenceGap] {
match self {
Self::Partial { gaps, .. } | Self::Missing { gaps } => gaps,
Self::Complete { .. } | Self::NotApplicable => &[],
}
}
pub fn has_gaps(&self) -> bool {
!self.gaps().is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum EvidenceGap {
CollectionFailed {
source: String,
subject: String,
detail: String,
},
Truncated {
source: String,
subject: String,
},
MissingField {
source: String,
subject: String,
field: String,
},
DiffUnavailable {
subject: String,
},
Unsupported {
source: String,
capability: String,
},
}
impl fmt::Display for EvidenceGap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CollectionFailed {
source,
subject,
detail,
} => write!(f, "collection failed: {source}/{subject}: {detail}"),
Self::Truncated { source, subject } => write!(f, "truncated: {source}/{subject}"),
Self::MissingField {
source,
subject,
field,
} => write!(f, "missing field: {source}/{subject}.{field}"),
Self::DiffUnavailable { subject } => write!(f, "diff unavailable: {subject}"),
Self::Unsupported { source, capability } => {
write!(f, "unsupported: {source}/{capability}")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangeRequestId {
pub system: String,
pub value: String,
}
impl ChangeRequestId {
pub fn new(system: impl Into<String>, value: impl Into<String>) -> Self {
Self {
system: system.into(),
value: value.into(),
}
}
}
impl fmt::Display for ChangeRequestId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.system, self.value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkItemRef {
pub system: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangedAsset {
pub path: String,
pub diff_available: bool,
#[serde(default)]
pub additions: u32,
#[serde(default)]
pub deletions: u32,
#[serde(default)]
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diff: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalDisposition {
Approved,
Rejected,
Commented,
Dismissed,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovalDecision {
pub actor: String,
pub disposition: ApprovalDisposition,
pub submitted_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthenticityEvidence {
pub verified: bool,
pub mechanism: Option<String>,
}
impl AuthenticityEvidence {
pub fn new(verified: bool, mechanism: Option<String>) -> Self {
Self {
verified,
mechanism,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceRevision {
pub id: String,
pub authored_by: Option<String>,
pub committed_at: Option<String>,
pub merge: bool,
pub authenticity: EvidenceState<AuthenticityEvidence>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GovernedChange {
pub id: ChangeRequestId,
pub title: String,
pub summary: Option<String>,
pub submitted_by: Option<String>,
pub changed_assets: EvidenceState<Vec<ChangedAsset>>,
pub approval_decisions: EvidenceState<Vec<ApprovalDecision>>,
pub source_revisions: EvidenceState<Vec<SourceRevision>>,
pub work_item_refs: EvidenceState<Vec<WorkItemRef>>,
}
impl GovernedChange {
pub fn is_bot_submitted(&self) -> bool {
let Some(author) = self.submitted_by.as_deref() else {
return false;
};
let lower = author.to_ascii_lowercase();
const BOT_SUBMITTERS: &[&str] = &[
"bors",
"bors[bot]",
"mergify[bot]",
"mergify",
"dependabot[bot]",
"dependabot",
"renovate[bot]",
"renovate",
"k8s-ci-robot",
"github-actions[bot]",
"copybara-service[bot]",
];
BOT_SUBMITTERS.contains(&lower.as_str()) || lower.ends_with("[bot]")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PromotionBatch {
pub id: String,
pub source_revisions: EvidenceState<Vec<SourceRevision>>,
pub linked_change_requests: EvidenceState<Vec<ChangeRequestId>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum VerificationOutcome {
Verified,
ChecksumMatch,
SignatureInvalid {
detail: String,
},
SignerMismatch {
detail: String,
},
TransparencyLogMissing {
detail: String,
},
AttestationAbsent {
detail: String,
},
DigestMismatch {
detail: String,
},
Failed {
detail: String,
},
}
impl VerificationOutcome {
pub fn is_verified(&self) -> bool {
matches!(self, Self::Verified | Self::ChecksumMatch)
}
pub fn is_cryptographically_signed(&self) -> bool {
matches!(self, Self::Verified)
}
pub fn failure_detail(&self) -> Option<&str> {
match self {
Self::Verified | Self::ChecksumMatch => None,
Self::SignatureInvalid { detail }
| Self::SignerMismatch { detail }
| Self::TransparencyLogMissing { detail }
| Self::AttestationAbsent { detail }
| Self::DigestMismatch { detail }
| Self::Failed { detail } => Some(detail),
}
}
pub fn failure_kind(&self) -> Option<&'static str> {
match self {
Self::Verified | Self::ChecksumMatch => None,
Self::SignatureInvalid { .. } => Some("signature_invalid"),
Self::SignerMismatch { .. } => Some("signer_mismatch"),
Self::TransparencyLogMissing { .. } => Some("transparency_log_missing"),
Self::AttestationAbsent { .. } => Some("attestation_absent"),
Self::DigestMismatch { .. } => Some("digest_mismatch"),
Self::Failed { .. } => Some("failed"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactAttestation {
pub subject: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject_digest: Option<String>,
pub predicate_type: String,
pub signer_workflow: Option<String>,
pub source_repo: Option<String>,
pub verification: VerificationOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CheckConclusion {
Success,
Failure,
Neutral,
Cancelled,
Skipped,
TimedOut,
ActionRequired,
Pending,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckRunEvidence {
pub name: String,
pub conclusion: CheckConclusion,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app_slug: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DependencySignatureEvidence {
pub name: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
pub verification: VerificationOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature_mechanism: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_identity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_repo: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pinned_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actual_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transparency_log_uri: Option<String>,
#[serde(default = "default_true")]
pub is_direct: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RegistryProvenanceCapability {
ChecksumOnly,
CryptographicProvenance,
FullTrustChain,
}
impl DependencySignatureEvidence {
pub fn registry_provenance_capability(&self) -> RegistryProvenanceCapability {
match self.registry.as_deref() {
Some(r) if r.contains("npmjs.org") => RegistryProvenanceCapability::FullTrustChain,
Some("pypi.org") => RegistryProvenanceCapability::FullTrustChain,
_ => RegistryProvenanceCapability::ChecksumOnly,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeownersEntry {
pub pattern: String,
pub owners: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RepositoryPosture {
pub codeowners_entries: Vec<CodeownersEntry>,
#[serde(default = "default_true")]
pub security_analysis_available: bool,
pub secret_scanning_enabled: bool,
#[serde(default)]
pub secret_push_protection_enabled: bool,
pub vulnerability_scanning_enabled: bool,
#[serde(default)]
pub code_scanning_enabled: bool,
pub security_policy_present: bool,
pub security_policy_has_disclosure: bool,
#[serde(default)]
pub default_branch_protected: bool,
#[serde(default)]
pub enforce_admins: bool,
#[serde(default)]
pub dismiss_stale_reviews: bool,
#[serde(default)]
pub unpinned_action_refs: Vec<UnpinnedActionRef>,
#[serde(default)]
pub production_environment_protected: bool,
#[serde(default)]
pub open_high_severity_alerts: u32,
#[serde(default)]
pub copyleft_dependencies: Vec<CopyleftDependency>,
#[serde(default)]
pub release_has_sbom: bool,
#[serde(default)]
pub release_assets_attested: bool,
#[serde(default)]
pub privileged_workflows: Vec<PrivilegedWorkflow>,
#[serde(default)]
pub default_workflow_permissions: String,
#[serde(default)]
pub dependency_update_tool_configured: bool,
#[serde(default)]
pub admin_count: u32,
#[serde(default)]
pub direct_collaborator_count: u32,
#[serde(default)]
pub tag_protection_enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnpinnedActionRef {
pub workflow_file: String,
pub action_ref: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CopyleftDependency {
pub name: String,
pub license: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrivilegedWorkflow {
pub file: String,
pub trigger: String,
pub risk: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildPlatformEvidence {
pub platform: String,
pub hosted: bool,
pub ephemeral: bool,
pub isolated: bool,
pub runner_labels: Vec<String>,
pub signing_key_isolated: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentAction {
pub tool: String,
pub command: String,
#[serde(default)]
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentActionLog {
pub agent_id: String,
pub session_id: String,
pub actions: Vec<AgentAction>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AgentSpec {
#[serde(default)]
pub allowed_paths: Vec<String>,
#[serde(default)]
pub forbidden_paths: Vec<String>,
#[serde(default)]
pub allowed_tools: Vec<String>,
#[serde(default)]
pub max_steps: Option<u32>,
#[serde(default)]
pub budget_cents: Option<u32>,
#[serde(default)]
pub custom_destructive_patterns: Vec<String>,
#[serde(default)]
pub forbidden_mcp_servers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentExecution {
pub agent_id: String,
pub session_id: String,
#[serde(default)]
pub files_touched: Vec<String>,
#[serde(default)]
pub tools_used: Vec<String>,
#[serde(default)]
pub steps_taken: u32,
#[serde(default)]
pub cost_cents: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct McpToolCall {
pub server: String,
pub tool: String,
#[serde(default)]
pub success: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrivilegedGitEvent {
pub actor: String,
pub action: PrivilegedAction,
pub branch: Option<String>,
pub tag: Option<String>,
#[serde(default)]
pub timestamp: Option<String>,
#[serde(default)]
pub commit_sha: Option<String>,
#[serde(default)]
pub detail: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PrivilegedAction {
ForcePush,
DirectPushToDefault,
AdminBypassProtection,
BranchDeletion,
TagDeletion,
ProtectionRuleOverride,
}
impl PrivilegedAction {
pub fn as_str(&self) -> &'static str {
match self {
Self::ForcePush => "force-push",
Self::DirectPushToDefault => "direct-push-to-default",
Self::AdminBypassProtection => "admin-bypass-protection",
Self::BranchDeletion => "branch-deletion",
Self::TagDeletion => "tag-deletion",
Self::ProtectionRuleOverride => "protection-rule-override",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HarnessResult {
pub name: String,
pub passed: bool,
#[serde(default)]
pub total: u32,
#[serde(default)]
pub passed_count: u32,
#[serde(default)]
pub failed_count: u32,
#[serde(default)]
pub skipped_count: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_secs: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_format: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CoverageReport {
pub line_coverage_pct: f64,
#[serde(default)]
pub lines_total: u32,
#[serde(default)]
pub lines_covered: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch_coverage_pct: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_format: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContainerImageEvidence {
pub reference: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
pub signature_verified: bool,
pub provenance_present: bool,
#[serde(default)]
pub sbom_present: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_identity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_repo: Option<String>,
pub verification: VerificationOutcome,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetricObservation {
pub name: String,
pub current: f64,
pub baseline: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub window_secs: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BehavioralDiff {
pub deployment_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<String>,
pub metrics: Vec<MetricObservation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct EvidenceBundle {
pub change_requests: Vec<GovernedChange>,
pub promotion_batches: Vec<PromotionBatch>,
pub artifact_attestations: EvidenceState<Vec<ArtifactAttestation>>,
pub check_runs: EvidenceState<Vec<CheckRunEvidence>>,
pub build_platform: EvidenceState<Vec<BuildPlatformEvidence>>,
pub dependency_signatures: EvidenceState<Vec<DependencySignatureEvidence>>,
#[serde(default)]
pub repository_posture: EvidenceState<RepositoryPosture>,
#[serde(default)]
pub container_images: EvidenceState<Vec<ContainerImageEvidence>>,
#[serde(default)]
pub agent_action_log: EvidenceState<AgentActionLog>,
#[serde(default)]
pub agent_spec: EvidenceState<AgentSpec>,
#[serde(default)]
pub agent_execution: EvidenceState<AgentExecution>,
#[serde(default)]
pub privileged_git_events: EvidenceState<Vec<PrivilegedGitEvent>>,
#[serde(default)]
pub mcp_tool_calls: EvidenceState<Vec<McpToolCall>>,
#[serde(default)]
pub harness_results: EvidenceState<Vec<HarnessResult>>,
#[serde(default)]
pub coverage_report: EvidenceState<CoverageReport>,
#[serde(default)]
pub behavioral_diff: EvidenceState<BehavioralDiff>,
}