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:
Alex Goodman 2025-05-12 21:03:51 -04:00 committed by GitHub
parent 58392a9717
commit e3e69596bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 3630 additions and 106 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$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", "$ref": "#/$defs/Document",
"$defs": { "$defs": {
"AlpmDbEntry": { "AlpmDbEntry": {
@ -2266,6 +2266,9 @@
"installedSize": { "installedSize": {
"type": "integer" "type": "integer"
}, },
"licenses": {
"type": "string"
},
"files": { "files": {
"items": { "items": {
"$ref": "#/$defs/PortageFileRecord" "$ref": "#/$defs/PortageFileRecord"

View File

@ -10,8 +10,16 @@ import (
) )
func TestPortageCataloger(t *testing.T) { func TestPortageCataloger(t *testing.T) {
expectedLicenseLocation := file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE") tests := []struct {
expectedPkgs := []pkg.Package{ name string
fixture string
expectedPackages []pkg.Package
expectedRelationships []artifact.Relationship
}{
{
name: "standard skopeo package",
fixture: "test-fixtures/layout",
expectedPackages: []pkg.Package{
{ {
Name: "app-containers/skopeo", Name: "app-containers/skopeo",
Version: "1.5.1", Version: "1.5.1",
@ -20,12 +28,17 @@ func TestPortageCataloger(t *testing.T) {
Locations: file.NewLocationSet( 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/CONTENTS"),
file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"), file.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"),
expectedLicenseLocation, 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")...,
), ),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(expectedLicenseLocation, "Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT")...),
Type: pkg.PortagePkg, Type: pkg.PortagePkg,
Metadata: pkg.PortageEntry{ Metadata: pkg.PortageEntry{
InstalledSize: 27937835, InstalledSize: 27937835,
Licenses: "Apache-2.0 BSD BSD-2 CC-BY-SA-4.0 ISC MIT",
Files: []pkg.PortageFileRecord{ Files: []pkg.PortageFileRecord{
{ {
Path: "/usr/bin/skopeo", Path: "/usr/bin/skopeo",
@ -58,16 +71,79 @@ func TestPortageCataloger(t *testing.T) {
}, },
}, },
}, },
},
// 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 for _, test := range tests {
var expectedRelationships []artifact.Relationship t.Run(test.name, func(t *testing.T) {
pkgtest.NewCatalogTester(). pkgtest.NewCatalogTester().
FromDirectory(t, "test-fixtures/layout"). FromDirectory(t, test.fixture).
Expects(expectedPkgs, expectedRelationships). Expects(test.expectedPackages, test.expectedRelationships).
TestCataloger(t, NewPortageCataloger()) TestCataloger(t, NewPortageCataloger())
})
}
} }
func TestCataloger_Globs(t *testing.T) { func TestCataloger_Globs(t *testing.T) {

View 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
}

View 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)
})
}
}

View File

@ -10,8 +10,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
@ -38,41 +36,41 @@ func parsePortageContents(_ context.Context, resolver file.Resolver, _ *generic.
return nil, nil, fmt.Errorf("failed to parse portage name and version") return nil, nil, fmt.Errorf("failed to parse portage name and version")
} }
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{ p := pkg.Package{
Name: name, Name: name,
Version: version, Version: version,
PURL: packageURL(name, version), PURL: packageURL(name, version),
Locations: file.NewLocationSet( Locations: locations,
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), Licenses: licenses,
),
Type: pkg.PortagePkg, Type: pkg.PortagePkg,
Metadata: pkg.PortageEntry{ Metadata: m,
// ensure the default value for a collection is never nil since this may be shown as JSON
Files: make([]pkg.PortageFileRecord, 0),
},
} }
addLicenses(resolver, reader.Location, &p)
addSize(resolver, reader.Location, &p)
addFiles(resolver, reader.Location, &p)
p.SetID() p.SetID()
return []pkg.Package{p}, nil, nil 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) contentsReader, err := resolver.FileContentsByLocation(dbLocation)
if err != nil { 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 return
} }
defer internal.CloseAndLogError(contentsReader, dbLocation.RealPath) defer internal.CloseAndLogError(contentsReader, dbLocation.RealPath)
entry, ok := p.Metadata.(pkg.PortageEntry)
if !ok {
return
}
scanner := bufio.NewScanner(contentsReader) scanner := bufio.NewScanner(contentsReader)
for scanner.Scan() { for scanner.Scan() {
line := strings.Trim(scanner.Text(), "\n") 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) 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) parentPath := filepath.Dir(dbLocation.RealPath)
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "LICENSE")) location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "LICENSE"))
if location == nil { if location == nil {
return return pkg.NewLicenseSet(), nil
} }
licenseReader, err := resolver.FileContentsByLocation(*location) licenseReader, err := resolver.FileContentsByLocation(*location)
if err != nil { if err != nil {
log.WithFields("path", dbLocation.RealPath, "error", err).Debug("failed to fetch portage LICENSE") log.WithFields("path", dbLocation.RealPath, "error", err).Debug("failed to fetch portage LICENSE")
return return pkg.NewLicenseSet(), nil
} }
defer internal.CloseAndLogError(licenseReader, location.RealPath) defer internal.CloseAndLogError(licenseReader, location.RealPath)
findings := strset.New() og, spdxExpression := extractLicenses(resolver, location, licenseReader)
scanner := bufio.NewScanner(licenseReader) entry.Licenses = og
scanner.Split(bufio.ScanWords)
for scanner.Scan() { return pkg.NewLicenseSet(
token := scanner.Text() pkg.NewLicenseFromLocations(spdxExpression, *location),
if token != "||" && token != "(" && token != ")" { ),
findings.Add(token) []file.Location{
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation),
} }
} }
licenseCandidates := findings.List() func addSize(resolver file.Resolver, dbLocation file.Location, entry *pkg.PortageEntry) []file.Location {
p.Licenses = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(*location, licenseCandidates...)...)
p.Locations.Add(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
}
func addSize(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) {
parentPath := filepath.Dir(dbLocation.RealPath) parentPath := filepath.Dir(dbLocation.RealPath)
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "SIZE")) location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "SIZE"))
if location == nil { if location == nil {
return return nil
}
entry, ok := p.Metadata.(pkg.PortageEntry)
if !ok {
return
} }
sizeReader, err := resolver.FileContentsByLocation(*location) sizeReader, err := resolver.FileContentsByLocation(*location)
if err != nil { if err != nil {
log.WithFields("name", p.Name, "error", err).Debug("failed to fetch portage SIZE") log.WithFields("path", dbLocation.RealPath, "error", err).Debug("failed to fetch portage SIZE")
return return nil
} }
defer internal.CloseAndLogError(sizeReader, location.RealPath) defer internal.CloseAndLogError(sizeReader, location.RealPath)
@ -155,6 +141,5 @@ func addSize(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) {
} }
} }
p.Metadata = entry return []file.Location{location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation)}
p.Locations.Add(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
} }

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
FSF-APPROVED @GPL-COMPATIBLE Apache-1.1
GPL-COMPATIBLE Apache-2.0 @FSF-APPROVED

View File

@ -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

View File

@ -0,0 +1,3 @@
FSF-APPROVED @GPL-COMPATIBLE Apache-1.1
GPL-COMPATIBLE Apache-2.0 @MISSING

View File

@ -0,0 +1,2 @@
FSF-APPROVED Apache-1.1
GPL-COMPATIBLE Apache-2.0 @GPL-COMPATIBLE

View File

@ -12,7 +12,8 @@ var _ FileOwner = (*PortageEntry)(nil)
// PortageEntry represents a single package entry in the portage DB flat-file store. // PortageEntry represents a single package entry in the portage DB flat-file store.
type PortageEntry struct { 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"` Files []PortageFileRecord `json:"files"`
} }