Files
Packer-Cn/internal/provenance/gitinfo.go
Tanmay Jain 014f8d1bbe FEAT(provenance): Add SLSA provenance attestation and verification (#13667)
* feat(provenance): add SLSA provenance and attestation signing libraries

Add internal/provenance for deriving in-toto subjects from Packer
artifacts, building SLSA Provenance v1 predicates, wrapping in-toto
statements, and best-effort git/CI source detection.

Add internal/attestation for DSSE envelope handling and a pluggable
Signer/Verifier backend supporting key (local PEM), kms (aws/gcp/
azure/hashivault), and keyless (Sigstore Fulcio) modes, plus Sigstore
bundle handling and DSSE/policy verification.

Add the supporting module dependencies in go.mod/go.sum.

* feat(provenance): add provenance post-processor

Add the opt-in "provenance" post-processor that runs after a build,
derives subjects from the artifact, emits DSSE-wrapped SLSA provenance
(and optional SBOM) attestations, signs them via the configured
signing backend, and writes sidecar files (including *.sigstore.json
bundles in keyless mode). Register it in the core post-processor set.

The provenance enable flag is a tri-state so an unset value stays
enabled through HCL2 decoding instead of being silently disabled.

* feat(provenance): add verify-attestation command

Add "packer verify-attestation" to verify signed DSSE attestations
against key, KMS, and keyless policy inputs, including optional
Sigstore bundle checks for Rekor and timestamp evidence. Register the
command in the CLI.

* docs(provenance): add reference CI workflows and changelog

Add reference GitHub Actions workflows under examples/ci for SLSA L2
keyless signing and L3-compatible delegated signing

* fix: lint and tests

* Added docs for Provenance PostProcessor
2026-07-21 13:40:42 +05:30

176 lines
4.3 KiB
Go

// Copyright IBM Corp. 2024, 2025
// SPDX-License-Identifier: BUSL-1.1
package provenance
import (
"fmt"
"net/url"
"os/exec"
"path/filepath"
"strings"
)
type gitRunner func(workingDir string, args ...string) (string, error)
func DetectBuilderID(env map[string]string) string {
for _, key := range []string{"GITHUB_WORKFLOW_REF", "CI_JOB_URL", "CI_PIPELINE_URL", "BUILD_URL"} {
if value := strings.TrimSpace(env[key]); value != "" {
return value
}
}
return DefaultLocalBuilderID
}
func DetectInvocationID(env map[string]string) string {
for _, key := range []string{"GITHUB_RUN_ID", "CI_PIPELINE_ID", "CI_JOB_ID", "BUILD_ID"} {
if value := strings.TrimSpace(env[key]); value != "" {
return value
}
}
return ""
}
func DetectGitDependency(workingDir string, env map[string]string) (ResolvedDependency, bool) {
return detectGitDependency(workingDir, env, runGitCommand)
}
func detectGitDependency(workingDir string, env map[string]string, runner gitRunner) (ResolvedDependency, bool) {
if dependency, ok := detectGitHubDependency(env); ok {
return dependency, true
}
if dependency, ok := detectGitLabDependency(env); ok {
return dependency, true
}
return detectLocalGitDependency(workingDir, runner)
}
func detectGitHubDependency(env map[string]string) (ResolvedDependency, bool) {
repository := strings.TrimSpace(env["GITHUB_REPOSITORY"])
commit := strings.TrimSpace(env["GITHUB_SHA"])
if repository == "" || commit == "" {
return ResolvedDependency{}, false
}
serverURL := strings.TrimRight(strings.TrimSpace(env["GITHUB_SERVER_URL"]), "/")
if serverURL == "" {
serverURL = "https://github.com"
}
uri := fmt.Sprintf("git+%s/%s", serverURL, strings.TrimLeft(repository, "/"))
if ref := strings.TrimSpace(env["GITHUB_REF"]); ref != "" {
uri += "@" + ref
}
return ResolvedDependency{
URI: uri,
Digest: DigestSet{
"gitCommit": commit,
},
}, true
}
func detectGitLabDependency(env map[string]string) (ResolvedDependency, bool) {
projectURL := strings.TrimSpace(env["CI_PROJECT_URL"])
commit := strings.TrimSpace(env["CI_COMMIT_SHA"])
if projectURL == "" || commit == "" {
return ResolvedDependency{}, false
}
uri := "git+" + strings.TrimRight(projectURL, "/")
if ref := strings.TrimSpace(env["CI_COMMIT_REF_NAME"]); ref != "" {
uri += "@refs/heads/" + ref
}
return ResolvedDependency{
URI: uri,
Digest: DigestSet{
"gitCommit": commit,
},
}, true
}
func detectLocalGitDependency(workingDir string, runner gitRunner) (ResolvedDependency, bool) {
if runner == nil {
return ResolvedDependency{}, false
}
commit, err := runner(workingDir, "rev-parse", "HEAD")
if err != nil || commit == "" {
return ResolvedDependency{}, false
}
repositoryURL, err := runner(workingDir, "config", "--get", "remote.origin.url")
if err != nil || repositoryURL == "" {
topLevel, topLevelErr := runner(workingDir, "rev-parse", "--show-toplevel")
if topLevelErr != nil || topLevel == "" {
repositoryURL = ""
} else {
repositoryURL = localGitFileURI(topLevel)
}
}
if repositoryURL == "" {
return ResolvedDependency{}, false
}
repositoryURL = sanitizeGitRemoteURL(repositoryURL)
uri := "git+" + repositoryURL
if branch, branchErr := runner(workingDir, "rev-parse", "--abbrev-ref", "HEAD"); branchErr == nil && branch != "" && branch != "HEAD" {
uri += "@refs/heads/" + branch
}
return ResolvedDependency{
URI: uri,
Digest: DigestSet{
"gitCommit": commit,
},
}, true
}
func sanitizeGitRemoteURL(raw string) string {
// SCP-style remotes (git@host:repo) carry no userinfo — leave them unchanged
if !strings.Contains(raw, "://") {
return raw
}
u, err := url.Parse(raw)
if err != nil {
return raw
}
u.User = nil // strips embedded username and password/token
return u.String()
}
func localGitFileURI(path string) string {
cleaned := filepath.Clean(path)
if cleaned == "" {
return ""
}
slashed := filepath.ToSlash(cleaned)
if !strings.HasPrefix(slashed, "/") {
slashed = "/" + slashed
}
return (&url.URL{Scheme: "file", Path: slashed}).String()
}
func runGitCommand(workingDir string, args ...string) (string, error) {
command := exec.Command("git", args...)
if workingDir != "" {
command.Dir = workingDir
}
output, err := command.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}