Merge pull request #247 from anchore/syft-java-cataloger-integration

General Java cataloger enhancements
This commit is contained in:
Alex Goodman 2020-10-30 13:55:16 -04:00 committed by GitHub
commit 6e98752c6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 410 additions and 277 deletions

View File

@ -126,7 +126,7 @@
}, },
"manifest": { "manifest": {
"properties": { "properties": {
"extraFields": { "main": {
"properties": { "properties": {
"Archiver-Version": { "Archiver-Version": {
"type": "string" "type": "string"
@ -149,6 +149,12 @@
"Hudson-Version": { "Hudson-Version": {
"type": "string" "type": "string"
}, },
"Implementation-Title": {
"type": "string"
},
"Implementation-Version": {
"type": "string"
},
"Jenkins-Version": { "Jenkins-Version": {
"type": "string" "type": "string"
}, },
@ -158,6 +164,9 @@
"Main-Class": { "Main-Class": {
"type": "string" "type": "string"
}, },
"Manifest-Version": {
"type": "string"
},
"Minimum-Java-Version": { "Minimum-Java-Version": {
"type": "string" "type": "string"
}, },
@ -181,51 +190,23 @@
}, },
"Short-Name": { "Short-Name": {
"type": "string" "type": "string"
},
"Specification-Title": {
"type": "string"
} }
}, },
"required": [ "required": [
"Archiver-Version", "Archiver-Version",
"Build-Jdk", "Build-Jdk",
"Built-By", "Built-By",
"Created-By" "Created-By",
"Manifest-Version"
], ],
"type": "object" "type": "object"
},
"implementationTitle": {
"type": "string"
},
"implementationVendor": {
"type": "string"
},
"implementationVersion": {
"type": "string"
},
"manifestVersion": {
"type": "string"
},
"name": {
"type": "string"
},
"specificationTitle": {
"type": "string"
},
"specificationVendor": {
"type": "string"
},
"specificationVersion": {
"type": "string"
} }
}, },
"required": [ "required": [
"extraFields", "main"
"implementationTitle",
"implementationVendor",
"implementationVersion",
"manifestVersion",
"name",
"specificationTitle",
"specificationVendor",
"specificationVersion"
], ],
"type": "object" "type": "object"
}, },

View File

