Skip to content
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
16 changes: 13 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/jetstack/tally/internal/db"
"github.com/jetstack/tally/internal/manager"
"github.com/jetstack/tally/internal/output"
"github.com/jetstack/tally/internal/repositories"
"github.com/jetstack/tally/internal/scorecard"
"github.com/jetstack/tally/internal/types"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -81,15 +82,24 @@ var rootCmd = &cobra.Command{
}
defer r.Close()
}
pkgs, err := bom.PackagesFromBOM(r, bom.Format(ro.Format))
sbom, err := bom.ParseBOM(r, bom.Format(ro.Format))
if err != nil {
return err
}
pkgs, err := sbom.Packages()
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Found %d supported packages in BOM\n", len(pkgs))

// Find repositories for the packages from the database
// Map packages to repositories, using various different sources
repoMapper := repositories.From(
repositories.PackageMapper,
repositories.BOMMapper(sbom),
repositories.DBMapper(tallyDB),
)
for i, pkg := range pkgs {
repos, err := tallyDB.GetRepositories(ctx, pkg.System, pkg.Name)
repos, err := repoMapper.Repositories(ctx, pkg)
if err != nil {
if err != db.ErrNotFound {
return err
Expand Down
174 changes: 14 additions & 160 deletions internal/bom/bom.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package bom

import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"

"github.com/CycloneDX/cyclonedx-go"
"github.com/jetstack/tally/internal/types"
"github.com/package-url/packageurl-go"
)

// Format is a supported SBOM format
Expand All @@ -28,168 +24,26 @@ var Formats = []Format{
FormatSyftJSON,
}

var (
ErrUnsupportedPackageType = errors.New("unsupported package type")
)
// BOM is a generic representation of a BOM
type BOM interface {
// Packages retrieves all the supported packages from the BOM
Packages() ([]types.Package, error)

// Repositories retrieves any repository information from the BOM for
// the provided package
Repositories(types.Package) ([]string, error)
}

// PackagesFromBOM extracts packages from a supported SBOM format
func PackagesFromBOM(r io.Reader, format Format) ([]types.Package, error) {
// ParseBOM parses a BOM from a given format
func ParseBOM(r io.Reader, format Format) (BOM, error) {
switch format {
case FormatCycloneDXJSON:
return packagesFromCycloneDX(r, cyclonedx.BOMFileFormatJSON)
return parseCycloneDX(r, cyclonedx.BOMFileFormatJSON)
case FormatCycloneDXXML:
return packagesFromCycloneDX(r, cyclonedx.BOMFileFormatXML)
return parseCycloneDX(r, cyclonedx.BOMFileFormatXML)
case FormatSyftJSON:
return packagesFromSyftJSON(r)
return parseSyftJSON(r)
default:
return nil, fmt.Errorf("unsupported format: %s", format)
}
}

func packagesFromCycloneDX(r io.Reader, format cyclonedx.BOMFileFormat) ([]types.Package, error) {
cdxBOM := &cyclonedx.BOM{}
if err := cyclonedx.NewBOMDecoder(r, format).Decode(cdxBOM); err != nil {
return nil, fmt.Errorf("decoding cyclonedx BOM: %w", err)
}
var (
pkgs []types.Package
components []cyclonedx.Component
)
if cdxBOM.Metadata != nil && cdxBOM.Metadata.Component != nil {
components = append(components, *cdxBOM.Metadata.Component)
}
if cdxBOM.Components != nil {
components = append(components, *cdxBOM.Components...)
}
if err := walkCycloneDXComponents(components, func(component cyclonedx.Component) error {
if component.PackageURL == "" {
return nil
}
purl, err := packageurl.FromString(component.PackageURL)
if err != nil {
return err
}
pkg, err := packageFromPurl(purl)
if errors.Is(err, ErrUnsupportedPackageType) {
return nil
}
if err != nil {
return err
}
if !containsPackage(pkgs, pkg) {
pkgs = append(pkgs, pkg)
}

return nil

}); err != nil {
return nil, err
}

return pkgs, nil
}

func walkCycloneDXComponents(components []cyclonedx.Component, fn func(cyclonedx.Component) error) error {
for _, component := range components {
if err := fn(component); err != nil {
return err
}
if component.Components == nil {
continue
}
if err := walkCycloneDXComponents(*component.Components, fn); err != nil {
return err
}
}

return nil
}

type syftJSON struct {
Artifacts []syftArtifact `json:"artifacts"`
}

type syftArtifact struct {
Purl string `json:"purl"`
}

func packagesFromSyftJSON(r io.Reader) ([]types.Package, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
doc := &syftJSON{}
if err := json.Unmarshal(data, doc); err != nil {
return nil, err
}
var pkgs []types.Package
for _, a := range doc.Artifacts {
if a.Purl == "" {
continue
}
purl, err := packageurl.FromString(a.Purl)
if err != nil {
return nil, err
}
pkg, err := packageFromPurl(purl)
if errors.Is(err, ErrUnsupportedPackageType) {
continue
}
if err != nil {
return nil, err
}
pkgs = append(pkgs, pkg)
}

return pkgs, nil
}

func packageFromPurl(purl packageurl.PackageURL) (types.Package, error) {
switch purl.Type {
case packageurl.TypeNPM:
name := purl.Name
if purl.Namespace != "" {
name = purl.Namespace + "/" + name
}
return types.Package{
System: "NPM",
Name: name,
}, nil
case packageurl.TypeGolang:
name := purl.Name
if purl.Namespace != "" {
name = purl.Namespace + "/" + purl.Name
}
return types.Package{
System: "GO",
Name: name,
}, nil
case packageurl.TypeMaven:
return types.Package{
System: "MAVEN",
Name: strings.Join([]string{purl.Namespace, purl.Name}, ":"),
}, nil
case packageurl.TypePyPi:
return types.Package{
System: "PYPI",
Name: purl.Name,
}, nil
case "cargo":
return types.Package{
System: "CARGO",
Name: purl.Name,
}, nil
default:
return types.Package{}, ErrUnsupportedPackageType
}
}

func containsPackage(pkgs []types.Package, pkg types.Package) bool {
for _, p := range pkgs {
if p.Equals(pkg) {
return true
}
}

return false
}
Loading