Skip to main content

libverify_core/
evidence.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5/// Represents the completeness of a collected evidence value.
6///
7/// Controls use this to distinguish between a verified absence and an
8/// evidence-collection failure, which maps to different control statuses.
9#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
10#[serde(tag = "state", rename_all = "snake_case")]
11pub enum EvidenceState<T> {
12    /// All expected data was collected successfully.
13    Complete { value: T },
14    /// Data was collected but some aspects are missing or degraded.
15    Partial { value: T, gaps: Vec<EvidenceGap> },
16    /// No usable data could be collected; only gap descriptions remain.
17    Missing { gaps: Vec<EvidenceGap> },
18    /// The evidence category does not apply to this context.
19    #[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/// Describes why a piece of evidence is incomplete or absent.
67#[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/// Platform-independent identifier for a change request (e.g. a pull request).
116#[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/// Reference to an external work item (issue, Jira ticket, etc.).
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct WorkItemRef {
140    pub system: String,
141    pub value: String,
142}
143
144/// A file or artifact that was modified in a change request.
145#[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/// Normalized outcome of a review action, independent of platform terminology.
160#[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/// A single review decision recorded against a change request.
171#[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/// Cryptographic verification state for a source revision.
179#[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/// A single commit or source revision associated with a change request.
195#[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/// Normalized representation of a governed change request (e.g. a pull request).
205#[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    /// Returns true if this change was submitted by a known merge/rollup bot.
219    /// Bot-submitted PRs aggregate already-reviewed changes and should not
220    /// be individually evaluated for review controls.
221    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/// A release or deployment batch that promotes one or more source revisions.
244#[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/// Structured outcome of attestation verification.
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(tag = "outcome", rename_all = "snake_case")]
254pub enum VerificationOutcome {
255    /// Cryptographic signature verified (Sigstore, PGP, cosign, etc.).
256    Verified,
257    /// Checksum/integrity hash matched but no cryptographic signature was verified.
258    /// This confirms download integrity but NOT authenticity.
259    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    /// Returns true for both `Verified` (signature) and `ChecksumMatch` (integrity).
282    pub fn is_verified(&self) -> bool {
283        matches!(self, Self::Verified | Self::ChecksumMatch)
284    }
285
286    /// Returns true only for cryptographic signature verification.
287    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/// Result of verifying an artifact's build provenance attestation.
317#[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/// Conclusion of a CI check run, normalized across platforms.
329#[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/// Evidence for a single CI check run executed against a commit.
344#[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/// Provenance and signature verification evidence for a single dependency.
353///
354/// Supports multiple verification mechanisms including:
355/// - **npm provenance**: Sigstore-signed SLSA provenance via `npm audit signatures`
356/// - **Sigstore/cosign**: General Sigstore verification with Rekor transparency log
357/// - **PGP signatures**: Traditional GPG/PGP package signatures
358/// - **Checksum pinning**: Lock-file checksum verification (e.g. Cargo.lock, package-lock.json)
359///
360/// The `verification` field uses `VerificationOutcome` for structured failure reasons,
361/// matching the pattern used by `ArtifactAttestation`.
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct DependencySignatureEvidence {
364    /// Package name (e.g. "serde", "lodash").
365    pub name: String,
366    /// Package version (e.g. "1.0.204", "4.17.21").
367    pub version: String,
368    /// Registry origin (e.g. "crates.io", "registry.npmjs.org").
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub registry: Option<String>,
371    /// Structured verification outcome, reusing `VerificationOutcome` for consistency
372    /// with `ArtifactAttestation`. `Verified` = signature valid, otherwise structured failure.
373    pub verification: VerificationOutcome,
374    /// Signing mechanism (e.g. "sigstore", "pgp", "checksum").
375    #[serde(default, skip_serializing_if = "Option::is_none")]
376    pub signature_mechanism: Option<String>,
377    /// Signer identity: OIDC issuer URI, public key fingerprint, or email.
378    /// For npm provenance this is the GitHub Actions OIDC token subject.
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub signer_identity: Option<String>,
381    /// Source repository that built the package (from SLSA provenance predicate).
382    #[serde(default, skip_serializing_if = "Option::is_none")]
383    pub source_repo: Option<String>,
384    /// Source commit SHA at which the package was built.
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub source_commit: Option<String>,
387    /// Expected artifact digest from lock file (e.g. "sha512:..." from Cargo.lock/package-lock.json).
388    /// Populated by lock-file parsers. Compare with `actual_digest` to detect artifact replacement.
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub pinned_digest: Option<String>,
391    /// Actual artifact digest computed from downloaded artifact at install/build time.
392    /// Populated by build-time adapters (not lock-file parsers). When both `pinned_digest`
393    /// and `actual_digest` are present, `has_digest_mismatch()` in the control detects
394    /// registry-side artifact replacement attacks.
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub actual_digest: Option<String>,
397    /// Transparency log entry URL (e.g. Rekor log index for Sigstore).
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub transparency_log_uri: Option<String>,
400    /// Whether this is a direct dependency (true) or transitive (false).
401    /// Transitive dependencies are more susceptible to typosquatting attacks.
402    #[serde(default = "default_true")]
403    pub is_direct: bool,
404}
405
406fn default_true() -> bool {
407    true
408}
409
410/// Provenance capability levels supported by a package registry.
411///
412/// Registries evolve at different speeds. This enum captures the highest
413/// SLSA Dependencies level a registry's infrastructure can currently support,
414/// allowing controls to skip dependencies from registries that lack the
415/// required infrastructure rather than producing false positives.
416///
417/// Current ecosystem status (as of March 2026):
418/// - **npm** (`registry.npmjs.org`): L3 — Sigstore keyless signing + Rekor.
419///   GA since Oct 2023, 134+ high-impact projects adopted.
420/// - **PyPI** (`pypi.org`): L3 — Trusted Publishers + Sigstore attestations
421///   (Fulcio + Rekor, same stack as npm). 17% of uploads include attestations.
422///   Packages with attestations provide full L3: signer identity
423///   (publisher.repository + Fulcio cert SAN) and Rekor transparency log.
424/// - **Maven Central**: L3 capability — Sigstore `.sigstore.json` validation
425///   added Jan 2025 (opt-in). PGP `.asc` still mandatory. Very low Sigstore
426///   adoption. No dedicated query API (URL convention only).
427/// - **crates.io**: L1 only — SHA-256 checksums in Cargo.lock.
428///   Trusted Publishing (RFC #3691) covers auth only; Sigstore RFC #3403
429///   proposed but not merged.
430/// - **Go** (`proxy.golang.org`): L1 only — `sum.golang.org` provides
431///   tamper-evident checksum log but no provenance/signing.
432/// - **NuGet** (`nuget.org`): L1 — X.509 signing exists but no
433///   Sigstore/attestation API at registry level.
434#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
435pub enum RegistryProvenanceCapability {
436    /// L1: integrity only (checksum). No cryptographic signing infrastructure.
437    ChecksumOnly,
438    /// L2: cryptographic signature + source provenance available.
439    CryptographicProvenance,
440    /// L3: signature + signer identity + transparency log available.
441    FullTrustChain,
442}
443
444impl DependencySignatureEvidence {
445    /// Returns the provenance capability level of this dependency's registry.
446    ///
447    /// This determines whether higher-level controls (L2 provenance, L3 signer
448    /// verification) are meaningful for this dependency. Dependencies from
449    /// registries that lack the required infrastructure are excluded from
450    /// evaluation rather than producing false positives.
451    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/// A single CODEOWNERS entry mapping a file pattern to its designated owners.
461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462pub struct CodeownersEntry {
463    /// File pattern (e.g. "*.rs", "/src/auth/", "*").
464    pub pattern: String,
465    /// Designated owners (e.g. "@org/security-team", "alice@example.com").
466    pub owners: Vec<String>,
467}
468
469/// Repository-level security posture evidence for ASPM controls.
470///
471/// Captures configuration-level signals that are independent of any single
472/// change request: code ownership, scanning settings, and security policy.
473/// Designed to be populated from GitHub REST API, GitLab API, or other platform adapters.
474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
475pub struct RepositoryPosture {
476    /// Parsed CODEOWNERS entries. Empty vec means no CODEOWNERS file found.
477    pub codeowners_entries: Vec<CodeownersEntry>,
478
479    // --- Security analysis availability ---
480    /// Whether the security_and_analysis API field was available.
481    /// `false` means the API token lacked permission to read security settings,
482    /// so `secret_scanning_enabled` / `vulnerability_scanning_enabled` etc. may be inaccurate.
483    #[serde(default = "default_true")]
484    pub security_analysis_available: bool,
485
486    // --- Secret scanning (CC6.1 / CC6.6) ---
487    /// Whether secret scanning is enabled (detection).
488    pub secret_scanning_enabled: bool,
489    /// Whether push protection is enabled (prevention). Requires GHAS on private repos.
490    #[serde(default)]
491    pub secret_push_protection_enabled: bool,
492
493    // --- Vulnerability scanning (CC7.1) ---
494    /// Whether dependency vulnerability scanning (Dependabot, Snyk, etc.) is enabled.
495    pub vulnerability_scanning_enabled: bool,
496    /// Whether code scanning / SAST (CodeQL, Semgrep, etc.) is enabled.
497    #[serde(default)]
498    pub code_scanning_enabled: bool,
499
500    // --- Security policy (CC7.3 / CC7.4) ---
501    /// Whether a SECURITY.md or equivalent security policy file exists.
502    pub security_policy_present: bool,
503    /// Whether the security policy describes a responsible disclosure process.
504    pub security_policy_has_disclosure: bool,
505
506    // --- Branch protection (CC6.1 / CC8.1) ---
507    /// Whether the default branch has protection rules configured.
508    #[serde(default)]
509    pub default_branch_protected: bool,
510
511    // --- Branch protection detail (enterprise controls) ---
512    /// Whether branch protection rules are enforced for admins (no bypass).
513    #[serde(default)]
514    pub enforce_admins: bool,
515    /// Whether stale pull request reviews are automatically dismissed on new push.
516    #[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    // --- Workflow permissions (CC6.8 / least privilege) ---
534    /// Default workflow permissions for the repository ("read" or "write").
535    /// Empty string means the field could not be collected.
536    #[serde(default)]
537    pub default_workflow_permissions: String,
538
539    // --- Dependency update tool (Scorecard Dependency-Update-Tool) ---
540    /// Whether a dependency update tool config exists (Dependabot or Renovate).
541    #[serde(default)]
542    pub dependency_update_tool_configured: bool,
543
544    // --- Repository permissions audit (CC6.1 / least privilege) ---
545    /// Number of users with admin access to the repository.
546    #[serde(default)]
547    pub admin_count: u32,
548    /// Number of direct (non-team) collaborators with write or admin access.
549    #[serde(default)]
550    pub direct_collaborator_count: u32,
551
552    // --- Tag protection (SA-10 / release integrity) ---
553    /// Whether at least one tag protection rule exists.
554    #[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/// Build platform evidence for Build Track L2+.
578#[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// ---------------------------------------------------------------------------
589// Dark Factory evidence types (Layers 1, 4)
590// ---------------------------------------------------------------------------
591
592/// A single action performed by an AI agent.
593#[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/// Log of all actions an agent performed in a session (Layer 4).
602#[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/// Spec constraining what an agent is allowed to do (Layer 1).
610#[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    /// Additional destructive command patterns (case-insensitive substring match).
623    /// Extends the built-in default set in `PrivilegedOperationAuditControl`.
624    #[serde(default)]
625    pub custom_destructive_patterns: Vec<String>,
626    /// MCP servers whose tool calls are always forbidden (e.g. "database", "admin").
627    #[serde(default)]
628    pub forbidden_mcp_servers: Vec<String>,
629}
630
631/// Record of what an agent actually did (Layer 1).
632#[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/// A single MCP tool call made by an agent (Layer 4 enhancement).
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648pub struct McpToolCall {
649    /// MCP server name (e.g. "github", "filesystem", "database").
650    pub server: String,
651    /// Tool name (e.g. "create_pull_request", "write_file", "execute_query").
652    pub tool: String,
653    /// Whether the call succeeded.
654    #[serde(default)]
655    pub success: bool,
656    /// Call timestamp (ISO 8601).
657    #[serde(default, skip_serializing_if = "Option::is_none")]
658    pub timestamp: Option<String>,
659    /// Duration in milliseconds.
660    #[serde(default, skip_serializing_if = "Option::is_none")]
661    pub duration_ms: Option<u64>,
662}
663
664/// A structured git/platform event for privileged operation auditing.
665#[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/// Categories of privileged operations that require audit.
680#[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/// Result of a single test/harness execution (Layer 2: Deterministic Gates).
705#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
706pub struct HarnessResult {
707    /// Harness name (e.g. "unit-tests", "integration-tests", "lint", "typecheck").
708    pub name: String,
709    /// Whether the harness passed.
710    pub passed: bool,
711    /// Total number of test cases (if applicable).
712    #[serde(default)]
713    pub total: u32,
714    /// Number of passed test cases.
715    #[serde(default)]
716    pub passed_count: u32,
717    /// Number of failed test cases.
718    #[serde(default)]
719    pub failed_count: u32,
720    /// Number of skipped test cases.
721    #[serde(default)]
722    pub skipped_count: u32,
723    /// Execution duration in seconds.
724    #[serde(default, skip_serializing_if = "Option::is_none")]
725    pub duration_secs: Option<f64>,
726    /// Source format (e.g. "junit-xml", "tap", "custom").
727    #[serde(default, skip_serializing_if = "Option::is_none")]
728    pub source_format: Option<String>,
729}
730
731/// Line/branch coverage metrics from a coverage report (Layer 2).
732#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
733pub struct CoverageReport {
734    /// Coverage percentage (0.0 - 100.0).
735    pub line_coverage_pct: f64,
736    /// Total lines instrumented.
737    #[serde(default)]
738    pub lines_total: u32,
739    /// Lines covered.
740    #[serde(default)]
741    pub lines_covered: u32,
742    /// Branch coverage percentage (optional).
743    #[serde(default, skip_serializing_if = "Option::is_none")]
744    pub branch_coverage_pct: Option<f64>,
745    /// Source format (e.g. "lcov", "cobertura", "clover").
746    #[serde(default, skip_serializing_if = "Option::is_none")]
747    pub source_format: Option<String>,
748}
749
750/// Evidence for a container image's provenance and signature status.
751#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
752pub struct ContainerImageEvidence {
753    /// Full image reference (e.g. "ghcr.io/owner/repo:v1.0.0").
754    pub reference: String,
755    /// Image digest (e.g. "sha256:abcdef...").
756    #[serde(default, skip_serializing_if = "Option::is_none")]
757    pub digest: Option<String>,
758    /// Whether a cosign signature was found and verified.
759    pub signature_verified: bool,
760    /// Whether SLSA provenance attestation exists.
761    pub provenance_present: bool,
762    /// Whether SBOM attestation exists.
763    #[serde(default)]
764    pub sbom_present: bool,
765    /// Signer identity (e.g. OIDC subject from Fulcio cert).
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub signer_identity: Option<String>,
768    /// Source repository from provenance.
769    #[serde(default, skip_serializing_if = "Option::is_none")]
770    pub source_repo: Option<String>,
771    /// Verification outcome (reuse existing VerificationOutcome).
772    pub verification: VerificationOutcome,
773}
774
775/// A single metric observation for behavioral comparison (Layer 3).
776#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
777pub struct MetricObservation {
778    /// Metric name (e.g. "http_request_duration_p99", "error_rate_5xx").
779    pub name: String,
780    /// Current (post-deploy) value.
781    pub current: f64,
782    /// Baseline (pre-deploy) value.
783    pub baseline: f64,
784    /// Unit of measurement (e.g. "ms", "percent", "count/sec").
785    #[serde(default, skip_serializing_if = "Option::is_none")]
786    pub unit: Option<String>,
787    /// Observation window in seconds.
788    #[serde(default, skip_serializing_if = "Option::is_none")]
789    pub window_secs: Option<u64>,
790}
791
792/// Behavioral diff evidence comparing pre-deploy and post-deploy metrics (Layer 3).
793#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
794pub struct BehavioralDiff {
795    /// Deployment identifier (e.g. commit SHA, release tag).
796    pub deployment_id: String,
797    /// Environment (e.g. "production", "staging", "canary").
798    #[serde(default, skip_serializing_if = "Option::is_none")]
799    pub environment: Option<String>,
800    /// Metric observations comparing baseline to current.
801    pub metrics: Vec<MetricObservation>,
802    /// Observation timestamp (ISO 8601).
803    #[serde(default, skip_serializing_if = "Option::is_none")]
804    pub observed_at: Option<String>,
805}
806
807/// Top-level container for all evidence collected from adapters.
808#[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    // Container image attestation evidence
819    #[serde(default)]
820    pub container_images: EvidenceState<Vec<ContainerImageEvidence>>,
821    // Dark Factory evidence (Layers 1, 4)
822    #[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    // Layer 2: Deterministic Gates
833    #[serde(default)]
834    pub harness_results: EvidenceState<Vec<HarnessResult>>,
835    #[serde(default)]
836    pub coverage_report: EvidenceState<CoverageReport>,
837    // Layer 3: Behavioral Diff
838    #[serde(default)]
839    pub behavioral_diff: EvidenceState<BehavioralDiff>,
840}