diff --git a/go.mod b/go.mod index ca9edd7..9d69184 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ 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 diff --git a/go.sum b/go.sum index 2c7bfc7..73aac61 100644 --- a/go.sum +++ b/go.sum @@ -168,8 +168,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= diff --git a/lib/ecosystems/enrich_cyclonedx.go b/lib/ecosystems/enrich_cyclonedx.go index c06e0bd..9edc08b 100644 --- a/lib/ecosystems/enrich_cyclonedx.go +++ b/lib/ecosystems/enrich_cyclonedx.go @@ -28,7 +28,7 @@ import ( "github.com/snyk/parlay/internal/utils" ) -type cdxEnricher = func(cdx.Component, packages.Package) cdx.Component +type cdxEnricher = func(*cdx.Component, *packages.Package) var cdxEnrichers = []cdxEnricher{ enrichCDXDescription, @@ -46,122 +46,114 @@ var cdxEnrichers = []cdxEnricher{ enrichCDXSupplier, } -func enrichCDXDescription(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.Description != nil { - component.Description = *packageData.Description +func enrichCDXDescription(comp *cdx.Component, data *packages.Package) { + if data.Description != nil { + comp.Description = *data.Description } - return component } -func enrichCDXLicense(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.NormalizedLicenses != nil { - if len(packageData.NormalizedLicenses) > 0 { - expression := packageData.NormalizedLicenses[0] +func enrichCDXLicense(comp *cdx.Component, data *packages.Package) { + if data.NormalizedLicenses != nil { + if len(data.NormalizedLicenses) > 0 { + expression := data.NormalizedLicenses[0] licenses := cdx.LicenseChoice{Expression: expression} - component.Licenses = &cdx.Licenses{licenses} + comp.Licenses = &cdx.Licenses{licenses} } } - return component } -func enrichExternalReference(component cdx.Component, _ packages.Package, url *string, refType cdx.ExternalReferenceType) cdx.Component { +func enrichExternalReference(comp *cdx.Component, url *string, refType cdx.ExternalReferenceType) { if url == nil { - return component + return } ext := cdx.ExternalReference{ URL: *url, Type: refType, } - if component.ExternalReferences == nil { - component.ExternalReferences = &[]cdx.ExternalReference{ext} + if comp.ExternalReferences == nil { + comp.ExternalReferences = &[]cdx.ExternalReference{ext} } else { - *component.ExternalReferences = append(*component.ExternalReferences, ext) + *comp.ExternalReferences = append(*comp.ExternalReferences, ext) } - return component } -func enrichProperty(component cdx.Component, name string, value string) cdx.Component { +func enrichProperty(comp *cdx.Component, name string, value string) { prop := cdx.Property{ Name: name, Value: value, } - if component.Properties == nil { - component.Properties = &[]cdx.Property{prop} + if comp.Properties == nil { + comp.Properties = &[]cdx.Property{prop} } else { - *component.Properties = append(*component.Properties, prop) + *comp.Properties = append(*comp.Properties, prop) } - return component } -func enrichCDXHomepage(component cdx.Component, packageData packages.Package) cdx.Component { - return enrichExternalReference(component, packageData, packageData.Homepage, cdx.ERTypeWebsite) +func enrichCDXHomepage(comp *cdx.Component, data *packages.Package) { + enrichExternalReference(comp, data.Homepage, cdx.ERTypeWebsite) } -func enrichCDXRegistryURL(component cdx.Component, packageData packages.Package) cdx.Component { - return enrichExternalReference(component, packageData, packageData.RegistryUrl, cdx.ERTypeDistribution) +func enrichCDXRegistryURL(comp *cdx.Component, data *packages.Package) { + enrichExternalReference(comp, data.RegistryUrl, cdx.ERTypeDistribution) } -func enrichCDXRepositoryURL(component cdx.Component, packageData packages.Package) cdx.Component { - return enrichExternalReference(component, packageData, packageData.RepositoryUrl, cdx.ERTypeVCS) +func enrichCDXRepositoryURL(comp *cdx.Component, data *packages.Package) { + enrichExternalReference(comp, data.RepositoryUrl, cdx.ERTypeVCS) } -func enrichCDXDocumentationURL(component cdx.Component, packageData packages.Package) cdx.Component { - return enrichExternalReference(component, packageData, packageData.DocumentationUrl, cdx.ERTypeDocumentation) +func enrichCDXDocumentationURL(comp *cdx.Component, data *packages.Package) { + enrichExternalReference(comp, data.DocumentationUrl, cdx.ERTypeDocumentation) } -func enrichCDXFirstReleasePublishedAt(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.FirstReleasePublishedAt == nil { - return component +func enrichCDXFirstReleasePublishedAt(comp *cdx.Component, data *packages.Package) { + if data.FirstReleasePublishedAt == nil { + return } - timestamp := packageData.FirstReleasePublishedAt.UTC().Format(time.RFC3339) - return enrichProperty(component, "ecosystems:first_release_published_at", timestamp) + timestamp := data.FirstReleasePublishedAt.UTC().Format(time.RFC3339) + enrichProperty(comp, "ecosystems:first_release_published_at", timestamp) } -func enrichCDXLatestReleasePublishedAt(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.LatestReleasePublishedAt == nil { - return component +func enrichCDXLatestReleasePublishedAt(comp *cdx.Component, data *packages.Package) { + if data.LatestReleasePublishedAt == nil { + return } - timestamp := packageData.LatestReleasePublishedAt.UTC().Format(time.RFC3339) - return enrichProperty(component, "ecosystems:latest_release_published_at", timestamp) + timestamp := data.LatestReleasePublishedAt.UTC().Format(time.RFC3339) + enrichProperty(comp, "ecosystems:latest_release_published_at", timestamp) } -func enrichCDXRepoArchived(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.RepoMetadata != nil { - if archived, ok := (*packageData.RepoMetadata)["archived"].(bool); ok && archived { - return enrichProperty(component, "ecosystems:repository_archived", "true") +func enrichCDXRepoArchived(comp *cdx.Component, data *packages.Package) { + if data.RepoMetadata != nil { + if archived, ok := (*data.RepoMetadata)["archived"].(bool); ok && archived { + enrichProperty(comp, "ecosystems:repository_archived", "true") } } - return component } -func enrichCDXLocation(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.RepoMetadata != nil { - meta := *packageData.RepoMetadata +func enrichCDXLocation(comp *cdx.Component, data *packages.Package) { + if data.RepoMetadata != nil { + meta := *data.RepoMetadata if ownerRecord, ok := meta["owner_record"].(map[string]interface{}); ok { if location, ok := ownerRecord["location"].(string); ok { - return enrichProperty(component, "ecosystems:owner_location", location) + enrichProperty(comp, "ecosystems:owner_location", location) } } } - return component } -func enrichCDXAuthor(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.RepoMetadata != nil { - meta := *packageData.RepoMetadata +func enrichCDXAuthor(comp *cdx.Component, data *packages.Package) { + if data.RepoMetadata != nil { + meta := *data.RepoMetadata if ownerRecord, ok := meta["owner_record"].(map[string]interface{}); ok { if name, ok := ownerRecord["name"].(string); ok { - component.Author = name - return component + comp.Author = name } } } - return component } -func enrichCDXSupplier(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.RepoMetadata != nil { - meta := *packageData.RepoMetadata +func enrichCDXSupplier(comp *cdx.Component, data *packages.Package) { + if data.RepoMetadata != nil { + meta := *data.RepoMetadata if ownerRecord, ok := meta["owner_record"].(map[string]interface{}); ok { if name, ok := ownerRecord["name"].(string); ok { supplier := cdx.OrganizationalEntity{ @@ -171,26 +163,22 @@ func enrichCDXSupplier(component cdx.Component, packageData packages.Package) cd websites := []string{website} supplier.URL = &websites } - component.Supplier = &supplier - return component + comp.Supplier = &supplier } } } - return component } -func enrichCDXTopics(component cdx.Component, packageData packages.Package) cdx.Component { - if packageData.RepoMetadata != nil { - meta := *packageData.RepoMetadata +func enrichCDXTopics(comp *cdx.Component, data *packages.Package) { + if data.RepoMetadata != nil { + meta := *data.RepoMetadata if topics, ok := meta["topics"].([]interface{}); ok { for _, topic := range topics { - component = enrichProperty(component, "ecosystems:topic", topic.(string)) + enrichProperty(comp, "ecosystems:topic", topic.(string)) } } - return component } - return component } func enrichCDX(bom *cdx.BOM, logger *zerolog.Logger) { @@ -229,7 +217,7 @@ func enrichCDX(bom *cdx.BOM, logger *zerolog.Logger) { } for _, enrichFunc := range cdxEnrichers { - *comp = enrichFunc(*comp, *resp.JSON200) + enrichFunc(comp, resp.JSON200) } }(comps[i]) } diff --git a/lib/ecosystems/enrich_cyclonedx_test.go b/lib/ecosystems/enrich_cyclonedx_test.go index 797f844..0027f7b 100644 --- a/lib/ecosystems/enrich_cyclonedx_test.go +++ b/lib/ecosystems/enrich_cyclonedx_test.go @@ -160,31 +160,34 @@ func TestEnrichSBOMWithoutLicense(t *testing.T) { } func TestEnrichDescription(t *testing.T) { - component := cdx.Component{ + component := &cdx.Component{ Type: cdx.ComponentTypeLibrary, Name: "cyclonedx-go", Version: "v0.3.0", } desc := "description" - pack := packages.Package{ + pack := &packages.Package{ Description: &desc, } - component = enrichCDXDescription(component, pack) + + enrichCDXDescription(component, pack) + assert.Equal(t, "description", component.Description) } func TestEnrichLicense(t *testing.T) { - component := cdx.Component{ + component := &cdx.Component{ Type: cdx.ComponentTypeLibrary, Name: "cyclonedx-go", Version: "v0.3.0", } - pack := packages.Package{ + pack := &packages.Package{ NormalizedLicenses: []string{"BSD-3-Clause"}, } - component = enrichCDXLicense(component, pack) - licenses := *component.Licenses + enrichCDXLicense(component, pack) + + licenses := *component.Licenses comp := cdx.LicenseChoice(cdx.LicenseChoice{Expression: "BSD-3-Clause"}) assert.Equal(t, comp, licenses[0]) } @@ -200,93 +203,91 @@ func TestEnrichBlankSBOM(t *testing.T) { } func TestEnrichExternalReferenceWithNilURL(t *testing.T) { - component := cdx.Component{} - packageData := packages.Package{Homepage: nil} + component := &cdx.Component{} + packageData := &packages.Package{Homepage: nil} - result := enrichExternalReference(component, packageData, packageData.Homepage, cdx.ERTypeWebsite) + enrichExternalReference(component, packageData.Homepage, cdx.ERTypeWebsite) - assert.Equal(t, component, result) + assert.Nil(t, component.ExternalReferences) } func TestEnrichExternalReferenceWithNonNullURL(t *testing.T) { - component := cdx.Component{} - packageData := packages.Package{Homepage: pointerToString("https://example.com")} + component := &cdx.Component{} + packageData := packages.Package{Homepage: pointerToString(t, "https://example.com")} - result := enrichExternalReference(component, packageData, packageData.Homepage, cdx.ERTypeWebsite) + enrichExternalReference(component, packageData.Homepage, cdx.ERTypeWebsite) - expected := cdx.Component{ - ExternalReferences: &[]cdx.ExternalReference{ - {URL: "https://example.com", Type: cdx.ERTypeWebsite}, - }, + expected := &[]cdx.ExternalReference{ + {URL: "https://example.com", Type: cdx.ERTypeWebsite}, } - assert.Equal(t, expected, result) + assert.Equal(t, expected, component.ExternalReferences) } func TestEnrichHomepageWithNilHomepage(t *testing.T) { - component := cdx.Component{} - packageData := packages.Package{Homepage: nil} + component := &cdx.Component{} + packageData := &packages.Package{Homepage: nil} - result := enrichCDXHomepage(component, packageData) + enrichCDXHomepage(component, packageData) - assert.Equal(t, component, result) + assert.Nil(t, component.ExternalReferences) } func TestEnrichHomepageWithNonNullHomepage(t *testing.T) { - component := cdx.Component{} - packageData := packages.Package{Homepage: pointerToString("https://example.com")} + component := &cdx.Component{} + packageData := &packages.Package{Homepage: pointerToString(t, "https://example.com")} - result := enrichCDXHomepage(component, packageData) + enrichCDXHomepage(component, packageData) - expected := cdx.Component{ - ExternalReferences: &[]cdx.ExternalReference{ - {URL: "https://example.com", Type: cdx.ERTypeWebsite}, - }, + expected := &[]cdx.ExternalReference{ + {URL: "https://example.com", Type: cdx.ERTypeWebsite}, } - assert.Equal(t, expected, result) + assert.Equal(t, expected, component.ExternalReferences) } func TestEnrichRegistryURLWithNilRegistryURL(t *testing.T) { - component := cdx.Component{} - packageData := packages.Package{RegistryUrl: nil} + component := &cdx.Component{} + packageData := &packages.Package{RegistryUrl: nil} - result := enrichCDXRegistryURL(component, packageData) + enrichCDXRegistryURL(component, packageData) - assert.Equal(t, component, result) + assert.Nil(t, component.ExternalReferences) } func TestEnrichRegistryURLWithNonNullRegistryURL(t *testing.T) { - component := cdx.Component{} - packageData := packages.Package{RegistryUrl: pointerToString("https://example.com")} + component := &cdx.Component{} + packageData := &packages.Package{RegistryUrl: pointerToString(t, "https://example.com")} - result := enrichCDXRegistryURL(component, packageData) + enrichCDXRegistryURL(component, packageData) - expected := cdx.Component{ - ExternalReferences: &[]cdx.ExternalReference{ - {URL: "https://example.com", Type: cdx.ERTypeDistribution}, - }, + expected := &[]cdx.ExternalReference{ + {URL: "https://example.com", Type: cdx.ERTypeDistribution}, } - assert.Equal(t, expected, result) + assert.Equal(t, expected, component.ExternalReferences) } -func pointerToString(s string) *string { +func pointerToString(t *testing.T, s string) *string { + t.Helper() return &s } func TestEnrichLatestReleasePublishedAt(t *testing.T) { - component := cdx.Component{} - packageData := packages.Package{ + component := &cdx.Component{} + packageData := &packages.Package{ LatestReleasePublishedAt: nil, } - result := enrichCDXLatestReleasePublishedAt(component, packageData) - assert.Equal(t, component, result) + enrichCDXLatestReleasePublishedAt(component, packageData) + + assert.Nil(t, component.Properties) latestReleasePublishedAt := time.Date(2023, time.May, 1, 0, 0, 0, 0, time.UTC) packageData.LatestReleasePublishedAt = &latestReleasePublishedAt expectedTimestamp := latestReleasePublishedAt.UTC().Format(time.RFC3339) - result = enrichCDXLatestReleasePublishedAt(component, packageData) - prop := (*result.Properties)[0] + enrichCDXLatestReleasePublishedAt(component, packageData) + + assert.Len(t, *component.Properties, 1) + prop := (*component.Properties)[0] assert.Equal(t, "ecosystems:latest_release_published_at", prop.Name) assert.Equal(t, expectedTimestamp, prop.Value) } @@ -295,42 +296,38 @@ func TestEnrichLocation(t *testing.T) { assert := assert.New(t) // Test case 1: packageData.RepoMetadata is nil - component := cdx.Component{Name: "test"} - packageData := packages.Package{} - result := enrichCDXLocation(component, packageData) - assert.Equal(component, result) + component := &cdx.Component{Name: "test"} + packageData := &packages.Package{} + enrichCDXLocation(component, packageData) + assert.Nil(component.Properties) // Test case 2: packageData.RepoMetadata is not nil, but "owner_record" is missing - component = cdx.Component{Name: "test"} - packageData = packages.Package{RepoMetadata: &map[string]interface{}{ + component = &cdx.Component{Name: "test"} + packageData = &packages.Package{RepoMetadata: &map[string]interface{}{ "not_owner_record": map[string]interface{}{}, }} - result = enrichCDXLocation(component, packageData) - assert.Equal(component, result) + enrichCDXLocation(component, packageData) + assert.Nil(component.Properties) // Test case 3: "location" field is missing in "owner_record" - component = cdx.Component{Name: "test"} - packageData = packages.Package{RepoMetadata: &map[string]interface{}{ + component = &cdx.Component{Name: "test"} + packageData = &packages.Package{RepoMetadata: &map[string]interface{}{ "owner_record": map[string]interface{}{ "not_location": "test", }, }} - result = enrichCDXLocation(component, packageData) - assert.Equal(component, result) + enrichCDXLocation(component, packageData) + assert.Nil(component.Properties) // Test case 4: "location" field is present in "owner_record" - component = cdx.Component{Name: "test"} - packageData = packages.Package{RepoMetadata: &map[string]interface{}{ + component = &cdx.Component{Name: "test"} + packageData = &packages.Package{RepoMetadata: &map[string]interface{}{ "owner_record": map[string]interface{}{ "location": "test_location", }, }} - expectedComponent := cdx.Component{ - Name: "test", - Properties: &[]cdx.Property{ - {Name: "ecosystems:owner_location", Value: "test_location"}, - }, - } - result = enrichCDXLocation(component, packageData) - assert.Equal(expectedComponent, result) + enrichCDXLocation(component, packageData) + assert.Equal(&[]cdx.Property{ + {Name: "ecosystems:owner_location", Value: "test_location"}, + }, component.Properties) } 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..8b746f5 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: "Other", + 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: "Other", + 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..2b4ad72 100644 --- a/lib/snyk/enrich_test.go +++ b/lib/snyk/enrich_test.go @@ -13,6 +13,8 @@ import ( "github.com/snyk/parlay/lib/sbom" ) +var logger = zerolog.Nop() + func TestEnrichSBOM_CycloneDXWithVulnerabilities(t *testing.T) { teardown := setupTestEnv(t) defer teardown() @@ -28,7 +30,6 @@ func TestEnrichSBOM_CycloneDXWithVulnerabilities(t *testing.T) { }, } doc := &sbom.SBOMDocument{BOM: bom} - logger := zerolog.Nop() EnrichSBOM(doc, &logger) @@ -39,6 +40,72 @@ 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} + + 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} + + 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,7 +129,6 @@ func TestEnrichSBOM_CycloneDXWithVulnerabilities_NestedComponents(t *testing.T) }, } doc := &sbom.SBOMDocument{BOM: bom} - logger := zerolog.Nop() EnrichSBOM(doc, &logger) @@ -85,7 +151,6 @@ func TestEnrichSBOM_CycloneDXWithoutVulnerabilities(t *testing.T) { }, } doc := &sbom.SBOMDocument{BOM: bom} - logger := zerolog.Nop() EnrichSBOM(doc, &logger) @@ -113,17 +178,58 @@ func TestEnrichSBOM_SPDXWithVulnerabilities(t *testing.T) { }, } doc := &sbom.SBOMDocument{BOM: bom} - 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} + + 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, "Other", 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, "Other", 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))