mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 07:26:34 +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>
165 lines
4.6 KiB
Go
165 lines
4.6 KiB
Go
package debian
|
|
|
|
import (
|
|
"io"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/scylladb/go-set/strset"
|
|
|
|
"github.com/anchore/syft/internal"
|
|
"github.com/anchore/syft/internal/log"
|
|
)
|
|
|
|
// For more information see: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-syntax
|
|
|
|
var (
|
|
licensePattern = regexp.MustCompile(`^License: (?P<license>\S*)`)
|
|
commonLicensePathPattern = regexp.MustCompile(`/usr/share/common-licenses/(?P<license>[0-9A-Za-z_.\-]+)`)
|
|
licenseAgreementHeadingPattern = regexp.MustCompile(`(?i)^\s*(?P<license>LICENSE AGREEMENT(?: FOR .+?)?)\s*$`)
|
|
formatHeaderPattern = regexp.MustCompile(`^Format:\s*https?://www\.debian\.org/doc/packaging-manuals/copyright-format/`)
|
|
)
|
|
|
|
// heading-detection states. Replaces licenseFirstSentenceAfterHeadingPattern,
|
|
// which only matched at the start of the file: a non-empty heading, a line
|
|
// of dashes, blank lines, then text up to the first period.
|
|
const (
|
|
expectHeading = iota
|
|
expectDashes
|
|
skipBlanks
|
|
captureLicense
|
|
headingDone // matched or impossible — stop checking
|
|
)
|
|
|
|
func parseLicensesFromCopyright(reader io.Reader) []string {
|
|
findings := strset.New()
|
|
scanner := internal.NewLineScanner(reader)
|
|
|
|
// per the DEP-5 spec, machine-readable copyright files MUST have a
|
|
// Format field whose value is a URI for the specification. Only files
|
|
// with this header should be parsed as machine-readable.
|
|
// See: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
formatVerified := false
|
|
headingState := expectHeading
|
|
var licenseText strings.Builder
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if !formatVerified {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
if !formatHeaderPattern.MatchString(line) {
|
|
return nil
|
|
}
|
|
formatVerified = true
|
|
}
|
|
|
|
for _, p := range []*regexp.Regexp{licensePattern, commonLicensePathPattern, licenseAgreementHeadingPattern} {
|
|
if value := findLicenseClause(p, line); value != "" {
|
|
findings.Add(value)
|
|
}
|
|
}
|
|
|
|
var found string
|
|
headingState, found = advanceHeadingState(headingState, line, &licenseText)
|
|
if found != "" {
|
|
findings.Add(found)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
log.WithFields("error", err).Debug("failed to fully read debian copyright file")
|
|
}
|
|
|
|
if !formatVerified {
|
|
return nil
|
|
}
|
|
|
|
results := findings.List()
|
|
sort.Strings(results)
|
|
|
|
return results
|
|
}
|
|
|
|
func advanceHeadingState(state int, line string, licenseText *strings.Builder) (int, string) {
|
|
switch state {
|
|
case expectHeading:
|
|
if strings.TrimSpace(line) != "" {
|
|
return expectDashes, ""
|
|
}
|
|
return headingDone, ""
|
|
case expectDashes:
|
|
trimmed := strings.TrimSpace(line)
|
|
if len(trimmed) > 0 && strings.Trim(trimmed, "-") == "" {
|
|
return skipBlanks, ""
|
|
}
|
|
return headingDone, ""
|
|
case skipBlanks:
|
|
if strings.TrimSpace(line) == "" {
|
|
return state, ""
|
|
}
|
|
licenseText.WriteString(line)
|
|
if value := extractUpToFirstPeriod(licenseText.String()); value != "" {
|
|
return headingDone, value
|
|
}
|
|
return captureLicense, ""
|
|
case captureLicense:
|
|
licenseText.WriteString(" ")
|
|
licenseText.WriteString(line)
|
|
if value := extractUpToFirstPeriod(licenseText.String()); value != "" {
|
|
return headingDone, value
|
|
}
|
|
return state, ""
|
|
case headingDone:
|
|
}
|
|
return state, ""
|
|
}
|
|
|
|
// extractUpToFirstPeriod returns the license text up to the first period,
|
|
// processed through ensureIsSingleLicense, or "" if no period found yet.
|
|
func extractUpToFirstPeriod(s string) string {
|
|
if idx := strings.Index(s, "."); idx >= 0 {
|
|
return ensureIsSingleLicense(s[:idx+1])
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func findLicenseClause(pattern *regexp.Regexp, line string) string {
|
|
valueGroup := "license"
|
|
matchesByGroup := internal.MatchNamedCaptureGroups(pattern, line)
|
|
|
|
candidate, ok := matchesByGroup[valueGroup]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
return ensureIsSingleLicense(candidate)
|
|
}
|
|
|
|
var multiLicenseExceptions = []string{
|
|
"NVIDIA Software License Agreement",
|
|
}
|
|
|
|
func ensureIsSingleLicense(candidate string) (license string) {
|
|
candidate = strings.TrimSpace(strings.ReplaceAll(candidate, "\n", " "))
|
|
|
|
// Check for exceptions first
|
|
for _, exception := range multiLicenseExceptions {
|
|
if strings.Contains(candidate, exception) {
|
|
return strings.TrimSuffix(candidate, ".")
|
|
}
|
|
}
|
|
if strings.Contains(candidate, " or ") || strings.Contains(candidate, " and ") {
|
|
// make sure this is not one of the license exceptions
|
|
// this is a multi-license summary, ignore this as other recurrent license lines should cover this
|
|
return
|
|
}
|
|
if candidate != "" && strings.ToLower(candidate) != "none" {
|
|
// the license may be at the end of a sentence, clean . characters
|
|
license = strings.TrimSuffix(candidate, ".")
|
|
}
|
|
return license
|
|
}
|