diff --git a/syft/pkg/cataloger/elixir/package.go b/syft/pkg/cataloger/elixir/package.go index 50cd3f28f..54fb21193 100644 --- a/syft/pkg/cataloger/elixir/package.go +++ b/syft/pkg/cataloger/elixir/package.go @@ -6,13 +6,18 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func newPackage(d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Package { +// newPackage builds a package from a mix.lock entry. source is the entry's +// source atom (`hex`, `git`, or `path`). Only hex entries are backed by the +// hex.pm registry, so only they receive a pkg:hex/ PURL; git/path entries get +// an empty PURL to avoid being asserted as hex.pm packages (which would produce +// false hex.pm vulnerability matches). +func newPackage(source string, d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Package { p := pkg.Package{ Name: d.Name, Version: d.Version, Language: pkg.Elixir, Locations: file.NewLocationSet(locations...), - PURL: packageURL(d), + PURL: packageURL(source, d), Type: pkg.HexPkg, Metadata: d, } @@ -22,7 +27,14 @@ func newPackage(d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Packag return p } -func packageURL(m pkg.ElixirMixLockEntry) string { +func packageURL(source string, m pkg.ElixirMixLockEntry) string { + // Non-hex sources (git, path) are not published to the hex.pm registry, so a + // pkg:hex/ PURL would be incorrect and would drive false hex.pm vulnerability + // matches. Emit no PURL for them. + if source != "hex" { + return "" + } + var qualifiers packageurl.Qualifiers return packageurl.NewPackageURL( diff --git a/syft/pkg/cataloger/elixir/parse_mix_lock.go b/syft/pkg/cataloger/elixir/parse_mix_lock.go index b444cde2a..e44aaa8c0 100644 --- a/syft/pkg/cataloger/elixir/parse_mix_lock.go +++ b/syft/pkg/cataloger/elixir/parse_mix_lock.go @@ -53,7 +53,34 @@ func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, re errs = unknown.Appendf(errs, reader, "unable to read mix lock line %d: %s", lineNum, line) continue } - name, version, hash, hashExt := tokens[1], tokens[4], tokens[5], tokens[len(tokens)-2] + + // tokens[2] is the source atom of the entry's tuple: `hex`, `git`, or + // `path`. The layout of the remaining tokens differs per source, so the + // version/hash positions must be read accordingly. Only hex entries are + // backed by the hex.pm registry; git/path entries must not be emitted as + // hex packages (see newPackage), otherwise a bogus pkg:hex/ PURL produces + // false hex.pm vulnerability matches. + source := tokens[2] + + var name, version, hash, hashExt string + switch source { + case "git": + // e.g. `"dep": {:git, "https://host/dep.git", "", [ref: "..."]}` + // tokens: ["", name, "git", "", "//host/dep.git", "", ...] + // The version is the commit SHA/ref immediately after the URL; there + // is no hex checksum for a git-sourced dependency. + name, version = tokens[1], tokens[5] + case "path": + // e.g. `"dep": {:path, "../local", []}` + // tokens: ["", name, "path", "../local", "[]", ""] + // A path dependency has no version or checksum; tokens[4] is the empty + // dependency list `[]`, not a version. + name = tokens[1] + default: + // hex (and any registry-style tuple): keep the original behavior. + // tokens: ["", name, "hex", name, version, hash, ..., hashExt, ""] + name, version, hash, hashExt = tokens[1], tokens[4], tokens[5], tokens[len(tokens)-2] + } if name == "" { log.WithFields("path", reader.RealPath).Debug("skipping empty package name from mix.lock file") @@ -63,6 +90,7 @@ func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, re packages = append(packages, newPackage( + source, pkg.ElixirMixLockEntry{ Name: name, Version: version, diff --git a/syft/pkg/cataloger/elixir/parse_mix_lock_test.go b/syft/pkg/cataloger/elixir/parse_mix_lock_test.go index 0e1e36e3c..c19e908d0 100644 --- a/syft/pkg/cataloger/elixir/parse_mix_lock_test.go +++ b/syft/pkg/cataloger/elixir/parse_mix_lock_test.go @@ -230,6 +230,34 @@ func TestParseMixLock(t *testing.T) { Dependencies: []string{"decimal"}, }, }, + { + // git-sourced dependency: version is the commit SHA (not a URL + // fragment) and no pkg:hex/ PURL is emitted, so it cannot be + // matched as a hex.pm package. + Name: "enacl", + Version: "2f50ba6289f2f2d9fef05d22a396c0bab4f64149", + Language: pkg.Elixir, + Type: pkg.HexPkg, + Locations: locations, + PURL: "", + Metadata: pkg.ElixirMixLockEntry{ + Name: "enacl", + Version: "2f50ba6289f2f2d9fef05d22a396c0bab4f64149", + }, + }, + { + // path-sourced dependency: no version (tokens[4] is the empty + // dependency list `[]`, not a version) and no pkg:hex/ PURL. + Name: "local_dep", + Version: "", + Language: pkg.Elixir, + Type: pkg.HexPkg, + Locations: locations, + PURL: "", + Metadata: pkg.ElixirMixLockEntry{ + Name: "local_dep", + }, + }, } fixture := "testdata/mix.lock" diff --git a/syft/pkg/cataloger/elixir/testdata/mix.lock b/syft/pkg/cataloger/elixir/testdata/mix.lock index 8d9806c1b..8b67d4e02 100644 --- a/syft/pkg/cataloger/elixir/testdata/mix.lock +++ b/syft/pkg/cataloger/elixir/testdata/mix.lock @@ -14,4 +14,6 @@ "gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"}, "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "enacl": {:git, "https://github.com/aeternity/enacl.git", "2f50ba6289f2f2d9fef05d22a396c0bab4f64149", [ref: "2f50ba6"]}, + "local_dep": {:path, "../local_dep", []}, }