From 2abc4793ca9035c9b75beda5e09b501aeed57aaf Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Wed, 6 Jul 2022 17:28:33 -0500 Subject: [PATCH 01/12] Create signoff methods for builds validate the image signature and attestation from the validated attestation determine the signoff source get the signoff from the source --- cmd/signOff.go | 86 +++++++++++++++++++ internal/image/build.go | 186 ++++++++++++++++++++++++++++++++++++++++ internal/image/image.go | 142 ++++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 cmd/signOff.go create mode 100644 internal/image/build.go create mode 100644 internal/image/image.go diff --git a/cmd/signOff.go b/cmd/signOff.go new file mode 100644 index 000000000..4125ef802 --- /dev/null +++ b/cmd/signOff.go @@ -0,0 +1,86 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/hacbs-contract/ec-cli/internal/image" + "github.com/spf13/cobra" +) + +func signOffCmd() *cobra.Command { + var data = struct { + imageRef string + publicKey string + }{ + imageRef: "", + publicKey: "", + } + cmd := &cobra.Command{ + Use: "signOff", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples + and usage of using your command. For example: + + Cobra is a CLI library for Go that empowers applications. + This application is a tool to generate the needed files + to quickly create a Cobra application.`, + RunE: func(cmd *cobra.Command, args []string) error { + imageValidator, err := image.NewImageValidator(cmd.Context(), data.imageRef, data.publicKey, "") + if err != nil { + return err + } + + validatedImage, err := imageValidator.ValidateImage(cmd.Context()) + if err != nil { + return err + } + + for _, att := range validatedImage.Attestations { + signoffSource, err := att.AttestationSignoffSource() + if err != nil { + return err + } + if signoffSource == nil { + return errors.New("there is no signoff source in attestation") + } + + signOff, _ := signoffSource.GetBuildSignOff() + + if signOff.Payload != "" { + payload, err := json.Marshal(signOff) + if err != nil { + return err + } + fmt.Println(string(payload)) + } + } + return nil + }, + } + + // attestation download options + cmd.Flags().StringVar(&data.publicKey, "public-key", "", "Public key") + cmd.Flags().StringVar(&data.imageRef, "image-ref", data.imageRef, "The OCI repo to fetch the attestation from.") + + return cmd +} + +func init() { + rootCmd.AddCommand(signOffCmd()) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // signOffCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // signOffCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/internal/image/build.go b/internal/image/build.go new file mode 100644 index 000000000..8ef9deeb9 --- /dev/null +++ b/internal/image/build.go @@ -0,0 +1,186 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package image + +import ( + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/memory" +) + +type invocation struct { + ConfigSource map[string]interface{} `json:"configSource"` + Parameters map[string]string `json:"parameters"` + Environment map[string]interface{} `json:"environment"` +} + +type predicate struct { + Invocation invocation `json:"invocation"` + BuildType string `json:"buildType"` + Metadata map[string]interface{} `json:"metadata"` + Builder map[string]interface{} `json:"builder"` + BuildConfig map[string]interface{} `json:"buildConfig"` + Materials []map[string]interface{} `json:"materials"` +} + +type attestation struct { + Predicate predicate `json:"predicate"` + PredicateType string `json:"predicateType"` + Subject []map[string]interface{} `json:"subject"` + Type string `json:"_type"` +} + +type buildSigner interface { + GetBuildSignOff() (*signOffSignature, error) +} + +type commitSignoffSource struct { + source string + commitSha string +} + +type jiraSignoffSource struct { + source string + jiraid string +} + +type tagSignoffSource struct { + source string + tag string + commitSha string +} + +type signOffSignature struct { + Payload interface{} `json:"payload"` + Source string `json:"source"` +} + +// mocking +type sourceRepository interface { + getRepository(string) (*git.Repository, error) +} + +// From an attestation, find the signOff source (commit, tag, jira) +func (a *attestation) AttestationSignoffSource() (buildSigner, error) { + // the signoff source can be determined by looking into the attestation. + // the attestation can have an env var or something that this can key off of + + // A tag is the preferred sign off method, then the commit, then jira + tag := a.getBuildTag() + commitSha := a.getBuildCommitSha() + + if tag != "" && commitSha != "" { + return &tagSignoffSource{ + source: a.getBuildSCM(), + tag: tag, + commitSha: commitSha, + }, nil + } + + if commitSha != "" { + return &commitSignoffSource{ + source: a.getBuildSCM(), + commitSha: commitSha, + }, nil + } + + return nil, nil +} + +// get the last commit used for the component build +func (a *attestation) getBuildCommitSha() string { + return a.Predicate.Invocation.Parameters["revision"] +} + +// the git url used for the component build +func (a *attestation) getBuildSCM() string { + return a.Predicate.Invocation.Parameters["git-url"] +} + +// if the component repo was tagged, get the tag from the attestation +func (a *attestation) getBuildTag() string { + return a.Predicate.Invocation.Parameters["tag"] +} + +// clone the repo for use +func getRepository(repositoryUrl string) (*git.Repository, error) { + return git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ + URL: repositoryUrl, + }) +} + +// get the commit used for the component build +func getCommit(repositoryUrl, commitSha string) (*object.Commit, error) { + repo, err := getRepository(repositoryUrl) + if err != nil { + return nil, err + } + + commit, err := repo.CommitObject(plumbing.NewHash(commitSha)) + if err != nil { + return nil, err + } + + return commit, nil +} + +// get the tag used for the component build +func getTag(repositoryUrl, tag string) (*plumbing.Reference, error) { + repo, err := getRepository(repositoryUrl) + if err != nil { + return nil, err + } + + ref, err := repo.Tag(tag) + if err != nil { + return nil, err + } + + return ref, nil +} + +// get the commit used for the build and return the repo url and the commit +func (c *commitSignoffSource) GetBuildSignOff() (*signOffSignature, error) { + commit, err := getCommit(c.source, c.commitSha) + if err != nil { + return nil, err + } + + return &signOffSignature{ + Payload: commit, + Source: c.source, + }, nil +} + +// get the tag used for the build and return the repo url and the commit +func (t *tagSignoffSource) GetBuildSignOff() (*signOffSignature, error) { + ref, err := getTag(t.source, t.tag) + if err != nil { + return nil, err + } + + return &signOffSignature{ + Payload: ref, + Source: t.source, + }, nil +} + +// get the jira used for sign off and return the jira and the jira url +func (j *jiraSignoffSource) GetBuildSignOff() (*signOffSignature, error) { + return &signOffSignature{}, nil +} diff --git a/internal/image/image.go b/internal/image/image.go new file mode 100644 index 000000000..8006307a8 --- /dev/null +++ b/internal/image/image.go @@ -0,0 +1,142 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package image + +import ( + "context" + "encoding/json" + "errors" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/sigstore/cosign/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/policy" + "github.com/sigstore/cosign/pkg/signature" +) + +type imageValidator struct { + reference name.Reference + checkOpts cosign.CheckOpts + attestations []oci.Signature +} + +type validatedImage struct { + Reference name.Reference + Attestations []attestation + Signatures []oci.Signature +} + +// NewImageValidator constructs a new imageValidator with the provided parameters +func NewImageValidator(ctx context.Context, image string, publicKey string, rekorURL string) (*imageValidator, error) { + ref, err := name.ParseReference(image) + if err != nil { + return nil, err + } + + verifier, err := signature.PublicKeyFromKeyRef(ctx, publicKey) + if err != nil { + return nil, err + } + + checkOpts := cosign.CheckOpts{} + checkOpts.SigVerifier = verifier + + if rekorURL != "" { + rekorClient, err := rekor.NewClient(rekorURL) + if err != nil { + return nil, err + } + + checkOpts.RekorClient = rekorClient + } + + return &imageValidator{ + reference: ref, + checkOpts: checkOpts, + }, nil +} + +func (i *imageValidator) ValidateImageSignature(ctx context.Context) error { + // TODO check what to do with _, _ + _, _, err := cosign.VerifyImageSignatures(ctx, i.reference, &i.checkOpts) + + return err +} + +func (i *imageValidator) ValidateAttestationSignature(ctx context.Context) error { + // TODO check what to do with _ + attestations, _, err := cosign.VerifyImageAttestations(ctx, i.reference, &i.checkOpts) + if err != nil { + return err + } + + i.attestations = attestations + + return nil +} + +func (i *imageValidator) ValidateImage(ctx context.Context) (*validatedImage, error) { + signatures, _, err := cosign.VerifyImageSignatures(ctx, i.reference, &i.checkOpts) + if err != nil { + return nil, err + } + + attestations, _, err := cosign.VerifyImageAttestations(ctx, i.reference, &i.checkOpts) + attStatements := make([]attestation, 0, len(attestations)) + for _, att := range attestations { + attStatement, err := signatureToAttestation(ctx, att) + if err != nil { + return nil, err + } + attStatements = append(attStatements, attStatement) + + } + if err != nil { + return nil, err + } + + return &validatedImage{ + i.reference, + attStatements, + signatures, + }, nil + +} + +func signatureToAttestation(ctx context.Context, signature oci.Signature) (attestation, error) { + var att attestation + payload, err := policy.AttestationToPayloadJSON(ctx, "slsaprovenance", signature) + if err != nil { + return attestation{}, err + } + + if len(payload) == 0 { + return attestation{}, errors.New("predicate (slsaprovenance) did not match the attestation.") + } + + err = json.Unmarshal(payload, &att) + if err != nil { + return attestation{}, err + } + + return att, nil +} + +func (i *imageValidator) Attestations() []oci.Signature { + return i.attestations +} From 2f77851c34bf9e301078a02386b994965198f375 Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Tue, 12 Jul 2022 15:02:46 -0500 Subject: [PATCH 02/12] creating tests --- cmd/signOff.go | 4 +- internal/image/build.go | 108 ++++-------------- internal/image/build_test.go | 212 +++++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 90 deletions(-) create mode 100644 internal/image/build_test.go diff --git a/cmd/signOff.go b/cmd/signOff.go index 4125ef802..5ed9b6261 100644 --- a/cmd/signOff.go +++ b/cmd/signOff.go @@ -42,7 +42,7 @@ func signOffCmd() *cobra.Command { } for _, att := range validatedImage.Attestations { - signoffSource, err := att.AttestationSignoffSource() + signoffSource, err := att.NewSignoffSource() if err != nil { return err } @@ -50,7 +50,7 @@ func signOffCmd() *cobra.Command { return errors.New("there is no signoff source in attestation") } - signOff, _ := signoffSource.GetBuildSignOff() + signOff, _ := signoffSource.GetSignOff() if signOff.Payload != "" { payload, err := json.Marshal(signOff) diff --git a/internal/image/build.go b/internal/image/build.go index 8ef9deeb9..c70c14639 100644 --- a/internal/image/build.go +++ b/internal/image/build.go @@ -45,60 +45,33 @@ type attestation struct { Type string `json:"_type"` } -type buildSigner interface { - GetBuildSignOff() (*signOffSignature, error) -} - -type commitSignoffSource struct { +type commitSignOff struct { source string commitSha string } -type jiraSignoffSource struct { - source string - jiraid string -} - -type tagSignoffSource struct { - source string - tag string - commitSha string +type signOffSource interface { + GetSignOff() (*signOffSignature, error) + getSource() (interface{}, error) } type signOffSignature struct { - Payload interface{} `json:"payload"` - Source string `json:"source"` -} - -// mocking -type sourceRepository interface { - getRepository(string) (*git.Repository, error) + Payload interface{} `json:"payload"` + Signatures string `json:"signatures"` } // From an attestation, find the signOff source (commit, tag, jira) -func (a *attestation) AttestationSignoffSource() (buildSigner, error) { +func (a *attestation) NewSignoffSource() (signOffSource, error) { // the signoff source can be determined by looking into the attestation. // the attestation can have an env var or something that this can key off of - // A tag is the preferred sign off method, then the commit, then jira - tag := a.getBuildTag() commitSha := a.getBuildCommitSha() - - if tag != "" && commitSha != "" { - return &tagSignoffSource{ - source: a.getBuildSCM(), - tag: tag, - commitSha: commitSha, - }, nil - } - if commitSha != "" { - return &commitSignoffSource{ + return &commitSignOff{ source: a.getBuildSCM(), commitSha: commitSha, }, nil } - return nil, nil } @@ -112,75 +85,34 @@ func (a *attestation) getBuildSCM() string { return a.Predicate.Invocation.Parameters["git-url"] } -// if the component repo was tagged, get the tag from the attestation +// the git url used for the component build func (a *attestation) getBuildTag() string { return a.Predicate.Invocation.Parameters["tag"] } -// clone the repo for use -func getRepository(repositoryUrl string) (*git.Repository, error) { - return git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ - URL: repositoryUrl, - }) -} - -// get the commit used for the component build -func getCommit(repositoryUrl, commitSha string) (*object.Commit, error) { - repo, err := getRepository(repositoryUrl) - if err != nil { - return nil, err - } - - commit, err := repo.CommitObject(plumbing.NewHash(commitSha)) - if err != nil { - return nil, err - } - - return commit, nil -} - -// get the tag used for the component build -func getTag(repositoryUrl, tag string) (*plumbing.Reference, error) { - repo, err := getRepository(repositoryUrl) - if err != nil { - return nil, err - } - - ref, err := repo.Tag(tag) +func (c *commitSignOff) GetSignOff() (*signOffSignature, error) { + commit, err := c.getSource() if err != nil { return nil, err } - return ref, nil + return &signOffSignature{ + Payload: commit.(*object.Commit), + }, nil } -// get the commit used for the build and return the repo url and the commit -func (c *commitSignoffSource) GetBuildSignOff() (*signOffSignature, error) { - commit, err := getCommit(c.source, c.commitSha) +func (c *commitSignOff) getSource() (interface{}, error) { + repo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ + URL: c.source, + }) if err != nil { return nil, err } - return &signOffSignature{ - Payload: commit, - Source: c.source, - }, nil -} - -// get the tag used for the build and return the repo url and the commit -func (t *tagSignoffSource) GetBuildSignOff() (*signOffSignature, error) { - ref, err := getTag(t.source, t.tag) + commit, err := repo.CommitObject(plumbing.NewHash(c.commitSha)) if err != nil { return nil, err } - return &signOffSignature{ - Payload: ref, - Source: t.source, - }, nil -} - -// get the jira used for sign off and return the jira and the jira url -func (j *jiraSignoffSource) GetBuildSignOff() (*signOffSignature, error) { - return &signOffSignature{}, nil + return commit, nil } diff --git a/internal/image/build_test.go b/internal/image/build_test.go new file mode 100644 index 000000000..6e5da7458 --- /dev/null +++ b/internal/image/build_test.go @@ -0,0 +1,212 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package image + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func paramsInput(input string) attestation { + params := map[string]string{} + if input == "good-commit" { + params = map[string]string{ + "git-url": "https://github.com/joejstuart/ec-cli.git", + "revision": "7efe1826a741d23f8ed874598f6ce9b882734d88", + } + } else if input == "bad-commit" { + params = map[string]string{ + "git-url": "https://github.com/joejstuart/ec-cli.git", + } + } else if input == "bad-git" { + params = map[string]string{ + "tag": "v0.0.1", + } + } else if input == "good-git" { + params = map[string]string{ + "git-url": "https://github.com/joejstuart/ec-cli.git", + "tag": "v0.0.1", + } + } else if input == "good-just-tag" { + params = map[string]string{ + "tag": "v0.0.1", + } + } else if input == "bad-just-tag" { + params = map[string]string{ + "git-url": "https://github.com/joejstuart/ec-cli.git", + } + } + + invocation := invocation{ + Parameters: params, + } + + pred := predicate{ + Invocation: invocation, + } + att := attestation{ + Predicate: pred, + } + + return att +} + +func Test_AttestationSignoffSource_commit(t *testing.T) { + tests := []struct { + input attestation + want signOffSource + }{ + { + paramsInput("good-commit"), + &commitSignOff{ + source: "https://github.com/joejstuart/ec-cli.git", + commitSha: "7efe1826a741d23f8ed874598f6ce9b882734d88", + }, + }, + { + paramsInput("bad-commit"), + nil, + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("AttestationSignoffSource=%d", i), func(t *testing.T) { + got, _ := tc.input.NewSignoffSource() + if !reflect.DeepEqual(got, tc.want) { + t.Fatalf("got %v; want %v", got, tc.want) + } else { + t.Logf("Success !") + } + }) + } +} + +func Test_GetBuildCommitSha(t *testing.T) { + tests := []struct { + input attestation + want string + }{ + {paramsInput("good-commit"), "7efe1826a741d23f8ed874598f6ce9b882734d88"}, + {paramsInput("bad-commit"), ""}, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("GetBuildCommitSha=%d", i), func(t *testing.T) { + got := tc.input.getBuildCommitSha() + if got != tc.want { + t.Fatalf("got %v; want %v", got, tc.want) + } else { + t.Logf("Success !") + } + }) + } +} + +func Test_GetBuildSCM(t *testing.T) { + tests := []struct { + input attestation + want string + }{ + {paramsInput("good-git"), "https://github.com/joejstuart/ec-cli.git"}, + {paramsInput("bad-git"), ""}, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("GetBuildSCM=%d", i), func(t *testing.T) { + got := tc.input.getBuildSCM() + if got != tc.want { + t.Fatalf("got %v; want %v", got, tc.want) + } else { + t.Logf("Success !") + } + }) + } +} + +func Test_GetBuildTag(t *testing.T) { + tests := []struct { + input attestation + want string + }{ + {paramsInput("good-just-tag"), "v0.0.1"}, + {paramsInput("bad-just-tag"), ""}, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("GetBuildTag=%d", i), func(t *testing.T) { + got := tc.input.getBuildTag() + if got != tc.want { + t.Fatalf("got %v; want %v", got, tc.want) + } else { + t.Logf("Success !") + } + }) + } +} + +func Test_GetSignOff(t *testing.T) { + hash := "7efe1826a741d23f8ed874598f6ce9b882734d88" + message := `Create signoff methods for builds + +validate the image signature and attestation + +from the validated attestation determine the signoff source + +get the signoff from the source +` + + commitObject := &object.Commit{ + Hash: plumbing.NewHash(hash), + Message: message, + Author: object.Signature{ + Name: "Joe Stuart", + Email: "joe.stuart@gmail.com", + When: time.Date( + 2022, 07, 06, 17, 28, 33, 50000, time.Local), + }, + } + tests := []struct { + input *commitSignOff + want *signOffSignature + }{ + { + &commitSignOff{ + source: "https://github.com/joejstuart/ec-cli.git", + commitSha: "7efe1826a741d23f8ed874598f6ce9b882734d88", + }, + &signOffSignature{ + Payload: commitObject, + }, + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("GetSignoffSource=%d", i), func(t *testing.T) { + signOff, _ := tc.input.GetSignOff() + if reflect.TypeOf(signOff) != reflect.TypeOf(tc.want) { + t.Fatalf("got %v want %v", signOff, tc.want) + } else { + t.Logf("Success !") + } + }) + } +} From 03afa037dce5b782205f4e40d2b841fa4b4ff6c0 Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Wed, 13 Jul 2022 11:56:04 -0500 Subject: [PATCH 03/12] capture actual signature --- cmd/signOff.go | 39 ++++++++++++++-------------- internal/image/build.go | 49 ++++++++++++++++++++++++++++++------ internal/image/build_test.go | 25 +++++++++++------- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/cmd/signOff.go b/cmd/signOff.go index 5ed9b6261..2605c06d6 100644 --- a/cmd/signOff.go +++ b/cmd/signOff.go @@ -1,7 +1,19 @@ -/* -Copyright © 2022 NAME HERE +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 -*/ package cmd import ( @@ -23,13 +35,10 @@ func signOffCmd() *cobra.Command { } cmd := &cobra.Command{ Use: "signOff", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples - and usage of using your command. For example: + Short: "Capture signed off signatures from a source (github repo, Jira)", + Long: `capture signed off signatures from a source (github repo, Jira): - Cobra is a CLI library for Go that empowers applications. - This application is a tool to generate the needed files - to quickly create a Cobra application.`, + supported signature sources are commits captured from a git repo and jira issues.`, RunE: func(cmd *cobra.Command, args []string) error { imageValidator, err := image.NewImageValidator(cmd.Context(), data.imageRef, data.publicKey, "") if err != nil { @@ -52,7 +61,7 @@ func signOffCmd() *cobra.Command { signOff, _ := signoffSource.GetSignOff() - if signOff.Payload != "" { + if signOff != nil { payload, err := json.Marshal(signOff) if err != nil { return err @@ -73,14 +82,4 @@ func signOffCmd() *cobra.Command { func init() { rootCmd.AddCommand(signOffCmd()) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // signOffCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // signOffCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/internal/image/build.go b/internal/image/build.go index c70c14639..77af8e9dc 100644 --- a/internal/image/build.go +++ b/internal/image/build.go @@ -17,6 +17,10 @@ package image import ( + "fmt" + "regexp" + "strings" + "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" @@ -50,13 +54,13 @@ type commitSignOff struct { commitSha string } +// there can be multiple sign off sources (git commit, tag and jira issues) type signOffSource interface { GetSignOff() (*signOffSignature, error) - getSource() (interface{}, error) } type signOffSignature struct { - Payload interface{} `json:"payload"` + Body interface{} `json:"body"` Signatures string `json:"signatures"` } @@ -90,29 +94,60 @@ func (a *attestation) getBuildTag() string { return a.Predicate.Invocation.Parameters["tag"] } +// returns the signOff signature and body of the source func (c *commitSignOff) GetSignOff() (*signOffSignature, error) { - commit, err := c.getSource() + commit, err := getCommitSource(c) if err != nil { return nil, err } return &signOffSignature{ - Payload: commit.(*object.Commit), + Body: commit, + Signatures: captureCommitSignatures(commit.Message), }, nil } -func (c *commitSignOff) getSource() (interface{}, error) { +// get the build commit source for use in GetSignOff +func getCommitSource(data *commitSignOff) (*object.Commit, error) { repo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ - URL: c.source, + URL: data.source, }) if err != nil { return nil, err } - commit, err := repo.CommitObject(plumbing.NewHash(c.commitSha)) + commit, err := repo.CommitObject(plumbing.NewHash(data.commitSha)) if err != nil { return nil, err } return commit, nil } + +// parse a commit and capture signatures +func captureCommitSignatures(message string) string { + var capturedSignatures []string + signatureHeader := "Signed-off-by" + // loop over each line of the commit message looking for "Signed-off-by" + for _, line := range strings.Split(message, "\n") { + regex := fmt.Sprintf("^%s", signatureHeader) + match, _ := regexp.MatchString(regex, line) + // if there's a match, split on "Signed-off-by", then capture each signature after + if match { + results := strings.Split(line, signatureHeader) + for _, signature := range strings.Split(results[len(results)-1], ",") { + sigRegex := regexp.MustCompile("([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+.[a-zA-Z0-9_-]+)") + sigMatch := sigRegex.FindAllStringSubmatch(signature, -1) + if len(sigMatch) > 0 { + capturedSignatures = append(capturedSignatures, sigMatch[0][0]) + } + } + } + } + + if len(capturedSignatures) > 0 { + return strings.Join(capturedSignatures, ",") + } + return "" + +} diff --git a/internal/image/build_test.go b/internal/image/build_test.go index 6e5da7458..efb343f25 100644 --- a/internal/image/build_test.go +++ b/internal/image/build_test.go @@ -31,7 +31,7 @@ func paramsInput(input string) attestation { if input == "good-commit" { params = map[string]string{ "git-url": "https://github.com/joejstuart/ec-cli.git", - "revision": "7efe1826a741d23f8ed874598f6ce9b882734d88", + "revision": "6c1f093c0c197add71579d392da8a79a984fcd62", } } else if input == "bad-commit" { params = map[string]string{ @@ -79,7 +79,7 @@ func Test_AttestationSignoffSource_commit(t *testing.T) { paramsInput("good-commit"), &commitSignOff{ source: "https://github.com/joejstuart/ec-cli.git", - commitSha: "7efe1826a741d23f8ed874598f6ce9b882734d88", + commitSha: "6c1f093c0c197add71579d392da8a79a984fcd62", }, }, { @@ -105,7 +105,7 @@ func Test_GetBuildCommitSha(t *testing.T) { input attestation want string }{ - {paramsInput("good-commit"), "7efe1826a741d23f8ed874598f6ce9b882734d88"}, + {paramsInput("good-commit"), "6c1f093c0c197add71579d392da8a79a984fcd62"}, {paramsInput("bad-commit"), ""}, } @@ -164,7 +164,7 @@ func Test_GetBuildTag(t *testing.T) { } func Test_GetSignOff(t *testing.T) { - hash := "7efe1826a741d23f8ed874598f6ce9b882734d88" + hash := "6c1f093c0c197add71579d392da8a79a984fcd62" message := `Create signoff methods for builds validate the image signature and attestation @@ -172,6 +172,8 @@ validate the image signature and attestation from the validated attestation determine the signoff source get the signoff from the source + +Signed-off-by: , ` commitObject := &object.Commit{ @@ -191,21 +193,26 @@ get the signoff from the source { &commitSignOff{ source: "https://github.com/joejstuart/ec-cli.git", - commitSha: "7efe1826a741d23f8ed874598f6ce9b882734d88", + commitSha: "6c1f093c0c197add71579d392da8a79a984fcd62", }, &signOffSignature{ - Payload: commitObject, + Body: commitObject, + Signatures: "jstuart@redhat.com", }, }, } for i, tc := range tests { t.Run(fmt.Sprintf("GetSignoffSource=%d", i), func(t *testing.T) { - signOff, _ := tc.input.GetSignOff() - if reflect.TypeOf(signOff) != reflect.TypeOf(tc.want) { + signOff, err := tc.input.GetSignOff() + if err != nil { + t.Logf("error: %v", err) + } else if reflect.TypeOf(signOff) != reflect.TypeOf(tc.want) { t.Fatalf("got %v want %v", signOff, tc.want) + } else if signOff.Signatures != tc.want.Signatures { + t.Fatalf("got %v want %v", signOff.Signatures, tc.want.Signatures) } else { - t.Logf("Success !") + t.Logf("Success!") } }) } From f4ed7bbe063d47a65c460fafb0e7a50451884f30 Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Wed, 13 Jul 2022 15:11:53 -0500 Subject: [PATCH 04/12] use list of emails --- internal/image/build.go | 8 ++++---- internal/image/build_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/image/build.go b/internal/image/build.go index 77af8e9dc..109cab75f 100644 --- a/internal/image/build.go +++ b/internal/image/build.go @@ -61,7 +61,7 @@ type signOffSource interface { type signOffSignature struct { Body interface{} `json:"body"` - Signatures string `json:"signatures"` + Signatures []string `json:"signatures"` } // From an attestation, find the signOff source (commit, tag, jira) @@ -125,7 +125,7 @@ func getCommitSource(data *commitSignOff) (*object.Commit, error) { } // parse a commit and capture signatures -func captureCommitSignatures(message string) string { +func captureCommitSignatures(message string) []string { var capturedSignatures []string signatureHeader := "Signed-off-by" // loop over each line of the commit message looking for "Signed-off-by" @@ -146,8 +146,8 @@ func captureCommitSignatures(message string) string { } if len(capturedSignatures) > 0 { - return strings.Join(capturedSignatures, ",") + return capturedSignatures } - return "" + return []string{} } diff --git a/internal/image/build_test.go b/internal/image/build_test.go index efb343f25..c8f9b3575 100644 --- a/internal/image/build_test.go +++ b/internal/image/build_test.go @@ -197,7 +197,7 @@ Signed-off-by: , }, &signOffSignature{ Body: commitObject, - Signatures: "jstuart@redhat.com", + Signatures: []string{"jstuart@redhat.com"}, }, }, } @@ -209,7 +209,7 @@ Signed-off-by: , t.Logf("error: %v", err) } else if reflect.TypeOf(signOff) != reflect.TypeOf(tc.want) { t.Fatalf("got %v want %v", signOff, tc.want) - } else if signOff.Signatures != tc.want.Signatures { + } else if signOff.Signatures[0] != tc.want.Signatures[0] { t.Fatalf("got %v want %v", signOff.Signatures, tc.want.Signatures) } else { t.Logf("Success!") From 97a5421abd88170004184bb942770659b8fc7279 Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Thu, 14 Jul 2022 11:36:41 -0500 Subject: [PATCH 05/12] mock git clone --- cmd/{signOff.go => sign_off.go} | 15 +++++++++------ internal/image/build.go | 12 ++++++++---- internal/image/build_test.go | 15 ++++++++++++++- 3 files changed, 31 insertions(+), 11 deletions(-) rename cmd/{signOff.go => sign_off.go} (82%) diff --git a/cmd/signOff.go b/cmd/sign_off.go similarity index 82% rename from cmd/signOff.go rename to cmd/sign_off.go index 2605c06d6..b999e93d5 100644 --- a/cmd/signOff.go +++ b/cmd/sign_off.go @@ -34,11 +34,11 @@ func signOffCmd() *cobra.Command { publicKey: "", } cmd := &cobra.Command{ - Use: "signOff", + Use: "sign-off", Short: "Capture signed off signatures from a source (github repo, Jira)", - Long: `capture signed off signatures from a source (github repo, Jira): - - supported signature sources are commits captured from a git repo and jira issues.`, + Long: `Supported sign off sources are commits captured from a git repo and jira issues. + The git sources return a signed off value and the git commit. The jira issue is + a TODO, but will return the Jira issue with any sign off values.`, RunE: func(cmd *cobra.Command, args []string) error { imageValidator, err := image.NewImageValidator(cmd.Context(), data.imageRef, data.publicKey, "") if err != nil { @@ -51,7 +51,7 @@ func signOffCmd() *cobra.Command { } for _, att := range validatedImage.Attestations { - signoffSource, err := att.NewSignoffSource() + signoffSource, err := att.NewSignOffSource() if err != nil { return err } @@ -59,7 +59,10 @@ func signOffCmd() *cobra.Command { return errors.New("there is no signoff source in attestation") } - signOff, _ := signoffSource.GetSignOff() + signOff, err := signoffSource.GetSignOff() + if err != nil { + return err + } if signOff != nil { payload, err := json.Marshal(signOff) diff --git a/internal/image/build.go b/internal/image/build.go index 109cab75f..348f9bb5f 100644 --- a/internal/image/build.go +++ b/internal/image/build.go @@ -27,6 +27,8 @@ import ( "github.com/go-git/go-git/v5/storage/memory" ) +var gitClone = git.Clone + type invocation struct { ConfigSource map[string]interface{} `json:"configSource"` Parameters map[string]string `json:"parameters"` @@ -65,7 +67,7 @@ type signOffSignature struct { } // From an attestation, find the signOff source (commit, tag, jira) -func (a *attestation) NewSignoffSource() (signOffSource, error) { +func (a *attestation) NewSignOffSource() (signOffSource, error) { // the signoff source can be determined by looking into the attestation. // the attestation can have an env var or something that this can key off of @@ -103,15 +105,17 @@ func (c *commitSignOff) GetSignOff() (*signOffSignature, error) { return &signOffSignature{ Body: commit, - Signatures: captureCommitSignatures(commit.Message), + Signatures: captureCommitSignOff(commit.Message), }, nil } // get the build commit source for use in GetSignOff func getCommitSource(data *commitSignOff) (*object.Commit, error) { - repo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ + repo, err := gitClone(memory.NewStorage(), nil, &git.CloneOptions{ URL: data.source, }) + + fmt.Println(repo) if err != nil { return nil, err } @@ -125,7 +129,7 @@ func getCommitSource(data *commitSignOff) (*object.Commit, error) { } // parse a commit and capture signatures -func captureCommitSignatures(message string) []string { +func captureCommitSignOff(message string) []string { var capturedSignatures []string signatureHeader := "Signed-off-by" // loop over each line of the commit message looking for "Signed-off-by" diff --git a/internal/image/build_test.go b/internal/image/build_test.go index c8f9b3575..ae51d804c 100644 --- a/internal/image/build_test.go +++ b/internal/image/build_test.go @@ -22,8 +22,12 @@ import ( "testing" "time" + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage" + "github.com/go-git/go-git/v5/storage/memory" ) func paramsInput(input string) attestation { @@ -90,7 +94,7 @@ func Test_AttestationSignoffSource_commit(t *testing.T) { for i, tc := range tests { t.Run(fmt.Sprintf("AttestationSignoffSource=%d", i), func(t *testing.T) { - got, _ := tc.input.NewSignoffSource() + got, _ := tc.input.NewSignOffSource() if !reflect.DeepEqual(got, tc.want) { t.Fatalf("got %v; want %v", got, tc.want) } else { @@ -202,6 +206,15 @@ Signed-off-by: , }, } + savedClone := gitClone + defer func() { gitClone = savedClone }() + + gitClone = func(s storage.Storer, worktree billy.Filesystem, o *git.CloneOptions) (*git.Repository, error) { + return &git.Repository{ + Storer: memory.NewStorage(), + }, nil + } + for i, tc := range tests { t.Run(fmt.Sprintf("GetSignoffSource=%d", i), func(t *testing.T) { signOff, err := tc.input.GetSignOff() From 9c0cd259b7b1ad9f8f202d07e5efbc5377c15199 Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Mon, 18 Jul 2022 13:11:39 -0500 Subject: [PATCH 06/12] use materials --- internal/image/build.go | 38 +++++++++++----- internal/image/build_test.go | 85 +++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 50 deletions(-) diff --git a/internal/image/build.go b/internal/image/build.go index 348f9bb5f..def97d66b 100644 --- a/internal/image/build.go +++ b/internal/image/build.go @@ -23,7 +23,6 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/storage/memory" ) @@ -61,6 +60,13 @@ type signOffSource interface { GetSignOff() (*signOffSignature, error) } +type commit struct { + Sha string `json:"sha"` + Author string `json:"author"` + Date string `json:"date"` + Message string `json:"message"` +} + type signOffSignature struct { Body interface{} `json:"body"` Signatures []string `json:"signatures"` @@ -72,7 +78,8 @@ func (a *attestation) NewSignOffSource() (signOffSource, error) { // the attestation can have an env var or something that this can key off of commitSha := a.getBuildCommitSha() - if commitSha != "" { + repo := a.getBuildSCM() + if commitSha != "" && repo != "" { return &commitSignOff{ source: a.getBuildSCM(), commitSha: commitSha, @@ -83,17 +90,21 @@ func (a *attestation) NewSignOffSource() (signOffSource, error) { // get the last commit used for the component build func (a *attestation) getBuildCommitSha() string { - return a.Predicate.Invocation.Parameters["revision"] + return a.getMaterialsString("CHAINS-GIT_COMMIT") } // the git url used for the component build func (a *attestation) getBuildSCM() string { - return a.Predicate.Invocation.Parameters["git-url"] + return a.getMaterialsString("CHAINS-GIT_URL") } -// the git url used for the component build -func (a *attestation) getBuildTag() string { - return a.Predicate.Invocation.Parameters["tag"] +func (a *attestation) getMaterialsString(key string) string { + materials := a.Predicate.Materials + // materials is an array. If it's empty or greater than 1, return nothing + if len(materials) != 1 { + return "" + } + return a.Predicate.Materials[0][key].(string) } // returns the signOff signature and body of the source @@ -110,22 +121,27 @@ func (c *commitSignOff) GetSignOff() (*signOffSignature, error) { } // get the build commit source for use in GetSignOff -func getCommitSource(data *commitSignOff) (*object.Commit, error) { +func getCommitSource(data *commitSignOff) (*commit, error) { repo, err := gitClone(memory.NewStorage(), nil, &git.CloneOptions{ URL: data.source, }) - fmt.Println(repo) if err != nil { return nil, err } - commit, err := repo.CommitObject(plumbing.NewHash(data.commitSha)) + gitCommit, err := repo.CommitObject(plumbing.NewHash(data.commitSha)) if err != nil { return nil, err } - return commit, nil + return &commit{ + Sha: gitCommit.Hash.String(), + Author: fmt.Sprintf("%s <%s>", gitCommit.Author.Name, gitCommit.Author.Email), + Date: gitCommit.Author.When.String(), + Message: gitCommit.Message, + }, nil + } // parse a commit and capture signatures diff --git a/internal/image/build_test.go b/internal/image/build_test.go index ae51d804c..937d22256 100644 --- a/internal/image/build_test.go +++ b/internal/image/build_test.go @@ -20,14 +20,11 @@ import ( "fmt" "reflect" "testing" - "time" "github.com/go-git/go-billy/v5" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/storage" - "github.com/go-git/go-git/v5/storage/memory" ) func paramsInput(input string) attestation { @@ -146,28 +143,7 @@ func Test_GetBuildSCM(t *testing.T) { } } -func Test_GetBuildTag(t *testing.T) { - tests := []struct { - input attestation - want string - }{ - {paramsInput("good-just-tag"), "v0.0.1"}, - {paramsInput("bad-just-tag"), ""}, - } - - for i, tc := range tests { - t.Run(fmt.Sprintf("GetBuildTag=%d", i), func(t *testing.T) { - got := tc.input.getBuildTag() - if got != tc.want { - t.Fatalf("got %v; want %v", got, tc.want) - } else { - t.Logf("Success !") - } - }) - } -} - -func Test_GetSignOff(t *testing.T) { +func mock_commit_getter(data *commitSignOff) (*commit, error) { hash := "6c1f093c0c197add71579d392da8a79a984fcd62" message := `Create signoff methods for builds @@ -179,17 +155,46 @@ get the signoff from the source Signed-off-by: , ` - - commitObject := &object.Commit{ - Hash: plumbing.NewHash(hash), + return &commit{ + Sha: hash, + Author: "ec RedHat ", + Date: "Wed July 6 5:28:33 2022 -0500", Message: message, - Author: object.Signature{ - Name: "Joe Stuart", - Email: "joe.stuart@gmail.com", - When: time.Date( - 2022, 07, 06, 17, 28, 33, 50000, time.Local), + }, nil +} + +type repo interface { + CommitObject(hash plumbing.Hash) (*mockCommit, error) +} + +type mockAuthor struct { + Name string + Email string + When string +} + +type mockCommit struct { + Author mockAuthor + Message string + Hash string +} + +type mockRepo struct { +} + +func (r *mockRepo) CommitObject(hash plumbing.Hash) (*mockCommit, error) { + return &mockCommit{ + Author: mockAuthor{ + Name: "ec RedHat", + Email: "", + When: "Wed July 6 5:28:33 2022 -0500", }, - } + Message: "Signed-off-by: ", + Hash: "6c1f093c0c197add71579d392da8a79a984fcd62", + }, nil +} + +func Test_GetSignOff(t *testing.T) { tests := []struct { input *commitSignOff want *signOffSignature @@ -200,7 +205,11 @@ Signed-off-by: , commitSha: "6c1f093c0c197add71579d392da8a79a984fcd62", }, &signOffSignature{ - Body: commitObject, + Body: commit{ + Sha: "6c1f093c0c197add71579d392da8a79a984fcd62", + Author: "ec RedHat ", + Date: "Wed July 6 5:28:33 2022 -0500", + }, Signatures: []string{"jstuart@redhat.com"}, }, }, @@ -209,10 +218,8 @@ Signed-off-by: , savedClone := gitClone defer func() { gitClone = savedClone }() - gitClone = func(s storage.Storer, worktree billy.Filesystem, o *git.CloneOptions) (*git.Repository, error) { - return &git.Repository{ - Storer: memory.NewStorage(), - }, nil + gitClone = func(s storage.Storer, worktree billy.Filesystem, o *git.CloneOptions) (*repo, error) { + return &mockRepo{}, nil } for i, tc := range tests { From f87fc10da144378b7c50ea76154e4883acbf4278 Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Mon, 18 Jul 2022 14:45:51 -0500 Subject: [PATCH 07/12] test update --- internal/image/build.go | 19 +++++++++++++++--- internal/image/build_test.go | 39 +++++------------------------------- 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/internal/image/build.go b/internal/image/build.go index def97d66b..2370ee193 100644 --- a/internal/image/build.go +++ b/internal/image/build.go @@ -90,12 +90,25 @@ func (a *attestation) NewSignOffSource() (signOffSource, error) { // get the last commit used for the component build func (a *attestation) getBuildCommitSha() string { - return a.getMaterialsString("CHAINS-GIT_COMMIT") + digest := a.getMaterialsMap("digest") + if digest != nil { + return digest["sha"] + } + return "" } // the git url used for the component build func (a *attestation) getBuildSCM() string { - return a.getMaterialsString("CHAINS-GIT_URL") + return a.getMaterialsString("uri") +} + +func (a *attestation) getMaterialsMap(key string) map[string]string { + materials := a.Predicate.Materials + // materials is an array. If it's empty or greater than 1, return nothing + if len(materials) != 1 { + return map[string]string{} + } + return materials[0][key].(map[string]string) } func (a *attestation) getMaterialsString(key string) string { @@ -104,7 +117,7 @@ func (a *attestation) getMaterialsString(key string) string { if len(materials) != 1 { return "" } - return a.Predicate.Materials[0][key].(string) + return materials[0][key].(string) } // returns the signOff signature and body of the source diff --git a/internal/image/build_test.go b/internal/image/build_test.go index 937d22256..f919b6d3a 100644 --- a/internal/image/build_test.go +++ b/internal/image/build_test.go @@ -23,8 +23,8 @@ import ( "github.com/go-git/go-billy/v5" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/storage" + "github.com/go-git/go-git/v5/storage/memory" ) func paramsInput(input string) attestation { @@ -163,37 +163,6 @@ Signed-off-by: , }, nil } -type repo interface { - CommitObject(hash plumbing.Hash) (*mockCommit, error) -} - -type mockAuthor struct { - Name string - Email string - When string -} - -type mockCommit struct { - Author mockAuthor - Message string - Hash string -} - -type mockRepo struct { -} - -func (r *mockRepo) CommitObject(hash plumbing.Hash) (*mockCommit, error) { - return &mockCommit{ - Author: mockAuthor{ - Name: "ec RedHat", - Email: "", - When: "Wed July 6 5:28:33 2022 -0500", - }, - Message: "Signed-off-by: ", - Hash: "6c1f093c0c197add71579d392da8a79a984fcd62", - }, nil -} - func Test_GetSignOff(t *testing.T) { tests := []struct { input *commitSignOff @@ -218,8 +187,10 @@ func Test_GetSignOff(t *testing.T) { savedClone := gitClone defer func() { gitClone = savedClone }() - gitClone = func(s storage.Storer, worktree billy.Filesystem, o *git.CloneOptions) (*repo, error) { - return &mockRepo{}, nil + gitClone = func(s storage.Storer, worktree billy.Filesystem, o *git.CloneOptions) (*git.Repository, error) { + return &git.Repository{ + Storer: memory.NewStorage(), + }, nil } for i, tc := range tests { From 62d5ce715308cdfb883ea420d583aba8c903e39b Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Mon, 18 Jul 2022 17:47:10 -0500 Subject: [PATCH 08/12] using mail parser --- cmd/sign_off.go | 3 +- internal/image/build.go | 63 +++++++++++++++--------------------- internal/image/build_test.go | 59 ++++++++------------------------- 3 files changed, 42 insertions(+), 83 deletions(-) diff --git a/cmd/sign_off.go b/cmd/sign_off.go index b999e93d5..8d6277e09 100644 --- a/cmd/sign_off.go +++ b/cmd/sign_off.go @@ -21,8 +21,9 @@ import ( "errors" "fmt" - "github.com/hacbs-contract/ec-cli/internal/image" "github.com/spf13/cobra" + + "github.com/hacbs-contract/ec-cli/internal/image" ) func signOffCmd() *cobra.Command { diff --git a/internal/image/build.go b/internal/image/build.go index 2370ee193..fffcdbb9c 100644 --- a/internal/image/build.go +++ b/internal/image/build.go @@ -18,6 +18,7 @@ package image import ( "fmt" + "net/mail" "regexp" "strings" @@ -34,13 +35,18 @@ type invocation struct { Environment map[string]interface{} `json:"environment"` } +type materials struct { + Uri string `json:"sha"` + Digest map[string]string `json:"digest"` +} + type predicate struct { - Invocation invocation `json:"invocation"` - BuildType string `json:"buildType"` - Metadata map[string]interface{} `json:"metadata"` - Builder map[string]interface{} `json:"builder"` - BuildConfig map[string]interface{} `json:"buildConfig"` - Materials []map[string]interface{} `json:"materials"` + Invocation invocation `json:"invocation"` + BuildType string `json:"buildType"` + Metadata map[string]interface{} `json:"metadata"` + Builder map[string]interface{} `json:"builder"` + BuildConfig map[string]interface{} `json:"buildConfig"` + Materials []materials `json:"materials"` } type attestation struct { @@ -90,34 +96,18 @@ func (a *attestation) NewSignOffSource() (signOffSource, error) { // get the last commit used for the component build func (a *attestation) getBuildCommitSha() string { - digest := a.getMaterialsMap("digest") - if digest != nil { - return digest["sha"] + if len(a.Predicate.Materials) == 1 { + return a.Predicate.Materials[0].Digest["sha"] } return "" } // the git url used for the component build func (a *attestation) getBuildSCM() string { - return a.getMaterialsString("uri") -} - -func (a *attestation) getMaterialsMap(key string) map[string]string { - materials := a.Predicate.Materials - // materials is an array. If it's empty or greater than 1, return nothing - if len(materials) != 1 { - return map[string]string{} - } - return materials[0][key].(map[string]string) -} - -func (a *attestation) getMaterialsString(key string) string { - materials := a.Predicate.Materials - // materials is an array. If it's empty or greater than 1, return nothing - if len(materials) != 1 { - return "" + if len(a.Predicate.Materials) == 1 { + return a.Predicate.Materials[0].Uri } - return materials[0][key].(string) + return "" } // returns the signOff signature and body of the source @@ -160,20 +150,20 @@ func getCommitSource(data *commitSignOff) (*commit, error) { // parse a commit and capture signatures func captureCommitSignOff(message string) []string { var capturedSignatures []string - signatureHeader := "Signed-off-by" - // loop over each line of the commit message looking for "Signed-off-by" + signatureHeader := "Signed-off-by:" + // loop over each line of the commit message looking for "Signed-off-by:" for _, line := range strings.Split(message, "\n") { regex := fmt.Sprintf("^%s", signatureHeader) match, _ := regexp.MatchString(regex, line) - // if there's a match, split on "Signed-off-by", then capture each signature after + // if there's a match, split on "Signed-off-by:", then capture each signature after if match { results := strings.Split(line, signatureHeader) - for _, signature := range strings.Split(results[len(results)-1], ",") { - sigRegex := regexp.MustCompile("([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+.[a-zA-Z0-9_-]+)") - sigMatch := sigRegex.FindAllStringSubmatch(signature, -1) - if len(sigMatch) > 0 { - capturedSignatures = append(capturedSignatures, sigMatch[0][0]) - } + signatures, err := mail.ParseAddressList(results[len(results)-1]) + if err != nil { + continue + } + for _, signature := range signatures { + capturedSignatures = append(capturedSignatures, signature.Address) } } } @@ -182,5 +172,4 @@ func captureCommitSignOff(message string) []string { return capturedSignatures } return []string{} - } diff --git a/internal/image/build_test.go b/internal/image/build_test.go index f919b6d3a..8ff04929f 100644 --- a/internal/image/build_test.go +++ b/internal/image/build_test.go @@ -25,44 +25,33 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/storage" "github.com/go-git/go-git/v5/storage/memory" + "github.com/stretchr/testify/assert" ) func paramsInput(input string) attestation { - params := map[string]string{} + params := materials{} if input == "good-commit" { - params = map[string]string{ - "git-url": "https://github.com/joejstuart/ec-cli.git", - "revision": "6c1f093c0c197add71579d392da8a79a984fcd62", + params.Digest = map[string]string{ + "sha": "6c1f093c0c197add71579d392da8a79a984fcd62", } + params.Uri = "https://github.com/joejstuart/ec-cli.git" } else if input == "bad-commit" { - params = map[string]string{ - "git-url": "https://github.com/joejstuart/ec-cli.git", - } + params.Uri = "" } else if input == "bad-git" { - params = map[string]string{ - "tag": "v0.0.1", - } + params.Uri = "" } else if input == "good-git" { - params = map[string]string{ - "git-url": "https://github.com/joejstuart/ec-cli.git", - "tag": "v0.0.1", - } - } else if input == "good-just-tag" { - params = map[string]string{ - "tag": "v0.0.1", - } - } else if input == "bad-just-tag" { - params = map[string]string{ - "git-url": "https://github.com/joejstuart/ec-cli.git", + params.Uri = "https://github.com/joejstuart/ec-cli.git" + params.Digest = map[string]string{ + "sha": "6c1f093c0c197add71579d392da8a79a984fcd62", } } - invocation := invocation{ - Parameters: params, + materials := []materials{ + params, } pred := predicate{ - Invocation: invocation, + Materials: materials, } att := attestation{ Predicate: pred, @@ -92,7 +81,7 @@ func Test_AttestationSignoffSource_commit(t *testing.T) { for i, tc := range tests { t.Run(fmt.Sprintf("AttestationSignoffSource=%d", i), func(t *testing.T) { got, _ := tc.input.NewSignOffSource() - if !reflect.DeepEqual(got, tc.want) { + if !assert.ObjectsAreEqual(tc.want, got) { t.Fatalf("got %v; want %v", got, tc.want) } else { t.Logf("Success !") @@ -143,26 +132,6 @@ func Test_GetBuildSCM(t *testing.T) { } } -func mock_commit_getter(data *commitSignOff) (*commit, error) { - hash := "6c1f093c0c197add71579d392da8a79a984fcd62" - message := `Create signoff methods for builds - -validate the image signature and attestation - -from the validated attestation determine the signoff source - -get the signoff from the source - -Signed-off-by: , -` - return &commit{ - Sha: hash, - Author: "ec RedHat ", - Date: "Wed July 6 5:28:33 2022 -0500", - Message: message, - }, nil -} - func Test_GetSignOff(t *testing.T) { tests := []struct { input *commitSignOff From 0f05e37e1106fea72e8e5a4769dce001e9dbcf15 Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Tue, 19 Jul 2022 09:18:21 -0500 Subject: [PATCH 09/12] support application snapshot input --- cmd/sign_off.go | 80 ++++++++++++++++++--------- cmd/validate_image.go | 56 +------------------ cmd/validate_image_test.go | 3 +- internal/applicationsnapshot/input.go | 61 ++++++++++++++++++++ 4 files changed, 119 insertions(+), 81 deletions(-) create mode 100644 internal/applicationsnapshot/input.go diff --git a/cmd/sign_off.go b/cmd/sign_off.go index 8d6277e09..19868d223 100644 --- a/cmd/sign_off.go +++ b/cmd/sign_off.go @@ -17,12 +17,16 @@ package cmd import ( + "context" "encoding/json" "errors" "fmt" + "log" + appstudioshared "github.com/redhat-appstudio/managed-gitops/appstudio-shared/apis/appstudio.redhat.com/v1alpha1" "github.com/spf13/cobra" + "github.com/hacbs-contract/ec-cli/internal/applicationsnapshot" "github.com/hacbs-contract/ec-cli/internal/image" ) @@ -30,6 +34,9 @@ func signOffCmd() *cobra.Command { var data = struct { imageRef string publicKey string + filePath string + input string + spec *appstudioshared.ApplicationSnapshotSpec }{ imageRef: "", publicKey: "", @@ -40,50 +47,73 @@ func signOffCmd() *cobra.Command { Long: `Supported sign off sources are commits captured from a git repo and jira issues. The git sources return a signed off value and the git commit. The jira issue is a TODO, but will return the Jira issue with any sign off values.`, - RunE: func(cmd *cobra.Command, args []string) error { - imageValidator, err := image.NewImageValidator(cmd.Context(), data.imageRef, data.publicKey, "") - if err != nil { - return err - } - validatedImage, err := imageValidator.ValidateImage(cmd.Context()) + PreRunE: func(cmd *cobra.Command, args []string) error { + spec, err := applicationsnapshot.DetermineInputSpec(data.filePath, data.input, data.imageRef) if err != nil { return err } - for _, att := range validatedImage.Attestations { - signoffSource, err := att.NewSignOffSource() - if err != nil { - return err - } - if signoffSource == nil { - return errors.New("there is no signoff source in attestation") - } + data.spec = spec - signOff, err := signoffSource.GetSignOff() + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + for _, comp := range data.spec.Components { + err := validate(cmd.Context(), comp.ContainerImage, data.publicKey) if err != nil { - return err - } - - if signOff != nil { - payload, err := json.Marshal(signOff) - if err != nil { - return err - } - fmt.Println(string(payload)) + log.Println(err) + continue } } return nil }, } - // attestation download options cmd.Flags().StringVar(&data.publicKey, "public-key", "", "Public key") cmd.Flags().StringVar(&data.imageRef, "image-ref", data.imageRef, "The OCI repo to fetch the attestation from.") + cmd.Flags().StringVarP(&data.filePath, "file-path", "f", data.filePath, "Path to ApplicationSnapshot JSON file") + cmd.Flags().StringVarP(&data.input, "json-input", "j", data.input, "ApplicationSnapshot JSON string") return cmd } +func validate(ctx context.Context, imageRef, publicKey string) error { + imageValidator, err := image.NewImageValidator(ctx, imageRef, publicKey, "") + if err != nil { + return err + } + + validatedImage, err := imageValidator.ValidateImage(ctx) + if err != nil { + return err + } + + for _, att := range validatedImage.Attestations { + signoffSource, err := att.NewSignOffSource() + if err != nil { + return err + } + if signoffSource == nil { + return errors.New("there is no signoff source in attestation") + } + + signOff, err := signoffSource.GetSignOff() + if err != nil { + return err + } + + if signOff != nil { + payload, err := json.Marshal(signOff) + if err != nil { + return err + } + fmt.Println(string(payload)) + } + } + return nil +} + func init() { rootCmd.AddCommand(signOffCmd()) } diff --git a/cmd/validate_image.go b/cmd/validate_image.go index b11aea1b3..3e0d81dd7 100644 --- a/cmd/validate_image.go +++ b/cmd/validate_image.go @@ -18,7 +18,6 @@ package cmd import ( "context" - "encoding/json" "errors" "fmt" "io/ioutil" @@ -26,13 +25,10 @@ import ( "github.com/hashicorp/go-multierror" appstudioshared "github.com/redhat-appstudio/managed-gitops/appstudio-shared/apis/appstudio.redhat.com/v1alpha1" - log "github.com/sirupsen/logrus" - "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/hacbs-contract/ec-cli/internal/applicationsnapshot" "github.com/hacbs-contract/ec-cli/internal/output" - "github.com/hacbs-contract/ec-cli/internal/utils" ) type imageValidationFunc func(ctx context.Context, imageRef, policyConfiguration, publicKey, rekorURL string) (*output.Output, error) @@ -72,7 +68,7 @@ instance in strict mode: ec validate image --file-path my-app.yaml --public-key my-key.pem --rekor-url https://rekor.example.org --strict`, PreRunE: func(cmd *cobra.Command, args []string) error { - s, err := determineInputSpec(data.filePath, data.input, data.imageRef) + s, err := applicationsnapshot.DetermineInputSpec(data.filePath, data.input, data.imageRef) if err != nil { return err } @@ -173,53 +169,3 @@ ec validate image --file-path my-app.yaml --public-key my-key.pem --rekor-url ht } return cmd } - -func determineInputSpec(filePath string, input string, imageRef string) (*appstudioshared.ApplicationSnapshotSpec, error) { - var appSnapshot appstudioshared.ApplicationSnapshotSpec - - // read ApplicationSnapshot provided as a file - if len(filePath) > 0 { - content, err := afero.ReadFile(utils.AppFS, filePath) - if err != nil { - log.Debugf("Problem reading application snapshot from file %s", filePath) - return nil, err - } - - err = json.Unmarshal(content, &appSnapshot) - if err != nil { - log.Debugf("Problem parsing application snapshot from file %s", filePath) - return nil, err - } - - log.Debugf("Read application snapshot from file %s", filePath) - return &appSnapshot, nil - } - - // read ApplicationSnapshot provided as a string - if len(input) > 0 { - // Unmarshall json into struct, exit on failure - if err := json.Unmarshal([]byte(input), &appSnapshot); err != nil { - log.Debugf("Problem parsing application snapshot from input param %s", input) - return nil, err - } - - log.Debug("Read application snapshot from input param") - return &appSnapshot, nil - } - - // create ApplicationSnapshot with a single image - if len(imageRef) > 0 { - log.Debugf("Generating application snapshot from imageRef %s", imageRef) - return &appstudioshared.ApplicationSnapshotSpec{ - Components: []appstudioshared.ApplicationSnapshotComponent{ - { - Name: "Unnamed", - ContainerImage: imageRef, - }, - }, - }, nil - } - - log.Debug("No application snapshot available") - return nil, errors.New("neither ApplicationSnapshot nor image reference provided to validate") -} diff --git a/cmd/validate_image_test.go b/cmd/validate_image_test.go index d604393fd..794536ae9 100644 --- a/cmd/validate_image_test.go +++ b/cmd/validate_image_test.go @@ -26,6 +26,7 @@ import ( appstudioshared "github.com/redhat-appstudio/managed-gitops/appstudio-shared/apis/appstudio.redhat.com/v1alpha1" "github.com/stretchr/testify/assert" + "github.com/hacbs-contract/ec-cli/internal/applicationsnapshot" "github.com/hacbs-contract/ec-cli/internal/output" ) @@ -136,7 +137,7 @@ func Test_determineInputSpec(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - s, err := determineInputSpec(c.arguments.filePath, c.arguments.input, c.arguments.imageRef) + s, err := applicationsnapshot.DetermineInputSpec(c.arguments.filePath, c.arguments.input, c.arguments.imageRef) if c.err != "" { assert.EqualError(t, err, c.err) } diff --git a/internal/applicationsnapshot/input.go b/internal/applicationsnapshot/input.go new file mode 100644 index 000000000..074dd2725 --- /dev/null +++ b/internal/applicationsnapshot/input.go @@ -0,0 +1,61 @@ +package applicationsnapshot + +import ( + "encoding/json" + "errors" + + "github.com/hacbs-contract/ec-cli/internal/utils" + appstudioshared "github.com/redhat-appstudio/managed-gitops/appstudio-shared/apis/appstudio.redhat.com/v1alpha1" + log "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +func DetermineInputSpec(filePath string, input string, imageRef string) (*appstudioshared.ApplicationSnapshotSpec, error) { + var appSnapshot appstudioshared.ApplicationSnapshotSpec + + // read ApplicationSnapshot provided as a file + if len(filePath) > 0 { + content, err := afero.ReadFile(utils.AppFS, filePath) + if err != nil { + log.Debugf("Problem reading application snapshot from file %s", filePath) + return nil, err + } + + err = json.Unmarshal(content, &appSnapshot) + if err != nil { + log.Debugf("Problem parsing application snapshot from file %s", filePath) + return nil, err + } + + log.Debugf("Read application snapshot from file %s", filePath) + return &appSnapshot, nil + } + + // read ApplicationSnapshot provided as a string + if len(input) > 0 { + // Unmarshall json into struct, exit on failure + if err := json.Unmarshal([]byte(input), &appSnapshot); err != nil { + log.Debugf("Problem parsing application snapshot from input param %s", input) + return nil, err + } + + log.Debug("Read application snapshot from input param") + return &appSnapshot, nil + } + + // create ApplicationSnapshot with a single image + if len(imageRef) > 0 { + log.Debugf("Generating application snapshot from imageRef %s", imageRef) + return &appstudioshared.ApplicationSnapshotSpec{ + Components: []appstudioshared.ApplicationSnapshotComponent{ + { + Name: "Unnamed", + ContainerImage: imageRef, + }, + }, + }, nil + } + + log.Debug("No application snapshot available") + return nil, errors.New("neither ApplicationSnapshot nor image reference provided to validate") +} From 97fba5dd030f6f307129be54f7f74a96e579524c Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Tue, 19 Jul 2022 14:47:53 -0500 Subject: [PATCH 10/12] validate commit with image --- internal/image/image.go | 4 ++-- internal/image/validate.go | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/internal/image/image.go b/internal/image/image.go index 8006307a8..d808975c9 100644 --- a/internal/image/image.go +++ b/internal/image/image.go @@ -99,7 +99,7 @@ func (i *imageValidator) ValidateImage(ctx context.Context) (*validatedImage, er attestations, _, err := cosign.VerifyImageAttestations(ctx, i.reference, &i.checkOpts) attStatements := make([]attestation, 0, len(attestations)) for _, att := range attestations { - attStatement, err := signatureToAttestation(ctx, att) + attStatement, err := SignatureToAttestation(ctx, att) if err != nil { return nil, err } @@ -118,7 +118,7 @@ func (i *imageValidator) ValidateImage(ctx context.Context) (*validatedImage, er } -func signatureToAttestation(ctx context.Context, signature oci.Signature) (attestation, error) { +func SignatureToAttestation(ctx context.Context, signature oci.Signature) (attestation, error) { var att attestation payload, err := policy.AttestationToPayloadJSON(ctx, "slsaprovenance", signature) if err != nil { diff --git a/internal/image/validate.go b/internal/image/validate.go index 8874312d8..32188eea5 100644 --- a/internal/image/validate.go +++ b/internal/image/validate.go @@ -18,6 +18,9 @@ package image import ( "context" + "encoding/json" + "errors" + "io/ioutil" conftestOutput "github.com/open-policy-agent/conftest/output" log "github.com/sirupsen/logrus" @@ -73,6 +76,20 @@ func ValidateImage(ctx context.Context, imageRef, policyConfiguration, publicKey return nil, err } + attStatements := make([]attestation, 0, len(a.Attestations())) + for _, att := range a.Attestations() { + attStatement, err := SignatureToAttestation(ctx, att) + if err != nil { + return nil, err + } + attStatements = append(attStatements, attStatement) + } + + commitFile := "/tmp/commit.json" + writeCommitData(attStatements, commitFile) + + inputs = append(inputs, commitFile) + results, err := a.Evaluator.Evaluate(ctx, inputs) if err != nil { @@ -85,3 +102,33 @@ func ValidateImage(ctx context.Context, imageRef, policyConfiguration, publicKey return out, nil } + +func writeCommitData(attestations []attestation, commitFile string) error { + payloads := make([]string, 0, len(attestations)) + for _, att := range attestations { + signoffSource, err := att.NewSignOffSource() + if err != nil { + return err + } + if signoffSource == nil { + return errors.New("there is no signoff source in attestation") + } + + signOff, err := signoffSource.GetSignOff() + if err != nil { + return err + } + + if signOff != nil { + payload, err := json.MarshalIndent(signOff, "", " ") + if err != nil { + return err + } + payloads = append(payloads, string(payload)) + } + } + + payloadJson, _ := json.Marshal(payloads) + _ = ioutil.WriteFile(commitFile, payloadJson, 0644) + return nil +} From e3bb8314297ae564d396072fa9d1d3fed052fd50 Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Tue, 19 Jul 2022 17:25:00 -0500 Subject: [PATCH 11/12] use struct instead of bytes --- internal/image/validate.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/image/validate.go b/internal/image/validate.go index 32188eea5..3c17c1c11 100644 --- a/internal/image/validate.go +++ b/internal/image/validate.go @@ -104,7 +104,7 @@ func ValidateImage(ctx context.Context, imageRef, policyConfiguration, publicKey } func writeCommitData(attestations []attestation, commitFile string) error { - payloads := make([]string, 0, len(attestations)) + payloads := make([]*signOffSignature, 0, len(attestations)) for _, att := range attestations { signoffSource, err := att.NewSignOffSource() if err != nil { @@ -120,11 +120,7 @@ func writeCommitData(attestations []attestation, commitFile string) error { } if signOff != nil { - payload, err := json.MarshalIndent(signOff, "", " ") - if err != nil { - return err - } - payloads = append(payloads, string(payload)) + payloads = append(payloads, signOff) } } From c367206736242f41e6145688ab768e4a8159836f Mon Sep 17 00:00:00 2001 From: Joe Stuart Date: Tue, 19 Jul 2022 20:45:49 -0500 Subject: [PATCH 12/12] updates --- internal/image/build.go | 4 +++- internal/image/validate.go | 27 +++++++++++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/internal/image/build.go b/internal/image/build.go index fffcdbb9c..33c84c5a8 100644 --- a/internal/image/build.go +++ b/internal/image/build.go @@ -96,14 +96,16 @@ func (a *attestation) NewSignOffSource() (signOffSource, error) { // get the last commit used for the component build func (a *attestation) getBuildCommitSha() string { + //return "6c1f093c0c197add71579d392da8a79a984fcd62" if len(a.Predicate.Materials) == 1 { - return a.Predicate.Materials[0].Digest["sha"] + return a.Predicate.Materials[0].Digest["sha1"] } return "" } // the git url used for the component build func (a *attestation) getBuildSCM() string { + //return "https://github.com/joejstuart/ec-cli.git" if len(a.Predicate.Materials) == 1 { return a.Predicate.Materials[0].Uri } diff --git a/internal/image/validate.go b/internal/image/validate.go index 3c17c1c11..fdda09207 100644 --- a/internal/image/validate.go +++ b/internal/image/validate.go @@ -21,6 +21,8 @@ import ( "encoding/json" "errors" "io/ioutil" + "os" + "path" conftestOutput "github.com/open-policy-agent/conftest/output" log "github.com/sirupsen/logrus" @@ -85,10 +87,17 @@ func ValidateImage(ctx context.Context, imageRef, policyConfiguration, publicKey attStatements = append(attStatements, attStatement) } - commitFile := "/tmp/commit.json" - writeCommitData(attStatements, commitFile) + inputDir, err := os.MkdirTemp("", "ec_input.*") + if err != nil { + log.Debug("Problem making temp dir!") + return nil, err + } + inputJSONPath := path.Join(inputDir, "commit.json") + signatures, err := writeCommitData(attStatements) + payloadJson, _ := json.Marshal(signatures) + _ = ioutil.WriteFile(inputJSONPath, payloadJson, 0644) - inputs = append(inputs, commitFile) + inputs = append(inputs, inputJSONPath) results, err := a.Evaluator.Evaluate(ctx, inputs) @@ -103,28 +112,26 @@ func ValidateImage(ctx context.Context, imageRef, policyConfiguration, publicKey return out, nil } -func writeCommitData(attestations []attestation, commitFile string) error { +func writeCommitData(attestations []attestation) ([]*signOffSignature, error) { payloads := make([]*signOffSignature, 0, len(attestations)) for _, att := range attestations { signoffSource, err := att.NewSignOffSource() if err != nil { - return err + return nil, err } if signoffSource == nil { - return errors.New("there is no signoff source in attestation") + return nil, errors.New("there is no signoff source in attestation") } signOff, err := signoffSource.GetSignOff() if err != nil { - return err + return nil, err } if signOff != nil { payloads = append(payloads, signOff) } } + return payloads, nil - payloadJson, _ := json.Marshal(payloads) - _ = ioutil.WriteFile(commitFile, payloadJson, 0644) - return nil }