mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
Translate Portage license strings to SPDX expressions (#1763)
* fix portage license handling Signed-off-by: Alex Goodman <alex.goodman@anchore.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * cover license_group file Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add licenses to portage metadata in json schema Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <alex.goodman@anchore.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
58392a9717
commit
e3e69596bd
@ -3,5 +3,5 @@ package internal
|
||||
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 = "16.0.28"
|
||||
JSONSchemaVersion = "16.0.29"
|
||||
)
|
||||
|
||||
2993
schema/json/schema-16.0.29.json
Normal file
2993
schema/json/schema-16.0.29.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.28/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.29/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@ -2266,6 +2266,9 @@
|
||||
"installedSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"licenses": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/PortageFileRecord"
|
||||
|
||||
@ -10,64 +10,140 @@ import (
|
||||
)
|
||||
|
||||
func TestPortageCataloger(t *testing.T) {
|
||||
expectedLicenseLocation := file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE")
|
||||
expectedPkgs := []pkg.Package{
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expectedPackages []pkg.Package
|
||||
expectedRelationships []artifact.Relationship
|
||||
}{
|
||||
{
|
||||
Name: "app-containers/skopeo",
|
||||
Version: "1.5.1",
|
||||
FoundBy: "portage-cataloger",
|
||||
PURL: "pkg:ebuild/app-containers%2Fskopeo@1.5.1",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS"),
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"),
|
||||
expectedLicenseLocation,
|
||||
),
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(expectedLicenseLocation, "Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT")...),
|
||||
Type: pkg.PortagePkg,
|
||||
Metadata: pkg.PortageEntry{
|
||||
InstalledSize: 27937835,
|
||||
Files: []pkg.PortageFileRecord{
|
||||
{
|
||||
Path: "/usr/bin/skopeo",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "376c02bd3b22804df8fdfdc895e7dbfb",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/containers/policy.json",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "c01eb6950f03419e09d4fc88cb42ff6f",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/containers/registries.d/default.yaml",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "e6e66cd3c24623e0667f26542e0e08f6",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/var/lib/atomic/sigstore/.keep_app-containers_skopeo-0",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "d41d8cd98f00b204e9800998ecf8427e",
|
||||
name: "standard skopeo package",
|
||||
fixture: "test-fixtures/layout",
|
||||
expectedPackages: []pkg.Package{
|
||||
{
|
||||
Name: "app-containers/skopeo",
|
||||
Version: "1.5.1",
|
||||
FoundBy: "portage-cataloger",
|
||||
PURL: "pkg:ebuild/app-containers%2Fskopeo@1.5.1",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS"),
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"),
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE"),
|
||||
),
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicensesFromLocation(
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE"),
|
||||
"Apache-2.0 AND BSD AND BSD-2 AND CC-BY-SA-4.0 AND ISC AND MIT")...,
|
||||
),
|
||||
Type: pkg.PortagePkg,
|
||||
Metadata: pkg.PortageEntry{
|
||||
InstalledSize: 27937835,
|
||||
Licenses: "Apache-2.0 BSD BSD-2 CC-BY-SA-4.0 ISC MIT",
|
||||
Files: []pkg.PortageFileRecord{
|
||||
{
|
||||
Path: "/usr/bin/skopeo",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "376c02bd3b22804df8fdfdc895e7dbfb",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/containers/policy.json",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "c01eb6950f03419e09d4fc88cb42ff6f",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/containers/registries.d/default.yaml",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "e6e66cd3c24623e0667f26542e0e08f6",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/var/lib/atomic/sigstore/.keep_app-containers_skopeo-0",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "d41d8cd98f00b204e9800998ecf8427e",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// not supported at this time
|
||||
expectedRelationships: nil,
|
||||
},
|
||||
{
|
||||
name: "standard skopeo package with license groups",
|
||||
fixture: "test-fixtures/layout-license-groups",
|
||||
expectedPackages: []pkg.Package{
|
||||
{
|
||||
Name: "app-containers/skopeo",
|
||||
Version: "1.5.1",
|
||||
FoundBy: "portage-cataloger",
|
||||
PURL: "pkg:ebuild/app-containers%2Fskopeo@1.5.1",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS"),
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"),
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE"),
|
||||
),
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicensesFromLocation(
|
||||
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE"),
|
||||
"Apache-2.0 AND BSD AND BSD-2 AND CC-BY-SA-4.0 AND ISC AND MIT")...,
|
||||
),
|
||||
Type: pkg.PortagePkg,
|
||||
Metadata: pkg.PortageEntry{
|
||||
InstalledSize: 27937835,
|
||||
Licenses: "@GROUP1 @MIT-LIKE",
|
||||
Files: []pkg.PortageFileRecord{
|
||||
{
|
||||
Path: "/usr/bin/skopeo",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "376c02bd3b22804df8fdfdc895e7dbfb",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/containers/policy.json",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "c01eb6950f03419e09d4fc88cb42ff6f",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/containers/registries.d/default.yaml",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "e6e66cd3c24623e0667f26542e0e08f6",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/var/lib/atomic/sigstore/.keep_app-containers_skopeo-0",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "d41d8cd98f00b204e9800998ecf8427e",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// not supported at this time
|
||||
expectedRelationships: nil,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: relationships are not under test yet
|
||||
var expectedRelationships []artifact.Relationship
|
||||
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, "test-fixtures/layout").
|
||||
Expects(expectedPkgs, expectedRelationships).
|
||||
TestCataloger(t, NewPortageCataloger())
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, test.fixture).
|
||||
Expects(test.expectedPackages, test.expectedRelationships).
|
||||
TestCataloger(t, NewPortageCataloger())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCataloger_Globs(t *testing.T) {
|
||||
|
||||
248
syft/pkg/cataloger/gentoo/license.go
Normal file
248
syft/pkg/cataloger/gentoo/license.go
Normal file
@ -0,0 +1,248 @@
|
||||
package gentoo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
// the licenses files seems to conform to a custom format that is common to gentoo packages.
|
||||
// see more details:
|
||||
// - https://www.gentoo.org/glep/glep-0023.html#id9
|
||||
// - https://devmanual.gentoo.org/general-concepts/licenses/index.html
|
||||
//
|
||||
// in short, the format is:
|
||||
//
|
||||
// mandatory-license
|
||||
// || ( choosable-licence1 chooseable-license-2 )
|
||||
// useflag? ( optional-component-license )
|
||||
//
|
||||
// "License names may contain [a-zA-Z0-9] (english alphanumeric characters), _ (underscore), - (hyphen), .
|
||||
// (dot) and + (plus sign). They must not begin with a hyphen, a dot or a plus sign."
|
||||
//
|
||||
// this does not conform to SPDX license expressions, which would be a great enhancement in the future.
|
||||
|
||||
// extractLicenses attempts to parse the license field into a valid SPDX license expression
|
||||
func extractLicenses(resolver file.Resolver, closestLocation *file.Location, reader io.Reader) (string, string) {
|
||||
findings := strset.New()
|
||||
contentsWriter := bytes.Buffer{}
|
||||
scanner := bufio.NewScanner(io.TeeReader(reader, &contentsWriter))
|
||||
scanner.Split(bufio.ScanWords)
|
||||
var (
|
||||
mandatoryLicenses, conditionalLicenses, useflagLicenses []string
|
||||
usesGroups bool
|
||||
pipe bool
|
||||
useflag bool
|
||||
)
|
||||
|
||||
for scanner.Scan() {
|
||||
token := scanner.Text()
|
||||
if token == "||" {
|
||||
pipe = true
|
||||
continue
|
||||
}
|
||||
// useflag
|
||||
if strings.Contains(token, "?") {
|
||||
useflag = true
|
||||
continue
|
||||
}
|
||||
if !strings.ContainsAny(token, "()|?") {
|
||||
switch {
|
||||
case useflag:
|
||||
useflagLicenses = append(useflagLicenses, token)
|
||||
case pipe:
|
||||
conditionalLicenses = append(conditionalLicenses, token)
|
||||
default:
|
||||
mandatoryLicenses = append(mandatoryLicenses, token)
|
||||
}
|
||||
if strings.HasPrefix(token, "@") {
|
||||
usesGroups = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var licenseGroups map[string][]string
|
||||
if usesGroups {
|
||||
licenseGroups = readLicenseGroups(resolver, closestLocation)
|
||||
}
|
||||
mandatoryLicenses = replaceLicenseGroups(mandatoryLicenses, licenseGroups)
|
||||
conditionalLicenses = replaceLicenseGroups(conditionalLicenses, licenseGroups)
|
||||
findings.Add(mandatoryLicenses...)
|
||||
findings.Add(conditionalLicenses...)
|
||||
findings.Add(useflagLicenses...)
|
||||
|
||||
var mandatoryStatement, conditionalStatement string
|
||||
|
||||
// attempt to build valid SPDX license expression
|
||||
if len(mandatoryLicenses) > 0 {
|
||||
mandatoryStatement = strings.Join(mandatoryLicenses, " AND ")
|
||||
}
|
||||
if len(conditionalLicenses) > 0 {
|
||||
conditionalStatement = strings.Join(conditionalLicenses, " OR ")
|
||||
}
|
||||
|
||||
contents := strings.TrimSpace(contentsWriter.String())
|
||||
|
||||
if mandatoryStatement != "" && conditionalStatement != "" {
|
||||
return contents, mandatoryStatement + " AND (" + conditionalStatement + ")"
|
||||
}
|
||||
|
||||
if mandatoryStatement != "" {
|
||||
return contents, mandatoryStatement
|
||||
}
|
||||
|
||||
if conditionalStatement != "" {
|
||||
return contents, conditionalStatement
|
||||
}
|
||||
|
||||
return contents, ""
|
||||
}
|
||||
|
||||
func readLicenseGroups(resolver file.Resolver, closestLocation *file.Location) map[string][]string {
|
||||
if resolver == nil || closestLocation == nil {
|
||||
return nil
|
||||
}
|
||||
var licenseGroups map[string][]string
|
||||
groupLocation := resolver.RelativeFileByPath(*closestLocation, "/etc/portage/license_groups")
|
||||
if groupLocation == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupReader, err := resolver.FileContentsByLocation(*groupLocation)
|
||||
defer internal.CloseAndLogError(groupReader, groupLocation.RealPath)
|
||||
if err != nil {
|
||||
log.WithFields("path", groupLocation.RealPath, "error", err).Debug("failed to fetch portage LICENSE")
|
||||
return nil
|
||||
}
|
||||
|
||||
if groupReader == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
licenseGroups, err = parseLicenseGroups(groupReader)
|
||||
if err != nil {
|
||||
log.WithFields("path", groupLocation.RealPath, "error", err).Debug("failed to parse portage LICENSE")
|
||||
}
|
||||
|
||||
return licenseGroups
|
||||
}
|
||||
|
||||
func replaceLicenseGroups(licenses []string, groups map[string][]string) []string {
|
||||
if groups == nil {
|
||||
return licenses
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(licenses))
|
||||
for _, license := range licenses {
|
||||
if strings.HasPrefix(license, "@") {
|
||||
// this is a license group...
|
||||
name := strings.TrimPrefix(license, "@")
|
||||
if expandedLicenses, ok := groups[name]; ok {
|
||||
result = append(result, expandedLicenses...)
|
||||
} else {
|
||||
// unable to expand, use the original license group value (including the '@')
|
||||
result = append(result, license)
|
||||
}
|
||||
} else {
|
||||
// this is a license...
|
||||
result = append(result, license)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseLicenseGroups(reader io.Reader) (map[string][]string, error) {
|
||||
result := make(map[string][]string)
|
||||
rawGroups := make(map[string][]string)
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
// first collect all raw groups
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
// skip empty lines and comments
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("invalid line format: %s", line)
|
||||
}
|
||||
|
||||
groupName := parts[0]
|
||||
licenses := parts[1:]
|
||||
|
||||
rawGroups[groupName] = licenses
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// next process each group to expand nested references
|
||||
for groupName, licenses := range rawGroups {
|
||||
expanded, err := expandLicenses(groupName, licenses, rawGroups, make(map[string]bool))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[groupName] = expanded
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// expandLicenses handles the recursive expansion of license groups, 'visited' is used to detect cycles. We are always
|
||||
// in terms of slices instead of sets to ensure original ordering is preserved.
|
||||
func expandLicenses(currentGroup string, licenses []string, rawGroups map[string][]string, visited map[string]bool) ([]string, error) {
|
||||
if visited[currentGroup] {
|
||||
return nil, fmt.Errorf("cycle detected in license group definitions for group: %s", currentGroup)
|
||||
}
|
||||
|
||||
visited[currentGroup] = true
|
||||
|
||||
result := make([]string, 0)
|
||||
|
||||
for _, item := range licenses {
|
||||
if strings.HasPrefix(item, "@") {
|
||||
// this is a reference to another group
|
||||
refGroupName := item[1:] // remove '@' prefix
|
||||
|
||||
refLicenses, exists := rawGroups[refGroupName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("referenced group not found: %s", refGroupName)
|
||||
}
|
||||
|
||||
newVisited := make(map[string]bool)
|
||||
for k, v := range visited {
|
||||
newVisited[k] = v
|
||||
}
|
||||
|
||||
expanded, err := expandLicenses(refGroupName, refLicenses, rawGroups, newVisited)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, license := range expanded {
|
||||
if !slices.Contains(result, license) {
|
||||
result = append(result, license)
|
||||
}
|
||||
}
|
||||
} else if !slices.Contains(result, item) {
|
||||
// ...this is a regular license
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
183
syft/pkg/cataloger/gentoo/license_test.go
Normal file
183
syft/pkg/cataloger/gentoo/license_test.go
Normal file
@ -0,0 +1,183 @@
|
||||
package gentoo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// you can get a good sense of test fixtures with:
|
||||
// docker run --rm -it gentoo/stage3 bash -c 'find var/db/pkg/ | grep LICENSE | xargs cat'
|
||||
|
||||
func Test_extractLicenses(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
license string
|
||||
wantExpression string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
license: "",
|
||||
wantExpression: "",
|
||||
},
|
||||
{
|
||||
name: "single",
|
||||
license: "GPL-2",
|
||||
wantExpression: "GPL-2",
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
license: "GPL-2 GPL-3 ", // note the extra space
|
||||
wantExpression: "GPL-2 AND GPL-3",
|
||||
},
|
||||
{
|
||||
name: "license choices",
|
||||
license: "|| ( GPL-2 GPL-3 )\n", // note the newline
|
||||
wantExpression: "GPL-2 OR GPL-3",
|
||||
},
|
||||
{
|
||||
// this might not be correct behavior, but we do our best with missing info
|
||||
name: "license choices with missing useflag suffix",
|
||||
license: "GPL-3+ LGPL-3+ || ( GPL-3+ libgcc libstdc++ gcc-runtime-library-exception-3.1 ) FDL-1.3+", // no use flag so what do we do with FDL here?
|
||||
wantExpression: "GPL-3+ AND LGPL-3+ AND (GPL-3+ OR libgcc OR libstdc++ OR gcc-runtime-library-exception-3.1 OR FDL-1.3+)", // "OR FDL-1.3+" is probably wrong at the end...
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
raw, expression := extractLicenses(nil, nil, strings.NewReader(tt.license))
|
||||
assert.Equalf(t, tt.wantExpression, expression, "unexpected expression for %v", tt.license)
|
||||
assert.Equalf(t, strings.TrimSpace(tt.license), raw, "unexpected raw for %v", tt.license)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLicenseGroups(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected map[string][]string
|
||||
expectError require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "basic nesting example",
|
||||
input: "test-fixtures/license-groups/example1",
|
||||
expected: map[string][]string{
|
||||
"FSF-APPROVED": {
|
||||
"Apache-2.0", "BSD", "BSD-2", "GPL-2", "GPL-3", "LGPL-2.1", "LGPL-3", "X11", "ZLIB",
|
||||
"Apache-1.1", "BSD-4", "MPL-1.0", "MPL-1.1", "PSF-2.0",
|
||||
},
|
||||
"GPL-COMPATIBLE": {
|
||||
"Apache-2.0", "BSD", "BSD-2", "GPL-2", "GPL-3", "LGPL-2.1", "LGPL-3", "X11", "ZLIB",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error on cycles",
|
||||
input: "test-fixtures/license-groups/cycle",
|
||||
expectError: require.Error,
|
||||
},
|
||||
{
|
||||
name: "error on self references",
|
||||
input: "test-fixtures/license-groups/self",
|
||||
expectError: require.Error,
|
||||
},
|
||||
{
|
||||
name: "error on missing reference",
|
||||
input: "test-fixtures/license-groups/missing",
|
||||
expectError: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.expectError == nil {
|
||||
tc.expectError = require.NoError
|
||||
}
|
||||
|
||||
contents, err := os.ReadFile(tc.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
actual, err := parseLicenseGroups(bytes.NewReader(contents))
|
||||
tc.expectError(t, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tc.expected, actual); d != "" {
|
||||
t.Errorf("unexpected license groups (-want +got):\n%s", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceLicenseGroups(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
licenses []string
|
||||
groups map[string][]string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil groups",
|
||||
licenses: []string{"MIT", "Apache-2.0", "@GPL"},
|
||||
groups: nil,
|
||||
expected: []string{"MIT", "Apache-2.0", "@GPL"},
|
||||
},
|
||||
{
|
||||
name: "empty groups",
|
||||
licenses: []string{"MIT", "Apache-2.0", "@GPL"},
|
||||
groups: map[string][]string{},
|
||||
expected: []string{"MIT", "Apache-2.0", "@GPL"},
|
||||
},
|
||||
{
|
||||
name: "no group references",
|
||||
licenses: []string{"MIT", "Apache-2.0", "GPL-2.0"},
|
||||
groups: map[string][]string{"GPL": {"GPL-2.0", "GPL-3.0"}},
|
||||
expected: []string{"MIT", "Apache-2.0", "GPL-2.0"},
|
||||
},
|
||||
{
|
||||
name: "single group reference",
|
||||
licenses: []string{"MIT", "@GPL", "Apache-2.0"},
|
||||
groups: map[string][]string{"GPL": {"GPL-2.0", "GPL-3.0"}},
|
||||
expected: []string{"MIT", "GPL-2.0", "GPL-3.0", "Apache-2.0"},
|
||||
},
|
||||
{
|
||||
name: "multiple group references",
|
||||
licenses: []string{"@MIT-LIKE", "@GPL", "BSD-3"},
|
||||
groups: map[string][]string{
|
||||
"MIT-LIKE": {"MIT", "ISC"},
|
||||
"GPL": {"GPL-2.0", "GPL-3.0"},
|
||||
},
|
||||
expected: []string{"MIT", "ISC", "GPL-2.0", "GPL-3.0", "BSD-3"},
|
||||
},
|
||||
{
|
||||
name: "unknown group reference",
|
||||
licenses: []string{"MIT", "@UNKNOWN", "Apache-2.0"},
|
||||
groups: map[string][]string{"GPL": {"GPL-2.0", "GPL-3.0"}},
|
||||
expected: []string{"MIT", "@UNKNOWN", "Apache-2.0"},
|
||||
},
|
||||
{
|
||||
name: "reference at end",
|
||||
licenses: []string{"MIT", "Apache-2.0", "@GPL"},
|
||||
groups: map[string][]string{"GPL": {"GPL-2.0", "GPL-3.0"}},
|
||||
expected: []string{"MIT", "Apache-2.0", "GPL-2.0", "GPL-3.0"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
inputLicenses := make([]string, len(tc.licenses))
|
||||
copy(inputLicenses, tc.licenses)
|
||||
|
||||
actual := replaceLicenseGroups(inputLicenses, tc.groups)
|
||||
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -10,8 +10,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
@ -38,41 +36,41 @@ func parsePortageContents(_ context.Context, resolver file.Resolver, _ *generic.
|
||||
return nil, nil, fmt.Errorf("failed to parse portage name and version")
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
PURL: packageURL(name, version),
|
||||
Locations: file.NewLocationSet(
|
||||
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
),
|
||||
Type: pkg.PortagePkg,
|
||||
Metadata: pkg.PortageEntry{
|
||||
// ensure the default value for a collection is never nil since this may be shown as JSON
|
||||
Files: make([]pkg.PortageFileRecord, 0),
|
||||
},
|
||||
m := pkg.PortageEntry{
|
||||
// ensure the default value for a collection is never nil since this may be shown as JSON
|
||||
Files: make([]pkg.PortageFileRecord, 0),
|
||||
}
|
||||
|
||||
locations := file.NewLocationSet(reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||
|
||||
licenses, licenseLocations := addLicenses(resolver, reader.Location, &m)
|
||||
locations.Add(licenseLocations...)
|
||||
locations.Add(addSize(resolver, reader.Location, &m)...)
|
||||
addFiles(resolver, reader.Location, &m)
|
||||
|
||||
p := pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
PURL: packageURL(name, version),
|
||||
Locations: locations,
|
||||
Licenses: licenses,
|
||||
Type: pkg.PortagePkg,
|
||||
Metadata: m,
|
||||
}
|
||||
addLicenses(resolver, reader.Location, &p)
|
||||
addSize(resolver, reader.Location, &p)
|
||||
addFiles(resolver, reader.Location, &p)
|
||||
|
||||
p.SetID()
|
||||
|
||||
return []pkg.Package{p}, nil, nil
|
||||
}
|
||||
|
||||
func addFiles(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) {
|
||||
func addFiles(resolver file.Resolver, dbLocation file.Location, entry *pkg.PortageEntry) {
|
||||
contentsReader, err := resolver.FileContentsByLocation(dbLocation)
|
||||
if err != nil {
|
||||
log.WithFields("path", dbLocation.RealPath, "package", p.Name, "error", err).Debug("failed to fetch portage contents")
|
||||
log.WithFields("path", dbLocation.RealPath, "error", err).Debug("failed to fetch portage contents")
|
||||
return
|
||||
}
|
||||
defer internal.CloseAndLogError(contentsReader, dbLocation.RealPath)
|
||||
|
||||
entry, ok := p.Metadata.(pkg.PortageEntry)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(contentsReader)
|
||||
for scanner.Scan() {
|
||||
line := strings.Trim(scanner.Text(), "\n")
|
||||
@ -89,60 +87,48 @@ func addFiles(resolver file.Resolver, dbLocation file.Location, p *pkg.Package)
|
||||
entry.Files = append(entry.Files, record)
|
||||
}
|
||||
}
|
||||
|
||||
p.Metadata = entry
|
||||
p.Locations.Add(dbLocation)
|
||||
}
|
||||
|
||||
func addLicenses(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) {
|
||||
func addLicenses(resolver file.Resolver, dbLocation file.Location, entry *pkg.PortageEntry) (pkg.LicenseSet, []file.Location) {
|
||||
parentPath := filepath.Dir(dbLocation.RealPath)
|
||||
|
||||
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "LICENSE"))
|
||||
|
||||
if location == nil {
|
||||
return
|
||||
return pkg.NewLicenseSet(), nil
|
||||
}
|
||||
|
||||
licenseReader, err := resolver.FileContentsByLocation(*location)
|
||||
if err != nil {
|
||||
log.WithFields("path", dbLocation.RealPath, "error", err).Debug("failed to fetch portage LICENSE")
|
||||
return
|
||||
return pkg.NewLicenseSet(), nil
|
||||
}
|
||||
defer internal.CloseAndLogError(licenseReader, location.RealPath)
|
||||
|
||||
findings := strset.New()
|
||||
scanner := bufio.NewScanner(licenseReader)
|
||||
scanner.Split(bufio.ScanWords)
|
||||
for scanner.Scan() {
|
||||
token := scanner.Text()
|
||||
if token != "||" && token != "(" && token != ")" {
|
||||
findings.Add(token)
|
||||
}
|
||||
}
|
||||
og, spdxExpression := extractLicenses(resolver, location, licenseReader)
|
||||
entry.Licenses = og
|
||||
|
||||
licenseCandidates := findings.List()
|
||||
p.Licenses = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(*location, licenseCandidates...)...)
|
||||
p.Locations.Add(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
|
||||
return pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocations(spdxExpression, *location),
|
||||
),
|
||||
[]file.Location{
|
||||
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation),
|
||||
}
|
||||
}
|
||||
|
||||
func addSize(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) {
|
||||
func addSize(resolver file.Resolver, dbLocation file.Location, entry *pkg.PortageEntry) []file.Location {
|
||||
parentPath := filepath.Dir(dbLocation.RealPath)
|
||||
|
||||
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "SIZE"))
|
||||
|
||||
if location == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry, ok := p.Metadata.(pkg.PortageEntry)
|
||||
if !ok {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
sizeReader, err := resolver.FileContentsByLocation(*location)
|
||||
if err != nil {
|
||||
log.WithFields("name", p.Name, "error", err).Debug("failed to fetch portage SIZE")
|
||||
return
|
||||
log.WithFields("path", dbLocation.RealPath, "error", err).Debug("failed to fetch portage SIZE")
|
||||
return nil
|
||||
}
|
||||
defer internal.CloseAndLogError(sizeReader, location.RealPath)
|
||||
|
||||
@ -155,6 +141,5 @@ func addSize(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) {
|
||||
}
|
||||
}
|
||||
|
||||
p.Metadata = entry
|
||||
p.Locations.Add(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
|
||||
return []file.Location{location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation)}
|
||||
}
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
# The FSF-APPROVED group includes the entire GPL-COMPATIBLE group and more.
|
||||
FSF-APPROVED @GPL-COMPATIBLE Apache-1.1 BSD-4 MPL-1.0 MPL-1.1 PSF-2.0
|
||||
# The GPL-COMPATIBLE group includes all licenses compatible with the GNU GPL.
|
||||
GPL-COMPATIBLE Apache-2.0 BSD BSD-2 GPL-2 GPL-3 LGPL-2.1 LGPL-3 X11 ZLIB
|
||||
# for skopeo
|
||||
GROUP1 Apache-2.0 @BSD-COMPATIBLE CC-BY-SA-4.0
|
||||
BSD-COMPATIBLE BSD BSD-2
|
||||
MIT-LIKE ISC MIT
|
||||
@ -0,0 +1,13 @@
|
||||
dir /usr
|
||||
dir /usr/bin
|
||||
obj /usr/bin/skopeo 376c02bd3b22804df8fdfdc895e7dbfb 1649284374
|
||||
dir /etc
|
||||
dir /etc/containers
|
||||
obj /etc/containers/policy.json c01eb6950f03419e09d4fc88cb42ff6f 1649284375
|
||||
dir /etc/containers/registries.d
|
||||
obj /etc/containers/registries.d/default.yaml e6e66cd3c24623e0667f26542e0e08f6 1649284375
|
||||
dir /var
|
||||
dir /var/lib
|
||||
dir /var/lib/atomic
|
||||
dir /var/lib/atomic/sigstore
|
||||
obj /var/lib/atomic/sigstore/.keep_app-containers_skopeo-0 d41d8cd98f00b204e9800998ecf8427e 1649284375
|
||||
@ -0,0 +1 @@
|
||||
@GROUP1 @MIT-LIKE
|
||||
@ -0,0 +1 @@
|
||||
27937835
|
||||
@ -0,0 +1,2 @@
|
||||
FSF-APPROVED @GPL-COMPATIBLE Apache-1.1
|
||||
GPL-COMPATIBLE Apache-2.0 @FSF-APPROVED
|
||||
@ -0,0 +1,5 @@
|
||||
# The FSF-APPROVED group includes the entire GPL-COMPATIBLE group and more.
|
||||
FSF-APPROVED @GPL-COMPATIBLE Apache-1.1 BSD-4 MPL-1.0 MPL-1.1 PSF-2.0
|
||||
|
||||
# The GPL-COMPATIBLE group includes all licenses compatible with the GNU GPL.
|
||||
GPL-COMPATIBLE Apache-2.0 BSD BSD-2 GPL-2 GPL-3 LGPL-2.1 LGPL-3 X11 ZLIB
|
||||
@ -0,0 +1,3 @@
|
||||
FSF-APPROVED @GPL-COMPATIBLE Apache-1.1
|
||||
|
||||
GPL-COMPATIBLE Apache-2.0 @MISSING
|
||||
@ -0,0 +1,2 @@
|
||||
FSF-APPROVED Apache-1.1
|
||||
GPL-COMPATIBLE Apache-2.0 @GPL-COMPATIBLE
|
||||
@ -12,7 +12,8 @@ var _ FileOwner = (*PortageEntry)(nil)
|
||||
|
||||
// PortageEntry represents a single package entry in the portage DB flat-file store.
|
||||
type PortageEntry struct {
|
||||
InstalledSize int `mapstructure:"InstalledSize" json:"installedSize" cyclonedx:"installedSize"`
|
||||
InstalledSize int `json:"installedSize" cyclonedx:"installedSize"`
|
||||
Licenses string `json:"licenses,omitempty"`
|
||||
Files []PortageFileRecord `json:"files"`
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user