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 a344168..83f3e07 100644 --- a/internal/provenance/provenance_test.go +++ b/internal/provenance/provenance_test.go @@ -1,4 +1,4 @@ -package provenance +package provenance_test import ( "encoding/json" @@ -7,8 +7,10 @@ import ( _ "embed" + "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/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -21,8 +23,6 @@ func TestConvertV02ToV1(t *testing.T) { err := json.Unmarshal(v02Data, &v02Prov) require.NoError(t, err, "Failed to unmarshal v0.2 data") - v1Prov := ConvertV02ToV1(v02Prov) - started, _ := time.Parse(time.RFC3339Nano, "2024-07-11T14:49:18.126688014Z") finished, _ := time.Parse(time.RFC3339Nano, "2024-07-11T14:51:00.499751748Z") @@ -51,21 +51,68 @@ func TestConvertV02ToV1(t *testing.T) { InternalParameters: map[string]interface{}{ "platform": "linux/amd64", }, + ResolvedDependencies: []v1.ResourceDescriptor{ + { + URI: "pkg:docker/docker/buildkit-syft-scanner@stable-1", + Digest: common.DigestSet{ + "sha256": "176e0869c38aeaede37e594fcf182c91d44391a932e1d71e99ec204873445a33", + }, + }, + { + URI: "pkg:docker/rancher/mirrored-tonistiigi-xx@1.3.0?platform=linux%2Famd64", + Digest: common.DigestSet{ + "sha256": "053f8e16c843695b7a23803fbfdd699a8b9c9fe863a613516e4911a6eba0a4cb", + }, + }, + { + URI: "pkg:docker/registry.suse.com/bci/bci-micro@15.6?platform=linux%2Famd64", + Digest: common.DigestSet{ + "sha256": "8f926e98dd809e5fc5971e39df2d88a7dbe4158dcf2c379be658acc67b1beb29", + }, + }, + { + URI: "pkg:docker/registry.suse.com/bci/golang@1.22?platform=linux%2Famd64", + Digest: common.DigestSet{ + "sha256": "fdf2b123574c9b00ee19a7009c6b8b11c4e97f3dcb5e27c7ab49d23f8e722d21", + }, + }, + { + URI: "https://dl.k8s.io/release/v1.28.7/bin/linux/amd64/kubectl", + Digest: common.DigestSet{ + "sha256": "aff42d3167685e4d8e86fda0ad9c6ce6ec6c047bc24d608041d54717a18192ba", + }, + }, + }, }, RunDetails: v1.ProvenanceRunDetails{ Builder: v1.Builder{ ID: "", }, BuildMetadata: v1.BuildMetadata{ - StartedOn: &started, - FinishedOn: &finished, + StartedOn: &started, + FinishedOn: &finished, + InvocationID: "ujss3xdtnmbv38uqh5wwftkpd", }, Byproducts: []v1.ResourceDescriptor{}, }, } - 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.StartedOn, v1Prov.RunDetails.BuildMetadata.StartedOn, "BuildMetadata StartedOn mismatch") - assert.Equal(t, expectedV1Prov.RunDetails.BuildMetadata.FinishedOn, v1Prov.RunDetails.BuildMetadata.FinishedOn, "BuildMetadata FinishedOn 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") } diff --git a/internal/provenance/security-scan.v1 b/internal/provenance/security-scan.v1 index 7b8c523..02e265c 100644 --- a/internal/provenance/security-scan.v1 +++ b/internal/provenance/security-scan.v1 @@ -26,15 +26,48 @@ }, "internalParameters": { "platform": "linux/amd64" - } + }, + "resolvedDependencies": [ + { + "digest": { + "sha256": "176e0869c38aeaede37e594fcf182c91d44391a932e1d71e99ec204873445a33" + }, + "uri": "pkg:docker/docker/buildkit-syft-scanner@stable-1" + }, + { + "digest": { + "sha256": "053f8e16c843695b7a23803fbfdd699a8b9c9fe863a613516e4911a6eba0a4cb" + }, + "uri": "pkg:docker/rancher/mirrored-tonistiigi-xx@1.3.0?platform=linux%2Famd64" + }, + { + "digest": { + "sha256": "8f926e98dd809e5fc5971e39df2d88a7dbe4158dcf2c379be658acc67b1beb29" + }, + "uri": "pkg:docker/registry.suse.com/bci/bci-micro@15.6?platform=linux%2Famd64" + }, + { + "digest": { + "sha256": "fdf2b123574c9b00ee19a7009c6b8b11c4e97f3dcb5e27c7ab49d23f8e722d21" + }, + "uri": "pkg:docker/registry.suse.com/bci/golang@1.22?platform=linux%2Famd64" + }, + { + "digest": { + "sha256": "aff42d3167685e4d8e86fda0ad9c6ce6ec6c047bc24d608041d54717a18192ba" + }, + "uri": "https://dl.k8s.io/release/v1.28.7/bin/linux/amd64/kubectl" + } + ] }, "runDetails": { "builder": { "id": "" }, "metadata": { + "invocationID": "ujss3xdtnmbv38uqh5wwftkpd", "startedOn": "2024-07-11T14:49:18.126688014Z", "finishedOn": "2024-07-11T14:51:00.499751748Z" } } -} \ No newline at end of file +}