From 44d67cea1bd78ef71630b788ee5f964854e550e6 Mon Sep 17 00:00:00 2001 From: Mick Grove Date: Sat, 2 May 2026 00:14:31 -0700 Subject: [PATCH] added SLSA provenance --- .github/workflows/release.yml | 41 ++++++++++++++++++++++++++++++----- README.md | 31 +++++++++++++++++++++----- src/access_map.rs | 5 +---- src/access_map/pinecone.rs | 26 +++++++--------------- src/reporter.rs | 7 ++---- src/update.rs | 4 +--- 6 files changed, 73 insertions(+), 41 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e1faff..136ebe9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -347,13 +347,47 @@ jobs: run: | awk ' BEGIN { grabbing = 0 } - /^## \[/ { + /^## \[/ { if (grabbing) exit; # already grabbed latest entry grabbing = 1 } grabbing { print } ' CHANGELOG.md > .latest_changelog.md + # ── Sign every release artifact with a SLSA v1 build-provenance attestation. + # actions/attest-build-provenance produces a multi-subject Sigstore Bundle + # in JSONL format and writes it to bundle-path. We ship that file alongside + # the binaries as `multiple.intoto.jsonl` so users can verify offline with + # `gh attestation verify`, `cosign`, or `slsa-verifier` — no GitHub API call + # required at verify time. This also satisfies the OSSF Scorecard + # `Signed-Releases` check, which scans for *.intoto.jsonl in release assets. + - name: Attest build provenance + id: attest + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + # Match the actual artifact files under target/release/ (download-artifact + # places them in a subdirectory, so we use ** to recurse). + subject-path: | + target/release/**/kingfisher-*.tgz + target/release/**/kingfisher-*.zip + target/release/**/kingfisher-*.deb + target/release/**/kingfisher-*.rpm + + - name: Stage attestation bundle as a release asset + shell: bash + run: | + set -euo pipefail + BUNDLE_PATH='${{ steps.attest.outputs.bundle-path }}' + if [[ -z "${BUNDLE_PATH}" || ! -f "${BUNDLE_PATH}" ]]; then + echo "::error::attest-build-provenance did not produce a bundle at '${BUNDLE_PATH}'" + exit 1 + fi + # Use the slsa-verifier-recognized filename for multi-subject bundles. + mkdir -p target/release + cp "${BUNDLE_PATH}" target/release/multiple.intoto.jsonl + echo "Bundle line count (one DSSE-wrapped attestation per subject):" + wc -l target/release/multiple.intoto.jsonl + # ── create the release using just that snippet ───────────────────── - name: Create release & upload assets uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 @@ -365,11 +399,6 @@ jobs: generateReleaseNotes: false artifacts: target/release/** - - name: Attest build provenance - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - with: - subject-path: 'target/release/*' - # ──────────────── Publish Docker image ──────────────── publish-docker: needs: [release] diff --git a/README.md b/README.md index 5e5cc7a..a1d3ebd 100644 --- a/README.md +++ b/README.md @@ -318,19 +318,40 @@ Kingfisher supports multiple installation methods: ## Verifying Releases -Every Kingfisher release includes GitHub build attestations so you can verify that artifacts were built by our CI pipeline and haven't been tampered with. +Every release ships [SLSA v1 build-provenance attestations](https://github.com/actions/attest-build-provenance) (Sigstore keyless OIDC) proving the artifact was built by our CI workflow at a known commit and hasn't been tampered with. Attestations are available via the GitHub attestation store or as the `multiple.intoto.jsonl` release asset. -### GitHub attestations +**Option 1 — `gh attestation verify`** (simplest; requires [GitHub CLI](https://cli.github.com/)) -Release artifacts have GitHub build attestations, verifiable with the GitHub CLI: +```bash +gh release download --repo mongodb/kingfisher --pattern 'kingfisher-linux-x64.tgz' +gh attestation verify kingfisher-linux-x64.tgz --repo mongodb/kingfisher +``` + +**Option 2 — `cosign`** (offline-friendly; requires [cosign](https://docs.sigstore.dev/system_config/installation/) ≥ 2.x) ```bash gh release download --repo mongodb/kingfisher \ - --pattern 'kingfisher-linux-x64.tgz' + --pattern 'kingfisher-linux-x64.tgz' --pattern 'multiple.intoto.jsonl' -gh attestation verify kingfisher-linux-x64.tgz --repo mongodb/kingfisher +cosign verify-blob-attestation \ + --bundle multiple.intoto.jsonl \ + --new-bundle-format \ + --certificate-identity-regexp '^https://github.com/mongodb/kingfisher/\.github/workflows/release\.yml@refs/tags/v.*$' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + kingfisher-linux-x64.tgz ``` +**Option 3 — `slsa-verifier`** (requires [slsa-verifier](https://github.com/slsa-framework/slsa-verifier)) + +```bash +slsa-verifier verify-artifact kingfisher-linux-x64.tgz \ + --provenance-path multiple.intoto.jsonl \ + --source-uri github.com/mongodb/kingfisher \ + --source-tag +``` + +A successful verification prints `Verified OK`. The attestation proves the artifact's SHA-256, the signing identity (the release workflow at a specific tag), and the source commit — all recorded in the public [Rekor transparency log](https://search.sigstore.dev/). + ## Report Viewer (local and hosted) diff --git a/src/access_map.rs b/src/access_map.rs index 3f39d28..b5c42bf 100644 --- a/src/access_map.rs +++ b/src/access_map.rs @@ -317,10 +317,7 @@ impl PermissionSummary { } pub fn total(&self) -> usize { - self.admin.len() - + self.privilege_escalation.len() - + self.risky.len() - + self.read_only.len() + self.admin.len() + self.privilege_escalation.len() + self.risky.len() + self.read_only.len() } } diff --git a/src/access_map/pinecone.rs b/src/access_map/pinecone.rs index de0e7f4..17f975c 100644 --- a/src/access_map/pinecone.rs +++ b/src/access_map/pinecone.rs @@ -138,10 +138,8 @@ pub async fn map_access_from_token(token: &str) -> Result { for index in &indexes { let name = index.name.clone().unwrap_or_else(|| "unknown".to_string()); let metric = index.metric.as_deref().unwrap_or("unknown"); - let dimension = index - .dimension - .map(|d| d.to_string()) - .unwrap_or_else(|| "unknown".to_string()); + let dimension = + index.dimension.map(|d| d.to_string()).unwrap_or_else(|| "unknown".to_string()); let ready = index.status.as_ref().and_then(|s| s.ready).unwrap_or(false); let state = index .status @@ -175,13 +173,9 @@ pub async fn map_access_from_token(token: &str) -> Result { } } - let host_suffix = - index.host.as_ref().map(|h| format!(" ({h})")).unwrap_or_default(); - let location_suffix = if location.is_empty() { - String::new() - } else { - format!(" — {location}") - }; + let host_suffix = index.host.as_ref().map(|h| format!(" ({h})")).unwrap_or_default(); + let location_suffix = + if location.is_empty() { String::new() } else { format!(" — {location}") }; let ready_marker = if ready { "ready" } else { "not ready" }; resources.push(ResourceExposure { @@ -316,16 +310,12 @@ async fn fetch_collections(client: &Client, token: &str) -> Result Severity { +fn derive_severity(indexes: &[PineconeIndex], collections: &[PineconeCollection]) -> Severity { let index_count = indexes.len(); let collection_count = collections.len(); let total = index_count + collection_count; - let any_unprotected = indexes - .iter() - .any(|i| i.deletion_protection.as_deref().unwrap_or("disabled") != "enabled"); + let any_unprotected = + indexes.iter().any(|i| i.deletion_protection.as_deref().unwrap_or("disabled") != "enabled"); if index_count > 10 { return Severity::High; diff --git a/src/reporter.rs b/src/reporter.rs index 0142969..9b2243d 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -1256,11 +1256,8 @@ impl DetailsReporter { groups.sort_by(|a, b| a.resources.cmp(&b.resources)); - let permissions_by_severity = if result.permissions.is_empty() { - None - } else { - Some(result.permissions.clone()) - }; + let permissions_by_severity = + if result.permissions.is_empty() { None } else { Some(result.permissions.clone()) }; let context = AccessIdentityContext::from_summary(&result.identity); entries.push(AccessMapEntry { diff --git a/src/update.rs b/src/update.rs index 7eb29f4..53dde7c 100644 --- a/src/update.rs +++ b/src/update.rs @@ -328,9 +328,7 @@ pub fn rewrite_argv_for_reexec(argv: impl IntoIterator) -> Vec< #[cfg(not(any(unix, windows)))] { // Fallback for unknown targets: best-effort UTF-8 conversion. - tok.to_str() - .map(|s| s.as_bytes().starts_with(prefix)) - .unwrap_or(false) + tok.to_str().map(|s| s.as_bytes().starts_with(prefix)).unwrap_or(false) } }