mirror of
https://github.com/hashicorp/packer.git
synced 2026-10-12 17:22:01 -04:00
* 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
176 lines
4.3 KiB
Go
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
|
|
}
|