8000 feat: add Bitnami matcher by juan131 · Pull Request #2538 · anchore/grype · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: add Bitnami matcher #2538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Sign up for GitHub

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/anchore/syft v1.26.1
github.com/aquasecurity/go-pep440-version v0.0.1
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/charmbracelet/bubbletea v1.3.5
github.com/charmbracelet/lipgloss v1.1.0
Expand Down Expand Up @@ -109,7 +110,6 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions grype/match/matcher_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
GoModuleMatcher MatcherType = "go-module-matcher"
OpenVexMatcher MatcherType = "openvex-matcher"
RustMatcher MatcherType = "rust-matcher"
BitnamiMatcher MatcherType = "bitnami-matcher"
)

var AllMatcherTypes = []MatcherType{
Expand All @@ -32,6 +33,7 @@ var AllMatcherTypes = []MatcherType{
GoModuleMatcher,
OpenVexMatcher,
RustMatcher,
BitnamiMatcher,
}

type MatcherType string
Expand Down
27 changes: 27 additions & 0 deletions grype/matcher/bitnami/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package bitnami

import (
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher/internal"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
syftPkg "github.com/anchore/syft/syft/pkg"
)

type Matcher struct{}

func (m *Matcher) PackageTypes() []syftPkg.Type {
return []syftPkg.Type{syftPkg.BitnamiPkg}
}

func (m *Matcher) Type() match.MatcherType {
return match.BitnamiMatcher
}

func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoredMatch, error) {
// Bitnami packages' metadata are built from the package URL which contains
// info such as the package name, version, revision, distro or architecture.
// ref: https://github.com/anchore/syft/blob/main/syft/pkg/bitnami.go#L3-L13
// ref: https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/bitnami/package.go#L18-L45
return internal.MatchPackageByEcosystemPackageName(store, p, p.Name, m.Type())
}
2 changes: 2 additions & 0 deletions grype/matcher/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package matcher
import (
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher/apk"
"github.com/anchore/grype/grype/matcher/bitnami"
"github.com/anchore/grype/grype/matcher/dotnet"
"github.com/anchore/grype/grype/matcher/dpkg"
"github.com/anchore/grype/grype/matcher/golang"
Expand Down Expand Up @@ -44,5 +45,6 @@ func NewDefaultMatchers(mc Config) []match.Matcher {
&portage.Matcher{},
rust.NewRustMatcher(mc.Rust),
stock.NewStockMatcher(mc.Stock),
&bitnami.Matcher{},
}
}
68 changes: 68 additions & 0 deletions grype/version/bitnami_constraint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package version

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestVersionBitnami(t *testing.T) {
tests := []testCase{
// empty values
{version: "2.3.1", constraint: "", satisfied: true},
// typical cases
{version: "1.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true},
{version: "0.2.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true},
{version: "0.0.1", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false},
{version: "0.6.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false},
{version: "2.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false},
{version: "2.3.1", constraint: "2.3.1", satisfied: true},
{version: "2.3.1", constraint: "= 2.3.1", satisfied: true},
{version: "2.3.1", constraint: " = 2.3.1", satisfied: true},
{version: "2.3.1", constraint: ">= 2.3.1", satisfied: true},
{version: "2.3.1", constraint: "> 2.0.0", satisfied: true},
{version: "2.3.1", constraint: "> 2.0", satisfied: true},
{version: "2.3.1", constraint: "> 2", satisfied: true},
{version: "2.3.1", constraint: "> 2, < 3", satisfied: true},
{version: "2.3.1", constraint: "> 2.3, < 3.1", satisfied: true},
{version: "2.3.1", constraint: "> 2.3.0, < 3.1", satisfied: true},
{version: "2.3.1", constraint: ">= 2.3.1, < 3.1", satisfied: true},
{version: "2.3.1", constraint: " = 2.3.2", satisfied: false},
{version: "2.3.1", constraint: ">= 2.3.2", satisfied: false},
{version: "2.3.1", constraint: "> 2.3.1", satisfied: false},
{version: "2.3.1", constraint: "< 2.0.0", satisfied: false},
{version: "2.3.1", constraint: "< 2.0", satisfied: false},
{version: "2.3.1", constraint: "< 2", satisfied: false},
{version: "2.3.1", constraint: "< 2, > 3", satisfied: false},
{version: "2.3.1-1", constraint: "2.3.1", satisfied: true},
{version: "2.3.1-1", constraint: "= 2.3.1", satisfied: true},
{version: "2.3.1-1", constraint: " = 2.3.1", satisfied: true},
{version: "2.3.1-1", constraint: ">= 2.3.1", satisfied: true},
{version: "2.3.1-1", constraint: "> 2.0.0", satisfied: true},
{version: "2.3.1-1", constraint: "> 2.0", satisfied: true},
{version: "2.3.1-1", constraint: "> 2", satisfied: true},
{version: "2.3.1-1", constraint: "> 2, < 3", satisfied: true},
{version: "2.3.1-1", constraint: "> 2.3, < 3.1", satisfied: true},
{version: "2.3.1-1", constraint: "> 2.3.0, < 3.1", satisfied: true},
{version: "2.3.1-1", constraint: ">= 2.3.1, < 3.1", satisfied: true},
{version: "2.3.1-1", constraint: " = 2.3.2", satisfied: false},
{version: "2.3.1-1", constraint: ">= 2.3.2", satisfied: false},
{version: "2.3.1-1", constraint: "< 2.0.0", satisfied: false},
{version: "2.3.1-1", constraint: "< 2.0", satisfied: false},
{version: "2.3.1-1", constraint: "< 2", satisfied: false},
{version: "2.3.1-1", constraint: "< 2, > 3", satisfied: false},
// Ignoring revisions
{version: "2.3.1-1", constraint: "> 2.3.1", satisfied: false},
{version: "2.3.1-1", constraint: "< 2.3.1-2", satisfied: false},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// We use newSemanticConstraint but using BitnamiFormat as the format
constraint, err := newSemanticConstraint(test.constraint)

assert.NoError(t, err, "unexpected error from newSemanticConstraint: %v", err)
test.assertVersionConstraint(t, BitnamiFormat, constraint)
})
}
}
20 changes: 20 additions & 0 deletions grype/version/bitnami_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package version

import (
"fmt"

bitnami "github.com/bitnami/go-version/pkg/version"
)

func newBitnamiVersion(raw string) (*semanticVersion, error) {
bitnamiVersion, err := bitnami.Parse(raw)
if err != nil {
return nil, err
}

// We can't assume Bitnami revisions can potentially address a
// known vulnerability given Bitnami package revisions use
// exactly the same upstream source code used to create the
// previous version. Then, we discard it.
return newSemanticVersion(fmt.Sprintf("%d.%d.%d", bitnamiVersion.Major(), bitnamiVersion.Minor(), bitnamiVersion.Patch()))
}
129 changes: 129 additions & 0 deletions grype/version/bitnami_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package version

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestBitnamiVersionCompare(t *testing.T) {
tests := []struct {
name string
thisVersion string
otherVersion string
otherFormat Format
expectError bool
errorSubstring string
}{
{
name: "same format successful comparison",
thisVersion: "1.2.3-4",
otherVersion: "1.2.3-5",
otherFormat: BitnamiFormat,
expectError: false,
},
{
name: "semantic versioning successful comparison",
thisVersion: "1.2.3-4",
otherVersion: "1.2.3",
otherFormat: SemanticFormat,
expectError: false,
},
{
name: "different format returns error - deb",
thisVersion: "1.2.3-4",
otherVersion: "1.2.3-1",
otherFormat: DebFormat,
expectError: true,
errorSubstring: "unsupported version format for comparison",
},
{
name: "unknown format attempts upgrade - valid semver format",
thisVersion: "1.2.3-4",
otherVersion: "1.2.3-5",
otherFormat: UnknownFormat,
expectError: false,
},
{
name: "unknown format attempts upgrade - invalid semver format",
thisVersion: "1.2.3-4",
otherVersion: "not-valid-semver-format",
otherFormat: UnknownFormat,
expectError: true,
errorSubstring: "unsupported version format for comparison",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
thisVer, err := newBitnamiVersion(test.thisVersion)
require.NoError(t, err)

otherVer, err := NewVersion(test.otherVersion, test.otherFormat)
require.NoError(t, err)

result, err := thisVer.Compare(otherVer)

if test.expectError {
assert.Error(t, err)
if test.errorSubstring != "" {
assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
"Expected error to contain '%s', got: %v", test.errorSubstring, err)
}
} else {
assert.NoError(t, err)
assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1")
}
})
}
}

