mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +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 (
|
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"
|
||||||
)
|
)
|
||||||
|
|||||||
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",
|
"$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"
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
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"
|
"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() {
|
|
||||||
token := scanner.Text()
|
|
||||||
if token != "||" && token != "(" && token != ")" {
|
|
||||||
findings.Add(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
licenseCandidates := findings.List()
|
return pkg.NewLicenseSet(
|
||||||
p.Licenses = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(*location, licenseCandidates...)...)
|
pkg.NewLicenseFromLocations(spdxExpression, *location),
|
||||||
p.Locations.Add(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
|
),
|
||||||
|
[]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)
|
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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
// 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user