mirror of
https://github.com/anchore/syft.git
synced 2026-07-04 18:18:26 +02:00
feat(elixir): emit dependency relationships from mix.lock (#4985)
adds dependency-of relationships between elixir locked packages, matching how other ecosystem catalogers (alpine, arch, debian, redhat, python) express the dependency graph via the shared dependency.Processor/Specifier mechanism. Signed-off-by: Chris Greeno <cgreeno@gmail.com>
This commit is contained in:
parent
1143c12a97
commit
37fee88b5c
@ -3,7 +3,7 @@ package internal
|
||||
const (
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "16.1.5"
|
||||
JSONSchemaVersion = "16.1.6"
|
||||
|
||||
// Changelog
|
||||
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
||||
@ -12,5 +12,6 @@ const (
|
||||
// 16.1.3 - add GGUFFileParts to GGUFFileHeader metadata
|
||||
// 16.1.4 - add BunLockEntry metadata type for bun.lock support
|
||||
// 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support
|
||||
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
|
||||
|
||||
)
|
||||
|
||||
4358
schema/json/schema-16.1.6.json
Normal file
4358
schema/json/schema-16.1.6.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.1.5/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.1.6/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@ -1267,6 +1267,13 @@
|
||||
"pkgHashExt": {
|
||||
"type": "string",
|
||||
"description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)"
|
||||
},
|
||||
"dependencies": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@ -6,10 +6,12 @@ package elixir
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
|
||||
)
|
||||
|
||||
// NewMixLockCataloger returns a cataloger object for Elixir mix.lock files.
|
||||
func NewMixLockCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("elixir-mix-lock-cataloger").
|
||||
WithParserByGlobs(parseMixLock, "**/mix.lock")
|
||||
WithParserByGlobs(parseMixLock, "**/mix.lock").
|
||||
WithProcessors(dependency.Processor(mixLockDependencySpecifier))
|
||||
}
|
||||
|
||||
@ -30,3 +30,15 @@ func TestCataloger_Globs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCataloger_Relationships(t *testing.T) {
|
||||
expectedRelationships := []string{
|
||||
"cowlib @ 2.11.0 (mix.lock) [dependency-of] cowboy @ 2.9.0 (mix.lock)",
|
||||
"ranch @ 1.8.0 (mix.lock) [dependency-of] cowboy @ 2.9.0 (mix.lock)",
|
||||
}
|
||||
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, "testdata/relationships").
|
||||
ExpectsRelationshipStrings(expectedRelationships).
|
||||
TestCataloger(t, NewMixLockCataloger())
|
||||
}
|
||||
|
||||
28
syft/pkg/cataloger/elixir/dependency.go
Normal file
28
syft/pkg/cataloger/elixir/dependency.go
Normal file
@ -0,0 +1,28 @@
|
||||
package elixir
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
|
||||
)
|
||||
|
||||
var _ dependency.Specifier = mixLockDependencySpecifier
|
||||
|
||||
// mixLockDependencySpecifier declares that a mix.lock entry provides its own
|
||||
// package name and requires the names listed in its dependency list, so the
|
||||
// dependency processor can resolve dependency-of relationships between the
|
||||
// locked packages.
|
||||
func mixLockDependencySpecifier(p pkg.Package) dependency.Specification {
|
||||
meta, ok := p.Metadata.(pkg.ElixirMixLockEntry)
|
||||
if !ok {
|
||||
log.Tracef("cataloger failed to extract mix.lock metadata for package %+v", p.Name)
|
||||
return dependency.Specification{}
|
||||
}
|
||||
|
||||
return dependency.Specification{
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{p.Name},
|
||||
Requires: meta.Dependencies,
|
||||
},
|
||||
}
|
||||
}
|
||||
103
syft/pkg/cataloger/elixir/dependency_test.go
Normal file
103
syft/pkg/cataloger/elixir/dependency_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
package elixir
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
|
||||
)
|
||||
|
||||
func Test_extractMixLockDependencies(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "no dependencies",
|
||||
line: ` "castore": {:hex, :castore, "0.1.17", "hash", [:mix], [], "hexpm", "ext"},`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single dependency",
|
||||
line: ` "esbuild": {:hex, :esbuild, "0.5.0", "hash", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ext"},`,
|
||||
want: []string{"castore"},
|
||||
},
|
||||
{
|
||||
name: "multiple dependencies",
|
||||
line: ` "cowboy": {:hex, :cowboy, "2.9.0", "hash", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib]}, {:ranch, "1.8.0", [hex: :ranch]}], "hexpm", "ext"},`,
|
||||
want: []string{"cowlib", "ranch"},
|
||||
},
|
||||
{
|
||||
name: "git source is skipped like hex source",
|
||||
line: ` "mydep": {:git, "https://github.com/example/mydep.git", "ref", [{:jason, "~> 1.0", [hex: :jason]}]},`,
|
||||
want: []string{"jason"},
|
||||
},
|
||||
{
|
||||
name: "not a package line",
|
||||
line: `%{`,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, extractMixLockDependencies(tt.line))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mixLockDependencySpecifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p pkg.Package
|
||||
want dependency.Specification
|
||||
}{
|
||||
{
|
||||
name: "provides its name and requires its dependencies",
|
||||
p: pkg.Package{
|
||||
Name: "cowboy",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "cowboy",
|
||||
Dependencies: []string{"cowlib", "ranch"},
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"cowboy"},
|
||||
Requires: []string{"cowlib", "ranch"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dependencies still provides its name",
|
||||
p: pkg.Package{
|
||||
Name: "castore",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "castore",
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"castore"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong metadata type yields empty specification",
|
||||
p: pkg.Package{
|
||||
Name: "mystery",
|
||||
Metadata: pkg.RubyGemspec{},
|
||||
},
|
||||
want: dependency.Specification{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, mixLockDependencySpecifier(tt.p))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,14 @@ var _ generic.Parser = parseMixLock
|
||||
|
||||
var mixLockDelimiter = regexp.MustCompile(`[%{}\n" ,:]+`)
|
||||
|
||||
// mixLockDependency matches each `{:name,` tuple opener on a mix.lock line. The
|
||||
// first match is the entry's own source tuple (e.g. `{:hex, :name, ...}`); the
|
||||
// remaining matches are the entry's dependency tuples within its dependency
|
||||
// list (e.g. `[{:cowlib, ...}, {:ranch, ...}]`). Build-tool lists like
|
||||
// `[:mix]` or `[:make, :rebar3]` and option keyword lists like
|
||||
// `[hex: :cowlib, ...]` use bare atoms, not `{:atom,`, so they don't match.
|
||||
var mixLockDependency = regexp.MustCompile(`\{\s*:(\w+)\s*,`)
|
||||
|
||||
// parseMixLock parses a mix.lock and returns the discovered Elixir packages.
|
||||
func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
var errs error
|
||||
@ -56,13 +64,30 @@ func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, re
|
||||
packages = append(packages,
|
||||
newPackage(
|
||||
pkg.ElixirMixLockEntry{
|
||||
Name: name,
|
||||
Version: version,
|
||||
PkgHash: hash,
|
||||
PkgHashExt: hashExt,
|
||||
Name: name,
|
||||
Version: version,
|
||||
PkgHash: hash,
|
||||
PkgHashExt: hashExt,
|
||||
Dependencies: extractMixLockDependencies(line),
|
||||
},
|
||||
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// extractMixLockDependencies returns the names of the packages depended on by
|
||||
// the entry described on a single mix.lock line, by reading its dependency
|
||||
// list. The entry's own source tuple (the first `{:atom,` on the line) is
|
||||
// skipped; everything after it is a dependency.
|
||||
func extractMixLockDependencies(line string) []string {
|
||||
matches := mixLockDependency.FindAllStringSubmatch(line, -1)
|
||||
if len(matches) <= 1 {
|
||||
return nil
|
||||
}
|
||||
deps := make([]string, 0, len(matches)-1)
|
||||
for _, m := range matches[1:] {
|
||||
deps = append(deps, m[1])
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
@ -48,10 +48,11 @@ func TestParseMixLock(t *testing.T) {
|
||||
Locations: locations,
|
||||
PURL: "pkg:hex/cowboy@2.9.0",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "cowboy",
|
||||
Version: "2.9.0",
|
||||
PkgHash: "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452",
|
||||
PkgHashExt: "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde",
|
||||
Name: "cowboy",
|
||||
Version: "2.9.0",
|
||||
PkgHash: "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452",
|
||||
PkgHashExt: "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde",
|
||||
Dependencies: []string{"cowlib", "ranch"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -62,10 +63,11 @@ func TestParseMixLock(t *testing.T) {
|
||||
Locations: locations,
|
||||
PURL: "pkg:hex/cowboy_telemetry@0.4.0",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "cowboy_telemetry",
|
||||
Version: "0.4.0",
|
||||
PkgHash: "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c",
|
||||
PkgHashExt: "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de",
|
||||
Name: "cowboy_telemetry",
|
||||
Version: "0.4.0",
|
||||
PkgHash: "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c",
|
||||
PkgHashExt: "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de",
|
||||
Dependencies: []string{"cowboy", "telemetry"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -90,10 +92,11 @@ func TestParseMixLock(t *testing.T) {
|
||||
Locations: locations,
|
||||
PURL: "pkg:hex/db_connection@2.4.2",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "db_connection",
|
||||
Version: "2.4.2",
|
||||
PkgHash: "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3",
|
||||
PkgHashExt: "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668",
|
||||
Name: "db_connection",
|
||||
Version: "2.4.2",
|
||||
PkgHash: "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3",
|
||||
PkgHashExt: "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668",
|
||||
Dependencies: []string{"connection", "telemetry"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -132,10 +135,11 @@ func TestParseMixLock(t *testing.T) {
|
||||
Locations: locations,
|
||||
PURL: "pkg:hex/ecto@3.8.1",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "ecto",
|
||||
Version: "3.8.1",
|
||||
PkgHash: "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56",
|
||||
PkgHashExt: "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b",
|
||||
Name: "ecto",
|
||||
Version: "3.8.1",
|
||||
PkgHash: "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56",
|
||||
PkgHashExt: "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b",
|
||||
Dependencies: []string{"decimal", "jason", "telemetry"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -146,10 +150,11 @@ func TestParseMixLock(t *testing.T) {
|
||||
Locations: locations,
|
||||
PURL: "pkg:hex/ecto_sql@3.8.1",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "ecto_sql",
|
||||
Version: "3.8.1",
|
||||
PkgHash: "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798",
|
||||
PkgHashExt: "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525",
|
||||
Name: "ecto_sql",
|
||||
Version: "3.8.1",
|
||||
PkgHash: "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798",
|
||||
PkgHashExt: "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525",
|
||||
Dependencies: []string{"db_connection", "ecto", "myxql", "postgrex", "tds", "telemetry"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -160,10 +165,11 @@ func TestParseMixLock(t *testing.T) {
|
||||
Locations: locations,
|
||||
PURL: "pkg:hex/esbuild@0.5.0",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "esbuild",
|
||||
Version: "0.5.0",
|
||||
PkgHash: "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3",
|
||||
PkgHashExt: "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5",
|
||||
Name: "esbuild",
|
||||
Version: "0.5.0",
|
||||
PkgHash: "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3",
|
||||
PkgHashExt: "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5",
|
||||
Dependencies: []string{"castore"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -174,10 +180,11 @@ func TestParseMixLock(t *testing.T) {
|
||||
Locations: locations,
|
||||
PURL: "pkg:hex/ex_doc@0.28.4",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "ex_doc",
|
||||
Version: "0.28.4",
|
||||
PkgHash: "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298",
|
||||
PkgHashExt: "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed",
|
||||
Name: "ex_doc",
|
||||
Version: "0.28.4",
|
||||
PkgHash: "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298",
|
||||
PkgHashExt: "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed",
|
||||
Dependencies: []string{"earmark_parser", "makeup_elixir", "makeup_erlang"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -216,10 +223,11 @@ func TestParseMixLock(t *testing.T) {
|
||||
Locations: locations,
|
||||
PURL: "pkg:hex/jason@1.3.0",
|
||||
Metadata: pkg.ElixirMixLockEntry{
|
||||
Name: "jason",
|
||||
Version: "1.3.0",
|
||||
PkgHash: "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946",
|
||||
PkgHashExt: "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac",
|
||||
Name: "jason",
|
||||
Version: "1.3.0",
|
||||
PkgHash: "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946",
|
||||
PkgHashExt: "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac",
|
||||
Dependencies: []string{"decimal"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
5
syft/pkg/cataloger/elixir/testdata/relationships/mix.lock
vendored
Normal file
5
syft/pkg/cataloger/elixir/testdata/relationships/mix.lock
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
%{
|
||||
"cowlib": {:hex, :cowlib, "2.11.0", "aaa", [:make, :rebar3], [], "hexpm", "bbb"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "ccc", [:make, :rebar3], [], "hexpm", "ddd"},
|
||||
"cowboy": {:hex, :cowboy, "2.9.0", "eee", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "fff"},
|
||||
}
|
||||
@ -13,4 +13,9 @@ type ElixirMixLockEntry struct {
|
||||
|
||||
// PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)
|
||||
PkgHashExt string `mapstructure:"pkgHashExt" json:"pkgHashExt"`
|
||||
|
||||
// Dependencies are the names of the packages this entry depends on, as
|
||||
// declared in the entry's dependency list within mix.lock. Used to derive
|
||||
// dependency-of relationships between locked packages.
|
||||
Dependencies []string `mapstructure:"dependencies" json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user