Skip to content

Commit

Permalink
feat(sbom): better support for third-party SBOMs (#3262)
Browse files Browse the repository at this point in the history
Co-authored-by: knqyf263 <knqyf263@gmail.com>
  • Loading branch information
masahiro331 and knqyf263 committed Dec 15, 2022
1 parent e879b06 commit bbccb44
Show file tree
Hide file tree
Showing 19 changed files with 342 additions and 108 deletions.
6 changes: 2 additions & 4 deletions pkg/fanal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type analyzer interface {
type configAnalyzer interface {
Type() Type
Version() int
Analyze(targetOS types.OS, content []byte) ([]types.Package, error)
Analyze(targetOS types.OS, content []byte) (types.Packages, error)
Required(osFound types.OS) bool
}

Expand Down Expand Up @@ -179,9 +179,7 @@ func (r *AnalysisResult) Sort() {
})

for _, pi := range r.PackageInfos {
sort.Slice(pi.Packages, func(i, j int) bool {
return pi.Packages[i].Name < pi.Packages[j].Name
})
sort.Sort(pi.Packages)
}

// Language-specific packages
Expand Down
2 changes: 1 addition & 1 deletion pkg/fanal/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (mockConfigAnalyzer) Required(targetOS types.OS) bool {
return targetOS.Family == "alpine"
}

func (mockConfigAnalyzer) Analyze(targetOS types.OS, configBlob []byte) ([]types.Package, error) {
func (mockConfigAnalyzer) Analyze(targetOS types.OS, configBlob []byte) (types.Packages, error) {
if string(configBlob) != `foo` {
return nil, errors.New("error")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/fanal/analyzer/command/apk/apk.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type pkg struct {

type version map[string]int

func (a alpineCmdAnalyzer) Analyze(targetOS types.OS, configBlob []byte) ([]types.Package, error) {
func (a alpineCmdAnalyzer) Analyze(targetOS types.OS, configBlob []byte) (types.Packages, error) {
var apkIndexArchive *apkIndex
var err error
if apkIndexArchive, err = a.fetchApkIndexArchive(targetOS); err != nil {
Expand Down
6 changes: 2 additions & 4 deletions pkg/fanal/analyzer/command/apk/apk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestAnalyze(t *testing.T) {
var tests = map[string]struct {
args args
apkIndexArchivePath string
expected []types.Package
expected types.Packages
}{
"old": {
args: args{
Expand Down Expand Up @@ -306,9 +306,7 @@ func TestAnalyze(t *testing.T) {
t.Run(testName, func(t *testing.T) {
apkIndexArchiveURL = v.apkIndexArchivePath
actual, _ := analyzer.Analyze(v.args.targetOS, v.args.configBlob)
sort.Slice(actual, func(i, j int) bool {
return actual[i].Name < actual[j].Name
})
sort.Sort(actual)
assert.Equal(t, v.expected, actual)
})
}
Expand Down
12 changes: 1 addition & 11 deletions pkg/fanal/analyzer/pkg/dpkg/dpkg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ func Test_dpkgAnalyzer_Analyze(t *testing.T) {

// Sort the result for consistency
for i := range got.PackageInfos {
got.PackageInfos[i].Packages = sortPkgs(got.PackageInfos[i].Packages)
sort.Sort(got.PackageInfos[i].Packages)
}

assert.Equal(t, tt.wantErr, err != nil, err)
Expand All @@ -1037,16 +1037,6 @@ func Test_dpkgAnalyzer_Analyze(t *testing.T) {
}
}

func sortPkgs(pkgs []types.Package) []types.Package {
sort.Slice(pkgs, func(i, j int) bool {
if pkgs[i].Name != pkgs[j].Name {
return pkgs[i].Name < pkgs[j].Name
}
return pkgs[i].Version < pkgs[j].Version
})
return pkgs
}

func Test_dpkgAnalyzer_Required(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion pkg/fanal/analyzer/pkg/rpm/rpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (a rpmPkgAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput)
}, nil
}

func (a rpmPkgAnalyzer) parsePkgInfo(rc io.Reader) ([]types.Package, []string, error) {
func (a rpmPkgAnalyzer) parsePkgInfo(rc io.Reader) (types.Packages, []string, error) {
filePath, err := writeToTempFile(rc)
if err != nil {
return nil, nil, xerrors.Errorf("temp file error: %w", err)
Expand Down
10 changes: 3 additions & 7 deletions pkg/fanal/analyzer/pkg/rpm/rpm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func TestParseRpmInfo(t *testing.T) {
var tests = map[string]struct {
path string
pkgs []types.Package
pkgs types.Packages
}{
"Valid": {
path: "./testdata/valid",
Expand Down Expand Up @@ -588,12 +588,8 @@ func TestParseRpmInfo(t *testing.T) {
got, _, err := a.parsePkgInfo(f)
require.NoError(t, err)

sort.Slice(tc.pkgs, func(i, j int) bool {
return tc.pkgs[i].Name < tc.pkgs[j].Name
})
sort.Slice(got, func(i, j int) bool {
return got[i].Name < got[j].Name
})
sort.Sort(tc.pkgs)
sort.Sort(got)

for i := range got {
got[i].ID = ""
Expand Down
4 changes: 1 addition & 3 deletions pkg/fanal/applier/applier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -691,9 +691,7 @@ func TestApplier_ApplyLayers(t *testing.T) {
require.NoError(t, err, tt.name)
}

sort.Slice(got.Packages, func(i, j int) bool {
return got.Packages[i].Name < got.Packages[j].Name
})
sort.Sort(got.Packages)
for _, app := range got.Applications {
sort.Slice(app.Libraries, func(i, j int) bool {
return app.Libraries[i].Name < app.Libraries[j].Name
Expand Down
4 changes: 1 addition & 3 deletions pkg/fanal/applier/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,9 +771,7 @@ func TestApplyLayers(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := applier.ApplyLayers(tt.inputLayers)
sort.Slice(got.Packages, func(i, j int) bool {
return got.Packages[i].Name < got.Packages[j].Name
})
sort.Sort(got.Packages)
sort.Slice(got.Applications, func(i, j int) bool {
return got.Applications[i].FilePath < got.Applications[j].FilePath
})
Expand Down
4 changes: 2 additions & 2 deletions pkg/fanal/test/integration/containerd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ func localImageTestWithNamespace(t *testing.T, namespace string) {
require.NoError(t, err)
defer golden.Close()

var wantPkgs []types.Package
var wantPkgs types.Packages
err = json.NewDecoder(golden).Decode(&wantPkgs)
require.NoError(t, err)

Expand Down Expand Up @@ -768,7 +768,7 @@ func TestContainerd_PullImage(t *testing.T) {
golden, err := os.Open(fmt.Sprintf("testdata/goldens/packages/%s.json.golden", tag))
require.NoError(t, err)

var wantPkgs []types.Package
var wantPkgs types.Packages
err = json.NewDecoder(golden).Decode(&wantPkgs)
require.NoError(t, err)

Expand Down
9 changes: 2 additions & 7 deletions pkg/fanal/test/integration/library_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,7 @@ func commonChecks(t *testing.T, detail types.ArtifactDetail, tc testCase) {

func checkOSPackages(t *testing.T, detail types.ArtifactDetail, tc testCase) {
// Sort OS packages for consistency
sort.Slice(detail.Packages, func(i, j int) bool {
if detail.Packages[i].Name != detail.Packages[j].Name {
return detail.Packages[i].Name < detail.Packages[j].Name
}
return detail.Packages[i].Version < detail.Packages[j].Version
})
sort.Sort(detail.Packages)

splitted := strings.Split(tc.remoteImageName, ":")
goldenFile := fmt.Sprintf("testdata/goldens/packages/%s.json.golden", splitted[len(splitted)-1])
Expand All @@ -277,7 +272,7 @@ func checkOSPackages(t *testing.T, detail types.ArtifactDetail, tc testCase) {

require.Equal(t, len(expectedPkgs), len(detail.Packages), tc.name)
sort.Slice(expectedPkgs, func(i, j int) bool { return expectedPkgs[i].Name < expectedPkgs[j].Name })
sort.Slice(detail.Packages, func(i, j int) bool { return detail.Packages[i].Name < detail.Packages[j].Name })
sort.Sort(detail.Packages)

for i := 0; i < len(expectedPkgs); i++ {
require.Equal(t, expectedPkgs[i].Name, detail.Packages[i].Name, tc.name)
Expand Down
24 changes: 22 additions & 2 deletions pkg/fanal/types/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ func (pkg *Package) Empty() bool {
return pkg.Name == "" || pkg.Version == ""
}

type Packages []Package

func (pkgs Packages) Len() int {
return len(pkgs)
}

func (pkgs Packages) Swap(i, j int) {
pkgs[i], pkgs[j] = pkgs[j], pkgs[i]
}

func (pkgs Packages) Less(i, j int) bool {
switch {
case pkgs[i].Name != pkgs[j].Name:
return pkgs[i].Name < pkgs[j].Name
case pkgs[i].Version != pkgs[j].Version:
return pkgs[i].Version < pkgs[j].Version
}
return pkgs[i].FilePath < pkgs[j].FilePath
}

type SrcPackage struct {
Name string `json:"name"`
Version string `json:"version"`
Expand All @@ -80,7 +100,7 @@ type SrcPackage struct {

type PackageInfo struct {
FilePath string
Packages []Package
Packages Packages
}

type Application struct {
Expand Down Expand Up @@ -198,7 +218,7 @@ func (b *BlobInfo) ToArtifactDetail() ArtifactDetail {
type ArtifactDetail struct {
OS *OS `json:",omitempty"`
Repository *Repository `json:",omitempty"`
Packages []Package `json:",omitempty"`
Packages Packages `json:",omitempty"`
Applications []Application `json:",omitempty"`
Misconfigurations []Misconfiguration `json:",omitempty"`
Secrets []Secret `json:",omitempty"`
Expand Down
23 changes: 13 additions & 10 deletions pkg/purl/purl.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import (
packageurl "github.com/package-url/packageurl-go"
"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer/os"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/scanner/utils"
"github.com/aquasecurity/trivy/pkg/types"
)

const (
TypeAPK = "apk" // not defined in github.com/package-url/packageurl-go
TypeOCI = "oci"
)

Expand All @@ -28,7 +28,7 @@ type PackageURL struct {
func FromString(purl string) (*PackageURL, error) {
p, err := packageurl.FromString(purl)
if err != nil {
return nil, xerrors.Errorf("failed to parse purl: %w", err)
return nil, xerrors.Errorf("failed to parse purl(%s): %w", purl, err)
}

return &PackageURL{
Expand Down Expand Up @@ -57,10 +57,9 @@ func (p *PackageURL) Package() *ftypes.Package {
pkg.Epoch = rpmVer.Epoch()
}

// TODO: replace with packageurl.TypeApk once they add it.
// Return of packages without Namespace.
// OS packages does not have namespace.
if p.Namespace == "" || p.Type == packageurl.TypeRPM || p.Type == packageurl.TypeDebian || p.Type == string(analyzer.TypeApk) {
// Return packages without namespace.
// OS packages are not supposed to have namespace.
if p.Namespace == "" || p.IsOSPkg() {
return pkg
}

Expand All @@ -76,8 +75,8 @@ func (p *PackageURL) Package() *ftypes.Package {
return pkg
}

// AppType returns an application type in Trivy
func (p *PackageURL) AppType() string {
// PackageType returns an application type in Trivy
func (p *PackageURL) PackageType() string {
switch p.Type {
case packageurl.TypeComposer:
return ftypes.Composer
Expand All @@ -101,6 +100,10 @@ func (p *PackageURL) AppType() string {
return p.Type
}

func (p *PackageURL) IsOSPkg() bool {
return p.Type == TypeAPK || p.Type == packageurl.TypeDebian || p.Type == packageurl.TypeRPM
}

func (p *PackageURL) BOMRef() string {
// 'bom-ref' must be unique within BOM, but PURLs may conflict
// when the same packages are installed in an artifact.
Expand Down Expand Up @@ -141,7 +144,7 @@ func NewPackageURL(t string, metadata types.Metadata, pkg ftypes.Package) (Packa
if metadata.OS != nil {
namespace = metadata.OS.Family
}
case string(analyzer.TypeApk): // TODO: replace with packageurl.TypeApk once they add it.
case TypeAPK: // TODO: replace with packageurl.TypeApk once they add it.
qualifiers = append(qualifiers, parseApk(metadata.OS)...)
if metadata.OS != nil {
namespace = metadata.OS.Family
Expand Down Expand Up @@ -306,7 +309,7 @@ func purlType(t string) string {
case ftypes.Cocoapods:
return packageurl.TypeSwift
case os.Alpine:
return string(analyzer.TypeApk)
return TypeAPK
case os.Debian, os.Ubuntu:
return packageurl.TypeDebian
case os.RedHat, os.CentOS, os.Rocky, os.Alma,
Expand Down
8 changes: 5 additions & 3 deletions pkg/sbom/cyclonedx/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import (
)

const (
Namespace = "aquasecurity:trivy:"
ToolVendor = "aquasecurity"
ToolName = "trivy"
Namespace = ToolVendor + ":" + ToolName + ":"

PropertySchemaVersion = "SchemaVersion"
PropertyType = "Type"
Expand Down Expand Up @@ -169,8 +171,8 @@ func (e *Marshaler) cdxMetadata() *cdx.Metadata {
Timestamp: e.clock.Now().UTC().Format(timeLayout),
Tools: &[]cdx.Tool{
{
Vendor: "aquasecurity",
Name: "trivy",
Vendor: ToolVendor,
Name: ToolName,
Version: e.appVersion,
},
},
Expand Down
36 changes: 36 additions & 0 deletions pkg/sbom/cyclonedx/testdata/happy/third-party-bom-no-os.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b",
"version": 1,
"metadata": {
"timestamp": "2022-05-28T10:20:03.79527Z",
"component": {
"bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a",
"type": "container",
"name": "test-project"
}
},
"components": [
{
"bom-ref": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
"type": "library",
"name": "musl",
"version": "1.2.3-r0",
"licenses": [
{
"expression": "MIT"
}
],
"purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0"
},
{
"bom-ref": "pkg:composer/pear/log@1.13.1",
"type": "library",
"name": "pear/log",
"version": "1.13.1",
"purl": "pkg:composer/pear/log@1.13.1"
}
],
"vulnerabilities": []
}
Loading

0 comments on commit bbccb44

Please sign in to comment.