Skip to content

Commit

Permalink
feat: add support for CycloneDX XML
Browse files Browse the repository at this point in the history
  • Loading branch information
mcombuechen authored and garethr committed Jun 20, 2023
1 parent 7ceeeb8 commit 32abd52
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 43 deletions.
5 changes: 5 additions & 0 deletions acceptance.bats
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
[ "$status" -eq 0 ]
}

@test "Not fail when testing a CycloneDX XML SBOM" {
run ./parlay ecosystems enrich testing/sbom.cyclonedx.xml
[ "$status" -eq 0 ]
}

@test "Fail when testing a non-existent file" {
run ./parlay ecosystems enrich not-here
[ "$status" -eq 1 ]
Expand Down
19 changes: 8 additions & 11 deletions internal/commands/ecosystems/enrich.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package ecosystems

import (
"bytes"
"os"

cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/rs/zerolog"
"github.com/spf13/cobra"

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

func NewEnrichCommand(logger zerolog.Logger) *cobra.Command {
Expand All @@ -23,17 +22,15 @@ func NewEnrichCommand(logger zerolog.Logger) *cobra.Command {
logger.Fatal().Err(err).Msg("Problem reading input")
}

bom := new(cdx.BOM)
decoder := cdx.NewBOMDecoder(bytes.NewReader(b), cdx.BOMFileFormatJSON)
if err = decoder.Decode(bom); err != nil {
logger.Fatal().Err(err).Msg("Input needs to be a valid CycloneDX SBOM")
doc, err := sbom.DecodeSBOMDocument(b)
if err != nil {
logger.Fatal().Err(err).Msg("Failed to read SBOM input")
}

bom = ecosystems.EnrichSBOM(bom)
err = cdx.NewBOMEncoder(os.Stdout, cdx.BOMFileFormatJSON).Encode(bom)
if err != nil {
// We dont wunt to eat this erorr.
logger.Fatal().Err(err).Msg("Failed to envode new SBOM")
ecosystems.EnrichSBOM(doc)

if err := doc.Encode(os.Stdout); err != nil {
logger.Fatal().Err(err).Msg("Failed to encode new SBOM")
}
},
}
Expand Down
10 changes: 7 additions & 3 deletions lib/ecosystems/enrich.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/lib/sbom"
)

func enrichDescription(component cdx.Component, packageData packages.Package) cdx.Component {
Expand Down Expand Up @@ -198,9 +199,11 @@ func enrichComponentsWithEcosystems(bom *cdx.BOM, enrichFuncs []func(cdx.Compone
bom.Components = &newComponents
}

func EnrichSBOM(bom *cdx.BOM) *cdx.BOM {
func EnrichSBOM(doc *sbom.SBOMDocument) *sbom.SBOMDocument {
bom := doc.BOM

if bom.Components == nil {
return bom
return doc
}

enrichFuncs := []func(cdx.Component, packages.Package) cdx.Component{
Expand All @@ -220,5 +223,6 @@ func EnrichSBOM(bom *cdx.BOM) *cdx.BOM {
}

enrichComponentsWithEcosystems(bom, enrichFuncs)
return bom

return doc
}
61 changes: 32 additions & 29 deletions lib/ecosystems/enrich_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/stretchr/testify/assert"

"github.com/snyk/parlay/ecosystems/packages"
"github.com/snyk/parlay/lib/sbom"
)

func TestEnrichSBOM(t *testing.T) {
Expand All @@ -42,23 +43,23 @@ func TestEnrichSBOM(t *testing.T) {
})
})

bom := new(cdx.BOM)

components := []cdx.Component{
{
BOMRef: "pkg:golang/github.com/CycloneDX/[email protected]",
Type: cdx.ComponentTypeLibrary,
Name: "cyclonedx-go",
Version: "v0.3.0",
PackageURL: "pkg:golang/github.com/CycloneDX/[email protected]",
doc := &sbom.SBOMDocument{
BOM: &cdx.BOM{
Components: &[]cdx.Component{
{
BOMRef: "pkg:golang/github.com/CycloneDX/[email protected]",
Type: cdx.ComponentTypeLibrary,
Name: "cyclonedx-go",
Version: "v0.3.0",
PackageURL: "pkg:golang/github.com/CycloneDX/[email protected]",
},
},
},
}

bom.Components = &components
EnrichSBOM(doc)

bom = EnrichSBOM(bom)

components = *bom.Components
components := *doc.BOM.Components
component := components[0]
licenses := *component.Licenses

Expand All @@ -84,23 +85,23 @@ func TestEnrichSBOMWithoutLicense(t *testing.T) {
})
})

bom := new(cdx.BOM)

components := []cdx.Component{
{
BOMRef: "pkg:golang/github.com/CycloneDX/[email protected]",
Type: cdx.ComponentTypeLibrary,
Name: "cyclonedx-go",
Version: "v0.3.0",
PackageURL: "pkg:golang/github.com/CycloneDX/[email protected]",
doc := &sbom.SBOMDocument{
BOM: &cdx.BOM{
Components: &[]cdx.Component{
{
BOMRef: "pkg:golang/github.com/CycloneDX/[email protected]",
Type: cdx.ComponentTypeLibrary,
Name: "cyclonedx-go",
Version: "v0.3.0",
PackageURL: "pkg:golang/github.com/CycloneDX/[email protected]",
},
},
},
}

bom.Components = &components

bom = EnrichSBOM(bom)
EnrichSBOM(doc)

components = *bom.Components
components := *doc.BOM.Components

assert.Equal(t, "description", components[0].Description)

Expand Down Expand Up @@ -140,9 +141,11 @@ func TestEnrichLicense(t *testing.T) {
}

func TestEnrichBlankSBOM(t *testing.T) {
bom := new(cdx.BOM)
bom = EnrichSBOM(bom)
assert.Nil(t, bom.Components)
doc := &sbom.SBOMDocument{
BOM: new(cdx.BOM),
}
EnrichSBOM(doc)
assert.Nil(t, doc.BOM.Components)
}

func TestEnrichExternalReferenceWithNilURL(t *testing.T) {
Expand Down
39 changes: 39 additions & 0 deletions lib/sbom/cyclonedx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package sbom

import (
"bytes"
"io"

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

func decodeCycloneDX1_4JSON(b []byte) (*cdx.BOM, error) {
return decodeCycloneDX(b, cdx.BOMFileFormatJSON)
}

func decodeCycloneDX1_4XML(b []byte) (*cdx.BOM, error) {
return decodeCycloneDX(b, cdx.BOMFileFormatXML)
}

func decodeCycloneDX(b []byte, f cdx.BOMFileFormat) (*cdx.BOM, error) {
bom := new(cdx.BOM)
decoder := cdx.NewBOMDecoder(bytes.NewReader(b), f)
if err := decoder.Decode(bom); err != nil {
return nil, err
}
return bom, nil
}

func encodeCycloneDX1_4JSON(bom *cdx.BOM) encoderFn {
return encodeCycloneDX(bom, cdx.BOMFileFormatJSON)
}

func encodeCycloneDX1_4XML(bom *cdx.BOM) encoderFn {
return encodeCycloneDX(bom, cdx.BOMFileFormatXML)
}

func encodeCycloneDX(bom *cdx.BOM, f cdx.BOMFileFormat) encoderFn {
return func(w io.Writer) error {
return cdx.NewBOMEncoder(w, f).Encode(bom)
}
}
50 changes: 50 additions & 0 deletions lib/sbom/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package sbom

import (
"bytes"
"errors"
"fmt"
)

func DecodeSBOMDocument(b []byte) (*SBOMDocument, error) {
doc := new(SBOMDocument)

format, err := identifySBOMFormat(b)
if err != nil {
return nil, err
}
doc.Format = format

switch doc.Format {
case SBOMFormatCycloneDX1_4JSON:
bom, err := decodeCycloneDX1_4JSON(b)
if err != nil {
return nil, fmt.Errorf("could not decode input: %w", err)
}
doc.BOM = bom
doc.encode = encodeCycloneDX1_4JSON(doc.BOM)
case SBOMFormatCycloneDX1_4XML:
bom, err := decodeCycloneDX1_4XML(b)
if err != nil {
return nil, fmt.Errorf("could not decode input: %w", err)
}
doc.BOM = bom
doc.encode = encodeCycloneDX1_4XML(doc.BOM)
default:
return nil, fmt.Errorf("no decoder for format %s", doc.Format)
}

return doc, nil
}

func identifySBOMFormat(b []byte) (SBOMFormat, error) {
if bytes.Contains(b, []byte("bomFormat")) && bytes.Contains(b, []byte("CycloneDX")) {
return SBOMFormatCycloneDX1_4JSON, nil
}

if bytes.Contains(b, []byte("xmlns")) && bytes.Contains(b, []byte("cyclonedx")) {
return SBOMFormatCycloneDX1_4XML, nil
}

return "", errors.New("could not identify SBOM format")
}
77 changes: 77 additions & 0 deletions lib/sbom/decode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package sbom

import (
"testing"

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

var (
fixedCycloneDX1_4JSON = []byte(`{"bomFormat":"CycloneDX","specVersion":"1.4","version":1}`)
fixedCycloneDX1_4XML = []byte(`<bom xmlns="http://cyclonedx.org/schema/bom/1.4" version="1"></bom>`)
fixedSPDX2_2JSON = []byte(`{"SPDXID":"SPDXRef-DOCUMENT","spdxVersion":"SPDX-2.2"}`)
)

func TestDecodeSBOMDocument_CycloneDX1_4JSON(t *testing.T) {
doc, err := DecodeSBOMDocument(fixedCycloneDX1_4JSON)
require.NoError(t, err)

assert.Equal(t, SBOMFormatCycloneDX1_4JSON, doc.Format)
assert.NotNil(t, doc.Encode)
assert.IsType(t, &cyclonedx.BOM{}, doc.BOM)
assert.Equal(t, cyclonedx.SpecVersion1_4, doc.BOM.SpecVersion)
}

func TestDecodeSBOMDocument_CycloneDX1_4XML(t *testing.T) {
doc, err := DecodeSBOMDocument(fixedCycloneDX1_4XML)
require.NoError(t, err)

assert.Equal(t, SBOMFormatCycloneDX1_4XML, doc.Format)
assert.NotNil(t, doc.Encode)
assert.IsType(t, &cyclonedx.BOM{}, doc.BOM)
assert.Equal(t, cyclonedx.SpecVersion1_4, doc.BOM.SpecVersion)
}

func TestDecodeSBOMDocument_Unknown(t *testing.T) {
doc, err := DecodeSBOMDocument(fixedSPDX2_2JSON)

assert.ErrorContains(t, err, "could not identify SBOM format")
assert.Nil(t, doc)
}

func Test_identifySBOMFormat(t *testing.T) {
tc := map[string]struct {
input []byte
format string
err string
}{
"CycloneDX 1.4 JSON": {
input: fixedCycloneDX1_4JSON,
format: "CycloneDX 1.4 JSON",
err: "",
},
"CycloneDX 1.4 XML": {
input: fixedCycloneDX1_4XML,
format: "CycloneDX 1.4 XML",
err: "",
},
"Unknown format": {
input: fixedSPDX2_2JSON,
format: "",
err: "could not identify SBOM format",
},
}

for name, tt := range tc {
t.Run(name, func(t *testing.T) {
format, err := identifySBOMFormat(tt.input)

if err != nil {
assert.ErrorContains(t, err, tt.err)
}
assert.Equal(t, tt.format, string(format))
})
}
}
11 changes: 11 additions & 0 deletions lib/sbom/encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sbom

import "io"

type (
SBOMEncoder interface {
Encode(w io.Writer) error
}

encoderFn func(io.Writer) error
)
8 changes: 8 additions & 0 deletions lib/sbom/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package sbom

type SBOMFormat string

const (
SBOMFormatCycloneDX1_4JSON = SBOMFormat("CycloneDX 1.4 JSON")
SBOMFormatCycloneDX1_4XML = SBOMFormat("CycloneDX 1.4 XML")
)
25 changes: 25 additions & 0 deletions lib/sbom/sbom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package sbom

import (
"fmt"
"io"

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

type SBOMDocument struct {
BOM *cdx.BOM
Format SBOMFormat

encode encoderFn
}

var _ SBOMEncoder = (*SBOMDocument)(nil)

func (d *SBOMDocument) Encode(w io.Writer) error {
if d.encode == nil {
return fmt.Errorf("no encoder for format %s", d.Format)
}

return d.encode(w)
}
1 change: 1 addition & 0 deletions testing/sbom.cyclonedx.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" version="1"><metadata><timestamp>2023-06-12T19:17:47Z</timestamp><tools><tool><vendor>Snyk</vendor><name>Snyk Open Source</name></tool></tools><component bom-ref="[email protected]" type="application"><name>package-file-basic</name><version>1.0.0</version><purl>pkg:npm/[email protected]</purl></component></metadata><components><component bom-ref="[email protected]" type="library"><name>debug</name><version>1.0.5</version><purl>pkg:npm/[email protected]</purl></component><component bom-ref="[email protected]" type="library"><name>ms</name><version>2.0.0</version><purl>pkg:npm/[email protected]</purl></component><component bom-ref="[email protected]" type="library"><name>minimatch</name><version>3.0.0</version><purl>pkg:npm/[email protected]</purl></component><component bom-ref="[email protected]" type="library"><name>brace-expansion</name><version>1.1.11</version><purl>pkg:npm/[email protected]</purl></component><component bom-ref="[email protected]" type="library"><name>balanced-match</name><version>1.0.2</version><purl>pkg:npm/[email protected]</purl></component><component bom-ref="[email protected]" type="library"><name>concat-map</name><version>0.0.1</version><purl>pkg:npm/[email protected]</purl></component></components><dependencies><dependency ref="[email protected]"><dependency ref="[email protected]"></dependency><dependency ref="[email protected]"></dependency></dependency><dependency ref="[email protected]"><dependency ref="[email protected]"></dependency></dependency><dependency ref="[email protected]"></dependency><dependency ref="[email protected]"><dependency ref="[email protected]"></dependency></dependency><dependency ref="[email protected]"><dependency ref="[email protected]"></dependency><dependency ref="[email protected]"></dependency></dependency><dependency ref="[email protected]"></dependency><dependency ref="[email protected]"></dependency></dependencies></bom>

0 comments on commit 32abd52

Please sign in to comment.