Skip to main content

sigstore_verification/sources/
github.rs

1use crate::Result;
2use crate::api::{Attestation, AttestationClient, FetchParams};
3use crate::sources::{ArtifactRef, AttestationSource};
4use async_trait::async_trait;
5
6/// GitHub source for fetching artifact attestations from GitHub's API
7pub struct GitHubSource {
8    client: AttestationClient,
9    owner: String,
10    repo: String,
11}
12
13impl GitHubSource {
14    /// Create a new `GitHubSource` targeting `https://api.github.com`.
15    pub fn new(
16        owner: impl Into<String>,
17        repo: impl Into<String>,
18        token: Option<&str>,
19    ) -> Result<Self> {
20        let mut builder = Self::builder().owner(owner).repo(repo);
21        if let Some(token) = token {
22            builder = builder.token(token);
23        }
24        builder.build()
25    }
26
27    /// Create a new `GitHubSource` targeting a custom API base URL (e.g. a
28    /// GitHub Enterprise Server instance such as
29    /// `https://github.enterprise.com/api/v3`).
30    pub fn with_base_url(
31        owner: impl Into<String>,
32        repo: impl Into<String>,
33        token: Option<&str>,
34        base_url: &str,
35    ) -> Result<Self> {
36        let mut builder = Self::builder().owner(owner).repo(repo).base_url(base_url);
37        if let Some(token) = token {
38            builder = builder.token(token);
39        }
40        builder.build()
41    }
42
43    /// Create a new `GitHubSource` from an already-built [`AttestationClient`],
44    /// allowing the caller to configure the HTTP client (base URL, token, etc.)
45    /// independently.
46    pub fn with_client(
47        owner: impl Into<String>,
48        repo: impl Into<String>,
49        client: AttestationClient,
50    ) -> Self {
51        Self {
52            client,
53            owner: owner.into(),
54            repo: repo.into(),
55        }
56    }
57
58    /// Start building a `GitHubSource` with a fluent builder.
59    pub fn builder() -> GitHubSourceBuilder {
60        GitHubSourceBuilder::default()
61    }
62}
63
64/// Builder for [`GitHubSource`].
65#[derive(Debug, Default)]
66pub struct GitHubSourceBuilder {
67    owner: Option<String>,
68    repo: Option<String>,
69    token: Option<String>,
70    base_url: Option<String>,
71}
72
73impl GitHubSourceBuilder {
74    pub fn owner(mut self, owner: impl Into<String>) -> Self {
75        self.owner = Some(owner.into());
76        self
77    }
78
79    pub fn repo(mut self, repo: impl Into<String>) -> Self {
80        self.repo = Some(repo.into());
81        self
82    }
83
84    pub fn token(mut self, token: impl Into<String>) -> Self {
85        self.token = Some(token.into());
86        self
87    }
88
89    pub fn base_url(mut self, url: impl Into<String>) -> Self {
90        self.base_url = Some(url.into());
91        self
92    }
93
94    pub fn build(self) -> Result<GitHubSource> {
95        let owner = self.owner.ok_or_else(|| {
96            crate::AttestationError::Api("GitHubSource: owner is required".into())
97        })?;
98        let repo = self
99            .repo
100            .ok_or_else(|| crate::AttestationError::Api("GitHubSource: repo is required".into()))?;
101
102        let mut client_builder = AttestationClient::builder();
103        if let Some(token) = self.token {
104            client_builder = client_builder.github_token(&token);
105        }
106        if let Some(base_url) = self.base_url {
107            client_builder = client_builder.base_url(&base_url);
108        }
109        let client = client_builder.build()?;
110
111        Ok(GitHubSource {
112            client,
113            owner,
114            repo,
115        })
116    }
117}
118
119#[async_trait]
120impl AttestationSource for GitHubSource {
121    /// Fetch attestations for an artifact from the configured GitHub API.
122    ///
123    /// Note: this filters the GitHub API response to SLSA provenance v1
124    /// (`https://slsa.dev/provenance/v1`) because `GitHubSource` is intended
125    /// for SLSA verification via the generic `verify_artifact` + `SlsaVerifier`
126    /// flow. Callers who need the unfiltered attestation set (e.g. non-SLSA
127    /// bundles such as SPDX SBOMs) should use
128    /// [`crate::verify_github_attestation`] /
129    /// [`crate::verify_github_attestation_with_base_url`], which send no
130    /// predicate filter, or drive [`AttestationClient`] directly.
131    async fn fetch_attestations(&self, artifact: &ArtifactRef) -> Result<Vec<Attestation>> {
132        let params = FetchParams {
133            owner: self.owner.clone(),
134            repo: Some(format!("{}/{}", self.owner, self.repo)),
135            digest: artifact.digest.clone(),
136            limit: 30,
137            predicate_type: Some("https://slsa.dev/provenance/v1".to_string()),
138        };
139
140        self.client.fetch_attestations(params).await
141    }
142
143    fn source_type(&self) -> &'static str {
144        "GitHub"
145    }
146}