diff --git a/schema/json/schema.json b/schema/json/schema.json index 370f9c27d..a908a335e 100644 --- a/schema/json/schema.json +++ b/schema/json/schema.json @@ -126,7 +126,7 @@ }, "manifest": { "properties": { - "extraFields": { + "main": { "properties": { "Archiver-Version": { "type": "string" @@ -149,6 +149,12 @@ "Hudson-Version": { "type": "string" }, + "Implementation-Title": { + "type": "string" + }, + "Implementation-Version": { + "type": "string" + }, "Jenkins-Version": { "type": "string" }, @@ -158,6 +164,9 @@ "Main-Class": { "type": "string" }, + "Manifest-Version": { + "type": "string" + }, "Minimum-Java-Version": { "type": "string" }, @@ -181,32 +190,23 @@ }, "Short-Name": { "type": "string" + }, + "Specification-Title": { + "type": "string" } }, "required": [ "Archiver-Version", "Build-Jdk", "Built-By", - "Created-By" + "Created-By", + "Manifest-Version" ], "type": "object" - }, - "implementationTitle": { - "type": "string" - }, - "implementationVersion": { - "type": "string" - }, - "manifestVersion": { - "type": "string" - }, - "specificationTitle": { - "type": "string" } }, "required": [ - "extraFields", - "manifestVersion" + "main" ], "type": "object" }, diff --git a/syft/cataloger/java/archive_parser.go b/syft/cataloger/java/archive_parser.go index ca0f13dc2..fa9e619cd 100644 --- a/syft/cataloger/java/archive_parser.go +++ b/syft/cataloger/java/archive_parser.go @@ -136,7 +136,7 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) { // parse the manifest file into a rich object manifestContents := contents[manifestMatches[0]] - manifest, err := parseJavaManifest(strings.NewReader(manifestContents)) + manifest, err := parseJavaManifest(j.archivePath, strings.NewReader(manifestContents)) if err != nil { return nil, fmt.Errorf("failed to parse java manifest (%s): %w", j.virtualPath, err) } diff --git a/syft/cataloger/java/archive_parser_test.go b/syft/cataloger/java/archive_parser_test.go index e93374cfc..9da709902 100644 --- a/syft/cataloger/java/archive_parser_test.go +++ b/syft/cataloger/java/archive_parser_test.go @@ -89,18 +89,19 @@ func TestSelectName(t *testing.T) { desc: "name from Implementation-Title", archive: archiveFilename{}, manifest: pkg.JavaManifest{ - Name: "", - SpecTitle: "", - ImplTitle: "maven-wrapper", + Main: map[string]string{ + "Implementation-Title": "maven-wrapper", + }, }, expected: "maven-wrapper", }, { desc: "Implementation-Title does not override", manifest: pkg.JavaManifest{ - Name: "Foo", - SpecTitle: "", - ImplTitle: "maven-wrapper", + Main: map[string]string{ + "Name": "foo", + "Implementation-Title": "maven-wrapper", + }, }, archive: archiveFilename{ fields: []map[string]string{ @@ -145,11 +146,12 @@ func TestParseJar(t *testing.T) { Metadata: pkg.JavaMetadata{ VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi", Manifest: &pkg.JavaManifest{ - ManifestVersion: "1.0", - SpecTitle: "The Jenkins Plugins Parent POM Project", - ImplTitle: "example-jenkins-plugin", - ImplVersion: "1.0-SNAPSHOT", - Extra: map[string]string{ + Main: map[string]string{ + "Manifest-Version": "1.0", + "Specification-Title": "The Jenkins Plugins Parent POM Project", + "Implementation-Title": "example-jenkins-plugin", + "Implementation-Version": "1.0-SNAPSHOT", + // extra fields... "Archiver-Version": "Plexus Archiver", "Plugin-License-Url": "https://opensource.org/licenses/MIT", "Plugin-License-Name": "MIT License", @@ -191,7 +193,9 @@ func TestParseJar(t *testing.T) { Metadata: pkg.JavaMetadata{ VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar", Manifest: &pkg.JavaManifest{ - ManifestVersion: "1.0", + Main: map[string]string{ + "Manifest-Version": "1.0", + }, }, }, }, @@ -212,8 +216,9 @@ func TestParseJar(t *testing.T) { Metadata: pkg.JavaMetadata{ VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", Manifest: &pkg.JavaManifest{ - ManifestVersion: "1.0", - Extra: map[string]string{ + Main: map[string]string{ + "Manifest-Version": "1.0", + // extra fields... "Archiver-Version": "Plexus Archiver", "Created-By": "Apache Maven 3.6.3", "Built-By": "?", @@ -305,11 +310,11 @@ func TestParseJar(t *testing.T) { metadata := a.Metadata.(pkg.JavaMetadata) metadata.Parent = nil - // ignore select fields + // ignore select fields (only works for the main section) for _, field := range test.ignoreExtras { - if metadata.Manifest != nil && metadata.Manifest.Extra != nil { - if _, ok := metadata.Manifest.Extra[field]; ok { - delete(metadata.Manifest.Extra, field) + if metadata.Manifest != nil && metadata.Manifest.Main != nil { + if _, ok := metadata.Manifest.Main[field]; ok { + delete(metadata.Manifest.Main, field) } } } diff --git a/syft/cataloger/java/java_manifest.go b/syft/cataloger/java/java_manifest.go index bb08062fe..c4ca715e2 100644 --- a/syft/cataloger/java/java_manifest.go +++ b/syft/cataloger/java/java_manifest.go @@ -4,16 +4,20 @@ import ( "bufio" "fmt" "io" + "strconv" "strings" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" - "github.com/mitchellh/mapstructure" ) const manifestGlob = "/META-INF/MANIFEST.MF" // 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 sections := []map[string]string{ make(map[string]string), @@ -63,13 +67,24 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) { return nil, fmt.Errorf("unable to read java manifest: %w", err) } - if err := mapstructure.Decode(sections[0], &manifest); err != nil { - return nil, fmt.Errorf("unable to parse java manifest: %w", err) - } - - // append on extra sections - if len(sections) > 1 { - manifest.Sections = sections[1:] + if len(sections) > 0 { + manifest.Main = sections[0] + if len(sections) > 1 { + manifest.Sections = make(map[string]map[string]string) + for i, s := range sections[1:] { + name, ok := s["Name"] + 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 + // cataloging entirely... for this reason we only log. + log.Errorf("java manifest section found without a name: %s", path) + name = strconv.Itoa(i) + } else { + delete(s, "Name") + } + manifest.Sections[name] = s + } + } } return &manifest, nil @@ -80,24 +95,21 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string switch { case filenameObj.name() != "": name = filenameObj.name() - case manifest.Name != "": + case manifest.Main["Name"] != "": // Manifest original spec... - name = manifest.Name - case manifest.Extra["Bundle-Name"] != "": + name = manifest.Main["Name"] + case manifest.Main["Bundle-Name"] != "": // BND tooling... - name = manifest.Extra["Bundle-Name"] - case manifest.Extra["Short-Name"] != "": + name = manifest.Main["Bundle-Name"] + case manifest.Main["Short-Name"] != "": // Jenkins... - name = manifest.Extra["Short-Name"] - case manifest.Extra["Extension-Name"] != "": + name = manifest.Main["Short-Name"] + case manifest.Main["Extension-Name"] != "": // Jenkins... - name = manifest.Extra["Extension-Name"] - } - - // in situations where we hit this point and no name was - // determined, look at the Implementation-Title - if name == "" && manifest.ImplTitle != "" { - name = manifest.ImplTitle + name = manifest.Main["Extension-Name"] + case manifest.Main["Implementation-Title"] != "": + // last ditch effort... + name = manifest.Main["Implementation-Title"] } return name } @@ -105,14 +117,14 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string { var version string switch { - case manifest.ImplVersion != "": - version = manifest.ImplVersion + case manifest.Main["Implementation-Version"] != "": + version = manifest.Main["Implementation-Version"] case filenameObj.version() != "": version = filenameObj.version() - case manifest.SpecVersion != "": - version = manifest.SpecVersion - case manifest.Extra["Plugin-Version"] != "": - version = manifest.Extra["Plugin-Version"] + case manifest.Main["Specification-Version"] != "": + version = manifest.Main["Specification-Version"] + case manifest.Main["Plugin-Version"] != "": + version = manifest.Main["Plugin-Version"] } return version } diff --git a/syft/cataloger/java/java_manifest_test.go b/syft/cataloger/java/java_manifest_test.go index 00e2560bf..fd6d096fb 100644 --- a/syft/cataloger/java/java_manifest_test.go +++ b/syft/cataloger/java/java_manifest_test.go @@ -17,35 +17,39 @@ func TestParseJavaManifest(t *testing.T) { { fixture: "test-fixtures/manifest/small", expected: pkg.JavaManifest{ - ManifestVersion: "1.0", + Main: map[string]string{ + "Manifest-Version": "1.0", + }, }, }, { fixture: "test-fixtures/manifest/standard-info", expected: pkg.JavaManifest{ - ManifestVersion: "1.0", - Name: "the-best-name", - SpecTitle: "the-spec-title", - SpecVersion: "the-spec-version", - SpecVendor: "the-spec-vendor", - ImplTitle: "the-impl-title", - ImplVersion: "the-impl-version", - ImplVendor: "the-impl-vendor", + Main: map[string]string{ + "Name": "the-best-name", + "Manifest-Version": "1.0", + "Specification-Title": "the-spec-title", + "Specification-Version": "the-spec-version", + "Specification-Vendor": "the-spec-vendor", + "Implementation-Title": "the-impl-title", + "Implementation-Version": "the-impl-version", + "Implementation-Vendor": "the-impl-vendor", + }, }, }, { fixture: "test-fixtures/manifest/extra-info", expected: pkg.JavaManifest{ - ManifestVersion: "1.0", - Extra: map[string]string{ + Main: map[string]string{ + "Manifest-Version": "1.0", "Archiver-Version": "Plexus Archiver", "Created-By": "Apache Maven 3.6.3", }, - Sections: []map[string]string{ - { + Sections: map[string]map[string]string{ + "thing-1": { "Built-By": "?", }, - { + "2": { "Build-Jdk": "14.0.1", "Main-Class": "hello.HelloWorld", }, @@ -55,9 +59,9 @@ func TestParseJavaManifest(t *testing.T) { { fixture: "test-fixtures/manifest/continuation", expected: pkg.JavaManifest{ - ManifestVersion: "1.0", - Extra: map[string]string{ - "Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin", + Main: map[string]string{ + "Manifest-Version": "1.0", + "Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin", }, }, }, @@ -65,8 +69,10 @@ func TestParseJavaManifest(t *testing.T) { // regression test, we should always keep the full version fixture: "test-fixtures/manifest/version-with-date", expected: pkg.JavaManifest{ - ManifestVersion: "1.0", - ImplVersion: "1.3 2244 October 5 2005", + Main: map[string]string{ + "Manifest-Version": "1.0", + "Implementation-Version": "1.3 2244 October 5 2005", + }, }, }, } @@ -78,7 +84,7 @@ func TestParseJavaManifest(t *testing.T) { t.Fatalf("could not open fixture: %+v", err) } - actual, err := parseJavaManifest(fixture) + actual, err := parseJavaManifest(test.fixture, fixture) if err != nil { t.Fatalf("failed to parse manifest: %+v", err) } diff --git a/syft/cataloger/java/test-fixtures/manifest/extra-info b/syft/cataloger/java/test-fixtures/manifest/extra-info index 8938f487c..c1fa40e5c 100644 --- a/syft/cataloger/java/test-fixtures/manifest/extra-info +++ b/syft/cataloger/java/test-fixtures/manifest/extra-info @@ -2,6 +2,7 @@ Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Created-By: Apache Maven 3.6.3 +Name: thing-1 Built-By: ? Build-Jdk: 14.0.1 diff --git a/syft/pkg/java_metadata.go b/syft/pkg/java_metadata.go index 3aa6fcb20..a4a9c7578 100644 --- a/syft/pkg/java_metadata.go +++ b/syft/pkg/java_metadata.go @@ -22,16 +22,8 @@ type PomProperties struct { // JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file. type JavaManifest struct { - Name string `mapstructure:"Name" json:"name,omitempty"` - ManifestVersion string `mapstructure:"Manifest-Version" json:"manifestVersion,omitempty"` - SpecTitle string `mapstructure:"Specification-Title" json:"specificationTitle,omitempty"` - SpecVersion string `mapstructure:"Specification-Version" json:"specificationVersion,omitempty"` - SpecVendor string `mapstructure:"Specification-Vendor" json:"specificationVendor,omitempty"` - ImplTitle string `mapstructure:"Implementation-Title" json:"implementationTitle,omitempty"` - ImplVersion string `mapstructure:"Implementation-Version" json:"implementationVersion,omitempty"` - ImplVendor string `mapstructure:"Implementation-Vendor" json:"implementationVendor,omitempty"` - Extra map[string]string `mapstructure:",remain" json:"extraFields,omitempty"` - Sections []map[string]string `json:"sections,omitempty"` + Main map[string]string `json:"main,omitempty"` + Sections map[string]map[string]string `json:"sections,omitempty"` } func (m JavaMetadata) PackageURL() string {