@ -10,11 +10,20 @@ import (
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// match on versions and anything after the version. This is used to isolate the name from the version.
// match examples:
// wagon-webdav-1.0.2-rc1-hudson.jar ---> -1.0.2-rc1-hudson.jar
// windows-remote-command-1.0.jar ---> -1.0.jar
// wstx-asl-1-2.jar ---> -1-2.jar
// guava-rc0.jar ---> -rc0.jar
var versionAreaPattern = regexp.MustCompile(`-(?P<version>(\d+\.)?(\d+\.)?(r?c?\d+)(-[a-zA-Z0-9\-.]+)*)(?P<remaining>.*)$`)
// match on explicit versions. This is used for extracting version information.
// match examples: // match examples:
// pkg-extra-field-4.3.2-rc1 --> match(name=pkg-extra-field version=4.3.2-rc1) // pkg-extra-field-4.3.2-rc1 --> match(name=pkg-extra-field version=4.3.2-rc1)
// pkg-extra-field-4.3-rc1 --> match(name=pkg-extra-field version=4.3-rc1) // pkg-extra-field-4.3-rc1 --> match(name=pkg-extra-field version=4.3-rc1)
// pkg-extra-field-4.3 --> match(name=pkg-extra-field version=4.3) // pkg-extra-field-4.3 --> match(name=pkg-extra-field version=4.3)
var versionPattern = regexp.MustCompile(`(?P<name>.+)-(?P<version>(\d+\.)?(\d+\.)?(\*|\d+)(-[a-zA-Z0-9\-\.]+)*)`) var versionPattern = regexp.MustCompile(`-(?P<version>(\d+\.)?(\d+\.)?(r?c?\d+)(-[a-zA-Z0-9\-.]+)*)`)
type archiveFilename struct { type archiveFilename struct {
raw string raw string
@ -70,14 +79,8 @@ func (a archiveFilename) version() string {
} }
func (a archiveFilename) name() string { func (a archiveFilename) name() string {
for _, fieldSet := range a.fields { // derive the name from the archive name (no path or extension) and remove any versions found
if name, ok := fieldSet["name"]; ok {
// return the first name
return name
}
}
// derive the name from the archive name (no path or extension)
basename := filepath.Base(a.raw) basename := filepath.Base(a.raw)
return strings.TrimSuffix(basename, filepath.Ext(basename)) cleaned := strings.TrimSuffix(basename, filepath.Ext(basename))
return versionAreaPattern.ReplaceAllString(cleaned, "")
} }

View File

@ -1,9 +1,10 @@
package java package java
import ( import (
"testing"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/sergi/go-diff/diffmatchpatch" "github.com/sergi/go-diff/diffmatchpatch"
"testing"
) )
func TestExtractInfoFromJavaArchiveFilename(t *testing.T) { func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
@ -56,12 +57,78 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
name: "pkg-extra-field-maven", name: "pkg-extra-field-maven",
ty: pkg.JenkinsPluginPkg, ty: pkg.JenkinsPluginPkg,
}, },
{
filename: "/some/path-with-version-5.4.3/wagon-webdav-1.0.2-beta-2.2.3a-hudson.jar",
version: "1.0.2-beta-2.2.3a-hudson",
extension: "jar",
name: "wagon-webdav",
ty: pkg.JavaPkg,
},
{
filename: "/some/path-with-version-5.4.3/wagon-webdav-1.0.2-beta-2.2.3-hudson.jar",
version: "1.0.2-beta-2.2.3-hudson",
extension: "jar",
name: "wagon-webdav",
ty: pkg.JavaPkg,
},
{
filename: "/some/path-with-version-5.4.3/windows-remote-command-1.0.jar",
version: "1.0",
extension: "jar",
name: "windows-remote-command",
ty: pkg.JavaPkg,
},
{
filename: "/some/path-with-version-5.4.3/wagon-http-lightweight-1.0.5-beta-2.jar",
version: "1.0.5-beta-2",
extension: "jar",
name: "wagon-http-lightweight",
ty: pkg.JavaPkg,
},
{
filename: "/hudson.war:WEB-INF/lib/commons-jelly-1.1-hudson-20100305.jar",
version: "1.1-hudson-20100305",
extension: "jar",
name: "commons-jelly",
ty: pkg.JavaPkg,
},
{
filename: "/hudson.war:WEB-INF/lib/jtidy-4aug2000r7-dev-hudson-1.jar",
// I don't see how we can reliably account for this case
//version: "4aug2000r7-dev-hudson-1",
version: "",
extension: "jar",
name: "jtidy",
ty: pkg.JavaPkg,
},
{
filename: "/hudson.war:WEB-INF/lib/trilead-ssh2-build212-hudson-5.jar",
// I don't see how we can reliably account for this case
//version: "build212-hudson-5",
version: "5",
extension: "jar",
// name: "trilead-ssh2",
name: "trilead-ssh2-build212-hudson",
ty: pkg.JavaPkg,
},
{
filename: "/hudson.war:WEB-INF/lib/guava-r06.jar",
version: "r06",
extension: "jar",
name: "guava",
ty: pkg.JavaPkg,
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.filename, func(t *testing.T) { t.Run(test.filename, func(t *testing.T) {
obj := newJavaArchiveFilename(test.filename) obj := newJavaArchiveFilename(test.filename)
ty := obj.pkgType()
if ty != test.ty {
t.Errorf("mismatched type: %+v != %v", ty, test.ty)
}
version := obj.version() version := obj.version()
if version != test.version { if version != test.version {
dmp := diffmatchpatch.New() dmp := diffmatchpatch.New()

View File

@ -66,13 +66,17 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
return nil, cleanupFn, fmt.Errorf("unable to read files from java archive: %w", err) return nil, cleanupFn, fmt.Errorf("unable to read files from java archive: %w", err)
} }
// fetch the last element of the virtual path
virtualElements := strings.Split(virtualPath, ":")
currentFilepath := virtualElements[len(virtualElements)-1]
return &archiveParser{ return &archiveParser{
discoveredPkgs: internal.NewStringSet(), discoveredPkgs: internal.NewStringSet(),
fileManifest: fileManifest, fileManifest: fileManifest,
virtualPath: virtualPath, virtualPath: virtualPath,
archivePath: archivePath, archivePath: archivePath,
contentPath: contentPath, contentPath: contentPath,
fileInfo: newJavaArchiveFilename(virtualPath), fileInfo: newJavaArchiveFilename(currentFilepath),
detectNested: detectNested, detectNested: detectNested,
}, cleanupFn, nil }, cleanupFn, nil
} }
@ -136,7 +140,7 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
// parse the manifest file into a rich object // parse the manifest file into a rich object
manifestContents := contents[manifestMatches[0]] manifestContents := contents[manifestMatches[0]]
manifest, err := parseJavaManifest(strings.NewReader(manifestContents)) manifest, err := parseJavaManifest(j.archivePath, strings.NewReader(manifestContents))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse java manifest (%s): %w", j.virtualPath, err) return nil, fmt.Errorf("failed to parse java manifest (%s): %w", j.virtualPath, err)
} }
@ -156,6 +160,7 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
// discoverPkgsFromPomProperties parses Maven POM properties for a given parent package, returning all listed Java packages found and // discoverPkgsFromPomProperties parses Maven POM properties for a given parent package, returning all listed Java packages found and
// associating each discovered package to the given parent package. // associating each discovered package to the given parent package.
// nolint:funlen,gocognit
func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([]pkg.Package, error) { func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0) var pkgs = make([]pkg.Package, 0)
parentKey := uniquePkgKey(parentPkg) parentKey := uniquePkgKey(parentPkg)
@ -177,6 +182,13 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
if propsObj.Version != "" && propsObj.ArtifactID != "" { if propsObj.Version != "" && propsObj.ArtifactID != "" {
// TODO: if there is no parentPkg (no java manifest) one of these poms could be the parent. We should discover the right parent and attach the correct info accordingly to each discovered package // TODO: if there is no parentPkg (no java manifest) one of these poms could be the parent. We should discover the right parent and attach the correct info accordingly to each discovered package
// keep the artifact name within the virtual path if this package does not match the parent package
vPathSuffix := ""
if !strings.HasPrefix(propsObj.ArtifactID, parentPkg.Name) {
vPathSuffix += ":" + propsObj.ArtifactID
}
virtualPath := j.virtualPath + vPathSuffix
// discovered props = new package // discovered props = new package
p := pkg.Package{ p := pkg.Package{
Name: propsObj.ArtifactID, Name: propsObj.ArtifactID,
@ -185,7 +197,7 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
Type: pkg.JavaPkg, Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType, MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{ Metadata: pkg.JavaMetadata{
VirtualPath: j.virtualPath, VirtualPath: virtualPath,
PomProperties: propsObj, PomProperties: propsObj,
Parent: parentPkg, Parent: parentPkg,
}, },
@ -193,16 +205,35 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
pkgKey := uniquePkgKey(&p) pkgKey := uniquePkgKey(&p)
if !j.discoveredPkgs.Contains(pkgKey) { // the name/version pair matches...
// only keep packages we haven't seen yet matchesParentPkg := pkgKey == parentKey
pkgs = append(pkgs, p)
} else if pkgKey == parentKey { // the virtual path matches...
matchesParentPkg = matchesParentPkg || parentPkg.Metadata.(pkg.JavaMetadata).VirtualPath == virtualPath
// the pom artifactId has the parent name or vice versa
if propsObj.ArtifactID != "" {
matchesParentPkg = matchesParentPkg || strings.Contains(parentPkg.Name, propsObj.ArtifactID) || strings.Contains(propsObj.ArtifactID, parentPkg.Name)
}
if matchesParentPkg {
// we've run across more information about our parent package, add this info to the parent package metadata // we've run across more information about our parent package, add this info to the parent package metadata
// the pom properties is typically a better source of information for name and version than the manifest
if p.Name != parentPkg.Name {
parentPkg.Name = p.Name
}
if p.Version != parentPkg.Version {
parentPkg.Version = p.Version
}
parentMetadata, ok := parentPkg.Metadata.(pkg.JavaMetadata) parentMetadata, ok := parentPkg.Metadata.(pkg.JavaMetadata)
if ok { if ok {
parentMetadata.PomProperties = propsObj parentMetadata.PomProperties = propsObj
parentPkg.Metadata = parentMetadata parentPkg.Metadata = parentMetadata
} }
} else if !j.discoveredPkgs.Contains(pkgKey) {
// only keep packages we haven't seen yet (and are not related to the parent package)
pkgs = append(pkgs, p)
} }
} }
} }

View File

@ -78,51 +78,6 @@ func generateJavaBuildFixture(t *testing.T, fixturePath string) {
} }
} }
func TestSelectName(t *testing.T) {
tests := []struct {
desc string
manifest pkg.JavaManifest
archive archiveFilename
expected string
}{
{
desc: "name from Implementation-Title",
archive: archiveFilename{},
manifest: pkg.JavaManifest{
Name: "",
SpecTitle: "",
ImplTitle: "maven-wrapper",
},
expected: "maven-wrapper",
},
{
desc: "Implementation-Title does not override",
manifest: pkg.JavaManifest{
Name: "Foo",
SpecTitle: "",
ImplTitle: "maven-wrapper",
},
archive: archiveFilename{
fields: []map[string]string{
{"name": "omg"},
},
},
expected: "omg",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
result := selectName(&test.manifest, test.archive)
if result != test.expected {
t.Errorf("mismatch in names: '%s' != '%s'", result, test.expected)
}
})
}
}
func TestParseJar(t *testing.T) { func TestParseJar(t *testing.T) {
tests := []struct { tests := []struct {
fixture string fixture string
@ -145,11 +100,12 @@ func TestParseJar(t *testing.T) {
Metadata: pkg.JavaMetadata{ Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi", VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
Manifest: &pkg.JavaManifest{ Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
SpecTitle: "The Jenkins Plugins Parent POM Project", "Manifest-Version": "1.0",
ImplTitle: "example-jenkins-plugin", "Specification-Title": "The Jenkins Plugins Parent POM Project",
ImplVersion: "1.0-SNAPSHOT", "Implementation-Title": "example-jenkins-plugin",
Extra: map[string]string{ "Implementation-Version": "1.0-SNAPSHOT",
// extra fields...
"Archiver-Version": "Plexus Archiver", "Archiver-Version": "Plexus Archiver",
"Plugin-License-Url": "https://opensource.org/licenses/MIT", "Plugin-License-Url": "https://opensource.org/licenses/MIT",
"Plugin-License-Name": "MIT License", "Plugin-License-Name": "MIT License",
@ -191,7 +147,9 @@ func TestParseJar(t *testing.T) {
Metadata: pkg.JavaMetadata{ Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar", VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
Manifest: &pkg.JavaManifest{ Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
"Manifest-Version": "1.0",
},
}, },
}, },
}, },
@ -212,8 +170,9 @@ func TestParseJar(t *testing.T) {
Metadata: pkg.JavaMetadata{ Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
Manifest: &pkg.JavaManifest{ Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
Extra: map[string]string{ "Manifest-Version": "1.0",
// extra fields...
"Archiver-Version": "Plexus Archiver", "Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3", "Created-By": "Apache Maven 3.6.3",
"Built-By": "?", "Built-By": "?",
@ -236,7 +195,9 @@ func TestParseJar(t *testing.T) {
Type: pkg.JavaPkg, Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType, MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{ Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", // ensure that nested packages with different names than that of the parent are appended as
// a suffix on the virtual path
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar:joda-time",
PomProperties: &pkg.PomProperties{ PomProperties: &pkg.PomProperties{
Path: "META-INF/maven/joda-time/joda-time/pom.properties", Path: "META-INF/maven/joda-time/joda-time/pom.properties",
GroupID: "joda-time", GroupID: "joda-time",
@ -303,11 +264,11 @@ func TestParseJar(t *testing.T) {
metadata := a.Metadata.(pkg.JavaMetadata) metadata := a.Metadata.(pkg.JavaMetadata)
metadata.Parent = nil metadata.Parent = nil
// ignore select fields // ignore select fields (only works for the main section)
for _, field := range test.ignoreExtras { for _, field := range test.ignoreExtras {
if metadata.Manifest != nil && metadata.Manifest.Extra != nil { if metadata.Manifest != nil && metadata.Manifest.Main != nil {
if _, ok := metadata.Manifest.Extra[field]; ok { if _, ok := metadata.Manifest.Main[field]; ok {
delete(metadata.Manifest.Extra, field) delete(metadata.Manifest.Main, field)
} }
} }
} }
@ -342,7 +303,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-boot-starter", Name: "spring-boot-starter",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "jul-to-slf4j", Name: "jul-to-slf4j",
@ -354,7 +315,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-boot-starter-validation", Name: "spring-boot-starter-validation",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "hibernate-validator", Name: "hibernate-validator",
@ -366,7 +327,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-expression", Name: "spring-expression",
Version: "5.2.2.RELEASE", Version: "5.2.2",
}, },
{ {
Name: "jakarta.validation-api", Name: "jakarta.validation-api",
@ -374,11 +335,11 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-web", Name: "spring-web",
Version: "5.2.2.RELEASE", Version: "5.2.2",
}, },
{ {
Name: "spring-boot-starter-actuator", Name: "spring-boot-starter-actuator",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "log4j-api", Name: "log4j-api",
@ -398,23 +359,23 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-aop", Name: "spring-aop",
Version: "5.2.2.RELEASE", Version: "5.2.2",
}, },
{ {
Name: "spring-boot-actuator-autoconfigure", Name: "spring-boot-actuator-autoconfigure",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "spring-jcl", Name: "spring-jcl",
Version: "5.2.2.RELEASE", Version: "5.2.2",
}, },
{ {
Name: "spring-boot", Name: "spring-boot",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "spring-boot-starter-logging", Name: "spring-boot-starter-logging",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "jakarta.annotation-api", Name: "jakarta.annotation-api",
@ -422,7 +383,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-webmvc", Name: "spring-webmvc",
Version: "5.2.2.RELEASE", Version: "5.2.2",
}, },
{ {
Name: "HdrHistogram", Name: "HdrHistogram",
@ -430,7 +391,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-boot-starter-web", Name: "spring-boot-starter-web",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "logback-classic", Name: "logback-classic",
@ -442,7 +403,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-boot-starter-json", Name: "spring-boot-starter-json",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "jackson-databind", Name: "jackson-databind",
@ -458,7 +419,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-boot-autoconfigure", Name: "spring-boot-autoconfigure",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "jackson-datatype-jdk8", Name: "jackson-datatype-jdk8",
@ -474,11 +435,11 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-beans", Name: "spring-beans",
Version: "5.2.2.RELEASE", Version: "5.2.2",
}, },
{ {
Name: "spring-boot-actuator", Name: "spring-boot-actuator",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "slf4j-api", Name: "slf4j-api",
@ -486,7 +447,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-core", Name: "spring-core",
Version: "5.2.2.RELEASE", Version: "5.2.2",
}, },
{ {
Name: "logback-core", Name: "logback-core",
@ -506,7 +467,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-boot-starter-tomcat", Name: "spring-boot-starter-tomcat",
Version: "2.2.2.RELEASE", Version: "2.2.2",
}, },
{ {
Name: "classmate", Name: "classmate",
@ -514,7 +475,7 @@ func TestParseNestedJar(t *testing.T) {
}, },
{ {
Name: "spring-context", Name: "spring-context",
Version: "5.2.2.RELEASE", Version: "5.2.2",
}, },
}, },
}, },
@ -535,7 +496,7 @@ func TestParseNestedJar(t *testing.T) {
t.Fatalf("failed to parse java archive: %+v", err) t.Fatalf("failed to parse java archive: %+v", err)
} }
nameVersionPairSet := internal.NewStringSet() expectedNameVersionPairSet := internal.NewStringSet()
makeKey := func(p *pkg.Package) string { makeKey := func(p *pkg.Package) string {
if p == nil { if p == nil {
@ -545,20 +506,32 @@ func TestParseNestedJar(t *testing.T) {
} }
for _, e := range test.expected { for _, e := range test.expected {
nameVersionPairSet.Add(makeKey(&e)) expectedNameVersionPairSet.Add(makeKey(&e))
} }
if len(actual) != len(nameVersionPairSet) { if len(actual) != len(expectedNameVersionPairSet) {
actualNameVersionPairSet := internal.NewStringSet()
for _, a := range actual { for _, a := range actual {
t.Log(" ", a) key := makeKey(&a)
actualNameVersionPairSet.Add(key)
if !expectedNameVersionPairSet.Contains(key) {
t.Logf("extra package: %s", a)
}
} }
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(nameVersionPairSet))
for _, key := range expectedNameVersionPairSet.ToSlice() {
if !actualNameVersionPairSet.Contains(key) {
t.Logf("missing package: %s", key)
}
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expectedNameVersionPairSet))
} }
for _, a := range actual { for _, a := range actual {
actualKey := makeKey(&a) actualKey := makeKey(&a)
if !nameVersionPairSet.Contains(actualKey) { if !expectedNameVersionPairSet.Contains(actualKey) {
t.Errorf("unexpected pkg: %q", actualKey) t.Errorf("unexpected pkg: %q", actualKey)
} }
@ -571,7 +544,7 @@ func TestParseNestedJar(t *testing.T) {
if metadata.Parent == nil { if metadata.Parent == nil {
t.Errorf("unassigned error for pkg=%q", actualKey) t.Errorf("unassigned error for pkg=%q", actualKey)
} else if makeKey(metadata.Parent) != "spring-boot|0.0.1-SNAPSHOT" { } else if makeKey(metadata.Parent) != "spring-boot|0.0.1-SNAPSHOT" {
// NB: this is a hard-coded condition to simplify the test harness // NB: this is a hard-coded condition to simplify the test harness to account for https://github.com/micrometer-metrics/micrometer/issues/1785
if a.Name == "pcollections" { if a.Name == "pcollections" {
if metadata.Parent.Name != "micrometer-core" { if metadata.Parent.Name != "micrometer-core" {
t.Errorf("nested 'pcollections' pkg has wrong parent: %q", metadata.Parent.Name) t.Errorf("nested 'pcollections' pkg has wrong parent: %q", metadata.Parent.Name)

View File

@ -4,16 +4,20 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io" "io"
"strconv"
"strings" "strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/mitchellh/mapstructure"
) )
const manifestGlob = "/META-INF/MANIFEST.MF" const manifestGlob = "/META-INF/MANIFEST.MF"
// nolint:funlen // nolint:funlen
func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) { // parseJavaManifest takes MANIFEST.MF file content and returns sections of parsed key/value pairs.
// For more information: https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest
func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error) {
var manifest pkg.JavaManifest var manifest pkg.JavaManifest
sections := []map[string]string{ sections := []map[string]string{
make(map[string]string), make(map[string]string),
@ -63,19 +67,24 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
return nil, fmt.Errorf("unable to read java manifest: %w", err) return nil, fmt.Errorf("unable to read java manifest: %w", err)
} }
if err := mapstructure.Decode(sections[0], &manifest); err != nil { if len(sections) > 0 {
return nil, fmt.Errorf("unable to parse java manifest: %w", err) manifest.Main = sections[0]
} if len(sections) > 1 {
manifest.NamedSections = make(map[string]map[string]string)
// append on extra sections for i, s := range sections[1:] {
if len(sections) > 1 { name, ok := s["Name"]
manifest.Sections = sections[1:] if !ok {
} // per the manifest spec (https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest)
// this should never happen. If it does we want to know about it, but not necessarily stop
// clean select fields // cataloging entirely... for this reason we only log.
if strings.Trim(manifest.ImplVersion, " ") != "" { log.Errorf("java manifest section found without a name: %s", path)
// transform versions with dates attached to just versions (e.g. "1.3 2244 October 5 2008" --> "1.3") name = strconv.Itoa(i)
manifest.ImplVersion = strings.Split(manifest.ImplVersion, " ")[0] } else {
delete(s, "Name")
}
manifest.NamedSections[name] = s
}
}
} }
return &manifest, nil return &manifest, nil
@ -86,24 +95,21 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
switch { switch {
case filenameObj.name() != "": case filenameObj.name() != "":
name = filenameObj.name() name = filenameObj.name()
case manifest.Name != "": case manifest.Main["Name"] != "":
// Manifest original spec... // Manifest original spec...
name = manifest.Name name = manifest.Main["Name"]
case manifest.Extra["Bundle-Name"] != "": case manifest.Main["Bundle-Name"] != "":
// BND tooling... // BND tooling...
name = manifest.Extra["Bundle-Name"] name = manifest.Main["Bundle-Name"]
case manifest.Extra["Short-Name"] != "": case manifest.Main["Short-Name"] != "":
// Jenkins... // Jenkins...
name = manifest.Extra["Short-Name"] name = manifest.Main["Short-Name"]
case manifest.Extra["Extension-Name"] != "": case manifest.Main["Extension-Name"] != "":
// Jenkins... // Jenkins...
name = manifest.Extra["Extension-Name"] name = manifest.Main["Extension-Name"]
} case manifest.Main["Implementation-Title"] != "":
// last ditch effort...
// in situations where we hit this point and no name was name = manifest.Main["Implementation-Title"]
// determined, look at the Implementation-Title
if name == "" && manifest.ImplTitle != "" {
name = manifest.ImplTitle
} }
return name return name
} }
@ -111,14 +117,14 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string { func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
var version string var version string
switch { switch {
case manifest.ImplVersion != "":
version = manifest.ImplVersion
case filenameObj.version() != "": case filenameObj.version() != "":
version = filenameObj.version() version = filenameObj.version()
case manifest.SpecVersion != "": case manifest.Main["Implementation-Version"] != "":
version = manifest.SpecVersion version = manifest.Main["Implementation-Version"]
case manifest.Extra["Plugin-Version"] != "": case manifest.Main["Specification-Version"] != "":
version = manifest.Extra["Plugin-Version"] version = manifest.Main["Specification-Version"]
case manifest.Main["Plugin-Version"] != "":
version = manifest.Main["Plugin-Version"]
} }
return version return version
} }

View File

@ -17,35 +17,39 @@ func TestParseJavaManifest(t *testing.T) {
{ {
fixture: "test-fixtures/manifest/small", fixture: "test-fixtures/manifest/small",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
"Manifest-Version": "1.0",
},
}, },
}, },
{ {
fixture: "test-fixtures/manifest/standard-info", fixture: "test-fixtures/manifest/standard-info",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
Name: "the-best-name", "Name": "the-best-name",
SpecTitle: "the-spec-title", "Manifest-Version": "1.0",
SpecVersion: "the-spec-version", "Specification-Title": "the-spec-title",
SpecVendor: "the-spec-vendor", "Specification-Version": "the-spec-version",
ImplTitle: "the-impl-title", "Specification-Vendor": "the-spec-vendor",
ImplVersion: "the-impl-version", "Implementation-Title": "the-impl-title",
ImplVendor: "the-impl-vendor", "Implementation-Version": "the-impl-version",
"Implementation-Vendor": "the-impl-vendor",
},
}, },
}, },
{ {
fixture: "test-fixtures/manifest/extra-info", fixture: "test-fixtures/manifest/extra-info",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
Extra: map[string]string{ "Manifest-Version": "1.0",
"Archiver-Version": "Plexus Archiver", "Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3", "Created-By": "Apache Maven 3.6.3",
}, },
Sections: []map[string]string{ NamedSections: map[string]map[string]string{
{ "thing-1": {
"Built-By": "?", "Built-By": "?",
}, },
{ "1": {
"Build-Jdk": "14.0.1", "Build-Jdk": "14.0.1",
"Main-Class": "hello.HelloWorld", "Main-Class": "hello.HelloWorld",
}, },
@ -55,17 +59,20 @@ func TestParseJavaManifest(t *testing.T) {
{ {
fixture: "test-fixtures/manifest/continuation", fixture: "test-fixtures/manifest/continuation",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
Extra: map[string]string{ "Manifest-Version": "1.0",
"Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin", "Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin",
}, },
}, },
}, },
{ {
// regression test, we should always keep the full version
fixture: "test-fixtures/manifest/version-with-date", fixture: "test-fixtures/manifest/version-with-date",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
ImplVersion: "1.3", // ensure the date is stripped off during processing "Manifest-Version": "1.0",
"Implementation-Version": "1.3 2244 October 5 2005",
},
}, },
}, },
} }
@ -77,7 +84,7 @@ func TestParseJavaManifest(t *testing.T) {
t.Fatalf("could not open fixture: %+v", err) t.Fatalf("could not open fixture: %+v", err)
} }
actual, err := parseJavaManifest(fixture) actual, err := parseJavaManifest(test.fixture, fixture)
if err != nil { if err != nil {
t.Fatalf("failed to parse manifest: %+v", err) t.Fatalf("failed to parse manifest: %+v", err)
} }
@ -98,3 +105,45 @@ func TestParseJavaManifest(t *testing.T) {
}) })
} }
} }
func TestSelectName(t *testing.T) {
tests := []struct {
desc string
manifest pkg.JavaManifest
archive archiveFilename
expected string
}{
{
desc: "Get name from Implementation-Title",
archive: archiveFilename{},
manifest: pkg.JavaManifest{
Main: map[string]string{
"Implementation-Title": "maven-wrapper",
},
},
expected: "maven-wrapper",
},
{
desc: "Implementation-Title does not override name from filename",
manifest: pkg.JavaManifest{
Main: map[string]string{
"Name": "foo",
"Implementation-Title": "maven-wrapper",
},
},
archive: newJavaArchiveFilename("/something/omg.jar"),
expected: "omg",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
result := selectName(&test.manifest, test.archive)
if result != test.expected {
t.Errorf("mismatch in names: '%s' != '%s'", result, test.expected)
}
})
}
}

