Skip to content

Commit

Permalink
fix: deep traversal of cyclonedx components
Browse files Browse the repository at this point in the history
feat: improve discoverComps readability and test coverage
  • Loading branch information
paulrosca-snyk committed Feb 29, 2024
1 parent b7a921b commit 0a23db4
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 42 deletions.
29 changes: 29 additions & 0 deletions internal/utils/cdx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package utils

import (
cdx "github.com/CycloneDX/cyclonedx-go"
)

func traverseComponent(comps *[]*cdx.Component, comp *cdx.Component) {
*comps = append(*comps, comp)
if comp.Components == nil {
return
}
for i := range *comp.Components {
traverseComponent(comps, &(*comp.Components)[i])
}
}

func DiscoverCDXComponents(bom *cdx.BOM) []*cdx.Component {
comps := make([]*cdx.Component, 0)
if bom.Metadata != nil && bom.Metadata.Component != nil {
traverseComponent(&comps, bom.Metadata.Component)
}

if bom.Components != nil {
for i := range *bom.Components {
traverseComponent(&comps, &(*bom.Components)[i])
}
}
return comps
}
33 changes: 33 additions & 0 deletions internal/utils/cdx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package utils_test

import (
"testing"

"github.com/snyk/parlay/internal/utils"

cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert"
)

func TestDiscoverCDXComponents(t *testing.T) {
assert := assert.New(t)

bom := &cdx.BOM{
Metadata: &cdx.Metadata{
Component: &cdx.Component{
Name: "MetaComp",
},
},
Components: &[]cdx.Component{
{
Name: "Parent",
Components: &[]cdx.Component{
{Name: "Child"},
},
},
},
}
result := utils.DiscoverCDXComponents(bom)

assert.Equal(len(result), 3)
}
25 changes: 11 additions & 14 deletions lib/ecosystems/enrich_cyclonedx.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/remeh/sizedwaitgroup"

"github.com/snyk/parlay/ecosystems/packages"
"github.com/snyk/parlay/internal/utils"
)

type cdxEnricher = func(cdx.Component, packages.Package) cdx.Component
Expand Down Expand Up @@ -192,30 +193,26 @@ func enrichCDXTopics(component cdx.Component, packageData packages.Package) cdx.
}

func enrichCDX(bom *cdx.BOM) {
if bom.Components == nil {
return
}

comps := utils.DiscoverCDXComponents(bom)
wg := sizedwaitgroup.New(20)
newComponents := make([]cdx.Component, len(*bom.Components))
for i, component := range *bom.Components {
for i := range comps {
wg.Add()
go func(component cdx.Component, i int) {
// TODO: return when there is no usable Purl on the component.
purl, _ := packageurl.FromString(component.PackageURL) //nolint:errcheck
go func(component *cdx.Component) {
defer wg.Done()
purl, err := packageurl.FromString(component.PackageURL)
if err != nil {
return
}
resp, err := GetPackageData(purl)
if err == nil {
packageData := resp.JSON200
if packageData != nil {
for _, enrichFunc := range cdxEnrichers {
component = enrichFunc(component, *packageData)
*component = enrichFunc(*component, *packageData)
}
}
}
newComponents[i] = component
wg.Done()
}(component, i)
}(comps[i])
}
wg.Wait()
bom.Components = &newComponents
}
49 changes: 48 additions & 1 deletion lib/ecosystems/enrich_cyclonedx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ func TestEnrichSBOM_CycloneDX(t *testing.T) {
})

