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 gaps(&self) -> &[EvidenceGap] {
48 match self {
49 Self::Partial { gaps, .. } | Self::Missing { gaps } => gaps,
50 Self::Complete { .. } | Self::NotApplicable => &[],
51 }
52 }
53
54 pub fn has_gaps(&self) -> bool {
55 !self.gaps().is_empty()
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(tag = "kind", rename_all = "snake_case")]
62pub enum EvidenceGap {
63 CollectionFailed {
64 source: String,
65 subject: String,
66 detail: String,
67 },
68 Truncated {
69 source: String,
70 subject: String,
71 },
72 MissingField {
73 source: String,
74 subject: String,
75 field: String,
76 },
77 DiffUnavailable {
78 subject: String,
79 },
80 Unsupported {
81 source: String,
82 capability: String,
83 },
84}
85
86impl fmt::Display for EvidenceGap {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::CollectionFailed {
90 source,
91 subject,
92 detail,
93 } => write!(f, "collection failed: {source}/{subject}: {detail}"),
94 Self::Truncated { source, subject } => write!(f, "truncated: {source}/{subject}"),
95 Self::MissingField {
96 source,
97 subject,
98 field,
99 } => write!(f, "missing field: {source}/{subject}.{field}"),
100 Self::DiffUnavailable { subject } => write!(f, "diff unavailable: {subject}"),
101 Self::Unsupported { source, capability } => {
102 write!(f, "unsupported: {source}/{capability}")
103 }
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct ChangeRequestId {
111 pub system: String,
112 pub value: String,
113}
114
115impl ChangeRequestId {
116 pub fn new(system: impl Into<String>, value: impl Into<String>) -> Self {
117 Self {
118 system: system.into(),
119 value: value.into(),
120 }
121 }
122}
123
124impl fmt::Display for ChangeRequestId {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 write!(f, "{}:{}", self.system, self.value)
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct WorkItemRef {
133 pub system: String,
134 pub value: String,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct ChangedAsset {
140 pub path: String,
141 pub diff_available: bool,
142 #[serde(default)]
143 pub additions: u32,
144 #[serde(default)]
145 pub deletions: u32,
146 #[serde(default)]
147 pub status: String,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub diff: Option<String>,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "snake_case")]
155pub enum ApprovalDisposition {
156 Approved,
157 Rejected,
158 Commented,
159 Dismissed,
160 Unknown,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct ApprovalDecision {
166 pub actor: String,
167 pub disposition: ApprovalDisposition,
168 pub submitted_at: Option<String>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct AuthenticityEvidence {
174 pub verified: bool,
175 pub mechanism: Option<String>,
176}
177
178impl AuthenticityEvidence {
179 pub fn new(verified: bool, mechanism: Option<String>) -> Self {
180 Self {
181 verified,
182 mechanism,
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct SourceRevision {
190 pub id: String,
191 pub authored_by: Option<String>,
192 pub committed_at: Option<String>,
193 pub merge: bool,
194 pub authenticity: EvidenceState<AuthenticityEvidence>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct GovernedChange {
200 pub id: ChangeRequestId,
201 pub title: String,
202 pub summary: Option<String>,
203 pub submitted_by: Option<String>,
204 pub changed_assets: EvidenceState<Vec<ChangedAsset>>,
205 pub approval_decisions: EvidenceState<Vec<ApprovalDecision>>,
206 pub source_revisions: EvidenceState<Vec<SourceRevision>>,
207 pub work_item_refs: EvidenceState<Vec<WorkItemRef>>,
208}
209
210impl GovernedChange {
211 pub fn is_bot_submitted(&self) -> bool {
215 let Some(author) = self.submitted_by.as_deref() else {
216 return false;
217 };
218 let lower = author.to_ascii_lowercase();
219 const BOT_SUBMITTERS: &[&str] = &[
220 "bors",
221 "bors[bot]",
222 "mergify[bot]",
223 "mergify",
224 "dependabot[bot]",
225 "dependabot",
226 "renovate[bot]",
227 "renovate",
228 "k8s-ci-robot",
229 "github-actions[bot]",
230 "copybara-service[bot]",
231 ];
232 BOT_SUBMITTERS.contains(&lower.as_str()) || lower.ends_with("[bot]")
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct PromotionBatch {
239 pub id: String,
240 pub source_revisions: EvidenceState<Vec<SourceRevision>>,
241 pub linked_change_requests: EvidenceState<Vec<ChangeRequestId>>,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(tag = "outcome", rename_all = "snake_case")]
247pub enum VerificationOutcome {
248 Verified,
250 ChecksumMatch,
253 SignatureInvalid {
254 detail: String,
255 },
256 SignerMismatch {
257 detail: String,
258 },
259 TransparencyLogMissing {
260 detail: String,
261 },
262 AttestationAbsent {
263 detail: String,
264 },
265 DigestMismatch {
266 detail: String,
267 },
268 Failed {
269 detail: String,
270 },
271}
272
273impl VerificationOutcome {
274 pub fn is_verified(&self) -> bool {
276 matches!(self, Self::Verified | Self::ChecksumMatch)
277 }
278
279 pub fn is_cryptographically_signed(&self) -> bool {
281 matches!(self, Self::Verified)
282 }
283
284 pub fn failure_detail(&self) -> Option<&str> {
285 match self {
286 Self::Verified | Self::ChecksumMatch => None,
287 Self::SignatureInvalid { detail }
288 | Self::SignerMismatch { detail }
289 | Self::TransparencyLogMissing { detail }
290 | Self::AttestationAbsent { detail }
291 | Self::DigestMismatch { detail }
292 | Self::Failed { detail } => Some(detail),
293 }
294 }
295
296 pub fn failure_kind(&self) -> Option<&'static str> {
297 match self {
298 Self::Verified | Self::ChecksumMatch => None,
299 Self::SignatureInvalid { .. } => Some("signature_invalid"),
300 Self::SignerMismatch { .. } => Some("signer_mismatch"),
301 Self::TransparencyLogMissing { .. } => Some("transparency_log_missing"),
302 Self::AttestationAbsent { .. } => Some("attestation_absent"),
303 Self::DigestMismatch { .. } => Some("digest_mismatch"),
304 Self::Failed { .. } => Some("failed"),
305 }
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct ArtifactAttestation {
312 pub subject: String,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub subject_digest: Option<String>,
315 pub predicate_type: String,
316 pub signer_workflow: Option<String>,
317 pub source_repo: Option<String>,
318 pub verification: VerificationOutcome,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case")]
324pub enum CheckConclusion {
325 Success,
326 Failure,
327 Neutral,
328 Cancelled,
329 Skipped,
330 TimedOut,
331 ActionRequired,
332 Pending,
333 Unknown,
334}
335
336#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
338pub struct CheckRunEvidence {
339 pub name: String,
340 pub conclusion: CheckConclusion,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub app_slug: Option<String>,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356pub struct DependencySignatureEvidence {
357 pub name: String,
359 pub version: String,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub registry: Option<String>,
364 pub verification: VerificationOutcome,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub signature_mechanism: Option<String>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub signer_identity: Option<String>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub source_repo: Option<String>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub source_commit: Option<String>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub pinned_digest: Option<String>,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub actual_digest: Option<String>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub transparency_log_uri: Option<String>,
393 #[serde(default = "default_true")]
396 pub is_direct: bool,
397}
398
399fn default_true() -> bool {
400 true
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
428pub enum RegistryProvenanceCapability {
429 ChecksumOnly,
431 CryptographicProvenance,
433 FullTrustChain,
435}
436
437impl DependencySignatureEvidence {
438 pub fn registry_provenance_capability(&self) -> RegistryProvenanceCapability {
445 match self.registry.as_deref() {
446 Some(r) if r.contains("npmjs.org") => RegistryProvenanceCapability::FullTrustChain,
447 Some("pypi.org") => RegistryProvenanceCapability::FullTrustChain,
448 _ => RegistryProvenanceCapability::ChecksumOnly,
449 }
450 }
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455pub struct CodeownersEntry {
456 pub pattern: String,
458 pub owners: Vec<String>,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
468pub struct RepositoryPosture {
469 pub codeowners_entries: Vec<CodeownersEntry>,
471
472 #[serde(default = "default_true")]
477 pub security_analysis_available: bool,
478
479 pub secret_scanning_enabled: bool,
482 #[serde(default)]
484 pub secret_push_protection_enabled: bool,
485
486 pub vulnerability_scanning_enabled: bool,
489 #[serde(default)]
491 pub code_scanning_enabled: bool,
492
493 pub security_policy_present: bool,
496 pub security_policy_has_disclosure: bool,
498
499 #[serde(default)]
502 pub default_branch_protected: bool,
503
504 #[serde(default)]
507 pub enforce_admins: bool,
508 #[serde(default)]
510 pub dismiss_stale_reviews: bool,
511 #[serde(default)]
512 pub unpinned_action_refs: Vec<UnpinnedActionRef>,
513 #[serde(default)]
514 pub production_environment_protected: bool,
515 #[serde(default)]
516 pub open_high_severity_alerts: u32,
517 #[serde(default)]
518 pub copyleft_dependencies: Vec<CopyleftDependency>,
519 #[serde(default)]
520 pub release_has_sbom: bool,
521 #[serde(default)]
522 pub release_assets_attested: bool,
523 #[serde(default)]
524 pub privileged_workflows: Vec<PrivilegedWorkflow>,
525
526 #[serde(default)]
530 pub default_workflow_permissions: String,
531
532 #[serde(default)]
535 pub dependency_update_tool_configured: bool,
536
537 #[serde(default)]
540 pub admin_count: u32,
541 #[serde(default)]
543 pub direct_collaborator_count: u32,
544
545 #[serde(default)]
548 pub tag_protection_enabled: bool,
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
552pub struct UnpinnedActionRef {
553 pub workflow_file: String,
554 pub action_ref: String,
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
558pub struct CopyleftDependency {
559 pub name: String,
560 pub license: String,
561}
562
563#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
564pub struct PrivilegedWorkflow {
565 pub file: String,
566 pub trigger: String,
567 pub risk: String,
568}
569
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
572pub struct BuildPlatformEvidence {
573 pub platform: String,
574 pub hosted: bool,
575 pub ephemeral: bool,
576 pub isolated: bool,
577 pub runner_labels: Vec<String>,
578 pub signing_key_isolated: bool,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
587pub struct AgentAction {
588 pub tool: String,
589 pub command: String,
590 #[serde(default)]
591 pub timestamp: Option<String>,
592}
593
594#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
596pub struct AgentActionLog {
597 pub agent_id: String,
598 pub session_id: String,
599 pub actions: Vec<AgentAction>,
600}
601
602#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
604pub struct AgentSpec {
605 #[serde(default)]
606 pub allowed_paths: Vec<String>,
607 #[serde(default)]
608 pub forbidden_paths: Vec<String>,
609 #[serde(default)]
610 pub allowed_tools: Vec<String>,
611 #[serde(default)]
612 pub max_steps: Option<u32>,
613 #[serde(default)]
614 pub budget_cents: Option<u32>,
615 #[serde(default)]
618 pub custom_destructive_patterns: Vec<String>,
619 #[serde(default)]
621 pub forbidden_mcp_servers: Vec<String>,
622}
623
624#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
626pub struct AgentExecution {
627 pub agent_id: String,
628 pub session_id: String,
629 #[serde(default)]
630 pub files_touched: Vec<String>,
631 #[serde(default)]
632 pub tools_used: Vec<String>,
633 #[serde(default)]
634 pub steps_taken: u32,
635 #[serde(default)]
636 pub cost_cents: u32,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
641pub struct McpToolCall {
642 pub server: String,
644 pub tool: String,
646 #[serde(default)]
648 pub success: bool,
649 #[serde(default, skip_serializing_if = "Option::is_none")]
651 pub timestamp: Option<String>,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub duration_ms: Option<u64>,
655}
656
657#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
659pub struct PrivilegedGitEvent {
660 pub actor: String,
661 pub action: PrivilegedAction,
662 pub branch: Option<String>,
663 pub tag: Option<String>,
664 #[serde(default)]
665 pub timestamp: Option<String>,
666 #[serde(default)]
667 pub commit_sha: Option<String>,
668 #[serde(default)]
669 pub detail: Option<String>,
670}
671
672#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
674#[serde(rename_all = "kebab-case")]
675pub enum PrivilegedAction {
676 ForcePush,
677 DirectPushToDefault,
678 AdminBypassProtection,
679 BranchDeletion,
680 TagDeletion,
681 ProtectionRuleOverride,
682}
683
684impl PrivilegedAction {
685 pub fn as_str(&self) -> &'static str {
686 match self {
687 Self::ForcePush => "force-push",
688 Self::DirectPushToDefault => "direct-push-to-default",
689 Self::AdminBypassProtection => "admin-bypass-protection",
690 Self::BranchDeletion => "branch-deletion",
691 Self::TagDeletion => "tag-deletion",
692 Self::ProtectionRuleOverride => "protection-rule-override",
693 }
694 }
695}
696
697#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
699pub struct HarnessResult {
700 pub name: String,
702 pub passed: bool,
704 #[serde(default)]
706 pub total: u32,
707 #[serde(default)]
709 pub passed_count: u32,
710 #[serde(default)]
712 pub failed_count: u32,
713 #[serde(default)]
715 pub skipped_count: u32,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub duration_secs: Option<f64>,
719 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub source_format: Option<String>,
722}
723
724#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
726pub struct CoverageReport {
727 pub line_coverage_pct: f64,
729 #[serde(default)]
731 pub lines_total: u32,
732 #[serde(default)]
734 pub lines_covered: u32,
735 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub branch_coverage_pct: Option<f64>,
738 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub source_format: Option<String>,
741}
742
743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
745pub struct ContainerImageEvidence {
746 pub reference: String,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
750 pub digest: Option<String>,
751 pub signature_verified: bool,
753 pub provenance_present: bool,
755 #[serde(default)]
757 pub sbom_present: bool,
758 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub signer_identity: Option<String>,
761 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub source_repo: Option<String>,
764 pub verification: VerificationOutcome,
766}
767
768#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
770pub struct MetricObservation {
771 pub name: String,
773 pub current: f64,
775 pub baseline: f64,
777 #[serde(default, skip_serializing_if = "Option::is_none")]
779 pub unit: Option<String>,
780 #[serde(default, skip_serializing_if = "Option::is_none")]
782 pub window_secs: Option<u64>,
783}
784
785#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
787pub struct BehavioralDiff {
788 pub deployment_id: String,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
792 pub environment: Option<String>,
793 pub metrics: Vec<MetricObservation>,
795 #[serde(default, skip_serializing_if = "Option::is_none")]
797 pub observed_at: Option<String>,
798}
799
800#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
802pub struct EvidenceBundle {
803 pub change_requests: Vec<GovernedChange>,
804 pub promotion_batches: Vec<PromotionBatch>,
805 pub artifact_attestations: EvidenceState<Vec<ArtifactAttestation>>,
806 pub check_runs: EvidenceState<Vec<CheckRunEvidence>>,
807 pub build_platform: EvidenceState<Vec<BuildPlatformEvidence>>,
808 pub dependency_signatures: EvidenceState<Vec<DependencySignatureEvidence>>,
809 #[serde(default)]
810 pub repository_posture: EvidenceState<RepositoryPosture>,
811 #[serde(default)]
813 pub container_images: EvidenceState<Vec<ContainerImageEvidence>>,
814 #[serde(default)]
816 pub agent_action_log: EvidenceState<AgentActionLog>,
817 #[serde(default)]
818 pub agent_spec: EvidenceState<AgentSpec>,
819 #[serde(default)]
820 pub agent_execution: EvidenceState<AgentExecution>,
821 #[serde(default)]
822 pub privileged_git_events: EvidenceState<Vec<PrivilegedGitEvent>>,
823 #[serde(default)]
824 pub mcp_tool_calls: EvidenceState<Vec<McpToolCall>>,
825 #[serde(default)]
827 pub harness_results: EvidenceState<Vec<HarnessResult>>,
828 #[serde(default)]
829 pub coverage_report: EvidenceState<CoverageReport>,
830 #[serde(default)]
832 pub behavioral_diff: EvidenceState<BehavioralDiff>,
833}