View File

@ -2,6 +2,7 @@ Manifest-Version: 1.0
Archiver-Version: Plexus Archiver Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.6.3 Created-By: Apache Maven 3.6.3
Name: thing-1
Built-By: ? Built-By: ?
Build-Jdk: 14.0.1 Build-Jdk: 14.0.1

View File

@ -22,16 +22,8 @@ type PomProperties struct {
// JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file. // JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file.
type JavaManifest struct { type JavaManifest struct {
Name string `mapstructure:"Name" json:"name"` Main map[string]string `json:"main,omitempty"`
ManifestVersion string `mapstructure:"Manifest-Version" json:"manifestVersion"` NamedSections map[string]map[string]string `json:"namedSections,omitempty"`
SpecTitle string `mapstructure:"Specification-Title" json:"specificationTitle"`
SpecVersion string `mapstructure:"Specification-Version" json:"specificationVersion"`
SpecVendor string `mapstructure:"Specification-Vendor" json:"specificationVendor"`
ImplTitle string `mapstructure:"Implementation-Title" json:"implementationTitle"`
ImplVersion string `mapstructure:"Implementation-Version" json:"implementationVersion"`
ImplVendor string `mapstructure:"Implementation-Vendor" json:"implementationVendor"`
Extra map[string]string `mapstructure:",remain" json:"extraFields"`
Sections []map[string]string `json:"sections,omitempty"`
} }
func (m JavaMetadata) PackageURL() string { func (m JavaMetadata) PackageURL() string {

View File

@ -1,8 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -eu set -eu
# TODO: Add "alpine:3.12.0" back in when we've figured out how to handle the apk version field w/ and w/o release information (see issue: https://github.com/anchore/syft/pull/195) images=("debian:10.5" "centos:8.2.2004" "rails:5.0.1" "alpine:3.12.0" "anchore/test_images:java" "anchore/test_images:py38" "anchore/anchore-engine:v0.8.2" "jenkins/jenkins:2.249.2-lts-jdk11" )
images=("debian:10.5" "centos:8.2.2004" "rails:5.0.1")
# gather all image analyses # gather all image analyses
for img in "${images[@]}"; do for img in "${images[@]}"; do

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import sys import sys
import difflib
import collections import collections
import utils.package import utils.package
@ -8,9 +9,15 @@ from utils.format import Colors, print_rows
from utils.inline import InlineScan from utils.inline import InlineScan
from utils.syft import Syft from utils.syft import Syft
QUALITY_GATE_THRESHOLD = 0.95 DEFAULT_QUALITY_GATE_THRESHOLD = 0.95
INDENT = " " INDENT = " "
IMAGE_QUALITY_GATE = collections.defaultdict(lambda: QUALITY_GATE_THRESHOLD, **{})
PACKAGE_QUALITY_GATE = collections.defaultdict(lambda: DEFAULT_QUALITY_GATE_THRESHOLD, **{})
METADATA_QUALITY_GATE = collections.defaultdict(lambda: DEFAULT_QUALITY_GATE_THRESHOLD, **{
# syft is better at detecting package versions in specific cases, leading to a drop in matching metadata
"anchore/test_images:java": 0.61,
"jenkins/jenkins:2.249.2-lts-jdk11": 0.82,
})
# We additionally fail if an image is above a particular threshold. Why? We expect the lower threshold to be 90%, # We additionally fail if an image is above a particular threshold. Why? We expect the lower threshold to be 90%,
# however additional functionality in grype is still being implemented, so this threshold may not be able to be met. # however additional functionality in grype is still being implemented, so this threshold may not be able to be met.
@ -18,10 +25,15 @@ IMAGE_QUALITY_GATE = collections.defaultdict(lambda: QUALITY_GATE_THRESHOLD, **{
# issues/enhancements are done we want to ensure that the lower threshold is bumped up to catch regression. The only way # issues/enhancements are done we want to ensure that the lower threshold is bumped up to catch regression. The only way
# to do this is to select an upper threshold for images with known threshold values, so we have a failure that # to do this is to select an upper threshold for images with known threshold values, so we have a failure that
# loudly indicates the lower threshold should be bumped. # loudly indicates the lower threshold should be bumped.
IMAGE_UPPER_THRESHOLD = collections.defaultdict(lambda: 1, **{}) PACKAGE_UPPER_THRESHOLD = collections.defaultdict(lambda: 1, **{})
METADATA_UPPER_THRESHOLD = collections.defaultdict(lambda: 1, **{
# syft is better at detecting package versions in specific cases, leading to a drop in matching metadata
"anchore/test_images:java": 0.65,
"jenkins/jenkins:2.249.2-lts-jdk11": 0.84,
})
def report(analysis): def report(image, analysis):
if analysis.extra_packages: if analysis.extra_packages:
rows = [] rows = []
print( print(
@ -47,7 +59,6 @@ def report(analysis):
print() print()
if analysis.missing_metadata: if analysis.missing_metadata:
rows = []
print( print(
Colors.bold + "Syft mismatched metadata:", Colors.bold + "Syft mismatched metadata:",
Colors.reset, Colors.reset,
@ -58,25 +69,17 @@ def report(analysis):
if pkg not in analysis.syft_data.metadata[pkg.type]: if pkg not in analysis.syft_data.metadata[pkg.type]:
continue continue
syft_metadata_item = analysis.syft_data.metadata[pkg.type][pkg] syft_metadata_item = analysis.syft_data.metadata[pkg.type][pkg]
rows.append(
[ diffs = difflib.ndiff([repr(syft_metadata_item)], [repr(metadata)])
INDENT,
"for:", print(INDENT + "for: " + repr(pkg), "(top is syft, bottom is inline)")
repr(pkg), print(INDENT+INDENT+("\n"+INDENT+INDENT).join(list(diffs)))
":",
repr(syft_metadata_item), if not analysis.missing_metadata:
"!=",
repr(metadata),
]
)
if rows:
print_rows(rows)
else:
print( print(
INDENT, INDENT,
"There are mismatches, but only due to packages Syft did not find (but inline did).", "There are mismatches, but only due to packages Syft did not find (but inline did).\n",
) )
print()
if analysis.similar_missing_packages: if analysis.similar_missing_packages:
rows = [] rows = []
@ -97,7 +100,9 @@ def report(analysis):
print_rows(rows) print_rows(rows)
print() print()
if analysis.unmatched_missing_packages and analysis.extra_packages: show_probable_mismatches = analysis.unmatched_missing_packages and analysis.extra_packages and len(analysis.unmatched_missing_packages) != len(analysis.missing_packages)
if show_probable_mismatches:
rows = [] rows = []
print( print(
Colors.bold + "Probably missed packages:", Colors.bold + "Probably missed packages:",
@ -109,17 +114,17 @@ def report(analysis):
print_rows(rows) print_rows(rows)
print() print()
print(Colors.bold + "Summary:", Colors.reset) print(Colors.bold + "Summary:", Colors.reset, image)
print(" Inline Packages : %d" % len(analysis.inline_data.packages)) print(" Inline Packages : %d" % len(analysis.inline_data.packages))
print(" Syft Packages : %d" % len(analysis.syft_data.packages)) print(" Syft Packages : %d" % len(analysis.syft_data.packages))
print( print(
" (extra) : %d (note: this is ignored in the analysis!)" " (extra) : %d (note: this is ignored by the quality gate!)"
% len(analysis.extra_packages) % len(analysis.extra_packages)
) )
print(" (missing) : %d" % len(analysis.missing_packages)) print(" (missing) : %d" % len(analysis.missing_packages))
print() print()
if analysis.unmatched_missing_packages and analysis.extra_packages: if show_probable_mismatches:
print( print(
" Probable Package Matches : %d (matches not made, but were probably found by both Inline and Syft)" " Probable Package Matches : %d (matches not made, but were probably found by both Inline and Syft)"
% len(analysis.similar_missing_packages) % len(analysis.similar_missing_packages)
@ -155,12 +160,37 @@ def report(analysis):
) )
) )
overall_score = (
analysis.percent_overlapping_packages + analysis.percent_overlapping_metadata
) / 2.0
print(Colors.bold + " Overall Score: %2.1f %%" % overall_score, Colors.reset) def enforce_quality_gate(title, actual_value, lower_gate_value, upper_gate_value):
if actual_value < lower_gate_value:
print(
Colors.bold
+ " %s Quality Gate:\t" % title
+ Colors.FG.red
+ "FAIL (is not >= %d %%)" % lower_gate_value,
Colors.reset,
)
return False
elif actual_value > upper_gate_value:
print(
Colors.bold
+ " %s Quality Gate:\t" % title
+ Colors.FG.orange
+ "FAIL (lower threshold is artificially low and should be updated)",
Colors.reset,
)
return False
print(
Colors.bold
+ " %s Quality Gate:\t" % title
+ Colors.FG.green
+ "Pass (>= %d %%)" % lower_gate_value,
Colors.reset,
)
return True
def main(image): def main(image):
cwd = os.path.dirname(os.path.abspath(__file__)) cwd = os.path.dirname(os.path.abspath(__file__))
@ -175,41 +205,27 @@ def main(image):
) )
# show some useful report data for debugging / warm fuzzies # show some useful report data for debugging / warm fuzzies
report(analysis) report(image, analysis)
# enforce a quality gate based on the comparison of package values and metadata values # enforce a quality gate based on the comparison of package values and metadata values
upper_gate_value = IMAGE_UPPER_THRESHOLD[image] * 100 success = True
lower_gate_value = IMAGE_QUALITY_GATE[image] * 100 success &= enforce_quality_gate(
if analysis.quality_gate_score < lower_gate_value: title="Package",
print( actual_value=analysis.percent_overlapping_packages,
Colors.bold lower_gate_value=PACKAGE_QUALITY_GATE[image] * 100,
+ " Quality Gate: " upper_gate_value=PACKAGE_UPPER_THRESHOLD[image] * 100
+ Colors.FG.red )
+ "FAILED (is not >= %d %%)\n" % lower_gate_value, success &= enforce_quality_gate(
Colors.reset, title="Metadata",
) actual_value=analysis.percent_overlapping_metadata,
return 1 lower_gate_value=METADATA_QUALITY_GATE[image] * 100,
elif analysis.quality_gate_score > upper_gate_value: upper_gate_value=METADATA_UPPER_THRESHOLD[image] * 100
print( )
Colors.bold
+ " Quality Gate: "
+ Colors.FG.orange
+ "FAILED (lower threshold is artificially low and should be updated)\n",
Colors.reset,
)
return 1
else:
print(
Colors.bold
+ " Quality Gate: "
+ Colors.FG.green
+ "pass (>= %d %%)\n" % lower_gate_value,
Colors.reset,
)
if not success:
return 1
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) != 2: if len(sys.argv) != 2:
sys.exit("provide an image") sys.exit("provide an image")

View File

@ -1,4 +1,5 @@
import os import os
import re
import json import json
import collections import collections
@ -66,13 +67,23 @@ class InlineScan:
elif pkg_type in ("java-jpi", "java-hpi"): elif pkg_type in ("java-jpi", "java-hpi"):
pkg_type = "java-?pi" pkg_type = "java-?pi"
# this would usually be "package" but this would not be able to account for duplicate dependencies in
# nested jars of the same name. Fallback to the package name if there is no given location
name = entry["location"]
# replace fields with "N/A" with None
for k, v in dict(entry).items():
if v in ("", "N/A"):
entry[k] = None
pkg = utils.package.Package( pkg = utils.package.Package(
name=entry["package"], name=name,
type=pkg_type, type=pkg_type,
) )
packages.add(pkg) packages.add(pkg)
metadata[pkg.type][pkg] = utils.package.Metadata( metadata[pkg.type][pkg] = utils.package.Metadata(
version=entry["maven-version"] version=entry["maven-version"],
) )
return packages, metadata return packages, metadata

View File

@ -144,13 +144,3 @@ class Analysis:
float(len(self.overlapping_packages) + len(self.similar_missing_packages)) float(len(self.overlapping_packages) + len(self.similar_missing_packages))
/ float(len(self.inline_data.packages)) / float(len(self.inline_data.packages))
) * 100.0 ) * 100.0
@property
def quality_gate_score(self):
"""
The result of the analysis in the form of an aggregated percentage; it is up to the caller to use this value
and enforce a quality gate.
"""
return (
self.percent_overlapping_packages + self.percent_overlapping_metadata
) / 2.0

View File

@ -43,12 +43,26 @@ class Syft:
elif pkg_type in ("apk",): elif pkg_type in ("apk",):
pkg_type = "apkg" pkg_type = "apkg"
name = entry["name"]
version = entry["version"]
if "java" in pkg_type:
# we need to use the virtual path instead of the name to account for nested dependencies with the same
# package name (but potentially different metadata)
name = entry.get("metadata", {}).get("virtualPath")
elif pkg_type == "apkg":
# inline scan strips off the release from the version, which should be normalized here
fields = entry["version"].split("-")
version = "-".join(fields[:-1])
pkg = utils.package.Package( pkg = utils.package.Package(
name=entry["name"], name=name,
type=pkg_type, type=pkg_type,
) )
packages.add(pkg) packages.add(pkg)
metadata[pkg.type][pkg] = utils.package.Metadata(version=entry["version"])
metadata[pkg.type][pkg] = utils.package.Metadata(version=version)
return utils.package.Info(packages=frozenset(packages), metadata=metadata) return utils.package.Info(packages=frozenset(packages), metadata=metadata)