diff --git a/internal/utils/spdx.go b/internal/utils/spdx.go new file mode 100644 index 0000000..da1a5c1 --- /dev/null +++ b/internal/utils/spdx.go @@ -0,0 +1,30 @@ +package utils + +import ( + "fmt" + + "github.com/package-url/packageurl-go" + spdx_2_3 "github.com/spdx/tools-golang/spdx/v2/v2_3" +) + +func GetPurlFromSPDXPackage(pkg *spdx_2_3.Package) (*packageurl.PackageURL, error) { + var p string + + for _, ref := range pkg.PackageExternalReferences { + if ref.RefType == "purl" { + p = ref.Locator + break + } + } + + if p == "" { + return nil, fmt.Errorf("no purl on package %s", pkg.PackageName) + } + + purl, err := packageurl.FromString(p) + if err != nil { + return nil, err + } + + return &purl, nil +} diff --git a/lib/scorecard/enrich.go b/lib/scorecard/enrich.go index 50d9d82..ae240ea 100644 --- a/lib/scorecard/enrich.go +++ b/lib/scorecard/enrich.go @@ -1,65 +1,35 @@ +/* + * © 2023 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 scorecard import ( - "net/http" - "strings" - cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/package-url/packageurl-go" - "github.com/remeh/sizedwaitgroup" + "github.com/spdx/tools-golang/spdx" - "github.com/snyk/parlay/lib/ecosystems" "github.com/snyk/parlay/lib/sbom" ) -func enrichExternalReference(component cdx.Component, url string, comment string, refType cdx.ExternalReferenceType) cdx.Component { - ext := cdx.ExternalReference{ - URL: url, - Comment: comment, - Type: refType, - } - if component.ExternalReferences == nil { - component.ExternalReferences = &[]cdx.ExternalReference{ext} - } else { - *component.ExternalReferences = append(*component.ExternalReferences, ext) - } - return component -} - func EnrichSBOM(doc *sbom.SBOMDocument) *sbom.SBOMDocument { - bom, ok := doc.BOM.(*cdx.BOM) - if !ok { - return doc - } - - if bom.Components == nil { - return doc - } - - wg := sizedwaitgroup.New(20) - newComponents := make([]cdx.Component, len(*bom.Components)) - for i, component := range *bom.Components { - 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 - 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 = enrichExternalReference(component, scorecardUrl, "OpenSSF Scorecard", cdx.ERTypeOther) - } - } - } - newComponents[i] = component - wg.Done() - }(component, i) + switch bom := doc.BOM.(type) { + case *cdx.BOM: + enrichCDX(bom) + case *spdx.Document: + enrichSPDX(bom) } - wg.Wait() - bom.Components = &newComponents return doc } diff --git a/lib/scorecard/enrich_cyclonedx.go b/lib/scorecard/enrich_cyclonedx.go new file mode 100644 index 0000000..46deac8 --- /dev/null +++ b/lib/scorecard/enrich_cyclonedx.go @@ -0,0 +1,73 @@ +/* + * © 2023 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 scorecard + +import ( + "net/http" + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/package-url/packageurl-go" + "github.com/remeh/sizedwaitgroup" + + "github.com/snyk/parlay/lib/ecosystems" +) + +func cdxEnrichExternalReference(component cdx.Component, url string, comment string, refType cdx.ExternalReferenceType) cdx.Component { + ext := cdx.ExternalReference{ + URL: url, + Comment: comment, + Type: refType, + } + if component.ExternalReferences == nil { + component.ExternalReferences = &[]cdx.ExternalReference{ext} + } else { + *component.ExternalReferences = append(*component.ExternalReferences, ext) + } + return component +} + +func enrichCDX(bom *cdx.BOM) { + if bom.Components == nil { + return + } + + wg := sizedwaitgroup.New(20) + newComponents := make([]cdx.Component, len(*bom.Components)) + for i, component := range *bom.Components { + 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 + 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) + } + } + } + newComponents[i] = component + wg.Done() + }(component, i) + } + wg.Wait() + bom.Components = &newComponents +} diff --git a/lib/scorecard/enrich_spdx.go b/lib/scorecard/enrich_spdx.go new file mode 100644 index 0000000..caeb8b5 --- /dev/null +++ b/lib/scorecard/enrich_spdx.go @@ -0,0 +1,66 @@ +/* + * © 2023 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 scorecard + +import ( + "net/http" + "strings" + + "github.com/remeh/sizedwaitgroup" + "github.com/spdx/tools-golang/spdx" + spdx_2_3 "github.com/spdx/tools-golang/spdx/v2/v2_3" + + "github.com/snyk/parlay/internal/utils" + "github.com/snyk/parlay/lib/ecosystems" +) + +func enrichSPDX(bom *spdx.Document) { + wg := sizedwaitgroup.New(20) + + for i, pkg := range bom.Packages { + wg.Add() + + go func(pkg *spdx_2_3.Package, i int) { + defer wg.Done() + + purl, err := utils.GetPurlFromSPDXPackage(pkg) + if err != nil { + return + } + + resp, err := ecosystems.GetPackageData(*purl) + if err != nil || resp.JSON200 == nil || resp.JSON200.RepositoryUrl == nil { + return + } + + scURL := strings.ReplaceAll(*resp.JSON200.RepositoryUrl, "https://", "https://api.securityscorecards.dev/projects/") + + response, err := http.Get(scURL) + if err != nil || response.StatusCode != http.StatusOK { + return + } + + pkg.PackageExternalReferences = append(pkg.PackageExternalReferences, &spdx_2_3.PackageExternalReference{ + Category: "OTHER", + RefType: "openssfscorecard", + Locator: scURL, + }) + }(pkg, i) + } + + wg.Wait() +} diff --git a/lib/scorecard/enrich_test.go b/lib/scorecard/enrich_test.go index 4e1253b..0261427 100644 --- a/lib/scorecard/enrich_test.go +++ b/lib/scorecard/enrich_test.go @@ -1,3 +1,19 @@ +/* + * © 2023 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 scorecard import ( @@ -7,29 +23,18 @@ import ( cdx "github.com/CycloneDX/cyclonedx-go" "github.com/jarcoal/httpmock" + "github.com/spdx/tools-golang/spdx" + spdx_2_3 "github.com/spdx/tools-golang/spdx/v2/v2_3" "github.com/stretchr/testify/assert" "github.com/snyk/parlay/lib/sbom" ) -func TestEnrichSBOM(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +const scorecardURL = "https://api.securityscorecards.dev/projects/example.com/repository" - httpmock.RegisterResponder("GET", `=~^https://packages.ecosyste.ms/api/v1/registries`, - func(req *http.Request) (*http.Response, error) { - return httpmock.NewJsonResponse(200, map[string]interface{}{ - "repository_url": "https://example.com/repository", - }) - }) - - httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { - return nil, errors.New("unexpected HTTP request: " + req.URL.String()) - }) - - scorecardUrl := "https://api.securityscorecards.dev/projects/example.com/repository" - httpmock.RegisterResponder("GET", scorecardUrl, - httpmock.NewStringResponder(http.StatusOK, "{}")) +func TestEnrichSBOM_CycloneDX(t *testing.T) { + teardown := setupEcosystemsAPIMock(t) + defer teardown() bom := &cdx.BOM{ Components: &[]cdx.Component{ @@ -48,7 +53,7 @@ func TestEnrichSBOM(t *testing.T) { enrichedComponent := (*bom.Components)[0] assert.NotNil(t, enrichedComponent.ExternalReferences) assert.Len(t, *enrichedComponent.ExternalReferences, 1) - assert.Equal(t, scorecardUrl, (*enrichedComponent.ExternalReferences)[0].URL) + 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) @@ -119,3 +124,59 @@ func TestEnrichSBOM_ErrorFetchingScorecard(t *testing.T) { enrichedComponent := (*bom.Components)[0] assert.Nil(t, enrichedComponent.ExternalReferences) } + +func TestEnrichSBOM_SPDX(t *testing.T) { + teardown := setupEcosystemsAPIMock(t) + defer teardown() + + bom := &spdx.Document{ + Packages: []*spdx_2_3.Package{ + { + PackageExternalReferences: []*spdx_2_3.PackageExternalReference{ + { + Category: "OTHER", + RefType: "purl", + Locator: "pkg:golang/snyk/parlay", + }, + }, + }, + }, + } + doc := &sbom.SBOMDocument{BOM: bom} + + EnrichSBOM(doc) + + pkg := bom.Packages[0] + assert.NotNil(t, pkg.PackageExternalReferences) + assert.Len(t, pkg.PackageExternalReferences, 2) + + scRef := pkg.PackageExternalReferences[1] + assert.Equal(t, scorecardURL, scRef.Locator) + assert.Equal(t, "openssfscorecard", scRef.RefType) + assert.Equal(t, "OTHER", scRef.Category) +} + +func setupEcosystemsAPIMock(t *testing.T) func() { + t.Helper() + + httpmock.Activate() + httpmock.RegisterResponder( + "GET", + "=~^https://packages.ecosyste.ms/api/v1/registries", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "repository_url": "https://example.com/repository", + }) + }, + ) + httpmock.RegisterResponder( + "GET", + scorecardURL, + httpmock.NewStringResponder(http.StatusOK, "{}"), + ) + httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { + return nil, errors.New("unexpected HTTP request: " + req.URL.String()) + }) + + return httpmock.DeactivateAndReset +} diff --git a/lib/snyk/enrich_spdx.go b/lib/snyk/enrich_spdx.go index 10ea0c3..fabcfeb 100644 --- a/lib/snyk/enrich_spdx.go +++ b/lib/snyk/enrich_spdx.go @@ -22,11 +22,11 @@ import ( "net/url" "sync" - "github.com/package-url/packageurl-go" "github.com/remeh/sizedwaitgroup" "github.com/spdx/tools-golang/spdx" spdx_2_3 "github.com/spdx/tools-golang/spdx/v2/v2_3" + "github.com/snyk/parlay/internal/utils" "github.com/snyk/parlay/snyk/issues" ) @@ -42,7 +42,7 @@ func enrichSPDX(bom *spdx.Document) *spdx.Document { for i, pkg := range bom.Packages { wg.Add() go func(pkg *spdx_2_3.Package, i int) { - purl, err := getPurlFromSPDXPackage(pkg) + purl, err := utils.GetPurlFromSPDXPackage(pkg) if err != nil { return } @@ -90,25 +90,3 @@ func enrichSPDX(bom *spdx.Document) *spdx.Document { return bom } - -func getPurlFromSPDXPackage(pkg *spdx_2_3.Package) (*packageurl.PackageURL, error) { - var p string - - for _, ref := range pkg.PackageExternalReferences { - if ref.RefType == "purl" { - p = ref.Locator - break - } - } - - if p == "" { - return nil, fmt.Errorf("no purl on package %s", pkg.PackageName) - } - - purl, err := packageurl.FromString(p) - if err != nil { - return nil, err - } - - return &purl, nil -}