feat: report the perl interpreter as a cpan package

The `perl-binary` classifier now emits `pkg:cpan/perl@<version>` instead of `pkg:generic/perl@<version>`.

`perl` is itself a CPAN distribution and carries advisories under that name, so typing it as `generic` left every one of them unreachable.

This changes an existing purl. Anything keyed on `pkg:generic/perl` (allowlists, policy, SBOM diffs) needs updating.

Builds on the cpan cataloging branch, which adds the package type this uses.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2026-08-04 11:01:06 -04:00
parent 1827ce2f4f
commit 29d4816575
No known key found for this signature in database
7 changed files with 119 additions and 11 deletions

View File

@ -502,6 +502,19 @@ func convertToJSONSchemaTypesFromMetadata(metadataTypes []string) []string {
return result return result
} }
// packageTypeConstName renders a package type using its go constant name, which is what the detector
// "type" field reports (e.g. "BinaryPkg").
// a switch rather than a derivation, since pkg.Type values don't reliably map back to their constant
// names (e.g. "java-archive" is JavaPkg); add an arm when a classifier declares a new type.
func packageTypeConstName(t pkg.Type) string {
switch t {
case pkg.CpanPkg:
return "CpanPkg"
default:
return "BinaryPkg"
}
}
// EnrichWithBinaryClassifier enriches an entry with binary classifier detectors if it's the binary-classifier-cataloger // EnrichWithBinaryClassifier enriches an entry with binary classifier detectors if it's the binary-classifier-cataloger
func (e *EnrichmentData) EnrichWithBinaryClassifier(catalogerName string, entry *capabilities.CatalogerEntry) { func (e *EnrichmentData) EnrichWithBinaryClassifier(catalogerName string, entry *capabilities.CatalogerEntry) {
// special handling for binary-classifier-cataloger: auto-generate one detector per classifier // special handling for binary-classifier-cataloger: auto-generate one detector per classifier
@ -523,7 +536,7 @@ func (e *EnrichmentData) EnrichWithBinaryClassifier(catalogerName string, entry
Name: classifier.Package, Name: classifier.Package,
PURL: purlStr, PURL: purlStr,
CPEs: cpeStrings, CPEs: cpeStrings,
Type: "BinaryPkg", Type: packageTypeConstName(classifier.PackageType()),
}, },
} }

View File

@ -155,10 +155,10 @@ catalogers:
packages: packages:
- class: perl-binary - class: perl-binary
name: perl name: perl
purl: pkg:generic/perl purl: pkg:cpan/perl
cpes: cpes:
- cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:* - cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:*
type: BinaryPkg type: CpanPkg
- method: glob - method: glob
criteria: criteria:
- '**/composer*' - '**/composer*'

View File

@ -436,8 +436,8 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
expected: pkg.Package{ expected: pkg.Package{
Name: "perl", Name: "perl",
Version: "5.12.5", Version: "5.12.5",
Type: "binary", Type: "cpan",
PURL: "pkg:generic/perl@5.12.5", PURL: "pkg:cpan/perl@5.12.5",
Locations: locations("perl"), Locations: locations("perl"),
Metadata: metadata("perl-binary"), Metadata: metadata("perl-binary"),
}, },
@ -449,8 +449,8 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
expected: pkg.Package{ expected: pkg.Package{
Name: "perl", Name: "perl",
Version: "5.20.0", Version: "5.20.0",
Type: "binary", Type: "cpan",
PURL: "pkg:generic/perl@5.20.0", PURL: "pkg:cpan/perl@5.20.0",
Locations: locations("perl"), Locations: locations("perl"),
Metadata: metadata("perl-binary"), Metadata: metadata("perl-binary"),
}, },
@ -462,8 +462,8 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
expected: pkg.Package{ expected: pkg.Package{
Name: "perl", Name: "perl",
Version: "5.37.8", Version: "5.37.8",
Type: "binary", Type: "cpan",
PURL: "pkg:generic/perl@5.37.8", PURL: "pkg:cpan/perl@5.37.8",
Locations: locations("perl"), Locations: locations("perl"),
Metadata: metadata("perl-binary"), Metadata: metadata("perl-binary"),
}, },
@ -3097,6 +3097,58 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
} }
} }
// Test_Cataloger_PackageTypes covers what assertPackagesAreEqual deliberately ignores: the package type and CPEs.
// The perl interpreter is reported as cpan so it lines up with the rest of the CPAN ecosystem, everything else
// keeps the default binary type.
func Test_Cataloger_PackageTypes(t *testing.T) {
tests := []struct {
logicalFixture string
wantType pkg.Type
wantPURL string
wantCPEs []string
}{
{
logicalFixture: "perl/5.20.0/linux-amd64",
wantType: pkg.CpanPkg,
wantPURL: "pkg:cpan/perl@5.20.0",
wantCPEs: []string{"cpe:2.3:a:perl:perl:5.20.0:*:*:*:*:*:*:*"},
},
{
logicalFixture: "haproxy/1.5.14/linux-amd64",
wantType: pkg.BinaryPkg,
wantPURL: "pkg:generic/haproxy@1.5.14",
wantCPEs: []string{"cpe:2.3:a:haproxy:haproxy:1.5.14:*:*:*:*:*:*:*"},
},
}
for _, test := range tests {
t.Run(test.logicalFixture, func(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries)
src, err := directorysource.NewFromPath(path)
require.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
packages, _, err := c.Catalog(context.Background(), resolver)
require.NoError(t, err)
require.Len(t, packages, 1)
var cpes []string
for _, c := range packages[0].CPEs {
cpes = append(cpes, c.Attributes.BindToFmtString())
}
assert.Equal(t, test.wantType, packages[0].Type)
assert.Equal(t, test.wantPURL, packages[0].PURL)
assert.Equal(t, test.wantCPEs, cpes)
})
}
}
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) { func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig()) c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())

