Add Erlang OTP Application cataloger (#2403)

* Add cataloger for Erlang OTP applications

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>

* Add OTP Package type and Purl for ErLang

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>

* remove erlang OTP metadata type

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* use OTP purl type

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* restore otp fixture and adjust tests for dir-only results

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Laurent Goderre 2024-02-02 13:40:18 -05:00 committed by GitHub
parent 3023a5a7bc
commit d7b9cc70b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 223 additions and 14 deletions

View File

@ -368,6 +368,14 @@ var dirOnlyTestCases = []testCase{
"unicode_util_compat": "0.7.0", "unicode_util_compat": "0.7.0",
}, },
}, },
{
name: "find ErLang OTP applications",
pkgType: pkg.ErlangOTPPkg,
pkgLanguage: pkg.Erlang,
pkgInfo: map[string]string{
"accept": "0.3.5",
},
},
{ {
name: "find swift package manager packages", name: "find swift package manager packages",
pkgType: pkg.SwiftPkg, pkgType: pkg.SwiftPkg,

View File

@ -57,8 +57,8 @@ func TestPkgCoverageImage(t *testing.T) {
definedLanguages.Remove(pkg.Swift.String()) definedLanguages.Remove(pkg.Swift.String())
definedLanguages.Remove(pkg.CPP.String()) definedLanguages.Remove(pkg.CPP.String())
definedLanguages.Remove(pkg.Haskell.String()) definedLanguages.Remove(pkg.Haskell.String())
definedLanguages.Remove(pkg.Erlang.String())
definedLanguages.Remove(pkg.Elixir.String()) definedLanguages.Remove(pkg.Elixir.String())
definedLanguages.Remove(pkg.Erlang.String())
observedPkgs := strset.New() observedPkgs := strset.New()
definedPkgs := strset.New() definedPkgs := strset.New()
@ -71,6 +71,7 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.GoModulePkg)) definedPkgs.Remove(string(pkg.GoModulePkg))
definedPkgs.Remove(string(pkg.RustPkg)) definedPkgs.Remove(string(pkg.RustPkg))
definedPkgs.Remove(string(pkg.DartPubPkg)) definedPkgs.Remove(string(pkg.DartPubPkg))
definedPkgs.Remove(string(pkg.ErlangOTPPkg))
definedPkgs.Remove(string(pkg.CocoapodsPkg)) definedPkgs.Remove(string(pkg.CocoapodsPkg))
definedPkgs.Remove(string(pkg.ConanPkg)) definedPkgs.Remove(string(pkg.ConanPkg))
definedPkgs.Remove(string(pkg.HackagePkg)) definedPkgs.Remove(string(pkg.HackagePkg))

View File

@ -0,0 +1,10 @@
{application,accept,
[{description,"Accept header(s) for Erlang/Elixir"},
{vsn,"0.3.5"},
{registered,[]},
{applications,[kernel,stdlib]},
{env,[]},
{modules, ['accept_encoding_header','accept_header','accept_neg','accept_parser']},
{maintainers,["Ilya Khaprov"]},
{licenses,["MIT"]},
{links,[{"Github","https://github.com/deadtrickster/accept"}]}]}.

2
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426
github.com/anchore/stereoscope v0.0.2-0.20240201224129-37291e81936d github.com/anchore/stereoscope v0.0.2-0.20240201224129-37291e81936d
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
// we are hinting brotli to latest due to warning when installing archiver v3: // we are hinting brotli to latest due to warning when installing archiver v3:

4
go.sum
View File

@ -107,8 +107,8 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8= github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426 h1:agoiZchSf1Nnnos1azwIg5hk5Ao9TzZNBD9++AChGEg=
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4=
github.com/anchore/stereoscope v0.0.2-0.20240201224129-37291e81936d h1:v+kf6J76l5nWvdvxptgyLXWr45G8CGVScL4AAISi3nI= github.com/anchore/stereoscope v0.0.2-0.20240201224129-37291e81936d h1:v+kf6J76l5nWvdvxptgyLXWr45G8CGVScL4AAISi3nI=
github.com/anchore/stereoscope v0.0.2-0.20240201224129-37291e81936d/go.mod h1:uydT2ful8TY7Hr1WH1V1ZecSq/2TqXpAsGkMiy7lxD0= github.com/anchore/stereoscope v0.0.2-0.20240201224129-37291e81936d/go.mod h1:uydT2ful8TY7Hr1WH1V1ZecSq/2TqXpAsGkMiy7lxD0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=

View File

@ -57,6 +57,7 @@ func DefaultPackageTaskFactories() PackageTaskFactories {
newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"), newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"), newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
newSimplePackageTaskFactory(erlang.NewRebarLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang"), newSimplePackageTaskFactory(erlang.NewRebarLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang"),
newSimplePackageTaskFactory(erlang.NewOTPCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang", "otp"),
newSimplePackageTaskFactory(haskell.NewHackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "haskell", "hackage", "cabal"), newSimplePackageTaskFactory(haskell.NewHackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "haskell", "hackage", "cabal"),
newPackageTaskFactory( newPackageTaskFactory(
func(cfg CatalogingFactoryConfig) pkg.Cataloger { func(cfg CatalogingFactoryConfig) pkg.Cataloger {

View File

@ -46,6 +46,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from cabal or stack manifest files" answer = "acquired package info from cabal or stack manifest files"
case pkg.HexPkg: case pkg.HexPkg:
answer = "acquired package info from rebar3 or mix manifest file" answer = "acquired package info from rebar3 or mix manifest file"
case pkg.ErlangOTPPkg:
answer = "acquired package info from ErLang application resource file"
case pkg.LinuxKernelPkg: case pkg.LinuxKernelPkg:
answer = "acquired package info from linux kernel archive" answer = "acquired package info from linux kernel archive"
case pkg.LinuxKernelModulePkg: case pkg.LinuxKernelModulePkg:

View File

@ -199,6 +199,14 @@ func Test_SourceInfo(t *testing.T) {
"from rebar3 or mix manifest file", "from rebar3 or mix manifest file",
}, },
}, },
{
input: pkg.Package{
Type: pkg.ErlangOTPPkg,
},
expected: []string{
"from ErLang application resource file",
},
},
{ {
input: pkg.Package{ input: pkg.Package{
Type: pkg.LinuxKernelPkg, Type: pkg.LinuxKernelPkg,

View File

@ -1,5 +1,5 @@
/* /*
Package erlang provides a concrete Cataloger implementation relating to packages within the Erlang language ecosystem. Package erlang provides concrete Catalogers implementation relating to packages within the Erlang language ecosystem.
*/ */
package erlang package erlang
@ -13,3 +13,8 @@ func NewRebarLockCataloger() pkg.Cataloger {
return generic.NewCataloger("erlang-rebar-lock-cataloger"). return generic.NewCataloger("erlang-rebar-lock-cataloger").
WithParserByGlobs(parseRebarLock, "**/rebar.lock") WithParserByGlobs(parseRebarLock, "**/rebar.lock")
} }
func NewOTPCataloger() pkg.Cataloger {
return generic.NewCataloger("erlang-otp-application-cataloger").
WithParserByGlobs(parseOTPApp, "**/*.app")
}

View File

@ -6,7 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
) )
func TestCataloger_Globs(t *testing.T) { func TestCatalogerRebar_Globs(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
fixture string fixture string
@ -30,3 +30,28 @@ func TestCataloger_Globs(t *testing.T) {
}) })
} }
} }
func TestCatalogerOTP_Globs(t *testing.T) {
tests := []struct {
name string
fixture string
expected []string
}{
{
name: "obtain OTP resource files",
fixture: "test-fixtures/glob-paths",
expected: []string{
"src/rabbitmq.app",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture).
ExpectsResolverContentQueries(test.expected).
TestCataloger(t, NewOTPCataloger())
})
}
}