bom := &cdx.BOM{
Metadata: &cdx.Metadata{
Component: &cdx.Component{
BOMRef: "pkg:golang/github.com/ACME/[email protected]",
Type: cdx.ComponentTypeApplication,
Name: "Project",
Version: "v1.0.0",
PackageURL: "pkg:golang/github.com/ACME/[email protected]",
},
},
Components: &[]cdx.Component{
{
BOMRef: "pkg:golang/github.com/CycloneDX/[email protected]",
Expand All @@ -69,7 +78,45 @@ func TestEnrichSBOM_CycloneDX(t *testing.T) {

httpmock.GetTotalCallCount()
calls := httpmock.GetCallCountInfo()
assert.Equal(t, len(components), calls[`GET =~^https://packages.ecosyste.ms/api/v1/registries`])
assert.Equal(t, 2, calls[`GET =~^https://packages.ecosyste.ms/api/v1/registries`])
}

func TestEnrichSBOM_CycloneDX_NestedComps(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder("GET", `=~^https://packages.ecosyste.ms/api/v1/registries`,
func(req *http.Request) (*http.Response, error) {
return httpmock.NewJsonResponse(200, map[string]interface{}{})
})

bom := &cdx.BOM{
Components: &[]cdx.Component{
{
BOMRef: "@emotion/[email protected]",
Type: cdx.ComponentTypeLibrary,
Name: "babel-plugin",
Version: "v11.11.0",
PackageURL: "pkg:npm/%40emotion/[email protected]",
Components: &[]cdx.Component{
{
Type: cdx.ComponentTypeLibrary,
Name: "convert-source-map",
Version: "v1.9.0",
BOMRef: "@emotion/[email protected]|[email protected]",
PackageURL: "pkg:npm/[email protected]",
},
},
},
},
}
doc := &sbom.SBOMDocument{BOM: bom}

EnrichSBOM(doc)

httpmock.GetTotalCallCount()
calls := httpmock.GetCallCountInfo()
assert.Equal(t, 2, calls[`GET =~^https://packages.ecosyste.ms/api/v1/registries`])
}

func TestEnrichSBOMWithoutLicense(t *testing.T) {
Expand Down
25 changes: 11 additions & 14 deletions lib/scorecard/enrich_cyclonedx.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/package-url/packageurl-go"
"github.com/remeh/sizedwaitgroup"

"github.com/snyk/parlay/internal/utils"
"github.com/snyk/parlay/lib/ecosystems"
)

Expand All @@ -42,32 +43,28 @@ func cdxEnrichExternalReference(component cdx.Component, url string, comment str
}

func enrichCDX(bom *cdx.BOM) {
if bom.Components == nil {
return
}

comps := utils.DiscoverCDXComponents(bom)
wg := sizedwaitgroup.New(20)
newComponents := make([]cdx.Component, len(*bom.Components))
for i, component := range *bom.Components {
for i := range comps {
wg.Add()
go func(component cdx.Component, i int) {
// TODO: return when there is no usable Purl on the component.
purl, _ := packageurl.FromString(component.PackageURL) //nolint:errcheck
go func(component *cdx.Component) {
defer wg.Done()
purl, err := packageurl.FromString(component.PackageURL)
if err != nil {
return
}
resp, err := ecosystems.GetPackageData(purl)
if err == nil && resp.JSON200 != nil && resp.JSON200.RepositoryUrl != nil {
scorecardUrl := strings.ReplaceAll(*resp.JSON200.RepositoryUrl, "https://", "https://api.securityscorecards.dev/projects/")
response, err := http.Get(scorecardUrl)
if err == nil {
defer response.Body.Close()
if response.StatusCode == http.StatusOK {
component = cdxEnrichExternalReference(component, scorecardUrl, "OpenSSF Scorecard", cdx.ERTypeOther)
*component = cdxEnrichExternalReference(*component, scorecardUrl, "OpenSSF Scorecard", cdx.ERTypeOther)
}
}
}
newComponents[i] = component
wg.Done()
}(component, i)
}(comps[i])
}
wg.Wait()
bom.Components = &newComponents
}
40 changes: 39 additions & 1 deletion lib/scorecard/enrich_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestEnrichSBOM_CycloneDX(t *testing.T) {
bom := &cdx.BOM{
Components: &[]cdx.Component{
{
PackageURL: "pkg:/example",
PackageURL: "pkg:type/example",
},
},
}
Expand All @@ -63,6 +63,44 @@ func TestEnrichSBOM_CycloneDX(t *testing.T) {
assert.Equal(t, 1, calls[`GET =~^https://packages.ecosyste.ms/api/v1/registries`])
}

func TestEnrichSBOM_CycloneDX_NestedComponents(t *testing.T) {
teardown := setupEcosystemsAPIMock(t)
defer teardown()

bom := &cdx.BOM{
Components: &[]cdx.Component{
{
PackageURL: "pkg:type/example",
Components: &[]cdx.Component{
{
PackageURL: "pkg:otherType/otherExample",
},
},
},
},
}
doc := &sbom.SBOMDocument{BOM: bom}

EnrichSBOM(doc)

assert.NotNil(t, bom.Components)
assert.Len(t, *bom.Components, 1)

for i := range *bom.Components {
enrichedComponent := (*bom.Components)[i]
assert.NotNil(t, enrichedComponent.ExternalReferences)
assert.Len(t, *enrichedComponent.ExternalReferences, 1)
assert.Equal(t, scorecardURL, (*enrichedComponent.ExternalReferences)[0].URL)
assert.Equal(t, "OpenSSF Scorecard", (*enrichedComponent.ExternalReferences)[0].Comment)
assert.Equal(t, cdx.ERTypeOther, (*enrichedComponent.ExternalReferences)[0].Type)
}

total := httpmock.GetTotalCallCount()
assert.Equal(t, 4, total)
calls := httpmock.GetCallCountInfo()
assert.Equal(t, 2, calls[`GET =~^https://packages.ecosyste.ms/api/v1/registries`])
}

func TestEnrichSBOM_ErrorFetchingPackageData(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
Expand Down
19 changes: 7 additions & 12 deletions lib/snyk/enrich_cyclonedx.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,11 @@ import (
"github.com/remeh/sizedwaitgroup"
"github.com/rs/zerolog"

"github.com/snyk/parlay/internal/utils"
"github.com/snyk/parlay/snyk/issues"
)

func enrichCycloneDX(bom *cdx.BOM, logger zerolog.Logger) *cdx.BOM {
if bom.Components == nil {
return bom
}

auth, err := AuthFromToken(APIToken())
if err != nil {
logger.Fatal().Err(err).Msg("Failed to authenticate.")
Expand All @@ -47,15 +44,14 @@ func enrichCycloneDX(bom *cdx.BOM, logger zerolog.Logger) *cdx.BOM {
return nil
}

wg := sizedwaitgroup.New(20)
var mutex = &sync.Mutex{}
vulnerabilities := make(map[cdx.Component][]issues.CommonIssueModelVTwo)

for i, component := range *bom.Components {
comps := utils.DiscoverCDXComponents(bom)
wg := sizedwaitgroup.New(20)
for i := range comps {
wg.Add()
go func(component cdx.Component, i int) {
go func(component *cdx.Component) {
defer wg.Done()

purl, err := packageurl.FromString(component.PackageURL)
if err != nil {
logger.Debug().
Expand Down Expand Up @@ -84,12 +80,11 @@ func enrichCycloneDX(bom *cdx.BOM, logger zerolog.Logger) *cdx.BOM {

if packageDoc.Data != nil {
mutex.Lock()
vulnerabilities[component] = *packageDoc.Data
vulnerabilities[*component] = *packageDoc.Data
mutex.Unlock()
}
}(component, i)
}(comps[i])
}

wg.Wait()

var vulns []cdx.Vulnerability
Expand Down
36 changes: 36 additions & 0 deletions lib/snyk/enrich_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,37 @@ func TestEnrichSBOM_CycloneDXWithVulnerabilities(t *testing.T) {
assert.Equal(t, "SNYK-PYTHON-NUMPY-73513", vuln.ID)
}

func TestEnrichSBOM_CycloneDXWithVulnerabilities_NestedComponents(t *testing.T) {
teardown := setupTestEnv(t)
defer teardown()

bom := &cdx.BOM{
Components: &[]cdx.Component{
{
BOMRef: "pkg:pypi/[email protected]",
Name: "pandas",
Version: "0.15.0",
PackageURL: "pkg:pypi/[email protected]",
Components: &[]cdx.Component{
{
BOMRef: "pkg:pypi/[email protected]",
Name: "numpy",
Version: "1.16.0",
PackageURL: "pkg:pypi/[email protected]",
},
},
},
},
}
doc := &sbom.SBOMDocument{BOM: bom}
logger := zerolog.Nop()

EnrichSBOM(doc, logger)

assert.NotNil(t, bom.Vulnerabilities)
assert.Len(t, *bom.Vulnerabilities, 2)
}

func TestEnrichSBOM_CycloneDXWithoutVulnerabilities(t *testing.T) {
teardown := setupTestEnv(t)
defer teardown()
Expand Down Expand Up @@ -109,6 +140,11 @@ func setupTestEnv(t *testing.T) func() {
`=~^https://api\.snyk\.io/rest/orgs/[a-z0-9-]+/packages/pkg%3Apypi%2Fnumpy%401.16.0/issues`,
httpmock.NewJsonResponderOrPanic(200, httpmock.File("testdata/numpy_issues.json")),
)
httpmock.RegisterResponder(
"GET",
`=~^https://api\.snyk\.io/rest/orgs/[a-z0-9-]+/packages/pkg%3Apypi%2Fpandas%400.15.0/issues`,
httpmock.NewJsonResponderOrPanic(200, httpmock.File("testdata/pandas_issues.json")),
)
httpmock.RegisterResponder(
"GET",
`=~^https://api\.snyk\.io/rest/orgs/[a-z0-9-]+/packages/.*/issues`,
Expand Down
Loading

0 comments on commit 0a23db4

Please sign in to comment.