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
2 changes: 2 additions & 0 deletions cmd/syft/internal/options/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/github"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
Expand Down Expand Up @@ -127,6 +128,7 @@ func supportedIDs() []sbom.FormatID {
table.ID,
text.ID,
template.ID,
purls.ID,

// encoders that support multiple versions
cyclonedxxml.ID,
Expand Down
3 changes: 3 additions & 0 deletions syft/format/common/spdxhelpers/to_syft_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/linux"
Expand Down Expand Up @@ -509,6 +510,8 @@ func toSyftPackage(p *spdx.Package) pkg.Package {
Metadata: extractMetadata(p, info),
}

internal.Backfill(sP)

if p.PackageSPDXIdentifier != "" {
// always prefer the IDs from the SBOM over derived IDs
sP.OverrideID(artifact.ID(p.PackageSPDXIdentifier))
Expand Down
4 changes: 4 additions & 0 deletions syft/format/cyclonedxjson/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
return nil, "", "", fmt.Errorf("unsupported cyclonedx json document version")
}

_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX JSON SBOM: %+v", err)
}
doc, err := d.decoder.Decode(reader)
if err != nil {
return nil, id, version, fmt.Errorf("unable to decode cyclonedx json document: %w", err)
Expand Down
4 changes: 4 additions & 0 deletions syft/format/cyclonedxjson/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cyclonedxjson

import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -43,6 +44,7 @@ func TestDecoder_Decode(t *testing.T) {
t.Run(test.file, func(t *testing.T) {
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
require.NoError(t, err)
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }

dec := NewFormatDecoder()

Expand All @@ -51,6 +53,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, sbom.FormatID(""), formatID)
assert.Equal(t, "", formatVersion)

reset()
_, decodeID, decodeVersion, err := dec.Decode(reader)
require.Error(t, err)
assert.Equal(t, sbom.FormatID(""), decodeID)
Expand All @@ -61,6 +64,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, ID, formatID)
assert.NotEmpty(t, formatVersion)

reset()
bom, decodeID, decodeVersion, err := dec.Decode(reader)
require.NotNil(t, bom)
require.NoError(t, err)
Expand Down
4 changes: 4 additions & 0 deletions syft/format/cyclonedxxml/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
return nil, "", "", fmt.Errorf("unsupported cyclonedx xml document version")
}

_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX XML SBOM: %w", err)
}
doc, err := d.decoder.Decode(reader)
if err != nil {
return nil, id, version, fmt.Errorf("unable to decode cyclonedx xml document: %w", err)
Expand Down
5 changes: 5 additions & 0 deletions syft/format/cyclonedxxml/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cyclonedxxml

import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -44,13 +45,16 @@ func TestDecoder_Decode(t *testing.T) {
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
require.NoError(t, err)

reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }

dec := NewFormatDecoder()

formatID, formatVersion := dec.Identify(reader)
if test.err {
assert.Equal(t, sbom.FormatID(""), formatID)
assert.Equal(t, "", formatVersion)

reset()
_, decodeID, decodeVersion, err := dec.Decode(reader)
require.Error(t, err)
assert.Equal(t, sbom.FormatID(""), decodeID)
Expand All @@ -61,6 +65,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, ID, formatID)
assert.NotEmpty(t, formatVersion)

reset()
bom, decodeID, decodeVersion, err := dec.Decode(reader)
require.NotNil(t, bom)
require.NoError(t, err)
Expand Down
2 changes: 2 additions & 0 deletions syft/format/decoders.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
Expand All @@ -24,6 +25,7 @@ func Decoders() []sbom.FormatDecoder {
cyclonedxjson.NewFormatDecoder(),
spdxtagvalue.NewFormatDecoder(),
spdxjson.NewFormatDecoder(),
purls.NewFormatDecoder(),
}
}

Expand Down
12 changes: 12 additions & 0 deletions syft/format/decoders_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ func (c *DecoderCollection) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, stri

