mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 07:56:36 +01:00
* fix: report scanner errors instead of returning a partial parse as complete A line longer than `bufio.Scanner`'s 64KB buffer ends the scan with `ErrTooLong`, which only `Err()` reports. Parsers now return what they found along with the error (recorded as an unknown for the file), or log it where there is no error to return. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: allow lines up to 1MB in line-oriented package metadata parsers Adds `internal.NewLineScanner`, a `bufio.Scanner` capped at 1MB per line rather than the 64KB default, the same cap the alpm parser already uses. Long single-line fields (descriptions, license text, dependency lists) now parse instead of ending the scan early. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: use io.ReadFull where a short read was silently accepted - dotnet bundle 7-bit int decoding could read a zero-byte result as a zero byte - wordpress plugin header parsing included the unread tail of its buffer Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: finish moving line scanners to the 1MB cap and checking scanner errors - java manifest, pom.properties, Gemfile.lock, requirements.txt, homebrew formula, alpm and the purl/cpe decoders now use `internal.NewLineScanner` - snap kernel changelog reports a scan error instead of silently dropping the base kernel package - the attestation output reader keeps draining the pipe after a scan error so cosign can't block on a full pipe Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: stop logging the full setup.py line once per dependency match The debug log ran once per regex match and included the whole line, so log volume grew with the square of line length. With 1MB lines that is hundreds of GB of debug output from a single crafted file. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(gentoo): don't dereference a nil location when a LICENSE scan fails `extractLicenses` accepts a nil location, but the scan error log added earlier on this branch dereferenced it. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(gentoo): bound license group expansion on hostile license_groups - dedup within a group uses a set instead of `slices.Contains`, which was quadratic in tokens per line and got much worse under the 1MB line cap - each group is expanded once and memoized, so a chain of groups that reference each other repeatedly no longer expands exponentially - repeated references to the same group within one line are skipped Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
256 lines
7.4 KiB
Go
256 lines
7.4 KiB
Go
package gentoo
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"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 := internal.NewLineScanner(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
|
|
}
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
fields := []any{"error", err}
|
|
if closestLocation != nil {
|
|
fields = append(fields, "path", closestLocation.RealPath)
|
|
}
|
|
log.WithFields(fields...).Debug("failed to fully read portage LICENSE")
|
|
}
|
|
|
|
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...)
|
|
|
|
return strings.TrimSpace(contentsWriter.String()), licenseExpression(mandatoryLicenses, conditionalLicenses)
|
|
}
|
|
|
|
// licenseExpression attempts to build a valid SPDX license expression
|
|
func licenseExpression(mandatoryLicenses, conditionalLicenses []string) string {
|
|
mandatoryStatement := strings.Join(mandatoryLicenses, " AND ")
|
|
conditionalStatement := strings.Join(conditionalLicenses, " OR ")
|
|
|
|
switch {
|
|
case mandatoryStatement != "" && conditionalStatement != "":
|
|
return mandatoryStatement + " AND (" + conditionalStatement + ")"
|
|
case mandatoryStatement != "":
|
|
return mandatoryStatement
|
|
default:
|
|
return conditionalStatement
|
|
}
|
|
}
|
|
|
|
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 after, ok := strings.CutPrefix(license, "@"); ok {
|
|
// this is a license group...
|
|
name := after
|
|
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) {
|
|
rawGroups := make(map[string][]string)
|
|
|
|
scanner := internal.NewLineScanner(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
|
|
expanded := make(map[string][]string)
|
|
for groupName := range rawGroups {
|
|
if _, err := expandLicenses(groupName, rawGroups, expanded, make(map[string]bool)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return expanded, nil
|
|
}
|
|
|
|
// expandLicenses handles the recursive expansion of license groups. Each group is expanded once and memoized in
|
|
// 'expanded' (the file is untrusted, so re-expanding on every reference would be exponential), and 'visiting' holds
|
|
// the groups on the current path to detect cycles. We are always in terms of slices instead of sets to ensure
|
|
// original ordering is preserved.
|
|
func expandLicenses(currentGroup string, rawGroups, expanded map[string][]string, visiting map[string]bool) ([]string, error) {
|
|
if result, ok := expanded[currentGroup]; ok {
|
|
return result, nil
|
|
}
|
|
if visiting[currentGroup] {
|
|
return nil, fmt.Errorf("cycle detected in license group definitions for group: %s", currentGroup)
|
|
}
|
|
visiting[currentGroup] = true
|
|
defer delete(visiting, currentGroup)
|
|
|
|
result := make([]string, 0)
|
|
// sets keep dedup linear, since a single group line can hold hundreds of thousands of tokens
|
|
seen := strset.New()
|
|
seenRefs := strset.New()
|
|
|
|
for _, item := range rawGroups[currentGroup] {
|
|
refGroupName, isRef := strings.CutPrefix(item, "@")
|
|
if !isRef {
|
|
// this is a regular license
|
|
if !seen.Has(item) {
|
|
seen.Add(item)
|
|
result = append(result, item)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// this is a reference to another group
|
|
if seenRefs.Has(refGroupName) {
|
|
continue
|
|
}
|
|
seenRefs.Add(refGroupName)
|
|
|
|
if _, exists := rawGroups[refGroupName]; !exists {
|
|
return nil, fmt.Errorf("referenced group not found: %s", refGroupName)
|
|
}
|
|
|
|
refLicenses, err := expandLicenses(refGroupName, rawGroups, expanded, visiting)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, license := range refLicenses {
|
|
if !seen.Has(license) {
|
|
seen.Add(license)
|
|
result = append(result, license)
|
|
}
|
|
}
|
|
}
|
|
|
|
expanded[currentGroup] = result
|
|
return result, nil
|
|
}
|