feat: update syft license concept to complex struct (#1743)

this PR makes the following changes to update the underlying license model to have more expressive capabilities
it also provides some guarantee's surrounding the license values themselves

- Licenses are updated from string -> pkg.LicenseSet which contain pkg.License with the following fields:
- original `Value` read by syft
- If it's possible to construct licenses will always have a valid SPDX expression for downstream consumption
- the above is run against a generated list of SPDX license ID to try and find the correct ID
- SPDX concluded vs declared is added to the new struct
- URL source for license is added to the new struct
- Location source is added to the new struct to show where the expression was pulled from
This commit is contained in:
Christopher Angelo Phillips 2023-05-15 16:23:39 -04:00 committed by GitHub
parent 8046f09562
commit 42fa9e4965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
126 changed files with 5165 additions and 1503 deletions

1
go.mod
View File

@ -56,6 +56,7 @@ require (
github.com/anchore/stereoscope v0.0.0-20230412183729-8602f1afc574 github.com/anchore/stereoscope v0.0.0-20230412183729-8602f1afc574
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/docker/docker v23.0.6+incompatible github.com/docker/docker v23.0.6+incompatible
github.com/github/go-spdx/v2 v2.1.2
github.com/go-git/go-billy/v5 v5.4.1 github.com/go-git/go-billy/v5 v5.4.1
github.com/go-git/go-git/v5 v5.6.1 github.com/go-git/go-git/v5 v5.6.1
github.com/google/go-containerregistry v0.15.1 github.com/google/go-containerregistry v0.15.1

2
go.sum
View File

@ -207,6 +207,8 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/github/go-spdx/v2 v2.1.2 h1:p+Tv0yMgcuO0/vnMe9Qh4tmUgYhI6AsLVlakZ/Sx+DM=
github.com/github/go-spdx/v2 v2.1.2/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=

View File

@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON encoder // JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "7.1.6" JSONSchemaVersion = "8.0.0"
) )

View File

@ -4,7 +4,10 @@ import (
"io" "io"
"github.com/google/licensecheck" "github.com/google/licensecheck"
"golang.org/x/exp/slices"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
) )
const ( const (
@ -13,21 +16,24 @@ const (
) )
// Parse scans the contents of a license file to attempt to determine the type of license it is // Parse scans the contents of a license file to attempt to determine the type of license it is
func Parse(reader io.Reader) (licenses []string, err error) { func Parse(reader io.Reader, l source.Location) (licenses []pkg.License, err error) {
licenses = make([]pkg.License, 0)
contents, err := io.ReadAll(reader) contents, err := io.ReadAll(reader)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cov := licensecheck.Scan(contents) cov := licensecheck.Scan(contents)
if cov.Percent < coverageThreshold {
// unknown or no licenses here?
return licenses, nil
}
if cov.Percent < float64(coverageThreshold) {
licenses = append(licenses, unknownLicenseType)
}
for _, m := range cov.Match { for _, m := range cov.Match {
if slices.Contains(licenses, m.ID) { lic := pkg.NewLicenseFromLocations(m.ID, l)
continue lic.Type = license.Concluded
licenses = append(licenses, lic)
} }
licenses = append(licenses, m.ID)
} return licenses, nil
return
} }

View File

@ -15,8 +15,10 @@ func NewStringSet(start ...string) StringSet {
} }
// Add a string to the set. // Add a string to the set.
func (s StringSet) Add(i string) { func (s StringSet) Add(i ...string) {
s[i] = struct{}{} for _, str := range i {
s[str] = struct{}{}
}
} }
// Remove a string from the set. // Remove a string from the set.
@ -41,3 +43,19 @@ func (s StringSet) ToSlice() []string {
sort.Strings(ret) sort.Strings(ret)
return ret return ret
} }
func (s StringSet) Equals(o StringSet) bool {
if len(s) != len(o) {
return false
}
for k := range s {
if !o.Contains(k) {
return false
}
}
return true
}
func (s StringSet) Empty() bool {
return len(s) < 1
}

View File

@ -78,7 +78,6 @@ func build() *jsonschema.Schema {
} }
documentSchema := reflector.ReflectFromType(reflect.TypeOf(&syftjsonModel.Document{})) documentSchema := reflector.ReflectFromType(reflect.TypeOf(&syftjsonModel.Document{}))
metadataSchema := reflector.ReflectFromType(reflect.TypeOf(&artifactMetadataContainer{})) metadataSchema := reflector.ReflectFromType(reflect.TypeOf(&artifactMetadataContainer{}))
// TODO: inject source definitions // TODO: inject source definitions
// inject the definitions of all metadatas into the schema definitions // inject the definitions of all metadatas into the schema definitions

File diff suppressed because it is too large Load Diff

32
syft/file/license.go Normal file
View File

@ -0,0 +1,32 @@
package file
import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/license"
)
type License struct {
Value string
SPDXExpression string
Type license.Type
LicenseEvidence *LicenseEvidence // evidence from license classifier
}
type LicenseEvidence struct {
Confidence int
Offset int
Extent int
}
func NewLicense(value string) License {
spdxExpression, err := license.ParseExpression(value)
if err != nil {
log.Trace("unable to parse license expression: %s, %w", value, err)
}
return License{
Value: value,
SPDXExpression: spdxExpression,
Type: license.Concluded,
}
}

View File

@ -78,7 +78,7 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
Name: c.Name, Name: c.Name,
Version: c.Version, Version: c.Version,
Locations: decodeLocations(values), Locations: decodeLocations(values),
Licenses: decodeLicenses(c), Licenses: pkg.NewLicenseSet(decodeLicenses(c)...),
CPEs: decodeCPEs(c), CPEs: decodeCPEs(c),
PURL: c.PackageURL, PURL: c.PackageURL,
} }

View File

@ -36,7 +36,6 @@ func Test_encodeComponentProperties(t *testing.T) {
OriginPackage: "libc-dev", OriginPackage: "libc-dev",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>", Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "0.7.2-r0", Version: "0.7.2-r0",
License: "BSD",
Architecture: "x86_64", Architecture: "x86_64",
URL: "http://alpinelinux.org", URL: "http://alpinelinux.org",
Description: "Meta package to pull in correct libc", Description: "Meta package to pull in correct libc",
@ -140,7 +139,6 @@ func Test_encodeComponentProperties(t *testing.T) {
Version: "0.9.2", Version: "0.9.2",
SourceRpm: "dive-0.9.2-1.src.rpm", SourceRpm: "dive-0.9.2-1.src.rpm",
Size: 12406784, Size: 12406784,
License: "MIT",
Vendor: "", Vendor: "",
Files: []pkg.RpmdbFileRecord{}, Files: []pkg.RpmdbFileRecord{},
}, },

View File

@ -322,8 +322,7 @@ func Test_missingDataDecode(t *testing.T) {
}, },
}, },
}) })
assert.Equal(t, pkg.Licenses.Empty(), true)
assert.Len(t, pkg.Licenses, 0)
} }
func Test_missingComponentsDecode(t *testing.T) { func Test_missingComponentsDecode(t *testing.T) {

View File

@ -50,7 +50,7 @@ func Test_encodeExternalReferences(t *testing.T) {
Language: pkg.Rust, Language: pkg.Rust,
Type: pkg.RustPkg, Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType, MetadataType: pkg.RustCargoPackageMetadataType,
Licenses: nil, Licenses: pkg.NewLicenseSet(),
Metadata: pkg.CargoPackageMetadata{ Metadata: pkg.CargoPackageMetadata{
Name: "ansi_term", Name: "ansi_term",
Version: "0.12.1", Version: "0.12.1",

View File

@ -1,53 +1,205 @@
package cyclonedxhelpers package cyclonedxhelpers
import ( import (
"fmt"
"strings"
"github.com/CycloneDX/cyclonedx-go" "github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/spdxlicense" "github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// This should be a function that just surfaces licenses already validated in the package struct
func encodeLicenses(p pkg.Package) *cyclonedx.Licenses { func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
lc := cyclonedx.Licenses{} spdxc, otherc, ex := separateLicenses(p)
for _, licenseName := range p.Licenses { if len(otherc) > 0 {
if value, exists := spdxlicense.ID(licenseName); exists { // found non spdx related licenses
lc = append(lc, cyclonedx.LicenseChoice{ // build individual license choices for each
// complex expressions are not combined and set as NAME fields
for _, e := range ex {
otherc = append(otherc, cyclonedx.LicenseChoice{
License: &cyclonedx.License{
Name: e,
},
})
}
otherc = append(otherc, spdxc...)
return &otherc
}
if len(spdxc) > 0 {
for _, l := range ex {
spdxc = append(spdxc, cyclonedx.LicenseChoice{
License: &cyclonedx.License{
Name: l,
},
})
}
return &spdxc
}
if len(ex) > 0 {
// only expressions found
var expressions cyclonedx.Licenses
expressions = append(expressions, cyclonedx.LicenseChoice{
Expression: mergeSPDX(ex),
})
return &expressions
}
return nil
}
func decodeLicenses(c *cyclonedx.Component) []pkg.License {
licenses := make([]pkg.License, 0)
if c == nil || c.Licenses == nil {
return licenses
}
for _, l := range *c.Licenses {
if l.License == nil {
continue
}
// these fields are mutually exclusive in the spec
switch {
case l.License.ID != "":
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.ID, l.License.URL))
case l.License.Name != "":
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL))
case l.Expression != "":
licenses = append(licenses, pkg.NewLicenseFromURLs(l.Expression, l.License.URL))
default:
}
}
return licenses
}
// nolint:funlen
func separateLicenses(p pkg.Package) (spdx, other cyclonedx.Licenses, expressions []string) {
ex := make([]string, 0)
spdxc := cyclonedx.Licenses{}
otherc := cyclonedx.Licenses{}
/*
pkg.License can be a couple of things: see above declarations
- Complex SPDX expression
- Some other Valid license ID
- Some non-standard non spdx license
To determine if an expression is a singular ID we first run it against the SPDX license list.
The weird case we run into is if there is a package with a license that is not a valid SPDX expression
and a license that is a valid complex expression. In this case we will surface the valid complex expression
as a license choice and the invalid expression as a license string.
*/
seen := make(map[string]bool)
for _, l := range p.Licenses.ToSlice() {
// singular expression case
// only ID field here since we guarantee that the license is valid
if value, exists := spdxlicense.ID(l.SPDXExpression); exists {
if !l.URL.Empty() {
processLicenseURLs(l, value, &spdxc)
continue
}
if _, exists := seen[value]; exists {
continue
}
// try making set of license choices to avoid duplicates
// only update if the license has more information
spdxc = append(spdxc, cyclonedx.LicenseChoice{
License: &cyclonedx.License{ License: &cyclonedx.License{
ID: value, ID: value,
}, },
}) })
seen[value] = true
// we have added the license to the SPDX license list check next license
continue continue
} }
// not found so append the licenseName as is if l.SPDXExpression != "" {
lc = append(lc, cyclonedx.LicenseChoice{ // COMPLEX EXPRESSION CASE
ex = append(ex, l.SPDXExpression)
continue
}
// license string that are not valid spdx expressions or ids
// we only use license Name here since we cannot guarantee that the license is a valid SPDX expression
if !l.URL.Empty() {
processLicenseURLs(l, "", &otherc)
continue
}
otherc = append(otherc, cyclonedx.LicenseChoice{
License: &cyclonedx.License{ License: &cyclonedx.License{
Name: licenseName, Name: l.Value,
}, },
}) })
} }
if len(lc) > 0 { return spdxc, otherc, ex
return &lc
}
return nil
} }
func decodeLicenses(c *cyclonedx.Component) (out []string) { func processLicenseURLs(l pkg.License, spdxID string, populate *cyclonedx.Licenses) {
if c.Licenses != nil { for _, url := range l.URL.ToSlice() {
for _, l := range *c.Licenses { if spdxID == "" {
if l.License != nil { *populate = append(*populate, cyclonedx.LicenseChoice{
var lic string License: &cyclonedx.License{
switch { URL: url,
case l.License.ID != "": Name: l.Value,
lic = l.License.ID },
case l.License.Name != "": })
lic = l.License.Name } else {
default: *populate = append(*populate, cyclonedx.LicenseChoice{
License: &cyclonedx.License{
ID: spdxID,
URL: url,
},
})
}
}
}
func mergeSPDX(ex []string) string {
var candidate []string
for _, e := range ex {
// if the expression does not have balanced parens add them
if !strings.HasPrefix(e, "(") && !strings.HasSuffix(e, ")") {
e = "(" + e + ")"
candidate = append(candidate, e)
}
}
if len(candidate) == 1 {
return reduceOuter(strings.Join(candidate, " AND "))
}
return strings.Join(candidate, " AND ")
}
func reduceOuter(expression string) string {
var (
sb strings.Builder
openCount int
)
for _, c := range expression {
if string(c) == "(" && openCount > 0 {
fmt.Fprintf(&sb, "%c", c)
}
if string(c) == "(" {
openCount++
continue continue
} }
out = append(out, lic) if string(c) == ")" && openCount > 1 {
fmt.Fprintf(&sb, "%c", c)
} }
if string(c) == ")" {
openCount--
continue
} }
fmt.Fprintf(&sb, "%c", c)
} }
return
return sb.String()
} }

View File

@ -6,6 +6,8 @@ import (
"github.com/CycloneDX/cyclonedx-go" "github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
@ -23,60 +25,170 @@ func Test_encodeLicense(t *testing.T) {
{ {
name: "no SPDX licenses", name: "no SPDX licenses",
input: pkg.Package{ input: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"made-up", pkg.NewLicense("RandomLicense"),
}, ),
}, },
expected: &cyclonedx.Licenses{ expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{Name: "made-up"}}, {
License: &cyclonedx.License{
Name: "RandomLicense",
},
},
}, },
}, },
{ {
name: "with SPDX license", name: "single SPDX ID and Non SPDX ID",
input: pkg.Package{ input: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"MIT", pkg.NewLicense("mit"),
}, pkg.NewLicense("FOOBAR"),
),
}, },
expected: &cyclonedx.Licenses{ expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "MIT"}}, {
License: &cyclonedx.License{
Name: "FOOBAR",
}, },
}, },
{ {
name: "with SPDX license expression", License: &cyclonedx.License{
input: pkg.Package{ ID: "MIT",
Licenses: []string{
"MIT",
"GPL-3.0",
}, },
}, },
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "MIT"}},
{License: &cyclonedx.License{ID: "GPL-3.0-only"}},
}, },
}, },
{ {
name: "cap insensitive", name: "with complex SPDX license expression",
input: pkg.Package{ input: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"gpl-3.0", pkg.NewLicense("MIT AND GPL-3.0-only"),
}, ),
}, },
expected: &cyclonedx.Licenses{ expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "GPL-3.0-only"}}, {
Expression: "MIT AND GPL-3.0-only",
},
}, },
}, },
{ {
name: "debian to spdx conversion", name: "with multiple complex SPDX license expression",
input: pkg.Package{ input: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"GPL-2", pkg.NewLicense("MIT AND GPL-3.0-only"),
}, pkg.NewLicense("MIT AND GPL-3.0-only WITH Classpath-exception-2.0"),
),
}, },
expected: &cyclonedx.Licenses{ expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "GPL-2.0-only"}}, {
Expression: "(MIT AND GPL-3.0-only) AND (MIT AND GPL-3.0-only WITH Classpath-exception-2.0)",
}, },
}, },
},
{
name: "with multiple URLs and expressions",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromURLs("MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicenseFromURLs("FakeLicense", "htts://someurl.com"),
),
},
expected: &cyclonedx.Licenses{
{
License: &cyclonedx.License{
Name: "FakeLicense",
URL: "htts://someurl.com",
},
},
{
License: &cyclonedx.License{
Name: "MIT AND GPL-3.0-only",
},
},
{
License: &cyclonedx.License{
ID: "MIT",
URL: "https://opensource.org/licenses/MIT",
},
},
{
License: &cyclonedx.License{
ID: "MIT",
URL: "https://spdx.org/licenses/MIT.html",
},
},
},
},
{
name: "with multiple values licenses are deduplicated",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("Apache-2"),
pkg.NewLicense("Apache-2.0"),
),
},
expected: &cyclonedx.Licenses{
{
License: &cyclonedx.License{
ID: "Apache-2.0",
},
},
},
},
{
name: "with multiple URLs and single with no URL",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicenseFromURLs("MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
),
},
expected: &cyclonedx.Licenses{
{
License: &cyclonedx.License{
ID: "MIT",
URL: "https://opensource.org/licenses/MIT",
},
},
{
License: &cyclonedx.License{
ID: "MIT",
URL: "https://spdx.org/licenses/MIT.html",
},
},
{
License: &cyclonedx.License{
Name: "MIT AND GPL-3.0-only",
},
},
},
},
// TODO: do we drop the non SPDX ID license and do a single expression
// OR do we keep the non SPDX ID license and do multiple licenses where the complex
// expressions are set as the NAME field?
//{
// name: "with multiple complex SPDX license expression and a non spdx id",
// input: pkg.Package{
// Licenses: []pkg.License{
// {
// SPDXExpression: "MIT AND GPL-3.0-only",
// },
// {
// SPDXExpression: "MIT AND GPL-3.0-only WITH Classpath-exception-2.0",
// },
// {
// Value: "FOOBAR",
// },
// },
// },
// expected: &cyclonedx.Licenses{
// {
// Expression: "(MIT AND GPL-3.0-only) AND (MIT AND GPL-3.0-only WITH Classpath-exception-2.0)",
// },
// },
//},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
@ -84,3 +196,81 @@ func Test_encodeLicense(t *testing.T) {
}) })
} }
} }
func TestDecodeLicenses(t *testing.T) {
tests := []struct {
name string
input *cyclonedx.Component
expected []pkg.License
}{
{
name: "no licenses",
input: &cyclonedx.Component{},
expected: []pkg.License{},
},
{
name: "no SPDX license ID or expression",
input: &cyclonedx.Component{
Licenses: &cyclonedx.Licenses{
{
License: &cyclonedx.License{
Name: "RandomLicense",
},
},
},
},
expected: []pkg.License{
{
Value: "RandomLicense",
// CycloneDX specification doesn't give a field for determining the license type
Type: license.Declared,
URL: internal.NewStringSet(),
},
},
},
{
name: "with SPDX license ID",
input: &cyclonedx.Component{
Licenses: &cyclonedx.Licenses{
{
License: &cyclonedx.License{
ID: "MIT",
},
},
},
},
expected: []pkg.License{
{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
URL: internal.NewStringSet(),
},
},
},
{
name: "with complex SPDX license expression",
input: &cyclonedx.Component{
Licenses: &cyclonedx.Licenses{
{
License: &cyclonedx.License{},
Expression: "MIT AND GPL-3.0-only WITH Classpath-exception-2.0",
},
},
},
expected: []pkg.License{
{
Value: "MIT AND GPL-3.0-only WITH Classpath-exception-2.0",
SPDXExpression: "MIT AND GPL-3.0-only WITH Classpath-exception-2.0",
Type: license.Declared,
URL: internal.NewStringSet(),
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, decodeLicenses(test.input))
})
}
}

View File

@ -4,10 +4,11 @@ import (
"strings" "strings"
"github.com/anchore/syft/internal/spdxlicense" "github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
func License(p pkg.Package) string { func License(p pkg.Package) (concluded, declared string) {
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license // source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
// The options to populate this field are limited to: // The options to populate this field are limited to:
// A valid SPDX License Expression as defined in Appendix IV; // A valid SPDX License Expression as defined in Appendix IV;
@ -17,35 +18,70 @@ func License(p pkg.Package) string {
// (ii) the SPDX file creator has made no attempt to determine this field; or // (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so). // (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if len(p.Licenses) == 0 { if p.Licenses.Empty() {
return NONE return NOASSERTION, NOASSERTION
} }
// take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/ // take all licenses and assume an AND expression;
parsedLicenses := parseLicenses(p.Licenses) // for information about license expressions see:
// https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
pc, pd := parseLicenses(p.Licenses.ToSlice())
for i, v := range parsedLicenses { for i, v := range pc {
if strings.HasPrefix(v, spdxlicense.LicenseRefPrefix) { if strings.HasPrefix(v, spdxlicense.LicenseRefPrefix) {
parsedLicenses[i] = SanitizeElementID(v) pc[i] = SanitizeElementID(v)
} }
} }
if len(parsedLicenses) == 0 { for i, v := range pd {
if strings.HasPrefix(v, spdxlicense.LicenseRefPrefix) {
pd[i] = SanitizeElementID(v)
}
}
return joinLicenses(pc), joinLicenses(pd)
}
func joinLicenses(licenses []string) string {
if len(licenses) == 0 {
return NOASSERTION return NOASSERTION
} }
return strings.Join(parsedLicenses, " AND ") var newLicenses []string
for _, v := range licenses {
// check if license does not start or end with parens
if !strings.HasPrefix(v, "(") && !strings.HasSuffix(v, ")") {
// if license contains AND, OR, or WITH, then wrap in parens
if strings.Contains(v, " AND ") ||
strings.Contains(v, " OR ") ||
strings.Contains(v, " WITH ") {
newLicenses = append(newLicenses, "("+v+")")
continue
}
}
newLicenses = append(newLicenses, v)
}
return strings.Join(newLicenses, " AND ")
} }
func parseLicenses(raw []string) (parsedLicenses []string) { func parseLicenses(raw []pkg.License) (concluded, declared []string) {
for _, l := range raw { for _, l := range raw {
if value, exists := spdxlicense.ID(l); exists { var candidate string
parsedLicenses = append(parsedLicenses, value) if l.SPDXExpression != "" {
candidate = l.SPDXExpression
} else { } else {
// we did not find a valid SPDX license ID so treat as separate license // we did not find a valid SPDX license ID so treat as separate license
otherLicense := spdxlicense.LicenseRefPrefix + l candidate = spdxlicense.LicenseRefPrefix + l.Value
parsedLicenses = append(parsedLicenses, otherLicense) }
switch l.Type {
case license.Concluded:
concluded = append(concluded, candidate)
case license.Declared:
declared = append(declared, candidate)
} }
} }
return return concluded, declared
} }

