fix(dpkg): extract License field for opkg/ipkg entries (#4963)

* fix(dpkg): extract License field for opkg/ipkg entries

opkg and ipkg use the dpkg cataloger but declare the package License
inline in the status DB (unlike Debian dpkg, where licenses live in
copyright files). The cataloger silently dropped the License field at
mapstructure decode time, so all opkg-managed packages reported empty
licenses.

This adds the field to the intermediate decode struct and the public
DpkgDBEntry, and populates licenses in newDpkgPackage using the alpine
cataloger's pattern: try license.ParseExpression first to keep valid
SPDX expressions whole, fall back to whitespace splitting for
space-separated lists.

Standard Debian dpkg status files never carry a License field per
Debian policy, so the new path is a no-op for them; the existing
copyright-file lookup in addLicenses is unaffected.

Closes #4940

Signed-off-by: David Dashti <47575784+Dashtid@users.noreply.github.com>

* remove license from dpkg metadata struct

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* restore format snapshot files

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add additional tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: David Dashti <47575784+Dashtid@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
David Dashti
2026-06-15 16:15:32 -04:00
committed by GitHub
co-authored by Alex Goodman
parent 00ca43d24a
commit cff5a05681
6 changed files with 246 additions and 44 deletions
+28 -7
View File
@@ -13,6 +13,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
)
@@ -23,21 +24,26 @@ const (
docsPath = "/usr/share/doc"
)
func newDpkgPackage(ctx context.Context, d pkg.DpkgDBEntry, dbLocation file.Location, resolver file.Resolver, release *linux.Release, evidence ...file.Location) pkg.Package {
// TODO: separate pr to license refactor, but explore extracting dpkg-specific license parsing into a separate function
var licenses []pkg.License
func newDpkgPackage(ctx context.Context, d dpkgExtractedMetadata, dbLocation file.Location, resolver file.Resolver, release *linux.Release, evidence ...file.Location) pkg.Package {
// the License field is empty for standard Debian dpkg entries (licenses live in copyright files),
// but opkg/ipkg derivatives carry it inline in the status DB — extract it here so those packages
// report licenses without requiring per-package copyright lookups. The license is not persisted on
// the final entry, so convert the raw metadata into the entry just-in-time here.
licenses := extractDeclaredLicenses(ctx, d.License, dbLocation)
entry := d.toDpkgEntry()
locations := file.NewLocationSet(dbLocation)
locations.Add(evidence...)
p := pkg.Package{
Name: d.Package,
Version: d.Version,
Name: entry.Package,
Version: entry.Version,
Licenses: pkg.NewLicenseSet(licenses...),
Locations: locations,
PURL: packageURL(d, release),
PURL: packageURL(entry, release),
Type: pkg.DebPkg,
Metadata: d,
Metadata: entry,
}
if resolver != nil {
@@ -55,6 +61,21 @@ func newDpkgPackage(ctx context.Context, d pkg.DpkgDBEntry, dbLocation file.Loca
return p
}
// extractDeclaredLicenses converts a License field from the status DB into a license set. Returns nil
// for empty input so standard dpkg entries (which never declare License inline) incur no allocation.
// Mirrors the alpine cataloger's approach: keep the value whole if it parses as a valid SPDX expression,
// otherwise split on whitespace to handle space-separated lists.
func extractDeclaredLicenses(ctx context.Context, raw string, dbLocation file.Location) []pkg.License {
if raw == "" {
return nil
}
licenseStrings := []string{raw}
if _, err := license.ParseExpression(raw); err != nil {
licenseStrings = strings.Fields(raw)
}
return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, licenseStrings...)
}
func newDebArchivePackage(ctx context.Context, location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package {
p := pkg.Package{
Name: metadata.Package,