View File

@ -5,6 +5,7 @@ import (
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/binutils" "github.com/anchore/syft/syft/pkg/cataloger/internal/binutils"
) )
@ -248,8 +249,9 @@ func DefaultClassifiers() []binutils.Classifier {
EvidenceMatcher: m.FileContentsVersionMatcher( EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\/usr\/local\/lib\/perl\d\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`), `(?m)\/usr\/local\/lib\/perl\d\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "perl", Package: "perl",
PURL: mustPURL("pkg:generic/perl@version"), PURL: mustPURL("pkg:cpan/perl@version"),
CPEs: singleCPE("cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), CPEs: singleCPE("cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
Type: pkg.CpanPkg,
}, },
{ {
Class: "php-composer-binary", Class: "php-composer-binary",

View File

@ -48,6 +48,17 @@ type Classifier struct {
// CPEs are the specific CPEs we want to include for this binary with updated version information // CPEs are the specific CPEs we want to include for this binary with updated version information
CPEs []cpe.CPE `json:"cpes"` CPEs []cpe.CPE `json:"cpes"`
// Type optionally overrides the package type reported for matches; when unset pkg.BinaryPkg is used
Type pkg.Type `json:"-"`
}
// PackageType is the package type reported for matches of this classifier, defaulting to pkg.BinaryPkg.
func (cfg Classifier) PackageType() pkg.Type {
if cfg.Type == "" {
return pkg.BinaryPkg
}
return cfg.Type
} }
func (cfg Classifier) MarshalJSON() ([]byte, error) { func (cfg Classifier) MarshalJSON() ([]byte, error) {

View File

@ -35,7 +35,7 @@ func NewClassifierPackage(classifier Classifier, location file.Location, matchMe
Locations: file.NewLocationSet( Locations: file.NewLocationSet(
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
), ),
Type: pkg.BinaryPkg, Type: classifier.PackageType(),
CPEs: cpes, CPEs: cpes,
FoundBy: catalogerName, FoundBy: catalogerName,
Metadata: pkg.BinarySignature{ Metadata: pkg.BinarySignature{

View File

@ -12,6 +12,7 @@ import (
"github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource" "github.com/anchore/syft/syft/source/directorysource"
) )
@ -102,6 +103,35 @@ func Test_ClassifierCPEs(t *testing.T) {
} }
} }
func Test_ClassifierPackageType(t *testing.T) {
tests := []struct {
name string
classifier Classifier
want pkg.Type
}{
{
name: "defaults to binary when unset",
classifier: Classifier{Class: "some-binary", Package: "some-app"},
want: pkg.BinaryPkg,
},
{
name: "honors an explicit type",
classifier: Classifier{Class: "perl-binary", Package: "perl", Type: pkg.CpanPkg},
want: pkg.CpanPkg,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.want, test.classifier.PackageType())
p := NewClassifierPackage(test.classifier, file.NewLocation("/usr/bin/app"), map[string]string{"version": "1.2.3"}, "cataloger-name")
require.NotNil(t, p)
assert.Equal(t, test.want, p.Type)
})
}
}
func TestClassifier_MarshalJSON(t *testing.T) { func TestClassifier_MarshalJSON(t *testing.T) {
tests := []struct { tests := []struct {