View File

@ -6,13 +6,13 @@ import (
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
func newPackage(d pkg.ErlangRebarLockEntry, locations ...file.Location) pkg.Package { func newPackageFromRebar(d pkg.ErlangRebarLockEntry, 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.Erlang, Language: pkg.Erlang,
Locations: file.NewLocationSet(locations...), Locations: file.NewLocationSet(locations...),
PURL: packageURL(d), PURL: packageURLFromRebar(d),
Type: pkg.HexPkg, Type: pkg.HexPkg,
Metadata: d, Metadata: d,
} }
@ -22,7 +22,7 @@ func newPackage(d pkg.ErlangRebarLockEntry, locations ...file.Location) pkg.Pack
return p return p
} }
func packageURL(m pkg.ErlangRebarLockEntry) string { func packageURLFromRebar(m pkg.ErlangRebarLockEntry) string {
var qualifiers packageurl.Qualifiers var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL( return packageurl.NewPackageURL(
@ -34,3 +34,31 @@ func packageURL(m pkg.ErlangRebarLockEntry) string {
"", "",
).ToString() ).ToString()
} }
func newPackageFromOTP(name, version string, locations ...file.Location) pkg.Package {
p := pkg.Package{
Name: name,
Version: version,
Language: pkg.Erlang,
Locations: file.NewLocationSet(locations...),
PURL: packageURLFromOTP(name, version),
Type: pkg.ErlangOTPPkg,
}
p.SetID()
return p
}
func packageURLFromOTP(name, version string) string {
var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL(
packageurl.TypeOTP,
"",
name,
version,
qualifiers,
"",
).ToString()
}

View File

@ -0,0 +1,48 @@
package erlang
import (
"context"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// parseOTPApp parses a OTP *.app files to a package objects
func parseOTPApp(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
doc, err := parseErlang(reader)
if err != nil {
// there are multiple file formats that use the *.app extension, so it's possible that this is not an OTP app file at all
// ... which means we should not return an error here
log.WithFields("error", err).Trace("unable to parse Erlang OTP app")
return nil, nil, nil
}
var packages []pkg.Package
root := doc.Get(0)
name := root.Get(1).String()
keys := root.Get(2)
for _, key := range keys.Slice() {
if key.Get(0).String() == "vsn" {
version := key.Get(1).String()
p := newPackageFromOTP(
name, version,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
packages = append(packages, p)
}
}
return packages, nil, nil
}
// integrity check
var _ generic.Parser = parseOTPApp

View File

@ -0,0 +1,43 @@
package erlang
import (
"testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParseOTPApplication(t *testing.T) {
tests := []struct {
fixture string
expected []pkg.Package
}{
{
fixture: "test-fixtures/rabbitmq.app",
expected: []pkg.Package{
{
Name: "rabbit",
Version: "3.12.10",
Language: pkg.Erlang,
Type: pkg.ErlangOTPPkg,
PURL: "pkg:otp/rabbit@3.12.10",
},
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
// TODO: relationships are not under test
var expectedRelationships []artifact.Relationship
for idx := range test.expected {
test.expected[idx].Locations = file.NewLocationSet(file.NewLocation(test.fixture))
}
pkgtest.TestFileParser(t, test.fixture, parseOTPApp, test.expected, expectedRelationships)
})
}
}

View File

@ -50,7 +50,7 @@ func parseRebarLock(_ context.Context, _ file.Resolver, _ *generic.Environment,
version = versionNode.Get(2).Get(1).String() version = versionNode.Get(2).Get(1).String()
} }
p := newPackage( p := newPackageFromRebar(
pkg.ErlangRebarLockEntry{ pkg.ErlangRebarLockEntry{
Name: name, Name: name,
Version: version, Version: version,

View File

@ -0,0 +1 @@
bogus erlang file

View File

@ -0,0 +1,18 @@
{application, 'rabbit', [
{description, "RabbitMQ"},
{vsn, "3.12.10"},
{id, "v3.12.9-9-g1f61ca8"},
{modules, ['amqqueue','background_gc']},
{optional_applications, []},
{env, [
{memory_monitor_interval, 2500},
{disk_free_limit, 50000000}, %% 50MB
{msg_store_index_module, rabbit_msg_store_ets_index},
{backing_queue_module, rabbit_variable_queue},
%% 0 ("no limit") would make a better default, but that
%% breaks the QPid Java client
{frame_max, 131072},
%% see rabbitmq-server#1593
{channel_max, 2047}
]}
]}.

View File

@ -80,7 +80,7 @@ func LanguageByName(name string) Language {
return Rust return Rust
case packageurl.TypePub, string(DartPubPkg), string(Dart): case packageurl.TypePub, string(DartPubPkg), string(Dart):
return Dart return Dart
case packageurl.TypeDotnet, packageurl.TypeNuget: case string(Dotnet), ".net", packageurl.TypeNuget:
return Dotnet return Dotnet
case packageurl.TypeCocoapods, packageurl.TypeSwift, string(CocoapodsPkg), string(SwiftPkg): case packageurl.TypeCocoapods, packageurl.TypeSwift, string(CocoapodsPkg), string(SwiftPkg):
return Swift return Swift
@ -88,7 +88,7 @@ func LanguageByName(name string) Language {
return CPP return CPP
case packageurl.TypeHackage, string(Haskell): case packageurl.TypeHackage, string(Haskell):
return Haskell return Haskell
case packageurl.TypeHex, "beam", "elixir", "erlang": case packageurl.TypeHex, packageurl.TypeOTP, "beam", "elixir", "erlang":
// should we support returning multiple languages to support this case? // should we support returning multiple languages to support this case?
// answer: no. We want this to definitively answer "which language does this package represent?" // answer: no. We want this to definitively answer "which language does this package represent?"
// which might not be possible in all cases. See for more context: https://github.com/package-url/purl-spec/pull/178 // which might not be possible in all cases. See for more context: https://github.com/package-url/purl-spec/pull/178

View File

@ -18,6 +18,7 @@ const (
DartPubPkg Type = "dart-pub" DartPubPkg Type = "dart-pub"
DebPkg Type = "deb" DebPkg Type = "deb"
DotnetPkg Type = "dotnet" DotnetPkg Type = "dotnet"
ErlangOTPPkg Type = "erlang-otp"
GemPkg Type = "gem" GemPkg Type = "gem"
GithubActionPkg Type = "github-action" GithubActionPkg Type = "github-action"
GithubActionWorkflowPkg Type = "github-action-workflow" GithubActionWorkflowPkg Type = "github-action-workflow"
@ -51,6 +52,7 @@ var AllPkgs = []Type{
DartPubPkg, DartPubPkg,
DebPkg, DebPkg,
DotnetPkg, DotnetPkg,
ErlangOTPPkg,
GemPkg, GemPkg,
GithubActionPkg, GithubActionPkg,
GithubActionWorkflowPkg, GithubActionWorkflowPkg,
@ -91,7 +93,9 @@ func (t Type) PackageURLType() string {
case DebPkg: case DebPkg:
return "deb" return "deb"
case DotnetPkg: case DotnetPkg:
return packageurl.TypeDotnet return "dotnet"
case ErlangOTPPkg:
return packageurl.TypeOTP
case GemPkg: case GemPkg:
return packageurl.TypeGem return packageurl.TypeGem
case HexPkg: case HexPkg:
@ -146,6 +150,7 @@ func TypeFromPURL(p string) Type {
return TypeByName(ptype) return TypeByName(ptype)
} }
//nolint:funlen
func TypeByName(name string) Type { func TypeByName(name string) Type {
switch name { switch name {
case packageurl.TypeDebian: case packageurl.TypeDebian:
@ -172,7 +177,7 @@ func TypeByName(name string) Type {
return RustPkg return RustPkg
case packageurl.TypePub: case packageurl.TypePub:
return DartPubPkg return DartPubPkg
case packageurl.TypeDotnet: case "dotnet": // here to support legacy use cases
return DotnetPkg return DotnetPkg
case packageurl.TypeCocoapods: case packageurl.TypeCocoapods:
return CocoapodsPkg return CocoapodsPkg
@ -184,6 +189,8 @@ func TypeByName(name string) Type {
return PortagePkg return PortagePkg
case packageurl.TypeHex: case packageurl.TypeHex:
return HexPkg return HexPkg
case packageurl.TypeOTP:
return ErlangOTPPkg
case "linux-kernel": case "linux-kernel":
return LinuxKernelPkg return LinuxKernelPkg
case "linux-kernel-module": case "linux-kernel-module":

View File

@ -83,6 +83,10 @@ func TestTypeFromPURL(t *testing.T) {
purl: "pkg:hex/hpax/hpax@0.1.1", purl: "pkg:hex/hpax/hpax@0.1.1",
expected: HexPkg, expected: HexPkg,
}, },
{
purl: "pkg:otp/accept@0.3.5",
expected: ErlangOTPPkg,
},
{ {
purl: "pkg:generic/linux-kernel@5.10.15", purl: "pkg:generic/linux-kernel@5.10.15",
expected: LinuxKernelPkg, expected: LinuxKernelPkg,