mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
* allow decoding of import sbom file shape Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * address formatting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add file mode and type processing Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use type to interpret the raw value Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * safe mode convert should use uint32 Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * simpler decoder type Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
367 lines
9.9 KiB
Go
367 lines
9.9 KiB
Go
package syftjson
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
|
"github.com/anchore/syft/internal/log"
|
|
"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/syftjson/model"
|
|
"github.com/anchore/syft/syft/linux"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
"github.com/anchore/syft/syft/sbom"
|
|
"github.com/anchore/syft/syft/source"
|
|
)
|
|
|
|
func toSyftModel(doc model.Document) *sbom.SBOM {
|
|
idAliases := make(map[string]string)
|
|
|
|
catalog := toSyftCatalog(doc.Artifacts, idAliases)
|
|
|
|
fileArtifacts := toSyftFiles(doc.Files)
|
|
|
|
return &sbom.SBOM{
|
|
Artifacts: sbom.Artifacts{
|
|
Packages: catalog,
|
|
FileMetadata: fileArtifacts.FileMetadata,
|
|
FileDigests: fileArtifacts.FileDigests,
|
|
FileContents: fileArtifacts.FileContents,
|
|
FileLicenses: fileArtifacts.FileLicenses,
|
|
Executables: fileArtifacts.Executables,
|
|
Unknowns: fileArtifacts.Unknowns,
|
|
LinuxDistribution: toSyftLinuxRelease(doc.Distro),
|
|
},
|
|
Source: *toSyftSourceData(doc.Source),
|
|
Descriptor: toSyftDescriptor(doc.Descriptor),
|
|
Relationships: warnConversionErrors(toSyftRelationships(&doc, catalog, doc.ArtifactRelationships, idAliases)),
|
|
}
|
|
}
|
|
|
|
func warnConversionErrors[T any](converted []T, errors []error) []T {
|
|
errorMessages := deduplicateErrors(errors)
|
|
for _, msg := range errorMessages {
|
|
log.Warn(msg)
|
|
}
|
|
return converted
|
|
}
|
|
|
|
func deduplicateErrors(errors []error) []string {
|
|
errorCounts := make(map[string]int)
|
|
var errorMessages []string
|
|
for _, e := range errors {
|
|
errorCounts[e.Error()] = errorCounts[e.Error()] + 1
|
|
}
|
|
for msg, count := range errorCounts {
|
|
errorMessages = append(errorMessages, fmt.Sprintf("%q occurred %d time(s)", msg, count))
|
|
}
|
|
return errorMessages
|
|
}
|
|
|
|
//nolint:funlen
|
|
func toSyftFiles(files []model.File) sbom.Artifacts {
|
|
ret := sbom.Artifacts{
|
|
FileMetadata: make(map[file.Coordinates]file.Metadata),
|
|
FileDigests: make(map[file.Coordinates][]file.Digest),
|
|
FileContents: make(map[file.Coordinates]string),
|
|
FileLicenses: make(map[file.Coordinates][]file.License),
|
|
Executables: make(map[file.Coordinates]file.Executable),
|
|
Unknowns: make(map[file.Coordinates][]string),
|
|
}
|
|
|
|
for _, f := range files {
|
|
coord := f.Location
|
|
if f.Metadata != nil {
|
|
fm, err := safeFileModeConvert(f.Metadata.Mode)
|
|
if err != nil {
|
|
log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coord, f.Metadata.Mode, err)
|
|
fm = 0
|
|
}
|
|
|
|
ret.FileMetadata[coord] = file.Metadata{
|
|
FileInfo: stereoscopeFile.ManualInfo{
|
|
NameValue: path.Base(coord.RealPath),
|
|
SizeValue: f.Metadata.Size,
|
|
ModeValue: fm,
|
|
},
|
|
Path: coord.RealPath,
|
|
LinkDestination: f.Metadata.LinkDestination,
|
|
UserID: f.Metadata.UserID,
|
|
GroupID: f.Metadata.GroupID,
|
|
Type: toSyftFileType(f.Metadata.Type),
|
|
MIMEType: f.Metadata.MIMEType,
|
|
}
|
|
}
|
|
|
|
for _, d := range f.Digests {
|
|
ret.FileDigests[coord] = append(ret.FileDigests[coord], file.Digest{
|
|
Algorithm: d.Algorithm,
|
|
Value: d.Value,
|
|
})
|
|
}
|
|
|
|
if f.Contents != "" {
|
|
ret.FileContents[coord] = f.Contents
|
|
}
|
|
|
|
for _, l := range f.Licenses {
|
|
var evidence *file.LicenseEvidence
|
|
if e := l.Evidence; e != nil {
|
|
evidence = &file.LicenseEvidence{
|
|
Confidence: e.Confidence,
|
|
Offset: e.Offset,
|
|
Extent: e.Extent,
|
|
}
|
|
}
|
|
ret.FileLicenses[coord] = append(ret.FileLicenses[coord], file.License{
|
|
Value: l.Value,
|
|
SPDXExpression: l.SPDXExpression,
|
|
Type: l.Type,
|
|
LicenseEvidence: evidence,
|
|
})
|
|
}
|
|
|
|
if f.Executable != nil {
|
|
ret.Executables[coord] = *f.Executable
|
|
}
|
|
|
|
if len(f.Unknowns) > 0 {
|
|
ret.Unknowns[coord] = f.Unknowns
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func safeFileModeConvert(val int) (fs.FileMode, error) {
|
|
mode, err := strconv.ParseInt(strconv.Itoa(val), 8, 64)
|
|
if mode < 0 || mode > math.MaxUint32 {
|
|
// value is out of the range that int32 can represent
|
|
return 0, fmt.Errorf("value %d is out of the range that int32 can represent", val)
|
|
}
|
|
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return os.FileMode(mode), nil
|
|
}
|
|
|
|
func toSyftLicenses(m []model.License) (p []pkg.License) {
|
|
for _, l := range m {
|
|
p = append(p, pkg.License{
|
|
Value: l.Value,
|
|
SPDXExpression: l.SPDXExpression,
|
|
Type: l.Type,
|
|
URLs: l.URLs,
|
|
Locations: file.NewLocationSet(l.Locations...),
|
|
Contents: l.Contents,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
func toSyftFileType(ty string) stereoscopeFile.Type {
|
|
switch ty {
|
|
case "SymbolicLink":
|
|
return stereoscopeFile.TypeSymLink
|
|
case "HardLink":
|
|
return stereoscopeFile.TypeHardLink
|
|
case "Directory":
|
|
return stereoscopeFile.TypeDirectory
|
|
case "Socket":
|
|
return stereoscopeFile.TypeSocket
|
|
case "BlockDevice":
|
|
return stereoscopeFile.TypeBlockDevice
|
|
case "CharacterDevice":
|
|
return stereoscopeFile.TypeCharacterDevice
|
|
case "FIFONode":
|
|
return stereoscopeFile.TypeFIFO
|
|
case "RegularFile":
|
|
return stereoscopeFile.TypeRegular
|
|
case "IrregularFile":
|
|
return stereoscopeFile.TypeIrregular
|
|
default:
|
|
return stereoscopeFile.TypeIrregular
|
|
}
|
|
}
|
|
|
|
func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release {
|
|
if cmp.Equal(d, model.LinuxRelease{}) {
|
|
return nil
|
|
}
|
|
return &linux.Release{
|
|
PrettyName: d.PrettyName,
|
|
Name: d.Name,
|
|
ID: d.ID,
|
|
IDLike: d.IDLike,
|
|
Version: d.Version,
|
|
VersionID: d.VersionID,
|
|
VersionCodename: d.VersionCodename,
|
|
BuildID: d.BuildID,
|
|
ImageID: d.ImageID,
|
|
ImageVersion: d.ImageVersion,
|
|
Variant: d.Variant,
|
|
VariantID: d.VariantID,
|
|
HomeURL: d.HomeURL,
|
|
SupportURL: d.SupportURL,
|
|
BugReportURL: d.BugReportURL,
|
|
PrivacyPolicyURL: d.PrivacyPolicyURL,
|
|
CPEName: d.CPEName,
|
|
SupportEnd: d.SupportEnd,
|
|
}
|
|
}
|
|
|
|
func toSyftRelationships(doc *model.Document, catalog *pkg.Collection, relationships []model.Relationship, idAliases map[string]string) ([]artifact.Relationship, []error) {
|
|
idMap := make(map[string]interface{})
|
|
|
|
for _, p := range catalog.Sorted() {
|
|
idMap[string(p.ID())] = p
|
|
locations := p.Locations.ToSlice()
|
|
for _, l := range locations {
|
|
idMap[string(l.ID())] = l.Coordinates
|
|
}
|
|
}
|
|
|
|
// set source metadata in identifier map
|
|
idMap[doc.Source.ID] = toSyftSource(doc.Source)
|
|
|
|
for _, f := range doc.Files {
|
|
idMap[f.ID] = f.Location
|
|
}
|
|
|
|
var out []artifact.Relationship
|
|
var conversionErrors []error
|
|
for _, r := range relationships {
|
|
syftRelationship, err := toSyftRelationship(idMap, r, idAliases)
|
|
if err != nil {
|
|
conversionErrors = append(conversionErrors, err)
|
|
}
|
|
if syftRelationship != nil {
|
|
out = append(out, *syftRelationship)
|
|
}
|
|
}
|
|
|
|
return out, conversionErrors
|
|
}
|
|
|
|
func toSyftSource(s model.Source) source.Source {
|
|
description := toSyftSourceData(s)
|
|
if description == nil {
|
|
return nil
|
|
}
|
|
return source.FromDescription(*description)
|
|
}
|
|
|
|
func toSyftRelationship(idMap map[string]interface{}, relationship model.Relationship, idAliases map[string]string) (*artifact.Relationship, error) {
|
|
id := func(id string) string {
|
|
aliased, ok := idAliases[id]
|
|
if ok {
|
|
return aliased
|
|
}
|
|
return id
|
|
}
|
|
|
|
from, ok := idMap[id(relationship.Parent)].(artifact.Identifiable)
|
|
if !ok {
|
|
return nil, fmt.Errorf("relationship mapping from key %s is not a valid artifact.Identifiable type: %+v", relationship.Parent, idMap[relationship.Parent])
|
|
}
|
|
|
|
to, ok := idMap[id(relationship.Child)].(artifact.Identifiable)
|
|
if !ok {
|
|
return nil, fmt.Errorf("relationship mapping to key %s is not a valid artifact.Identifiable type: %+v", relationship.Child, idMap[relationship.Child])
|
|
}
|
|
|
|
typ := artifact.RelationshipType(relationship.Type)
|
|
|
|
switch typ {
|
|
case artifact.OwnershipByFileOverlapRelationship, artifact.ContainsRelationship, artifact.DependencyOfRelationship, artifact.EvidentByRelationship:
|
|
default:
|
|
if !strings.Contains(string(typ), "dependency-of") {
|
|
return nil, fmt.Errorf("unknown relationship type: %s", string(typ))
|
|
}
|
|
// lets try to stay as compatible as possible with similar relationship types without dropping the relationship
|
|
log.Warnf("assuming %q for relationship type %q", artifact.DependencyOfRelationship, typ)
|
|
typ = artifact.DependencyOfRelationship
|
|
}
|
|
return &artifact.Relationship{
|
|
From: from,
|
|
To: to,
|
|
Type: typ,
|
|
Data: relationship.Metadata,
|
|
}, nil
|
|
}
|
|
|
|
func toSyftDescriptor(d model.Descriptor) sbom.Descriptor {
|
|
return sbom.Descriptor{
|
|
Name: d.Name,
|
|
Version: d.Version,
|
|
Configuration: d.Configuration,
|
|
}
|
|
}
|
|
|
|
func toSyftSourceData(s model.Source) *source.Description {
|
|
return &source.Description{
|
|
ID: s.ID,
|
|
Name: s.Name,
|
|
Version: s.Version,
|
|
Metadata: s.Metadata,
|
|
}
|
|
}
|
|
|
|
func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Collection {
|
|
catalog := pkg.NewCollection()
|
|
for _, p := range pkgs {
|
|
catalog.Add(toSyftPackage(p, idAliases))
|
|
}
|
|
return catalog
|
|
}
|
|
|
|
func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
|
|
var cpes []cpe.CPE
|
|
for _, c := range p.CPEs {
|
|
value, err := cpe.New(c.Value, cpe.Source(c.Source))
|
|
if err != nil {
|
|
log.Warnf("excluding invalid Attributes %q: %v", c, err)
|
|
continue
|
|
}
|
|
|
|
cpes = append(cpes, value)
|
|
}
|
|
|
|
out := pkg.Package{
|
|
Name: p.Name,
|
|
Version: p.Version,
|
|
FoundBy: p.FoundBy,
|
|
Locations: file.NewLocationSet(p.Locations...),
|
|
Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
|
|
Language: p.Language,
|
|
Type: p.Type,
|
|
CPEs: cpes,
|
|
PURL: p.PURL,
|
|
Metadata: p.Metadata,
|
|
}
|
|
|
|
internal.Backfill(&out)
|
|
|
|
// always prefer the IDs from the SBOM over derived IDs
|
|
out.OverrideID(artifact.ID(p.ID))
|
|
|
|
// this alias mapping is currently defunct, but could be useful in the future.
|
|
id := string(out.ID())
|
|
if id != p.ID {
|
|
idAliases[p.ID] = id
|
|
}
|
|
|
|
return out
|
|
}
|