func TestBitnamiVersionCompareEdgeCases(t *testing.T) {
tests := []struct {
name string
setupFunc func() (*semanticVersion, *Version)
expectError bool
errorSubstring string
}{
{
name: "nil version object",
setupFunc: func() (*semanticVersion, *Version) {
thisVer, _ := newBitnamiVersion("1.2.3-4")
return thisVer, nil
},
expectError: true,
errorSubstring: "no version provided for comparison",
},
{
name: "empty semanticVersion in other object",
setupFunc: func() (*semanticVersion, *Version) {
thisVer, _ := newBitnamiVersion("1.2.3-4")
otherVer := &Version{
Raw: "1.2.3-5",
Format: SemanticFormat,
rich: rich{},
}

return thisVer, otherVer
},
expectError: true,
errorSubstring: "given empty semanticVersion object",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
thisVer, otherVer := test.setupFunc()

_, err := thisVer.Compare(otherVer)

assert.Error(t, err)
if test.errorSubstring != "" {
assert.True(t, strings.Contains(err.Error(), test.errorSubstring),
"Expected error to contain '%s', got: %v", test.errorSubstring, err)
}
})
}
}
2 changes: 1 addition & 1 deletion grype/version/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func GetConstraint(constStr string, format Format) (Constraint, error) {
switch format {
case ApkFormat:
return newApkConstraint(constStr)
case SemanticFormat, GemFormat:
case SemanticFormat, GemFormat, BitnamiFormat:
return newSemanticConstraint(constStr)
case DebFormat:
return newDebConstraint(constStr)
Expand Down
7 changes: 7 additions & 0 deletions grype/version/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
PortageFormat
GolangFormat
JVMFormat
BitnamiFormat
)

type Format int
Expand All @@ -37,6 +38,7 @@ var formatStr = []string{
"Portage",
"Go",
"JVM",
"Bitnami",
}

var Formats = []Format{
10000 Expand All @@ -51,6 +53,7 @@ var Formats = []Format{
PortageFormat,
GolangFormat,
JVMFormat,
BitnamiFormat,
}

func ParseFormat(userStr string) Format {
Expand All @@ -59,6 +62,8 @@ func ParseFormat(userStr string) Format {
return SemanticFormat
case strings.ToLower(ApkFormat.String()), "apk":
return ApkFormat
case strings.ToLower(BitnamiFormat.String()), "bitnami":
return BitnamiFormat
case strings.ToLower(DebFormat.String()), "dpkg":
return DebFormat
case strings.ToLower(GolangFormat.String()), "go":
Expand All @@ -85,6 +90,8 @@ func FormatFromPkg(p pkg.Package) Format {
switch p.Type {
case syftPkg.ApkPkg:
return ApkFormat
case syftPkg.BitnamiPkg:
return BitnamiFormat
case syftPkg.DebPkg:
return DebFormat
case syftPkg.JavaPkg:
Expand Down
11 changes: 11 additions & 0 deletions grype/version/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ func TestParseFormat(t *testing.T) {
input: "dpkg",
format: DebFormat,
},
{
input: "bitnami",
format: BitnamiFormat,
},
{
input: "maven",
format: MavenFormat,
Expand Down Expand Up @@ -56,6 +60,13 @@ func TestFormatFromPkgType(t *testing.T) {
p pkg.Package
format Format
}{
{
name: "bitnami",
p: pkg.Package{
Type: syftPkg.BitnamiPkg,
},
format: BitnamiFormat,
},
{
name: "deb",
p: pkg.Package{
Expand Down
4 changes: 3 additions & 1 deletion grype/version/semantic_constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ func (c semanticConstraint) supported(format Format) bool {
// portion and makes a semVer object that is compatible with
// these constraints. In practice two formats (semVer, gem version) follow semVer,
// but one of them needs extra cleanup to function (gem).
return format == SemanticFormat || format == GemFormat
// Bitnami is a special case that uses semantic versioning given semVer
// is used on the Bitnami Vulndb but it's not used on the Bitnami packages.
return format == SemanticFormat || format == GemFormat || format == BitnamiFormat
}

func (c semanticConstraint) Satisfied(version *Version) (bool, error) {
Expand Down
9 changes: 9 additions & 0 deletions grype/version/semantic_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ func newSemanticVersion(raw string) (*semanticVersion, error) {
}

func (v *semanticVersion) Compare(other *Version) (int, error) {
if other != nil && other.Format == BitnamiFormat {
transformed, err := newBitnamiVersion(other.Raw)
if err != nil {
return -1, fmt.Errorf("unable to transform bitnami version: %w", err)
}

return transformed.verObj.Compare(v.verObj), nil
}

other, err := finalizeComparisonVersion(other, SemanticFormat)
if err != nil {
return -1, err
Expand Down
Loading
Loading
0