mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +01:00
Add CycloneDX decoder (#811)
This commit is contained in:
parent
4b16737b2f
commit
20c1d14f6e
@ -7,7 +7,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Author(p pkg.Package) string {
|
||||
func encodeAuthor(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
@ -30,3 +30,18 @@ func Author(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeAuthor(author string, metadata interface{}) {
|
||||
switch meta := metadata.(type) {
|
||||
case *pkg.NpmPackageJSONMetadata:
|
||||
meta.Author = author
|
||||
case *pkg.PythonPackageMetadata:
|
||||
parts := strings.SplitN(author, " <", 2)
|
||||
meta.Author = parts[0]
|
||||
if len(parts) > 1 {
|
||||
meta.AuthorEmail = strings.TrimSuffix(parts[1], ">")
|
||||
}
|
||||
case *pkg.GemMetadata:
|
||||
meta.Authors = strings.Split(author, ",")
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Author(t *testing.T) {
|
||||
func Test_encodeAuthor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@ -81,7 +81,7 @@ func Test_Author(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Author(test.input))
|
||||
assert.Equal(t, test.expected, encodeAuthor(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +1,164 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Component(p pkg.Package) cyclonedx.Component {
|
||||
func encodeComponent(p pkg.Package) cyclonedx.Component {
|
||||
return cyclonedx.Component{
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: p.Name,
|
||||
Group: Group(p),
|
||||
Group: encodeGroup(p),
|
||||
Version: p.Version,
|
||||
PackageURL: p.PURL,
|
||||
Licenses: Licenses(p),
|
||||
CPE: CPE(p),
|
||||
Author: Author(p),
|
||||
Publisher: Publisher(p),
|
||||
Description: Description(p),
|
||||
ExternalReferences: ExternalReferences(p),
|
||||
Properties: Properties(p),
|
||||
Licenses: encodeLicenses(p),
|
||||
CPE: encodeCPE(p),
|
||||
Author: encodeAuthor(p),
|
||||
Publisher: encodePublisher(p),
|
||||
Description: encodeDescription(p),
|
||||
ExternalReferences: encodeExternalReferences(p),
|
||||
Properties: encodeProperties(p),
|
||||
}
|
||||
}
|
||||
|
||||
func hasMetadata(p pkg.Package) bool {
|
||||
return p.Metadata != nil
|
||||
}
|
||||
|
||||
func decodeComponent(c *cyclonedx.Component) *pkg.Package {
|
||||
typ := pkg.Type(findPropertyValue(c, "type"))
|
||||
purl := c.PackageURL
|
||||
if typ == "" && purl != "" {
|
||||
typ = pkg.TypeFromPURL(purl)
|
||||
}
|
||||
|
||||
metaType, meta := decodePackageMetadata(c)
|
||||
|
||||
p := &pkg.Package{
|
||||
Name: c.Name,
|
||||
Version: c.Version,
|
||||
FoundBy: findPropertyValue(c, "foundBy"),
|
||||
Locations: decodeLocations(c),
|
||||
Licenses: decodeLicenses(c),
|
||||
Language: pkg.Language(findPropertyValue(c, "language")),
|
||||
Type: typ,
|
||||
CPEs: decodeCPEs(c),
|
||||
PURL: purl,
|
||||
MetadataType: metaType,
|
||||
Metadata: meta,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func decodeLocations(c *cyclonedx.Component) (out []source.Location) {
|
||||
if c.Properties != nil {
|
||||
props := *c.Properties
|
||||
for i := 0; i < len(props)-1; i++ {
|
||||
if props[i].Name == "path" && props[i+1].Name == "layerID" {
|
||||
out = append(out, source.Location{
|
||||
Coordinates: source.Coordinates{
|
||||
RealPath: props[i].Value,
|
||||
FileSystemID: props[i+1].Value,
|
||||
},
|
||||
})
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapAllProps(c *cyclonedx.Component, obj reflect.Value) {
|
||||
value := obj
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
structType := value.Type()
|
||||
if structType.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
field := structType.Field(i)
|
||||
fieldType := field.Type
|
||||
fieldValue := value.Field(i)
|
||||
|
||||
name, mapped := field.Tag.Lookup("cyclonedx")
|
||||
if !mapped {
|
||||
continue
|
||||
}
|
||||
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
if fieldValue.IsNil() {
|
||||
newValue := reflect.New(fieldType)
|
||||
fieldValue.Set(newValue)
|
||||
}
|
||||
fieldValue = fieldValue.Elem()
|
||||
}
|
||||
|
||||
propertyValue := findPropertyValue(c, name)
|
||||
switch fieldType.Kind() {
|
||||
case reflect.String:
|
||||
if fieldValue.CanSet() {
|
||||
fieldValue.SetString(propertyValue)
|
||||
} else {
|
||||
msg := fmt.Sprintf("unable to set field: %s.%s", structType.Name(), field.Name)
|
||||
log.Info(msg)
|
||||
}
|
||||
case reflect.Bool:
|
||||
if b, err := strconv.ParseBool(propertyValue); err == nil {
|
||||
fieldValue.SetBool(b)
|
||||
}
|
||||
case reflect.Int:
|
||||
if i, err := strconv.Atoi(propertyValue); err == nil {
|
||||
fieldValue.SetInt(int64(i))
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if i, err := strconv.ParseFloat(propertyValue, 64); err == nil {
|
||||
fieldValue.SetFloat(i)
|
||||
}
|
||||
case reflect.Struct:
|
||||
mapAllProps(c, fieldValue)
|
||||
case reflect.Complex128, reflect.Complex64:
|
||||
fallthrough
|
||||
case reflect.Ptr:
|
||||
msg := fmt.Sprintf("decoding CycloneDX properties to a pointer is not supported: %s.%s", field.Type.Name(), field.Name)
|
||||
log.Warnf(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{}) {
|
||||
if c.Properties != nil {
|
||||
typ := pkg.MetadataType(findPropertyValue(c, "metadataType"))
|
||||
if typ != "" {
|
||||
meta := reflect.New(pkg.MetadataTypeByName[typ])
|
||||
metaPtr := meta.Interface()
|
||||
|
||||
// Map all dynamic properties
|
||||
mapAllProps(c, meta.Elem())
|
||||
|
||||
// Map all explicit metadata properties
|
||||
decodeAuthor(c.Author, metaPtr)
|
||||
decodeGroup(c.Group, metaPtr)
|
||||
decodePublisher(c.Publisher, metaPtr)
|
||||
decodeDescription(c.Description, metaPtr)
|
||||
decodeExternalReferences(c, metaPtr)
|
||||
|
||||
// return the actual interface{} | struct ( not interface{} | *struct )
|
||||
return typ, meta.Elem().Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return pkg.UnknownMetadataType, nil
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func CPE(p pkg.Package) string {
|
||||
func encodeCPE(p pkg.Package) string {
|
||||
// Since the CPEs in a package are sorted by specificity
|
||||
// we can extract the first CPE as the one to output in cyclonedx
|
||||
if len(p.CPEs) > 0 {
|
||||
@ -10,3 +14,17 @@ func CPE(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeCPEs(c *cyclonedx.Component) []pkg.CPE {
|
||||
// FIXME we not encoding all the CPEs (see above), so here we just use the single provided one
|
||||
if c.CPE != "" {
|
||||
cp, err := pkg.NewCPE(c.CPE)
|
||||
if err != nil {
|
||||
log.Warnf("invalid CPE: %s", c.CPE)
|
||||
} else {
|
||||
return []pkg.CPE{cp}
|
||||
}
|
||||
}
|
||||
|
||||
return []pkg.CPE{}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_CPE(t *testing.T) {
|
||||
func Test_encodeCPE(t *testing.T) {
|
||||
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
|
||||
testCPE2 := pkg.MustCPE("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*")
|
||||
tests := []struct {
|
||||
@ -51,7 +51,7 @@ func Test_CPE(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, CPE(test.input))
|
||||
assert.Equal(t, test.expected, encodeCPE(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
222
internal/formats/common/cyclonedxhelpers/decoder.go
Normal file
222
internal/formats/common/cyclonedxhelpers/decoder.go
Normal file
@ -0,0 +1,222 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"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 GetValidator(format cyclonedx.BOMFileFormat) format.Validator {
|
||||
return func(reader io.Reader) error {
|
||||
bom := &cyclonedx.BOM{}
|
||||
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// random JSON does not necessarily cause an error (e.g. SPDX)
|
||||
if (cyclonedx.BOM{} == *bom) {
|
||||
return fmt.Errorf("not a valid CycloneDX document")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetDecoder(format cyclonedx.BOMFileFormat) format.Decoder {
|
||||
return func(reader io.Reader) (*sbom.SBOM, error) {
|
||||
bom := &cyclonedx.BOM{}
|
||||
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s, err := toSyftModel(bom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
func toSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
|
||||
meta := source.Metadata{}
|
||||
if bom.Metadata != nil {
|
||||
meta = decodeMetadata(bom.Metadata.Component)
|
||||
}
|
||||
s := &sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: pkg.NewCatalog(),
|
||||
LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
|
||||
},
|
||||
Source: meta,
|
||||
//Descriptor: sbom.Descriptor{},
|
||||
}
|
||||
|
||||
idMap := make(map[string]interface{})
|
||||
|
||||
if err := collectBomPackages(bom, s, idMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectRelationships(bom, s, idMap)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error {
|
||||
if bom.Components == nil {
|
||||
return fmt.Errorf("no components are defined in the CycloneDX BOM")
|
||||
}
|
||||
for i := range *bom.Components {
|
||||
collectPackages(&(*bom.Components)[i], s, idMap)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) {
|
||||
switch component.Type {
|
||||
case cyclonedx.ComponentTypeOS:
|
||||
case cyclonedx.ComponentTypeContainer:
|
||||
case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary:
|
||||
p := decodeComponent(component)
|
||||
idMap[component.BOMRef] = p
|
||||
// TODO there must be a better way than needing to call this manually:
|
||||
p.SetID()
|
||||
s.Artifacts.PackageCatalog.Add(*p)
|
||||
}
|
||||
|
||||
if component.Components != nil {
|
||||
for i := range *component.Components {
|
||||
collectPackages(&(*component.Components)[i], s, idMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
|
||||
for i := range components {
|
||||
component := &components[i]
|
||||
if component.Type == cyclonedx.ComponentTypeOS {
|
||||
return linuxReleaseFromOSComponent(component)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release {
|
||||
if component == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var name string
|
||||
var version string
|
||||
if component.SWID != nil {
|
||||
name = component.SWID.Name
|
||||
version = component.SWID.Version
|
||||
}
|
||||
if name == "" {
|
||||
name = component.Name
|
||||
}
|
||||
if name == "" {
|
||||
name = getPropertyValue(component, "id")
|
||||
}
|
||||
if version == "" {
|
||||
version = component.Version
|
||||
}
|
||||
if version == "" {
|
||||
version = getPropertyValue(component, "versionID")
|
||||
}
|
||||
|
||||
rel := &linux.Release{
|
||||
CPEName: component.CPE,
|
||||
PrettyName: name,
|
||||
Name: name,
|
||||
ID: name,
|
||||
IDLike: []string{name},
|
||||
Version: version,
|
||||
VersionID: version,
|
||||
}
|
||||
if component.ExternalReferences != nil {
|
||||
for _, ref := range *component.ExternalReferences {
|
||||
switch ref.Type {
|
||||
case cyclonedx.ERTypeIssueTracker:
|
||||
rel.BugReportURL = ref.URL
|
||||
case cyclonedx.ERTypeWebsite:
|
||||
rel.HomeURL = ref.URL
|
||||
case cyclonedx.ERTypeOther:
|
||||
switch ref.Comment {
|
||||
case "support":
|
||||
rel.SupportURL = ref.URL
|
||||
case "privacyPolicy":
|
||||
rel.PrivacyPolicyURL = ref.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rel
|
||||
}
|
||||
|
||||
func getPropertyValue(component *cyclonedx.Component, name string) string {
|
||||
if component.Properties != nil {
|
||||
for _, p := range *component.Properties {
|
||||
if p.Name == name {
|
||||
return p.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) {
|
||||
if bom.Dependencies == nil {
|
||||
return
|
||||
}
|
||||
for _, d := range *bom.Dependencies {
|
||||
from, fromOk := idMap[d.Ref].(artifact.Identifiable)
|
||||
if fromOk {
|
||||
if d.Dependencies == nil {
|
||||
continue
|
||||
}
|
||||
for _, t := range *d.Dependencies {
|
||||
to, toOk := idMap[t.Ref].(artifact.Identifiable)
|
||||
if toOk {
|
||||
s.Relationships = append(s.Relationships, artifact.Relationship{
|
||||
From: from,
|
||||
To: to,
|
||||
Type: artifact.DependencyOfRelationship, // FIXME this information is lost
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeMetadata(component *cyclonedx.Component) source.Metadata {
|
||||
switch component.Type {
|
||||
case cyclonedx.ComponentTypeContainer:
|
||||
return source.Metadata{
|
||||
Scheme: source.ImageScheme,
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: component.Name,
|
||||
ID: component.BOMRef,
|
||||
ManifestDigest: component.Version,
|
||||
},
|
||||
}
|
||||
case cyclonedx.ComponentTypeFile:
|
||||
return source.Metadata{
|
||||
Scheme: source.FileScheme, // or source.DirectoryScheme
|
||||
Path: component.Name,
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: component.Name,
|
||||
ID: component.BOMRef,
|
||||
ManifestDigest: component.Version,
|
||||
},
|
||||
}
|
||||
}
|
||||
return source.Metadata{}
|
||||
}
|
||||
260
internal/formats/common/cyclonedxhelpers/decoder_test.go
Normal file
260
internal/formats/common/cyclonedxhelpers/decoder_test.go
Normal file
@ -0,0 +1,260 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_decode(t *testing.T) {
|
||||
type expected struct {
|
||||
os string
|
||||
pkg string
|
||||
ver string
|
||||
relation string
|
||||
purl string
|
||||
cpe string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
input cyclonedx.BOM
|
||||
expected []expected
|
||||
}{
|
||||
{
|
||||
name: "basic mapping from cyclonedx",
|
||||
input: cyclonedx.BOM{
|
||||
Metadata: nil,
|
||||
Components: &[]cyclonedx.Component{
|
||||
{
|
||||
BOMRef: "p1",
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Description: "",
|
||||
Hashes: nil,
|
||||
Licenses: &cyclonedx.Licenses{
|
||||
{
|
||||
License: &cyclonedx.License{
|
||||
ID: "MIT",
|
||||
},
|
||||
},
|
||||
},
|
||||
CPE: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||
PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
|
||||
ExternalReferences: &[]cyclonedx.ExternalReference{
|
||||
{
|
||||
URL: "",
|
||||
Comment: "",
|
||||
Hashes: nil,
|
||||
Type: "",
|
||||
},
|
||||
},
|
||||
Properties: &[]cyclonedx.Property{
|
||||
{
|
||||
Name: "foundBy",
|
||||
Value: "the-cataloger-1",
|
||||
},
|
||||
{
|
||||
Name: "language",
|
||||
Value: "python",
|
||||
},
|
||||
{
|
||||
Name: "type",
|
||||
Value: "python",
|
||||
},
|
||||
{
|
||||
Name: "metadataType",
|
||||
Value: "PythonPackageMetadata",
|
||||
},
|
||||
{
|
||||
Name: "path",
|
||||
Value: "/some/path/pkg1",
|
||||
},
|
||||
},
|
||||
Components: nil,
|
||||
Evidence: nil,
|
||||
},
|
||||
{
|
||||
BOMRef: "p2",
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: "package-2",
|
||||
Version: "2.0.2",
|
||||
Hashes: nil,
|
||||
Licenses: &cyclonedx.Licenses{
|
||||
{
|
||||
License: &cyclonedx.License{
|
||||
ID: "MIT",
|
||||
},
|
||||
},
|
||||
},
|
||||
CPE: "cpe:2.3:*:another:package:2:*:*:*:*:*:*:*",
|
||||
PackageURL: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
|
||||
Properties: &[]cyclonedx.Property{
|
||||
|
||||
{
|
||||
Name: "foundBy",
|
||||
Value: "apkdb-cataloger",
|
||||
},
|
||||
{
|
||||
Name: "type",
|
||||
Value: "apk",
|
||||
},
|
||||
{
|
||||
Name: "metadataType",
|
||||
Value: "ApkMetadata",
|
||||
},
|
||||
{
|
||||
Name: "path",
|
||||
Value: "/lib/apk/db/installed",
|
||||
},
|
||||
{
|
||||
Name: "layerID",
|
||||
Value: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
|
||||
},
|
||||
{
|
||||
Name: "originPackage",
|
||||
Value: "zlib",
|
||||
},
|
||||
{
|
||||
Name: "size",
|
||||
Value: "51213",
|
||||
},
|
||||
{
|
||||
Name: "installedSize",
|
||||
Value: "110592",
|
||||
},
|
||||
{
|
||||
Name: "pullDependencies",
|
||||
Value: "so:libc.musl-x86_64.so.1",
|
||||
},
|
||||
{
|
||||
Name: "pullChecksum",
|
||||
Value: "Q1uss4DfpvL16Nw2YUTwmzGBABz3Y=",
|
||||
},
|
||||
{
|
||||
Name: "gitCommitOfApkPort",
|
||||
Value: "d2bfb22c8e8f67ad7d8d02704f35ec4d2a19f9b9",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: cyclonedx.ComponentTypeOS,
|
||||
Name: "debian",
|
||||
Version: "1.2.3",
|
||||
Hashes: nil,
|
||||
Licenses: &cyclonedx.Licenses{
|
||||
{
|
||||
License: &cyclonedx.License{
|
||||
ID: "MIT",
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: &[]cyclonedx.Property{
|
||||
{
|
||||
Name: "prettyName",
|
||||
Value: "debian",
|
||||
},
|
||||
{
|
||||
Name: "id",
|
||||
Value: "debian",
|
||||
},
|
||||
{
|
||||
Name: "versionID",
|
||||
Value: "1.2.3",
|
||||
},
|
||||
},
|
||||
Components: nil,
|
||||
Evidence: nil,
|
||||
},
|
||||
},
|
||||
Dependencies: &[]cyclonedx.Dependency{
|
||||
{
|
||||
Ref: "p1",
|
||||
Dependencies: &[]cyclonedx.Dependency{
|
||||
{
|
||||
Ref: "p2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []expected{
|
||||
{
|
||||
os: "debian",
|
||||
ver: "1.2.3",
|
||||
},
|
||||
{
|
||||
pkg: "package-1",
|
||||
ver: "1.0.1",
|
||||
cpe: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||
purl: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
|
||||
relation: "package-2",
|
||||
},
|
||||
{
|
||||
pkg: "package-2",
|
||||
ver: "2.0.2",
|
||||
purl: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
sbom, err := toSyftModel(&test.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
test:
|
||||
for _, e := range test.expected {
|
||||
if e.os != "" {
|
||||
assert.Equal(t, e.os, sbom.Artifacts.LinuxDistribution.ID)
|
||||
assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistribution.VersionID)
|
||||
}
|
||||
if e.pkg != "" {
|
||||
for p := range sbom.Artifacts.PackageCatalog.Enumerate() {
|
||||
if e.pkg != p.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, e.ver, p.Version)
|
||||
|
||||
if e.cpe != "" {
|
||||
foundCPE := false
|
||||
for _, c := range p.CPEs {
|
||||
cstr := c.BindToFmtString()
|
||||
if e.cpe == cstr {
|
||||
foundCPE = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundCPE {
|
||||
assert.Fail(t, fmt.Sprintf("CPE not found in package: %s", e.cpe))
|
||||
}
|
||||
}
|
||||
|
||||
if e.purl != "" {
|
||||
assert.Equal(t, e.purl, p.PURL)
|
||||
}
|
||||
|
||||
if e.relation != "" {
|
||||
foundRelation := false
|
||||
for _, r := range sbom.Relationships {
|
||||
p := sbom.Artifacts.PackageCatalog.Package(r.To.ID())
|
||||
if e.relation == p.Name {
|
||||
foundRelation = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundRelation {
|
||||
assert.Fail(t, fmt.Sprintf("relation not found: %s", e.relation))
|
||||
}
|
||||
}
|
||||
continue test
|
||||
}
|
||||
assert.Fail(t, fmt.Sprintf("package should be present: %s", e.pkg))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ package cyclonedxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func Description(p pkg.Package) string {
|
||||
func encodeDescription(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
@ -13,3 +13,12 @@ func Description(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeDescription(description string, metadata interface{}) {
|
||||
switch meta := metadata.(type) {
|
||||
case *pkg.ApkMetadata:
|
||||
meta.Description = description
|
||||
case *pkg.NpmPackageJSONMetadata:
|
||||
meta.Description = description
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Description(t *testing.T) {
|
||||
func Test_encodeDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@ -50,7 +50,7 @@ func Test_Description(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Description(test.input))
|
||||
assert.Equal(t, test.expected, encodeDescription(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,13 @@ package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
|
||||
func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
|
||||
refs := []cyclonedx.ExternalReference{}
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
@ -63,3 +64,51 @@ func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeExternalReferences(c *cyclonedx.Component, metadata interface{}) {
|
||||
if c.ExternalReferences == nil {
|
||||
return
|
||||
}
|
||||
switch meta := metadata.(type) {
|
||||
case *pkg.ApkMetadata:
|
||||
meta.URL = refURL(c, cyclonedx.ERTypeDistribution)
|
||||
case *pkg.CargoPackageMetadata:
|
||||
meta.Source = refURL(c, cyclonedx.ERTypeDistribution)
|
||||
case *pkg.NpmPackageJSONMetadata:
|
||||
meta.URL = refURL(c, cyclonedx.ERTypeDistribution)
|
||||
meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite)
|
||||
case *pkg.GemMetadata:
|
||||
meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite)
|
||||
case *pkg.PythonPackageMetadata:
|
||||
if meta.DirectURLOrigin == nil {
|
||||
meta.DirectURLOrigin = &pkg.PythonDirectURLOriginInfo{}
|
||||
}
|
||||
meta.DirectURLOrigin.URL = refURL(c, cyclonedx.ERTypeVCS)
|
||||
meta.DirectURLOrigin.CommitID = strings.TrimPrefix(refComment(c, cyclonedx.ERTypeVCS), "commit: ")
|
||||
}
|
||||
}
|
||||
|
||||
func findExternalRef(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) *cyclonedx.ExternalReference {
|
||||
if c.ExternalReferences != nil {
|
||||
for _, r := range *c.ExternalReferences {
|
||||
if r.Type == typ {
|
||||
return &r
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func refURL(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string {
|
||||
if r := findExternalRef(c, typ); r != nil {
|
||||
return r.URL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func refComment(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string {
|
||||
if r := findExternalRef(c, typ); r != nil {
|
||||
return r.Comment
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ExternalReferences(t *testing.T) {
|
||||
func Test_encodeExternalReferences(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@ -127,7 +127,7 @@ func Test_ExternalReferences(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, ExternalReferences(test.input))
|
||||
assert.Equal(t, test.expected, encodeExternalReferences(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
packages := s.Artifacts.PackageCatalog.Sorted()
|
||||
components := make([]cyclonedx.Component, len(packages))
|
||||
for i, p := range packages {
|
||||
components[i] = Component(p)
|
||||
components[i] = encodeComponent(p)
|
||||
}
|
||||
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
|
||||
cdxBOM.Components = &components
|
||||
@ -80,9 +80,17 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
|
||||
}
|
||||
return []cyclonedx.Component{
|
||||
{
|
||||
Type: cyclonedx.ComponentTypeOS,
|
||||
Name: distro.Name,
|
||||
Version: distro.Version,
|
||||
Type: cyclonedx.ComponentTypeOS,
|
||||
// FIXME is it idiomatic to be using SWID here for specific name and version information?
|
||||
SWID: &cyclonedx.SWID{
|
||||
TagID: distro.ID,
|
||||
Name: distro.ID,
|
||||
Version: distro.VersionID,
|
||||
},
|
||||
Description: distro.PrettyName,
|
||||
Name: distro.ID,
|
||||
Version: distro.VersionID,
|
||||
// TODO should we add a PURL?
|
||||
CPE: distro.CPEName,
|
||||
ExternalReferences: eRefs,
|
||||
Properties: props,
|
||||
|
||||
@ -2,7 +2,7 @@ package cyclonedxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func Group(p pkg.Package) string {
|
||||
func encodeGroup(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
|
||||
return metadata.PomProperties.GroupID
|
||||
@ -10,3 +10,12 @@ func Group(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeGroup(group string, metadata interface{}) {
|
||||
if meta, ok := metadata.(*pkg.JavaMetadata); ok {
|
||||
if meta.PomProperties == nil {
|
||||
meta.PomProperties = &pkg.PomProperties{}
|
||||
}
|
||||
meta.PomProperties.GroupID = group
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
func Test_encodeGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@ -46,7 +46,7 @@ func TestGroup(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Group(test.input))
|
||||
assert.Equal(t, test.expected, encodeGroup(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Licenses(p pkg.Package) *cyclonedx.Licenses {
|
||||
func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
|
||||
lc := cyclonedx.Licenses{}
|
||||
for _, licenseName := range p.Licenses {
|
||||
if value, exists := spdxlicense.ID(licenseName); exists {
|
||||
@ -22,3 +22,12 @@ func Licenses(p pkg.Package) *cyclonedx.Licenses {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeLicenses(c *cyclonedx.Component) (out []string) {
|
||||
if c.Licenses != nil {
|
||||
for _, l := range *c.Licenses {
|
||||
out = append(out, l.License.ID)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_License(t *testing.T) {
|
||||
func Test_encodeLicense(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@ -77,7 +77,7 @@ func Test_License(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Licenses(test.input))
|
||||
assert.Equal(t, test.expected, encodeLicenses(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Properties(p pkg.Package) *[]cyclonedx.Property {
|
||||
func encodeProperties(p pkg.Package) *[]cyclonedx.Property {
|
||||
props := []cyclonedx.Property{}
|
||||
props = append(props, *getCycloneDXProperties(p)...)
|
||||
if len(p.Locations) > 0 {
|
||||
@ -76,3 +76,12 @@ func getCycloneDXPropertyValue(field reflect.Value) interface{} {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findPropertyValue(c *cyclonedx.Component, name string) string {
|
||||
for _, p := range *c.Properties {
|
||||
if p.Name == name {
|
||||
return p.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Properties(t *testing.T) {
|
||||
func Test_encodeProperties(t *testing.T) {
|
||||
epoch := 2
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -132,7 +132,7 @@ func Test_Properties(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Properties(test.input))
|
||||
assert.Equal(t, test.expected, encodeProperties(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Publisher(p pkg.Package) string {
|
||||
func encodePublisher(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
@ -17,3 +17,14 @@ func Publisher(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodePublisher(publisher string, metadata interface{}) {
|
||||
switch meta := metadata.(type) {
|
||||
case *pkg.ApkMetadata:
|
||||
meta.Maintainer = publisher
|
||||
case *pkg.RpmdbMetadata:
|
||||
meta.Vendor = publisher
|
||||
case *pkg.DpkgMetadata:
|
||||
meta.Maintainer = publisher
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Publisher(t *testing.T) {
|
||||
func Test_encodePublisher(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@ -59,7 +59,7 @@ func Test_Publisher(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Publisher(test.input))
|
||||
assert.Equal(t, test.expected, encodePublisher(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
70
internal/formats/cyclonedx13json/decoder_test.go
Normal file
70
internal/formats/cyclonedx13json/decoder_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package cyclonedx13json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_decodeJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
file string
|
||||
err bool
|
||||
distro string
|
||||
packages []string
|
||||
}{
|
||||
{
|
||||
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
file: "snapshot/TestCycloneDxImageEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
file: "image-simple/Dockerfile",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.file, func(t *testing.T) {
|
||||
reader, err := os.Open("test-fixtures/" + test.file)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if test.err {
|
||||
err = Format().Validate(reader)
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
bom, err := Format().Decode(reader)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
split := strings.SplitN(test.distro, ":", 2)
|
||||
name := split[0]
|
||||
version := split[1]
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name)
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version)
|
||||
|
||||
pkgs:
|
||||
for _, pkg := range test.packages {
|
||||
split = strings.SplitN(pkg, ":", 2)
|
||||
name = split[0]
|
||||
version = split[1]
|
||||
for p := range bom.Artifacts.PackageCatalog.Enumerate() {
|
||||
if p.Name == name {
|
||||
assert.Equal(t, version, p.Version)
|
||||
continue pkgs
|
||||
}
|
||||
}
|
||||
assert.Fail(t, fmt.Sprintf("package should be present: %s", pkg))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,16 @@
|
||||
package cyclonedx13json
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
)
|
||||
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.CycloneDxJSONOption,
|
||||
encoder,
|
||||
nil,
|
||||
nil,
|
||||
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
|
||||
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.3",
|
||||
"serialNumber": "urn:uuid:258d2616-5b1f-48cd-82a3-d6c95e262950",
|
||||
"serialNumber": "urn:uuid:326afa86-5620-4a80-8f2b-7f283b954b9b",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2022-01-14T22:47:00Z",
|
||||
"timestamp": "2022-02-10T17:19:38-05:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
@ -13,6 +13,7 @@
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"bom-ref": "163686ac6e30c752",
|
||||
"type": "file",
|
||||
"name": "/some/path",
|
||||
"version": ""
|
||||
@ -84,6 +85,12 @@
|
||||
"type": "operating-system",
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
"description": "debian",
|
||||
"swid": {
|
||||
"tagId": "debian",
|
||||
"name": "debian",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"name": "prettyName",
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.3",
|
||||
"serialNumber": "urn:uuid:8a84b1cf-e918-4842-a6a8-c7fdafc55bc0",
|
||||
"serialNumber": "urn:uuid:761a2036-0f25-4787-bf28-f5e9a7d9a0bf",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2022-01-14T22:47:00Z",
|
||||
"timestamp": "2022-02-10T17:19:38-05:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
@ -13,6 +13,7 @@
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"bom-ref": "4f9453fd20e0cf80",
|
||||
"type": "container",
|
||||
"name": "user-image-input",
|
||||
"version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
@ -55,7 +56,7 @@
|
||||
},
|
||||
{
|
||||
"name": "layerID",
|
||||
"value": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab"
|
||||
"value": "sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -84,7 +85,7 @@
|
||||
},
|
||||
{
|
||||
"name": "layerID",
|
||||
"value": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67"
|
||||
"value": "sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -92,6 +93,12 @@
|
||||
"type": "operating-system",
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
"description": "debian",
|
||||
"swid": {
|
||||
"tagId": "debian",
|
||||
"name": "debian",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"name": "prettyName",
|
||||
|
||||
Binary file not shown.
70
internal/formats/cyclonedx13xml/decoder_test.go
Normal file
70
internal/formats/cyclonedx13xml/decoder_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package cyclonedx13xml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_decodeXML(t *testing.T) {
|
||||
tests := []struct {
|
||||
file string
|
||||
err bool
|
||||
distro string
|
||||
packages []string
|
||||
}{
|
||||
{
|
||||
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
file: "snapshot/TestCycloneDxImageEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
file: "image-simple/Dockerfile",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.file, func(t *testing.T) {
|
||||
reader, err := os.Open("test-fixtures/" + test.file)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if test.err {
|
||||
err = Format().Validate(reader)
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
bom, err := Format().Decode(reader)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
split := strings.SplitN(test.distro, ":", 2)
|
||||
name := split[0]
|
||||
version := split[1]
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name)
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version)
|
||||
|
||||
pkgs:
|
||||
for _, pkg := range test.packages {
|
||||
split = strings.SplitN(pkg, ":", 2)
|
||||
name = split[0]
|
||||
version = split[1]
|
||||
for p := range bom.Artifacts.PackageCatalog.Enumerate() {
|
||||
if p.Name == name {
|
||||
assert.Equal(t, version, p.Version)
|
||||
continue pkgs
|
||||
}
|
||||
}
|
||||
assert.Fail(t, fmt.Sprintf("package should be present: %s", pkg))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,16 @@
|
||||
package cyclonedx13xml
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
)
|
||||
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.CycloneDxXMLOption,
|
||||
encoder,
|
||||
nil,
|
||||
nil,
|
||||
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
|
||||
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:16a426e7-fcc7-4b94-abd2-66c67569cc44" version="1">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:25133adc-01af-46ad-a198-002034e54097" version="1">
|
||||
<metadata>
|
||||
<timestamp>2022-01-14T22:46:49Z</timestamp>
|
||||
<timestamp>2022-02-10T17:18:31-05:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@ -9,7 +9,7 @@
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component type="file">
|
||||
<component bom-ref="163686ac6e30c752" type="file">
|
||||
<name>/some/path</name>
|
||||
<version></version>
|
||||
</component>
|
||||
@ -48,6 +48,8 @@
|
||||
<component type="operating-system">
|
||||
<name>debian</name>
|
||||
<version>1.2.3</version>
|
||||
<description>debian</description>
|
||||
<swid tagId="debian" name="debian" version="1.2.3"></swid>
|
||||
<properties>
|
||||
<property name="prettyName">debian</property>
|
||||
<property name="id">debian</property>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:a3d776c6-a2ca-4116-8b99-b3538dd1a460" version="1">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:e702d78f-9372-4458-ac38-be8e80fc3005" version="1">
|
||||
<metadata>
|
||||
<timestamp>2022-01-14T22:46:49Z</timestamp>
|
||||
<timestamp>2022-02-10T17:18:31-05:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@ -9,7 +9,7 @@
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component type="container">
|
||||
<component bom-ref="4f9453fd20e0cf80" type="container">
|
||||
<name>user-image-input</name>
|
||||
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
|
||||
</component>
|
||||
@ -31,7 +31,7 @@
|
||||
<property name="type">python</property>
|
||||
<property name="metadataType">PythonPackageMetadata</property>
|
||||
<property name="path">/somefile-1.txt</property>
|
||||
<property name="layerID">sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab</property>
|
||||
<property name="layerID">sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component type="library">
|
||||
@ -44,12 +44,14 @@
|
||||
<property name="type">deb</property>
|
||||
<property name="metadataType">DpkgMetadata</property>
|
||||
<property name="path">/somefile-2.txt</property>
|
||||
<property name="layerID">sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67</property>
|
||||
<property name="layerID">sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component type="operating-system">
|
||||
<name>debian</name>
|
||||
<version>1.2.3</version>
|
||||
<description>debian</description>
|
||||
<swid tagId="debian" name="debian" version="1.2.3"></swid>
|
||||
<properties>
|
||||
<property name="prettyName">debian</property>
|
||||
<property name="id">debian</property>
|
||||
|
||||
Binary file not shown.
@ -1,14 +1,12 @@
|
||||
package rust
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
package pkg
|
||||
|
||||
type CargoMetadata struct {
|
||||
Packages []pkg.CargoPackageMetadata `toml:"package"`
|
||||
Packages []CargoPackageMetadata `toml:"package"`
|
||||
}
|
||||
|
||||
// Pkgs returns all of the packages referenced within the Cargo.lock metadata.
|
||||
func (m CargoMetadata) Pkgs() []*pkg.Package {
|
||||
pkgs := make([]*pkg.Package, 0)
|
||||
func (m CargoMetadata) Pkgs() []*Package {
|
||||
pkgs := make([]*Package, 0)
|
||||
|
||||
for _, p := range m.Packages {
|
||||
if p.Dependencies == nil {
|
||||
@ -20,7 +20,7 @@ func parseCargoLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela
|
||||
return nil, nil, fmt.Errorf("unable to load Cargo.lock for parsing: %v", err)
|
||||
}
|
||||
|
||||
metadata := CargoMetadata{}
|
||||
metadata := pkg.CargoMetadata{}
|
||||
err = tree.Unmarshal(&metadata)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to parse Cargo.lock: %v", err)
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// MetadataType represents the data shape stored within pkg.Package.Metadata.
|
||||
type MetadataType string
|
||||
|
||||
@ -33,3 +37,17 @@ var AllMetadataTypes = []MetadataType{
|
||||
GolangBinMetadataType,
|
||||
PhpComposerJSONMetadataType,
|
||||
}
|
||||
|
||||
var MetadataTypeByName = map[MetadataType]reflect.Type{
|
||||
ApkMetadataType: reflect.TypeOf(ApkMetadata{}),
|
||||
DpkgMetadataType: reflect.TypeOf(DpkgMetadata{}),
|
||||
GemMetadataType: reflect.TypeOf(GemMetadata{}),
|
||||
JavaMetadataType: reflect.TypeOf(JavaMetadata{}),
|
||||
NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}),
|
||||
RpmdbMetadataType: reflect.TypeOf(RpmdbMetadata{}),
|
||||
PythonPackageMetadataType: reflect.TypeOf(PythonPackageMetadata{}),
|
||||
RustCargoPackageMetadataType: reflect.TypeOf(CargoMetadata{}),
|
||||
KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}),
|
||||
GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}),
|
||||
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft"
|
||||
@ -20,11 +21,27 @@ import (
|
||||
// encode-decode-encode loop which will detect lossy behavior in both directions.
|
||||
func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
||||
tests := []struct {
|
||||
format format.Option
|
||||
format format.Option
|
||||
redactor func(in []byte) []byte
|
||||
}{
|
||||
{
|
||||
format: format.JSONOption,
|
||||
},
|
||||
{
|
||||
format: format.CycloneDxJSONOption,
|
||||
redactor: func(in []byte) []byte {
|
||||
in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+").ReplaceAll(in, []byte{})
|
||||
return in
|
||||
},
|
||||
},
|
||||
{
|
||||
format: format.CycloneDxXMLOption,
|
||||
redactor: func(in []byte) []byte {
|
||||
in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+").ReplaceAll(in, []byte{})
|
||||
in = regexp.MustCompile("<timestamp>[^<]+</timestamp>").ReplaceAll(in, []byte{})
|
||||
return in
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(string(test.format), func(t *testing.T) {
|
||||
@ -33,6 +50,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
||||
|
||||
by1, err := syft.Encode(originalSBOM, test.format)
|
||||
assert.NoError(t, err)
|
||||
|
||||
newSBOM, newFormat, err := syft.Decode(bytes.NewReader(by1))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.format, newFormat)
|
||||
@ -40,6 +58,11 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
||||
by2, err := syft.Encode(*newSBOM, test.format)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if test.redactor != nil {
|
||||
by1 = test.redactor(by1)
|
||||
by2 = test.redactor(by2)
|
||||
}
|
||||
|
||||
if !assert.True(t, bytes.Equal(by1, by2)) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(by1), string(by2), true)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user