Skip to content

Commit

Permalink
feat(sbom): Add marshal for spdx (#2867)
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 Sep 14, 2022
1 parent dac2b4a commit 3165c37
Show file tree
Hide file tree
Showing 4 changed files with 1,110 additions and 565 deletions.
178 changes: 20 additions & 158 deletions pkg/report/spdx/spdx.go
@@ -1,185 +1,47 @@
package spdx

import (
"fmt"
"io"
"strings"
"time"

"github.com/google/uuid"
"github.com/mitchellh/hashstructure/v2"
"github.com/spdx/tools-golang/jsonsaver"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/tvsaver"
"golang.org/x/xerrors"
"k8s.io/utils/clock"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/sbom/spdx"
"github.com/aquasecurity/trivy/pkg/types"
)

const (
SPDXVersion = "SPDX-2.2"
DataLicense = "CC0-1.0"
SPDXIdentifier = "DOCUMENT"
DocumentNamespace = "http://aquasecurity.github.io/trivy"
CreatorOrganization = "aquasecurity"
CreatorTool = "trivy"
)

type Hash func(v interface{}, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error)

type Writer struct {
output io.Writer
version string
*options
}

type newUUID func() uuid.UUID

type options struct {
format spdx.Document2_1
clock clock.Clock
newUUID newUUID
hasher Hash
spdxFormat string
}

type option func(*options)

type spdxSaveFunction func(*spdx.Document2_2, io.Writer) error

func WithClock(clock clock.Clock) option {
return func(opts *options) {
opts.clock = clock
}
}

func WithNewUUID(newUUID newUUID) option {
return func(opts *options) {
opts.newUUID = newUUID
}
}

func WithHasher(hasher Hash) option {
return func(opts *options) {
opts.hasher = hasher
}
output io.Writer
version string
format string
marshaler *spdx.Marshaler
}

func NewWriter(output io.Writer, version string, spdxFormat string, opts ...option) Writer {
o := &options{
format: spdx.Document2_1{},
clock: clock.RealClock{},
newUUID: uuid.New,
hasher: hashstructure.Hash,
spdxFormat: spdxFormat,
}

for _, opt := range opts {
opt(o)
}

func NewWriter(output io.Writer, version string, spdxFormat string) Writer {
return Writer{
output: output,
version: version,
options: o,
output: output,
version: version,
format: spdxFormat,
marshaler: spdx.NewMarshaler(),
}
}

func (cw Writer) Write(report types.Report) error {
spdxDoc, err := cw.convertToBom(report, cw.version)
func (w Writer) Write(report types.Report) error {
spdxDoc, err := w.marshaler.Marshal(report)
if err != nil {
return xerrors.Errorf("failed to convert bom: %w", err)
return xerrors.Errorf("failed to marshal spdx: %w", err)
}

var saveFunc spdxSaveFunction
if cw.spdxFormat != "spdx-json" {
saveFunc = tvsaver.Save2_2
if w.format == "spdx-json" {
if err := jsonsaver.Save2_2(spdxDoc, w.output); err != nil {
return xerrors.Errorf("failed to save spdx json: %w", err)
}
} else {
saveFunc = jsonsaver.Save2_2
}

if err = saveFunc(spdxDoc, cw.output); err != nil {
return xerrors.Errorf("failed to save bom: %w", err)
}
return nil
}

func (cw *Writer) convertToBom(r types.Report, version string) (*spdx.Document2_2, error) {
packages := make(map[spdx.ElementID]*spdx.Package2_2)

for _, result := range r.Results {
for _, pkg := range result.Packages {
spdxPackage, err := cw.pkgToSpdxPackage(pkg)
if err != nil {
return nil, xerrors.Errorf("failed to parse pkg: %w", err)
}
packages[spdxPackage.PackageSPDXIdentifier] = &spdxPackage
if err := tvsaver.Save2_2(spdxDoc, w.output); err != nil {
return xerrors.Errorf("failed to save spdx tag-value: %w", err)
}
}

return &spdx.Document2_2{
CreationInfo: &spdx.CreationInfo2_2{
SPDXVersion: SPDXVersion,
DataLicense: DataLicense,
SPDXIdentifier: SPDXIdentifier,
DocumentName: r.ArtifactName,
DocumentNamespace: getDocumentNamespace(r, cw),
CreatorOrganizations: []string{CreatorOrganization},
CreatorTools: []string{CreatorTool},
Created: cw.clock.Now().UTC().Format(time.RFC3339Nano),
},
Packages: packages,
}, nil
}

func (cw *Writer) pkgToSpdxPackage(pkg ftypes.Package) (spdx.Package2_2, error) {
var spdxPackage spdx.Package2_2
license := getLicense(pkg)

pkgID, err := getPackageID(cw.hasher, pkg)
if err != nil {
return spdx.Package2_2{}, xerrors.Errorf("failed to get %s package ID: %w", pkg.Name, err)
}

spdxPackage.PackageSPDXIdentifier = spdx.ElementID(pkgID)
spdxPackage.PackageName = pkg.Name
spdxPackage.PackageVersion = pkg.Version

// The Declared License is what the authors of a project believe govern the package
spdxPackage.PackageLicenseConcluded = license

// The Concluded License field is the license the SPDX file creator believes governs the package
spdxPackage.PackageLicenseDeclared = license

return spdxPackage, nil
}

func getLicense(p ftypes.Package) string {
if len(p.Licenses) == 0 {
return "NONE"
}

return strings.Join(p.Licenses, ", ")
}

func getDocumentNamespace(r types.Report, cw *Writer) string {
return DocumentNamespace + "/" + string(r.ArtifactType) + "/" + r.ArtifactName + "-" + cw.newUUID().String()
}

func getPackageID(h Hash, p ftypes.Package) (string, error) {
// Not use these values for the hash
p.Layer = ftypes.Layer{}
p.FilePath = ""

f, err := h(p, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
SlicesAsSets: true,
})
if err != nil {
return "", xerrors.Errorf("could not build package ID for package=%+v: %+v", p, err)
}

return fmt.Sprintf("%x", f), nil
return nil
}

0 comments on commit 3165c37

Please sign in to comment.