Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dependencies and purl for Native Image SBOMs #3399

Merged
merged 1 commit into from
Oct 31, 2024
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
142 changes: 45 additions & 97 deletions syft/pkg/cataloger/java/graalvm_native_image_cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"debug/macho"
"debug/pe"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -18,34 +17,14 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/mimetype"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
)

type nativeImageCycloneDX struct {
BomFormat string `json:"bomFormat"`
SpecVersion string `json:"specVersion"`
Version int `json:"version"`
Components []nativeImageComponent `json:"components"`
}

type nativeImageComponent struct {
Type string `json:"type"`
Group string `json:"group"`
Name string `json:"name"`
Version string `json:"version"`
Properties []nativeImageCPE `json:"properties"`
}

type nativeImageCPE struct {
Name string `json:"name"`
Value string `json:"value"`
}

type nativeImage interface {
fetchPkgs() ([]pkg.Package, error)
fetchPkgs() ([]pkg.Package, []artifact.Relationship, error)
}

type nativeImageElf struct {
Expand Down Expand Up @@ -113,80 +92,44 @@ func (c *nativeImageCataloger) Name() string {
return nativeImageCatalogerName
}

// getPackage returns the package given within a NativeImageComponent.
func getPackage(component nativeImageComponent) pkg.Package {
var cpes []cpe.CPE
for _, property := range component.Properties {
c, err := cpe.New(property.Value, cpe.DeclaredSource)
if err != nil {
log.Debugf("unable to parse Attributes: %v", err)
continue
}
cpes = append(cpes, c)
}
return pkg.Package{
Name: component.Name,
Version: component.Version,
Language: pkg.Java,
Type: pkg.GraalVMNativeImagePkg,
FoundBy: nativeImageCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: component.Group,
},
},
CPEs: cpes,
}
}

