Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: deep traversal of CycloneDX components #60

Merged
merged 1 commit into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
paulrosca-snyk marked this conversation as resolved.
Show resolved Hide resolved

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
Loading