diff --git a/grype/matcher/common/distro_matchers.go b/grype/matcher/common/distro_matchers.go index cb76097579d..94ae8693935 100644 --- a/grype/matcher/common/distro_matchers.go +++ b/grype/matcher/common/distro_matchers.go @@ -1,8 +1,11 @@ package common import ( + "errors" "fmt" + "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" @@ -32,7 +35,12 @@ func FindMatchesByPackageDistro(store vulnerability.ProviderByDistro, d *distro. // if the constraint it met, then the given package has the vulnerability isPackageVulnerable, err := vuln.Constraint.Satisfied(verObj) if err != nil { - return nil, fmt.Errorf("distro matcher failed to check constraint='%s' version='%s': %w", vuln.Constraint, verObj, err) + var e *version.NonFatalConstraintError + if errors.As(err, &e) { + log.Warn(e) + } else { + return nil, fmt.Errorf("distro matcher failed to check constraint='%s' version='%s': %w", vuln.Constraint, verObj, err) + } } if !isPackageVulnerable { diff --git a/grype/version/constraint.go b/grype/version/constraint.go index b3d54a966e4..7a64d0d4c4f 100644 --- a/grype/version/constraint.go +++ b/grype/version/constraint.go @@ -40,3 +40,17 @@ func MustGetConstraint(constStr string, format Format) Constraint { } return constraint } + +// NonFatalConstraintError should be used any time an unexpected but recoverable condition is encountered while +// checking version constraint satisfaction. The error should get returned by any implementer of the Constraint +// interface. If returned by the Satisfied method on the Constraint interface, this error will be caught and +// logged as a warning in the FindMatchesByPackageDistro function in grype/matcher/common/distro_matchers.go +type NonFatalConstraintError struct { + constraint Constraint + version *Version + message string +} + +func (e NonFatalConstraintError) Error() string { + return fmt.Sprintf("Matching raw constraint %s against version %s caused a non-fatal error: %s", e.constraint, e.version, e.message) +} diff --git a/grype/version/kb_contraint.go b/grype/version/kb_contraint.go index 7bb227a7066..dc8d2e92365 100644 --- a/grype/version/kb_contraint.go +++ b/grype/version/kb_contraint.go @@ -37,14 +37,16 @@ func (c kbConstraint) supported(format Format) bool { } func (c kbConstraint) Satisfied(version *Version) (bool, error) { - if c.raw == "" && version != nil { - // an empty constraint is always satisfied - return true, nil - } else if version == nil { - if c.raw != "" { - // a non-empty constraint with no version given should always fail - return false, nil + if c.raw == "" { + // an empty constraint is never satisfied + return false, &NonFatalConstraintError{ + constraint: c, + version: version, + message: "Unexpected data in DB: Empty raw version constraint.", } + } + + if version == nil { return true, nil } @@ -57,7 +59,7 @@ func (c kbConstraint) Satisfied(version *Version) (bool, error) { func (c kbConstraint) String() string { if c.raw == "" { - return "none (kb)" + return fmt.Sprintf("%q (kb)", c.raw) } return fmt.Sprintf("%s (kb)", c.raw) } diff --git a/grype/version/kb_contraint_test.go b/grype/version/kb_contraint_test.go index 6d2ba8ab455..d6b5348e89f 100644 --- a/grype/version/kb_contraint_test.go +++ b/grype/version/kb_contraint_test.go @@ -1,28 +1,54 @@ package version import ( - "errors" + "github.com/stretchr/testify/assert" "testing" ) func TestVersionKbConstraint(t *testing.T) { - tests := []testCase{ - {version: "", constraint: "", satisfied: true}, - {version: "", constraint: "foo", satisfied: false}, - {version: "1", constraint: "foo", satisfied: false}, - {version: "1", constraint: "1", satisfied: true}, - {version: "878787", constraint: "979797 || 101010 || 878787", satisfied: true}, - {version: "478787", constraint: "979797 || 101010 || 878787", satisfied: false}, + tests := []struct { + name string + version string + constraint string + satisfied bool + shouldErr bool + errorAssertion func(t *testing.T, err error) + }{ + {name: "no constraint no version raises error", version: "", constraint: "", satisfied: false, shouldErr: true, errorAssertion: func(t *testing.T, err error) { + var expectedError *NonFatalConstraintError + assert.ErrorAs(t, err, &expectedError, "Unexpected error type from kbConstraint.Satisfied: %v", err) + }}, + {name: "no constraint with version raises error", version: "878787", constraint: "", satisfied: false, shouldErr: true, errorAssertion: func(t *testing.T, err error) { + var expectedError *NonFatalConstraintError + assert.ErrorAs(t, err, &expectedError, "Unexpected error type from kbConstraint.Satisfied: %v", err) + }}, + {name: "no version is unsatisifed", version: "", constraint: "foo", satisfied: false}, + {name: "version constraint mismatch", version: "1", constraint: "foo", satisfied: false}, + {name: "matching version and constraint", version: "1", constraint: "1", satisfied: true}, + {name: "base keyword matching version and constraint", version: "base", constraint: "base", satisfied: true}, + {name: "version and OR constraint match", version: "878787", constraint: "979797 || 101010 || 878787", satisfied: true}, + {name: "version and OR constraint mismatch", version: "478787", constraint: "979797 || 101010 || 878787", satisfied: false}, } for _, test := range tests { - t.Run(test.name(), func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { constraint, err := newKBConstraint(test.constraint) - if !errors.Is(err, test.constErr) { - t.Fatalf("unexpected constraint error: '%+v'!='%+v'", err, test.constErr) - } + assert.NoError(t, err, "unexpected error from newKBConstraint: %v", err) + + version, err := NewVersion(test.version, KBFormat) + assert.NoError(t, err, "unexpected error from NewVersion: %v", err) - test.assert(t, KBFormat, constraint) + isSatisfied, err := constraint.Satisfied(version) + if test.shouldErr { + if test.errorAssertion != nil { + test.errorAssertion(t, err) + } else { + assert.Error(t, err) + } + } else { + assert.NoError(t, err, "unexpected error from kbConstraint.Satisfied: %v", err) + } + assert.Equal(t, test.satisfied, isSatisfied, "unexpected constraint check result") }) } }