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/deitch/magic v0.0.0-20230404182410-1ff89d7342da
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-git/v5 v5.6.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/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
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/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
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
// 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"
"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 (
@ -13,21 +16,24 @@ const (
)
// 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)
if err != nil {
return nil, err
}
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 {
if slices.Contains(licenses, m.ID) {
continue
}
licenses = append(licenses, m.ID)
lic := pkg.NewLicenseFromLocations(m.ID, l)
lic.Type = license.Concluded
licenses = append(licenses, lic)
}
return
return licenses, nil
}

View file

@ -15,8 +15,10 @@ func NewStringSet(start ...string) StringSet {
}
// Add a string to the set.
func (s StringSet) Add(i string) {
s[i] = struct{}{}
func (s StringSet) Add(i ...string) {
for _, str := range i {
s[str] = struct{}{}
}
}
// Remove a string from the set.
@ -41,3 +43,19 @@ func (s StringSet) ToSlice() []string {
sort.Strings(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{}))
metadataSchema := reflector.ReflectFromType(reflect.TypeOf(&artifactMetadataContainer{}))
// TODO: inject source 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,
Version: c.Version,
Locations: decodeLocations(values),
Licenses: decodeLicenses(c),
Licenses: pkg.NewLicenseSet(decodeLicenses(c)...),
CPEs: decodeCPEs(c),
PURL: c.PackageURL,
}

View file

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

View file

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

View file

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

View file

@ -1,53 +1,205 @@
package cyclonedxhelpers
import (
"fmt"
"strings"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/spdxlicense"
"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 {
lc := cyclonedx.Licenses{}
for _, licenseName := range p.Licenses {
if value, exists := spdxlicense.ID(licenseName); exists {
lc = append(lc, cyclonedx.LicenseChoice{
spdxc, otherc, ex := separateLicenses(p)
if len(otherc) > 0 {
// found non spdx related licenses
// 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{
ID: value,
},
})
seen[value] = true
// we have added the license to the SPDX license list check next license
continue
}
// not found so append the licenseName as is
lc = append(lc, cyclonedx.LicenseChoice{
if l.SPDXExpression != "" {
// 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{
Name: licenseName,
Name: l.Value,
},
})
}
if len(lc) > 0 {
return &lc
}
return nil
return spdxc, otherc, ex
}
func decodeLicenses(c *cyclonedx.Component) (out []string) {
if c.Licenses != nil {
for _, l := range *c.Licenses {
if l.License != nil {
var lic string
switch {
case l.License.ID != "":
lic = l.License.ID
case l.License.Name != "":
lic = l.License.Name
default:
continue
}
out = append(out, lic)
}
func processLicenseURLs(l pkg.License, spdxID string, populate *cyclonedx.Licenses) {
for _, url := range l.URL.ToSlice() {
if spdxID == "" {
*populate = append(*populate, cyclonedx.LicenseChoice{
License: &cyclonedx.License{
URL: url,
Name: l.Value,
},
})
} else {
*populate = append(*populate, cyclonedx.LicenseChoice{
License: &cyclonedx.License{
ID: spdxID,
URL: url,
},
})
}
}
return
}
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
}
if string(c) == ")" && openCount > 1 {
fmt.Fprintf(&sb, "%c", c)
}
if string(c) == ")" {
openCount--
continue
}
fmt.Fprintf(&sb, "%c", c)
}
return sb.String()
}

View file

