Fix mix.lock git/path deps mislabeled as hex.pm with bogus PURL (#5041)

parseMixLock read the version from a hex-only token position and hardcoded a pkg:hex PURL, so git/path deps got a URL-fragment version and a false hex.pm PURL (false CVE matches). Branch on the source atom: git uses the commit SHA, path an empty version, and pkg:hex PURLs are emitted only for hex deps.

Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
This commit is contained in:
Synvoya 2026-07-07 01:13:20 +10:00 committed by GitHub
parent f1457f2554
commit aebdf4bff4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 74 additions and 4 deletions

View File

@ -6,13 +6,18 @@ import (
"github.com/anchore/syft/syft/pkg" "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{ p := pkg.Package{
Name: d.Name, Name: d.Name,
Version: d.Version, Version: d.Version,
Language: pkg.Elixir, Language: pkg.Elixir,
Locations: file.NewLocationSet(locations...), Locations: file.NewLocationSet(locations...),
PURL: packageURL(d), PURL: packageURL(source, d),
Type: pkg.HexPkg, Type: pkg.HexPkg,
Metadata: d, Metadata: d,
} }
@ -22,7 +27,14 @@ func newPackage(d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Packag
return p 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 var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL( return packageurl.NewPackageURL(

View File

@ -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) errs = unknown.Appendf(errs, reader, "unable to read mix lock line %d: %s", lineNum, line)
continue 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", "<sha-or-ref>", [ref: "..."]}`
// tokens: ["", name, "git", "<url-scheme>", "//host/dep.git", "<sha-or-ref>", ...]
// 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 == "" { if name == "" {
log.WithFields("path", reader.RealPath).Debug("skipping empty package name from mix.lock file") 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, packages = append(packages,
newPackage( newPackage(
source,
pkg.ElixirMixLockEntry{ pkg.ElixirMixLockEntry{
Name: name, Name: name,
Version: version, Version: version,

View File

@ -230,6 +230,34 @@ func TestParseMixLock(t *testing.T) {
Dependencies: []string{"decimal"}, 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" fixture := "testdata/mix.lock"

View File

@ -14,4 +14,6 @@
"gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"}, "gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"},
"hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"}, "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"}, "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", []},
} }