diff --git a/.gitignore b/.gitignore index 1317860b0..f164ae6f6 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,4 @@ AUnitResults.html cmd/checkmarx/piper_checkmarx_report.json cmd/fortify/piper_fortify_report.html cmd/fortify/piper_fortify_report.json -cmd/toolruns/toolrun_malwarescan_20220519143229.json -cmd/toolruns/toolrun_protecode_20220519143230.json +cmd/toolruns diff --git a/cmd/golangBuild.go b/cmd/golangBuild.go index d1414d324..087f892e4 100644 --- a/cmd/golangBuild.go +++ b/cmd/golangBuild.go @@ -9,7 +9,6 @@ import ( "path/filepath" "regexp" "strings" - "text/template" "github.com/SAP/jenkins-library/pkg/buildsettings" "github.com/SAP/jenkins-library/pkg/certutils" @@ -174,11 +173,11 @@ func runGolangBuild(config *golangBuildOptions, telemetryData *telemetry.CustomD ldflags := "" if len(config.LdflagsTemplate) > 0 { - var err error - ldflags, err = prepareLdflags(config, utils, GeneralConfig.EnvRootPath) + ldf, err := prepareLdflags(config, utils, GeneralConfig.EnvRootPath) if err != nil { return err } + ldflags = (*ldf).String() log.Entry().Infof("ldflags from template: '%v'", ldflags) } @@ -405,7 +404,7 @@ func reportGolangTestCoverage(config *golangBuildOptions, utils golangBuildUtils return nil } -func prepareLdflags(config *golangBuildOptions, utils golangBuildUtils, envRootPath string) (string, error) { +func prepareLdflags(config *golangBuildOptions, utils golangBuildUtils, envRootPath string) (*bytes.Buffer, error) { cpe := piperenv.CPEMap{} err := cpe.LoadFromDisk(path.Join(envRootPath, "commonPipelineEnvironment")) if err != nil { @@ -413,23 +412,7 @@ func prepareLdflags(config *golangBuildOptions, utils golangBuildUtils, envRootP } log.Entry().Debugf("ldflagsTemplate in use: %v", config.LdflagsTemplate) - tmpl, err := template.New("ldflags").Parse(config.LdflagsTemplate) - if err != nil { - return "", fmt.Errorf("failed to parse ldflagsTemplate '%v': %w", config.LdflagsTemplate, err) - } - - ldflagsParams := struct { - CPE map[string]interface{} - }{ - CPE: map[string]interface{}(cpe), - } - var generatedLdflags bytes.Buffer - err = tmpl.Execute(&generatedLdflags, ldflagsParams) - if err != nil { - return "", fmt.Errorf("failed to execute ldflagsTemplate '%v': %w", config.LdflagsTemplate, err) - } - - return generatedLdflags.String(), nil + return cpe.ParseTemplate(config.LdflagsTemplate) } func runGolangBuildPerArchitecture(config *golangBuildOptions, goModFile *modfile.File, utils golangBuildUtils, ldflags string, architecture multiarch.Platform) ([]string, error) { diff --git a/cmd/golangBuild_test.go b/cmd/golangBuild_test.go index 57d6295e9..987af26bd 100644 --- a/cmd/golangBuild_test.go +++ b/cmd/golangBuild_test.go @@ -310,7 +310,7 @@ go 1.17` telemetryData := telemetry.CustomData{} err := runGolangBuild(&config, &telemetryData, utils, &cpe) - assert.Contains(t, fmt.Sprint(err), "failed to parse ldflagsTemplate") + assert.Contains(t, fmt.Sprint(err), "failed to parse cpe template") }) t.Run("failure - build failure", func(t *testing.T) { @@ -626,14 +626,14 @@ func TestPrepareLdflags(t *testing.T) { utils := newGolangBuildTestsUtils() result, err := prepareLdflags(&config, utils, dir) assert.NoError(t, err) - assert.Equal(t, "-X version=1.2.3", result) + assert.Equal(t, "-X version=1.2.3", (*result).String()) }) t.Run("error - template parsing", func(t *testing.T) { config := golangBuildOptions{LdflagsTemplate: "-X version={{ .CPE.artifactVersion "} utils := newGolangBuildTestsUtils() _, err := prepareLdflags(&config, utils, dir) - assert.Contains(t, fmt.Sprint(err), "failed to parse ldflagsTemplate") + assert.Contains(t, fmt.Sprint(err), "failed to parse cpe template") }) } diff --git a/pkg/piperenv/environment.go b/pkg/piperenv/environment.go index b9a1709cb..fd237b908 100644 --- a/pkg/piperenv/environment.go +++ b/pkg/piperenv/environment.go @@ -2,6 +2,7 @@ package piperenv import ( "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -56,7 +57,10 @@ func writeToDisk(filename string, data []byte) error { if _, err := os.Stat(filepath.Dir(filename)); os.IsNotExist(err) { log.Entry().Debugf("Creating directory: %v", filepath.Dir(filename)) - os.MkdirAll(filepath.Dir(filename), 0777) + cErr := os.MkdirAll(filepath.Dir(filename), 0777) + if cErr != nil { + return fmt.Errorf("failed to create directory %v, %w", filepath.Dir(filename), cErr) + } } //ToDo: make sure to not overwrite file but rather add another file? Create error if already existing? diff --git a/pkg/piperenv/templating.go b/pkg/piperenv/templating.go new file mode 100644 index 000000000..9bfa6d9f0 --- /dev/null +++ b/pkg/piperenv/templating.go @@ -0,0 +1,87 @@ +package piperenv + +import ( + "bytes" + "fmt" + "strings" + "text/template" +) + +// ParseTemplate allows to parse a template which contains references to the CPE +// Utility functions make it simple to access specific parts of the CPE +func (c *CPEMap) ParseTemplate(cpeTemplate string) (*bytes.Buffer, error) { + funcMap := template.FuncMap{ + "cpe": c.cpe, + "cpecustom": c.custom, + "git": c.git, + "imageDigest": c.imageDigest, + "imageTag": c.imageTag, + + // ToDo: add template function for artifacts + // This requires alignment on artifact handling before, though + } + + tmpl, err := template.New("cpetemplate").Funcs(funcMap).Parse(cpeTemplate) + if err != nil { + return nil, fmt.Errorf("failed to parse cpe template '%v': %w", cpeTemplate, err) + } + + tmplParams := struct { + CPE map[string]interface{} + }{ + CPE: map[string]interface{}(*c), + } + + var generated bytes.Buffer + err = tmpl.Execute(&generated, tmplParams) + if err != nil { + return nil, fmt.Errorf("failed to execute cpe template '%v': %w", cpeTemplate, err) + } + + return &generated, nil +} + +func (c *CPEMap) cpe(element string) string { + // ToDo: perform validity checks to allow only selected fields for now? + // This would allow a stable contract and could perform conversions in case a contract changes. + + return fmt.Sprint(map[string]interface{}(*c)[element]) +} + +func (c *CPEMap) custom(element string) string { + return fmt.Sprint(map[string]interface{}(*c)[fmt.Sprintf("custom/%v", element)]) +} + +func (c *CPEMap) git(element string) string { + var el string + if element == "organization" || element == "repository" { + el = fmt.Sprint(map[string]interface{}(*c)[fmt.Sprintf("github/%v", element)]) + } else { + el = fmt.Sprint(map[string]interface{}(*c)[fmt.Sprintf("git/%v", element)]) + } + return el +} + +func (c *CPEMap) imageDigest(imageName string) string { + digests, _ := map[string]interface{}(*c)["container/imageDigests"].([]interface{}) + imageNames, _ := map[string]interface{}(*c)["container/imageNames"].([]interface{}) + if len(digests) > 0 && len(digests) == len(imageNames) { + for i, image := range imageNames { + if fmt.Sprint(image) == imageName { + return fmt.Sprint(digests[i]) + } + } + } + return "" +} + +func (c *CPEMap) imageTag(imageName string) string { + nameTags, _ := map[string]interface{}(*c)["container/imageNameTags"].([]interface{}) + for _, nameTag := range nameTags { + nt := strings.Split(fmt.Sprint(nameTag), ":") + if nt[0] == imageName { + return nt[1] + } + } + return "" +} diff --git a/pkg/piperenv/templating_test.go b/pkg/piperenv/templating_test.go new file mode 100644 index 000000000..15733fc0f --- /dev/null +++ b/pkg/piperenv/templating_test.go @@ -0,0 +1,219 @@ +package piperenv + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseTemplate(t *testing.T) { + tt := []struct { + template string + cpe CPEMap + expected string + expectedError error + }{ + {template: `version: {{index .CPE "artifactVersion"}}, sha: {{git "commitId"}}`, expected: "version: 1.2.3, sha: thisIsMyTestSha"}, + {template: "version: {{", expectedError: fmt.Errorf("failed to parse cpe template 'version: {{'")}, + } + + cpe := CPEMap{ + "artifactVersion": "1.2.3", + "git/commitId": "thisIsMyTestSha", + } + + for _, test := range tt { + res, err := cpe.ParseTemplate(test.template) + if test.expectedError != nil { + assert.Contains(t, fmt.Sprint(err), fmt.Sprint(test.expectedError)) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expected, (*res).String()) + } + + } +} + +func TestTemplateFunctionCpe(t *testing.T) { + t.Run("CPE from object", func(t *testing.T) { + tt := []struct { + element string + expected string + }{ + {element: "artifactVersion", expected: "1.2.3"}, + {element: "git/commitId", expected: "thisIsMyTestSha"}, + } + + cpe := CPEMap{ + "artifactVersion": "1.2.3", + "git/commitId": "thisIsMyTestSha", + } + + for _, test := range tt { + assert.Equal(t, test.expected, cpe.cpe(test.element)) + } + }) + + t.Run("CPE from files", func(t *testing.T) { + theVersion := "1.2.3" + dir := t.TempDir() + assert.NoError(t, ioutil.WriteFile(filepath.Join(dir, "artifactVersion"), []byte(theVersion), 0o666)) + cpe := CPEMap{} + assert.NoError(t, cpe.LoadFromDisk(dir)) + + res, err := cpe.ParseTemplate(`{{cpe "artifactVersion"}}`) + assert.NoError(t, err) + assert.Equal(t, theVersion, (*res).String()) + }) +} + +func TestTemplateFunctionCustom(t *testing.T) { + tt := []struct { + element string + expected string + }{ + {element: "repositoryUrl", expected: "https://this.is.the.repo.url"}, + {element: "repositoryId", expected: "repoTestId"}, + } + + cpe := CPEMap{ + "custom/repositoryUrl": "https://this.is.the.repo.url", + "custom/repositoryId": "repoTestId", + } + + for _, test := range tt { + assert.Equal(t, test.expected, cpe.custom(test.element)) + } +} + +func TestTemplateFunctionGit(t *testing.T) { + tt := []struct { + element string + expected string + }{ + {element: "commitId", expected: "thisIsMyTestSha"}, + {element: "repository", expected: "testRepo"}, + } + + cpe := CPEMap{ + "git/commitId": "thisIsMyTestSha", + "github/repository": "testRepo", + } + + for _, test := range tt { + assert.Equal(t, test.expected, cpe.git(test.element)) + } +} + +func TestTemplateFunctionImageDigest(t *testing.T) { + t.Run("CPE from object", func(t *testing.T) { + tt := []struct { + imageName string + cpe CPEMap + expected string + }{ + { + imageName: "image1", + cpe: CPEMap{}, + expected: "", + }, + { + imageName: "image2", + cpe: CPEMap{ + "container/imageDigests": []interface{}{"digest1", "digest2", "digest3"}, + "container/imageNames": []interface{}{"image1", "image2", "image3"}, + }, + expected: "digest2", + }, + { + imageName: "image4", + cpe: CPEMap{ + "container/imageDigests": []interface{}{"digest1", "digest2", "digest3"}, + "container/imageNames": []interface{}{"image1", "image2", "image3"}, + }, + expected: "", + }, + { + imageName: "image1", + cpe: CPEMap{ + "container/imageDigests": []interface{}{"digest1", "digest3"}, + "container/imageNames": []interface{}{"image1", "image2", "image3"}, + }, + expected: "", + }, + } + + for _, test := range tt { + assert.Equal(t, test.expected, test.cpe.imageDigest(test.imageName)) + } + }) + + t.Run("CPE from files", func(t *testing.T) { + dir := t.TempDir() + + imageDigests := []string{"digest1", "digest2", "digest3"} + imageNames := []string{"image1", "image2", "image3"} + cpeOut := CPEMap{"container/imageDigests": imageDigests, "container/imageNames": imageNames} + assert.NoError(t, cpeOut.WriteToDisk(dir)) + + cpe := CPEMap{} + assert.NoError(t, cpe.LoadFromDisk(dir)) + + res, err := cpe.ParseTemplate(`{{imageDigest "image2"}}`) + assert.NoError(t, err) + assert.Equal(t, "digest2", (*res).String()) + }) +} + +func TestTemplateFunctionImageTag(t *testing.T) { + t.Run("CPE from object", func(t *testing.T) { + tt := []struct { + imageName string + cpe CPEMap + expected string + }{ + { + imageName: "image1", + cpe: CPEMap{}, + expected: "", + }, + { + imageName: "image2", + cpe: CPEMap{ + "container/imageNameTags": []interface{}{"image1:tag1", "image2:tag2", "image3:tag3"}, + }, + expected: "tag2", + }, + { + imageName: "image4", + cpe: CPEMap{ + "container/imageNameTags": []interface{}{"image1:tag1", "image2:tag2", "image3:tag3"}, + }, + expected: "", + }, + } + + for _, test := range tt { + assert.Equal(t, test.expected, test.cpe.imageTag(test.imageName)) + } + }) + + t.Run("CPE from files", func(t *testing.T) { + dir := t.TempDir() + + imageNameTags := []string{"image1:tag1", "image2:tag2", "image3:tag3"} + imageNames := []string{"image1", "image2", "image3"} + cpeOut := CPEMap{"container/imageNameTags": imageNameTags, "container/imageNames": imageNames} + assert.NoError(t, cpeOut.WriteToDisk(dir)) + + cpe := CPEMap{} + assert.NoError(t, cpe.LoadFromDisk(dir)) + + res, err := cpe.ParseTemplate(`{{imageTag "image2"}}`) + assert.NoError(t, err) + assert.Equal(t, "tag2", (*res).String()) + }) +}