2026-09-15 17:10:54 +05:30
// Copyright IBM Corp. 2024, 2026
2026-07-21 13:40:42 +05:30
// SPDX-License-Identifier: BUSL-1.1
//go:generate packer-sdc mapstructure-to-hcl2 -type Config
//go:generate packer-sdc struct-markdown
package provenance
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"unicode"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/common"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
internalattestation "github.com/hashicorp/packer/internal/attestation"
internalprovenance "github.com/hashicorp/packer/internal/provenance"
internalsbom "github.com/hashicorp/packer/internal/sbom"
)
var buildSigstoreBundleForSigner = internalattestation . BuildBundleForSigner
type Config struct {
common . PackerConfig `mapstructure:",squash"`
// Whether to emit provenance attestations. Enabled by default; set to
// `false` to skip the post-processor and pass the artifact through unchanged.
Provenance config . Trilean `mapstructure:"provenance"`
// The SLSA `buildType` URI recorded in the provenance predicate. Defaults to
// `https://packer.io/buildtypes/hcl2/v1`.
BuildType string `mapstructure:"build_type"`
// Directory where attestation sidecar files are written. Defaults to the
// directory containing the artifact's first file, or the current directory
// for artifacts without local files.
OutputDir string `mapstructure:"output_dir"`
// Path to the Packer template that produced the artifact. Recorded as an
// external parameter in the provenance predicate.
TemplatePath string `mapstructure:"template"`
// The list of builds this artifact came from. Recorded as an external
// parameter in the provenance predicate.
OnlyBuilds [] string `mapstructure:"only_builds"`
// Additional user variables to record as external parameters in the
// provenance predicate. Values for variables named in
// `packer_sensitive_variables` are redacted.
UserVariables map [ string ] string `mapstructure:"user_variables"`
// Overrides the auto-detected source repository URI recorded as a resolved
// dependency. By default the source is detected from the Git repository
// containing the current working directory or from CI environment variables.
SourceURI string `mapstructure:"source_uri"`
// Whether to also generate a software bill of materials (SBOM) and a
// corresponding SBOM attestation alongside the provenance statement.
SBOM bool `mapstructure:"sbom"`
// The SBOM output format, either `cyclonedx` (default) or `spdx`.
SBOMFormat string `mapstructure:"sbom_format"`
// The path to scan when generating the SBOM. Defaults to the artifact's
// files or their common parent directory. Required when the artifact files
// span multiple directories.
SBOMScanPath string `mapstructure:"sbom_scan_path"`
// The SBOM scan scope, either `squashed` (default) or `all-layers`.
SBOMScope string `mapstructure:"sbom_scope"`
// Glob patterns of paths to exclude from the SBOM scan.
SBOMExclude [] string `mapstructure:"sbom_exclude"`
// The signing mode for attestations: `none` (default, unsigned JSON),
// `key` (local PEM key), `kms` (KMS or Vault URI), or `keyless` (Sigstore
// Fulcio).
SigningMode string `mapstructure:"signing_mode"`
// The signer reference. A PEM private key path for `key` mode, or a KMS or
// Vault URI such as `awskms://...`, `gcpkms://...`, `azurekms://...`, or
// `hashivault://...` for `kms` mode.
Signer string `mapstructure:"signer"`
// An alias for `signer`. When both are set they must be equal.
Key string `mapstructure:"key"`
// The PEM verifier path used to verify the signature in `key` and `kms`
// modes. Defaults to the signer's derived public key.
Verifier string `mapstructure:"verifier"`
// The Fulcio certificate authority URL used for `keyless` signing. Defaults
// to `https://fulcio.sigstore.dev`.
FulcioURL string `mapstructure:"fulcio_url"`
// The Rekor transparency log URL used when `upload_tlog` is enabled.
// Defaults to `https://rekor.sigstore.dev`.
RekorURL string `mapstructure:"rekor_url"`
// Whether to upload the `keyless` signature to the Rekor transparency log
// and emit a Sigstore bundle carrying the transparency evidence.
UploadTlog bool `mapstructure:"upload_tlog"`
// An optional path to a Sigstore trusted-root JSON file used to pin keyless
// verification. When unset, the public Sigstore trusted root is fetched.
TrustedRootPath string `mapstructure:"trusted_root_path"`
// The expected signing identity for `keyless` mode, such as the workflow
// ref `https://github.com/OWNER/REPO/.github/workflows/build.yml@refs/heads/main`.
// Required for keyless signing.
KeylessIdentity string `mapstructure:"keyless_identity"`
// The expected OIDC issuer for `keyless` mode, such as
// `https://token.actions.githubusercontent.com`. Required for keyless signing.
KeylessOIDCIssuer string `mapstructure:"keyless_oidc_issuer"`
ctx interpolate . Context
}
type PostProcessor struct {
config Config
now func () time . Time
env map [ string ] string
workingDir string
generateSBOM func ( context . Context , internalsbom . Config ) ([] byte , error )
signingResourcesFn func ( context . Context , internalattestation . BackendConfig ) ( internalattestation . Signer , internalattestation . Verifier , error )
}
func ( p * PostProcessor ) ConfigSpec () hcldec . ObjectSpec { return p . config . FlatMapstructure (). HCL2Spec () }
func ( p * PostProcessor ) Configure ( raws ... interface {}) error {
err := config . Decode ( & p . config , & config . DecodeOpts {
PluginType : "packer.post-processor.provenance" ,
Interpolate : true ,
InterpolateContext : & p . config . ctx ,
InterpolateFilter : & interpolate . RenderFilter {
Exclude : [] string {},
},
}, raws ... )
if err != nil {
return err
}
// Defaults are applied after decoding because the HCL2 decode path zeroes
// unset fields, which would otherwise clobber any pre-decode defaults.
if p . config . BuildType == "" {
p . config . BuildType = internalprovenance . DefaultBuildType
}
if p . config . SBOMFormat == "" {
p . config . SBOMFormat = string ( internalsbom . FormatCycloneDX )
}
if p . config . SBOMScope == "" {
p . config . SBOMScope = internalsbom . ScopeSquashed
}
if p . config . SigningMode == "" {
p . config . SigningMode = internalattestation . SigningModeNone
}
if p . config . FulcioURL == "" {
p . config . FulcioURL = "https://fulcio.sigstore.dev"
}
if p . config . RekorURL == "" {
p . config . RekorURL = "https://rekor.sigstore.dev"
}
if p . config . OutputDir != "" {
if err := interpolate . Validate ( p . config . OutputDir , & p . config . ctx ); err != nil {
return fmt . Errorf ( "error parsing output_dir template: %w" , err )
}
}
if _ , err := p . signingBackendConfig (); err != nil {
return err
}
if p . generateSBOM == nil {
p . generateSBOM = func ( ctx context . Context , cfg internalsbom . Config ) ([] byte , error ) {
return internalsbom . NewGenerator ( cfg ). Generate ( ctx )
}
}
return nil
}
func ( p * PostProcessor ) PostProcess ( ctx context . Context , ui packersdk . Ui , source packersdk . Artifact ) ( packersdk . Artifact , bool , bool , error ) {
if p . config . Provenance . False () {
return source , true , true , nil
}
env := p . currentEnv ()
select {
case <- ctx . Done ():
return source , true , true , ctx . Err ()
default :
}
subjects , err := internalprovenance . DeriveSubjects ( source )
if err != nil {
return source , true , true , err
}
var byproducts [] internalprovenance . Byproduct
if len ( source . Files ()) == 0 {
identityRecord , err := internalprovenance . DeriveIdentityRecord ( source )
if err != nil {
return source , true , true , err
}
identityBytes , err := json . Marshal ( identityRecord )
if err != nil {
return source , true , true , err
}
byproducts = append ( byproducts , internalprovenance . Byproduct {
Name : "cloud-artifact-identity" ,
Content : base64 . StdEncoding . EncodeToString ( identityBytes ),
})
}
invocationID := internalprovenance . DetectInvocationID ( env )
if invocationID == "" {
invocationID , _ = uuid . GenerateUUID ()
}
predicate := internalprovenance . BuildSLSAPredicate ( internalprovenance . PredicateInput {
BuildType : p . config . BuildType ,
ExternalParameters : p . externalParameters ( env ),
InternalParameters : p . internalParameters (),
ResolvedDependencies : p . resolvedDependencies ( env ),
BuilderID : internalprovenance . DetectBuilderID ( env ),
Byproducts : byproducts ,
InvocationID : invocationID ,
})
statement := internalprovenance . WrapInToto ( subjects , internalprovenance . SLSAProvenanceV1PredicateType , predicate )
paths , err := p . outputPaths ( source )
if err != nil {
return source , true , true , err
}
if err := p . writeAttestation ( ctx , ui , statement , paths . ProvenanceStatement ); err != nil {
return source , true , true , err
}
if p . config . SBOM {
if err := p . writeSBOMAttestation ( ctx , ui , source , subjects , paths ); err != nil {
return source , true , true , err
}
}
return source , true , true , nil
}
const (
predicateTypeCycloneDX = "https://cyclonedx.org/bom"
predicateTypeSPDX = "https://spdx.dev/Document"
)
// redactedSensitiveValue replaces sensitive user-variable values in the
// provenance predicate so secrets are never written to the attestation.
const redactedSensitiveValue = "[sensitive value redacted]"
type outputPaths struct {
BaseDir string
Stem string
ProvenanceStatement string
SBOMRaw string
SBOMAttestation string
}
func ( p * PostProcessor ) writeSBOMAttestation ( ctx context . Context , ui packersdk . Ui , source packersdk . Artifact , subjects [] internalprovenance . Subject , paths outputPaths ) error {
format , rawSBOM , err := p . resolveSBOM ( ctx , source , paths )
if err != nil {
return err
}
predicate , predicateType , err := buildSBOMPredicate ( rawSBOM , format )
if err != nil {
return err
}
statement := internalprovenance . WrapInToto ( subjects , predicateType , predicate )
if err := p . writeAttestation ( ctx , ui , statement , paths . SBOMAttestation ); err != nil {
return err
}
ui . Say ( fmt . Sprintf ( "Wrote SBOM to %s" , paths . SBOMRaw ))
return nil
}
func ( p * PostProcessor ) writeAttestation ( ctx context . Context , ui packersdk . Ui , statement interface {}, outputPath string ) error {
if p . config . SigningMode == internalattestation . SigningModeNone {
payload , err := json . MarshalIndent ( statement , "" , " " )
if err != nil {
return fmt . Errorf ( "marshal attestation payload: %w" , err )
}
if err := atomicWriteFile ( outputPath , payload , 0664 ); err != nil {
return fmt . Errorf ( "write attestation %q: %w" , outputPath , err )
}
ui . Say ( fmt . Sprintf ( "Wrote attestation to %s" , outputPath ))
return nil
}
backendConfig , err := p . signingBackendConfig ()
if err != nil {
return err
}
signer , verifier , err := p . signingResources ( ctx , backendConfig )
if err != nil {
return err
}
payload , err := internalattestation . MarshalPayload ( statement )
if err != nil {
return fmt . Errorf ( "marshal canonical attestation payload: %w" , err )
}
bundlePath := sigstoreBundleOutputPath ( outputPath )
bundleJSON := [] byte ( nil )
var envelope internalattestation . Envelope
if backendConfig . Mode == internalattestation . SigningModeKeyless {
envelope , bundleJSON , err = buildSigstoreBundleForSigner ( ctx , signer , backendConfig , internalattestation . InTotoPayloadType , payload )
if err != nil {
return fmt . Errorf ( "sign attestation with Sigstore bundle: %w" , err )
}
} else {
signature , signErr := signer . Sign ( ctx , internalattestation . InTotoPayloadType , payload )
if signErr != nil {
return fmt . Errorf ( "sign attestation: %w" , signErr )
}
envelope = internalattestation . NewEnvelope ( internalattestation . InTotoPayloadType , payload , signature )
}
if err := internalattestation . VerifyEnvelope ( ctx , envelope , verifier ); err != nil {
return fmt . Errorf ( "verify signed attestation: %w" , err )
}
output , err := json . MarshalIndent ( envelope , "" , " " )
if err != nil {
return fmt . Errorf ( "marshal signed envelope: %w" , err )
}
if err := atomicWriteFile ( outputPath , output , 0664 ); err != nil {
return fmt . Errorf ( "write attestation %q: %w" , outputPath , err )
}
if len ( bundleJSON ) > 0 {
if err := atomicWriteFile ( bundlePath , bundleJSON , 0664 ); err != nil {
return fmt . Errorf ( "write Sigstore bundle %q: %w" , bundlePath , err )
}
ui . Say ( fmt . Sprintf ( "Wrote Sigstore bundle to %s" , bundlePath ))
}
ui . Say ( fmt . Sprintf ( "Wrote attestation to %s" , outputPath ))
return nil
}
func ( p * PostProcessor ) signingResources ( ctx context . Context , backendConfig internalattestation . BackendConfig ) ( internalattestation . Signer , internalattestation . Verifier , error ) {
if p . signingResourcesFn != nil {
return p . signingResourcesFn ( ctx , backendConfig )
}
if backendConfig . Mode == internalattestation . SigningModeNone {
return nil , nil , nil
}
signer , err := internalattestation . NewSigner ( ctx , backendConfig )
if err != nil {
return nil , nil , err
}
verifier , err := internalattestation . NewVerifier ( ctx , backendConfig , signer )
if err != nil {
return nil , nil , err
}
return signer , verifier , nil
}
func ( p * PostProcessor ) signingBackendConfig () ( internalattestation . BackendConfig , error ) {
mode := p . config . SigningMode
if mode == "" {
mode = internalattestation . SigningModeNone
}
signerRef := p . config . Signer
if p . config . Key != "" {
if signerRef != "" && signerRef != p . config . Key {
return internalattestation . BackendConfig {}, fmt . Errorf ( "signer and key must match when both are set" )
}
signerRef = p . config . Key
}
switch mode {
case internalattestation . SigningModeNone :
return internalattestation . BackendConfig { Mode : mode }, nil
case internalattestation . SigningModeKey :
if signerRef == "" {
return internalattestation . BackendConfig {}, fmt . Errorf ( "signing_mode %q requires signer or key" , mode )
}
return internalattestation . BackendConfig {
Mode : mode ,
SignerRef : signerRef ,
VerifierRef : p . config . Verifier ,
Env : p . currentEnv (),
}, nil
case internalattestation . SigningModeKMS :
if signerRef == "" {
return internalattestation . BackendConfig {}, fmt . Errorf ( "signing_mode %q requires signer or key" , mode )
}
if ! isRecognizedKMSSigner ( signerRef ) {
return internalattestation . BackendConfig {}, fmt . Errorf ( "signing_mode %q requires a recognized KMS or Vault URI: awskms://, gcpkms://, azurekms://, or hashivault://" , mode )
}
return internalattestation . BackendConfig {
Mode : mode ,
SignerRef : signerRef ,
VerifierRef : p . config . Verifier ,
Env : p . currentEnv (),
}, nil
case internalattestation . SigningModeKeyless :
if p . config . Verifier != "" {
return internalattestation . BackendConfig {}, fmt . Errorf ( "signing_mode %q does not support verifier overrides; keyless attestations are verified against keyless_identity and keyless_oidc_issuer" , mode )
}
if strings . TrimSpace ( p . config . KeylessIdentity ) == "" || strings . TrimSpace ( p . config . KeylessOIDCIssuer ) == "" {
return internalattestation . BackendConfig {}, fmt . Errorf ( "signing_mode %q requires keyless_identity and keyless_oidc_issuer" , mode )
}
return internalattestation . BackendConfig {
Mode : mode ,
Env : p . currentEnv (),
FulcioURL : p . config . FulcioURL ,
RekorURL : p . config . RekorURL ,
UploadTlog : p . config . UploadTlog ,
TrustedRootPath : p . config . TrustedRootPath ,
KeylessIdentity : p . config . KeylessIdentity ,
KeylessOIDCIssuer : p . config . KeylessOIDCIssuer ,
}, nil
default :
return internalattestation . BackendConfig {}, fmt . Errorf ( "signing_mode %q is not implemented" , mode )
}
}
func isRecognizedKMSSigner ( value string ) bool {
for _ , prefix := range [] string { "awskms://" , "gcpkms://" , "azurekms://" , "hashivault://" } {
if strings . HasPrefix ( value , prefix ) {
return true
}
}
return false
}
func ( p * PostProcessor ) resolveSBOM ( ctx context . Context , source packersdk . Artifact , paths outputPaths ) ( internalsbom . Format , [] byte , error ) {
// The SBOM is always regenerated so it reflects the artifact being attested.
// Reusing a pre-existing SBOM file could attest stale contents if the
// artifact changed between runs.
format , err := internalsbom . ParseFormatFromArgs ( p . config . SBOMFormat )
if err != nil {
return "" , nil , err
}
scanPath , err := p . resolveSBOMScanPath ( source )
if err != nil {
return "" , nil , err
}
rawSBOM , err := p . generateSBOM ( ctx , internalsbom . Config {
ScanPath : scanPath ,
Format : format ,
Scope : p . config . SBOMScope ,
Exclude : append ([] string ( nil ), p . config . SBOMExclude ... ),
})
if err != nil {
return "" , nil , fmt . Errorf ( "generate SBOM: %w" , err )
}
if err := atomicWriteFile ( paths . SBOMRaw , rawSBOM , 0664 ); err != nil {
return "" , nil , fmt . Errorf ( "write SBOM %q: %w" , paths . SBOMRaw , err )
}
return format , rawSBOM , nil
}
func ( p * PostProcessor ) resolveSBOMScanPath ( source packersdk . Artifact ) ( string , error ) {
if p . config . SBOMScanPath != "" {
return p . config . SBOMScanPath , nil
}
files := source . Files ()
if len ( files ) == 1 {
return files [ 0 ], nil
}
if len ( files ) > 1 {
parent := filepath . Dir ( files [ 0 ])
for _ , file := range files [ 1 :] {
if filepath . Dir ( file ) != parent {
return "" , fmt . Errorf ( "sbom=true requires sbom_scan_path when artifact files span multiple directories" )
}
}
return parent , nil
}
return "" , fmt . Errorf ( "sbom=true requires local artifact files or sbom_scan_path" )
}
func buildSBOMPredicate ( rawSBOM [] byte , format internalsbom . Format ) ( interface {}, string , error ) {
decoder := json . NewDecoder ( bytes . NewReader ( rawSBOM ))
decoder . UseNumber ()
var predicate interface {}
if err := decoder . Decode ( & predicate ); err != nil {
return nil , "" , fmt . Errorf ( "decode SBOM payload: %w" , err )
}
switch format {
case internalsbom . FormatCycloneDX :
return predicate , predicateTypeCycloneDX , nil
case internalsbom . FormatSPDX :
return predicate , predicateTypeSPDX , nil
default :
return nil , "" , fmt . Errorf ( "unsupported SBOM format %q" , format )
}
}
func ( p * PostProcessor ) externalParameters ( env map [ string ] string ) map [ string ] interface {} {
externalParameters := map [ string ] interface {}{}
if p . config . TemplatePath != "" {
externalParameters [ "template" ] = p . config . TemplatePath
}
if len ( p . config . OnlyBuilds ) > 0 {
externalParameters [ "onlyBuilds" ] = append ([] string ( nil ), p . config . OnlyBuilds ... )
}
userVariables := collectUserVariables ( env )
for key , value := range p . config . UserVariables {
userVariables [ key ] = value
}
redactSensitiveVariables ( userVariables , p . config . PackerSensitiveVars )
if len ( userVariables ) > 0 {
externalParameters [ "userVariables" ] = userVariables
}
if len ( externalParameters ) == 0 {
return nil
}
return externalParameters
}
func ( p * PostProcessor ) internalParameters () map [ string ] interface {} {
return map [ string ] interface {}{
"packerBuildName" : p . config . PackerBuildName ,
"packerBuilderType" : p . config . PackerBuilderType ,
}
}
func ( p * PostProcessor ) resolvedDependencies ( env map [ string ] string ) [] internalprovenance . ResolvedDependency {
workingDir := p . currentWorkingDir ()
dependency , ok := internalprovenance . DetectGitDependency ( workingDir , env )
if p . config . SourceURI != "" {
if ok {
dependency . URI = p . config . SourceURI
} else {
dependency = internalprovenance . ResolvedDependency { URI : p . config . SourceURI }
ok = true
}
}
if ! ok {
return nil
}
return [] internalprovenance . ResolvedDependency { dependency }
}
func ( p * PostProcessor ) currentEnv () map [ string ] string {
if p . env != nil {
copiedEnv := make ( map [ string ] string , len ( p . env ))
for key , value := range p . env {
copiedEnv [ key ] = value
}
return copiedEnv
}
env := make ( map [ string ] string )
for _ , item := range os . Environ () {
parts := strings . SplitN ( item , "=" , 2 )
if len ( parts ) != 2 {
continue
}
env [ parts [ 0 ]] = parts [ 1 ]
}
return env
}
func ( p * PostProcessor ) currentWorkingDir () string {
if p . workingDir != "" {
return p . workingDir
}
workingDir , err := os . Getwd ()
if err != nil {
return ""
}
return workingDir
}
func collectUserVariables ( env map [ string ] string ) map [ string ] string {
userVariables := map [ string ] string {}
keys := make ([] string , 0 )
for key := range env {
if strings . HasPrefix ( key , "PKR_VAR_" ) {
keys = append ( keys , key )
}
}
sort . Strings ( keys )
for _ , key := range keys {
userVariables [ strings . TrimPrefix ( key , "PKR_VAR_" )] = env [ key ]
}
return userVariables
}
// redactSensitiveVariables replaces the values of any user variables whose names
// were marked sensitive (packer_sensitive_variables) so that secrets are not
// embedded in the provenance predicate, per SLSA guidance.
func redactSensitiveVariables ( userVariables map [ string ] string , sensitiveKeys [] string ) {
for _ , key := range sensitiveKeys {
if _ , ok := userVariables [ key ]; ok {
userVariables [ key ] = redactedSensitiveValue
}
}
}
func ( p * PostProcessor ) outputPaths ( source packersdk . Artifact ) ( outputPaths , error ) {
baseDir := p . config . OutputDir
if baseDir == "" && len ( source . Files ()) > 0 {
baseDir = filepath . Dir ( source . Files ()[ 0 ])
}
if baseDir == "" {
baseDir = "."
}
if err := os . MkdirAll ( baseDir , 0755 ); err != nil {
return outputPaths {}, fmt . Errorf ( "create output dir %q: %w" , baseDir , err )
}
name := p . outputStem ( source )
sbomFormat := internalsbom . FormatCycloneDX
if parsed , err := internalsbom . ParseFormatFromArgs ( p . config . SBOMFormat ); err == nil {
sbomFormat = parsed
}
sbomRaw := filepath . Join ( baseDir , name + ".sbom.cdx.json" )
if sbomFormat == internalsbom . FormatSPDX {
sbomRaw = filepath . Join ( baseDir , name + ".sbom.spdx.json" )
}
return outputPaths {
BaseDir : baseDir ,
Stem : name ,
ProvenanceStatement : filepath . Join ( baseDir , name + ".provenance.json" ),
SBOMRaw : sbomRaw ,
SBOMAttestation : filepath . Join ( baseDir , name + ".sbom.att.json" ),
}, nil
}
// outputStem returns the base filename used for all provenance outputs. When a
// build name is available it is prefixed so that parallel builds writing to a
// shared output directory cannot collide on the same output paths.
func ( p * PostProcessor ) outputStem ( source packersdk . Artifact ) string {
base := artifactStem ( source )
buildName := sanitizeFilename ( strings . TrimSpace ( p . config . PackerBuildName ))
if buildName == "" || base == buildName || strings . HasPrefix ( base , buildName + "." ) {
return base
}
return buildName + "." + base
}
func artifactStem ( source packersdk . Artifact ) string {
if files := source . Files (); len ( files ) > 0 {
return filepath . Base ( files [ 0 ])
}
return sanitizeFilename ( fmt . Sprintf ( "%s-%s" , source . BuilderId (), source . Id ()))
}
func sigstoreBundleOutputPath ( attestationPath string ) string {
if strings . HasSuffix ( attestationPath , ".json" ) {
return strings . TrimSuffix ( attestationPath , ".json" ) + ".sigstore.json"
}
return attestationPath + ".sigstore.json"
}
func sanitizeFilename ( value string ) string {
return strings . Map ( func ( r rune ) rune {
switch {
case unicode . IsLetter ( r ), unicode . IsDigit ( r ):
return r
case r == '.' , r == '-' , r == '_' :
return r
default :
return '_'
}
}, value )
}
// atomicWriteFile writes data to path atomically by writing to a temporary file
// in the same directory and renaming it into place. This prevents partially
// written or interleaved outputs when builds run in parallel, and ensures a
// crash mid-write cannot leave a corrupt attestation on disk.
func atomicWriteFile ( path string , data [] byte , perm os . FileMode ) error {
dir := filepath . Dir ( path )
tmp , err := os . CreateTemp ( dir , "." + filepath . Base ( path ) + ".tmp-*" )
if err != nil {
return err
}
tmpName := tmp . Name ()
committed := false
defer func () {
if ! committed {
_ = os . Remove ( tmpName )
}
}()
if _ , err := tmp . Write ( data ); err != nil {
_ = tmp . Close ()
return err
}
if err := tmp . Chmod ( perm ); err != nil {
_ = tmp . Close ()
return err
}
if err := tmp . Sync (); err != nil {
_ = tmp . Close ()
return err
}
if err := tmp . Close (); err != nil {
return err
}
if err := os . Rename ( tmpName , path ); err != nil {
return err
}
committed = true
return nil
}