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)]
468pub struct RepositoryPosture {
469 pub codeowners_entries: Vec<CodeownersEntry>,
471
472 pub secret_scanning_enabled: bool,
475 #[serde(default)]
477 pub secret_push_protection_enabled: bool,
478
479 pub vulnerability_scanning_enabled: bool,
482 #[serde(default)]
484 pub code_scanning_enabled: bool,
485
486 pub security_policy_present: bool,
489 pub security_policy_has_disclosure: bool,
491
492 #[serde(default)]
495 pub default_branch_protected: bool,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
500pub struct BuildPlatformEvidence {
501 pub platform: String,
502 pub hosted: bool,
503 pub ephemeral: bool,
504 pub isolated: bool,
505 pub runner_labels: Vec<String>,
506 pub signing_key_isolated: bool,
507}
508
509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
511pub struct EvidenceBundle {
512 pub change_requests: Vec<GovernedChange>,
513 pub promotion_batches: Vec<PromotionBatch>,
514 pub artifact_attestations: EvidenceState<Vec<ArtifactAttestation>>,
515 pub check_runs: EvidenceState<Vec<CheckRunEvidence>>,
516 pub build_platform: EvidenceState<Vec<BuildPlatformEvidence>>,
517 pub dependency_signatures: EvidenceState<Vec<DependencySignatureEvidence>>,
518 #[serde(default)]
519 pub repository_posture: EvidenceState<RepositoryPosture>,
520}