diff --git a/go.mod b/go.mod index ca9edd7..e337a30 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,13 @@ require ( github.com/edoardottt/depsdev v0.0.3 github.com/google/uuid v1.3.0 github.com/jarcoal/httpmock v1.3.0 - github.com/package-url/packageurl-go v0.1.2-0.20230717211154-3587d8c2829e + github.com/package-url/packageurl-go v0.1.2 github.com/remeh/sizedwaitgroup v1.0.0 github.com/rs/zerolog v1.29.1 - github.com/spdx/tools-golang v0.5.2 + github.com/spdx/tools-golang v0.5.4-0.20240304222056-8baafa1a79c4 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 ) require ( diff --git a/go.sum b/go.sum index 2c7bfc7..e9758be 100644 --- a/go.sum +++ b/go.sum @@ -117,6 +117,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -168,8 +169,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/package-url/packageurl-go v0.1.2-0.20230717211154-3587d8c2829e h1:h/TWC+mVfoco4qhPEsaxkdwymlTaDe/BGnzljU8SIPw= -github.com/package-url/packageurl-go v0.1.2-0.20230717211154-3587d8c2829e/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= +github.com/package-url/packageurl-go v0.1.2 h1:0H2DQt6DHd/NeRlVwW4EZ4oEI6Bn40XlNPRqegcxuo4= +github.com/package-url/packageurl-go v0.1.2/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -188,6 +189,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= github.com/spdx/tools-golang v0.5.2 h1:dtMNjJreWPe37584ajk7m/rQtfJaLpRMk7pUGgvekOg= github.com/spdx/tools-golang v0.5.2/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI= +github.com/spdx/tools-golang v0.5.4-0.20240304222056-8baafa1a79c4 h1:h1iNkxAggQH5lpDxHslTTB3Y61XN2G/rjA/n/TAIwFg= +github.com/spdx/tools-golang v0.5.4-0.20240304222056-8baafa1a79c4/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -204,6 +207,7 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -214,6 +218,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= @@ -533,3 +539,4 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/lib/scorecard/enrich_spdx.go b/lib/scorecard/enrich_spdx.go index caeb8b5..c1fb51d 100644 --- a/lib/scorecard/enrich_spdx.go +++ b/lib/scorecard/enrich_spdx.go @@ -55,7 +55,7 @@ func enrichSPDX(bom *spdx.Document) { } pkg.PackageExternalReferences = append(pkg.PackageExternalReferences, &spdx_2_3.PackageExternalReference{ - Category: "OTHER", + Category: spdx.CategoryOther, RefType: "openssfscorecard", Locator: scURL, }) diff --git a/lib/scorecard/enrich_test.go b/lib/scorecard/enrich_test.go index 260660d..e8ae754 100644 --- a/lib/scorecard/enrich_test.go +++ b/lib/scorecard/enrich_test.go @@ -172,7 +172,7 @@ func TestEnrichSBOM_SPDX(t *testing.T) { { PackageExternalReferences: []*spdx_2_3.PackageExternalReference{ { - Category: "OTHER", + Category: spdx.CategoryOther, RefType: "purl", Locator: "pkg:golang/snyk/parlay", }, @@ -191,7 +191,7 @@ func TestEnrichSBOM_SPDX(t *testing.T) { scRef := pkg.PackageExternalReferences[1] assert.Equal(t, scorecardURL, scRef.Locator) assert.Equal(t, "openssfscorecard", scRef.RefType) - assert.Equal(t, "OTHER", scRef.Category) + assert.Equal(t, spdx.CategoryOther, scRef.Category) } func setupEcosystemsAPIMock(t *testing.T) func() { diff --git a/lib/snyk/enrich_cyclonedx.go b/lib/snyk/enrich_cyclonedx.go index 3298c2f..ab6c02e 100644 --- a/lib/snyk/enrich_cyclonedx.go +++ b/lib/snyk/enrich_cyclonedx.go @@ -31,6 +31,45 @@ import ( "github.com/snyk/parlay/snyk/issues" ) +type cdxEnricher = func(*cdx.Component, *packageurl.PackageURL) + +var cdxEnrichers = []cdxEnricher{ + enrichCDXSnykAdvisorData, + enrichCDXSnykVulnerabilityDBData, +} + +func enrichCDXSnykVulnerabilityDBData(component *cdx.Component, purl *packageurl.PackageURL) { + url := SnykVulnURL(purl) + if url != "" { + ext := cdx.ExternalReference{ + URL: url, + Comment: "Snyk Vulnerability DB", + Type: "Other", + } + if component.ExternalReferences == nil { + component.ExternalReferences = &[]cdx.ExternalReference{ext} + } else { + *component.ExternalReferences = append(*component.ExternalReferences, ext) + } + } +} + +func enrichCDXSnykAdvisorData(component *cdx.Component, purl *packageurl.PackageURL) { + url := SnykAdvisorURL(purl) + if url != "" { + ext := cdx.ExternalReference{ + URL: url, + Comment: "Snyk Advisor", + Type: "Other", + } + if component.ExternalReferences == nil { + component.ExternalReferences = &[]cdx.ExternalReference{ext} + } else { + *component.ExternalReferences = append(*component.ExternalReferences, ext) + } + } +} + func enrichCycloneDX(bom *cdx.BOM, logger *zerolog.Logger) *cdx.BOM { auth, err := AuthFromToken(APIToken()) if err != nil { @@ -65,7 +104,9 @@ func enrichCycloneDX(bom *cdx.BOM, logger *zerolog.Logger) *cdx.BOM { Msg("Could not identify package") return } - + for _, enrichFunc := range cdxEnrichers { + enrichFunc(component, &purl) + } resp, err := GetPackageVulnerabilities(&purl, auth, orgID) if err != nil { l.Err(err). diff --git a/lib/snyk/enrich_spdx.go b/lib/snyk/enrich_spdx.go index 39eecd3..47d694e 100644 --- a/lib/snyk/enrich_spdx.go +++ b/lib/snyk/enrich_spdx.go @@ -22,6 +22,7 @@ import ( "net/url" "sync" + "github.com/package-url/packageurl-go" "github.com/remeh/sizedwaitgroup" "github.com/rs/zerolog" "github.com/spdx/tools-golang/spdx" @@ -35,6 +36,47 @@ const ( snykVulnerabilityDB_URI = "https://security.snyk.io" ) +type spdxEnricher = func(*spdx_2_3.Package, *packageurl.PackageURL) + +var spdxEnrichers = []spdxEnricher{ + enrichSPDXSnykAdvisorData, + enrichSPDXSnykVulnerabilityDBData, +} + +func enrichSPDXSnykAdvisorData(component *spdx_2_3.Package, purl *packageurl.PackageURL) { + url := SnykAdvisorURL(purl) + if url != "" { + ext := &spdx_2_3.PackageExternalReference{ + Locator: url, + RefType: "advisory", + Category: spdx.CategoryOther, + ExternalRefComment: "Snyk Advisor", + } + if component.PackageExternalReferences == nil { + component.PackageExternalReferences = []*spdx_2_3.PackageExternalReference{ext} + } else { + component.PackageExternalReferences = append(component.PackageExternalReferences, ext) + } + } +} + +func enrichSPDXSnykVulnerabilityDBData(component *spdx_2_3.Package, purl *packageurl.PackageURL) { + url := SnykVulnURL(purl) + if url != "" { + ext := &spdx_2_3.PackageExternalReference{ + Locator: url, + RefType: "url", + Category: spdx.CategoryOther, + ExternalRefComment: "Snyk Vulnerability DB", + } + if component.PackageExternalReferences == nil { + component.PackageExternalReferences = []*spdx_2_3.PackageExternalReference{ext} + } else { + component.PackageExternalReferences = append(component.PackageExternalReferences, ext) + } + } +} + func enrichSPDX(bom *spdx.Document, logger *zerolog.Logger) *spdx.Document { auth, err := AuthFromToken(APIToken()) if err != nil { @@ -71,7 +113,9 @@ func enrichSPDX(bom *spdx.Document, logger *zerolog.Logger) *spdx.Document { l.Debug().Msg("Could not identify package") return } - + for _, enrichFn := range spdxEnrichers { + enrichFn(pkg, purl) + } resp, err := GetPackageVulnerabilities(purl, auth, orgID) if err != nil { l.Err(err). diff --git a/lib/snyk/enrich_test.go b/lib/snyk/enrich_test.go index 19a13a4..bcb1b10 100644 --- a/lib/snyk/enrich_test.go +++ b/lib/snyk/enrich_test.go @@ -28,8 +28,8 @@ func TestEnrichSBOM_CycloneDXWithVulnerabilities(t *testing.T) { }, } doc := &sbom.SBOMDocument{BOM: bom} - logger := zerolog.Nop() + logger := zerolog.Nop() EnrichSBOM(doc, &logger) assert.NotNil(t, bom.Vulnerabilities) @@ -39,6 +39,74 @@ func TestEnrichSBOM_CycloneDXWithVulnerabilities(t *testing.T) { assert.Equal(t, "SNYK-PYTHON-NUMPY-73513", vuln.ID) } +func TestEnrichSBOM_CycloneDXExternalRefs(t *testing.T) { + teardown := setupTestEnv(t) + defer teardown() + + bom := &cdx.BOM{ + Components: &[]cdx.Component{ + { + BOMRef: "pkg:pypi/numpy@1.16.0", + Name: "numpy", + Version: "1.16.0", + PackageURL: "pkg:pypi/numpy@1.16.0", + }, + }, + } + doc := &sbom.SBOMDocument{BOM: bom} + + logger := zerolog.Nop() + EnrichSBOM(doc, &logger) + + assert.NotNil(t, bom.Components) + refs := (*bom.Components)[0].ExternalReferences + assert.Len(t, *refs, 2) + + ref1 := (*refs)[0] + assert.Equal(t, "https://snyk.io/advisor/python/numpy", ref1.URL) + assert.Equal(t, "Snyk Advisor", ref1.Comment) + assert.Equal(t, cdx.ExternalReferenceType("Other"), ref1.Type) + + ref2 := (*refs)[1] + assert.Equal(t, "https://security.snyk.io/package/pip/numpy", ref2.URL) + assert.Equal(t, "Snyk Vulnerability DB", ref2.Comment) + assert.Equal(t, cdx.ExternalReferenceType("Other"), ref2.Type) +} + +func TestEnrichSBOM_CycloneDXExternalRefs_WithNamespace(t *testing.T) { + teardown := setupTestEnv(t) + defer teardown() + + bom := &cdx.BOM{ + Components: &[]cdx.Component{ + { + BOMRef: "@emotion/react@11.11.3", + Name: "react", + Version: "11.11.3", + PackageURL: "pkg:npm/%40emotion/react@11.11.3", + }, + }, + } + doc := &sbom.SBOMDocument{BOM: bom} + + logger := zerolog.Nop() + EnrichSBOM(doc, &logger) + + assert.NotNil(t, bom.Components) + refs := (*bom.Components)[0].ExternalReferences + assert.Len(t, *refs, 2) + + ref1 := (*refs)[0] + assert.Equal(t, "https://snyk.io/advisor/npm-package/@emotion/react", ref1.URL) + assert.Equal(t, "Snyk Advisor", ref1.Comment) + assert.Equal(t, cdx.ExternalReferenceType("Other"), ref1.Type) + + ref2 := (*refs)[1] + assert.Equal(t, "https://security.snyk.io/package/npm/@emotion%2Freact", ref2.URL) + assert.Equal(t, "Snyk Vulnerability DB", ref2.Comment) + assert.Equal(t, cdx.ExternalReferenceType("Other"), ref2.Type) +} + func TestEnrichSBOM_CycloneDXWithVulnerabilities_NestedComponents(t *testing.T) { teardown := setupTestEnv(t) defer teardown() @@ -62,8 +130,8 @@ func TestEnrichSBOM_CycloneDXWithVulnerabilities_NestedComponents(t *testing.T) }, } doc := &sbom.SBOMDocument{BOM: bom} - logger := zerolog.Nop() + logger := zerolog.Nop() EnrichSBOM(doc, &logger) assert.NotNil(t, bom.Vulnerabilities) @@ -85,8 +153,8 @@ func TestEnrichSBOM_CycloneDXWithoutVulnerabilities(t *testing.T) { }, } doc := &sbom.SBOMDocument{BOM: bom} - logger := zerolog.Nop() + logger := zerolog.Nop() EnrichSBOM(doc, &logger) assert.Nil(t, bom.Vulnerabilities, "should not extend vulnerabilities if there are none") @@ -113,17 +181,60 @@ func TestEnrichSBOM_SPDXWithVulnerabilities(t *testing.T) { }, } doc := &sbom.SBOMDocument{BOM: bom} - logger := zerolog.Nop() + logger := zerolog.Nop() EnrichSBOM(doc, &logger) - vulnRef := bom.Packages[0].PackageExternalReferences[1] + vulnRef := bom.Packages[0].PackageExternalReferences[3] assert.Equal(t, "SECURITY", vulnRef.Category) assert.Equal(t, "advisory", vulnRef.RefType) assert.Equal(t, "https://security.snyk.io/vuln/SNYK-PYTHON-NUMPY-73513", vulnRef.Locator) assert.Equal(t, "Arbitrary Code Execution", vulnRef.ExternalRefComment) } +func TestEnrichSBOM_SPDXExternalRefs(t *testing.T) { + teardown := setupTestEnv(t) + defer teardown() + + bom := &spdx_2_3.Document{ + Packages: []*spdx_2_3.Package{ + { + PackageSPDXIdentifier: "pkg:pypi/numpy@1.16.0", + PackageName: "numpy", + PackageVersion: "1.16.0", + PackageExternalReferences: []*spdx_2_3.PackageExternalReference{ + { + Category: spdx.CategoryPackageManager, + RefType: "purl", + Locator: "pkg:pypi/numpy@1.16.0", + }, + }, + }, + }, + } + + doc := &sbom.SBOMDocument{BOM: bom} + + logger := zerolog.Nop() + EnrichSBOM(doc, &logger) + + assert.NotNil(t, bom.Packages) + refs := (*bom.Packages[0]).PackageExternalReferences + assert.Len(t, refs, 4) + + ref1 := refs[1] + assert.Equal(t, "https://snyk.io/advisor/python/numpy", ref1.Locator) + assert.Equal(t, "Snyk Advisor", ref1.ExternalRefComment) + assert.Equal(t, "advisory", ref1.RefType) + assert.Equal(t, spdx.CategoryOther, ref1.Category) + + ref2 := refs[2] + assert.Equal(t, "https://security.snyk.io/package/pip/numpy", ref2.Locator) + assert.Equal(t, "Snyk Vulnerability DB", ref2.ExternalRefComment) + assert.Equal(t, "url", ref2.RefType) + assert.Equal(t, spdx.CategoryOther, ref2.Category) +} + func setupTestEnv(t *testing.T) func() { t.Helper() diff --git a/lib/snyk/package.go b/lib/snyk/package.go index ea10e0f..2f3403b 100644 --- a/lib/snyk/package.go +++ b/lib/snyk/package.go @@ -26,8 +26,64 @@ import ( "github.com/snyk/parlay/snyk/issues" ) -const snykServer = "https://api.snyk.io/rest" -const version = "2023-04-28" +const ( + snykServer = "https://api.snyk.io/rest" + version = "2023-04-28" + snykAdvisorServer = "https://snyk.io/advisor" + snykVulnDBServer = "https://security.snyk.io/package" +) + +func purlToSnykAdvisor(purl *packageurl.PackageURL) string { + return map[string]string{ + packageurl.TypeNPM: "npm-package", + packageurl.TypePyPi: "python", + packageurl.TypeGolang: "golang", + packageurl.TypeDocker: "docker", + }[purl.Type] +} + +func SnykAdvisorURL(purl *packageurl.PackageURL) string { + ecosystem := purlToSnykAdvisor(purl) + if ecosystem == "" { + return "" + } + url := snykAdvisorServer + "/" + ecosystem + "/" + if purl.Namespace != "" { + url += purl.Namespace + "/" + } + url += purl.Name + return url +} + +func purlToSnykVulnDB(purl *packageurl.PackageURL) string { + return map[string]string{ + packageurl.TypeCargo: "cargo", + packageurl.TypeCocoapods: "cocoapods", + packageurl.TypeComposer: "composer", + packageurl.TypeGolang: "golang", + packageurl.TypeHex: "hex", + packageurl.TypeMaven: "maven", + packageurl.TypeNPM: "npm", + packageurl.TypeNuget: "nuget", + packageurl.TypePyPi: "pip", + packageurl.TypePub: "pub", + packageurl.TypeGem: "rubygems", + packageurl.TypeSwift: "swift", + }[purl.Type] +} + +func SnykVulnURL(purl *packageurl.PackageURL) string { + ecosystem := purlToSnykVulnDB(purl) + if ecosystem == "" { + return "" + } + url := snykVulnDBServer + "/" + ecosystem + "/" + if purl.Namespace != "" { + url += purl.Namespace + "%2F" + } + url += purl.Name + return url +} func GetPackageVulnerabilities(purl *packageurl.PackageURL, auth *securityprovider.SecurityProviderApiKey, orgID *uuid.UUID) (*issues.FetchIssuesPerPurlResponse, error) { client, err := issues.NewClientWithResponses(snykServer, issues.WithRequestEditorFn(auth.Intercept))