diff --git a/.goreleaser.yml b/.goreleaser.yml index 27ed88d..599fbaf 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -22,7 +22,7 @@ builds: ldflags: - "-s" - "-w" - - "-X github.com/snyk/parlay/internal/commands.version={{.Version}}" + - "-X github.com/snyk/parlay/internal/utils.version={{.Version}}" archives: - format: tar.gz diff --git a/internal/commands/default.go b/internal/commands/default.go index 334678d..bf8bbf9 100644 --- a/internal/commands/default.go +++ b/internal/commands/default.go @@ -11,11 +11,7 @@ import ( "github.com/snyk/parlay/internal/commands/ecosystems" "github.com/snyk/parlay/internal/commands/scorecard" "github.com/snyk/parlay/internal/commands/snyk" -) - -// These values are set at build time -var ( - version = "" + "github.com/snyk/parlay/internal/utils" ) func NewDefaultCommand() *cobra.Command { @@ -26,7 +22,7 @@ func NewDefaultCommand() *cobra.Command { Use: "parlay", Short: "Enrich an SBOM with context from third party services", SilenceUsage: true, - Version: GetVersion(), + Version: utils.GetVersion(), DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { if err := cmd.Help(); err != nil { @@ -55,7 +51,3 @@ func NewDefaultCommand() *cobra.Command { return &cmd } - -func GetVersion() string { - return version -} diff --git a/internal/commands/ecosystems/enrich.go b/internal/commands/ecosystems/enrich.go index 3b97adc..09acf8d 100644 --- a/internal/commands/ecosystems/enrich.go +++ b/internal/commands/ecosystems/enrich.go @@ -12,10 +12,13 @@ import ( ) func NewEnrichCommand(logger *zerolog.Logger) *cobra.Command { + var omitWatermark bool + cmd := cobra.Command{ - Use: "enrich ", - Short: "Enrich an SBOM with ecosyste.ms data", - Args: cobra.ExactArgs(1), + Use: "enrich ", + Short: "Enrich an SBOM with ecosyste.ms data", + Args: cobra.ExactArgs(1), + Version: utils.GetVersion(), Run: func(cmd *cobra.Command, args []string) { b, err := utils.GetUserInput(args[0], os.Stdin) if err != nil { @@ -29,10 +32,17 @@ func NewEnrichCommand(logger *zerolog.Logger) *cobra.Command { ecosystems.EnrichSBOM(doc, logger) + if !omitWatermark { + sbom.AddParlayWatermark(doc, "parlay", cmd.Version) + } + if err := doc.Encode(os.Stdout); err != nil { logger.Fatal().Err(err).Msg("Failed to encode new SBOM") } }, } + + cmd.Flags().BoolVar(&omitWatermark, "omit-watermark", false, "omit parlay watermark") + return &cmd } diff --git a/internal/commands/scorecard/enrich.go b/internal/commands/scorecard/enrich.go index 945e98f..7f047b4 100644 --- a/internal/commands/scorecard/enrich.go +++ b/internal/commands/scorecard/enrich.go @@ -12,10 +12,13 @@ import ( ) func NewEnrichCommand(logger *zerolog.Logger) *cobra.Command { + var omitWatermark bool + cmd := cobra.Command{ - Use: "enrich ", - Short: "Enrich an SBOM with OpenSSF Scorecard data", - Args: cobra.ExactArgs(1), + Use: "enrich ", + Short: "Enrich an SBOM with OpenSSF Scorecard data", + Args: cobra.ExactArgs(1), + Version: utils.GetVersion(), Run: func(cmd *cobra.Command, args []string) { b, err := utils.GetUserInput(args[0], os.Stdin) if err != nil { @@ -29,10 +32,17 @@ func NewEnrichCommand(logger *zerolog.Logger) *cobra.Command { scorecard.EnrichSBOM(doc) + if !omitWatermark { + sbom.AddParlayWatermark(doc, "parlay", cmd.Version) + } + if err := doc.Encode(os.Stdout); err != nil { logger.Fatal().Err(err).Msg("Failed to encode new SBOM") } }, } + + cmd.Flags().BoolVar(&omitWatermark, "omit-watermark", false, "omit parlay watermark") + return &cmd } diff --git a/internal/commands/snyk/enrich.go b/internal/commands/snyk/enrich.go index dc780c9..e2b44c9 100644 --- a/internal/commands/snyk/enrich.go +++ b/internal/commands/snyk/enrich.go @@ -12,10 +12,13 @@ import ( ) func NewEnrichCommand(logger *zerolog.Logger) *cobra.Command { + var omitWatermark bool + cmd := cobra.Command{ - Use: "enrich ", - Short: "Enrich an SBOM with Snyk data", - Args: cobra.ExactArgs(1), + Use: "enrich ", + Short: "Enrich an SBOM with Snyk data", + Args: cobra.ExactArgs(1), + Version: utils.GetVersion(), Run: func(cmd *cobra.Command, args []string) { cfg := config() svc := snyk.NewService(cfg, logger) @@ -32,10 +35,17 @@ func NewEnrichCommand(logger *zerolog.Logger) *cobra.Command { svc.EnrichSBOM(doc) + if !omitWatermark { + sbom.AddParlayWatermark(doc, "parlay", cmd.Version) + } + if err := doc.Encode(os.Stdout); err != nil { logger.Fatal().Err(err).Msg("Failed to encode new SBOM") } }, } + + cmd.Flags().BoolVar(&omitWatermark, "omit-watermark", false, "omit parlay watermark") + return &cmd } diff --git a/internal/utils/version.go b/internal/utils/version.go new file mode 100644 index 0000000..5f5661e --- /dev/null +++ b/internal/utils/version.go @@ -0,0 +1,8 @@ +package utils + +// This value gets set at build time +var version string + +func GetVersion() string { + return version +} diff --git a/lib/sbom/cyclonedx.go b/lib/sbom/cyclonedx.go index 05bb8bc..b9014ea 100644 --- a/lib/sbom/cyclonedx.go +++ b/lib/sbom/cyclonedx.go @@ -53,3 +53,35 @@ func encodeCycloneDX(bom *cdx.BOM, f cdx.BOMFileFormat) encoderFn { return cdx.NewBOMEncoder(w, f).Encode(bom) } } + +func addCDXTool(bom *cdx.BOM, name, version string) { + if bom.Metadata == nil { + bom.Metadata = &cdx.Metadata{} + } + + if bom.Metadata.Tools == nil { + bom.Metadata.Tools = &cdx.ToolsChoice{} + } + + // Handle possibly existing, legacy "Tools" entry + if bom.Metadata.Tools.Tools != nil { + *bom.Metadata.Tools.Tools = append(*bom.Metadata.Tools.Tools, cdx.Tool{ + Vendor: "Snyk", + Name: name, + }) + return + } + + if bom.Metadata.Tools.Components == nil { + bom.Metadata.Tools.Components = &[]cdx.Component{} + } + + *bom.Metadata.Tools.Components = append( + *bom.Metadata.Tools.Components, + cdx.Component{ + Type: cdx.ComponentTypeApplication, + Name: name, + Version: version, + Publisher: "Snyk", + PackageURL: "pkg:github/snyk/parlay"}) +} diff --git a/lib/sbom/spdx.go b/lib/sbom/spdx.go index 9a5f3d1..3515362 100644 --- a/lib/sbom/spdx.go +++ b/lib/sbom/spdx.go @@ -33,3 +33,13 @@ func encodeSPDX2_3JSON(bom *spdx.Document) encoderFn { return spdx_json.Write(bom, w) } } + +func addSPDXTool(bom *spdx.Document, name, _ string) { + if bom.CreationInfo == nil { + bom.CreationInfo = &spdx.CreationInfo{} + } + + bom.CreationInfo.Creators = append( + bom.CreationInfo.Creators, + spdx.Creator{Creator: name, CreatorType: "Tool"}) +} diff --git a/lib/sbom/tool.go b/lib/sbom/tool.go new file mode 100644 index 0000000..723fe31 --- /dev/null +++ b/lib/sbom/tool.go @@ -0,0 +1,31 @@ +/* + * © 2024 Snyk Limited All rights reserved. + * + * 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. + */ + +package sbom + +import ( + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/spdx/tools-golang/spdx" +) + +func AddParlayWatermark(doc *SBOMDocument, name, version string) { + switch bom := doc.BOM.(type) { + case *cdx.BOM: + addCDXTool(bom, name, version) + case *spdx.Document: + addSPDXTool(bom, name, version) + } +} diff --git a/lib/sbom/tool_test.go b/lib/sbom/tool_test.go new file mode 100644 index 0000000..e2fe3ce --- /dev/null +++ b/lib/sbom/tool_test.go @@ -0,0 +1,62 @@ +/* + * © 2024 Snyk Limited All rights reserved. + * + * 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. + */ + +package sbom + +import ( + "testing" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/spdx/tools-golang/spdx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddParlayWatermark_CycloneDX(t *testing.T) { + doc, err := DecodeSBOMDocument([]byte(`{ + "bomFormat": "CycloneDX", + "specVersion": "1.6" + }`)) + require.NoError(t, err) + + AddParlayWatermark(doc, "parlay", "0.0.0") + + bom, ok := doc.BOM.(*cdx.BOM) + require.True(t, ok) + + require.NotNil(t, bom.Metadata.Tools.Components) + require.Len(t, *bom.Metadata.Tools.Components, 1) + tool := (*bom.Metadata.Tools.Components)[0] + assert.Equal(t, "application", string(tool.Type)) + assert.Equal(t, "parlay", tool.Name) + assert.Equal(t, "0.0.0", tool.Version) + assert.Equal(t, "Snyk", tool.Publisher) +} + +func TestAddParlayWatermark_SPDX(t *testing.T) { + doc, err := DecodeSBOMDocument([]byte(`{ + "spdxVersion": "SPDX-2.3", + "SPDXID": "SPDXRef-DOCUMENT" + }`)) + require.NoError(t, err) + + AddParlayWatermark(doc, "parlay", "0.0.0") + + bom, ok := doc.BOM.(*spdx.Document) + require.True(t, ok) + + assert.Contains(t, bom.CreationInfo.Creators, spdx.Creator{Creator: "parlay", CreatorType: "Tool"}) +}