From 96865a252ab15762036871cac21d20c6cd4712b1 Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Tue, 12 Nov 2024 16:11:44 +0000 Subject: [PATCH] Add draft for buildkit-gha build type Signed-off-by: Paulo Gomes --- buildtypes/buildkit-gha/v1/README.md | 27 +++++++++ buildtypes/buildkit-gha/v1/example.json | 62 ++++++++++++++++++++ cmd/provenance.go | 78 ++++++++++++++++++++++++- internal/provenance/provenance.go | 22 ++++++- internal/provenance/provenance_test.go | 26 ++++++--- 5 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 buildtypes/buildkit-gha/v1/README.md create mode 100644 buildtypes/buildkit-gha/v1/example.json diff --git a/buildtypes/buildkit-gha/v1/README.md b/buildtypes/buildkit-gha/v1/README.md new file mode 100644 index 0000000..5dc9f35 --- /dev/null +++ b/buildtypes/buildkit-gha/v1/README.md @@ -0,0 +1,27 @@ +# Build Type: BuildKit + GitHub Actions + +This is a [SLSA Provenance](https://slsa.dev/provenance/v1) +`buildType` that describes builds for container images which combine the +use of BuildKit v1 and GitHub Actions workflows. + +## Description + +```jsonc +"buildType": "https://github.com/rancherlabs/slsactl/tree/main/buildtypes/buildkit-gha/v1" +``` + +## Build Definition + +### Internal parameters + +All internal parameters are REQUIRED. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `trigger` | string | The GitHub Action event that caused the build to be executed. | +| `invocationUri` | string | Resource URI for the GitHub action workflow instance. | + +### Resolved Dependencies + +The resolved dependencies MUST include the source code URI and its `gitCommit`, optionally +a Git tag can be added as an annotation. diff --git a/buildtypes/buildkit-gha/v1/example.json b/buildtypes/buildkit-gha/v1/example.json new file mode 100644 index 0000000..015a9b1 --- /dev/null +++ b/buildtypes/buildkit-gha/v1/example.json @@ -0,0 +1,62 @@ +{ + "buildDefinition": { + "buildType": "https://mobyproject.org/buildkit@v1", + "externalParameters": { + "args": { + "build-arg:VERSION": "v1.3.0" + }, + "frontend": "dockerfile.v0", + "locals": [ + { + "name": "context" + }, + { + "name": "dockerfile" + } + ] + }, + "internalParameters": { + "trigger": "push", + "invocationUri": "https://github.com/rancher/cis-operator/actions/runs/11725698930/attempts/1" + }, + "resolvedDependencies": [ + { + "uri": "pkg:docker/docker/buildkit-syft-scanner@stable-1", + "digest": { + "sha256": "176e0869c38aeaede37e594fcf182c91d44391a932e1d71e99ec204873445a33" + } + }, + { + "uri": "pkg:docker/rancher/mirrored-tonistiigi-xx@1.5.0?platform=linux%2Famd64", + "digest": { + "sha256": "9872511e4e59256b73cc3d5577c75c4434d883b64f5365cf2fc5c9d8a43323bd" + } + }, + { + "uri": "pkg:docker/registry.suse.com/bci/golang@1.22?platform=linux%2Famd64", + "digest": { + "sha256": "670d86a82103520f105a97320d3075e5b3d72ccff0b17467ac4277d909ef18cb" + } + }, + { + "uri": "https://github.com/rancher/cis-operator", + "digest": { + "gitCommit": "1151c64ed3f84c4bb6ecb310bbe62f0b7a450aad" + }, + "annotations": { + "ref": "refs/tags/v1.3.0" + } + } + ] + }, + "runDetails": { + "builder": { + "id": "https://github.com/rancherlabs/slsactl/tree/main/buildtypes/buildkit-gha/v1" + }, + "metadata": { + "invocationID": "hxcpfkp0jwmhk8c1is2x2mi01", + "startedOn": "2024-11-07T15:15:01.764583687Z", + "finishedOn": "2024-11-07T15:15:02.984902536Z" + } + } +} diff --git a/cmd/provenance.go b/cmd/provenance.go index 2c707e1..65ee93d 100644 --- a/cmd/provenance.go +++ b/cmd/provenance.go @@ -2,14 +2,27 @@ package cmd import ( "bytes" + "context" "encoding/json" "fmt" "io" "os" "strings" + "github.com/google/go-containerregistry/pkg/name" + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" "github.com/rancherlabs/slsactl/internal/provenance" + "github.com/sigstore/cosign/v2/pkg/cosign" + certificate "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" +) + +var ( + // builderId defines the builder ID when the provenance has been modified. + builderId = "https://github.com/rancherlabs/slsactl/tree/main/buildtypes/buildkit-gha/v1" + // buildKitV1 holds the buildType supported for provenance enrichment. + buildKitV1 = "https://mobyproject.org/buildkit@v1" ) func provenanceCmd(img, format, platform string) error { @@ -46,7 +59,16 @@ func provenanceCmd(img, format, platform string) error { case "slsav0.2": err = print(os.Stdout, predicate) case "slsav1": - provV1 := provenance.ConvertV02ToV1(*predicate) + if predicate.BuildType != buildKitV1 { + return fmt.Errorf("image builtType not supported: %q", predicate.BuildType) + } + + override, err := cosignCertData(img) + if err != nil { + return err + } + + provV1 := provenance.ConvertV02ToV1(*predicate, override) err = print(os.Stdout, provV1) default: @@ -65,3 +87,57 @@ func print(w io.Writer, v interface{}) error { _, err = fmt.Fprintln(w, string(outData)) return err } + +func cosignCertData(img string) (*v1.ProvenancePredicate, error) { + ref, err := name.ParseReference(img, name.StrictValidation) + if err != nil { + return nil, err + } + + payloads, err := cosign.FetchSignaturesForReference(context.Background(), ref) + if err != nil { + return nil, fmt.Errorf("failed to fetch image signatures: %w", err) + } + + if len(payloads) == 0 { + return nil, fmt.Errorf("no payloads found for image") + } + + var inparams provenance.InternalParameters + var commitID, commitRef, repoURL string + + for _, ext := range payloads[0].Cert.Extensions { + switch { + case ext.Id.Equal(certificate.OIDSourceRepositoryDigest): + certificate.ParseDERString(ext.Value, &commitID) + case ext.Id.Equal(certificate.OIDSourceRepositoryURI): + certificate.ParseDERString(ext.Value, &repoURL) + case ext.Id.Equal(certificate.OIDSourceRepositoryRef): + certificate.ParseDERString(ext.Value, &commitRef) + case ext.Id.Equal(certificate.OIDBuildTrigger): + certificate.ParseDERString(ext.Value, &inparams.Trigger) + case ext.Id.Equal(certificate.OIDRunInvocationURI): + certificate.ParseDERString(ext.Value, &inparams.InvocationUri) + } + } + + override := &v1.ProvenancePredicate{} + override.BuildDefinition.InternalParameters = inparams + deps := []v1.ResourceDescriptor{ + { + URI: repoURL, + Digest: common.DigestSet{"gitCommit": commitID}, + }, + } + + if commitRef != "" { + deps[0].Annotations = map[string]interface{}{ + "ref": commitRef, + } + } + + override.BuildDefinition.ResolvedDependencies = deps + override.RunDetails.Builder.ID = builderId + + return override, nil +} diff --git a/internal/provenance/provenance.go b/internal/provenance/provenance.go index c37d0f6..512452e 100644 --- a/internal/provenance/provenance.go +++ b/internal/provenance/provenance.go @@ -5,6 +5,12 @@ import ( v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" ) +type InternalParameters struct { + Platform string `json:"platform,omitempty"` + Trigger string `json:"trigger,omitempty"` + InvocationUri string `json:"invocationUri,omitempty"` +} + type BuildKitProvenance02 struct { LinuxAmd64 *ArchProvenance `json:"linux/amd64,omitempty"` LinuxArm64 *ArchProvenance `json:"linux/arm64,omitempty"` @@ -23,7 +29,7 @@ type ArchProvenanceV1 struct { SLSA v1.ProvenancePredicate `json:"SLSA,omitempty"` } -func ConvertV02ToV1(v02Prov v02.ProvenancePredicate) v1.ProvenancePredicate { +func ConvertV02ToV1(v02Prov v02.ProvenancePredicate, override *v1.ProvenancePredicate) v1.ProvenancePredicate { prov := v1.ProvenancePredicate{ BuildDefinition: v1.ProvenanceBuildDefinition{ BuildType: v02Prov.BuildType, @@ -53,5 +59,19 @@ func ConvertV02ToV1(v02Prov v02.ProvenancePredicate) v1.ProvenancePredicate { prov.BuildDefinition.ResolvedDependencies = deps + if override != nil { + if override.RunDetails.Builder.ID != "" { + prov.RunDetails.Builder.ID = override.RunDetails.Builder.ID + } + if len(override.BuildDefinition.ResolvedDependencies) > 0 { + prov.BuildDefinition.ResolvedDependencies = + append(prov.BuildDefinition.ResolvedDependencies, + override.BuildDefinition.ResolvedDependencies...) + } + if override.BuildDefinition.InternalParameters != nil { + prov.BuildDefinition.InternalParameters = override.BuildDefinition.InternalParameters + } + } + return prov } diff --git a/internal/provenance/provenance_test.go b/internal/provenance/provenance_test.go index 1622002..83f3e07 100644 --- a/internal/provenance/provenance_test.go +++ b/internal/provenance/provenance_test.go @@ -23,8 +23,6 @@ func TestConvertV02ToV1(t *testing.T) { err := json.Unmarshal(v02Data, &v02Prov) require.NoError(t, err, "Failed to unmarshal v0.2 data") - v1Prov := provenance.ConvertV02ToV1(v02Prov) - started, _ := time.Parse(time.RFC3339Nano, "2024-07-11T14:49:18.126688014Z") finished, _ := time.Parse(time.RFC3339Nano, "2024-07-11T14:51:00.499751748Z") @@ -99,10 +97,22 @@ func TestConvertV02ToV1(t *testing.T) { }, } - assert.Equal(t, expectedV1Prov.BuildDefinition.BuildType, v1Prov.BuildDefinition.BuildType, "BuildType mismatch") - assert.Equal(t, expectedV1Prov.RunDetails.Builder.ID, v1Prov.RunDetails.Builder.ID, "Builder ID mismatch") - assert.Equal(t, expectedV1Prov.RunDetails.BuildMetadata.InvocationID, v1Prov.RunDetails.BuildMetadata.InvocationID, "BuildMetadata InvocationID mismatch") - assert.Equal(t, expectedV1Prov.RunDetails.BuildMetadata.StartedOn, v1Prov.RunDetails.BuildMetadata.StartedOn, "BuildMetadata StartedOn mismatch") - assert.Equal(t, expectedV1Prov.RunDetails.BuildMetadata.FinishedOn, v1Prov.RunDetails.BuildMetadata.FinishedOn, "BuildMetadata FinishedOn mismatch") - assert.Equal(t, expectedV1Prov.BuildDefinition.ResolvedDependencies, v1Prov.BuildDefinition.ResolvedDependencies, "BuildDefinition ResolvedDependencies mismatch") + v1Prov := provenance.ConvertV02ToV1(v02Prov, nil) + equal(t, expectedV1Prov, v1Prov) + + override := &v1.ProvenancePredicate{} + override.RunDetails.Builder.ID = "new-build-id" + expectedV1Prov.RunDetails.Builder.ID = "new-build-id" + + v1Prov = provenance.ConvertV02ToV1(v02Prov, override) + equal(t, expectedV1Prov, v1Prov) +} + +func equal(t *testing.T, want, got v1.ProvenancePredicate) { + assert.Equal(t, want.BuildDefinition.BuildType, got.BuildDefinition.BuildType, "BuildType mismatch") + assert.Equal(t, want.RunDetails.Builder.ID, got.RunDetails.Builder.ID, "Builder ID mismatch") + assert.Equal(t, want.RunDetails.BuildMetadata.InvocationID, got.RunDetails.BuildMetadata.InvocationID, "BuildMetadata InvocationID mismatch") + assert.Equal(t, want.RunDetails.BuildMetadata.StartedOn, got.RunDetails.BuildMetadata.StartedOn, "BuildMetadata StartedOn mismatch") + assert.Equal(t, want.RunDetails.BuildMetadata.FinishedOn, got.RunDetails.BuildMetadata.FinishedOn, "BuildMetadata FinishedOn mismatch") + assert.Equal(t, want.BuildDefinition.ResolvedDependencies, got.BuildDefinition.ResolvedDependencies, "BuildDefinition ResolvedDependencies mismatch") }