Files
syft/syft/pkg/cataloger/snap/parse_kernel_changelog.go
T
Alex Goodman 1dc16fbac4 fix: report scanner errors and raise the line cap in metadata parsers (#5353)
* 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>
2026-09-29 16:52:58 -04:00

147 lines
4.7 KiB
Go

package snap
import (
"compress/gzip"
"context"
"fmt"
"regexp"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// kernelVersionInfo holds parsed kernel version information
type kernelVersionInfo struct {
baseVersion string // e.g., "5.4.0-195"
releaseVersion string // e.g., "215"
fullVersion string // e.g., "5.4.0-195.215"
majorVersion string // e.g., "5.4"
}
// parseKernelChangelog parses changelog files from kernel snaps to extract kernel version.
// The changelog is gzip-compressed and may be very large, so we stream it line-by-line
// rather than reading it entirely into memory.
func parseKernelChangelog(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
gzReader, err := gzip.NewReader(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to create gzip reader for changelog: %w", err)
}
defer gzReader.Close()
scanner := internal.NewLineScanner(gzReader)
// read the first line to extract kernel version
// Format: "linux (5.4.0-195.215) focal; urgency=medium"
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return nil, nil, fmt.Errorf("failed to read changelog content: %w", err)
}
return nil, nil, fmt.Errorf("changelog file is empty")
}
versionInfo, err := extractKernelVersion(scanner.Text())
if err != nil {
return nil, nil, err
}
snapMetadata := pkg.SnapEntry{
SnapType: pkg.SnapTypeKernel,
}
packages := createMainKernelPackage(versionInfo, snapMetadata, reader.Location)
// stream remaining lines looking for the base kernel entry
baseKernelEntry := fmt.Sprintf("%s/linux:", strings.ReplaceAll(versionInfo.releaseVersion, ";", "/"))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, baseKernelEntry) {
if basePackage := parseBaseKernelLine(line, versionInfo.majorVersion, snapMetadata, reader.Location); basePackage != nil {
packages = append(packages, *basePackage)
}
break
}
}
if err := scanner.Err(); err != nil {
return packages, nil, fmt.Errorf("failed to read changelog content: %w", err)
}
return packages, nil, nil
}
// extractKernelVersion parses version information from the first changelog line
func extractKernelVersion(firstLine string) (*kernelVersionInfo, error) {
// Format: "linux (5.4.0-195.215) focal; urgency=medium"
kernelVersionRegex := regexp.MustCompile(`linux \(([0-9]+\.[0-9]+\.[0-9]+-[0-9]+)\.([0-9]+)\)`)
matches := kernelVersionRegex.FindStringSubmatch(firstLine)
if len(matches) < 3 {
return nil, fmt.Errorf("could not parse kernel version from changelog: %s", firstLine)
}
info := &kernelVersionInfo{
baseVersion: matches[1], // e.g., "5.4.0-195"
releaseVersion: matches[2], // eg., "215"
}
// eg "5.4.0-195.215"
info.fullVersion = fmt.Sprintf("%s.%s", info.baseVersion, info.releaseVersion)
// Extract major version; package naming
majorVersionRegex := regexp.MustCompile(`([0-9]+\.[0-9]+)\.[0-9]+-[0-9]+`)
majorMatches := majorVersionRegex.FindStringSubmatch(info.baseVersion)
if len(majorMatches) >= 2 {
info.majorVersion = majorMatches[1]
} else {
info.majorVersion = info.baseVersion
}
return info, nil
}
// createMainKernelPackage creates the main kernel package
func createMainKernelPackage(versionInfo *kernelVersionInfo, snapMetadata pkg.SnapEntry, location file.Location) []pkg.Package {
kernelPackageName := fmt.Sprintf("linux-image-%s-generic", versionInfo.baseVersion)
kernelPkg := newDebianPackageFromSnap(
kernelPackageName,
versionInfo.fullVersion,
snapMetadata,
location,
)
return []pkg.Package{kernelPkg}
}
// parseBaseKernelLine extracts base kernel version from a changelog line
func parseBaseKernelLine(line string, majorVersion string, snapMetadata pkg.SnapEntry, location file.Location) *pkg.Package {
baseKernelRegex := regexp.MustCompile(fmt.Sprintf(`(%s-[0-9]+)\.?[0-9]*`, regexp.QuoteMeta(majorVersion)))
baseMatches := baseKernelRegex.FindStringSubmatch(line)
if len(baseMatches) < 2 {
return nil
}
baseKernelVersion := baseMatches[1]
baseKernelFullRegex := regexp.MustCompile(fmt.Sprintf(`(%s-[0-9]+\.[0-9]+)`, regexp.QuoteMeta(majorVersion)))
baseFullMatches := baseKernelFullRegex.FindStringSubmatch(line)
var baseFullVersion string
if len(baseFullMatches) >= 2 {
baseFullVersion = baseFullMatches[1]
} else {
baseFullVersion = baseKernelVersion
}
baseKernelPkg := newDebianPackageFromSnap(
fmt.Sprintf("linux-image-%s-generic", baseKernelVersion),
baseFullVersion,
snapMetadata,
location,
)
return &baseKernelPkg
}