var bestID sbom.FormatID
for _, d := range c.decoders {
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
id, version := d.Identify(reader)
if id == "" || version == "" {
if id != "" {
Expand All @@ -42,6 +46,10 @@ func (c *DecoderCollection) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, stri
continue
}

_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
return d.Decode(reader)
}

Expand All @@ -65,6 +73,10 @@ func (c *DecoderCollection) Identify(r io.Reader) (sbom.FormatID, string) {
}

for _, d := range c.decoders {
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
log.Debugf("unable to seek to start of SBOM: %v", err)
}
id, version := d.Identify(reader)
if id != "" && version != "" {
return id, version
Expand Down
2 changes: 2 additions & 0 deletions syft/format/encoders.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/github"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
Expand Down Expand Up @@ -62,6 +63,7 @@ func (o EncodersConfig) Encoders() ([]sbom.FormatEncoder, error) {
l.addWithErr(syftjson.ID)(o.syftJSONEncoders())
l.add(table.ID)(table.NewFormatEncoder())
l.add(text.ID)(text.NewFormatEncoder())
l.add(purls.ID)(purls.NewFormatEncoder())
l.add(github.ID)(github.NewFormatEncoder())
l.addWithErr(cyclonedxxml.ID)(o.cyclonedxXMLEncoders())
l.addWithErr(cyclonedxjson.ID)(o.cyclonedxJSONEncoders())
Expand Down
2 changes: 2 additions & 0 deletions syft/format/encoders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func expectedDefaultEncoders() *strset.Set {
expected.Add("syft-table@") // no version
expected.Add("syft-text@") // no version
expected.Add("github-json@") // no version
expected.Add("purls@") // no version
for _, v := range spdxjson.SupportedVersions() {
expected.Add("spdx-json@" + v)
}
Expand Down Expand Up @@ -108,6 +109,7 @@ func TestEncodersConfig_Encoders(t *testing.T) {
expected.Add("syft-table@") // no version
expected.Add("syft-text@") // no version
expected.Add("github-json@") // no version
expected.Add("purls@") // no version
expected.Add("spdx-json@" + spdxutil.DefaultVersion)
expected.Add("spdx-tag-value@" + spdxutil.DefaultVersion)
expected.Add("cyclonedx-json@" + cyclonedxutil.DefaultVersion)
Expand Down
137 changes: 137 additions & 0 deletions syft/format/internal/backfill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package internal

import (
"fmt"
"regexp"
"slices"
"strings"

"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)

// Backfill takes all information present in the package and attempts to fill in any missing information
// from any available sources, such as the Metadata and PURL.
//
// Backfill does not call p.SetID(), but this needs to be called later to ensure it's up to date
func Backfill(p *pkg.Package) {
if p.PURL == "" {
return
}

purl, err := packageurl.FromString(p.PURL)
if err != nil {
log.Debug("unable to parse purl: %s: %w", p.PURL, err)
return
}

var cpes []cpe.CPE
epoch := ""

for _, qualifier := range purl.Qualifiers {
switch qualifier.Key {
case pkg.PURLQualifierCPES:
rawCpes := strings.Split(qualifier.Value, ",")
for _, rawCpe := range rawCpes {
c, err := cpe.New(rawCpe, cpe.DeclaredSource)
if err != nil {
log.Debugf("unable to decode cpe %s in purl %s: %w", rawCpe, p.PURL, err)
continue
}
cpes = append(cpes, c)
}
case pkg.PURLQualifierEpoch:
epoch = qualifier.Value
}
}

if p.Type == "" {
p.Type = pkg.TypeFromPURL(p.PURL)
}
if p.Language == "" {
p.Language = pkg.LanguageFromPURL(p.PURL)
}
if p.Name == "" {
p.Name = nameFromPurl(purl)
}

setVersionFromPurl(p, purl, epoch)

if p.Language == pkg.Java {
setJavaMetadataFromPurl(p, purl)
}

for _, c := range cpes {
if slices.Contains(p.CPEs, c) {
continue
}
p.CPEs = append(p.CPEs, c)
}
}

func setJavaMetadataFromPurl(p *pkg.Package, purl packageurl.PackageURL) {
if p.Type != pkg.JavaPkg {
return
}
if purl.Namespace != "" {
if p.Metadata == nil {
p.Metadata = pkg.JavaArchive{}
}
meta, got := p.Metadata.(pkg.JavaArchive)
if got && meta.PomProperties == nil {
meta.PomProperties = &pkg.JavaPomProperties{}
p.Metadata = meta
}
if meta.PomProperties != nil {
// capture the group id from the purl if it is not already set
if meta.PomProperties.ArtifactID == "" {
meta.PomProperties.ArtifactID = purl.Name
}
if meta.PomProperties.GroupID == "" {
meta.PomProperties.GroupID = purl.Namespace
}
if meta.PomProperties.Version == "" {
meta.PomProperties.Version = purl.Version
}
}
}
}

func setVersionFromPurl(p *pkg.Package, purl packageurl.PackageURL, epoch string) {
if p.Version == "" {
p.Version = purl.Version
}

if epoch != "" && p.Type == pkg.RpmPkg && !epochPrefix.MatchString(p.Version) {
Comment thread
kzantow marked this conversation as resolved.
p.Version = fmt.Sprintf("%s:%s", epoch, p.Version)
}
}

var epochPrefix = regexp.MustCompile(`^\d+:`)

// nameFromPurl returns the syft package name of the package from the purl. If the purl includes a namespace,
// the name is prefixed as appropriate based on the PURL type
func nameFromPurl(purl packageurl.PackageURL) string {
if !nameExcludesPurlNamespace(purl.Type) && purl.Namespace != "" {
return fmt.Sprintf("%s/%s", purl.Namespace, purl.Name)
}
return purl.Name
}

func nameExcludesPurlNamespace(purlType string) bool {
switch purlType {
case packageurl.TypeAlpine,
packageurl.TypeAlpm,
packageurl.TypeConan,
packageurl.TypeCpan,
packageurl.TypeDebian,
packageurl.TypeMaven,
packageurl.TypeQpkg,
packageurl.TypeRPM,
packageurl.TypeSWID:
return true
}
return false
}
Loading
Loading