mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
fix(binary): preserve semver build metadata in ffmpeg version (#5452)
Signed-off-by: Adam Shaver <ashaver@nvidia.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/internal/manager/testutil"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/binutils"
|
||||
@@ -3192,6 +3194,134 @@ 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.
|
||||
// Test_Cataloger_ffmpeg_versionSuffix verifies the ffmpeg classifiers no longer
|
||||
// unconditionally truncate the embedded "FFmpeg version <ver>" string to X.Y[.Z].
|
||||
// Semver build metadata (a backported-patch stamp such as
|
||||
// "9.0.3+dev.9.0.2.p1.eb9cef3") must be preserved in the reported version, because
|
||||
// it compares equal to its core version and so does not disturb vulnerability
|
||||
// matching. A "-" pre-release suffix (a distro revision such as "6.1.1-3ubuntu5")
|
||||
// must stay truncated: it sorts below the release in semver and would corrupt
|
||||
// version-range matching. Suffix-free binaries match exactly as before, and
|
||||
// trailing non-version text (or NUL padding) is never absorbed into the version.
|
||||
func Test_Cataloger_ffmpeg_versionSuffix(t *testing.T) {
|
||||
// collect the ffmpeg classifiers under test, keyed by their FileGlob
|
||||
classifiers := map[string]binutils.Classifier{}
|
||||
for _, c := range DefaultClassifiers() {
|
||||
if c.Package == "ffmpeg" {
|
||||
classifiers[c.FileGlob] = c
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, classifiers, "expected to find ffmpeg classifiers")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fileGlob string // which classifier to run
|
||||
data string // simulated binary contents
|
||||
want string // expected reported version ("" = no match expected)
|
||||
}{
|
||||
{
|
||||
name: "library bare version unchanged",
|
||||
fileGlob: "**/libav*",
|
||||
data: "FFmpeg version 7.1.1",
|
||||
want: "7.1.1",
|
||||
},
|
||||
{
|
||||
name: "library two-component version unchanged",
|
||||
fileGlob: "**/libav*",
|
||||
data: "FFmpeg version 6.0",
|
||||
want: "6.0",
|
||||
},
|
||||
{
|
||||
name: "library build metadata with patch provenance preserved",
|
||||
fileGlob: "**/libav*",
|
||||
data: "FFmpeg version 9.0.3+dev.9.0.2.p1.eb9cef3",
|
||||
want: "9.0.3+dev.9.0.2.p1.eb9cef3",
|
||||
},
|
||||
{
|
||||
name: "library build metadata short sha preserved",
|
||||
fileGlob: "**/libav*",
|
||||
data: "FFmpeg version 9.0.2+eb9cef3",
|
||||
want: "9.0.2+eb9cef3",
|
||||
},
|
||||
{
|
||||
// a "-" pre-release suffix is intentionally NOT captured: it sorts below
|
||||
// the release in semver and would corrupt version-range vulnerability
|
||||
// matching, so distro revisions stay truncated exactly as before
|
||||
name: "library distro hyphen revision truncated",
|
||||
fileGlob: "**/libav*",
|
||||
data: "FFmpeg version 6.1.1-3ubuntu5",
|
||||
want: "6.1.1",
|
||||
},
|
||||
{
|
||||
name: "library distro dotted hyphen revision truncated",
|
||||
fileGlob: "**/libav*",
|
||||
data: "FFmpeg version 4.4.2-0ubuntu0.22.04.1",
|
||||
want: "4.4.2",
|
||||
},
|
||||
{
|
||||
name: "library trailing text not captured",
|
||||
fileGlob: "**/libav*",
|
||||
data: "FFmpeg version 6.1.1 Copyright (c) 2000-2023 the FFmpeg developers",
|
||||
want: "6.1.1",
|
||||
},
|
||||
{
|
||||
name: "library NUL-padded version not over-captured",
|
||||
fileGlob: "**/libav*",
|
||||
data: "FFmpeg version 9.0.3\x00libavformat.so",
|
||||
want: "9.0.3",
|
||||
},
|
||||
{
|
||||
name: "libswresample build metadata preserved",
|
||||
fileGlob: "**/libswresample*",
|
||||
data: "FFmpeg version 9.0.3+dev.9.0.2.p1.eb9cef3",
|
||||
want: "9.0.3+dev.9.0.2.p1.eb9cef3",
|
||||
},
|
||||
{
|
||||
// the compiled-in printf format string in the ffmpeg binary is the
|
||||
// literal "%s version <ver>" (the program name is substituted at runtime)
|
||||
name: "binary build metadata preserved",
|
||||
fileGlob: "**/ffmpeg",
|
||||
data: "%s version 9.0.3+dev.9.0.2.p1.eb9cef3",
|
||||
want: "9.0.3+dev.9.0.2.p1.eb9cef3",
|
||||
},
|
||||
{
|
||||
name: "binary bare version unchanged",
|
||||
fileGlob: "**/ffmpeg",
|
||||
data: "%s version 7.1.1",
|
||||
want: "7.1.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cls, ok := classifiers[tt.fileGlob]
|
||||
require.Truef(t, ok, "no ffmpeg classifier for glob %q", tt.fileGlob)
|
||||
|
||||
ctx := binutils.MatcherContext{
|
||||
GetReader: func(binutils.MatcherContext) (unionreader.UnionReader, error) {
|
||||
return unionreader.GetUnionReader(io.NopCloser(bytes.NewBufferString(tt.data)))
|
||||
},
|
||||
}
|
||||
|
||||
pkgs, err := cls.EvidenceMatcher(cls, ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.want == "" {
|
||||
require.Empty(t, pkgs, "expected no package match")
|
||||
return
|
||||
}
|
||||
require.Len(t, pkgs, 1, "expected exactly one package")
|
||||
assert.Equal(t, tt.want, pkgs[0].Version)
|
||||
// the fuller version must also flow into the PURL so the SBOM is not lossy;
|
||||
// compare the decoded PURL version, since packageurl-go percent-encodes
|
||||
// reserved characters (e.g. '+' -> %2B) in the serialized form
|
||||
parsed, err := packageurl.FromString(pkgs[0].PURL)
|
||||
require.NoError(t, err, "PURL should parse")
|
||||
assert.Equal(t, tt.want, parsed.Version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Cataloger_PackageTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
logicalFixture string
|
||||
|
||||
@@ -972,7 +972,10 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
EvidenceMatcher: m.FileContentsVersionMatcher(
|
||||
// Pattern found in the binary: "%s version 7.1.1" or "%s version 6.0"
|
||||
// When executed outputs: "ffmpeg version 7.1.1" or "ffmpeg version 6.0"
|
||||
`(?m)%s version (?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?)`,
|
||||
// The optional "+" suffix preserves semver build metadata (e.g. a
|
||||
// backported-patch stamp like "9.0.3+dev.9.0.2.p1.eb9cef3"); see the
|
||||
// ffmpeg-library classifier for why "-" pre-release suffixes stay truncated.
|
||||
`(?m)%s version (?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?)`,
|
||||
),
|
||||
Package: "ffmpeg",
|
||||
PURL: mustPURL("pkg:generic/ffmpeg@version"),
|
||||
@@ -982,8 +985,15 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
Class: "ffmpeg-library",
|
||||
FileGlob: "**/libav*",
|
||||
EvidenceMatcher: binutils.MatchAny(
|
||||
// Primary pattern: FFmpeg version found in most libraries
|
||||
m.FileContentsVersionMatcher(`(?m)FFmpeg version (?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?)`),
|
||||
// Primary pattern: FFmpeg version found in most libraries.
|
||||
// The optional "+" suffix preserves semver build metadata (e.g. a
|
||||
// backported-patch stamp like "9.0.3+dev.9.0.2.p1.eb9cef3") instead of
|
||||
// truncating to X.Y[.Z]. Build metadata compares equal to its core
|
||||
// version, so this does not change vulnerability matching. A "-"
|
||||
// pre-release suffix (e.g. a distro revision "6.1.1-3ubuntu5") is
|
||||
// deliberately NOT captured: it sorts below the release in semver and
|
||||
// would corrupt version-range matching, so it stays truncated as before.
|
||||
m.FileContentsVersionMatcher(`(?m)FFmpeg version (?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?)`),
|
||||
// Fallback: library-specific version patterns for libavcodec and libavformat
|
||||
m.FileContentsVersionMatcher(`(?m)Lavc(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
m.FileContentsVersionMatcher(`(?m)Lavf(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
@@ -996,8 +1006,9 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
Class: "ffmpeg-library",
|
||||
FileGlob: "**/libswresample*",
|
||||
EvidenceMatcher: m.FileContentsVersionMatcher(
|
||||
// FFmpeg version pattern for libswresample
|
||||
`(?m)FFmpeg version (?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?)`),
|
||||
// FFmpeg version pattern for libswresample. Same optional "+" build
|
||||
// metadata handling as the ffmpeg-library classifier above.
|
||||
`(?m)FFmpeg version (?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?)`),
|
||||
Package: "ffmpeg",
|
||||
PURL: mustPURL("pkg:generic/ffmpeg@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:ffmpeg:ffmpeg:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
|
||||
|
||||
Reference in New Issue
Block a user