// decompressSbom returns the packages given within a native image executable's SBOM.
func decompressSbom(dataBuf []byte, sbomStart uint64, lengthStart uint64) ([]pkg.Package, error) {
var pkgs []pkg.Package

func decompressSbom(dataBuf []byte, sbomStart uint64, lengthStart uint64) ([]pkg.Package, []artifact.Relationship, error) {
lengthEnd := lengthStart + 8
bufLen := len(dataBuf)
if lengthEnd > uint64(bufLen) {
return nil, errors.New("the 'sbom_length' symbol overflows the binary")
return nil, nil, errors.New("the 'sbom_length' symbol overflows the binary")
}

length := dataBuf[lengthStart:lengthEnd]
p := bytes.NewBuffer(length)
var storedLength uint64
err := binary.Read(p, binary.LittleEndian, &storedLength)
if err != nil {
return nil, fmt.Errorf("could not read from binary file: %w", err)
return nil, nil, fmt.Errorf("could not read from binary file: %w", err)
}

log.WithFields("len", storedLength).Trace("found java native-image SBOM")
sbomEnd := sbomStart + storedLength
if sbomEnd > uint64(bufLen) {
return nil, errors.New("the sbom symbol overflows the binary")
return nil, nil, errors.New("the sbom symbol overflows the binary")
}

sbomCompressed := dataBuf[sbomStart:sbomEnd]
p = bytes.NewBuffer(sbomCompressed)
gzreader, err := gzip.NewReader(p)
if err != nil {
return nil, fmt.Errorf("could not decompress the java native-image SBOM: %w", err)
}

output, err := io.ReadAll(gzreader)
if err != nil {
return nil, fmt.Errorf("could not read the java native-image SBOM: %w", err)
return nil, nil, fmt.Errorf("could not decompress the java native-image SBOM: %w", err)
}

var sbomContent nativeImageCycloneDX
err = json.Unmarshal(output, &sbomContent)
sbom, _, _, err := cyclonedxjson.NewFormatDecoder().Decode(gzreader)
if err != nil {
return nil, fmt.Errorf("could not unmarshal the java native-image SBOM: %w", err)
return nil, nil, fmt.Errorf("could not unmarshal the java native-image SBOM: %w", err)
}

for _, component := range sbomContent.Components {
p := getPackage(component)
var pkgs []pkg.Package
for p := range sbom.Artifacts.Packages.Enumerate() {
pkgs = append(pkgs, p)
}

return pkgs, nil
return pkgs, sbom.Relationships, nil
}

// fileError logs an error message when an executable cannot be read.
Expand Down Expand Up @@ -294,7 +237,7 @@ func newPE(filename string, r io.ReaderAt) (nativeImage, error) {
}

// fetchPkgs obtains the packages given in the binary.
func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, retErr error) {
func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, relationships []artifact.Relationship, retErr error) {
defer func() {
if r := recover(); r != nil {
// this can happen in cases where a malformed binary is passed in can be initially parsed, but not
Expand All @@ -310,10 +253,10 @@ func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, retErr error) {

si, err := bi.Symbols()
if err != nil {
return nil, fmt.Errorf("no symbols found in binary: %w", err)
return nil, nil, fmt.Errorf("no symbols found in binary: %w", err)
}
if si == nil {
return nil, errors.New(nativeImageMissingSymbolsError)
return nil, nil, errors.New(nativeImageMissingSymbolsError)
}
for _, s := range si {
switch s.Name {
Expand All @@ -326,16 +269,16 @@ func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, retErr error) {
}
}
if sbom.Value == 0 || sbomLength.Value == 0 || svmVersion.Value == 0 {
return nil, errors.New(nativeImageMissingSymbolsError)
return nil, nil, errors.New(nativeImageMissingSymbolsError)
}
dataSection := bi.Section(".data")
if dataSection == nil {
return nil, fmt.Errorf("no .data section found in binary: %w", err)
return nil, nil, fmt.Errorf("no .data section found in binary: %w", err)
}
dataSectionBase := dataSection.SectionHeader.Addr
data, err := dataSection.Data()
if err != nil {
return nil, fmt.Errorf("cannot read the .data section: %w", err)
return nil, nil, fmt.Errorf("cannot read the .data section: %w", err)
}
sbomLocation := sbom.Value - dataSectionBase
lengthLocation := sbomLength.Value - dataSectionBase
Expand All @@ -344,7 +287,7 @@ func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, retErr error) {
}

// fetchPkgs obtains the packages from a Native Image given as a Mach O file.
func (ni nativeImageMachO) fetchPkgs() (pkgs []pkg.Package, retErr error) {
func (ni nativeImageMachO) fetchPkgs() (pkgs []pkg.Package, relationships []artifact.Relationship, retErr error) {
defer func() {
if r := recover(); r != nil {
// this can happen in cases where a malformed binary is passed in can be initially parsed, but not
Expand All @@ -359,7 +302,7 @@ func (ni nativeImageMachO) fetchPkgs() (pkgs []pkg.Package, retErr error) {

bi := ni.file
if bi.Symtab == nil {
return nil, errors.New(nativeImageMissingSymbolsError)
return nil, nil, errors.New(nativeImageMissingSymbolsError)
}
for _, s := range bi.Symtab.Syms {
switch s.Name {
Expand All @@ -372,17 +315,17 @@ func (ni nativeImageMachO) fetchPkgs() (pkgs []pkg.Package, retErr error) {
}
}
if sbom.Value == 0 || sbomLength.Value == 0 || svmVersion.Value == 0 {
return nil, errors.New(nativeImageMissingSymbolsError)
return nil, nil, errors.New(nativeImageMissingSymbolsError)
}

dataSegment := bi.Segment("__DATA")
if dataSegment == nil {
return nil, nil
return nil, nil, nil
}
dataBuf, err := dataSegment.Data()
if err != nil {
log.Tracef("cannot obtain buffer from data segment")
return nil, nil
return nil, nil, nil
}
sbomLocation := sbom.Value - dataSegment.Addr
lengthLocation := sbomLength.Value - dataSegment.Addr
Expand Down Expand Up @@ -494,7 +437,7 @@ func (ni nativeImagePE) fetchSbomSymbols(content *exportContentPE) {
}

// fetchPkgs obtains the packages from a Native Image given as a PE file.
func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, retErr error) {
func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, relationships []artifact.Relationship, retErr error) {
defer func() {
if r := recover(); r != nil {
// this can happen in cases where a malformed binary is passed in can be initially parsed, but not
Expand All @@ -506,32 +449,32 @@ func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, retErr error) {
content, err := ni.fetchExportContent()
if err != nil {
log.Debugf("could not fetch the content of the export directory entry: %v", err)
return nil, err
return nil, nil, err
}
ni.fetchSbomSymbols(content)
if content.addressOfSbom == uint32(0) || content.addressOfSbomLength == uint32(0) || content.addressOfSvmVersion == uint32(0) {
return nil, errors.New(nativeImageMissingSymbolsError)
return nil, nil, errors.New(nativeImageMissingSymbolsError)
}
functionsBase := content.addressOfFunctions - ni.exportSymbols.VirtualAddress
sbomOffset := content.addressOfSbom
sbomAddress, err := ni.fetchExportFunctionPointer(functionsBase, sbomOffset)
if err != nil {
return nil, fmt.Errorf("could not fetch SBOM pointer from exported functions: %w", err)
return nil, nil, fmt.Errorf("could not fetch SBOM pointer from exported functions: %w", err)
}
sbomLengthOffset := content.addressOfSbomLength
sbomLengthAddress, err := ni.fetchExportFunctionPointer(functionsBase, sbomLengthOffset)
if err != nil {
return nil, fmt.Errorf("could not fetch SBOM length pointer from exported functions: %w", err)
return nil, nil, fmt.Errorf("could not fetch SBOM length pointer from exported functions: %w", err)
}
bi := ni.file
dataSection := bi.Section(".data")
if dataSection == nil {
return nil, nil
return nil, nil, nil
}
dataBuf, err := dataSection.Data()
if err != nil {
log.Tracef("cannot obtain buffer from the java native-image .data section")
return nil, nil
return nil, nil, nil
}
sbomLocation := sbomAddress - dataSection.VirtualAddress
lengthLocation := sbomLengthAddress - dataSection.VirtualAddress
Expand All @@ -540,16 +483,17 @@ func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, retErr error) {
}

// fetchPkgs provides the packages available in a UnionReader.
func fetchPkgs(reader unionreader.UnionReader, filename string) []pkg.Package {
func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package, []artifact.Relationship) {
var pkgs []pkg.Package
var relationships []artifact.Relationship
imageFormats := []func(string, io.ReaderAt) (nativeImage, error){newElf, newMachO, newPE}

// NOTE: multiple readers are returned to cover universal binaries, which are files
// with more than one binary
readers, err := unionreader.GetReaders(reader)
if err != nil {
log.Debugf("failed to open the java native-image binary: %v", err)
return nil
return nil, nil
}
for _, r := range readers {
for _, makeNativeImage := range imageFormats {
Expand All @@ -560,47 +504,51 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) []pkg.Package {
if ni == nil {
continue
}
newPkgs, err := ni.fetchPkgs()
newPkgs, newRelationships, err := ni.fetchPkgs()
if err != nil {
log.Tracef("unable to extract SBOM from possible java native-image %s: %v", filename, err)
continue
}
pkgs = append(pkgs, newPkgs...)
relationships = append(relationships, newRelationships...)
}
}
return pkgs
return pkgs, relationships
}

// Catalog attempts to find any native image executables reachable from a resolver.
func (c *nativeImageCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
var relationships []artifact.Relationship
fileMatches, err := resolver.FilesByMIMEType(mimetype.ExecutableMIMETypeSet.List()...)
if err != nil {
return pkgs, nil, fmt.Errorf("failed to find binaries by mime types: %w", err)
}

for _, location := range fileMatches {
newPkgs, err := processLocation(location, resolver)
newPkgs, newRelationships, err := processLocation(location, resolver)
if err != nil {
return nil, nil, err
}
pkgs = append(pkgs, newPkgs...)
relationships = append(relationships, newRelationships...)
}

return pkgs, nil, nil
return pkgs, relationships, nil
}

func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Package, error) {
func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
readerCloser, err := resolver.FileContentsByLocation(location)
if err != nil {
log.Debugf("error opening file: %v", err)
return nil, nil
return nil, nil, nil
}
defer internal.CloseAndLogError(readerCloser, location.RealPath)

reader, err := unionreader.GetUnionReader(readerCloser)
if err != nil {
return nil, err
return nil, nil, err
}
return fetchPkgs(reader, location.RealPath), nil
pkgs, relationships := fetchPkgs(reader, location.RealPath)
return pkgs, relationships, nil
}
Loading
Loading