View File

@ -9,77 +9,120 @@ import (
) )
func Test_License(t *testing.T) { func Test_License(t *testing.T) {
type expected struct {
concluded string
declared string
}
tests := []struct { tests := []struct {
name string name string
input pkg.Package input pkg.Package
expected string expected expected
}{ }{
{ {
name: "no licenses", name: "no licenses",
input: pkg.Package{}, input: pkg.Package{},
expected: NONE, expected: expected{
concluded: "NOASSERTION",
declared: "NOASSERTION",
},
}, },
{ {
name: "no SPDX licenses", name: "no SPDX licenses",
input: pkg.Package{ input: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(pkg.NewLicense("made-up")),
"made-up",
}, },
expected: expected{
concluded: "NOASSERTION",
declared: "LicenseRef-made-up",
}, },
expected: "LicenseRef-made-up",
}, },
{ {
name: "with SPDX license", name: "with SPDX license",
input: pkg.Package{ input: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
"MIT",
}, },
expected: struct {
concluded string
declared string
}{
concluded: "NOASSERTION",
declared: "MIT",
}, },
expected: "MIT",
}, },
{ {
name: "with SPDX license expression", name: "with SPDX license expression",
input: pkg.Package{ input: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"MIT", pkg.NewLicense("MIT"),
"GPL-3.0", pkg.NewLicense("GPL-3.0-only"),
),
}, },
expected: expected{
concluded: "NOASSERTION",
// because we sort licenses alphabetically GPL ends up at the start
declared: "GPL-3.0-only AND MIT",
}, },
expected: "MIT AND GPL-3.0-only",
},
{
name: "cap insensitive",
input: pkg.Package{
Licenses: []string{
"gpl-3.0",
},
},
expected: "GPL-3.0-only",
},
{
name: "debian to spdx conversion",
input: pkg.Package{
Licenses: []string{
"GPL-2",
},
},
expected: "GPL-2.0-only",
}, },
{ {
name: "includes valid LicenseRef-", name: "includes valid LicenseRef-",
input: pkg.Package{ input: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"one thing first", pkg.NewLicense("one thing first"),
"two things/#$^second", pkg.NewLicense("two things/#$^second"),
"MIT", pkg.NewLicense("MIT"),
),
},
expected: expected{
concluded: "NOASSERTION",
// because we separate licenses between valid SPDX and non valid, valid ID always end at the front
declared: "MIT AND LicenseRef-one-thing-first AND LicenseRef-two-things----second",
}, },
}, },
expected: "LicenseRef-one-thing-first AND LicenseRef-two-things----second AND MIT", {
name: "join parentheses correctly",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("one thing first"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicense("MIT OR APACHE-2.0"),
),
},
expected: expected{
concluded: "NOASSERTION",
// because we separate licenses between valid SPDX and non valid, valid ID always end at the front
declared: "(MIT AND GPL-3.0-only) AND (MIT OR APACHE-2.0) AND LicenseRef-one-thing-first",
},
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, License(test.input)) c, d := License(test.input)
assert.Equal(t, test.expected.concluded, c)
assert.Equal(t, test.expected.declared, d)
})
}
}
func Test_joinLicenses(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{
name: "multiple licenses",
args: []string{"MIT", "GPL-3.0-only"},
want: "MIT AND GPL-3.0-only",
},
{
name: "multiple licenses with complex expressions",
args: []string{"MIT AND Apache", "GPL-3.0-only"},
want: "(MIT AND Apache) AND GPL-3.0-only",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, joinLicenses(tt.args), "joinLicenses(%v)", tt.args)
}) })
} }
} }

View File

@ -170,7 +170,8 @@ func toPackages(catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Packag
// If the Concluded License is not the same as the Declared License, a written explanation should be provided // If the Concluded License is not the same as the Declared License, a written explanation should be provided
// in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in // in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in
// the Comments on License field (section 7.16) is preferred. // the Comments on License field (section 7.16) is preferred.
license := License(p) // extract these correctly to the spdx license format
concluded, declared := License(p)
// two ways to get filesAnalyzed == true: // two ways to get filesAnalyzed == true:
// 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger // 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger
@ -274,7 +275,7 @@ func toPackages(catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Packag
// Cardinality: mandatory, one // Cardinality: mandatory, one
// Purpose: Contain the license the SPDX file creator has concluded as governing the // Purpose: Contain the license the SPDX file creator has concluded as governing the
// package or alternative values, if the governing license cannot be determined. // package or alternative values, if the governing license cannot be determined.
PackageLicenseConcluded: license, PackageLicenseConcluded: concluded,
// 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION" // 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one or many if filesAnalyzed is true / omitted; // Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
@ -286,7 +287,7 @@ func toPackages(catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Packag
// Purpose: List the licenses that have been declared by the authors of the package. // Purpose: List the licenses that have been declared by the authors of the package.
// Any license information that does not originate from the package authors, e.g. license // Any license information that does not originate from the package authors, e.g. license
// information from a third party repository, should not be included in this field. // information from a third party repository, should not be included in this field.
PackageLicenseDeclared: license, PackageLicenseDeclared: declared,
// 7.16: Comments on License // 7.16: Comments on License
// Cardinality: optional, one // Cardinality: optional, one
@ -534,10 +535,18 @@ func toFileTypes(metadata *source.FileMetadata) (ty []string) {
return ty return ty
} }
// other licenses are for licenses from the pkg.Package that do not have an SPDXExpression
// field. The spdxexpression field is only filled given a validated Value field.
func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense { func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense {
licenses := map[string]bool{} licenses := map[string]bool{}
for _, p := range catalog.Sorted() { for _, p := range catalog.Sorted() {
for _, license := range parseLicenses(p.Licenses) { declaredLicenses, concludedLicenses := parseLicenses(p.Licenses.ToSlice())
for _, license := range declaredLicenses {
if strings.HasPrefix(license, spdxlicense.LicenseRefPrefix) {
licenses[license] = true
}
}
for _, license := range concludedLicenses {
if strings.HasPrefix(license, spdxlicense.LicenseRefPrefix) { if strings.HasPrefix(license, spdxlicense.LicenseRefPrefix) {
licenses[license] = true licenses[license] = true
} }
@ -549,12 +558,12 @@ func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense {
sorted := maps.Keys(licenses) sorted := maps.Keys(licenses)
slices.Sort(sorted) slices.Sort(sorted)
for _, license := range sorted { for _, license := range sorted {
// separate the actual ID from the prefix // separate the found value from the prefix
// this only contains licenses that are not found on the SPDX License List
name := strings.TrimPrefix(license, spdxlicense.LicenseRefPrefix) name := strings.TrimPrefix(license, spdxlicense.LicenseRefPrefix)
result = append(result, &spdx.OtherLicense{ result = append(result, &spdx.OtherLicense{
LicenseIdentifier: SanitizeElementID(license), LicenseIdentifier: SanitizeElementID(license),
LicenseName: name, ExtractedText: name,
ExtractedText: NONE, // we probably should have some extracted text here, but this is good enough for now
}) })
} }
return result return result

View File

@ -448,46 +448,40 @@ func Test_OtherLicenses(t *testing.T) {
{ {
name: "no licenseRef", name: "no licenseRef",
pkg: pkg.Package{ pkg: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(),
"MIT",
},
}, },
expected: nil, expected: nil,
}, },
{ {
name: "single licenseRef", name: "single licenseRef",
pkg: pkg.Package{ pkg: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"un known", pkg.NewLicense("foobar"),
}, ),
}, },
expected: []*spdx.OtherLicense{ expected: []*spdx.OtherLicense{
{ {
LicenseIdentifier: "LicenseRef-un-known", LicenseIdentifier: "LicenseRef-foobar",
LicenseName: "un known", ExtractedText: "foobar",
ExtractedText: NONE,
}, },
}, },
}, },
{ {
name: "multiple licenseRef", name: "multiple licenseRef",
pkg: pkg.Package{ pkg: pkg.Package{
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"un known", pkg.NewLicense("internal made up license name"),
"not known %s", pkg.NewLicense("new apple license 2.0"),
"MIT", ),
},
}, },
expected: []*spdx.OtherLicense{ expected: []*spdx.OtherLicense{
{ {
LicenseIdentifier: "LicenseRef-not-known--s", LicenseIdentifier: "LicenseRef-internal-made-up-license-name",
LicenseName: "not known %s", ExtractedText: "internal made up license name",
ExtractedText: NONE,
}, },
{ {
LicenseIdentifier: "LicenseRef-un-known", LicenseIdentifier: "LicenseRef-new-apple-license-2.0",
LicenseName: "un known", ExtractedText: "new apple license 2.0",
ExtractedText: NONE,
}, },
}, },
}, },

View File

@ -14,6 +14,7 @@ import (
"github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/formats/common/util" "github.com/anchore/syft/syft/formats/common/util"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
@ -279,7 +280,7 @@ func toSyftPackage(p *spdx.Package) *pkg.Package {
Type: info.typ, Type: info.typ,
Name: p.PackageName, Name: p.PackageName,
Version: p.PackageVersion, Version: p.PackageVersion,
Licenses: parseLicense(p.PackageLicenseDeclared), Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...),
CPEs: extractCPEs(p), CPEs: extractCPEs(p),
PURL: info.purl.String(), PURL: info.purl.String(),
Language: info.lang, Language: info.lang,
@ -292,6 +293,33 @@ func toSyftPackage(p *spdx.Package) *pkg.Package {
return &sP return &sP
} }
func parseSPDXLicenses(p *spdx.Package) []pkg.License {
licenses := make([]pkg.License, 0)
// concluded
if p.PackageLicenseConcluded != NOASSERTION && p.PackageLicenseConcluded != NONE && p.PackageLicenseConcluded != "" {
l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseConcluded))
l.Type = license.Concluded
licenses = append(licenses, l)
}
// declared
if p.PackageLicenseDeclared != NOASSERTION && p.PackageLicenseDeclared != NONE && p.PackageLicenseDeclared != "" {
l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseDeclared))
l.Type = license.Declared
licenses = append(licenses, l)
}
return licenses
}
func cleanSPDXID(id string) string {
if strings.HasPrefix(id, "LicenseRef-") {
return strings.TrimPrefix(id, "LicenseRef-")
}
return id
}
//nolint:funlen //nolint:funlen
func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface{}) { func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface{}) {
arch := info.qualifierValue(pkg.PURLQualifierArch) arch := info.qualifierValue(pkg.PURLQualifierArch)
@ -317,7 +345,6 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
OriginPackage: upstreamName, OriginPackage: upstreamName,
Maintainer: supplier, Maintainer: supplier,
Version: p.PackageVersion, Version: p.PackageVersion,
License: p.PackageLicenseDeclared,
Architecture: arch, Architecture: arch,
URL: p.PackageHomePage, URL: p.PackageHomePage,
Description: p.PackageDescription, Description: p.PackageDescription,
@ -330,17 +357,12 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
} else { } else {
epoch = &converted epoch = &converted
} }
license := p.PackageLicenseDeclared
if license == "" {
license = p.PackageLicenseConcluded
}
return pkg.RpmMetadataType, pkg.RpmMetadata{ return pkg.RpmMetadataType, pkg.RpmMetadata{
Name: p.PackageName, Name: p.PackageName,
Version: p.PackageVersion, Version: p.PackageVersion,
Epoch: epoch, Epoch: epoch,
Arch: arch, Arch: arch,
SourceRpm: upstreamValue, SourceRpm: upstreamValue,
License: license,
Vendor: originator, Vendor: originator,
} }
case pkg.DebPkg: case pkg.DebPkg:
@ -400,10 +422,3 @@ func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) {
} }
return cpes return cpes
} }
func parseLicense(l string) []string {
if l == NOASSERTION || l == NONE {
return nil
}
return strings.Split(l, " AND ")
}

View File

