syft/syft/pkg/cataloger/internal/binutils/classifier_test.go
witchcraze 514efb03e0
fix: prevent redis classifier from detecting valkey (#4619)
Signed-off-by: witchcraze <witchcraze@gmail.com>
2026-05-04 14:07:29 -04:00

283 lines
7.4 KiB
Go

package binutils
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
)
func Test_ClassifierCPEs(t *testing.T) {
tests := []struct {
name string
fixture string
classifier Classifier
cpes []string
}{
{
name: "no CPEs",
fixture: "testdata/version.txt",
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{},
},
cpes: nil,
},
{
name: "one Attributes",
fixture: "testdata/version.txt",
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
},
},
cpes: []string{
"cpe:2.3:a:some:app:1.8:*:*:*:*:*:*:*",
},
},
{
name: "multiple CPEs",
fixture: "testdata/version.txt",
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
},
},
cpes: []string{
"cpe:2.3:a:some:app:1.8:*:*:*:*:*:*:*",
"cpe:2.3:a:some:apps:1.8:*:*:*:*:*:*:*",
},
},
{
name: "version in parts",
fixture: "testdata/version-parts.txt",
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version-parts.txt",
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)\x00(?P<major>[0-9.]+)\x00(?P<minor>[0-9.]+)\x00(?P<patch>[0-9.]+)\x00`),
CPEs: []cpe.CPE{},
},
cpes: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resolver := file.NewMockResolverForPaths(test.fixture)
ls, err := resolver.FilesByPath(test.fixture)
require.NoError(t, err)
require.Len(t, ls, 1)
pkgs, err := test.classifier.EvidenceMatcher(test.classifier, MatcherContext{Resolver: resolver, Location: ls[0]})
require.NoError(t, err)
require.Len(t, pkgs, 1)
p := pkgs[0]
var cpes []string
for _, c := range p.CPEs {
cpes = append(cpes, c.Attributes.String())
}
require.Equal(t, test.cpes, cpes)
})
}
}
func TestClassifier_MarshalJSON(t *testing.T) {
tests := []struct {
name string
classifier Classifier
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "go case",
classifier: Classifier{
Class: "class",
FileGlob: "glob",
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", ".thing"),
Package: "pkg",
PURL: packageurl.PackageURL{
Type: "type",
Namespace: "namespace",
Name: "name",
Version: "version",
Qualifiers: nil,
Subpath: "subpath",
},
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource)},
},
want: `{"class":"class","fileGlob":"glob","package":"pkg","purl":"pkg:type/namespace/name@version#subpath","cpes":["cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"]}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
cfg := tt.classifier
got, err := cfg.MarshalJSON()
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, string(got))
})
}
}
func TestFileContentsVersionMatcher(t *testing.T) {
tests := []struct {
name string
pattern string
data string
expected string
}{
{
name: "simple version string regexp",
pattern: `some data (?P<version>[0-9]+\.[0-9]+\.[0-9]+) some data`,
data: "some data 1.2.3 some data",
expected: "1.2.3",
},
{
name: "version parts regexp",
pattern: `\x00\x23(?P<major>[0-9]+)\x00\x23(?P<minor>[0-9]+)\x00\x23(?P<patch>[0-9]+)\x00\x23`,
data: "\x00\x239\x00\x239\x00\x239\x00\x23",
expected: "9.9.9",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockGetContent := func(context MatcherContext) (unionreader.UnionReader, error) {
return unionreader.GetUnionReader(io.NopCloser(bytes.NewBufferString(tt.data)))
}
fn := FileContentsVersionMatcher("cataloger-name", tt.pattern)
p, err := fn(Classifier{}, MatcherContext{
GetReader: mockGetContent,
})
if err != nil {
t.Errorf("Unexpected error %#v", err)
}
if p[0].Version != tt.expected {
t.Errorf("Versions don't match.\ngot\n%q\n\nexpected\n%q", p[0].Version, tt.expected)
}
})
}
}
func Test_SupportingEvidenceMatcher(t *testing.T) {
tests := []struct {
name string
classifier Classifier
expected string
}{
{
name: "simple version string regexp",
classifier: Classifier{
FileGlob: "**/some-binary",
EvidenceMatcher: SupportingEvidenceMatcher("../version.txt",
FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`)),
Package: "some-binary",
},
expected: "1.8",
},
{
name: "not matching version string regexp",
classifier: Classifier{
FileGlob: "**/some-binary",
EvidenceMatcher: SupportingEvidenceMatcher("../version.txt",
FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>abdd)`)),
Package: "some-binary",
},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := directorysource.NewFromPath("testdata")
require.NoError(t, err)
r, err := s.FileResolver(source.AllLayersScope)
require.NoError(t, err)
results, err := r.FilesByGlob(tt.classifier.FileGlob)
require.NoError(t, err)
for _, result := range results {
got, err := tt.classifier.EvidenceMatcher(tt.classifier, MatcherContext{
Resolver: r,
Location: result,
GetReader: func(ctx MatcherContext) (unionreader.UnionReader, error) {
return getReader(ctx)
},
})
require.NoError(t, err)
if tt.expected != "" {
require.NotEmpty(t, got)
require.Equal(t, tt.expected, got[0].Version)
} else {
require.Empty(t, got)
}
}
})
}
}
func TestMatchNone(t *testing.T) {
matchingMatcher := MatchPath("**")
notMatchingMatcher := MatchPath("will-not-match")
tests := []struct {
name string
matcher EvidenceMatcher
expected bool // true if MatchNone should succeed (inner failed)
}{
{
name: "inner matches, MatchNone fails",
matcher: MatchNone(matchingMatcher),
expected: false,
},
{
name: "inner fails, MatchNone succeeds",
matcher: MatchNone(notMatchingMatcher),
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pkgs, err := tt.matcher(Classifier{}, MatcherContext{
Location: file.NewLocation("some/path"),
})
require.NoError(t, err)
if tt.expected {
assert.NotNil(t, pkgs)
assert.Empty(t, pkgs)
} else {
assert.Nil(t, pkgs)
}
})
}
}