@ -6,6 +6,8 @@ import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
@ -23,60 +25,170 @@ func Test_encodeLicense(t *testing.T) {
{
name: "no SPDX licenses",
input: pkg.Package{
Licenses: []string{
"made-up",
},
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("RandomLicense"),
),
},
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{
Licenses: []string{
"MIT",
},
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("mit"),
pkg.NewLicense("FOOBAR"),
),
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "MIT"}},
{
License: &cyclonedx.License{
Name: "FOOBAR",
},
},
{
License: &cyclonedx.License{
ID: "MIT",
},
},
},
},
{
name: "with SPDX license expression",
name: "with complex SPDX license expression",
input: pkg.Package{
Licenses: []string{
"MIT",
"GPL-3.0",
},
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT AND GPL-3.0-only"),
),
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "MIT"}},
{License: &cyclonedx.License{ID: "GPL-3.0-only"}},
{
Expression: "MIT AND GPL-3.0-only",
},
},
},
{
name: "cap insensitive",
name: "with multiple complex SPDX license expression",
input: pkg.Package{
Licenses: []string{
"gpl-3.0",
},
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicense("MIT AND GPL-3.0-only WITH Classpath-exception-2.0"),
),
},
expected: &cyclonedx.Licenses{
{License: &cyclonedx.License{ID: "GPL-3.0-only"}},
{
Expression: "(MIT AND GPL-3.0-only) AND (MIT AND GPL-3.0-only WITH Classpath-exception-2.0)",
},
},
},
{
name: "debian to spdx conversion",
name: "with multiple URLs and expressions",
input: pkg.Package{
Licenses: []string{
"GPL-2",
},
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{ID: "GPL-2.0-only"}},
{
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 {
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"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/license"
"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
// The options to populate this field are limited to:
// 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
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if len(p.Licenses) == 0 {
return NONE
if p.Licenses.Empty() {
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/
parsedLicenses := parseLicenses(p.Licenses)
// take all licenses and assume an AND expression;
// 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) {
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 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 {
if value, exists := spdxlicense.ID(l); exists {
parsedLicenses = append(parsedLicenses, value)
var candidate string
if l.SPDXExpression != "" {
candidate = l.SPDXExpression
} else {
// we did not find a valid SPDX license ID so treat as separate license
otherLicense := spdxlicense.LicenseRefPrefix + l
parsedLicenses = append(parsedLicenses, otherLicense)
candidate = spdxlicense.LicenseRefPrefix + l.Value
}
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) {
type expected struct {
concluded string
declared string
}
tests := []struct {
name string
input pkg.Package
expected string
expected expected
}{
{
name: "no licenses",
input: pkg.Package{},
expected: NONE,
name: "no licenses",
input: pkg.Package{},
expected: expected{
concluded: "NOASSERTION",
declared: "NOASSERTION",
},
},
{
name: "no SPDX licenses",
input: pkg.Package{
Licenses: []string{
"made-up",
},
Licenses: pkg.NewLicenseSet(pkg.NewLicense("made-up")),
},
expected: expected{
concluded: "NOASSERTION",
declared: "LicenseRef-made-up",
},
expected: "LicenseRef-made-up",
},
{
name: "with SPDX license",
input: pkg.Package{
Licenses: []string{
"MIT",
},
Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
},
expected: struct {
concluded string
declared string
}{
concluded: "NOASSERTION",
declared: "MIT",
},
expected: "MIT",
},
{
name: "with SPDX license expression",
input: pkg.Package{
Licenses: []string{
"MIT",
"GPL-3.0",
},
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicense("GPL-3.0-only"),
),
},
expected: "MIT AND GPL-3.0-only",
},
{
name: "cap insensitive",
input: pkg.Package{
Licenses: []string{
"gpl-3.0",
},
expected: expected{
concluded: "NOASSERTION",
// because we sort licenses alphabetically GPL ends up at the start
declared: "GPL-3.0-only AND MIT",
},
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-",
input: pkg.Package{
Licenses: []string{
"one thing first",
"two things/#$^second",
"MIT",
},
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("one thing first"),
pkg.NewLicense("two things/#$^second"),
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",
},
},
{
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",
},
expected: "LicenseRef-one-thing-first AND LicenseRef-two-things----second AND MIT",
},
}
for _, test := range tests {
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
// 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.
license := License(p)
// extract these correctly to the spdx license format
concluded, declared := License(p)
// two ways to get filesAnalyzed == true:
// 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
// Purpose: Contain the license the SPDX file creator has concluded as governing the
// 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"
// 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.
// 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.
PackageLicenseDeclared: license,
PackageLicenseDeclared: declared,
// 7.16: Comments on License
// Cardinality: optional, one
@ -534,10 +535,18 @@ func toFileTypes(metadata *source.FileMetadata) (ty []string) {
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 {
licenses := map[string]bool{}
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) {
licenses[license] = true
}
@ -549,12 +558,12 @@ func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense {
sorted := maps.Keys(licenses)
slices.Sort(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)
result = append(result, &spdx.OtherLicense{
LicenseIdentifier: SanitizeElementID(license),
LicenseName: name,
ExtractedText: NONE, // we probably should have some extracted text here, but this is good enough for now
ExtractedText: name,
})
}
return result

View file

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

View file

@ -14,6 +14,7 @@ import (
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"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/pkg"
"github.com/anchore/syft/syft/sbom"
@ -279,7 +280,7 @@ func toSyftPackage(p *spdx.Package) *pkg.Package {
Type: info.typ,
Name: p.PackageName,
Version: p.PackageVersion,
Licenses: parseLicense(p.PackageLicenseDeclared),
Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...),
CPEs: extractCPEs(p),
PURL: info.purl.String(),
Language: info.lang,
@ -292,6 +293,33 @@ func toSyftPackage(p *spdx.Package) *pkg.Package {
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
func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface{}) {
arch := info.qualifierValue(pkg.PURLQualifierArch)
@ -317,7 +345,6 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
OriginPackage: upstreamName,
Maintainer: supplier,
Version: p.PackageVersion,
License: p.PackageLicenseDeclared,
Architecture: arch,
URL: p.PackageHomePage,
Description: p.PackageDescription,
@ -330,17 +357,12 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
} else {
epoch = &converted
}
license := p.PackageLicenseDeclared
if license == "" {
license = p.PackageLicenseConcluded
}
return pkg.RpmMetadataType, pkg.RpmMetadata{
Name: p.PackageName,
Version: p.PackageVersion,
Epoch: epoch,
Arch: arch,
SourceRpm: upstreamValue,
License: license,
Vendor: originator,
}
case pkg.DebPkg:
@ -400,10 +422,3 @@ func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) {
}
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",
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:redacted",
"serialNumber": "urn:uuid:1b71a5b4-4bc5-4548-a51a-212e631976cd",
"version": 1,
"metadata": {
"timestamp": "timestamp:redacted",
"timestamp": "2023-05-08T14:40:32-04:00",
"tools": [
{
"vendor": "anchore",
@ -14,14 +14,14 @@
}
],
"component": {
"bom-ref": "redacted",
"bom-ref": "163686ac6e30c752",
"type": "file",
"name": "/some/path"
}
},
"components": [
{
"bom-ref": "redacted",
"bom-ref": "8c7e1242588c971a",
"type": "library",
"name": "package-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",
"name": "package-2",
"version": "2.0.1",

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<?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>
<timestamp>2022-11-07T09:11:06-05:00</timestamp>
<timestamp>2023-05-08T14:40:52-04:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -14,7 +14,7 @@
</component>
</metadata>
<components>
<component bom-ref="1b1d0be59ac59d2c" type="library">
<component bom-ref="8c7e1242588c971a" type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>

View file

@ -1,7 +1,7 @@
<?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>
<timestamp>2022-11-07T09:11:06-05:00</timestamp>
<timestamp>2023-05-08T14:40:52-04:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@ -9,13 +9,13 @@
<version>v0.42.0-bogus</version>
</tool>
</tools>
<component bom-ref="522dc6b135a55bb4" type="container">
<component bom-ref="38160ebc2a6876e8" type="container">
<name>user-image-input</name>
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
</component>
</metadata>
<components>
<component bom-ref="66ba429119b8bec6" type="library">
<component bom-ref="ec2e0c93617507ef" type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
@ -30,7 +30,7 @@
<property name="syft:package:language">python</property>
<property name="syft:package:metadataType">PythonPackageMetadata</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>
</properties>
</component>
@ -43,7 +43,7 @@
<property name="syft:package:foundBy">the-cataloger-2</property>
<property name="syft:package:metadataType">DpkgMetadata</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:metadata:installedSize">0</property>
</properties>

View file

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

View file

@ -3,23 +3,23 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"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": {
"licenseListVersion": "3.20",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus"
],
"created": "2023-05-02T18:24:17Z"
"created": "2023-05-09T17:11:26Z"
},
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-1b1d0be59ac59d2c",
"SPDXID": "SPDXRef-Package-python-package-1-9265397e5e15168a",
"versionInfo": "1.0.1",
"downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
"licenseConcluded": "MIT",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "MIT",
"copyrightText": "NOASSERTION",
"externalRefs": [
@ -41,8 +41,8 @@
"versionInfo": "2.0.1",
"downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"externalRefs": [
{

View file

@ -3,23 +3,23 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"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": {
"licenseListVersion": "3.20",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus"
],
"created": "2023-05-02T18:24:18Z"
"created": "2023-05-09T17:11:26Z"
},
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"SPDXID": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"versionInfo": "1.0.1",
"downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt",
"licenseConcluded": "MIT",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "MIT",
"copyrightText": "NOASSERTION",
"externalRefs": [
@ -41,8 +41,8 @@
"versionInfo": "2.0.1",
"downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt",
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"externalRefs": [
{

View file

@ -3,23 +3,23 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"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": {
"licenseListVersion": "3.20",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus"
],
"created": "2023-05-02T18:24:18Z"
"created": "2023-05-09T17:11:26Z"
},
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"SPDXID": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"versionInfo": "1.0.1",
"downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt",
"licenseConcluded": "MIT",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "MIT",
"copyrightText": "NOASSERTION",
"externalRefs": [
@ -41,8 +41,8 @@
"versionInfo": "2.0.1",
"downloadLocation": "NOASSERTION",
"sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt",
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"externalRefs": [
{
@ -152,32 +152,32 @@
],
"relationships": [
{
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-f1-5265a4dde3edbf7c",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-z1-f5-839d99ee67d9d174",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-a1-f6-9c2f7510199b17f6",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-d2-f4-c641caa71518099f",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-d1-f3-c6f5b29dca12661f",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"spdxElementId": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd",
"relationshipType": "CONTAINS"
},

View file

@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
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
Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: 2023-05-02T18:24:33Z
Created: 2023-05-09T17:11:49Z
##### Package: @at-sign
@ -15,8 +15,8 @@ SPDXID: SPDXRef-Package---at-sign-3732f7a5679bdec4
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths:
PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
##### Package: some/slashes
@ -26,8 +26,8 @@ SPDXID: SPDXRef-Package--some-slashes-1345166d4801153b
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths:
PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
##### Package: under_scores
@ -37,8 +37,8 @@ SPDXID: SPDXRef-Package--under-scores-290d5c77210978c1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths:
PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
##### Relationships

View file

@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
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
Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: 2023-05-02T18:24:33Z
Created: 2023-05-09T17:11:49Z
##### Unpackaged files
@ -54,8 +54,8 @@ PackageVersion: 2.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from DPKG DB: /somefile-2.txt
PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
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
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-66ba429119b8bec6
SPDXID: SPDXRef-Package-python-package-1-125840abc1c66dd7
PackageVersion: 1.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from installed python package manifest file: /somefile-1.txt
PackageLicenseConcluded: MIT
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: MIT
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:*
@ -76,11 +76,11 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 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-66ba429119b8bec6 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-66ba429119b8bec6 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT

View file

@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
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
Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: 2023-05-02T18:24:33Z
Created: 2023-05-09T17:11:49Z
##### Package: package-2
@ -16,8 +16,8 @@ PackageVersion: 2.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from DPKG DB: /some/path/pkg1
PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
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
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-1b1d0be59ac59d2c
SPDXID: SPDXRef-Package-python-package-1-9265397e5e15168a
PackageVersion: 1.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from installed python package manifest file: /some/path/pkg1
PackageLicenseConcluded: MIT
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: MIT
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*

View file

@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
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
Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: 2023-05-02T18:24:33Z
Created: 2023-05-09T17:11:49Z
##### Package: package-2
@ -16,8 +16,8 @@ PackageVersion: 2.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from DPKG DB: /somefile-2.txt
PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
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
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-66ba429119b8bec6
SPDXID: SPDXRef-Package-python-package-1-125840abc1c66dd7
PackageVersion: 1.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from installed python package manifest file: /somefile-1.txt
PackageLicenseConcluded: MIT
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: MIT
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:*

View file

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

View file

@ -7,6 +7,7 @@ import (
"reflect"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@ -27,12 +28,50 @@ type PackageBasicData struct {
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Licenses licenses `json:"licenses"`
Language pkg.Language `json:"language"`
CPEs []string `json:"cpes"`
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.
type PackageCustomData struct {
MetadataType pkg.MetadataType `json:"metadataType,omitempty"`

View file

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
@ -30,7 +31,14 @@ func TestUnmarshalPackageGolang(t *testing.T) {
"path": "/Users/hal/go/bin/syft"
}
],
"licenses": [],
"licenses": [
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": []
}
],
"language": "go",
"cpes": [],
"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"
}
],
"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",
"cpes": [],
"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)
},
},
{
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 {
@ -152,9 +250,6 @@ func Test_unpackMetadata(t *testing.T) {
"layerID": "sha256:74ddd0ec08fa43d09f32636ba91a0a3053b02cb4627c35051aff89f853606b59"
}
],
"licenses": [
"GPLv2+"
],
"language": "",
"cpes": [
"cpe:2.3:a:centos:acl:2.2.53-1.el8:*:*:*:*:*:*:*",

View file

@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "1b1d0be59ac59d2c",
"id": "9265397e5e15168a",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@ -12,7 +12,13 @@
}
],
"licenses": [
"MIT"
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": [],
"locations": []
}
],
"language": "python",
"cpes": [
@ -23,7 +29,6 @@
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
@ -87,5 +92,9 @@
"configuration": {
"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": [
{
"id": "304a5a8e5958a49d",
"id": "271e49ba46e0b601",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@ -12,7 +12,13 @@
}
],
"licenses": [
"MIT"
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": [],
"locations": []
}
],
"language": "python",
"cpes": [
@ -23,7 +29,6 @@
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
@ -187,5 +192,9 @@
"configuration": {
"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": [
{
"id": "66ba429119b8bec6",
"id": "125840abc1c66dd7",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@ -9,11 +9,17 @@
"locations": [
{
"path": "/somefile-1.txt",
"layerID": "sha256:7e139310bd6ce0956d65a70d26a6d31b240a4f47094a831638f05d381b6c424a"
"layerID": "sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777"
}
],
"licenses": [
"MIT"
{
"value": "MIT",
"spdxExpression": "MIT",
"type": "declared",
"url": [],
"locations": []
}
],
"language": "python",
"cpes": [
@ -24,7 +30,6 @@
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
@ -40,7 +45,7 @@
"locations": [
{
"path": "/somefile-2.txt",
"layerID": "sha256:cc833bf31a480c064d65ca67ee37f77f0d0c8ab98eedde7b286ad1ef6f5bdcac"
"layerID": "sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2"
}
],
"licenses": [],
@ -64,11 +69,11 @@
],
"artifactRelationships": [],
"source": {
"id": "0af8fa79f5497297e4e32f3e03de14ac20ad695159df0ac8373e6543614b9a50",
"id": "c8ac88bbaf3d1c036f6a1d601c3d52bafbf05571c97d68322e7cb3a7ecaa304f",
"type": "image",
"target": {
"userInput": "user-image-input",
"imageID": "sha256:0cb4395791986bda17562bd6f76811bb6f163f686e198397197ef8241bed58df",
"imageID": "sha256:a3c61dc134d2f31b415c50324e75842d7f91622f39a89468e51938330b3fd3af",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
@ -78,17 +83,17 @@
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:7e139310bd6ce0956d65a70d26a6d31b240a4f47094a831638f05d381b6c424a",
"digest": "sha256:ab62016f9bec7286af65604081564cadeeb364a48faca2346c3f5a5a1f5ef777",
"size": 22
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:cc833bf31a480c064d65ca67ee37f77f0d0c8ab98eedde7b286ad1ef6f5bdcac",
"digest": "sha256:f1803845b6747d94d6e4ecce2331457e5f1c4fb97de5216f392a76f4582f63b2",
"size": 16
}
],
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzEsImRpZ2VzdCI6InNoYTI1NjowY2I0Mzk1NzkxOTg2YmRhMTc1NjJiZDZmNzY4MTFiYjZmMTYzZjY4NmUxOTgzOTcxOTdlZjgyNDFiZWQ1OGRmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo3ZTEzOTMxMGJkNmNlMDk1NmQ2NWE3MGQyNmE2ZDMxYjI0MGE0ZjQ3MDk0YTgzMTYzOGYwNWQzODFiNmM0MjRhIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmNjODMzYmYzMWE0ODBjMDY0ZDY1Y2E2N2VlMzdmNzdmMGQwYzhhYjk4ZWVkZGU3YjI4NmFkMWVmNmY1YmRjYWMifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjMtMDQtMThUMTQ6MDk6NDIuMzAxMDI2MzhaIiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjMtMDQtMThUMTQ6MDk6NDIuMjg3OTQyNzEzWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMy0wNC0xOFQxNDowOTo0Mi4zMDEwMjYzOFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMi50eHQgL3NvbWVmaWxlLTIudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OjdlMTM5MzEwYmQ2Y2UwOTU2ZDY1YTcwZDI2YTZkMzFiMjQwYTRmNDcwOTRhODMxNjM4ZjA1ZDM4MWI2YzQyNGEiLCJzaGEyNTY6Y2M4MzNiZjMxYTQ4MGMwNjRkNjVjYTY3ZWUzN2Y3N2YwZDBjOGFiOThlZWRkZTdiMjg2YWQxZWY2ZjViZGNhYyJdfX0=",
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1NjphM2M2MWRjMTM0ZDJmMzFiNDE1YzUwMzI0ZTc1ODQyZDdmOTE2MjJmMzlhODk0NjhlNTE5MzgzMzBiM2ZkM2FmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjphYjYyMDE2ZjliZWM3Mjg2YWY2NTYwNDA4MTU2NGNhZGVlYjM2NGE0OGZhY2EyMzQ2YzNmNWE1YTFmNWVmNzc3In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmYxODAzODQ1YjY3NDdkOTRkNmU0ZWNjZTIzMzE0NTdlNWYxYzRmYjk3ZGU1MjE2ZjM5MmE3NmY0NTgyZjYzYjIifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjMtMDQtMjFUMTk6MTA6MzcuNjUxODMxMjM0WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIzLTA0LTIxVDE5OjEwOjM3LjYwNzYxMzU1NVoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjMtMDQtMjFUMTk6MTA6MzcuNjUxODMxMjM0WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6YWI2MjAxNmY5YmVjNzI4NmFmNjU2MDQwODE1NjRjYWRlZWIzNjRhNDhmYWNhMjM0NmMzZjVhNWExZjVlZjc3NyIsInNoYTI1NjpmMTgwMzg0NWI2NzQ3ZDk0ZDZlNGVjY2UyMzMxNDU3ZTVmMWM0ZmI5N2RlNTIxNmYzOTJhNzZmNDU4MmY2M2IyIl19fQ==",
"repoDigests": [],
"architecture": "",
"os": ""
@ -110,5 +115,9 @@
"configuration": {
"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
}
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.
func toPackageModel(p pkg.Package) model.Package {
var cpes = make([]string, len(p.CPEs))
@ -191,9 +209,11 @@ func toPackageModel(p pkg.Package) model.Package {
cpes[i] = cpe.String(c)
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
// we want to make sure all catalogers are
// initializing the array; this is a good choke point for this check
var licenses = make([]model.License, 0)
if !p.Licenses.Empty() {
licenses = toLicenseModel(p.Licenses.ToSlice())
}
return model.Package{

View file

@ -9,6 +9,7 @@ import (
"github.com/google/go-cmp/cmp"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
@ -101,6 +102,19 @@ func toSyftFiles(files []model.File) sbom.Artifacts {
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 {
switch ty {
case "SymbolicLink":
@ -304,7 +318,7 @@ func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
Version: p.Version,
FoundBy: p.FoundBy,
Locations: source.NewLocationSet(p.Locations...),
Licenses: p.Licenses,
Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
Language: p.Language,
Type: p.Type,
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"`
Architecture string `mapstructure:"arch" json:"architecture" cyclonedx:"architecture"`
Size int `mapstructure:"size" json:"size" cyclonedx:"size"`
Packager string `mapstructure:"packager" json:"packager" cyclonedx:"packager"`
License string `mapstructure:"license" json:"license" cyclonedx:"license"`
URL string `mapstructure:"url" json:"url" cyclonedx:"url"`
Validation string `mapstructure:"validation" json:"validation" cyclonedx:"validation"`
Reason int `mapstructure:"reason" json:"reason" cyclonedx:"reason"`
Files []AlpmFileRecord `mapstructure:"files" json:"files" cyclonedx:"files"`
Backup []AlpmFileRecord `mapstructure:"backup" json:"backup" cyclonedx:"backup"`
Packager string `mapstructure:"packager" json:"packager"`
URL string `mapstructure:"url" json:"url"`
Validation string `mapstructure:"validation" json:"validation"`
Reason int `mapstructure:"reason" json:"reason"`
Files []AlpmFileRecord `mapstructure:"files" json:"files"`
Backup []AlpmFileRecord `mapstructure:"backup" json:"backup"`
}
type AlpmFileRecord struct {

View file

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

View file

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

View file

@ -17,6 +17,49 @@ type expectedIndexes struct {
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) {
tests := []struct {
name string

View file

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

View file

@ -1,29 +1,33 @@
package alpm
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"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{
Name: m.Package,
Version: m.Version,
Locations: source.NewLocationSet(locations...),
Locations: source.NewLocationSet(dbLocation),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation.WithoutAnnotations(), licenseCandidates...)...),
Type: pkg.AlpmPkg,
Licenses: internal.SplitAny(m.License, " \n"),
PURL: packageURL(m, release),
MetadataType: pkg.AlpmMetadataType,
Metadata: m,
Metadata: m.AlpmMetadata,
}
p.SetID()
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" {
// note: there is no namespace variation (like with debian ID_LIKE for ubuntu ID, for example)
return ""

View file

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

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) {
metadata, err := parseAlpmDBEntry(reader)
data, err := parseAlpmDBEntry(reader)
if err != nil {
return nil, nil, err
}
@ -48,9 +53,10 @@ func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader
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.
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
files := filepath.Join(base, "files")
@ -62,23 +68,23 @@ func parseAlpmDB(resolver source.FileResolver, env *generic.Environment, reader
if err != nil {
return nil, nil, err
} else if filesMetadata != nil {
metadata.Backup = filesMetadata.Backup
data.Backup = filesMetadata.Backup
}
if metadata.Package == "" {
if data.Package == "" {
return nil, nil, nil
}
return []pkg.Package{
newPackage(
*metadata,
data,
env.LinuxRelease,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
}, nil, nil
}
func parseAlpmDBEntry(reader io.Reader) (*pkg.AlpmMetadata, error) {
func parseAlpmDBEntry(reader io.Reader) (*parsedData, error) {
scanner := newScanner(reader)
metadata, err := parseDatabase(scanner)
if err != nil {
@ -128,8 +134,7 @@ func getFileReader(path string, resolver source.FileResolver) (io.Reader, error)
return dbContentReader, nil
}
func parseDatabase(b *bufio.Scanner) (*pkg.AlpmMetadata, error) {
var entry pkg.AlpmMetadata
func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
var err error
pkgFields := make(map[string]interface{})
for b.Scan() {
@ -181,16 +186,23 @@ func parseDatabase(b *bufio.Scanner) (*pkg.AlpmMetadata, error) {
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 {
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 {
entry.Backup = make([]pkg.AlpmFileRecord, 0)
}
if entry.Package == "" && len(entry.Files) == 0 && len(entry.Backup) == 0 {
return nil, nil
}
return &entry, nil
}

View file

@ -9,16 +9,18 @@ import (
"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{
Name: d.Package,
Version: d.Version,
Locations: source.NewLocationSet(locations...),
Licenses: strings.Split(d.License, " "),
PURL: packageURL(d, release),
Locations: source.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation, licenseStrings...)...),
PURL: packageURL(d.ApkMetadata, release),
Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType,
Metadata: d,
Metadata: d.ApkMetadata,
}
p.SetID()

View file

@ -15,16 +15,19 @@ import (
func Test_PackageURL(t *testing.T) {
tests := []struct {
name string
metadata pkg.ApkMetadata
metadata parsedData
distro linux.Release
expected string
}{
{
name: "non-alpine distro",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
},
},
distro: linux.Release{
ID: "something else",
@ -34,10 +37,13 @@ func Test_PackageURL(t *testing.T) {
},
{
name: "gocase",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
},
},
distro: linux.Release{
ID: "alpine",
@ -47,9 +53,12 @@ func Test_PackageURL(t *testing.T) {
},
{
name: "missing architecture",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
},
},
distro: linux.Release{
ID: "alpine",
@ -59,10 +68,13 @@ func Test_PackageURL(t *testing.T) {
},
// verify #351
{
metadata: pkg.ApkMetadata{
Package: "g++",
Version: "v84",
Architecture: "am86",
metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "g++",
Version: "v84",
Architecture: "am86",
},
},
distro: linux.Release{
ID: "alpine",
@ -71,10 +83,13 @@ func Test_PackageURL(t *testing.T) {
expected: "pkg:apk/alpine/g++@v84?arch=am86&distro=alpine-3.4.6",
},
{
metadata: pkg.ApkMetadata{
Package: "g plus plus",
Version: "v84",
Architecture: "am86",
metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "g plus plus",
Version: "v84",
Architecture: "am86",
},
},
distro: linux.Release{
ID: "alpine",
@ -84,11 +99,14 @@ func Test_PackageURL(t *testing.T) {
},
{
name: "add source information as qualifier",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
OriginPackage: "origin",
metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
OriginPackage: "origin",
},
},
distro: linux.Release{
ID: "alpine",
@ -98,10 +116,13 @@ func Test_PackageURL(t *testing.T) {
},
{
name: "wolfi distro",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
metadata: parsedData{
License: "",
ApkMetadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
},
},
distro: linux.Release{
ID: "wolfi",
@ -113,7 +134,7 @@ func Test_PackageURL(t *testing.T) {
for _, test := range tests {
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 {
dmp := diffmatchpatch.New()
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_]+)$`)
)
type parsedData struct {
License string `mapstructure:"L" json:"license"`
pkg.ApkMetadata
}
// parseApkDB parses packages from a given APK installed DB file. For more
// 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) {
scanner := bufio.NewScanner(reader)
var apks []pkg.ApkMetadata
var currentEntry pkg.ApkMetadata
var apks []parsedData
var currentEntry parsedData
entryParsingInProgress := false
fileParsingCtx := newApkFileParsingContext()
// creating a dedicated append-like function here instead of using `append(...)`
// below since there is nontrivial logic to be performed for each finalized apk
// entry.
appendApk := func(p pkg.ApkMetadata) {
appendApk := func(p parsedData) {
if files := fileParsingCtx.files; len(files) >= 1 {
// attached accumulated files to current package
p.Files = files
@ -68,7 +73,7 @@ func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader s
entryParsingInProgress = false
// zero-out currentEntry for use by any future entry
currentEntry = pkg.ApkMetadata{}
currentEntry = parsedData{}
continue
}
@ -123,7 +128,7 @@ func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader s
pkgs := make([]pkg.Package, 0, len(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
@ -201,7 +206,7 @@ type apkField struct {
}
//nolint:funlen
func (f apkField) apply(p *pkg.ApkMetadata, ctx *apkFileParsingContext) {
func (f apkField) apply(p *parsedData, ctx *apkFileParsingContext) {
switch f.name {
// APKINDEX field parsing
@ -347,7 +352,7 @@ func parseListValue(value string) []string {
return nil
}
func nilFieldsToEmptySlice(p *pkg.ApkMetadata) {
func nilFieldsToEmptySlice(p *parsedData) {
if p.Dependencies == nil {
p.Dependencies = []string{}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -22,9 +22,12 @@ const (
)
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{
Name: d.Package,
Version: d.Version,
Licenses: pkg.NewLicenseSet(licenses...),
Locations: source.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(d, release),
Type: pkg.DebPkg,
@ -93,8 +96,10 @@ func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pk
if copyrightReader != nil && copyrightLocation != nil {
defer internal.CloseAndLogError(copyrightReader, copyrightLocation.VirtualPath)
// 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
p.Locations.Add(*copyrightLocation)
}

View file

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

View file

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

View file

@ -13,24 +13,42 @@ import (
"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"
)
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 {
name string
version string
expected string
expected pkg.License
}{
{
name: "github.com/someorg/somename",
version: "v0.3.2",
expected: "Apache-2.0",
name: "github.com/someorg/somename",
version: "v0.3.2",
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",
version: "v4.111.5",
expected: "MIT",
name: "github.com/CapORG/CapProject",
version: "v4.111.5",
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 {
t.Run(test.name, func(t *testing.T) {
l := newGoLicenses(GoCatalogerOpts{
searchLocalModCacheLicenses: true,
localModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
})
l := newGoLicenses(
GoCatalogerOpts{
searchLocalModCacheLicenses: true,
localModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
},
)
licenses, err := l.getLicenses(source.EmptyResolver{}, test.name, test.version)
require.NoError(t, err)
@ -54,6 +74,9 @@ func Test_LocalLicenseSearch(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) {
buf := &bytes.Buffer{}
uri := strings.TrimPrefix(strings.TrimSuffix(r.RequestURI, ".zip"), "/")
@ -94,17 +117,29 @@ func Test_RemoteProxyLicenseSearch(t *testing.T) {
tests := []struct {
name string
version string
expected string
expected pkg.License
}{
{
name: "github.com/someorg/somename",
version: "v0.3.2",
expected: "Apache-2.0",
name: "github.com/someorg/somename",
version: "v0.3.2",
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",
version: "v4.111.5",
expected: "MIT",
name: "github.com/CapORG/CapProject",
version: "v4.111.5",
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{
Name: dep.Path,
Version: dep.Version,
Licenses: licenses,
Licenses: pkg.NewLicenseSet(licenses...),
PURL: packageURL(dep.Path, dep.Version),
Language: pkg.Go,
Type: pkg.GoModulePkg,

View file

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

View file

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

View file

@ -88,12 +88,6 @@ func TestParseGoMod(t *testing.T) {
for _, test := range tests {
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{}
pkgtest.NewCatalogTester().
FromFile(t, test.fixture).
@ -154,12 +148,6 @@ func Test_GoSumHashes(t *testing.T) {
for _, test := range tests {
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().
FromDirectory(t, test.fixture).
Expects(test.expected, nil).

View file

@ -21,6 +21,7 @@ import (
)
type locationComparer func(x, y source.Location) bool
type licenseComparer func(x, y pkg.License) bool
type CatalogTester struct {
expectedPkgs []pkg.Package
@ -36,12 +37,14 @@ type CatalogTester struct {
wantErr require.ErrorAssertionFunc
compareOptions []cmp.Option
locationComparer locationComparer
licenseComparer licenseComparer
}
func NewCatalogTester() *CatalogTester {
return &CatalogTester{
wantErr: require.NoError,
locationComparer: DefaultLocationComparer,
licenseComparer: DefaultLicenseComparer,
ignoreUnfulfilledPathResponses: map[string][]string{
"FilesByPath": {
// 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)
}
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 {
t.Helper()
@ -139,6 +161,26 @@ func (p *CatalogTester) IgnoreLocationLayer() *CatalogTester {
p.locationComparer = func(x, y source.Location) bool {
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
}
@ -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) {
t.Helper()
@ -233,6 +276,30 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi
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
},
),
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 != "" {

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)
}
// 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{
Name: selectName(manifest, j.fileInfo),
Version: selectVersion(manifest, j.fileInfo),
Licenses: selectLicense(manifest),
Language: pkg.Java,
Licenses: pkg.NewLicenseSet(licenses...),
Locations: source.NewLocationSet(
j.location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),

View file

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

View file

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

View file

@ -12,14 +12,16 @@ func Test_JavascriptCataloger(t *testing.T) {
locationSet := source.NewLocationSet(source.NewLocation("package-lock.json"))
expectedPkgs := []pkg.Package{
{
Name: "@actions/core",
Version: "1.6.0",
FoundBy: "javascript-lock-cataloger",
PURL: "pkg:npm/%40actions/core@1.6.0",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: []string{"MIT"},
Name: "@actions/core",
Version: "1.6.0",
FoundBy: "javascript-lock-cataloger",
PURL: "pkg:npm/%40actions/core@1.6.0",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation("package-lock.json")),
),
MetadataType: pkg.NpmPackageLockJSONMetadataType,
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", Integrity: "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw=="},
},
@ -35,14 +37,16 @@ func Test_JavascriptCataloger(t *testing.T) {
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", Integrity: "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="},
},
{
Name: "cowsay",
Version: "1.4.0",
FoundBy: "javascript-lock-cataloger",
PURL: "pkg:npm/cowsay@1.4.0",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: []string{"MIT"},
Name: "cowsay",
Version: "1.4.0",
FoundBy: "javascript-lock-cataloger",
PURL: "pkg:npm/cowsay@1.4.0",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation("package-lock.json")),
),
MetadataType: pkg.NpmPackageLockJSONMetadataType,
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"
)
func newPackageJSONPackage(u packageJSON, locations ...source.Location) pkg.Package {
licenses, err := u.licensesFromJSON()
func newPackageJSONPackage(u packageJSON, indexLocation source.Location) pkg.Package {
licenseCandidates, err := u.licensesFromJSON()
if err != nil {
log.Warnf("unable to extract licenses from javascript package.json: %+v", err)
}
license := pkg.NewLicensesFromLocation(indexLocation, licenseCandidates...)
p := pkg.Package{
Name: u.Name,
Version: u.Version,
Licenses: licenses,
PURL: packageURL(u.Name, u.Version),
Locations: source.NewLocationSet(locations...),
Locations: source.NewLocationSet(indexLocation),
Language: pkg.JavaScript,
Licenses: pkg.NewLicenseSet(license...),
Type: pkg.NpmPkg,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
Name: u.Name,
Version: u.Version,
Description: u.Description,
Author: u.Author.AuthorString(),
Homepage: u.Homepage,
URL: u.Repository.URL,
Licenses: licenses,
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 {
var licenses []string
if u.License != nil {
licenses = u.License
}
return finalizeLockPkg(
resolver,
location,
@ -90,10 +84,10 @@ func newPackageLockV2Package(resolver source.FileResolver, location source.Locat
Name: name,
Version: u.Version,
Locations: source.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(location, u.License...)...),
PURL: packageURL(name, u.Version),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: licenses,
MetadataType: pkg.NpmPackageLockJSONMetadataType,
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 {
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()
return p
}
@ -140,13 +135,13 @@ func addLicenses(name string, resolver source.FileResolver, location source.Loca
if resolver == nil {
return allLicenses
}
dir := path.Dir(location.RealPath)
pkgPath := []string{dir, "node_modules"}
pkgPath = append(pkgPath, strings.Split(name, "/")...)
pkgPath = append(pkgPath, "package.json")
pkgFile := path.Join(pkgPath...)
locations, err := resolver.FilesByPath(pkgFile)
if err != nil {
log.Debugf("an error occurred attempting to read: %s - %+v", pkgFile, err)
return allLicenses

View file

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

View file

@ -18,12 +18,14 @@ func TestParsePackageJSON(t *testing.T) {
{
Fixture: "test-fixtures/pkg-json/package.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: []string{"Artistic-2.0"},
Language: pkg.JavaScript,
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package.json")),
),
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
Name: "npm",
@ -31,7 +33,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: []string{"Artistic-2.0"},
Description: "a package manager for JavaScript",
},
},
@ -39,12 +40,14 @@ func TestParsePackageJSON(t *testing.T) {
{
Fixture: "test-fixtures/pkg-json/package-license-object.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: []string{"ISC"},
Language: pkg.JavaScript,
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("ISC", source.NewLocation("test-fixtures/pkg-json/package-license-object.json")),
),
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
Name: "npm",
@ -52,7 +55,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: []string{"ISC"},
Description: "a package manager for JavaScript",
},
},
@ -60,11 +62,14 @@ func TestParsePackageJSON(t *testing.T) {
{
Fixture: "test-fixtures/pkg-json/package-license-objects.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: []string{"MIT", "Apache-2.0"},
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
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,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
@ -73,7 +78,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: []string{"MIT", "Apache-2.0"},
Description: "a package manager for JavaScript",
},
},
@ -85,7 +89,6 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: nil,
Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
@ -94,7 +97,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: nil,
Description: "a package manager for JavaScript",
},
},
@ -106,7 +108,6 @@ func TestParsePackageJSON(t *testing.T) {
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: []string{},
Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
@ -115,7 +116,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: []string{},
Description: "a package manager for JavaScript",
},
},
@ -123,11 +123,13 @@ func TestParsePackageJSON(t *testing.T) {
{
Fixture: "test-fixtures/pkg-json/package-nested-author.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: []string{"Artistic-2.0"},
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package-nested-author.json")),
),
Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
@ -136,7 +138,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: []string{"Artistic-2.0"},
Description: "a package manager for JavaScript",
},
},
@ -144,11 +145,13 @@ func TestParsePackageJSON(t *testing.T) {
{
Fixture: "test-fixtures/pkg-json/package-repo-string.json",
ExpectedPkg: pkg.Package{
Name: "function-bind",
Version: "1.1.1",
PURL: "pkg:npm/function-bind@1.1.1",
Type: pkg.NpmPkg,
Licenses: []string{"MIT"},
Name: "function-bind",
Version: "1.1.1",
PURL: "pkg:npm/function-bind@1.1.1",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation("test-fixtures/pkg-json/package-repo-string.json")),
),
Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
@ -157,7 +160,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Raynos <raynos2@gmail.com>",
Homepage: "https://github.com/Raynos/function-bind",
URL: "git://github.com/Raynos/function-bind.git",
Licenses: []string{"MIT"},
Description: "Implementation of Function.prototype.bind",
},
},
@ -165,11 +167,13 @@ func TestParsePackageJSON(t *testing.T) {
{
Fixture: "test-fixtures/pkg-json/package-private.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: []string{"Artistic-2.0"},
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Artistic-2.0", source.NewLocation("test-fixtures/pkg-json/package-private.json")),
),
Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
@ -178,7 +182,6 @@ func TestParsePackageJSON(t *testing.T) {
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: []string{"Artistic-2.0"},
Private: true,
Description: "a package manager for JavaScript",
},

View file

@ -25,39 +25,6 @@ type packageLock struct {
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
type lockDependency struct {
Version string `json:"version"`
@ -73,6 +40,9 @@ type lockPackage struct {
License packageLockLicense `json:"license"`
}
// packageLockLicense
type packageLockLicense []string
// 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) {
// 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
}
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 {
parts := strings.Split(path, "node_modules/")
return parts[len(parts)-1]

View file

@ -134,42 +134,50 @@ func TestParsePackageLockV2(t *testing.T) {
Metadata: pkg.NpmPackageLockJSONMetadata{},
},
{
Name: "@types/prop-types",
Version: "15.7.5",
PURL: "pkg:npm/%40types/prop-types@15.7.5",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: []string{"MIT"},
Name: "@types/prop-types",
Version: "15.7.5",
PURL: "pkg:npm/%40types/prop-types@15.7.5",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", Integrity: "sha1-XxnSuFqY6VWANvajysyIGUIPBc8="},
},
{
Name: "@types/react",
Version: "18.0.17",
PURL: "pkg:npm/%40types/react@18.0.17",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: []string{"MIT"},
Name: "@types/react",
Version: "18.0.17",
PURL: "pkg:npm/%40types/react@18.0.17",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", Integrity: "sha1-RYPZwyLWfv5LOak10iPtzHBQzPQ="},
},
{
Name: "@types/scheduler",
Version: "0.16.2",
PURL: "pkg:npm/%40types/scheduler@0.16.2",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: []string{"MIT"},
Name: "@types/scheduler",
Version: "0.16.2",
PURL: "pkg:npm/%40types/scheduler@0.16.2",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{Resolved: "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", Integrity: "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk="},
},
{
Name: "csstype",
Version: "3.1.0",
PURL: "pkg:npm/csstype@3.1.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: []string{"MIT"},
Name: "csstype",
Version: "3.1.0",
PURL: "pkg:npm/csstype@3.1.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
MetadataType: "NpmPackageLockJsonMetadata",
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) {
},
}
v2Pkg := pkg.Package{
Name: "alias-check",
Version: "1.0.0",
PURL: "pkg:npm/alias-check@1.0.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: []string{"ISC"},
MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{},
}
packageLockV1 := "test-fixtures/pkg-lock/alias-package-lock-1.json"
packageLockV2 := "test-fixtures/pkg-lock/alias-package-lock-2.json"
packageLocks := []string{packageLockV1, packageLockV2}
for _, packageLock := range packageLocks {
v2Pkg := pkg.Package{
Name: "alias-check",
Version: "1.0.0",
PURL: "pkg:npm/alias-check@1.0.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("ISC", source.NewLocation(packageLockV2)),
),
MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{},
}
for _, pl := range packageLocks {
expected := make([]pkg.Package, len(commonPkgs))
copy(expected, commonPkgs)
if packageLock == packageLockV2 {
if pl == packageLockV2 {
expected = append(expected, v2Pkg)
}
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)
}
}
@ -303,31 +313,39 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) {
var expectedRelationships []artifact.Relationship
expectedPkgs := []pkg.Package{
{
Name: "tmp",
Version: "1.0.0",
Licenses: []string{"ISC"},
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Name: "tmp",
Version: "1.0.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("ISC", source.NewLocation(fixture)),
),
PURL: "pkg:npm/tmp@1.0.0",
MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{},
},
{
Name: "pause-stream",
Version: "0.0.11",
Licenses: []string{"MIT", "Apache2"},
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Name: "pause-stream",
Version: "0.0.11",
Language: pkg.JavaScript,
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",
MetadataType: "NpmPackageLockJsonMetadata",
Metadata: pkg.NpmPackageLockJSONMetadata{},
},
{
Name: "through",
Version: "2.3.8",
Licenses: []string{"MIT"},
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Name: "through",
Version: "2.3.8",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
PURL: "pkg:npm/through@2.3.8",
MetadataType: "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",
),
),
Licenses: []string{
"GPL v2",
},
Licenses: pkg.NewLicenseSet(
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,
PURL: "pkg:generic/ttynull",
MetadataType: pkg.LinuxKernelModuleMetadataType,

View file

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

View file

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

View file

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

View file

@ -8,23 +8,24 @@ import (
"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{
Name: m.Name,
Version: m.Version,
Locations: source.NewLocationSet(location...),
Locations: source.NewLocationSet(indexLocation),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, m.License...)...),
PURL: packageURL(m),
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: m,
Metadata: m.PhpComposerJSONMetadata,
}
p.SetID()
return p
}
func packageURL(m pkg.PhpComposerJSONMetadata) string {
func packageURL(m parsedData) string {
var name, vendor string
fields := strings.Split(m.Name, "/")
switch len(fields) {

View file

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

View file

@ -14,9 +14,14 @@ import (
var _ generic.Parser = parseComposerLock
type parsedData struct {
License []string `json:"license"`
pkg.PhpComposerJSONMetadata
}
type composerLock struct {
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
PackageDev []pkg.PhpComposerJSONMetadata `json:"packages-dev"`
Packages []parsedData `json:"packages"`
PackageDev []parsedData `json:"packages-dev"`
}
// 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

View file

@ -15,10 +15,13 @@ func TestParseComposerFileLock(t *testing.T) {
locations := source.NewLocationSet(source.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "adoy/fastcgi-client",
Version: "1.0.2",
PURL: "pkg:composer/adoy/fastcgi-client@1.0.2",
Locations: locations,
Name: "adoy/fastcgi-client",
Version: "1.0.2",
PURL: "pkg:composer/adoy/fastcgi-client@1.0.2",
Locations: locations,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
@ -37,9 +40,6 @@ func TestParseComposerFileLock(t *testing.T) {
},
Type: "library",
NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{
{
Name: "Pierrick Charron",
@ -55,11 +55,14 @@ func TestParseComposerFileLock(t *testing.T) {
},
},
{
Name: "alcaeus/mongo-php-adapter",
Version: "1.1.11",
Locations: locations,
PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11",
Language: pkg.PHP,
Name: "alcaeus/mongo-php-adapter",
Version: "1.1.11",
Locations: locations,
PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11",
Language: pkg.PHP,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", source.NewLocation(fixture)),
),
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkg.PhpComposerJSONMetadata{
@ -91,9 +94,6 @@ func TestParseComposerFileLock(t *testing.T) {
},
Type: "library",
NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{
{
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
type installedJSONComposerV2 struct {
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
Packages []parsedData `json:"packages"`
}
func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error {
type compv2 struct {
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
Packages []parsedData `json:"packages"`
}
compv2er := new(compv2)
err := json.Unmarshal(data, &compv2er)
if err != nil {
// If we had an err or, we may be dealing with a composer v.1 installed.json
// which should be all arrays
var packages []pkg.PhpComposerJSONMetadata
var packages []parsedData
err := json.Unmarshal(data, &packages)
if err != nil {
return err

View file

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

View file

@ -11,7 +11,7 @@ import (
)
func TestPortageCataloger(t *testing.T) {
expectedLicenseLocation := source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE")
expectedPkgs := []pkg.Package{
{
Name: "app-containers/skopeo",
@ -20,10 +20,10 @@ func TestPortageCataloger(t *testing.T) {
PURL: "pkg:ebuild/app-containers/skopeo@1.5.1",
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/LICENSE"),
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,
MetadataType: pkg.PortageMetadataType,
Metadata: pkg.PortageMetadata{

View file

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

View file

@ -40,18 +40,19 @@ func Test_PackageCataloger(t *testing.T) {
"test-fixtures/egg-info/top_level.txt",
},
expectedPackage: pkg.Package{
Name: "requests",
Version: "2.22.0",
PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: []string{"Apache 2.0"},
Name: "requests",
Version: "2.22.0",
PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Apache 2.0", source.NewLocation("test-fixtures/egg-info/PKG-INFO")),
),
FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "requests",
Version: "2.22.0",
License: "Apache 2.0",
Platform: "UNKNOWN",
Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org",
@ -77,18 +78,19 @@ func Test_PackageCataloger(t *testing.T) {
"test-fixtures/dist-info/direct_url.json",
},
expectedPackage: pkg.Package{
Name: "Pygments",
Version: "2.6.1",
PURL: "pkg:pypi/Pygments@2.6.1?vcs_url=git+https://github.com/python-test/test.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: []string{"BSD License"},
Name: "Pygments",
Version: "2.6.1",
PURL: "pkg:pypi/Pygments@2.6.1?vcs_url=git+https://github.com/python-test/test.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/dist-info/METADATA")),
),
FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "Pygments",
Version: "2.6.1",
License: "BSD License",
Platform: "any",
Author: "Georg Brandl",
AuthorEmail: "georg@python.org",
@ -114,18 +116,19 @@ func Test_PackageCataloger(t *testing.T) {
"test-fixtures/malformed-record/dist-info/RECORD",
},
expectedPackage: pkg.Package{
Name: "Pygments",
Version: "2.6.1",
PURL: "pkg:pypi/Pygments@2.6.1",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: []string{"BSD License"},
Name: "Pygments",
Version: "2.6.1",
PURL: "pkg:pypi/Pygments@2.6.1",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/malformed-record/dist-info/METADATA")),
),
FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "Pygments",
Version: "2.6.1",
License: "BSD License",
Platform: "any",
Author: "Georg Brandl",
AuthorEmail: "georg@python.org",
@ -145,18 +148,19 @@ func Test_PackageCataloger(t *testing.T) {
name: "partial dist-info directory",
fixtures: []string{"test-fixtures/partial.dist-info/METADATA"},
expectedPackage: pkg.Package{
Name: "Pygments",
Version: "2.6.1",
PURL: "pkg:pypi/Pygments@2.6.1",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: []string{"BSD License"},
Name: "Pygments",
Version: "2.6.1",
PURL: "pkg:pypi/Pygments@2.6.1",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", source.NewLocation("test-fixtures/partial.dist-info/METADATA")),
),
FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "Pygments",
Version: "2.6.1",
License: "BSD License",
Platform: "any",
Author: "Georg Brandl",
AuthorEmail: "georg@python.org",
@ -168,18 +172,19 @@ func Test_PackageCataloger(t *testing.T) {
name: "egg-info regular file",
fixtures: []string{"test-fixtures/test.egg-info"},
expectedPackage: pkg.Package{
Name: "requests",
Version: "2.22.0",
PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: []string{"Apache 2.0"},
Name: "requests",
Version: "2.22.0",
PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Apache 2.0", source.NewLocation("test-fixtures/test.egg-info")),
),
FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "requests",
Version: "2.22.0",
License: "Apache 2.0",
Platform: "UNKNOWN",
Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org",

View file

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

View file

@ -17,21 +17,21 @@ import (
// 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) {
metadata, sources, err := assembleEggOrWheelMetadata(resolver, reader.Location)
pd, sources, err := assembleEggOrWheelMetadata(resolver, reader.Location)
if err != nil {
return nil, nil, err
}
if metadata == nil {
if pd == nil {
return nil, nil, nil
}
// 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
if metadata.Name == "Python" {
if pd.Name == "Python" {
return nil, nil, nil
}
pkgs := []pkg.Package{newPackageForPackage(*metadata, sources...)}
pkgs := []pkg.Package{newPackageForPackage(*pd, sources...)}
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.
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{
metadataLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
}
@ -171,12 +171,12 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s
}
defer internal.CloseAndLogError(metadataContents, metadataLocation.VirtualPath)
metadata, err := parseWheelOrEggMetadata(metadataLocation.RealPath, metadataContents)
pd, err := parseWheelOrEggMetadata(metadataLocation.RealPath, metadataContents)
if err != nil {
return nil, nil, err
}
if metadata.Name == "" {
if pd.Name == "" {
return nil, nil, nil
}
@ -186,14 +186,14 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s
return nil, nil, err
}
if len(r) == 0 {
r, s, err = fetchInstalledFiles(resolver, metadataLocation, metadata.SitePackagesRootPath)
r, s, err = fetchInstalledFiles(resolver, metadataLocation, pd.SitePackagesRootPath)
if err != nil {
return nil, nil, err
}
}
sources = append(sources, s...)
metadata.Files = r
pd.Files = r
// attach any top-level package names found for the given wheel/egg installation
p, s, err := fetchTopLevelPackages(resolver, metadataLocation)
@ -201,15 +201,15 @@ func assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation s
return nil, nil, err
}
sources = append(sources, s...)
metadata.TopLevelPackages = p
pd.TopLevelPackages = p
// attach any direct-url package data found for the given wheel/egg installation
d, s, err := fetchDirectURLData(resolver, metadataLocation)
if err != nil {
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/log"
"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),
// 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)
var key string
@ -43,7 +50,7 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe
// a field-body continuation
updatedValue, err := handleFieldBodyContinuation(key, line, fields)
if err != nil {
return pkg.PythonPackageMetadata{}, err
return parsedData{}, err
}
fields[key] = updatedValue
@ -62,19 +69,22 @@ func parseWheelOrEggMetadata(path string, reader io.Reader) (pkg.PythonPackageMe
}
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
if err := mapstructure.Decode(fields, &metadata); err != nil {
return pkg.PythonPackageMetadata{}, fmt.Errorf("unable to parse APK metadata: %w", err)
var pd parsedData
if err := mapstructure.Decode(fields, &pd); err != nil {
return pd, fmt.Errorf("unable to parse APK metadata: %w", err)
}
// 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

View file

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

View file

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

View file

@ -1,6 +1,8 @@
package r
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
@ -11,11 +13,14 @@ func newPackage(pd parseData, locations ...source.Location) pkg.Package {
for _, loc := range locations {
locationSet.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
}
licenses := parseLicenseData(pd.License)
result := pkg.Package{
Name: pd.Package,
Version: pd.Version,
Locations: locationSet,
Licenses: []string{pd.License},
Licenses: pkg.NewLicenseSet(licenses...),
Language: pkg.R,
Type: pkg.Rpkg,
PURL: packageURL(pd),
@ -30,3 +35,95 @@ func newPackage(pd parseData, locations ...source.Location) pkg.Package {
func packageURL(m parseData) string {
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
import "testing"
import (
"testing"
func Test_newPackage(t *testing.T) {
"github.com/anchore/syft/syft/pkg"
)
func Test_NewPackageLicenses(t *testing.T) {
testCases := []struct {
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 {
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,42 +13,46 @@ import (
"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{
Name: metadata.Name,
Version: toELVersion(metadata),
PURL: packageURL(metadata, distro),
Locations: source.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Name: pd.Name,
Version: toELVersion(pd.RpmMetadata),
Licenses: pkg.NewLicenseSet(pd.Licenses...),
PURL: packageURL(pd.RpmMetadata, distro),
Locations: source.NewLocationSet(dbOrRpmLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Type: pkg.RpmPkg,
MetadataType: pkg.RpmMetadataType,
Metadata: metadata,
}
if metadata.License != "" {
p.Licenses = append(p.Licenses, metadata.License)
Metadata: pd.RpmMetadata,
}
p.SetID()
return p
}
func newMetadataFromEntry(entry rpmdb.PackageInfo, files []pkg.RpmdbFileRecord) pkg.RpmMetadata {
return pkg.RpmMetadata{
Name: entry.Name,
Version: entry.Version,
Epoch: entry.Epoch,
Arch: entry.Arch,
Release: entry.Release,
SourceRpm: entry.SourceRpm,
Vendor: entry.Vendor,
License: entry.License,
Size: entry.Size,
ModularityLabel: entry.Modularitylabel,
Files: files,
type parsedData struct {
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,
Version: entry.Version,
Epoch: entry.Epoch,
Arch: entry.Arch,
Release: entry.Release,
SourceRpm: entry.SourceRpm,
Vendor: entry.Vendor,
Size: entry.Size,
ModularityLabel: entry.Modularitylabel,
Files: files,
},
}
}
func newMetadataFromManifestLine(entry string) (*pkg.RpmMetadata, error) {
func newMetadataFromManifestLine(entry string) (*parsedData, error) {
parts := strings.Split(entry, "\t")
if len(parts) < 10 {
return nil, fmt.Errorf("unexpected number of fields in line: %s", entry)
@ -74,16 +78,17 @@ func newMetadataFromManifestLine(entry string) (*pkg.RpmMetadata, error) {
if err == nil {
size = converted
}
return &pkg.RpmMetadata{
Name: parts[0],
Version: version,
Epoch: epoch,
Arch: parts[7],
Release: release,
SourceRpm: parts[9],
Vendor: parts[4],
Size: size,
return &parsedData{
RpmMetadata: pkg.RpmMetadata{
Name: parts[0],
Version: version,
Epoch: epoch,
Arch: parts[7],
Release: release,
SourceRpm: parts[9],
Vendor: parts[4],
Size: size,
},
}, nil
}

View file

@ -3,7 +3,6 @@ package rpm
import (
"fmt"
"strconv"
"strings"
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
"github.com/sassoftware/go-rpmutils"
@ -34,20 +33,22 @@ func parseRpm(_ source.FileResolver, _ *generic.Environment, reader source.Locat
size, _ := rpm.Header.InstalledSize()
files, _ := rpm.Header.GetFiles()
metadata := pkg.RpmMetadata{
Name: nevra.Name,
Version: nevra.Version,
Epoch: parseEpoch(nevra.Epoch),
Arch: nevra.Arch,
Release: nevra.Release,
SourceRpm: sourceRpm,
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),
Files: mapFiles(files, digestAlgorithm),
pd := parsedData{
Licenses: pkg.NewLicensesFromLocation(reader.Location, licenses...),
RpmMetadata: pkg.RpmMetadata{
Name: nevra.Name,
Version: nevra.Version,
Epoch: parseEpoch(nevra.Epoch),
Arch: nevra.Arch,
Release: nevra.Release,
SourceRpm: sourceRpm,
Vendor: vendor,
Size: int(size),
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 {

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