@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
"bomFormat": "CycloneDX", "bomFormat": "CycloneDX",
"specVersion": "1.4", "specVersion": "1.4",
"serialNumber": "urn:uuid:redacted", "serialNumber": "urn:uuid:1b71a5b4-4bc5-4548-a51a-212e631976cd",
"version": 1, "version": 1,
"metadata": { "metadata": {
"timestamp": "timestamp:redacted", "timestamp": "2023-05-08T14:40:32-04:00",
"tools": [ "tools": [
{ {
"vendor": "anchore", "vendor": "anchore",
@ -14,14 +14,14 @@
} }
], ],
"component": { "component": {
"bom-ref": "redacted", "bom-ref": "163686ac6e30c752",
"type": "file", "type": "file",
"name": "/some/path" "name": "/some/path"
} }
}, },
"components": [ "components": [
{ {
"bom-ref": "redacted", "bom-ref": "8c7e1242588c971a",
"type": "library", "type": "library",
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
@ -58,7 +58,7 @@
] ]
}, },
{ {
"bom-ref": "redacted", "bom-ref": "pkg:deb/debian/package-2@2.0.1?package-id=db4abfe497c180d3",
"type": "library", "type": "library",
"name": "package-2", "name": "package-2",
"version": "2.0.1", "version": "2.0.1",

View File

@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
"bomFormat": "CycloneDX", "bomFormat": "CycloneDX",
"specVersion": "1.4", "specVersion": "1.4",
"serialNumber": "urn:uuid:redacted", "serialNumber": "urn:uuid:1695d6ae-0ddf-4e77-9c9d-74df1bdd8d5b",
"version": 1, "version": 1,
"metadata": { "metadata": {
"timestamp": "timestamp:redacted", "timestamp": "2023-05-08T14:40:32-04:00",
"tools": [ "tools": [
{ {
"vendor": "anchore", "vendor": "anchore",
@ -14,15 +14,15 @@
} }
], ],
"component": { "component": {
"bom-ref": "redacted", "bom-ref": "38160ebc2a6876e8",
"type": "container", "type": "container",
"name": "user-image-input", "name": "user-image-input",
"version": "sha256:redacted" "version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
} }
}, },
"components": [ "components": [
{ {
"bom-ref": "redacted", "bom-ref": "ec2e0c93617507ef",
"type": "library", "type": "library",
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
@ -54,7 +54,7 @@
}, },
{ {
"name": "syft:location:0:layerID", "name": "syft:location:0:layerID",
"value": "sha256:redacted" "value": "sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777"
}, },
{ {
"name": "syft:location:0:path", "name": "syft:location:0:path",
@ -63,7 +63,7 @@
] ]
}, },
{ {
"bom-ref": "redacted", "bom-ref": "pkg:deb/debian/package-2@2.0.1?package-id=958443e2d9304af4",
"type": "library", "type": "library",
"name": "package-2", "name": "package-2",
"version": "2.0.1", "version": "2.0.1",
@ -84,7 +84,7 @@
}, },
{ {
"name": "syft:location:0:layerID", "name": "syft:location:0:layerID",
"value": "sha256:redacted" "value": "sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2"
}, },
{ {
"name": "syft:location:0:path", "name": "syft:location:0:path",

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func Test_decodeXML(t *testing.T) { func Test_decodeXML(t *testing.T) {
@ -34,7 +35,7 @@ func Test_decodeXML(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.file, func(t *testing.T) { t.Run(test.file, func(t *testing.T) {
reader, err := os.Open("test-fixtures/" + test.file) reader, err := os.Open("test-fixtures/" + test.file)
assert.NoError(t, err) require.NoError(t, err)
if test.err { if test.err {
err = Format().Validate(reader) err = Format().Validate(reader)
@ -44,7 +45,7 @@ func Test_decodeXML(t *testing.T) {
bom, err := Format().Decode(reader) bom, err := Format().Decode(reader)
assert.NoError(t, err) require.NoError(t, err)
split := strings.SplitN(test.distro, ":", 2) split := strings.SplitN(test.distro, ":", 2)
name := split[0] name := split[0]

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:2939b822-b9cb-489d-8a8b-4431b755031d" version="1"> <bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:60f4e726-e884-4ae3-9b0e-18a918fbb02e" version="1">
<metadata> <metadata>
<timestamp>2022-11-07T09:11:06-05:00</timestamp> <timestamp>2023-05-08T14:40:52-04:00</timestamp>
<tools> <tools>
<tool> <tool>
<vendor>anchore</vendor> <vendor>anchore</vendor>
@ -14,7 +14,7 @@
</component> </component>
</metadata> </metadata>
<components> <components>
<component bom-ref="1b1d0be59ac59d2c" type="library"> <component bom-ref="8c7e1242588c971a" type="library">
<name>package-1</name> <name>package-1</name>
<version>1.0.1</version> <version>1.0.1</version>
<licenses> <licenses>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:2896b5ce-2016-49e8-a422-239d662846c7" version="1"> <bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:c8894728-c156-4fc5-8f5d-3e397eede5a7" version="1">
<metadata> <metadata>
<timestamp>2022-11-07T09:11:06-05:00</timestamp> <timestamp>2023-05-08T14:40:52-04:00</timestamp>
<tools> <tools>
<tool> <tool>
<vendor>anchore</vendor> <vendor>anchore</vendor>
@ -9,13 +9,13 @@
<version>v0.42.0-bogus</version> <version>v0.42.0-bogus</version>
</tool> </tool>
</tools> </tools>
<component bom-ref="522dc6b135a55bb4" type="container"> <component bom-ref="38160ebc2a6876e8" type="container">
<name>user-image-input</name> <name>user-image-input</name>
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version> <version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
</component> </component>
</metadata> </metadata>
<components> <components>
<component bom-ref="66ba429119b8bec6" type="library"> <component bom-ref="ec2e0c93617507ef" type="library">
<name>package-1</name> <name>package-1</name>
<version>1.0.1</version> <version>1.0.1</version>
<licenses> <licenses>
@ -30,7 +30,7 @@
<property name="syft:package:language">python</property> <property name="syft:package:language">python</property>
<property name="syft:package:metadataType">PythonPackageMetadata</property> <property name="syft:package:metadataType">PythonPackageMetadata</property>
<property name="syft:package:type">python</property> <property name="syft:package:type">python</property>
<property name="syft:location:0:layerID">sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59</property> <property name="syft:location:0:layerID">sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777</property>
<property name="syft:location:0:path">/somefile-1.txt</property> <property name="syft:location:0:path">/somefile-1.txt</property>
</properties> </properties>
</component> </component>
@ -43,7 +43,7 @@
<property name="syft:package:foundBy">the-cataloger-2</property> <property name="syft:package:foundBy">the-cataloger-2</property>
<property name="syft:package:metadataType">DpkgMetadata</property> <property name="syft:package:metadataType">DpkgMetadata</property>
<property name="syft:package:type">deb</property> <property name="syft:package:type">deb</property>
<property name="syft:location:0:layerID">sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec</property> <property name="syft:location:0:layerID">sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2</property>
<property name="syft:location:0:path">/somefile-2.txt</property> <property name="syft:location:0:path">/somefile-2.txt</property>
<property name="syft:metadata:installedSize">0</property> <property name="syft:metadata:installedSize">0</property>
</properties> </properties>

View File

@ -162,7 +162,9 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
FoundBy: "the-cataloger-1", FoundBy: "the-cataloger-1",
Language: pkg.Python, Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
),
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "package-1", Name: "package-1",
Version: "1.0.1", Version: "1.0.1",
@ -268,7 +270,9 @@ func newDirectoryCatalog() *pkg.Collection {
), ),
Language: pkg.Python, Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
),
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "package-1", Name: "package-1",
Version: "1.0.1", Version: "1.0.1",
@ -319,7 +323,9 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection {
), ),
Language: pkg.Python, Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
),
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "package-1", Name: "package-1",
Version: "1.0.1", Version: "1.0.1",

View File

@ -3,23 +3,23 @@
"dataLicense": "CC0-1.0", "dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT", "SPDXID": "SPDXRef-DOCUMENT",
"name": "/some/path", "name": "/some/path",
"documentNamespace": "https://anchore.com/syft/dir/some/path-4029b5ec-6d70-4c0c-aedf-b61c8f5ea93c", "documentNamespace": "https://anchore.com/syft/dir/some/path-5ea40e59-d91a-4682-a016-da45ddd540e4",
"creationInfo": { "creationInfo": {
"licenseListVersion": "3.20", "licenseListVersion": "3.20",
"creators": [ "creators": [
"Organization: Anchore, Inc", "Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus" "Tool: syft-v0.42.0-bogus"
], ],
"created": "2023-05-02T18:24:17Z" "created": "2023-05-09T17:11:26Z"
}, },
"packages": [ "packages": [
{ {
"name": "package-1", "name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-1b1d0be59ac59d2c", "SPDXID": "SPDXRef-Package-python-package-1-9265397e5e15168a",
"versionInfo": "1.0.1", "versionInfo": "1.0.1",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1", "sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
"licenseConcluded": "MIT", "licenseConcluded": "NOASSERTION",
"licenseDeclared": "MIT", "licenseDeclared": "MIT",
"copyrightText": "NOASSERTION", "copyrightText": "NOASSERTION",
"externalRefs": [ "externalRefs": [
@ -41,8 +41,8 @@
"versionInfo": "2.0.1", "versionInfo": "2.0.1",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1", "sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
"licenseConcluded": "NONE", "licenseConcluded": "NOASSERTION",
"licenseDeclared": "NONE", "licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION", "copyrightText": "NOASSERTION",
"externalRefs": [ "externalRefs": [
{ {

View File

@ -3,23 +3,23 @@
"dataLicense": "CC0-1.0", "dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT", "SPDXID": "SPDXRef-DOCUMENT",
"name": "user-image-input", "name": "user-image-input",
"documentNamespace": "https://anchore.com/syft/image/user-image-input-6b0c6ff8-0f5f-4d95-8c1b-eb966d400804", "documentNamespace": "https://anchore.com/syft/image/user-image-input-2cc737fb-af51-4e4b-9395-cceabcc305eb",
"creationInfo": { "creationInfo": {
"licenseListVersion": "3.20", "licenseListVersion": "3.20",
"creators": [ "creators": [
"Organization: Anchore, Inc", "Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus" "Tool: syft-v0.42.0-bogus"
], ],
"created": "2023-05-02T18:24:18Z" "created": "2023-05-09T17:11:26Z"
}, },
"packages": [ "packages": [
{ {
"name": "package-1", "name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-66ba429119b8bec6", "SPDXID": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"versionInfo": "1.0.1", "versionInfo": "1.0.1",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt", "sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt",
"licenseConcluded": "MIT", "licenseConcluded": "NOASSERTION",
"licenseDeclared": "MIT", "licenseDeclared": "MIT",
"copyrightText": "NOASSERTION", "copyrightText": "NOASSERTION",
"externalRefs": [ "externalRefs": [
@ -41,8 +41,8 @@
"versionInfo": "2.0.1", "versionInfo": "2.0.1",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt", "sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt",
"licenseConcluded": "NONE", "licenseConcluded": "NOASSERTION",
"licenseDeclared": "NONE", "licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION", "copyrightText": "NOASSERTION",
"externalRefs": [ "externalRefs": [
{ {

View File

@ -3,23 +3,23 @@
"dataLicense": "CC0-1.0", "dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT", "SPDXID": "SPDXRef-DOCUMENT",
"name": "user-image-input", "name": "user-image-input",
"documentNamespace": "https://anchore.com/syft/image/user-image-input-ec2f9b25-22ca-46b8-b7f4-484994fe126c", "documentNamespace": "https://anchore.com/syft/image/user-image-input-1de3ac0e-5829-4294-9198-8d8fcdb5dd51",
"creationInfo": { "creationInfo": {
"licenseListVersion": "3.20", "licenseListVersion": "3.20",
"creators": [ "creators": [
"Organization: Anchore, Inc", "Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus" "Tool: syft-v0.42.0-bogus"
], ],
"created": "2023-05-02T18:24:18Z" "created": "2023-05-09T17:11:26Z"
}, },
"packages": [ "packages": [
{ {
"name": "package-1", "name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-66ba429119b8bec6", "SPDXID": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"versionInfo": "1.0.1", "versionInfo": "1.0.1",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt", "sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt",
"licenseConcluded": "MIT", "licenseConcluded": "NOASSERTION",
"licenseDeclared": "MIT", "licenseDeclared": "MIT",
"copyrightText": "NOASSERTION", "copyrightText": "NOASSERTION",
"externalRefs": [ "externalRefs": [
@ -41,8 +41,8 @@
"versionInfo": "2.0.1", "versionInfo": "2.0.1",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt", "sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt",
"licenseConcluded": "NONE", "licenseConcluded": "NOASSERTION",
"licenseDeclared": "NONE", "licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION", "copyrightText": "NOASSERTION",
"externalRefs": [ "externalRefs": [
{ {
@ -152,32 +152,32 @@
], ],
"relationships": [ "relationships": [
{ {
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-f1-5265a4dde3edbf7c", "relatedSpdxElement": "SPDXRef-File-f1-5265a4dde3edbf7c",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },
{ {
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-z1-f5-839d99ee67d9d174", "relatedSpdxElement": "SPDXRef-File-z1-f5-839d99ee67d9d174",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },
{ {
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-a1-f6-9c2f7510199b17f6", "relatedSpdxElement": "SPDXRef-File-a1-f6-9c2f7510199b17f6",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },
{ {
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-d2-f4-c641caa71518099f", "relatedSpdxElement": "SPDXRef-File-d2-f4-c641caa71518099f",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },
{ {
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-d1-f3-c6f5b29dca12661f", "relatedSpdxElement": "SPDXRef-File-d1-f3-c6f5b29dca12661f",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },
{ {
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", "spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd", "relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },

View File

@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0 DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT SPDXID: SPDXRef-DOCUMENT
DocumentName: foobar/baz DocumentName: foobar/baz
DocumentNamespace: https://anchore.com/syft/dir/foobar/baz-9c1f31fb-7c72-40a6-8c81-3a08590000a2 DocumentNamespace: https://anchore.com/syft/dir/foobar/baz-1813dede-1ac5-4c44-a640-4c56e213d575
LicenseListVersion: 3.20 LicenseListVersion: 3.20
Creator: Organization: Anchore, Inc Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus Creator: Tool: syft-v0.42.0-bogus
Created: 2023-05-02T18:24:33Z Created: 2023-05-09T17:11:49Z
##### Package: @at-sign ##### Package: @at-sign
@ -15,8 +15,8 @@ SPDXID: SPDXRef-Package---at-sign-3732f7a5679bdec4
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths: PackageSourceInfo: acquired package info from the following paths:
PackageLicenseConcluded: NONE PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NONE PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
##### Package: some/slashes ##### Package: some/slashes
@ -26,8 +26,8 @@ SPDXID: SPDXRef-Package--some-slashes-1345166d4801153b
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths: PackageSourceInfo: acquired package info from the following paths:
PackageLicenseConcluded: NONE PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NONE PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
##### Package: under_scores ##### Package: under_scores
@ -37,8 +37,8 @@ SPDXID: SPDXRef-Package--under-scores-290d5c77210978c1
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths: PackageSourceInfo: acquired package info from the following paths:
PackageLicenseConcluded: NONE PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NONE PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
##### Relationships ##### Relationships

View File

@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0 DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT SPDXID: SPDXRef-DOCUMENT
DocumentName: user-image-input DocumentName: user-image-input
DocumentNamespace: https://anchore.com/syft/image/user-image-input-5be37b11-b99a-47ff-8725-3984e323d129 DocumentNamespace: https://anchore.com/syft/image/user-image-input-96ea886a-3297-4847-b211-6da405ff1f8f
LicenseListVersion: 3.20 LicenseListVersion: 3.20
Creator: Organization: Anchore, Inc Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus Creator: Tool: syft-v0.42.0-bogus
Created: 2023-05-02T18:24:33Z Created: 2023-05-09T17:11:49Z
##### Unpackaged files ##### Unpackaged files
@ -54,8 +54,8 @@ PackageVersion: 2.0.1
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from DPKG DB: /somefile-2.txt PackageSourceInfo: acquired package info from DPKG DB: /somefile-2.txt
PackageLicenseConcluded: NONE PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NONE PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
@ -63,12 +63,12 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1 ##### Package: package-1
PackageName: package-1 PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-66ba429119b8bec6 SPDXID: SPDXRef-Package-python-package-1-125840abc1c66dd7
PackageVersion: 1.0.1 PackageVersion: 1.0.1
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from installed python package manifest file: /somefile-1.txt PackageSourceInfo: acquired package info from installed python package manifest file: /somefile-1.txt
PackageLicenseConcluded: MIT PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: MIT PackageLicenseDeclared: MIT
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:* ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:*
@ -76,11 +76,11 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships ##### Relationships
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174 Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6 Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT

View File

@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0 DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT SPDXID: SPDXRef-DOCUMENT
DocumentName: /some/path DocumentName: /some/path
DocumentNamespace: https://anchore.com/syft/dir/some/path-0f346656-6d10-4dec-b549-a256468cbd35 DocumentNamespace: https://anchore.com/syft/dir/some/path-f7bdb1ee-7fef-48e7-a386-6ee3836d4a28
LicenseListVersion: 3.20 LicenseListVersion: 3.20
Creator: Organization: Anchore, Inc Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus Creator: Tool: syft-v0.42.0-bogus
Created: 2023-05-02T18:24:33Z Created: 2023-05-09T17:11:49Z
##### Package: package-2 ##### Package: package-2
@ -16,8 +16,8 @@ PackageVersion: 2.0.1
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from DPKG DB: /some/path/pkg1 PackageSourceInfo: acquired package info from DPKG DB: /some/path/pkg1
PackageLicenseConcluded: NONE PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NONE PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
@ -25,12 +25,12 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1 ##### Package: package-1
PackageName: package-1 PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-1b1d0be59ac59d2c SPDXID: SPDXRef-Package-python-package-1-9265397e5e15168a
PackageVersion: 1.0.1 PackageVersion: 1.0.1
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from installed python package manifest file: /some/path/pkg1 PackageSourceInfo: acquired package info from installed python package manifest file: /some/path/pkg1
PackageLicenseConcluded: MIT PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: MIT PackageLicenseDeclared: MIT
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*

View File

@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0 DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT SPDXID: SPDXRef-DOCUMENT
DocumentName: user-image-input DocumentName: user-image-input
DocumentNamespace: https://anchore.com/syft/image/user-image-input-4ce1e7c7-642f-4428-bb44-1b48b8edf74d DocumentNamespace: https://anchore.com/syft/image/user-image-input-44d44a85-2207-4b51-bd73-d0c7b080f6d3
LicenseListVersion: 3.20 LicenseListVersion: 3.20
Creator: Organization: Anchore, Inc Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus Creator: Tool: syft-v0.42.0-bogus
Created: 2023-05-02T18:24:33Z Created: 2023-05-09T17:11:49Z
##### Package: package-2 ##### Package: package-2
@ -16,8 +16,8 @@ PackageVersion: 2.0.1
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from DPKG DB: /somefile-2.txt PackageSourceInfo: acquired package info from DPKG DB: /somefile-2.txt
PackageLicenseConcluded: NONE PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NONE PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:* ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1 ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
@ -25,12 +25,12 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1 ##### Package: package-1
PackageName: package-1 PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-66ba429119b8bec6 SPDXID: SPDXRef-Package-python-package-1-125840abc1c66dd7
PackageVersion: 1.0.1 PackageVersion: 1.0.1
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from installed python package manifest file: /somefile-1.txt PackageSourceInfo: acquired package info from installed python package manifest file: /somefile-1.txt
PackageLicenseConcluded: MIT PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: MIT PackageLicenseDeclared: MIT
PackageCopyrightText: NOASSERTION PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:* ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:*

View File

@ -61,7 +61,7 @@ func TestEncodeFullJSONDocument(t *testing.T) {
FoundBy: "the-cataloger-1", FoundBy: "the-cataloger-1",
Language: pkg.Python, Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "package-1", Name: "package-1",
Version: "1.0.1", Version: "1.0.1",

View File

@ -7,6 +7,7 @@ import (
"reflect" "reflect"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
@ -27,12 +28,50 @@ type PackageBasicData struct {
Type pkg.Type `json:"type"` Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"` FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"` Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"` Licenses licenses `json:"licenses"`
Language pkg.Language `json:"language"` Language pkg.Language `json:"language"`
CPEs []string `json:"cpes"` CPEs []string `json:"cpes"`
PURL string `json:"purl"` PURL string `json:"purl"`
} }
type licenses []License
type License struct {
Value string `json:"value"`
SPDXExpression string `json:"spdxExpression"`
Type license.Type `json:"type"`
URL []string `json:"url"`
Location []source.Location `json:"locations"`
}
func newModelLicensesFromValues(licenses []string) (ml []License) {
for _, v := range licenses {
expression, err := license.ParseExpression(v)
if err != nil {
log.Trace("could not find valid spdx expression for %s: %w", v, err)
}
ml = append(ml, License{
Value: v,
SPDXExpression: expression,
Type: license.Declared,
})
}
return ml
}
func (f *licenses) UnmarshalJSON(b []byte) error {
var licenses []License
if err := json.Unmarshal(b, &licenses); err != nil {
var simpleLicense []string
if err := json.Unmarshal(b, &simpleLicense); err != nil {
return fmt.Errorf("unable to unmarshal license: %w", err)
}
licenses = newModelLicensesFromValues(simpleLicense)
}
*f = licenses
return nil
}
// PackageCustomData contains ambiguous values (type-wise) from pkg.Package. // PackageCustomData contains ambiguous values (type-wise) from pkg.Package.
type PackageCustomData struct { type PackageCustomData struct {
MetadataType pkg.MetadataType `json:"metadataType,omitempty"` MetadataType pkg.MetadataType `json:"metadataType,omitempty"`

View File

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
@ -30,7 +31,14 @@ func TestUnmarshalPackageGolang(t *testing.T) {
"path": "/Users/hal/go/bin/syft" "path": "/Users/hal/go/bin/syft"
} }
], ],
"licenses": [], "licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": []
}
],
"language": "go", "language": "go",
"cpes": [], "cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0", "purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
@ -61,7 +69,20 @@ func TestUnmarshalPackageGolang(t *testing.T) {
"path": "/Users/hal/go/bin/syft" "path": "/Users/hal/go/bin/syft"
} }
], ],
"licenses": [], "licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": ["https://www.github.com"]
},
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"locations": [{"path": "/Users/hal/go/bin/syft"}]
}
],
"language": "go", "language": "go",
"cpes": [], "cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0" "purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
@ -71,6 +92,83 @@ func TestUnmarshalPackageGolang(t *testing.T) {
assert.Empty(t, p.Metadata) assert.Empty(t, p.Metadata)
}, },
}, },
{
name: "can handle package with []string licenses",
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-mod-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": ["MIT", "Apache-2.0"],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
}`),
assert: func(p *Package) {
assert.Equal(t, licenses{
{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
},
{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: license.Declared,
},
}, p.Licenses)
},
},
{
name: "can handle package with []pkg.License licenses",
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
"version": "v2.6.0",
"type": "go-module",
"foundBy": "go-mod-cataloger",
"locations": [
{
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared"
},
{
"value": "Apache-2.0",
"spdxExpression": "Apache-2.0",
"type": "declared"
}
],
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
}`),
assert: func(p *Package) {
assert.Equal(t, licenses{
{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
},
{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: license.Declared,
},
}, p.Licenses)
},
},
} }
for _, test := range tests { for _, test := range tests {
@ -152,9 +250,6 @@ func Test_unpackMetadata(t *testing.T) {
"layerID": "sha256:74ddd0ec08fa43d09f32636ba91a0a3053b02cb4627c35051aff89f853606b59" "layerID": "sha256:74ddd0ec08fa43d09f32636ba91a0a3053b02cb4627c35051aff89f853606b59"
} }
], ],
"licenses": [
"GPLv2+"
],
"language": "", "language": "",
"cpes": [ "cpes": [
"cpe:2.3:a:centos:acl:2.2.53-1.el8:*:*:*:*:*:*:*", "cpe:2.3:a:centos:acl:2.2.53-1.el8:*:*:*:*:*:*:*",

View File

@ -1,7 +1,7 @@
{ {
"artifacts": [ "artifacts": [
{ {
"id": "1b1d0be59ac59d2c", "id": "9265397e5e15168a",
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
"type": "python", "type": "python",
@ -12,7 +12,13 @@
} }
], ],
"licenses": [ "licenses": [
"MIT" {
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": [],
"locations": []
}
], ],
"language": "python", "language": "python",
"cpes": [ "cpes": [
@ -23,7 +29,6 @@
"metadata": { "metadata": {
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
"license": "",
"author": "", "author": "",
"authorEmail": "", "authorEmail": "",
"platform": "", "platform": "",
@ -87,5 +92,9 @@
"configuration": { "configuration": {
"config-key": "config-value" "config-key": "config-value"
} }
},
"schema": {
"version": "8.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-8.0.0.json"
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"artifacts": [ "artifacts": [
{ {
"id": "304a5a8e5958a49d", "id": "271e49ba46e0b601",
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
"type": "python", "type": "python",
@ -12,7 +12,13 @@
} }
], ],
"licenses": [ "licenses": [
"MIT" {
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": [],
"locations": []
}
], ],
"language": "python", "language": "python",
"cpes": [ "cpes": [
@ -23,7 +29,6 @@
"metadata": { "metadata": {
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
"license": "",
"author": "", "author": "",
"authorEmail": "", "authorEmail": "",
"platform": "", "platform": "",
@ -187,5 +192,9 @@
"configuration": { "configuration": {
"config-key": "config-value" "config-key": "config-value"
} }
},
"schema": {
"version": "8.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-8.0.0.json"
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"artifacts": [ "artifacts": [
{ {
"id": "66ba429119b8bec6", "id": "125840abc1c66dd7",
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
"type": "python", "type": "python",
@ -9,11 +9,17 @@
"locations": [ "locations": [
{ {
"path": "/somefile-1.txt", "path": "/somefile-1.txt",
"layerID": "sha256:7e139310bd6ce0956d65a70d26a6d31b240a4f47094a831638f05d381b6c424a" "layerID": "sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777"
} }
], ],
"licenses": [ "licenses": [
"MIT" {
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": [],
"locations": []
}
], ],
"language": "python", "language": "python",
"cpes": [ "cpes": [
@ -24,7 +30,6 @@
"metadata": { "metadata": {
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
"license": "",
"author": "", "author": "",
"authorEmail": "", "authorEmail": "",
"platform": "", "platform": "",
@ -40,7 +45,7 @@
"locations": [ "locations": [
{ {
"path": "/somefile-2.txt", "path": "/somefile-2.txt",
"layerID": "sha256:cc833bf31a480c064d65ca67ee37f77f0d0c8ab98eedde7b286ad1ef6f5bdcac" "layerID": "sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2"
} }
], ],
"licenses": [], "licenses": [],
@ -64,11 +69,11 @@
], ],
"artifactRelationships": [], "artifactRelationships": [],
"source": { "source": {
"id": "0af8fa79f5497297e4e32f3e03de14ac20ad695159df0ac8373e6543614b9a50", "id": "c8ac88bbaf3d1c036f6a1d601c3d52bafbf05571c97d68322e7cb3a7ecaa304f",
"type": "image", "type": "image",
"target": { "target": {
"userInput": "user-image-input", "userInput": "user-image-input",
"imageID": "sha256:0cb4395791986bda17562bd6f76811bb6f163f686e198397197ef8241bed58df", "imageID": "sha256:a3c61dc134d2f31b415c50324e75842d7f91622f39a89468e51938330b3fd3af",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json", "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [ "tags": [
@ -78,17 +83,17 @@
"layers": [ "layers": [
{ {
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:7e139310bd6ce0956d65a70d26a6d31b240a4f47094a831638f05d381b6c424a", "digest": "sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777",
"size": 22 "size": 22
}, },
{ {
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:cc833bf31a480c064d65ca67ee37f77f0d0c8ab98eedde7b286ad1ef6f5bdcac", "digest": "sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2",
"size": 16 "size": 16
} }
], ],
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzEsImRpZ2VzdCI6InNoYTI1NjowY2I0Mzk1NzkxOTg2YmRhMTc1NjJiZDZmNzY4MTFiYjZmMTYzZjY4NmUxOTgzOTcxOTdlZjgyNDFiZWQ1OGRmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo3ZTEzOTMxMGJkNmNlMDk1NmQ2NWE3MGQyNmE2ZDMxYjI0MGE0ZjQ3MDk0YTgzMTYzOGYwNWQzODFiNmM0MjRhIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmNjODMzYmYzMWE0ODBjMDY0ZDY1Y2E2N2VlMzdmNzdmMGQwYzhhYjk4ZWVkZGU3YjI4NmFkMWVmNmY1YmRjYWMifV19", "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1NjphM2M2MWRjMTM0ZDJmMzFiNDE1YzUwMzI0ZTc1ODQyZDdmOTE2MjJmMzlhODk0NjhlNTE5MzgzMzBiM2ZkM2FmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjphYjYyMDE2ZjliZWM3Mjg2YWY2NTYwNDA4MTU2NGNhZGVlYjM2NGE0OGZhY2EyMzQ2YzNmNWE1YTFmNWVmNzc3In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmYxODAzODQ1YjY3NDdkOTRkNmU0ZWNjZTIzMzE0NTdlNWYxYzRmYjk3ZGU1MjE2ZjM5MmE3NmY0NTgyZjYzYjIifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjMtMDQtMThUMTQ6MDk6NDIuMzAxMDI2MzhaIiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjMtMDQtMThUMTQ6MDk6NDIuMjg3OTQyNzEzWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMy0wNC0xOFQxNDowOTo0Mi4zMDEwMjYzOFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMi50eHQgL3NvbWVmaWxlLTIudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OjdlMTM5MzEwYmQ2Y2UwOTU2ZDY1YTcwZDI2YTZkMzFiMjQwYTRmNDcwOTRhODMxNjM4ZjA1ZDM4MWI2YzQyNGEiLCJzaGEyNTY6Y2M4MzNiZjMxYTQ4MGMwNjRkNjVjYTY3ZWUzN2Y3N2YwZDBjOGFiOThlZWRkZTdiMjg2YWQxZWY2ZjViZGNhYyJdfX0=", "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjMtMDQtMjFUMTk6MTA6MzcuNjUxODMxMjM0WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIzLTA0LTIxVDE5OjEwOjM3LjYwNzYxMzU1NVoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjMtMDQtMjFUMTk6MTA6MzcuNjUxODMxMjM0WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6YWI2MjAxNmY5YmVjNzI4NmFmNjU2MDQwODE1NjRjYWRlZWIzNjRhNDhmYWNhMjM0NmMzZjVhNWExZjVlZjc3NyIsInNoYTI1NjpmMTgwMzg0NWI2NzQ3ZDk0ZDZlNGVjY2UyMzMxNDU3ZTVmMWM0ZmI5N2RlNTIxNmYzOTJhNzZmNDU4MmY2M2IyIl19fQ==",
"repoDigests": [], "repoDigests": [],
"architecture": "", "architecture": "",
"os": "" "os": ""
@ -110,5 +115,9 @@
"configuration": { "configuration": {
"config-key": "config-value" "config-key": "config-value"
} }
},
"schema": {
"version": "8.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-8.0.0.json"
} }
} }

View File

@ -184,6 +184,24 @@ func toPackageModels(catalog *pkg.Collection) []model.Package {
return artifacts return artifacts
} }
func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) {
for _, l := range pkgLicenses {
// guarantee collection
locations := make([]source.Location, 0)
if v := l.Location.ToSlice(); v != nil {
locations = v
}
modelLicenses = append(modelLicenses, model.License{
Value: l.Value,
SPDXExpression: l.SPDXExpression,
Type: l.Type,
URL: l.URL.ToSlice(),
Location: locations,
})
}
return
}
// toPackageModel crates a new Package from the given pkg.Package. // toPackageModel crates a new Package from the given pkg.Package.
func toPackageModel(p pkg.Package) model.Package { func toPackageModel(p pkg.Package) model.Package {
var cpes = make([]string, len(p.CPEs)) var cpes = make([]string, len(p.CPEs))
@ -191,9 +209,11 @@ func toPackageModel(p pkg.Package) model.Package {
cpes[i] = cpe.String(c) cpes[i] = cpe.String(c)
} }
var licenses = make([]string, 0) // we want to make sure all catalogers are
if p.Licenses != nil { // initializing the array; this is a good choke point for this check
licenses = p.Licenses var licenses = make([]model.License, 0)
if !p.Licenses.Empty() {
licenses = toLicenseModel(p.Licenses.ToSlice())
} }
return model.Package{ return model.Package{

View File

@ -9,6 +9,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file" stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/cpe"
@ -101,6 +102,19 @@ func toSyftFiles(files []model.File) sbom.Artifacts {
return ret return ret
} }
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,
URL: internal.NewStringSet(l.URL...),
Location: source.NewLocationSet(l.Location...),
})
}
return
}
func toSyftFileType(ty string) stereoscopeFile.Type { func toSyftFileType(ty string) stereoscopeFile.Type {
switch ty { switch ty {
case "SymbolicLink": case "SymbolicLink":
@ -304,7 +318,7 @@ func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
Version: p.Version, Version: p.Version,
FoundBy: p.FoundBy, FoundBy: p.FoundBy,
Locations: source.NewLocationSet(p.Locations...), Locations: source.NewLocationSet(p.Locations...),
Licenses: p.Licenses, Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
Language: p.Language, Language: p.Language,
Type: p.Type, Type: p.Type,
CPEs: cpes, CPEs: cpes,

35
syft/license/license.go Normal file
View File

@ -0,0 +1,35 @@
// package license provides common methods for working with SPDX license data
package license
import (
"fmt"
"github.com/github/go-spdx/v2/spdxexp"
"github.com/anchore/syft/internal/spdxlicense"
)
type Type string
const (
Declared Type = "declared"
Concluded Type = "concluded"
)
func ParseExpression(expression string) (string, error) {
licenseID, exists := spdxlicense.ID(expression)
if exists {
return licenseID, nil
}
// If it doesn't exist initially in the SPDX list it might be a more complex expression
// ignored variable is any invalid expressions
// TODO: contribute to spdxexp to expose deprecated license IDs
// https://github.com/anchore/syft/issues/1814
valid, _ := spdxexp.ValidateLicenses([]string{expression})
if !valid {
return "", fmt.Errorf("failed to validate spdx expression: %s", expression)
}
return expression, nil
}

View File

@ -0,0 +1,42 @@
package license
import "testing"
func TestParseExpression(t *testing.T) {
tests := []struct {
name string
expression string
want string
wantErr bool
}{
{
name: "valid single ID expression returns SPDX ID",
expression: "mit",
want: "MIT",
wantErr: false,
},
{
name: "Valid SPDX expression returns SPDX expression",
expression: "MIT OR Apache-2.0",
want: "MIT OR Apache-2.0",
},
{
name: "Invalid SPDX expression returns error",
expression: "MIT OR Apache-2.0 OR invalid",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseExpression(tt.expression)
if (err != nil) != tt.wantErr {
t.Errorf("ParseExpression() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ParseExpression() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -20,13 +20,12 @@ type AlpmMetadata struct {
Description string `mapstructure:"desc" json:"description" cyclonedx:"description"` Description string `mapstructure:"desc" json:"description" cyclonedx:"description"`
Architecture string `mapstructure:"arch" json:"architecture" cyclonedx:"architecture"` Architecture string `mapstructure:"arch" json:"architecture" cyclonedx:"architecture"`
Size int `mapstructure:"size" json:"size" cyclonedx:"size"` Size int `mapstructure:"size" json:"size" cyclonedx:"size"`
Packager string `mapstructure:"packager" json:"packager" cyclonedx:"packager"` Packager string `mapstructure:"packager" json:"packager"`
License string `mapstructure:"license" json:"license" cyclonedx:"license"` URL string `mapstructure:"url" json:"url"`
URL string `mapstructure:"url" json:"url" cyclonedx:"url"` Validation string `mapstructure:"validation" json:"validation"`
Validation string `mapstructure:"validation" json:"validation" cyclonedx:"validation"` Reason int `mapstructure:"reason" json:"reason"`
Reason int `mapstructure:"reason" json:"reason" cyclonedx:"reason"` Files []AlpmFileRecord `mapstructure:"files" json:"files"`
Files []AlpmFileRecord `mapstructure:"files" json:"files" cyclonedx:"files"` Backup []AlpmFileRecord `mapstructure:"backup" json:"backup"`
Backup []AlpmFileRecord `mapstructure:"backup" json:"backup" cyclonedx:"backup"`
} }
type AlpmFileRecord struct { type AlpmFileRecord struct {

View File

@ -27,7 +27,6 @@ type ApkMetadata struct {
OriginPackage string `mapstructure:"o" json:"originPackage" cyclonedx:"originPackage"` OriginPackage string `mapstructure:"o" json:"originPackage" cyclonedx:"originPackage"`
Maintainer string `mapstructure:"m" json:"maintainer"` Maintainer string `mapstructure:"m" json:"maintainer"`
Version string `mapstructure:"V" json:"version"` Version string `mapstructure:"V" json:"version"`
License string `mapstructure:"L" json:"license"`
Architecture string `mapstructure:"A" json:"architecture"` Architecture string `mapstructure:"A" json:"architecture"`
URL string `mapstructure:"U" json:"url"` URL string `mapstructure:"U" json:"url"`
Description string `mapstructure:"T" json:"description"` Description string `mapstructure:"T" json:"description"`

View File

@ -47,7 +47,6 @@ func TestApkMetadata_UnmarshalJSON(t *testing.T) {
OriginPackage: "pax-utils", OriginPackage: "pax-utils",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>", Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "1.3.4-r0", Version: "1.3.4-r0",
License: "GPL-2.0-only",
Architecture: "x86_64", Architecture: "x86_64",
URL: "https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities", URL: "https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities",
Description: "Scan ELF binaries for stuff", Description: "Scan ELF binaries for stuff",
@ -86,7 +85,6 @@ func TestApkMetadata_UnmarshalJSON(t *testing.T) {
OriginPackage: "pax-utils", OriginPackage: "pax-utils",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>", Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "1.3.4-r0", Version: "1.3.4-r0",
License: "GPL-2.0-only",
Architecture: "x86_64", Architecture: "x86_64",
URL: "https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities", URL: "https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities",
Description: "Scan ELF binaries for stuff", Description: "Scan ELF binaries for stuff",

View File

@ -17,6 +17,49 @@ type expectedIndexes struct {
byPath map[string]*strset.Set byPath map[string]*strset.Set
} }
func TestCatalogMergePackageLicenses(t *testing.T) {
tests := []struct {
name string
pkgs []Package
expectedPkgs []Package
}{
{
name: "merges licenses of packages with equal ID",
pkgs: []Package{
{
id: "equal",
Licenses: NewLicenseSet(
NewLicensesFromValues("foo", "baq", "quz")...,
),
},
{
id: "equal",
Licenses: NewLicenseSet(
NewLicensesFromValues("bar", "baz", "foo", "qux")...,
),
},
},
expectedPkgs: []Package{
{
id: "equal",
Licenses: NewLicenseSet(
NewLicensesFromValues("foo", "baq", "quz", "qux", "bar", "baz")...,
),
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
collection := NewCollection(test.pkgs...)
for i, p := range collection.Sorted() {
assert.Equal(t, test.expectedPkgs[i].Licenses, p.Licenses)
}
})
}
}
func TestCatalogDeleteRemovesPackages(t *testing.T) { func TestCatalogDeleteRemovesPackages(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@ -13,15 +13,18 @@ import (
) )
func TestAlpmCataloger(t *testing.T) { func TestAlpmCataloger(t *testing.T) {
dbLocation := source.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/desc")
expectedPkgs := []pkg.Package{ expectedPkgs := []pkg.Package{
{ {
Name: "gmp", Name: "gmp",
Version: "6.2.1-2", Version: "6.2.1-2",
Type: pkg.AlpmPkg, Type: pkg.AlpmPkg,
FoundBy: "alpmdb-cataloger", FoundBy: "alpmdb-cataloger",
Licenses: []string{"LGPL3", "GPL"}, Licenses: pkg.NewLicenseSet(
Locations: source.NewLocationSet(source.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/desc")), pkg.NewLicenseFromLocations("LGPL3", dbLocation),
pkg.NewLicenseFromLocations("GPL", dbLocation),
),
Locations: source.NewLocationSet(dbLocation),
CPEs: nil, CPEs: nil,
PURL: "", PURL: "",
MetadataType: "AlpmMetadata", MetadataType: "AlpmMetadata",
@ -33,7 +36,6 @@ func TestAlpmCataloger(t *testing.T) {
Architecture: "x86_64", Architecture: "x86_64",
Size: 1044438, Size: 1044438,
Packager: "Antonio Rojas <arojas@archlinux.org>", Packager: "Antonio Rojas <arojas@archlinux.org>",
License: "LGPL3\nGPL",
URL: "https://gmplib.org/", URL: "https://gmplib.org/",
Validation: "pgp", Validation: "pgp",
Reason: 1, Reason: 1,

View File

@ -1,29 +1,33 @@
package alpm package alpm
import ( import (
"strings"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
func newPackage(m pkg.AlpmMetadata, release *linux.Release, locations ...source.Location) pkg.Package { func newPackage(m *parsedData, release *linux.Release, dbLocation source.Location) pkg.Package {
licenseCandidates := strings.Split(m.Licenses, "\n")
p := pkg.Package{ p := pkg.Package{
Name: m.Package, Name: m.Package,
Version: m.Version, Version: m.Version,
Locations: source.NewLocationSet(locations...), Locations: source.NewLocationSet(dbLocation),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation.WithoutAnnotations(), licenseCandidates...)...),
Type: pkg.AlpmPkg, Type: pkg.AlpmPkg,
Licenses: internal.SplitAny(m.License, " \n"),
PURL: packageURL(m, release), PURL: packageURL(m, release),
MetadataType: pkg.AlpmMetadataType, MetadataType: pkg.AlpmMetadataType,
Metadata: m, Metadata: m.AlpmMetadata,
} }
p.SetID() p.SetID()
return p return p
} }
func packageURL(m pkg.AlpmMetadata, distro *linux.Release) string { func packageURL(m *parsedData, distro *linux.Release) string {
if distro == nil || distro.ID != "arch" { if distro == nil || distro.ID != "arch" {
// note: there is no namespace variation (like with debian ID_LIKE for ubuntu ID, for example) // note: there is no namespace variation (like with debian ID_LIKE for ubuntu ID, for example)
return "" return ""

View File

@ -13,17 +13,20 @@ import (
func Test_PackageURL(t *testing.T) { func Test_PackageURL(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
metadata pkg.AlpmMetadata metadata *parsedData
distro linux.Release distro linux.Release
expected string expected string
}{ }{
{ {
name: "bad distro id", name: "bad distro id",
metadata: pkg.AlpmMetadata{ metadata: &parsedData{
Licenses: "",
AlpmMetadata: pkg.AlpmMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
Architecture: "a", Architecture: "a",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "something-else", ID: "something-else",
BuildID: "rolling", BuildID: "rolling",
@ -32,11 +35,14 @@ func Test_PackageURL(t *testing.T) {
}, },
{ {
name: "gocase", name: "gocase",
metadata: pkg.AlpmMetadata{ metadata: &parsedData{
Licenses: "",
AlpmMetadata: pkg.AlpmMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
Architecture: "a", Architecture: "a",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "arch", ID: "arch",
BuildID: "rolling", BuildID: "rolling",
@ -45,21 +51,27 @@ func Test_PackageURL(t *testing.T) {
}, },
{ {
name: "missing architecture", name: "missing architecture",
metadata: pkg.AlpmMetadata{ metadata: &parsedData{
Licenses: "",
AlpmMetadata: pkg.AlpmMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "arch", ID: "arch",
}, },
expected: "pkg:alpm/arch/p@v?distro=arch", expected: "pkg:alpm/arch/p@v?distro=arch",
}, },
{ {
metadata: pkg.AlpmMetadata{ metadata: &parsedData{
Licenses: "",
AlpmMetadata: pkg.AlpmMetadata{
Package: "python", Package: "python",
Version: "3.10.0", Version: "3.10.0",
Architecture: "any", Architecture: "any",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "arch", ID: "arch",
BuildID: "rolling", BuildID: "rolling",
@ -67,11 +79,14 @@ func Test_PackageURL(t *testing.T) {
expected: "pkg:alpm/arch/python@3.10.0?arch=any&distro=arch-rolling", expected: "pkg:alpm/arch/python@3.10.0?arch=any&distro=arch-rolling",
}, },
{ {
metadata: pkg.AlpmMetadata{ metadata: &parsedData{
Licenses: "",
AlpmMetadata: pkg.AlpmMetadata{
Package: "g plus plus", Package: "g plus plus",
Version: "v84", Version: "v84",
Architecture: "x86_64", Architecture: "x86_64",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "arch", ID: "arch",
BuildID: "rolling", BuildID: "rolling",
@ -80,12 +95,15 @@ func Test_PackageURL(t *testing.T) {
}, },
{ {
name: "add source information as qualifier", name: "add source information as qualifier",
metadata: pkg.AlpmMetadata{ metadata: &parsedData{
Licenses: "",
AlpmMetadata: pkg.AlpmMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
Architecture: "a", Architecture: "a",
BasePackage: "origin", BasePackage: "origin",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "arch", ID: "arch",
BuildID: "rolling", BuildID: "rolling",

View File

@ -31,8 +31,13 @@ var (
} }
) )
type parsedData struct {
Licenses string `mapstructure:"license"`
pkg.AlpmMetadata `mapstructure:",squash"`
}
func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
metadata, err := parseAlpmDBEntry(reader) data, err := parseAlpmDBEntry(reader)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -48,9 +53,10 @@ func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader
return nil, nil, err return nil, nil, err
} }
// The replace the files found the the pacman database with the files from the mtree These contain more metadata and // replace the files found the pacman database with the files from the mtree These contain more metadata and
// thus more useful. // thus more useful.
metadata.Files = pkgFiles // TODO: probably want to use MTREE and PKGINFO here
data.Files = pkgFiles
// We only really do this to get any backup database entries from the files database // We only really do this to get any backup database entries from the files database
files := filepath.Join(base, "files") files := filepath.Join(base, "files")
@ -62,23 +68,23 @@ func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} else if filesMetadata != nil { } else if filesMetadata != nil {
metadata.Backup = filesMetadata.Backup data.Backup = filesMetadata.Backup
} }
if metadata.Package == "" { if data.Package == "" {
return nil, nil, nil return nil, nil, nil
} }
return []pkg.Package{ return []pkg.Package{
newPackage( newPackage(
*metadata, data,
env.LinuxRelease, env.LinuxRelease,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
), ),
}, nil, nil }, nil, nil
} }
func parseAlpmDBEntry(reader io.Reader) (*pkg.AlpmMetadata, error) { func parseAlpmDBEntry(reader io.Reader) (*parsedData, error) {
scanner := newScanner(reader) scanner := newScanner(reader)
metadata, err := parseDatabase(scanner) metadata, err := parseDatabase(scanner)
if err != nil { if err != nil {
@ -128,8 +134,7 @@ func getFileReader(path string, resolver source.FileResolver) (io.Reader, error)
return dbContentReader, nil return dbContentReader, nil
} }
func parseDatabase(b *bufio.Scanner) (*pkg.AlpmMetadata, error) { func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
var entry pkg.AlpmMetadata
var err error var err error
pkgFields := make(map[string]interface{}) pkgFields := make(map[string]interface{})
for b.Scan() { for b.Scan() {
@ -181,16 +186,23 @@ func parseDatabase(b *bufio.Scanner) (*pkg.AlpmMetadata, error) {
pkgFields[key] = value pkgFields[key] = value
} }
} }
return parsePkgFiles(pkgFields)
}
func parsePkgFiles(pkgFields map[string]interface{}) (*parsedData, error) {
var entry parsedData
if err := mapstructure.Decode(pkgFields, &entry); err != nil { if err := mapstructure.Decode(pkgFields, &entry); err != nil {
return nil, fmt.Errorf("unable to parse ALPM metadata: %w", err) return nil, fmt.Errorf("unable to parse ALPM metadata: %w", err)
} }
if entry.Package == "" && len(entry.Files) == 0 && len(entry.Backup) == 0 {
return nil, nil
}
if entry.Backup == nil { if entry.Backup == nil {
entry.Backup = make([]pkg.AlpmFileRecord, 0) entry.Backup = make([]pkg.AlpmFileRecord, 0)
} }
if entry.Package == "" && len(entry.Files) == 0 && len(entry.Backup) == 0 {
return nil, nil
}
return &entry, nil return &entry, nil
} }

View File

@ -9,16 +9,18 @@ import (
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
func newPackage(d pkg.ApkMetadata, release *linux.Release, locations ...source.Location) pkg.Package { func newPackage(d parsedData, release *linux.Release, dbLocation source.Location) pkg.Package {
licenseStrings := strings.Split(d.License, " ")
p := pkg.Package{ p := pkg.Package{
Name: d.Package, Name: d.Package,
Version: d.Version, Version: d.Version,
Locations: source.NewLocationSet(locations...), Locations: source.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Licenses: strings.Split(d.License, " "), Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation, licenseStrings...)...),
PURL: packageURL(d, release), PURL: packageURL(d.ApkMetadata, release),
Type: pkg.ApkPkg, Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType, MetadataType: pkg.ApkMetadataType,
Metadata: d, Metadata: d.ApkMetadata,
} }
p.SetID() p.SetID()

View File

@ -15,17 +15,20 @@ import (
func Test_PackageURL(t *testing.T) { func Test_PackageURL(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
metadata pkg.ApkMetadata metadata parsedData
distro linux.Release distro linux.Release
expected string expected string
}{ }{
{ {
name: "non-alpine distro", name: "non-alpine distro",
metadata: pkg.ApkMetadata{ metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
Architecture: "a", Architecture: "a",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "something else", ID: "something else",
VersionID: "3.4.6", VersionID: "3.4.6",
@ -34,11 +37,14 @@ func Test_PackageURL(t *testing.T) {
}, },
{ {
name: "gocase", name: "gocase",
metadata: pkg.ApkMetadata{ metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
Architecture: "a", Architecture: "a",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "alpine", ID: "alpine",
VersionID: "3.4.6", VersionID: "3.4.6",
@ -47,10 +53,13 @@ func Test_PackageURL(t *testing.T) {
}, },
{ {
name: "missing architecture", name: "missing architecture",
metadata: pkg.ApkMetadata{ metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "alpine", ID: "alpine",
VersionID: "3.4.6", VersionID: "3.4.6",
@ -59,11 +68,14 @@ func Test_PackageURL(t *testing.T) {
}, },
// verify #351 // verify #351
{ {
metadata: pkg.ApkMetadata{ metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "g++", Package: "g++",
Version: "v84", Version: "v84",
Architecture: "am86", Architecture: "am86",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "alpine", ID: "alpine",
VersionID: "3.4.6", VersionID: "3.4.6",
@ -71,11 +83,14 @@ func Test_PackageURL(t *testing.T) {
expected: "pkg:apk/alpine/g++@v84?arch=am86&distro=alpine-3.4.6", expected: "pkg:apk/alpine/g++@v84?arch=am86&distro=alpine-3.4.6",
}, },
{ {
metadata: pkg.ApkMetadata{ metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "g plus plus", Package: "g plus plus",
Version: "v84", Version: "v84",
Architecture: "am86", Architecture: "am86",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "alpine", ID: "alpine",
VersionID: "3.15.0", VersionID: "3.15.0",
@ -84,12 +99,15 @@ func Test_PackageURL(t *testing.T) {
}, },
{ {
name: "add source information as qualifier", name: "add source information as qualifier",
metadata: pkg.ApkMetadata{ metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
Architecture: "a", Architecture: "a",
OriginPackage: "origin", OriginPackage: "origin",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "alpine", ID: "alpine",
VersionID: "3.4.6", VersionID: "3.4.6",
@ -98,11 +116,14 @@ func Test_PackageURL(t *testing.T) {
}, },
{ {
name: "wolfi distro", name: "wolfi distro",
metadata: pkg.ApkMetadata{ metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p", Package: "p",
Version: "v", Version: "v",
Architecture: "a", Architecture: "a",
}, },
},
distro: linux.Release{ distro: linux.Release{
ID: "wolfi", ID: "wolfi",
VersionID: "20221230", VersionID: "20221230",
@ -113,7 +134,7 @@ func Test_PackageURL(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
actual := packageURL(test.metadata, &test.distro) actual := packageURL(test.metadata.ApkMetadata, &test.distro)
if actual != test.expected { if actual != test.expected {
dmp := diffmatchpatch.New() dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true) diffs := dmp.DiffMain(test.expected, actual, true)

View File

@ -26,6 +26,11 @@ var (
repoRegex = regexp.MustCompile(`(?m)^https://.*\.alpinelinux\.org/alpine/v([^/]+)/([a-zA-Z0-9_]+)$`) repoRegex = regexp.MustCompile(`(?m)^https://.*\.alpinelinux\.org/alpine/v([^/]+)/([a-zA-Z0-9_]+)$`)
) )
type parsedData struct {
License string `mapstructure:"L" json:"license"`
pkg.ApkMetadata
}
// parseApkDB parses packages from a given APK installed DB file. For more // parseApkDB parses packages from a given APK installed DB file. For more
// information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec. // information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec.
// //
@ -33,15 +38,15 @@ var (
func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
var apks []pkg.ApkMetadata var apks []parsedData
var currentEntry pkg.ApkMetadata var currentEntry parsedData
entryParsingInProgress := false entryParsingInProgress := false
fileParsingCtx := newApkFileParsingContext() fileParsingCtx := newApkFileParsingContext()
// creating a dedicated append-like function here instead of using `append(...)` // creating a dedicated append-like function here instead of using `append(...)`
// below since there is nontrivial logic to be performed for each finalized apk // below since there is nontrivial logic to be performed for each finalized apk
// entry. // entry.
appendApk := func(p pkg.ApkMetadata) { appendApk := func(p parsedData) {
if files := fileParsingCtx.files; len(files) >= 1 { if files := fileParsingCtx.files; len(files) >= 1 {
// attached accumulated files to current package // attached accumulated files to current package
p.Files = files p.Files = files
@ -68,7 +73,7 @@ func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader s
entryParsingInProgress = false entryParsingInProgress = false
// zero-out currentEntry for use by any future entry // zero-out currentEntry for use by any future entry
currentEntry = pkg.ApkMetadata{} currentEntry = parsedData{}
continue continue
} }
@ -123,7 +128,7 @@ func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader s
pkgs := make([]pkg.Package, 0, len(apks)) pkgs := make([]pkg.Package, 0, len(apks))
for _, apk := range apks { for _, apk := range apks {
pkgs = append(pkgs, newPackage(apk, r, reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))) pkgs = append(pkgs, newPackage(apk, r, reader.Location))
} }
return pkgs, discoverPackageDependencies(pkgs), nil return pkgs, discoverPackageDependencies(pkgs), nil
@ -201,7 +206,7 @@ type apkField struct {
} }
//nolint:funlen //nolint:funlen
func (f apkField) apply(p *pkg.ApkMetadata, ctx *apkFileParsingContext) { func (f apkField) apply(p *parsedData, ctx *apkFileParsingContext) {
switch f.name { switch f.name {
// APKINDEX field parsing // APKINDEX field parsing
@ -347,7 +352,7 @@ func parseListValue(value string) []string {
return nil return nil
} }
func nilFieldsToEmptySlice(p *pkg.ApkMetadata) { func nilFieldsToEmptySlice(p *parsedData) {
if p.Dependencies == nil { if p.Dependencies == nil {
p.Dependencies = []string{} p.Dependencies = []string{}
} }

View File

@ -80,17 +80,26 @@ func TestExtraFileAttributes(t *testing.T) {
func TestSinglePackageDetails(t *testing.T) { func TestSinglePackageDetails(t *testing.T) {
tests := []struct { tests := []struct {
fixture string fixture string
expected pkg.ApkMetadata expected pkg.Package
}{ }{
{ {
fixture: "test-fixtures/single", fixture: "test-fixtures/single",
expected: pkg.ApkMetadata{ expected: pkg.Package{
Name: "musl-utils",
Version: "1.1.24-r2",
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicense("BSD"),
pkg.NewLicense("GPL2+"),
),
Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType,
Metadata: pkg.ApkMetadata{
Package: "musl-utils", Package: "musl-utils",
OriginPackage: "musl", OriginPackage: "musl",
Version: "1.1.24-r2", Version: "1.1.24-r2",
Description: "the musl c library (libc) implementation", Description: "the musl c library (libc) implementation",
Maintainer: "Timo Teräs <timo.teras@iki.fi>", Maintainer: "Timo Teräs <timo.teras@iki.fi>",
License: "MIT BSD GPL2+",
Architecture: "x86_64", Architecture: "x86_64",
URL: "https://musl.libc.org/", URL: "https://musl.libc.org/",
Size: 37944, Size: 37944,
@ -162,15 +171,23 @@ func TestSinglePackageDetails(t *testing.T) {
}, },
}, },
}, },
},
{ {
fixture: "test-fixtures/empty-deps-and-provides", fixture: "test-fixtures/empty-deps-and-provides",
expected: pkg.ApkMetadata{ expected: pkg.Package{
Name: "alpine-baselayout-data",
Version: "3.4.0-r0",
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("GPL-2.0-only"),
),
Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType,
Metadata: pkg.ApkMetadata{
Package: "alpine-baselayout-data", Package: "alpine-baselayout-data",
OriginPackage: "alpine-baselayout", OriginPackage: "alpine-baselayout",
Version: "3.4.0-r0", Version: "3.4.0-r0",
Description: "Alpine base dir structure and init scripts", Description: "Alpine base dir structure and init scripts",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>", Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
License: "GPL-2.0-only",
Architecture: "x86_64", Architecture: "x86_64",
URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout",
Size: 11664, Size: 11664,
@ -199,15 +216,24 @@ func TestSinglePackageDetails(t *testing.T) {
}, },
}, },
}, },
},
{ {
fixture: "test-fixtures/base", fixture: "test-fixtures/base",
expected: pkg.ApkMetadata{ expected: pkg.Package{
Name: "alpine-baselayout",
Version: "3.2.0-r6",
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("GPL-2.0-only"),
),
Type: pkg.ApkPkg,
PURL: "",
MetadataType: pkg.ApkMetadataType,
Metadata: pkg.ApkMetadata{
Package: "alpine-baselayout", Package: "alpine-baselayout",
OriginPackage: "alpine-baselayout", OriginPackage: "alpine-baselayout",
Version: "3.2.0-r6", Version: "3.2.0-r6",
Description: "Alpine base dir structure and init scripts", Description: "Alpine base dir structure and init scripts",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>", Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
License: "GPL-2.0-only",
Architecture: "x86_64", Architecture: "x86_64",
URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", URL: "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout",
Size: 19917, Size: 19917,
@ -649,32 +675,34 @@ func TestSinglePackageDetails(t *testing.T) {
}, },
}, },
}, },
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) { t.Run(test.fixture, func(t *testing.T) {
lrc := newLocationReadCloser(t, test.fixture) fixtureLocation := source.NewLocation(test.fixture)
test.expected.Locations = source.NewLocationSet(fixtureLocation)
pkgs, _, err := parseApkDB(nil, new(generic.Environment), lrc) licenses := test.expected.Licenses.ToSlice()
require.NoError(t, err) for i := range licenses {
require.Len(t, pkgs, 1) licenses[i].Location.Add(fixtureLocation)
metadata := pkgs[0].Metadata.(pkg.ApkMetadata)
if diff := cmp.Diff(test.expected, metadata); diff != "" {
t.Errorf("Entry mismatch (-want +got):\n%s", diff)
} }
test.expected.Licenses = pkg.NewLicenseSet(licenses...)
pkgtest.TestFileParser(t, test.fixture, parseApkDB, []pkg.Package{test.expected}, nil)
}) })
} }
} }
func TestMultiplePackages(t *testing.T) { func TestMultiplePackages(t *testing.T) {
fixture := "test-fixtures/multiple" fixture := "test-fixtures/multiple"
fixtureLocationSet := source.NewLocationSet(source.NewLocation(fixture)) location := source.NewLocation(fixture)
fixtureLocationSet := source.NewLocationSet(location)
expectedPkgs := []pkg.Package{ expectedPkgs := []pkg.Package{
{ {
Name: "libc-utils", Name: "libc-utils",
Version: "0.7.2-r0", Version: "0.7.2-r0",
Licenses: []string{"BSD"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD", location),
),
Type: pkg.ApkPkg, Type: pkg.ApkPkg,
PURL: "pkg:apk/alpine/libc-utils@0.7.2-r0?arch=x86_64&upstream=libc-dev&distro=alpine-3.12", PURL: "pkg:apk/alpine/libc-utils@0.7.2-r0?arch=x86_64&upstream=libc-dev&distro=alpine-3.12",
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
@ -684,7 +712,6 @@ func TestMultiplePackages(t *testing.T) {
OriginPackage: "libc-dev", OriginPackage: "libc-dev",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>", Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "0.7.2-r0", Version: "0.7.2-r0",
License: "BSD",
Architecture: "x86_64", Architecture: "x86_64",
URL: "http://alpinelinux.org", URL: "http://alpinelinux.org",
Description: "Meta package to pull in correct libc", Description: "Meta package to pull in correct libc",
@ -700,10 +727,14 @@ func TestMultiplePackages(t *testing.T) {
{ {
Name: "musl-utils", Name: "musl-utils",
Version: "1.1.24-r2", Version: "1.1.24-r2",
Licenses: []string{"MIT", "BSD", "GPL2+"},
Type: pkg.ApkPkg, Type: pkg.ApkPkg,
PURL: "pkg:apk/alpine/musl-utils@1.1.24-r2?arch=x86_64&upstream=musl&distro=alpine-3.12", PURL: "pkg:apk/alpine/musl-utils@1.1.24-r2?arch=x86_64&upstream=musl&distro=alpine-3.12",
Locations: fixtureLocationSet, Locations: fixtureLocationSet,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", location),
pkg.NewLicenseFromLocations("BSD", location),
pkg.NewLicenseFromLocations("GPL2+", location),
),
MetadataType: pkg.ApkMetadataType, MetadataType: pkg.ApkMetadataType,
Metadata: pkg.ApkMetadata{ Metadata: pkg.ApkMetadata{
Package: "musl-utils", Package: "musl-utils",
@ -711,7 +742,6 @@ func TestMultiplePackages(t *testing.T) {
Version: "1.1.24-r2", Version: "1.1.24-r2",
Description: "the musl c library (libc) implementation", Description: "the musl c library (libc) implementation",
Maintainer: "Timo Teräs <timo.teras@iki.fi>", Maintainer: "Timo Teräs <timo.teras@iki.fi>",
License: "MIT BSD GPL2+",
Architecture: "x86_64", Architecture: "x86_64",
URL: "https://musl.libc.org/", URL: "https://musl.libc.org/",
Size: 37944, Size: 37944,
@ -994,7 +1024,7 @@ func Test_discoverPackageDependencies(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
pkgs, wantRelationships := test.genFn() pkgs, wantRelationships := test.genFn()
gotRelationships := discoverPackageDependencies(pkgs) gotRelationships := discoverPackageDependencies(pkgs)
d := cmp.Diff(wantRelationships, gotRelationships, cmpopts.IgnoreUnexported(pkg.Package{}, source.LocationSet{})) d := cmp.Diff(wantRelationships, gotRelationships, cmpopts.IgnoreUnexported(pkg.Package{}, source.LocationSet{}, pkg.LicenseSet{}))
if d != "" { if d != "" {
t.Fail() t.Fail()
t.Log(d) t.Log(d)

View File

@ -10,12 +10,17 @@ import (
) )
func TestDpkgCataloger(t *testing.T) { func TestDpkgCataloger(t *testing.T) {
licenseLocation := source.NewVirtualLocation("/usr/share/doc/libpam-runtime/copyright", "/usr/share/doc/libpam-runtime/copyright")
expected := []pkg.Package{ expected := []pkg.Package{
{ {
Name: "libpam-runtime", Name: "libpam-runtime",
Version: "1.1.8-3.6", Version: "1.1.8-3.6",
FoundBy: "dpkgdb-cataloger", FoundBy: "dpkgdb-cataloger",
Licenses: []string{"GPL-1", "GPL-2", "LGPL-2.1"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("GPL-1", licenseLocation),
pkg.NewLicenseFromLocations("GPL-2", licenseLocation),
pkg.NewLicenseFromLocations("LGPL-2.1", licenseLocation),
),
Locations: source.NewLocationSet( Locations: source.NewLocationSet(
source.NewVirtualLocation("/var/lib/dpkg/status", "/var/lib/dpkg/status"), source.NewVirtualLocation("/var/lib/dpkg/status", "/var/lib/dpkg/status"),
source.NewVirtualLocation("/var/lib/dpkg/info/libpam-runtime.md5sums", "/var/lib/dpkg/info/libpam-runtime.md5sums"), source.NewVirtualLocation("/var/lib/dpkg/info/libpam-runtime.md5sums", "/var/lib/dpkg/info/libpam-runtime.md5sums"),

View File

@ -22,9 +22,12 @@ const (
) )
func newDpkgPackage(d pkg.DpkgMetadata, dbLocation source.Location, resolver source.FileResolver, release *linux.Release) pkg.Package { func newDpkgPackage(d pkg.DpkgMetadata, dbLocation source.Location, resolver source.FileResolver, release *linux.Release) pkg.Package {
// TODO: separate pr to license refactor, but explore extracting dpkg-specific license parsing into a separate function
licenses := make([]pkg.License, 0)
p := pkg.Package{ p := pkg.Package{
Name: d.Package, Name: d.Package,
Version: d.Version, Version: d.Version,
Licenses: pkg.NewLicenseSet(licenses...),
Locations: source.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Locations: source.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(d, release), PURL: packageURL(d, release),
Type: pkg.DebPkg, Type: pkg.DebPkg,
@ -93,8 +96,10 @@ func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pk
if copyrightReader != nil && copyrightLocation != nil { if copyrightReader != nil && copyrightLocation != nil {
defer internal.CloseAndLogError(copyrightReader, copyrightLocation.VirtualPath) defer internal.CloseAndLogError(copyrightReader, copyrightLocation.VirtualPath)
// attach the licenses // attach the licenses
p.Licenses = parseLicensesFromCopyright(copyrightReader) licenseStrs := parseLicensesFromCopyright(copyrightReader)
for _, licenseStr := range licenseStrs {
p.Licenses.Add(pkg.NewLicenseFromLocations(licenseStr, copyrightLocation.WithoutAnnotations()))
}
// keep a record of the file where this was discovered // keep a record of the file where this was discovered
p.Locations.Add(*copyrightLocation) p.Locations.Add(*copyrightLocation)
} }

View File

@ -307,6 +307,7 @@ Installed-Size: 10kib
Name: "apt", Name: "apt",
Type: "deb", Type: "deb",
PURL: "pkg:deb/debian/apt?distro=debian-10", PURL: "pkg:deb/debian/apt?distro=debian-10",
Licenses: pkg.NewLicenseSet(),
Locations: source.NewLocationSet(source.NewLocation("place")), Locations: source.NewLocationSet(source.NewLocation("place")),
MetadataType: "DpkgMetadata", MetadataType: "DpkgMetadata",
Metadata: pkg.DpkgMetadata{ Metadata: pkg.DpkgMetadata{

View File

@ -22,6 +22,7 @@ import (
"github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
@ -74,7 +75,7 @@ func modCacheResolver(modCacheDir string) source.WritableFileResolver {
return r return r
} }
func (c *goLicenses) getLicenses(resolver source.FileResolver, moduleName, moduleVersion string) (licenses []string, err error) { func (c *goLicenses) getLicenses(resolver source.FileResolver, moduleName, moduleVersion string) (licenses []pkg.License, err error) {
licenses, err = findLicenses(resolver, licenses, err = findLicenses(resolver,
fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, processCaps(moduleName), moduleVersion), fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, processCaps(moduleName), moduleVersion),
) )
@ -93,7 +94,7 @@ func (c *goLicenses) getLicenses(resolver source.FileResolver, moduleName, modul
return requireCollection(licenses), err return requireCollection(licenses), err
} }
func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]string, error) { func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]pkg.License, error) {
if !c.opts.searchLocalModCacheLicenses { if !c.opts.searchLocalModCacheLicenses {
return nil, nil return nil, nil
} }
@ -103,7 +104,7 @@ func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]s
return findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion)) return findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion))
} }
func (c *goLicenses) getLicensesFromRemote(moduleName, moduleVersion string) ([]string, error) { func (c *goLicenses) getLicensesFromRemote(moduleName, moduleVersion string) ([]pkg.License, error) {
if !c.opts.searchRemoteLicenses { if !c.opts.searchRemoteLicenses {
return nil, nil return nil, nil
} }
@ -148,14 +149,15 @@ func moduleSearchGlob(moduleName, moduleVersion string) string {
return fmt.Sprintf("%s/*", moduleDir(moduleName, moduleVersion)) return fmt.Sprintf("%s/*", moduleDir(moduleName, moduleVersion))
} }
func requireCollection(licenses []string) []string { func requireCollection(licenses []pkg.License) []pkg.License {
if licenses == nil { if licenses == nil {
return []string{} return make([]pkg.License, 0)
} }
return licenses return licenses
} }
func findLicenses(resolver source.FileResolver, globMatch string) (out []string, err error) { func findLicenses(resolver source.FileResolver, globMatch string) (out []pkg.License, err error) {
out = make([]pkg.License, 0)
if resolver == nil { if resolver == nil {
return return
} }
@ -172,7 +174,7 @@ func findLicenses(resolver source.FileResolver, globMatch string) (out []string,
if err != nil { if err != nil {
return nil, err return nil, err
} }
parsed, err := licenses.Parse(contents) parsed, err := licenses.Parse(contents, l)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -13,24 +13,42 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
func Test_LocalLicenseSearch(t *testing.T) { func Test_LocalLicenseSearch(t *testing.T) {
loc1 := source.NewLocation("github.com/someorg/somename@v0.3.2/LICENSE")
loc2 := source.NewLocation("github.com/!cap!o!r!g/!cap!project@v4.111.5/LICENSE.txt")
tests := []struct { tests := []struct {
name string name string
version string version string
expected string expected pkg.License
}{ }{
{ {
name: "github.com/someorg/somename", name: "github.com/someorg/somename",
version: "v0.3.2", version: "v0.3.2",
expected: "Apache-2.0", expected: pkg.License{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: license.Concluded,
Location: source.NewLocationSet(loc1),
URL: internal.NewStringSet(),
},
}, },
{ {
name: "github.com/CapORG/CapProject", name: "github.com/CapORG/CapProject",
version: "v4.111.5", version: "v4.111.5",
expected: "MIT", expected: pkg.License{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Concluded,
Location: source.NewLocationSet(loc2),
URL: internal.NewStringSet(),
},
}, },
} }
@ -39,10 +57,12 @@ func Test_LocalLicenseSearch(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
l := newGoLicenses(GoCatalogerOpts{ l := newGoLicenses(
GoCatalogerOpts{
searchLocalModCacheLicenses: true, searchLocalModCacheLicenses: true,
localModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"), localModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
}) },
)
licenses, err := l.getLicenses(source.EmptyResolver{}, test.name, test.version) licenses, err := l.getLicenses(source.EmptyResolver{}, test.name, test.version)
require.NoError(t, err) require.NoError(t, err)
@ -54,6 +74,9 @@ func Test_LocalLicenseSearch(t *testing.T) {
} }
func Test_RemoteProxyLicenseSearch(t *testing.T) { func Test_RemoteProxyLicenseSearch(t *testing.T) {
loc1 := source.NewLocation("github.com/someorg/somename@v0.3.2/LICENSE")
loc2 := source.NewLocation("github.com/!cap!o!r!g/!cap!project@v4.111.5/LICENSE.txt")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
uri := strings.TrimPrefix(strings.TrimSuffix(r.RequestURI, ".zip"), "/") uri := strings.TrimPrefix(strings.TrimSuffix(r.RequestURI, ".zip"), "/")
@ -94,17 +117,29 @@ func Test_RemoteProxyLicenseSearch(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
version string version string
expected string expected pkg.License
}{ }{
{ {
name: "github.com/someorg/somename", name: "github.com/someorg/somename",
version: "v0.3.2", version: "v0.3.2",
expected: "Apache-2.0", expected: pkg.License{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: license.Concluded,
Location: source.NewLocationSet(loc1),
URL: internal.NewStringSet(),
},
}, },
{ {
name: "github.com/CapORG/CapProject", name: "github.com/CapORG/CapProject",
version: "v4.111.5", version: "v4.111.5",
expected: "MIT", expected: pkg.License{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Concluded,
Location: source.NewLocationSet(loc2),
URL: internal.NewStringSet(),
},
}, },
} }

View File

@ -24,7 +24,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(resolver source.FileResolver, dep
p := pkg.Package{ p := pkg.Package{
Name: dep.Path, Name: dep.Path,
Version: dep.Version, Version: dep.Version,
Licenses: licenses, Licenses: pkg.NewLicenseSet(licenses...),
PURL: packageURL(dep.Path, dep.Version), PURL: packageURL(dep.Path, dep.Version),
Language: pkg.Go, Language: pkg.Go,
Type: pkg.GoModulePkg, Type: pkg.GoModulePkg,

View File

@ -497,9 +497,6 @@ func TestBuildGoPkgInfo(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
for i := range test.expected { for i := range test.expected {
p := &test.expected[i] p := &test.expected[i]
if p.Licenses == nil {
p.Licenses = []string{}
}
p.SetID() p.SetID()
} }
location := source.NewLocationFromCoordinates( location := source.NewLocationFromCoordinates(

View File

@ -50,7 +50,7 @@ func (c *goModCataloger) parseGoModFile(resolver source.FileResolver, _ *generic
packages[m.Mod.Path] = pkg.Package{ packages[m.Mod.Path] = pkg.Package{
Name: m.Mod.Path, Name: m.Mod.Path,
Version: m.Mod.Version, Version: m.Mod.Version,
Licenses: licenses, Licenses: pkg.NewLicenseSet(licenses...),
Locations: source.NewLocationSet(reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Locations: source.NewLocationSet(reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(m.Mod.Path, m.Mod.Version), PURL: packageURL(m.Mod.Path, m.Mod.Version),
Language: pkg.Go, Language: pkg.Go,
@ -72,7 +72,7 @@ func (c *goModCataloger) parseGoModFile(resolver source.FileResolver, _ *generic
packages[m.New.Path] = pkg.Package{ packages[m.New.Path] = pkg.Package{
Name: m.New.Path, Name: m.New.Path,
Version: m.New.Version, Version: m.New.Version,
Licenses: licenses, Licenses: pkg.NewLicenseSet(licenses...),
Locations: source.NewLocationSet(reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Locations: source.NewLocationSet(reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(m.New.Path, m.New.Version), PURL: packageURL(m.New.Path, m.New.Version),
Language: pkg.Go, Language: pkg.Go,

View File

@ -88,12 +88,6 @@ func TestParseGoMod(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) { t.Run(test.fixture, func(t *testing.T) {
for i := range test.expected {
p := &test.expected[i]
if p.Licenses == nil {
p.Licenses = []string{}
}
}
c := goModCataloger{} c := goModCataloger{}
pkgtest.NewCatalogTester(). pkgtest.NewCatalogTester().
FromFile(t, test.fixture). FromFile(t, test.fixture).
@ -154,12 +148,6 @@ func Test_GoSumHashes(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) { t.Run(test.fixture, func(t *testing.T) {
for i := range test.expected {
p := &test.expected[i]
if p.Licenses == nil {
p.Licenses = []string{}
}
}
pkgtest.NewCatalogTester(). pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture). FromDirectory(t, test.fixture).
Expects(test.expected, nil). Expects(test.expected, nil).

View File

@ -21,6 +21,7 @@ import (
) )
type locationComparer func(x, y source.Location) bool type locationComparer func(x, y source.Location) bool
type licenseComparer func(x, y pkg.License) bool
type CatalogTester struct { type CatalogTester struct {
expectedPkgs []pkg.Package expectedPkgs []pkg.Package
@ -36,12 +37,14 @@ type CatalogTester struct {
wantErr require.ErrorAssertionFunc wantErr require.ErrorAssertionFunc
compareOptions []cmp.Option compareOptions []cmp.Option
locationComparer locationComparer locationComparer locationComparer
licenseComparer licenseComparer
} }
func NewCatalogTester() *CatalogTester { func NewCatalogTester() *CatalogTester {
return &CatalogTester{ return &CatalogTester{
wantErr: require.NoError, wantErr: require.NoError,
locationComparer: DefaultLocationComparer, locationComparer: DefaultLocationComparer,
licenseComparer: DefaultLicenseComparer,
ignoreUnfulfilledPathResponses: map[string][]string{ ignoreUnfulfilledPathResponses: map[string][]string{
"FilesByPath": { "FilesByPath": {
// most catalogers search for a linux release, which will not be fulfilled in testing // most catalogers search for a linux release, which will not be fulfilled in testing
@ -59,6 +62,25 @@ func DefaultLocationComparer(x, y source.Location) bool {
return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.VirtualPath, y.VirtualPath) return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.VirtualPath, y.VirtualPath)
} }
func DefaultLicenseComparer(x, y pkg.License) bool {
return cmp.Equal(x, y, cmp.Comparer(DefaultLocationComparer), cmp.Comparer(
func(x, y source.LocationSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !DefaultLocationComparer(xe, ye) {
return false
}
}
return true
},
))
}
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester { func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
t.Helper() t.Helper()
@ -139,6 +161,26 @@ func (p *CatalogTester) IgnoreLocationLayer() *CatalogTester {
p.locationComparer = func(x, y source.Location) bool { p.locationComparer = func(x, y source.Location) bool {
return cmp.Equal(x.Coordinates.RealPath, y.Coordinates.RealPath) && cmp.Equal(x.VirtualPath, y.VirtualPath) return cmp.Equal(x.Coordinates.RealPath, y.Coordinates.RealPath) && cmp.Equal(x.VirtualPath, y.VirtualPath)
} }
// we need to update the license comparer to use the ignored location layer
p.licenseComparer = func(x, y pkg.License) bool {
return cmp.Equal(x, y, cmp.Comparer(p.locationComparer), cmp.Comparer(
func(x, y source.LocationSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !p.locationComparer(xe, ye) {
return false
}
}
return true
}))
}
return p return p
} }
@ -209,6 +251,7 @@ func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) {
} }
} }
// nolint:funlen
func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) { func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
t.Helper() t.Helper()
@ -233,6 +276,30 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi
return true return true
}, },
), ),
cmp.Comparer(
func(x, y pkg.LicenseSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !p.licenseComparer(xe, ye) {
return false
}
}
return true
},
),
cmp.Comparer(
p.locationComparer,
),
cmp.Comparer(
p.licenseComparer,
),
) )
{ {
@ -295,6 +362,30 @@ func AssertPackagesEqual(t *testing.T, a, b pkg.Package) {
return true return true
}, },
), ),
cmp.Comparer(
func(x, y pkg.LicenseSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !DefaultLicenseComparer(xe, ye) {
return false
}
}
return true
},
),
cmp.Comparer(
DefaultLocationComparer,
),
cmp.Comparer(
DefaultLicenseComparer,
),
} }
if diff := cmp.Diff(a, b, opts...); diff != "" { if diff := cmp.Diff(a, b, opts...); diff != "" {

View File

@ -185,11 +185,13 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
log.Warnf("failed to create digest for file=%q: %+v", j.archivePath, err) log.Warnf("failed to create digest for file=%q: %+v", j.archivePath, err)
} }
// we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest
licenses := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...)
return &pkg.Package{ return &pkg.Package{
Name: selectName(manifest, j.fileInfo), Name: selectName(manifest, j.fileInfo),
Version: selectVersion(manifest, j.fileInfo), Version: selectVersion(manifest, j.fileInfo),
Licenses: selectLicense(manifest),
Language: pkg.Java, Language: pkg.Java,
Licenses: pkg.NewLicenseSet(licenses...),
Locations: source.NewLocationSet( Locations: source.NewLocationSet(
j.location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), j.location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
), ),

View File

@ -99,7 +99,9 @@ func TestParseJar(t *testing.T) {
Name: "example-jenkins-plugin", Name: "example-jenkins-plugin",
Version: "1.0-SNAPSHOT", Version: "1.0-SNAPSHOT",
PURL: "pkg:maven/io.jenkins.plugins/example-jenkins-plugin@1.0-SNAPSHOT", PURL: "pkg:maven/io.jenkins.plugins/example-jenkins-plugin@1.0-SNAPSHOT",
Licenses: []string{"MIT License"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT License", source.NewLocation("test-fixtures/java-builds/packages/example-jenkins-plugin.hpi")),
),
Language: pkg.Java, Language: pkg.Java,
Type: pkg.JenkinsPluginPkg, Type: pkg.JenkinsPluginPkg,
MetadataType: pkg.JavaMetadataType, MetadataType: pkg.JavaMetadataType,
@ -150,7 +152,6 @@ func TestParseJar(t *testing.T) {
Name: "example-java-app-gradle", Name: "example-java-app-gradle",
Version: "0.1.0", Version: "0.1.0",
PURL: "pkg:maven/example-java-app-gradle/example-java-app-gradle@0.1.0", PURL: "pkg:maven/example-java-app-gradle/example-java-app-gradle@0.1.0",
Licenses: []string{},
Language: pkg.Java, Language: pkg.Java,
Type: pkg.JavaPkg, Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType, MetadataType: pkg.JavaMetadataType,
@ -205,7 +206,6 @@ func TestParseJar(t *testing.T) {
Name: "example-java-app-maven", Name: "example-java-app-maven",
Version: "0.1.0", Version: "0.1.0",
PURL: "pkg:maven/org.anchore/example-java-app-maven@0.1.0", PURL: "pkg:maven/org.anchore/example-java-app-maven@0.1.0",
Licenses: []string{},
Language: pkg.Java, Language: pkg.Java,
Type: pkg.JavaPkg, Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType, MetadataType: pkg.JavaMetadataType,

View File

@ -157,7 +157,7 @@ func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) stri
return "" return ""
} }
func selectLicense(manifest *pkg.JavaManifest) []string { func selectLicenses(manifest *pkg.JavaManifest) []string {
result := []string{} result := []string{}
if manifest == nil { if manifest == nil {
return result return result

View File

@ -19,7 +19,9 @@ func Test_JavascriptCataloger(t *testing.T) {
Locations: locationSet, Locations: locationSet,
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation("package-lock.json")),
),
MetadataType: pkg.NpmPackageLockJSONMetadataType, MetadataType: pkg.NpmPackageLockJSONMetadataType,
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", Integrity: "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw=="}, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", Integrity: "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw=="},
}, },
@ -42,7 +44,9 @@ func Test_JavascriptCataloger(t *testing.T) {
Locations: locationSet, Locations: locationSet,
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation("package-lock.json")),
),
MetadataType: pkg.NpmPackageLockJSONMetadataType, MetadataType: pkg.NpmPackageLockJSONMetadataType,
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/cowsay/-/cowsay-1.4.0.tgz", Integrity: "sha512-rdg5k5PsHFVJheO/pmE3aDg2rUDDTfPJau6yYkZYlHFktUz+UxbE+IgnUAEyyCyv4noL5ltxXD0gZzmHPCy/9g=="}, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/cowsay/-/cowsay-1.4.0.tgz", Integrity: "sha512-rdg5k5PsHFVJheO/pmE3aDg2rUDDTfPJau6yYkZYlHFktUz+UxbE+IgnUAEyyCyv4noL5ltxXD0gZzmHPCy/9g=="},
}, },

View File

@ -12,30 +12,30 @@ import (
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
func newPackageJSONPackage(u packageJSON, locations ...source.Location) pkg.Package { func newPackageJSONPackage(u packageJSON, indexLocation source.Location) pkg.Package {
licenses, err := u.licensesFromJSON() licenseCandidates, err := u.licensesFromJSON()
if err != nil { if err != nil {
log.Warnf("unable to extract licenses from javascript package.json: %+v", err) log.Warnf("unable to extract licenses from javascript package.json: %+v", err)
} }
license := pkg.NewLicensesFromLocation(indexLocation, licenseCandidates...)
p := pkg.Package{ p := pkg.Package{
Name: u.Name, Name: u.Name,
Version: u.Version, Version: u.Version,
Licenses: licenses,
PURL: packageURL(u.Name, u.Version), PURL: packageURL(u.Name, u.Version),
Locations: source.NewLocationSet(locations...), Locations: source.NewLocationSet(indexLocation),
Language: pkg.JavaScript, Language: pkg.JavaScript,
Licenses: pkg.NewLicenseSet(license...),
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
Name: u.Name, Name: u.Name,
Version: u.Version, Version: u.Version,
Description: u.Description,
Author: u.Author.AuthorString(), Author: u.Author.AuthorString(),
Homepage: u.Homepage, Homepage: u.Homepage,
URL: u.Repository.URL, URL: u.Repository.URL,
Licenses: licenses,
Private: u.Private, Private: u.Private,
Description: u.Description,
}, },
} }
@ -77,12 +77,6 @@ func newPackageLockV1Package(resolver source.FileResolver, location source.Locat
} }
func newPackageLockV2Package(resolver source.FileResolver, location source.Location, name string, u lockPackage) pkg.Package { func newPackageLockV2Package(resolver source.FileResolver, location source.Location, name string, u lockPackage) pkg.Package {
var licenses []string
if u.License != nil {
licenses = u.License
}
return finalizeLockPkg( return finalizeLockPkg(
resolver, resolver,
location, location,
@ -90,10 +84,10 @@ func newPackageLockV2Package(resolver source.FileResolver, location source.Locat
Name: name, Name: name,
Version: u.Version, Version: u.Version,
Locations: source.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Locations: source.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(location, u.License...)...),
PURL: packageURL(name, u.Version), PURL: packageURL(name, u.Version),
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: licenses,
MetadataType: pkg.NpmPackageLockJSONMetadataType, MetadataType: pkg.NpmPackageLockJSONMetadataType,
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: u.Resolved, Integrity: u.Integrity}, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: u.Resolved, Integrity: u.Integrity},
}, },
@ -131,7 +125,8 @@ func newYarnLockPackage(resolver source.FileResolver, location source.Location,
} }
func finalizeLockPkg(resolver source.FileResolver, location source.Location, p pkg.Package) pkg.Package { func finalizeLockPkg(resolver source.FileResolver, location source.Location, p pkg.Package) pkg.Package {
p.Licenses = append(p.Licenses, addLicenses(p.Name, resolver, location)...) licenseCandidate := addLicenses(p.Name, resolver, location)
p.Licenses.Add(pkg.NewLicensesFromLocation(location, licenseCandidate...)...)
p.SetID() p.SetID()
return p return p
} }
@ -140,13 +135,13 @@ func addLicenses(name string, resolver source.FileResolver, location source.Loca
if resolver == nil { if resolver == nil {
return allLicenses return allLicenses
} }
dir := path.Dir(location.RealPath) dir := path.Dir(location.RealPath)
pkgPath := []string{dir, "node_modules"} pkgPath := []string{dir, "node_modules"}
pkgPath = append(pkgPath, strings.Split(name, "/")...) pkgPath = append(pkgPath, strings.Split(name, "/")...)
pkgPath = append(pkgPath, "package.json") pkgPath = append(pkgPath, "package.json")
pkgFile := path.Join(pkgPath...) pkgFile := path.Join(pkgPath...)
locations, err := resolver.FilesByPath(pkgFile) locations, err := resolver.FilesByPath(pkgFile)
if err != nil { if err != nil {
log.Debugf("an error occurred attempting to read: %s - %+v", pkgFile, err) log.Debugf("an error occurred attempting to read: %s - %+v", pkgFile, err)
return allLicenses return allLicenses

View File

@ -140,7 +140,7 @@ func (r *repository) UnmarshalJSON(b []byte) error {
return nil return nil
} }
type license struct { type npmPackageLicense struct {
Type string `json:"type"` Type string `json:"type"`
URL string `json:"url"` URL string `json:"url"`
} }
@ -154,7 +154,7 @@ func licenseFromJSON(b []byte) (string, error) {
} }
// then try as object (this format is deprecated) // then try as object (this format is deprecated)
var licenseObject license var licenseObject npmPackageLicense
err = json.Unmarshal(b, &licenseObject) err = json.Unmarshal(b, &licenseObject)
if err == nil { if err == nil {
return licenseObject.Type, nil return licenseObject.Type, nil
@ -178,7 +178,7 @@ func (p packageJSON) licensesFromJSON() ([]string, error) {
// The "licenses" field is deprecated. It should be inspected as a last resort. // The "licenses" field is deprecated. It should be inspected as a last resort.
if multiLicense != nil && err == nil { if multiLicense != nil && err == nil {
mapLicenses := func(licenses []license) []string { mapLicenses := func(licenses []npmPackageLicense) []string {
mappedLicenses := make([]string, len(licenses)) mappedLicenses := make([]string, len(licenses))
for i, l := range licenses { for i, l := range licenses {
mappedLicenses[i] = l.Type mappedLicenses[i] = l.Type
@ -192,8 +192,8 @@ func (p packageJSON) licensesFromJSON() ([]string, error) {
return nil, err return nil, err
} }
func licensesFromJSON(b []byte) ([]license, error) { func licensesFromJSON(b []byte) ([]npmPackageLicense, error) {
var licenseObject []license var licenseObject []npmPackageLicense
err := json.Unmarshal(b, &licenseObject) err := json.Unmarshal(b, &licenseObject)
if err == nil { if err == nil {
return licenseObject, nil return licenseObject, nil

View File

@ -22,8 +22,10 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6", Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6", PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"Artistic-2.0"},
Language: pkg.JavaScript, Language: pkg.JavaScript,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package.json")),
),
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
Name: "npm", Name: "npm",
@ -31,7 +33,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/", Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli", URL: "https://github.com/npm/cli",
Licenses: []string{"Artistic-2.0"},
Description: "a package manager for JavaScript", Description: "a package manager for JavaScript",
}, },
}, },
@ -43,8 +44,10 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6", Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6", PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"ISC"},
Language: pkg.JavaScript, Language: pkg.JavaScript,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("ISC", source.NewLocation("test-fixtures/pkg-json/package-license-object.json")),
),
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
Name: "npm", Name: "npm",
@ -52,7 +55,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/", Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli", URL: "https://github.com/npm/cli",
Licenses: []string{"ISC"},
Description: "a package manager for JavaScript", Description: "a package manager for JavaScript",
}, },
}, },
@ -64,7 +66,10 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6", Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6", PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"MIT", "Apache-2.0"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation("test-fixtures/pkg-json/package-license-objects.json")),
pkg.NewLicenseFromLocations("Apache-2.0", source.NewLocation("test-fixtures/pkg-json/package-license-objects.json")),
),
Language: pkg.JavaScript, Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
@ -73,7 +78,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/", Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli", URL: "https://github.com/npm/cli",
Licenses: []string{"MIT", "Apache-2.0"},
Description: "a package manager for JavaScript", Description: "a package manager for JavaScript",
}, },
}, },
@ -85,7 +89,6 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6", Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6", PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: nil,
Language: pkg.JavaScript, Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
@ -94,7 +97,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/", Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli", URL: "https://github.com/npm/cli",
Licenses: nil,
Description: "a package manager for JavaScript", Description: "a package manager for JavaScript",
}, },
}, },
@ -106,7 +108,6 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6", Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6", PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{},
Language: pkg.JavaScript, Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
@ -115,7 +116,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/", Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli", URL: "https://github.com/npm/cli",
Licenses: []string{},
Description: "a package manager for JavaScript", Description: "a package manager for JavaScript",
}, },
}, },
@ -127,7 +127,9 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6", Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6", PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"Artistic-2.0"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package-nested-author.json")),
),
Language: pkg.JavaScript, Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
@ -136,7 +138,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/", Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli", URL: "https://github.com/npm/cli",
Licenses: []string{"Artistic-2.0"},
Description: "a package manager for JavaScript", Description: "a package manager for JavaScript",
}, },
}, },
@ -148,7 +149,9 @@ func TestParsePackageJSON(t *testing.T) {
Version: "1.1.1", Version: "1.1.1",
PURL: "pkg:npm/function-bind@1.1.1", PURL: "pkg:npm/function-bind@1.1.1",
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation("test-fixtures/pkg-json/package-repo-string.json")),
),
Language: pkg.JavaScript, Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
@ -157,7 +160,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Raynos <raynos2@gmail.com>", Author: "Raynos <raynos2@gmail.com>",
Homepage: "https://github.com/Raynos/function-bind", Homepage: "https://github.com/Raynos/function-bind",
URL: "git://github.com/Raynos/function-bind.git", URL: "git://github.com/Raynos/function-bind.git",
Licenses: []string{"MIT"},
Description: "Implementation of Function.prototype.bind", Description: "Implementation of Function.prototype.bind",
}, },
}, },
@ -169,7 +171,9 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6", Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6", PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"Artistic-2.0"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package-private.json")),
),
Language: pkg.JavaScript, Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType, MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{ Metadata: pkg.NpmPackageJSONMetadata{
@ -178,7 +182,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/", Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli", URL: "https://github.com/npm/cli",
Licenses: []string{"Artistic-2.0"},
Private: true, Private: true,
Description: "a package manager for JavaScript", Description: "a package manager for JavaScript",
}, },

View File

@ -25,39 +25,6 @@ type packageLock struct {
Packages map[string]lockPackage Packages map[string]lockPackage
} }
// packageLockLicense
type packageLockLicense []string
func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) {
// The license field could be either a string or an array.
// 1. An array
var arr []string
if err := json.Unmarshal(data, &arr); err == nil {
*licenses = arr
return nil
}
// 2. A string
var str string
if err = json.Unmarshal(data, &str); err == nil {
*licenses = make([]string, 1)
(*licenses)[0] = str
return nil
}
// debug the content we did not expect
if len(data) > 0 {
log.WithFields("license", string(data)).Debug("Unable to parse the following `license` value in package-lock.json")
}
// 3. Unexpected
// In case we are unable to parse the license field,
// i.e if we have not covered the full specification,
// we do not want to throw an error, instead assign nil.
return nil
}
// lockDependency represents a single package dependency listed in the package.lock json file // lockDependency represents a single package dependency listed in the package.lock json file
type lockDependency struct { type lockDependency struct {
Version string `json:"version"` Version string `json:"version"`
@ -73,6 +40,9 @@ type lockPackage struct {
License packageLockLicense `json:"license"` License packageLockLicense `json:"license"`
} }
// packageLockLicense
type packageLockLicense []string
// parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages. // parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages.
func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
// in the case we find package-lock.json files in the node_modules directories, skip those // in the case we find package-lock.json files in the node_modules directories, skip those
@ -125,6 +95,36 @@ func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, read
return pkgs, nil, nil return pkgs, nil, nil
} }
func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) {
// The license field could be either a string or an array.
// 1. An array
var arr []string
if err := json.Unmarshal(data, &arr); err == nil {
*licenses = arr
return nil
}
// 2. A string
var str string
if err = json.Unmarshal(data, &str); err == nil {
*licenses = make([]string, 1)
(*licenses)[0] = str
return nil
}
// debug the content we did not expect
if len(data) > 0 {
log.WithFields("license", string(data)).Debug("Unable to parse the following `license` value in package-lock.json")
}
// 3. Unexpected
// In case we are unable to parse the license field,
// i.e if we have not covered the full specification,
// we do not want to throw an error, instead assign nil.
return nil
}
func getNameFromPath(path string) string { func getNameFromPath(path string) string {
parts := strings.Split(path, "node_modules/") parts := strings.Split(path, "node_modules/")
return parts[len(parts)-1] return parts[len(parts)-1]

View File

@ -139,7 +139,9 @@ func TestParsePackageLockV2(t *testing.T) {
PURL: "pkg:npm/%40types/prop-types@15.7.5", PURL: "pkg:npm/%40types/prop-types@15.7.5",
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
MetadataType: "NpmPackageLockJsonMetadata", MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", Integrity: "sha1-XxnSuFqY6VWANvajysyIGUIPBc8="}, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", Integrity: "sha1-XxnSuFqY6VWANvajysyIGUIPBc8="},
}, },
@ -149,7 +151,9 @@ func TestParsePackageLockV2(t *testing.T) {
PURL: "pkg:npm/%40types/react@18.0.17", PURL: "pkg:npm/%40types/react@18.0.17",
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
MetadataType: "NpmPackageLockJsonMetadata", MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", Integrity: "sha1-RYPZwyLWfv5LOak10iPtzHBQzPQ="}, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", Integrity: "sha1-RYPZwyLWfv5LOak10iPtzHBQzPQ="},
}, },
@ -159,7 +163,9 @@ func TestParsePackageLockV2(t *testing.T) {
PURL: "pkg:npm/%40types/scheduler@0.16.2", PURL: "pkg:npm/%40types/scheduler@0.16.2",
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
MetadataType: "NpmPackageLockJsonMetadata", MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", Integrity: "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk="}, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", Integrity: "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk="},
}, },
@ -169,7 +175,9 @@ func TestParsePackageLockV2(t *testing.T) {
PURL: "pkg:npm/csstype@3.1.0", PURL: "pkg:npm/csstype@3.1.0",
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"MIT"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
MetadataType: "NpmPackageLockJsonMetadata", MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", Integrity: "sha1-TdysNxjXh8+d8NG30VAzklyPKfI="}, Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", Integrity: "sha1-TdysNxjXh8+d8NG30VAzklyPKfI="},
}, },
@ -268,33 +276,35 @@ func TestParsePackageLockAlias(t *testing.T) {
}, },
} }
packageLockV1 := "test-fixtures/pkg-lock/alias-package-lock-1.json"
packageLockV2 := "test-fixtures/pkg-lock/alias-package-lock-2.json"
packageLocks := []string{packageLockV1, packageLockV2}
v2Pkg := pkg.Package{ v2Pkg := pkg.Package{
Name: "alias-check", Name: "alias-check",
Version: "1.0.0", Version: "1.0.0",
PURL: "pkg:npm/alias-check@1.0.0", PURL: "pkg:npm/alias-check@1.0.0",
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: []string{"ISC"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("ISC", source.NewLocation(packageLockV2)),
),
MetadataType: "NpmPackageLockJsonMetadata", MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{}, Metadata: pkg.NpmPackageLockJSONMetadata{},
} }
packageLockV1 := "test-fixtures/pkg-lock/alias-package-lock-1.json" for _, pl := range packageLocks {
packageLockV2 := "test-fixtures/pkg-lock/alias-package-lock-2.json"
packageLocks := []string{packageLockV1, packageLockV2}
for _, packageLock := range packageLocks {
expected := make([]pkg.Package, len(commonPkgs)) expected := make([]pkg.Package, len(commonPkgs))
copy(expected, commonPkgs) copy(expected, commonPkgs)
if packageLock == packageLockV2 { if pl == packageLockV2 {
expected = append(expected, v2Pkg) expected = append(expected, v2Pkg)
} }
for i := range expected { for i := range expected {
expected[i].Locations.Add(source.NewLocation(packageLock)) expected[i].Locations.Add(source.NewLocation(pl))
} }
pkgtest.TestFileParser(t, packageLock, parsePackageLock, expected, expectedRelationships) pkgtest.TestFileParser(t, pl, parsePackageLock, expected, expectedRelationships)
} }
} }
@ -305,9 +315,11 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) {
{ {
Name: "tmp", Name: "tmp",
Version: "1.0.0", Version: "1.0.0",
Licenses: []string{"ISC"},
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("ISC", source.NewLocation(fixture)),
),
PURL: "pkg:npm/tmp@1.0.0", PURL: "pkg:npm/tmp@1.0.0",
MetadataType: "NpmPackageLockJsonMetadata", MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{}, Metadata: pkg.NpmPackageLockJSONMetadata{},
@ -315,9 +327,13 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) {
{ {
Name: "pause-stream", Name: "pause-stream",
Version: "0.0.11", Version: "0.0.11",
Licenses: []string{"MIT", "Apache2"},
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
pkg.NewLicenseFromLocations("Apache2", source.NewLocation(fixture)),
),
PURL: "pkg:npm/pause-stream@0.0.11", PURL: "pkg:npm/pause-stream@0.0.11",
MetadataType: "NpmPackageLockJsonMetadata", MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{}, Metadata: pkg.NpmPackageLockJSONMetadata{},
@ -325,9 +341,11 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) {
{ {
Name: "through", Name: "through",
Version: "2.3.8", Version: "2.3.8",
Licenses: []string{"MIT"},
Language: pkg.JavaScript, Language: pkg.JavaScript,
Type: pkg.NpmPkg, Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
PURL: "pkg:npm/through@2.3.8", PURL: "pkg:npm/through@2.3.8",
MetadataType: "NpmPackageLockJsonMetadata", MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{}, Metadata: pkg.NpmPackageLockJSONMetadata{},

View File

@ -47,9 +47,14 @@ func Test_KernelCataloger(t *testing.T) {
"/lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko", "/lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko",
), ),
), ),
Licenses: []string{ Licenses: pkg.NewLicenseSet(
"GPL v2", pkg.NewLicenseFromLocations("GPL v2",
}, source.NewVirtualLocation(
"/lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko",
"/lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko",
),
),
),
Type: pkg.LinuxKernelModulePkg, Type: pkg.LinuxKernelModulePkg,
PURL: "pkg:generic/ttynull", PURL: "pkg:generic/ttynull",
MetadataType: pkg.LinuxKernelModuleMetadataType, MetadataType: pkg.LinuxKernelModuleMetadataType,

View File

@ -10,11 +10,11 @@ import (
const linuxKernelPackageName = "linux-kernel" const linuxKernelPackageName = "linux-kernel"
func newLinuxKernelPackage(metadata pkg.LinuxKernelMetadata, locations ...source.Location) pkg.Package { func newLinuxKernelPackage(metadata pkg.LinuxKernelMetadata, archiveLocation source.Location) pkg.Package {
p := pkg.Package{ p := pkg.Package{
Name: linuxKernelPackageName, Name: linuxKernelPackageName,
Version: metadata.Version, Version: metadata.Version,
Locations: source.NewLocationSet(locations...), Locations: source.NewLocationSet(archiveLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(linuxKernelPackageName, metadata.Version), PURL: packageURL(linuxKernelPackageName, metadata.Version),
Type: pkg.LinuxKernelPkg, Type: pkg.LinuxKernelPkg,
MetadataType: pkg.LinuxKernelMetadataType, MetadataType: pkg.LinuxKernelMetadataType,
@ -26,19 +26,12 @@ func newLinuxKernelPackage(metadata pkg.LinuxKernelMetadata, locations ...source
return p return p
} }
func newLinuxKernelModulePackage(metadata pkg.LinuxKernelModuleMetadata, locations ...source.Location) pkg.Package { func newLinuxKernelModulePackage(metadata pkg.LinuxKernelModuleMetadata, kmLocation source.Location) pkg.Package {
var licenses []string
if metadata.License != "" {
licenses = []string{metadata.License}
} else {
licenses = []string{}
}
p := pkg.Package{ p := pkg.Package{
Name: metadata.Name, Name: metadata.Name,
Version: metadata.Version, Version: metadata.Version,
Locations: source.NewLocationSet(locations...), Locations: source.NewLocationSet(kmLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Licenses: licenses, Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(kmLocation, metadata.License)...),
PURL: packageURL(metadata.Name, metadata.Version), PURL: packageURL(metadata.Name, metadata.Version),
Type: pkg.LinuxKernelModulePkg, Type: pkg.LinuxKernelModulePkg,
MetadataType: pkg.LinuxKernelModuleMetadataType, MetadataType: pkg.LinuxKernelModuleMetadataType,

View File

@ -37,7 +37,7 @@ func parseLinuxKernelFile(_ source.FileResolver, _ *generic.Environment, reader
return []pkg.Package{ return []pkg.Package{
newLinuxKernelPackage( newLinuxKernelPackage(
metadata, metadata,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), reader.Location,
), ),
}, nil, nil }, nil, nil
} }

View File

@ -32,7 +32,7 @@ func parseLinuxKernelModuleFile(_ source.FileResolver, _ *generic.Environment, r
return []pkg.Package{ return []pkg.Package{
newLinuxKernelModulePackage( newLinuxKernelModulePackage(
*metadata, *metadata,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), reader.Location,
), ),
}, nil, nil }, nil, nil
} }

View File

@ -8,23 +8,24 @@ import (
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
func newComposerLockPackage(m pkg.PhpComposerJSONMetadata, location ...source.Location) pkg.Package { func newComposerLockPackage(m parsedData, indexLocation source.Location) pkg.Package {
p := pkg.Package{ p := pkg.Package{
Name: m.Name, Name: m.Name,
Version: m.Version, Version: m.Version,
Locations: source.NewLocationSet(location...), Locations: source.NewLocationSet(indexLocation),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, m.License...)...),
PURL: packageURL(m), PURL: packageURL(m),
Language: pkg.PHP, Language: pkg.PHP,
Type: pkg.PhpComposerPkg, Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType, MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: m, Metadata: m.PhpComposerJSONMetadata,
} }
p.SetID() p.SetID()
return p return p
} }
func packageURL(m pkg.PhpComposerJSONMetadata) string { func packageURL(m parsedData) string {
var name, vendor string var name, vendor string
fields := strings.Split(m.Name, "/") fields := strings.Split(m.Name, "/")
switch len(fields) { switch len(fields) {

View File

@ -11,31 +11,40 @@ import (
func Test_packageURL(t *testing.T) { func Test_packageURL(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
metadata pkg.PhpComposerJSONMetadata metadata parsedData
expected string expected string
}{ }{
{ {
name: "with extractable vendor", name: "with extractable vendor",
metadata: pkg.PhpComposerJSONMetadata{ metadata: parsedData{
Name: "ven/name", []string{},
pkg.PhpComposerJSONMetadata{
Version: "1.0.1", Version: "1.0.1",
Name: "ven/name",
},
}, },
expected: "pkg:composer/ven/name@1.0.1", expected: "pkg:composer/ven/name@1.0.1",
}, },
{ {
name: "name with slashes (invalid)", name: "name with slashes (invalid)",
metadata: pkg.PhpComposerJSONMetadata{ metadata: parsedData{
[]string{},
pkg.PhpComposerJSONMetadata{
Name: "ven/name/component", Name: "ven/name/component",
Version: "1.0.1", Version: "1.0.1",
}, },
},
expected: "pkg:composer/ven/name-component@1.0.1", expected: "pkg:composer/ven/name-component@1.0.1",
}, },
{ {
name: "unknown vendor", name: "unknown vendor",
metadata: pkg.PhpComposerJSONMetadata{ metadata: parsedData{
[]string{},
pkg.PhpComposerJSONMetadata{
Name: "name", Name: "name",
Version: "1.0.1", Version: "1.0.1",
}, },
},
expected: "pkg:composer/name@1.0.1", expected: "pkg:composer/name@1.0.1",
}, },
} }

View File

@ -14,9 +14,14 @@ import (
var _ generic.Parser = parseComposerLock var _ generic.Parser = parseComposerLock
type parsedData struct {
License []string `json:"license"`
pkg.PhpComposerJSONMetadata
}
type composerLock struct { type composerLock struct {
Packages []pkg.PhpComposerJSONMetadata `json:"packages"` Packages []parsedData `json:"packages"`
PackageDev []pkg.PhpComposerJSONMetadata `json:"packages-dev"` PackageDev []parsedData `json:"packages-dev"`
} }
// parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered. // parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered.
@ -40,6 +45,11 @@ func parseComposerLock(_ source.FileResolver, _ *generic.Environment, reader sou
), ),
) )
} }
// TODO: did we omit this on purpose?
// for _, m := range lock.PackageDev {
// pkgs = append(pkgs, newComposerLockPackage(m, reader.Location))
//}
} }
return pkgs, nil, nil return pkgs, nil, nil

View File

@ -19,6 +19,9 @@ func TestParseComposerFileLock(t *testing.T) {
Version: "1.0.2", Version: "1.0.2",
PURL: "pkg:composer/adoy/fastcgi-client@1.0.2", PURL: "pkg:composer/adoy/fastcgi-client@1.0.2",
Locations: locations, Locations: locations,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
Language: pkg.PHP, Language: pkg.PHP,
Type: pkg.PhpComposerPkg, Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType, MetadataType: pkg.PhpComposerJSONMetadataType,
@ -37,9 +40,6 @@ func TestParseComposerFileLock(t *testing.T) {
}, },
Type: "library", Type: "library",
NotificationURL: "https://packagist.org/downloads/", NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{ Authors: []pkg.PhpComposerAuthors{
{ {
Name: "Pierrick Charron", Name: "Pierrick Charron",
@ -60,6 +60,9 @@ func TestParseComposerFileLock(t *testing.T) {
Locations: locations, Locations: locations,
PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11", PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11",
Language: pkg.PHP, Language: pkg.PHP,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
Type: pkg.PhpComposerPkg, Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType, MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkg.PhpComposerJSONMetadata{ Metadata: pkg.PhpComposerJSONMetadata{
@ -91,9 +94,6 @@ func TestParseComposerFileLock(t *testing.T) {
}, },
Type: "library", Type: "library",
NotificationURL: "https://packagist.org/downloads/", NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{ Authors: []pkg.PhpComposerAuthors{
{ {
Name: "alcaeus", Name: "alcaeus",

View File

@ -16,19 +16,19 @@ var _ generic.Parser = parseComposerLock
// Note: composer version 2 introduced a new structure for the installed.json file, so we support both // Note: composer version 2 introduced a new structure for the installed.json file, so we support both
type installedJSONComposerV2 struct { type installedJSONComposerV2 struct {
Packages []pkg.PhpComposerJSONMetadata `json:"packages"` Packages []parsedData `json:"packages"`
} }
func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error { func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error {
type compv2 struct { type compv2 struct {
Packages []pkg.PhpComposerJSONMetadata `json:"packages"` Packages []parsedData `json:"packages"`
} }
compv2er := new(compv2) compv2er := new(compv2)
err := json.Unmarshal(data, &compv2er) err := json.Unmarshal(data, &compv2er)
if err != nil { if err != nil {
// If we had an err or, we may be dealing with a composer v.1 installed.json // If we had an err or, we may be dealing with a composer v.1 installed.json
// which should be all arrays // which should be all arrays
var packages []pkg.PhpComposerJSONMetadata var packages []parsedData
err := json.Unmarshal(data, &packages) err := json.Unmarshal(data, &packages)
if err != nil { if err != nil {
return err return err

View File

@ -24,6 +24,9 @@ func TestParseInstalledJsonComposerV1(t *testing.T) {
Language: pkg.PHP, Language: pkg.PHP,
Type: pkg.PhpComposerPkg, Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType, MetadataType: pkg.PhpComposerJSONMetadataType,
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
),
Metadata: pkg.PhpComposerJSONMetadata{ Metadata: pkg.PhpComposerJSONMetadata{
Name: "asm89/stack-cors", Name: "asm89/stack-cors",
Version: "1.3.0", Version: "1.3.0",
@ -49,9 +52,6 @@ func TestParseInstalledJsonComposerV1(t *testing.T) {
Time: "2019-12-24T22:41:47+00:00", Time: "2019-12-24T22:41:47+00:00",
Type: "library", Type: "library",
NotificationURL: "https://packagist.org/downloads/", NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{ Authors: []pkg.PhpComposerAuthors{
{ {
Name: "Alexander", Name: "Alexander",
@ -73,6 +73,9 @@ func TestParseInstalledJsonComposerV1(t *testing.T) {
PURL: "pkg:composer/behat/mink@v1.8.1", PURL: "pkg:composer/behat/mink@v1.8.1",
Language: pkg.PHP, Language: pkg.PHP,
Type: pkg.PhpComposerPkg, Type: pkg.PhpComposerPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
),
MetadataType: pkg.PhpComposerJSONMetadataType, MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkg.PhpComposerJSONMetadata{ Metadata: pkg.PhpComposerJSONMetadata{
Name: "behat/mink", Name: "behat/mink",
@ -106,9 +109,6 @@ func TestParseInstalledJsonComposerV1(t *testing.T) {
Time: "2020-03-11T15:45:53+00:00", Time: "2020-03-11T15:45:53+00:00",
Type: "library", Type: "library",
NotificationURL: "https://packagist.org/downloads/", NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{ Authors: []pkg.PhpComposerAuthors{
{ {
Name: "Konstantin Kudryashov", Name: "Konstantin Kudryashov",
@ -133,9 +133,14 @@ func TestParseInstalledJsonComposerV1(t *testing.T) {
locations := source.NewLocationSet(source.NewLocation(fixture)) locations := source.NewLocationSet(source.NewLocation(fixture))
for i := range expectedPkgs { for i := range expectedPkgs {
expectedPkgs[i].Locations = locations expectedPkgs[i].Locations = locations
locationLicenses := pkg.NewLicenseSet()
for _, license := range expectedPkgs[i].Licenses.ToSlice() {
license.Location = locations
locationLicenses.Add(license)
}
expectedPkgs[i].Licenses = locationLicenses
} }
pkgtest.TestFileParser(t, fixture, parseInstalledJSON, expectedPkgs, expectedRelationships) pkgtest.TestFileParser(t, fixture, parseInstalledJSON, expectedPkgs, expectedRelationships)
}) })
} }
} }

View File

@ -11,7 +11,7 @@ import (
) )
func TestPortageCataloger(t *testing.T) { func TestPortageCataloger(t *testing.T) {
expectedLicenseLocation := source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE")
expectedPkgs := []pkg.Package{ expectedPkgs := []pkg.Package{
{ {
Name: "app-containers/skopeo", Name: "app-containers/skopeo",
@ -20,10 +20,10 @@ func TestPortageCataloger(t *testing.T) {
PURL: "pkg:ebuild/app-containers/skopeo@1.5.1", PURL: "pkg:ebuild/app-containers/skopeo@1.5.1",
Locations: source.NewLocationSet( Locations: source.NewLocationSet(
source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS"), source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS"),
source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE"),
source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"), source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"),
expectedLicenseLocation,
), ),
Licenses: []string{"Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT"}, Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(expectedLicenseLocation, "Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT")...),
Type: pkg.PortagePkg, Type: pkg.PortagePkg,
MetadataType: pkg.PortageMetadataType, MetadataType: pkg.PortageMetadataType,
Metadata: pkg.PortageMetadata{ Metadata: pkg.PortageMetadata{

View File

@ -6,7 +6,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -116,9 +115,9 @@ func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pk
findings.Add(token) findings.Add(token)
} }
} }
licenses := findings.ToSlice()
sort.Strings(licenses) licenseCandidates := findings.ToSlice()
p.Licenses = licenses p.Licenses = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(*location, licenseCandidates...)...)
p.Locations.Add(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation)) p.Locations.Add(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
} }

View File

@ -45,13 +45,14 @@ func Test_PackageCataloger(t *testing.T) {
PURL: "pkg:pypi/requests@2.22.0", PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Licenses: []string{"Apache 2.0"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Apache 2.0", source.NewLocation("test-fixtures/egg-info/PKG-INFO")),
),
FoundBy: "python-package-cataloger", FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "requests", Name: "requests",
Version: "2.22.0", Version: "2.22.0",
License: "Apache 2.0",
Platform: "UNKNOWN", Platform: "UNKNOWN",
Author: "Kenneth Reitz", Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org", AuthorEmail: "me@kennethreitz.org",
@ -82,13 +83,14 @@ func Test_PackageCataloger(t *testing.T) {
PURL: "pkg:pypi/Pygments@2.6.1?vcs_url=git+https://github.com/python-test/test.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", PURL: "pkg:pypi/Pygments@2.6.1?vcs_url=git+https://github.com/python-test/test.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Licenses: []string{"BSD License"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/dist-info/METADATA")),
),
FoundBy: "python-package-cataloger", FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "Pygments", Name: "Pygments",
Version: "2.6.1", Version: "2.6.1",
License: "BSD License",
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
@ -119,13 +121,14 @@ func Test_PackageCataloger(t *testing.T) {
PURL: "pkg:pypi/Pygments@2.6.1", PURL: "pkg:pypi/Pygments@2.6.1",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Licenses: []string{"BSD License"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/malformed-record/dist-info/METADATA")),
),
FoundBy: "python-package-cataloger", FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "Pygments", Name: "Pygments",
Version: "2.6.1", Version: "2.6.1",
License: "BSD License",
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
@ -150,13 +153,14 @@ func Test_PackageCataloger(t *testing.T) {
PURL: "pkg:pypi/Pygments@2.6.1", PURL: "pkg:pypi/Pygments@2.6.1",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Licenses: []string{"BSD License"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/partial.dist-info/METADATA")),
),
FoundBy: "python-package-cataloger", FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "Pygments", Name: "Pygments",
Version: "2.6.1", Version: "2.6.1",
License: "BSD License",
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
@ -173,13 +177,14 @@ func Test_PackageCataloger(t *testing.T) {
PURL: "pkg:pypi/requests@2.22.0", PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Licenses: []string{"Apache 2.0"}, Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Apache 2.0", source.NewLocation("test-fixtures/test.egg-info")),
),
FoundBy: "python-package-cataloger", FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{ Metadata: pkg.PythonPackageMetadata{
Name: "requests", Name: "requests",
Version: "2.22.0", Version: "2.22.0",
License: "Apache 2.0",
Platform: "UNKNOWN", Platform: "UNKNOWN",
Author: "Kenneth Reitz", Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org", AuthorEmail: "me@kennethreitz.org",

View File

@ -57,25 +57,21 @@ func newPackageForRequirementsWithMetadata(name, version string, metadata pkg.Py
return p return p
} }
func newPackageForPackage(m pkg.PythonPackageMetadata, sources ...source.Location) pkg.Package { func newPackageForPackage(m parsedData, sources ...source.Location) pkg.Package {
var licenses []string
if m.License != "" {
licenses = []string{m.License}
}
p := pkg.Package{ p := pkg.Package{
Name: m.Name, Name: m.Name,
Version: m.Version, Version: m.Version,
PURL: packageURL(m.Name, m.Version, &m), PURL: packageURL(m.Name, m.Version, &m.PythonPackageMetadata),
Locations: source.NewLocationSet(sources...), Locations: source.NewLocationSet(sources...),
Licenses: licenses, Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(m.LicenseLocation, m.Licenses)...),
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
MetadataType: pkg.PythonPackageMetadataType, MetadataType: pkg.PythonPackageMetadataType,
Metadata: m, Metadata: m.PythonPackageMetadata,
} }
p.SetID() p.SetID()
return p return p
} }

View File

@ -17,21 +17,21 @@ import (
// parseWheelOrEgg takes the primary metadata file reference and returns the python package it represents. // parseWheelOrEgg takes the primary metadata file reference and returns the python package it represents.
func parseWheelOrEgg(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { func parseWheelOrEgg(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
metadata, sources, err := assembleEggOrWheelMetadata(resolver, reader.Location) pd, sources, err := assembleEggOrWheelMetadata(resolver, reader.Location)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if metadata == nil { if pd == nil {
return nil, nil, nil return nil, nil, nil
} }
// This can happen for Python 2.7 where it is reported from an egg-info, but Python is // This can happen for Python 2.7 where it is reported from an egg-info, but Python is
// the actual runtime, it isn't a "package". The special-casing here allows to skip it // the actual runtime, it isn't a "package". The special-casing here allows to skip it
if metadata.Name == "Python" { if pd.Name == "Python" {
return nil, nil, nil return nil, nil, nil
} }
pkgs := []pkg.Package{newPackageForPackage(*metadata, sources...)} pkgs := []pkg.Package{newPackageForPackage(*pd, sources...)}
return pkgs, nil, nil return pkgs, nil, nil
} }
@ -160,7 +160,7 @@ func fetchDirectURLData(resolver source.FileResolver, metadataLocation source.Lo
} }
// assembleEggOrWheelMetadata discovers and accumulates python package metadata from multiple file sources and returns a single metadata object as well as a list of files where the metadata was derived from. // assembleEggOrWheelMetadata discovers and accumulates python package metadata from multiple file sources and returns a single metadata object as well as a list of files where the metadata was derived from.
func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation source.Location) (*pkg.PythonPackageMetadata, []source.Location, error) { func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation source.Location) (*parsedData, []source.Location, error) {
var sources = []source.Location{ var sources = []source.Location{
metadataLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), metadataLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
} }
@ -171,12 +171,12 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s
} }
defer internal.CloseAndLogError(metadataContents, metadataLocation.VirtualPath) defer internal.CloseAndLogError(metadataContents, metadataLocation.VirtualPath)
metadata, err := parseWheelOrEggMetadata(metadataLocation.RealPath, metadataContents) pd, err := parseWheelOrEggMetadata(metadataLocation.RealPath, metadataContents)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if metadata.Name == "" { if pd.Name == "" {
return nil, nil, nil return nil, nil, nil
} }
@ -186,14 +186,14 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s
return nil, nil, err return nil, nil, err
} }
if len(r) == 0 { if len(r) == 0 {
r, s, err = fetchInstalledFiles(resolver, metadataLocation, metadata.SitePackagesRootPath) r, s, err = fetchInstalledFiles(resolver, metadataLocation, pd.SitePackagesRootPath)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
} }
sources = append(sources, s...) sources = append(sources, s...)
metadata.Files = r pd.Files = r
// attach any top-level package names found for the given wheel/egg installation // attach any top-level package names found for the given wheel/egg installation
p, s, err := fetchTopLevelPackages(resolver, metadataLocation) p, s, err := fetchTopLevelPackages(resolver, metadataLocation)
@ -201,15 +201,15 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s
return nil, nil, err return nil, nil, err
} }
sources = append(sources, s...) sources = append(sources, s...)
metadata.TopLevelPackages = p pd.TopLevelPackages = p
// attach any direct-url package data found for the given wheel/egg installation // attach any direct-url package data found for the given wheel/egg installation
d, s, err := fetchDirectURLData(resolver, metadataLocation) d, s, err := fetchDirectURLData(resolver, metadataLocation)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
sources = append(sources, s...)
metadata.DirectURLOrigin = d
return &metadata, sources, nil sources = append(sources, s...)
pd.DirectURLOrigin = d
return &pd, sources, nil
} }

View File

@ -12,11 +12,18 @@ import (
"github.com/anchore/syft/internal/file" "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
) )
type parsedData struct {
Licenses string `mapstructure:"License"`
LicenseLocation source.Location
pkg.PythonPackageMetadata `mapstructure:",squash"`
}
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes), // parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
// returning all Python packages listed. // returning all Python packages listed.
func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMetadata, error) { func parseWheelOrEggMetadata(path string, reader io.Reader) (parsedData, error) {
fields := make(map[string]string) fields := make(map[string]string)
var key string var key string
@ -43,7 +50,7 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe
// a field-body continuation // a field-body continuation
updatedValue, err := handleFieldBodyContinuation(key, line, fields) updatedValue, err := handleFieldBodyContinuation(key, line, fields)
if err != nil { if err != nil {
return pkg.PythonPackageMetadata{}, err return parsedData{}, err
} }
fields[key] = updatedValue fields[key] = updatedValue
@ -62,19 +69,22 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return pkg.PythonPackageMetadata{}, fmt.Errorf("failed to parse python wheel/egg: %w", err) return parsedData{}, fmt.Errorf("failed to parse python wheel/egg: %w", err)
} }
var metadata pkg.PythonPackageMetadata var pd parsedData
if err := mapstructure.Decode(fields, &metadata); err != nil { if err := mapstructure.Decode(fields, &pd); err != nil {
return pkg.PythonPackageMetadata{}, fmt.Errorf("unable to parse APK metadata: %w", err) return pd, fmt.Errorf("unable to parse APK metadata: %w", err)
} }
// add additional metadata not stored in the egg/wheel metadata file // add additional metadata not stored in the egg/wheel metadata file
metadata.SitePackagesRootPath = determineSitePackagesRootPath(path) pd.SitePackagesRootPath = determineSitePackagesRootPath(path)
if pd.Licenses != "" {
pd.LicenseLocation = source.NewLocation(path)
}
return metadata, nil return pd, nil
} }
// isEggRegularFile determines if the specified path is the regular file variant // isEggRegularFile determines if the specified path is the regular file variant

View File

@ -7,37 +7,44 @@ import (
"github.com/go-test/deep" "github.com/go-test/deep"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
) )
func TestParseWheelEggMetadata(t *testing.T) { func TestParseWheelEggMetadata(t *testing.T) {
tests := []struct { tests := []struct {
Fixture string Fixture string
ExpectedMetadata pkg.PythonPackageMetadata ExpectedMetadata parsedData
}{ }{
{ {
Fixture: "test-fixtures/egg-info/PKG-INFO", Fixture: "test-fixtures/egg-info/PKG-INFO",
ExpectedMetadata: pkg.PythonPackageMetadata{ ExpectedMetadata: parsedData{
"Apache 2.0",
source.NewLocation("test-fixtures/egg-info/PKG-INFO"),
pkg.PythonPackageMetadata{
Name: "requests", Name: "requests",
Version: "2.22.0", Version: "2.22.0",
License: "Apache 2.0",
Platform: "UNKNOWN", Platform: "UNKNOWN",
Author: "Kenneth Reitz", Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org", AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: "test-fixtures",
}, },
}, },
},
{ {
Fixture: "test-fixtures/dist-info/METADATA", Fixture: "test-fixtures/dist-info/METADATA",
ExpectedMetadata: pkg.PythonPackageMetadata{ ExpectedMetadata: parsedData{
"BSD License",
source.NewLocation("test-fixtures/dist-info/METADATA"),
pkg.PythonPackageMetadata{
Name: "Pygments", Name: "Pygments",
Version: "2.6.1", Version: "2.6.1",
License: "BSD License",
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: "test-fixtures",
}, },
}, },
},
} }
for _, test := range tests { for _, test := range tests {
@ -122,16 +129,20 @@ func TestDetermineSitePackagesRootPath(t *testing.T) {
func TestParseWheelEggMetadataInvalid(t *testing.T) { func TestParseWheelEggMetadataInvalid(t *testing.T) {
tests := []struct { tests := []struct {
Fixture string Fixture string
ExpectedMetadata pkg.PythonPackageMetadata ExpectedMetadata parsedData
}{ }{
{ {
Fixture: "test-fixtures/egg-info/PKG-INFO-INVALID", Fixture: "test-fixtures/egg-info/PKG-INFO-INVALID",
ExpectedMetadata: pkg.PythonPackageMetadata{ ExpectedMetadata: parsedData{
"",
source.Location{},
pkg.PythonPackageMetadata{
Name: "mxnet", Name: "mxnet",
Version: "1.8.0", Version: "1.8.0",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: "test-fixtures",
}, },
}, },
},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -16,7 +16,7 @@ func TestRPackageCataloger(t *testing.T) {
Version: "4.3.0", Version: "4.3.0",
FoundBy: "r-package-cataloger", FoundBy: "r-package-cataloger",
Locations: source.NewLocationSet(source.NewLocation("base/DESCRIPTION")), Locations: source.NewLocationSet(source.NewLocation("base/DESCRIPTION")),
Licenses: []string{"Part of R 4.3.0"}, Licenses: pkg.NewLicenseSet([]pkg.License{pkg.NewLicense("Part of R 4.3.0")}...),
Language: pkg.R, Language: pkg.R,
Type: pkg.Rpkg, Type: pkg.Rpkg,
PURL: "pkg:cran/base@4.3.0", PURL: "pkg:cran/base@4.3.0",
@ -35,7 +35,7 @@ func TestRPackageCataloger(t *testing.T) {
Version: "1.5.0.9000", Version: "1.5.0.9000",
FoundBy: "r-package-cataloger", FoundBy: "r-package-cataloger",
Locations: source.NewLocationSet(source.NewLocation("stringr/DESCRIPTION")), Locations: source.NewLocationSet(source.NewLocation("stringr/DESCRIPTION")),
Licenses: []string{"MIT + file LICENSE"}, Licenses: pkg.NewLicenseSet([]pkg.License{pkg.NewLicense("MIT")}...),
Language: pkg.R, Language: pkg.R,
Type: pkg.Rpkg, Type: pkg.Rpkg,
PURL: "pkg:cran/stringr@1.5.0.9000", PURL: "pkg:cran/stringr@1.5.0.9000",

View File

@ -1,6 +1,8 @@
package r package r
import ( import (
"strings"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
@ -11,11 +13,14 @@ func newPackage(pd parseData, locations ...source.Location) pkg.Package {
for _, loc := range locations { for _, loc := range locations {
locationSet.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) locationSet.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
} }
licenses := parseLicenseData(pd.License)
result := pkg.Package{ result := pkg.Package{
Name: pd.Package, Name: pd.Package,
Version: pd.Version, Version: pd.Version,
Locations: locationSet, Locations: locationSet,
Licenses: []string{pd.License}, Licenses: pkg.NewLicenseSet(licenses...),
Language: pkg.R, Language: pkg.R,
Type: pkg.Rpkg, Type: pkg.Rpkg,
PURL: packageURL(pd), PURL: packageURL(pd),
@ -30,3 +35,95 @@ func newPackage(pd parseData, locations ...source.Location) pkg.Package {
func packageURL(m parseData) string { func packageURL(m parseData) string {
return packageurl.NewPackageURL("cran", "", m.Package, m.Version, nil, "").ToString() return packageurl.NewPackageURL("cran", "", m.Package, m.Version, nil, "").ToString()
} }
// https://r-pkgs.org/description.html#the-license-field
// four forms:
// 1. "GPL (>= 2)"
// 2. "GPL-2"
// 3. "MIT + file LICENSE"
// 4. "pointer to the full text of the license; file LICENSE"
// Multiple licences can be specified separated by |
// (surrounded by spaces) in which case the user can choose any of the above cases.
// https://cran.rstudio.com/doc/manuals/r-devel/R-exts.html#Licensing
func parseLicenseData(license string, locations ...source.Location) []pkg.License {
licenses := make([]pkg.License, 0)
// check if multiple licenses are separated by |
splitField := strings.Split(license, "|")
for _, l := range splitField {
// check case 1 for surrounding parens
l = strings.TrimSpace(l)
if strings.Contains(l, "(") && strings.Contains(l, ")") {
licenseVersion := strings.SplitN(l, " ", 2)
if len(licenseVersion) == 2 {
l = strings.Join([]string{licenseVersion[0], parseVersion(licenseVersion[1])}, "")
licenses = append(licenses, pkg.NewLicenseFromLocations(l, locations...))
continue
}
}
// case 3
if strings.Contains(l, "+") && strings.Contains(l, "LICENSE") {
splitField := strings.Split(l, " ")
if len(splitField) > 0 {
licenses = append(licenses, pkg.NewLicenseFromLocations(splitField[0], locations...))
continue
}
}
// TODO: case 4 if we are able to read the location data and find the adjacent file?
if l == "file LICENSE" {
continue
}
// no specific case found for the above so assume case 2
// check if the common name in case 2 is valid SDPX otherwise value will be populated
licenses = append(licenses, pkg.NewLicenseFromLocations(l, locations...))
continue
}
return licenses
}
// attempt to make best guess at SPDX license ID from version operator in case 2
/*
<, <=, >, >=, ==, or !=
cant be (>= 2.0) OR (>= 2.0, < 3)
since there is no way in SPDX licenses to express < some other version
we attempt to check the constraint to see if this should be + or not
*/
func parseVersion(version string) string {
version = strings.ReplaceAll(version, "(", "")
version = strings.ReplaceAll(version, ")", "")
// multiple constraints
if strings.Contains(version, ",") {
multipleConstraints := strings.Split(version, ",")
// SPDX does not support considering multiple constraints
// so we will just take the first one and attempt to form the best SPDX ID we can
for _, v := range multipleConstraints {
constraintVersion := strings.SplitN(v, " ", 2)
if len(constraintVersion) == 2 {
// switch on the operator and return the version with + or without
switch constraintVersion[0] {
case ">", ">=":
return constraintVersion[1] + "+"
default:
return constraintVersion[1]
}
}
}
}
// single constraint
singleContraint := strings.Split(version, " ")
if len(singleContraint) == 2 {
switch singleContraint[0] {
case ">", ">=":
return singleContraint[1] + "+"
default:
return singleContraint[1]
}
}
// could not parse version constraint so return ""
return ""
}

View File

@ -1,14 +1,106 @@
package r package r
import "testing" import (
"testing"
func Test_newPackage(t *testing.T) { "github.com/anchore/syft/syft/pkg"
)
func Test_NewPackageLicenses(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
}{} pd parseData
want []pkg.License
}{
{
"License field with single valid spdx",
parseData{
Package: "Foo",
Version: "1",
License: "MIT",
},
[]pkg.License{
pkg.NewLicense("MIT"),
},
},
{
"License field with single version separator no +",
parseData{
Package: "Bar",
Version: "2",
License: "LGPL (== 2.0)",
},
[]pkg.License{
pkg.NewLicense("LGPL2.0"),
},
},
{
"License field with multiple version separator",
parseData{
Package: "Bar",
Version: "2",
License: "LGPL (>= 2.0, < 3)",
},
[]pkg.License{
pkg.NewLicense("LGPL2.0+"),
},
},
{
"License field with file reference",
parseData{
Package: "Baz",
Version: "3",
License: "GPL-2 + file LICENSE",
},
[]pkg.License{
pkg.NewLicense("GPL-2"),
},
},
{
"License field which covers no case",
parseData{
Package: "Baz",
Version: "3",
License: "Mozilla Public License",
},
[]pkg.License{
pkg.NewLicense("Mozilla Public License"),
},
},
{
"License field with multiple cases",
parseData{
Package: "Baz",
Version: "3",
License: "GPL-2 | file LICENSE | LGPL (>= 2.0)",
},
[]pkg.License{
pkg.NewLicense("GPL-2"),
pkg.NewLicense("LGPL2.0+"),
},
},
}
for _, tt := range testCases { for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := parseLicenseData(tt.pd.License)
if len(got) != len(tt.want) {
t.Errorf("unexpected number of licenses: got=%d, want=%d", len(got), len(tt.want))
}
for _, wantLicense := range tt.want {
found := false
for _, gotLicense := range got {
if wantLicense.Type == gotLicense.Type &&
wantLicense.SPDXExpression == gotLicense.SPDXExpression &&
wantLicense.Value == gotLicense.Value {
found = true
}
}
if !found {
t.Errorf("could not find expected license: %+v; got: %+v", wantLicense, got)
}
}
}) })
} }
} }

View File

@ -13,27 +13,31 @@ import (
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
func newPackage(location source.Location, metadata pkg.RpmMetadata, distro *linux.Release) pkg.Package { func newPackage(dbOrRpmLocation source.Location, pd parsedData, distro *linux.Release) pkg.Package {
p := pkg.Package{ p := pkg.Package{
Name: metadata.Name, Name: pd.Name,
Version: toELVersion(metadata), Version: toELVersion(pd.RpmMetadata),
PURL: packageURL(metadata, distro), Licenses: pkg.NewLicenseSet(pd.Licenses...),
Locations: source.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), PURL: packageURL(pd.RpmMetadata, distro),
Locations: source.NewLocationSet(dbOrRpmLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Type: pkg.RpmPkg, Type: pkg.RpmPkg,
MetadataType: pkg.RpmMetadataType, MetadataType: pkg.RpmMetadataType,
Metadata: metadata, Metadata: pd.RpmMetadata,
}
if metadata.License != "" {
p.Licenses = append(p.Licenses, metadata.License)
} }
p.SetID() p.SetID()
return p return p
} }
func newMetadataFromEntry(entry rpmdb.PackageInfo, files []pkg.RpmdbFileRecord) pkg.RpmMetadata { type parsedData struct {
return pkg.RpmMetadata{ Licenses []pkg.License
pkg.RpmMetadata
}
func newParsedDataFromEntry(licenseLocation source.Location, entry rpmdb.PackageInfo, files []pkg.RpmdbFileRecord) parsedData {
return parsedData{
Licenses: pkg.NewLicensesFromLocation(licenseLocation, entry.License),
RpmMetadata: pkg.RpmMetadata{
Name: entry.Name, Name: entry.Name,
Version: entry.Version, Version: entry.Version,
Epoch: entry.Epoch, Epoch: entry.Epoch,
@ -41,14 +45,14 @@ func newMetadataFromEntry(entry rpmdb.PackageInfo, files []pkg.RpmdbFileRecord)
Release: entry.Release, Release: entry.Release,
SourceRpm: entry.SourceRpm, SourceRpm: entry.SourceRpm,
Vendor: entry.Vendor, Vendor: entry.Vendor,
License: entry.License,
Size: entry.Size, Size: entry.Size,
ModularityLabel: entry.Modularitylabel, ModularityLabel: entry.Modularitylabel,
Files: files, Files: files,
},
} }
} }
func newMetadataFromManifestLine(entry string) (*pkg.RpmMetadata, error) { func newMetadataFromManifestLine(entry string) (*parsedData, error) {
parts := strings.Split(entry, "\t") parts := strings.Split(entry, "\t")
if len(parts) < 10 { if len(parts) < 10 {
return nil, fmt.Errorf("unexpected number of fields in line: %s", entry) return nil, fmt.Errorf("unexpected number of fields in line: %s", entry)
@ -74,8 +78,8 @@ func newMetadataFromManifestLine(entry string) (*pkg.RpmMetadata, error) {
if err == nil { if err == nil {
size = converted size = converted
} }
return &parsedData{
return &pkg.RpmMetadata{ RpmMetadata: pkg.RpmMetadata{
Name: parts[0], Name: parts[0],
Version: version, Version: version,
Epoch: epoch, Epoch: epoch,
@ -84,6 +88,7 @@ func newMetadataFromManifestLine(entry string) (*pkg.RpmMetadata, error) {
SourceRpm: parts[9], SourceRpm: parts[9],
Vendor: parts[4], Vendor: parts[4],
Size: size, Size: size,
},
}, nil }, nil
} }

View File

@ -3,7 +3,6 @@ package rpm
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
rpmdb "github.com/knqyf263/go-rpmdb/pkg" rpmdb "github.com/knqyf263/go-rpmdb/pkg"
"github.com/sassoftware/go-rpmutils" "github.com/sassoftware/go-rpmutils"
@ -34,7 +33,9 @@ func parseRpm(_ source.FileResolver, _ *generic.Environment, reader source.Locat
size, _ := rpm.Header.InstalledSize() size, _ := rpm.Header.InstalledSize()
files, _ := rpm.Header.GetFiles() files, _ := rpm.Header.GetFiles()
metadata := pkg.RpmMetadata{ pd := parsedData{
Licenses: pkg.NewLicensesFromLocation(reader.Location, licenses...),
RpmMetadata: pkg.RpmMetadata{
Name: nevra.Name, Name: nevra.Name,
Version: nevra.Version, Version: nevra.Version,
Epoch: parseEpoch(nevra.Epoch), Epoch: parseEpoch(nevra.Epoch),
@ -42,12 +43,12 @@ func parseRpm(_ source.FileResolver, _ *generic.Environment, reader source.Locat
Release: nevra.Release, Release: nevra.Release,
SourceRpm: sourceRpm, SourceRpm: sourceRpm,
Vendor: vendor, Vendor: vendor,
License: strings.Join(licenses, " AND "), // TODO: AND conjunction is not necessarily correct, but we don't have a way to represent multiple licenses yet
Size: int(size), Size: int(size),
Files: mapFiles(files, digestAlgorithm), Files: mapFiles(files, digestAlgorithm),
},
} }
return []pkg.Package{newPackage(reader.Location, metadata, nil)}, nil, nil return []pkg.Package{newPackage(reader.Location, pd, nil)}, nil, nil
} }
func getDigestAlgorithm(header *rpmutils.RpmHeader) string { func getDigestAlgorithm(header *rpmutils.RpmHeader) string {

Some files were not shown because too many files have changed in this diff Show More