1
0
mirror of https://github.com/anchore/grype.git synced 2026-02-11 09:42:33 +02:00

feat: add hex matcher for Erlang/Elixir ecosystem (#3194)

* feat: add hex matcher for Erlang/Elixir ecosystem

Add a separate matcher for Hex packages so that the GHSA data for the
Erlang ecosystem can be used by Grype. Add a config to allow continued
us of CPEs for this ecosystem, but turn off CPE matching by default
because they cause a lot of false positives.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

* ci: integ test for mix.lock

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>

---------

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
This commit is contained in:
Will Murphy
2026-01-29 11:02:50 -05:00
committed by GitHub
parent a6b79a7e1b
commit e8af5fe91a
10 changed files with 100 additions and 1 deletions

View File

@@ -21,6 +21,7 @@ import (
"github.com/anchore/grype/grype/matcher/dotnet"
"github.com/anchore/grype/grype/matcher/dpkg"
"github.com/anchore/grype/grype/matcher/golang"
"github.com/anchore/grype/grype/matcher/hex"
"github.com/anchore/grype/grype/matcher/java"
"github.com/anchore/grype/grype/matcher/javascript"
"github.com/anchore/grype/grype/matcher/python"
@@ -371,6 +372,7 @@ func getMatcherConfig(opts *options.Grype) matcher.Config {
AlwaysUseCPEForStdlib: opts.Match.Golang.AlwaysUseCPEForStdlib,
AllowMainModulePseudoVersionComparison: opts.Match.Golang.AllowMainModulePseudoVersionComparison,
},
Hex: hex.MatcherConfig(opts.Match.Hex),
Stock: stock.MatcherConfig(opts.Match.Stock),
Dpkg: dpkg.MatcherConfig{
MissingEpochStrategy: opts.Match.Dpkg.MissingEpochStrategy,

View File

@@ -16,6 +16,7 @@ import (
"github.com/anchore/grype/grype/matcher/dotnet"
"github.com/anchore/grype/grype/matcher/dpkg"
"github.com/anchore/grype/grype/matcher/golang"
"github.com/anchore/grype/grype/matcher/hex"
"github.com/anchore/grype/grype/matcher/java"
"github.com/anchore/grype/grype/matcher/javascript"
"github.com/anchore/grype/grype/matcher/python"
@@ -114,6 +115,7 @@ func Test_getMatcherConfig(t *testing.T) {
AlwaysUseCPEForStdlib: true,
AllowMainModulePseudoVersionComparison: false,
},
Hex: hex.MatcherConfig{},
Stock: stock.MatcherConfig{UseCPEs: true},
Rpm: rpm.MatcherConfig{
MissingEpochStrategy: "auto",
@@ -148,6 +150,7 @@ func Test_getMatcherConfig(t *testing.T) {
AlwaysUseCPEForStdlib: true,
AllowMainModulePseudoVersionComparison: false,
},
Hex: hex.MatcherConfig{},
Stock: stock.MatcherConfig{UseCPEs: true},
Rpm: rpm.MatcherConfig{
MissingEpochStrategy: "zero",
@@ -182,6 +185,7 @@ func Test_getMatcherConfig(t *testing.T) {
AlwaysUseCPEForStdlib: true,
AllowMainModulePseudoVersionComparison: false,
},
Hex: hex.MatcherConfig{},
Stock: stock.MatcherConfig{UseCPEs: true},
Rpm: rpm.MatcherConfig{
MissingEpochStrategy: "auto",

View File

@@ -17,6 +17,7 @@ type matchConfig struct {
Python matcherConfig `yaml:"python" json:"python" mapstructure:"python"` // settings for the python matcher
Ruby matcherConfig `yaml:"ruby" json:"ruby" mapstructure:"ruby"` // settings for the ruby matcher
Rust matcherConfig `yaml:"rust" json:"rust" mapstructure:"rust"` // settings for the rust matcher
Hex matcherConfig `yaml:"hex" json:"hex" mapstructure:"hex"` // settings for the hex matcher (Elixir/Erlang)
Stock matcherConfig `yaml:"stock" json:"stock" mapstructure:"stock"` // settings for the default/stock matcher
Dpkg dpkgConfig `yaml:"dpkg" json:"dpkg" mapstructure:"dpkg"` // settings for the dpkg matcher
Rpm rpmConfig `yaml:"rpm" json:"rpm" mapstructure:"rpm"` // settings for the rpm matcher
@@ -125,6 +126,7 @@ func defaultMatchConfig() matchConfig {
Python: dontUseCpe,
Ruby: dontUseCpe,
Rust: dontUseCpe,
Hex: dontUseCpe,
Stock: useCpe,
Dpkg: defaultDpkgConfig(),
Rpm: defaultRpmConfig(),
@@ -170,6 +172,7 @@ func (cfg *matchConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&cfg.Python.UseCPEs, usingCpeDescription)
descriptions.Add(&cfg.Ruby.UseCPEs, usingCpeDescription)
descriptions.Add(&cfg.Rust.UseCPEs, usingCpeDescription)
descriptions.Add(&cfg.Hex.UseCPEs, usingCpeDescription)
descriptions.Add(&cfg.Stock.UseCPEs, usingCpeDescription)
descriptions.Add(&cfg.Dpkg.MissingEpochStrategy,
`strategy for handling missing epochs in dpkg package versions during matching (options: zero, auto)`)

View File

@@ -79,7 +79,8 @@ func KnownPackageSpecifierOverrides() []PackageSpecifierOverride {
{Ecosystem: pkg.Dart.String(), ReplacementEcosystem: ptr(string(pkg.DartPubPkg))},
{Ecosystem: pkg.Dotnet.String(), ReplacementEcosystem: ptr(string(pkg.DotnetPkg))},
{Ecosystem: pkg.Elixir.String(), ReplacementEcosystem: ptr(string(pkg.HexPkg))},
{Ecosystem: pkg.Erlang.String(), ReplacementEcosystem: ptr(string(pkg.ErlangOTPPkg))},
{Ecosystem: pkg.Erlang.String(), ReplacementEcosystem: ptr(string(pkg.HexPkg))}, // Erlang packages use hex.pm, same as Elixir
{Ecosystem: string(pkg.ErlangOTPPkg), ReplacementEcosystem: ptr(string(pkg.HexPkg))}, // remap erlang-otp to hex for GHSA matching
{Ecosystem: pkg.Go.String(), ReplacementEcosystem: ptr(string(pkg.GoModulePkg))},
{Ecosystem: pkg.Haskell.String(), ReplacementEcosystem: ptr(string(pkg.HackagePkg))},
{Ecosystem: pkg.Java.String(), ReplacementEcosystem: ptr(string(pkg.JavaPkg))},

View File

@@ -19,6 +19,7 @@ const (
RustMatcher MatcherType = "rust-matcher"
BitnamiMatcher MatcherType = "bitnami-matcher"
PacmanMatcher MatcherType = "pacman-matcher"
HexMatcher MatcherType = "hex-matcher"
)
var AllMatcherTypes = []MatcherType{
@@ -38,6 +39,7 @@ var AllMatcherTypes = []MatcherType{
RustMatcher,
BitnamiMatcher,
PacmanMatcher,
HexMatcher,
}
type MatcherType string

View File

@@ -0,0 +1,35 @@
package hex
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 {
cfg MatcherConfig
}
type MatcherConfig struct {
UseCPEs bool
}
func NewHexMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
cfg: cfg,
}
}
func (m *Matcher) PackageTypes() []syftPkg.Type {
return []syftPkg.Type{syftPkg.HexPkg, syftPkg.ErlangOTPPkg}
}
func (m *Matcher) Type() match.MatcherType {
return match.HexMatcher
}
func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {
return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/anchore/grype/grype/matcher/dotnet"
"github.com/anchore/grype/grype/matcher/dpkg"
"github.com/anchore/grype/grype/matcher/golang"
"github.com/anchore/grype/grype/matcher/hex"
"github.com/anchore/grype/grype/matcher/java"
"github.com/anchore/grype/grype/matcher/javascript"
"github.com/anchore/grype/grype/matcher/msrc"
@@ -28,6 +29,7 @@ type Config struct {
Javascript javascript.MatcherConfig
Golang golang.MatcherConfig
Rust rust.MatcherConfig
Hex hex.MatcherConfig
Stock stock.MatcherConfig
Dpkg dpkg.MatcherConfig
Rpm rpm.MatcherConfig
@@ -47,6 +49,7 @@ func NewDefaultMatchers(mc Config) []match.Matcher {
&msrc.Matcher{},
&portage.Matcher{},
rust.NewRustMatcher(mc.Rust),
hex.NewHexMatcher(mc.Hex),
stock.NewStockMatcher(mc.Stock),
&bitnami.Matcher{},
&pacman.Matcher{},

View File

@@ -172,6 +172,14 @@ func newMockDbProvider() vulnerability.Provider {
PackageName: "shellcheck",
Constraint: version.MustGetConstraint("< 0.9.0", version.UnknownFormat), // was: "haskell"
},
{
Reference: vulnerability.Reference{
ID: "CVE-hex-plug",
Namespace: "github:language:elixir",
},
PackageName: "plug",
Constraint: version.MustGetConstraint("< 1.12.0", version.UnknownFormat),
},
{
Reference: vulnerability.Reference{
ID: "CVE-rust-sample-1",

View File

@@ -613,6 +613,43 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
})
}
func addHexMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/hex/mix.lock")
if len(packages) < 1 {
t.Logf("Hex Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (elixir-mix-lock)")
}
thePkg := pkg.New(packages[0])
vulns, err := provider.FindVulnerabilities(byNamespace("github:language:elixir"), search.ByPackageName(thePkg.Name))
require.NoError(t, err)
require.NotEmpty(t, vulns)
vulnObj := vulns[0]
theResult.Add(match.Match{
Vulnerability: vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: match.EcosystemParameters{
Language: "elixir",
Namespace: "github:language:elixir",
Package: match.PackageParameter{
Name: thePkg.Name,
Version: thePkg.Version,
},
},
Found: match.EcosystemResult{
VersionConstraint: "< 1.12.0 (unknown)",
VulnerabilityID: "CVE-hex-plug",
},
Matcher: match.HexMatcher,
},
},
})
}
func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {
packages := catalog.PackagesByPath("/opt/java/openjdk/release")
if len(packages) < 1 {
@@ -723,6 +760,7 @@ func TestMatchByImage(t *testing.T) {
addDotnetMatches(t, theSource, catalog, provider, &expectedMatches)
addGolangMatches(t, theSource, catalog, provider, &expectedMatches)
addHaskellMatches(t, theSource, catalog, provider, &expectedMatches)
addHexMatches(t, theSource, catalog, provider, &expectedMatches)
return expectedMatches
},
},

View File

@@ -0,0 +1,3 @@
%{
"plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
}