1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
10#[serde(tag = "state", rename_all = "snake_case")]
11pub enum EvidenceState<T> {
12 Complete { value: T },
14 Partial { value: T, gaps: Vec<EvidenceGap> },
16 Missing { gaps: Vec<EvidenceGap> },
18 #[default]
20 NotApplicable,
21}
22
23impl<T> EvidenceState<T> {
24 pub fn complete(value: T) -> Self {
25 Self::Complete { value }
26 }
27
28 pub fn partial(value: T, gaps: Vec<EvidenceGap>) -> Self {
29 Self::Partial { value, gaps }
30 }
31
32 pub fn missing(gaps: Vec<EvidenceGap>) -> Self {
33 Self::Missing { gaps }
34 }
35
36 pub fn not_applicable() -> Self {
37 Self::NotApplicable
38 }
39
40 pub fn value(&self) -> Option<&T> {
41 match self {
42 Self::Complete { value } | Self::Partial { value, .. } => Some(value),
43 Self::Missing { .. } | Self::NotApplicable => None,
44 }
45 }
46
47 pub fn value_mut(&mut self) -> Option<&mut T> {
48 match self {
49 Self::Complete { value } | Self::Partial { value, .. } => Some(value),
50 Self::Missing { .. } | Self::NotApplicable => None,
51 }
52 }
53
54 pub fn gaps(&self) -> &[EvidenceGap] {
55 match self {
56 Self::Partial { gaps, .. } | Self::Missing { gaps } => gaps,
57 Self::Complete { .. } | Self::NotApplicable => &[],
58 }
59 }
60
61 pub fn has_gaps(&self) -> bool {
62 !self.gaps().is_empty()
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(tag = "kind", rename_all = "snake_case")]
69pub enum EvidenceGap {
70 CollectionFailed {
71 source: String,
72 subject: String,
73 detail: String,
74 },
75 Truncated {
76 source: String,
77 subject: String,
78 },
79 MissingField {
80 source: String,
81 subject: String,
82 field: String,
83 },
84 DiffUnavailable {
85 subject: String,
86 },
87 Unsupported {
88 source: String,
89 capability: String,
90 },
91}
92
93impl fmt::Display for EvidenceGap {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Self::CollectionFailed {
97 source,
98 subject,
99 detail,
100 } => write!(f, "collection failed: {source}/{subject}: {detail}"),
101 Self::Truncated { source, subject } => write!(f, "truncated: {source}/{subject}"),
102 Self::MissingField {
103 source,
104 subject,
105 field,
106 } => write!(f, "missing field: {source}/{subject}.{field}"),
107 Self::DiffUnavailable { subject } => write!(f, "diff unavailable: {subject}"),
108 Self::Unsupported { source, capability } => {
109 write!(f, "unsupported: {source}/{capability}")
110 }
111 }
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct ChangeRequestId {
118 pub system: String,
119 pub value: String,
120}
121
122impl ChangeRequestId {
123 pub fn new(system: impl Into<String>, value: impl Into<String>) -> Self {
124 Self {
125 system: system.into(),
126 value: value.into(),
127 }
128 }
129}
130
131impl fmt::Display for ChangeRequestId {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(f, "{}:{}", self.system, self.value)
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct WorkItemRef {
140 pub system: String,
141 pub value: String,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct ChangedAsset {
147 pub path: String,
148 pub diff_available: bool,
149 #[serde(default)]
150 pub additions: u32,
151 #[serde(default)]
152 pub deletions: u32,
153 #[serde(default)]
154 pub status: String,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub diff: Option<String>,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum ApprovalDisposition {
163 Approved,
164 Rejected,
165 Commented,
166 Dismissed,
167 Unknown,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct ApprovalDecision {
173 pub actor: String,
174 pub disposition: ApprovalDisposition,
175 pub submitted_at: Option<String>,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct AuthenticityEvidence {
181 pub verified: bool,
182 pub mechanism: Option<String>,
183}
184
185impl AuthenticityEvidence {
186 pub fn new(verified: bool, mechanism: Option<String>) -> Self {
187 Self {
188 verified,
189 mechanism,
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct SourceRevision {
197 pub id: String,
198 pub authored_by: Option<String>,
199 pub committed_at: Option<String>,
200 pub merge: bool,
201 pub authenticity: EvidenceState<AuthenticityEvidence>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub struct GovernedChange {
207 pub id: ChangeRequestId,
208 pub title: String,
209 pub summary: Option<String>,
210 pub submitted_by: Option<String>,
211 pub changed_assets: EvidenceState<Vec<ChangedAsset>>,
212 pub approval_decisions: EvidenceState<Vec<ApprovalDecision>>,
213 pub source_revisions: EvidenceState<Vec<SourceRevision>>,
214 pub work_item_refs: EvidenceState<Vec<WorkItemRef>>,
215}
216
217impl GovernedChange {
218 pub fn is_bot_submitted(&self) -> bool {
222 let Some(author) = self.submitted_by.as_deref() else {
223 return false;
224 };
225 let lower = author.to_ascii_lowercase();
226 const BOT_SUBMITTERS: &[&str] = &[
227 "bors",
228 "bors[bot]",
229 "mergify[bot]",
230 "mergify",
231 "dependabot[bot]",
232 "dependabot",
233 "renovate[bot]",
234 "renovate",
235 "k8s-ci-robot",
236 "github-actions[bot]",
237 "copybara-service[bot]",
238 ];
239 BOT_SUBMITTERS.contains(&lower.as_str()) || lower.ends_with("[bot]")
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct PromotionBatch {
246 pub id: String,
247 pub source_revisions: EvidenceState<Vec<SourceRevision>>,
248 pub linked_change_requests: EvidenceState<Vec<ChangeRequestId>>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(tag = "outcome", rename_all = "snake_case")]
254pub enum VerificationOutcome {
255 Verified,
257 ChecksumMatch,
260 SignatureInvalid {
261 detail: String,
262 },
263 SignerMismatch {
264 detail: String,
265 },
266 TransparencyLogMissing {
267 detail: String,
268 },
269 AttestationAbsent {
270 detail: String,
271 },
272 DigestMismatch {
273 detail: String,
274 },
275 Failed {
276 detail: String,
277 },
278}
279
280impl VerificationOutcome {
281 pub fn is_verified(&self) -> bool {
283 matches!(self, Self::Verified | Self::ChecksumMatch)
284 }
285
286 pub fn is_cryptographically_signed(&self) -> bool {
288 matches!(self, Self::Verified)
289 }
290
291 pub fn failure_detail(&self) -> Option<&str> {
292 match self {
293 Self::Verified | Self::ChecksumMatch => None,
294 Self::SignatureInvalid { detail }
295 | Self::SignerMismatch { detail }
296 | Self::TransparencyLogMissing { detail }
297 | Self::AttestationAbsent { detail }
298 | Self::DigestMismatch { detail }
299 | Self::Failed { detail } => Some(detail),
300 }
301 }
302
303 pub fn failure_kind(&self) -> Option<&'static str> {
304 match self {
305 Self::Verified | Self::ChecksumMatch => None,
306 Self::SignatureInvalid { .. } => Some("signature_invalid"),
307 Self::SignerMismatch { .. } => Some("signer_mismatch"),
308 Self::TransparencyLogMissing { .. } => Some("transparency_log_missing"),
309 Self::AttestationAbsent { .. } => Some("attestation_absent"),
310 Self::DigestMismatch { .. } => Some("digest_mismatch"),
311 Self::Failed { .. } => Some("failed"),
312 }
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318pub struct ArtifactAttestation {
319 pub subject: String,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub subject_digest: Option<String>,
322 pub predicate_type: String,
323 pub signer_workflow: Option<String>,
324 pub source_repo: Option<String>,
325 pub verification: VerificationOutcome,
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330#[serde(rename_all = "snake_case")]
331pub enum CheckConclusion {
332 Success,
333 Failure,
334 Neutral,
335 Cancelled,
336 Skipped,
337 TimedOut,
338 ActionRequired,
339 Pending,
340 Unknown,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
345pub struct CheckRunEvidence {
346 pub name: String,
347 pub conclusion: CheckConclusion,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub app_slug: Option<String>,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct DependencySignatureEvidence {
364 pub name: String,
366 pub version: String,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub registry: Option<String>,
371 pub verification: VerificationOutcome,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub signature_mechanism: Option<String>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub signer_identity: Option<String>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub source_repo: Option<String>,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub source_commit: Option<String>,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub pinned_digest: Option<String>,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub actual_digest: Option<String>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub transparency_log_uri: Option<String>,
400 #[serde(default = "default_true")]
403 pub is_direct: bool,
404}
405
406fn default_true() -> bool {
407 true
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
435pub enum RegistryProvenanceCapability {
436 ChecksumOnly,
438 CryptographicProvenance,
440 FullTrustChain,
442}
443
444impl DependencySignatureEvidence {
445 pub fn registry_provenance_capability(&self) -> RegistryProvenanceCapability {
452 match self.registry.as_deref() {
453 Some(r) if r.contains("npmjs.org") => RegistryProvenanceCapability::FullTrustChain,
454 Some("pypi.org") => RegistryProvenanceCapability::FullTrustChain,
455 _ => RegistryProvenanceCapability::ChecksumOnly,
456 }
457 }
458}
459
460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462pub struct CodeownersEntry {
463 pub pattern: String,
465 pub owners: Vec<String>,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
475pub struct RepositoryPosture {
476 pub codeowners_entries: Vec<CodeownersEntry>,
478
479 #[serde(default = "default_true")]
484 pub security_analysis_available: bool,
485
486 pub secret_scanning_enabled: bool,
489 #[serde(default)]
491 pub secret_push_protection_enabled: bool,
492
493 pub vulnerability_scanning_enabled: bool,
496 #[serde(default)]
498 pub code_scanning_enabled: bool,
499
500 pub security_policy_present: bool,
503 pub security_policy_has_disclosure: bool,
505
506 #[serde(default)]
509 pub default_branch_protected: bool,
510
511 #[serde(default)]
514 pub enforce_admins: bool,
515 #[serde(default)]
517 pub dismiss_stale_reviews: bool,
518 #[serde(default)]
519 pub unpinned_action_refs: Vec<UnpinnedActionRef>,
520 #[serde(default)]
521 pub production_environment_protected: bool,
522 #[serde(default)]
523 pub open_high_severity_alerts: u32,
524 #[serde(default)]
525 pub copyleft_dependencies: Vec<CopyleftDependency>,
526 #[serde(default)]
527 pub release_has_sbom: bool,
528 #[serde(default)]
529 pub release_assets_attested: bool,
530 #[serde(default)]
531 pub privileged_workflows: Vec<PrivilegedWorkflow>,
532
533 #[serde(default)]
537 pub default_workflow_permissions: String,
538
539 #[serde(default)]
542 pub dependency_update_tool_configured: bool,
543
544 #[serde(default)]
547 pub admin_count: u32,
548 #[serde(default)]
550 pub direct_collaborator_count: u32,
551
552 #[serde(default)]
555 pub tag_protection_enabled: bool,
556}
557
558#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
559pub struct UnpinnedActionRef {
560 pub workflow_file: String,
561 pub action_ref: String,
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
565pub struct CopyleftDependency {
566 pub name: String,
567 pub license: String,
568}
569
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
571pub struct PrivilegedWorkflow {
572 pub file: String,
573 pub trigger: String,
574 pub risk: String,
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579pub struct BuildPlatformEvidence {
580 pub platform: String,
581 pub hosted: bool,
582 pub ephemeral: bool,
583 pub isolated: bool,
584 pub runner_labels: Vec<String>,
585 pub signing_key_isolated: bool,
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
594pub struct AgentAction {
595 pub tool: String,
596 pub command: String,
597 #[serde(default)]
598 pub timestamp: Option<String>,
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
603pub struct AgentActionLog {
604 pub agent_id: String,
605 pub session_id: String,
606 pub actions: Vec<AgentAction>,
607}
608
609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
611pub struct AgentSpec {
612 #[serde(default)]
613 pub allowed_paths: Vec<String>,
614 #[serde(default)]
615 pub forbidden_paths: Vec<String>,
616 #[serde(default)]
617 pub allowed_tools: Vec<String>,
618 #[serde(default)]
619 pub max_steps: Option<u32>,
620 #[serde(default)]
621 pub budget_cents: Option<u32>,
622 #[serde(default)]
625 pub custom_destructive_patterns: Vec<String>,
626 #[serde(default)]
628 pub forbidden_mcp_servers: Vec<String>,
629}
630
631#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
633pub struct AgentExecution {
634 pub agent_id: String,
635 pub session_id: String,
636 #[serde(default)]
637 pub files_touched: Vec<String>,
638 #[serde(default)]
639 pub tools_used: Vec<String>,
640 #[serde(default)]
641 pub steps_taken: u32,
642 #[serde(default)]
643 pub cost_cents: u32,
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648pub struct McpToolCall {
649 pub server: String,
651 pub tool: String,
653 #[serde(default)]
655 pub success: bool,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub timestamp: Option<String>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub duration_ms: Option<u64>,
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
666pub struct PrivilegedGitEvent {
667 pub actor: String,
668 pub action: PrivilegedAction,
669 pub branch: Option<String>,
670 pub tag: Option<String>,
671 #[serde(default)]
672 pub timestamp: Option<String>,
673 #[serde(default)]
674 pub commit_sha: Option<String>,
675 #[serde(default)]
676 pub detail: Option<String>,
677}
678
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
681#[serde(rename_all = "kebab-case")]
682pub enum PrivilegedAction {
683 ForcePush,
684 DirectPushToDefault,
685 AdminBypassProtection,
686 BranchDeletion,
687 TagDeletion,
688 ProtectionRuleOverride,
689}
690
691impl PrivilegedAction {
692 pub fn as_str(&self) -> &'static str {
693 match self {
694 Self::ForcePush => "force-push",
695 Self::DirectPushToDefault => "direct-push-to-default",
696 Self::AdminBypassProtection => "admin-bypass-protection",
697 Self::BranchDeletion => "branch-deletion",
698 Self::TagDeletion => "tag-deletion",
699 Self::ProtectionRuleOverride => "protection-rule-override",
700 }
701 }
702}
703
704#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
706pub struct HarnessResult {
707 pub name: String,
709 pub passed: bool,
711 #[serde(default)]
713 pub total: u32,
714 #[serde(default)]
716 pub passed_count: u32,
717 #[serde(default)]
719 pub failed_count: u32,
720 #[serde(default)]
722 pub skipped_count: u32,
723 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub duration_secs: Option<f64>,
726 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub source_format: Option<String>,
729}
730
731#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
733pub struct CoverageReport {
734 pub line_coverage_pct: f64,
736 #[serde(default)]
738 pub lines_total: u32,
739 #[serde(default)]
741 pub lines_covered: u32,
742 #[serde(default, skip_serializing_if = "Option::is_none")]
744 pub branch_coverage_pct: Option<f64>,
745 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub source_format: Option<String>,
748}
749
750#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
752pub struct ContainerImageEvidence {
753 pub reference: String,
755 #[serde(default, skip_serializing_if = "Option::is_none")]
757 pub digest: Option<String>,
758 pub signature_verified: bool,
760 pub provenance_present: bool,
762 #[serde(default)]
764 pub sbom_present: bool,
765 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub signer_identity: Option<String>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub source_repo: Option<String>,
771 pub verification: VerificationOutcome,
773}
774
775#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
777pub struct MetricObservation {
778 pub name: String,
780 pub current: f64,
782 pub baseline: f64,
784 #[serde(default, skip_serializing_if = "Option::is_none")]
786 pub unit: Option<String>,
787 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub window_secs: Option<u64>,
790}
791
792#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
794pub struct BehavioralDiff {
795 pub deployment_id: String,
797 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub environment: Option<String>,
800 pub metrics: Vec<MetricObservation>,
802 #[serde(default, skip_serializing_if = "Option::is_none")]
804 pub observed_at: Option<String>,
805}
806
807#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
809pub struct EvidenceBundle {
810 pub change_requests: Vec<GovernedChange>,
811 pub promotion_batches: Vec<PromotionBatch>,
812 pub artifact_attestations: EvidenceState<Vec<ArtifactAttestation>>,
813 pub check_runs: EvidenceState<Vec<CheckRunEvidence>>,
814 pub build_platform: EvidenceState<Vec<BuildPlatformEvidence>>,
815 pub dependency_signatures: EvidenceState<Vec<DependencySignatureEvidence>>,
816 #[serde(default)]
817 pub repository_posture: EvidenceState<RepositoryPosture>,
818 #[serde(default)]
820 pub container_images: EvidenceState<Vec<ContainerImageEvidence>>,
821 #[serde(default)]
823 pub agent_action_log: EvidenceState<AgentActionLog>,
824 #[serde(default)]
825 pub agent_spec: EvidenceState<AgentSpec>,
826 #[serde(default)]
827 pub agent_execution: EvidenceState<AgentExecution>,
828 #[serde(default)]
829 pub privileged_git_events: EvidenceState<Vec<PrivilegedGitEvent>>,
830 #[serde(default)]
831 pub mcp_tool_calls: EvidenceState<Vec<McpToolCall>>,
832 #[serde(default)]
834 pub harness_results: EvidenceState<Vec<HarnessResult>>,
835 #[serde(default)]
836 pub coverage_report: EvidenceState<CoverageReport>,
837 #[serde(default)]
839 pub behavioral_diff: EvidenceState<BehavioralDiff>,
840}