diff --git a/internal/constants.go b/internal/constants.go index 6d1dd197439..93b0093ab6a 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -3,5 +3,5 @@ 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.0.15" + JSONSchemaVersion = "16.0.16" ) diff --git a/internal/relationship/exclude_binaries_by_file_ownership_overlap.go b/internal/relationship/exclude_binaries_by_file_ownership_overlap.go index 9733c00e717..2d9b6052580 100644 --- a/internal/relationship/exclude_binaries_by_file_ownership_overlap.go +++ b/internal/relationship/exclude_binaries_by_file_ownership_overlap.go @@ -25,55 +25,102 @@ var ( binaryMetadataTypes = []string{ reflect.TypeOf(pkg.ELFBinaryPackageNoteJSONPayload{}).Name(), reflect.TypeOf(pkg.BinarySignature{}).Name(), + reflect.TypeOf(pkg.JvmInstallation{}).Name(), } ) func ExcludeBinariesByFileOwnershipOverlap(accessor sbomsync.Accessor) { accessor.WriteToSBOM(func(s *sbom.SBOM) { for _, r := range s.Relationships { - if excludeBinaryByFileOwnershipOverlap(r, s.Artifacts.Packages) { - s.Artifacts.Packages.Delete(r.To.ID()) - s.Relationships = RemoveRelationshipsByID(s.Relationships, r.To.ID()) + if idToRemove := excludeByFileOwnershipOverlap(r, s.Artifacts.Packages); idToRemove != "" { + s.Artifacts.Packages.Delete(idToRemove) + s.Relationships = RemoveRelationshipsByID(s.Relationships, idToRemove) } } }) } -// excludeBinaryByFileOwnershipOverlap will remove packages from a collection given the following properties are true -// 1) the relationship between packages is OwnershipByFileOverlap -// 2) the parent is an "os" package -// 3) the child is a synthetic package generated by the binary cataloger -// 4) the package names are identical -// This was implemented as a way to help resolve: https://github.com/anchore/syft/issues/931 -func excludeBinaryByFileOwnershipOverlap(r artifact.Relationship, c *pkg.Collection) bool { +// excludeByFileOwnershipOverlap will remove packages that should be overridden by a more authoritative package, +// such as an OS package or a package from a cataloger with more specific information being raised up. +func excludeByFileOwnershipOverlap(r artifact.Relationship, c *pkg.Collection) artifact.ID { if artifact.OwnershipByFileOverlapRelationship != r.Type { - return false + return "" } parent := c.Package(r.From.ID()) if parent == nil { - return false - } - - parentInExclusion := slices.Contains(osCatalogerTypes, parent.Type) - if !parentInExclusion { - return false + return "" } child := c.Package(r.To.ID()) if child == nil { - return false + return "" + } + + if idToRemove := identifyOverlappingOSRelationship(parent, child); idToRemove != "" { + return idToRemove + } + + if idToRemove := identifyOverlappingJVMRelationship(parent, child); idToRemove != "" { + return idToRemove + } + + return "" +} + +// identifyOverlappingJVMRelationship indicates the package to remove if this is a binary -> binary pkg relationship +// with a java binary signature package and a more authoritative JVM release package. +func identifyOverlappingJVMRelationship(parent *pkg.Package, child *pkg.Package) artifact.ID { + if !slices.Contains(binaryCatalogerTypes, parent.Type) { + return "" + } + + if !slices.Contains(binaryCatalogerTypes, child.Type) { + return "" + } + + if child.Metadata == nil { + return "" + } + + var ( + foundJVM bool + idToRemove artifact.ID + ) + for _, p := range []*pkg.Package{parent, child} { + switch p.Metadata.(type) { + case pkg.JvmInstallation: + foundJVM = true + default: + idToRemove = p.ID() + } + } + + if foundJVM { + return idToRemove + } + + return "" +} + +// identifyOverlappingOSRelationship indicates the package ID to remove if this is an OS pkg -> bin pkg relationship. +// This was implemented as a way to help resolve: https://github.com/anchore/syft/issues/931 +func identifyOverlappingOSRelationship(parent *pkg.Package, child *pkg.Package) artifact.ID { + if !slices.Contains(osCatalogerTypes, parent.Type) { + return "" } if slices.Contains(binaryCatalogerTypes, child.Type) { - return true + return child.ID() } if child.Metadata == nil { - return false + return "" } - childMetadataType := reflect.TypeOf(child.Metadata) + if !slices.Contains(binaryMetadataTypes, reflect.TypeOf(child.Metadata).Name()) { + return "" + } - return slices.Contains(binaryMetadataTypes, childMetadataType.Name()) + return child.ID() } diff --git a/internal/relationship/exclude_binaries_by_file_ownership_overlap_test.go b/internal/relationship/exclude_binaries_by_file_ownership_overlap_test.go index e8347937bc2..95a0768fb4c 100644 --- a/internal/relationship/exclude_binaries_by_file_ownership_overlap_test.go +++ b/internal/relationship/exclude_binaries_by_file_ownership_overlap_test.go @@ -3,18 +3,17 @@ package relationship import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" ) -func TestExclude(t *testing.T) { +func TestExcludeByFileOwnershipOverlap(t *testing.T) { packageA := pkg.Package{Name: "package-a", Type: pkg.ApkPkg} - packageB := pkg.Package{Name: "package-a", Type: pkg.PythonPkg} - packageC := pkg.Package{Name: "package-a", Type: pkg.BinaryPkg} - packageD := pkg.Package{Name: "package-d", Type: pkg.BinaryPkg} - packageE := pkg.Package{Name: "package-e", Type: pkg.RpmPkg, Metadata: pkg.ELFBinaryPackageNoteJSONPayload{Type: "rpm"}} - packageF := pkg.Package{Name: "package-f", Type: pkg.RpmPkg, Metadata: pkg.BinarySignature{}} - for _, p := range []*pkg.Package{&packageA, &packageB, &packageC, &packageD, &packageE, &packageF} { + packageB := pkg.Package{Name: "package-b", Type: pkg.BinaryPkg, Metadata: pkg.JvmInstallation{}} + packageC := pkg.Package{Name: "package-c", Type: pkg.BinaryPkg, Metadata: pkg.ELFBinaryPackageNoteJSONPayload{Type: "rpm"}} + for _, p := range []*pkg.Package{&packageA, &packageB, &packageC} { p := p p.SetID() } @@ -26,73 +25,152 @@ func TestExclude(t *testing.T) { shouldExclude bool }{ { - name: "no exclusions from os -> python", - relationship: artifact.Relationship{ - Type: artifact.OwnershipByFileOverlapRelationship, - From: packageA, - To: packageB, - }, - packages: pkg.NewCollection(packageA, packageB), - shouldExclude: false, - }, - { - name: "exclusions from os -> binary", + // prove that OS -> bin exclusions are wired + name: "exclusions from os -> elf binary (as RPM)", relationship: artifact.Relationship{ Type: artifact.OwnershipByFileOverlapRelationship, - From: packageA, - To: packageC, + From: packageA, // OS + To: packageC, // ELF binary }, packages: pkg.NewCollection(packageA, packageC), shouldExclude: true, }, { - name: "exclusions from os -> elf binary (as RPM)", + // prove that bin -> JVM exclusions are wired + name: "exclusions from binary -> binary with JVM metadata", relationship: artifact.Relationship{ Type: artifact.OwnershipByFileOverlapRelationship, - From: packageA, - To: packageE, + From: packageB, // binary with JVM metadata + To: packageC, // binary }, - packages: pkg.NewCollection(packageA, packageE), + packages: pkg.NewCollection(packageC, packageB), shouldExclude: true, }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actualExclude := excludeByFileOwnershipOverlap(test.relationship, test.packages) + didExclude := actualExclude != "" + if !didExclude && test.shouldExclude { + t.Errorf("expected to exclude relationship %+v", test.relationship) + } + }) + + } +} + +func TestIdentifyOverlappingOSRelationship(t *testing.T) { + packageA := pkg.Package{Name: "package-a", Type: pkg.ApkPkg} // OS package + packageB := pkg.Package{Name: "package-b", Type: pkg.BinaryPkg} + packageC := pkg.Package{Name: "package-c", Type: pkg.BinaryPkg, Metadata: pkg.BinarySignature{}} + packageD := pkg.Package{Name: "package-d", Type: pkg.PythonPkg} // Language package + packageE := pkg.Package{Name: "package-e", Type: pkg.BinaryPkg, Metadata: pkg.ELFBinaryPackageNoteJSONPayload{}} + + for _, p := range []*pkg.Package{&packageA, &packageB, &packageC, &packageD, &packageE} { + p.SetID() + } + + tests := []struct { + name string + parent *pkg.Package + child *pkg.Package + expectedID artifact.ID + }{ { - name: "exclusions from os -> binary (masquerading as RPM)", - relationship: artifact.Relationship{ - Type: artifact.OwnershipByFileOverlapRelationship, - From: packageA, - To: packageF, - }, - packages: pkg.NewCollection(packageA, packageF), - shouldExclude: true, + name: "OS -> binary without metadata", + parent: &packageA, + child: &packageB, + expectedID: packageB.ID(), // OS package to binary package, should return child ID }, { - name: "no exclusions from python -> binary", - relationship: artifact.Relationship{ - Type: artifact.OwnershipByFileOverlapRelationship, - From: packageB, - To: packageC, - }, - packages: pkg.NewCollection(packageB, packageC), - shouldExclude: false, + name: "OS -> binary with binary metadata", + parent: &packageA, + child: &packageC, + expectedID: packageC.ID(), // OS package to binary package with binary metadata, should return child ID }, { - name: "no exclusions for different package names", - relationship: artifact.Relationship{ - Type: artifact.OwnershipByFileOverlapRelationship, - From: packageA, - To: packageD, - }, - packages: pkg.NewCollection(packageA, packageD), - shouldExclude: false, + name: "OS -> non-binary package", + parent: &packageA, + child: &packageD, + expectedID: "", // OS package to non-binary package, no exclusion + }, + { + name: "OS -> binary with ELF metadata", + parent: &packageA, + child: &packageE, + expectedID: packageE.ID(), // OS package to binary package with ELF metadata, should return child ID + }, + { + name: "non-OS parent", + parent: &packageD, // non-OS package + child: &packageC, + expectedID: "", // non-OS parent, no exclusion }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if !excludeBinaryByFileOwnershipOverlap(test.relationship, test.packages) && test.shouldExclude { - t.Errorf("expected to exclude relationship %+v", test.relationship) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resultID := identifyOverlappingOSRelationship(tt.parent, tt.child) + assert.Equal(t, tt.expectedID, resultID) }) + } +} + +func TestIdentifyOverlappingJVMRelationship(t *testing.T) { + + packageA := pkg.Package{Name: "package-a", Type: pkg.BinaryPkg} + packageB := pkg.Package{Name: "package-b", Type: pkg.BinaryPkg, Metadata: pkg.BinarySignature{}} + packageC := pkg.Package{Name: "package-c", Type: pkg.BinaryPkg, Metadata: pkg.JvmInstallation{}} + packageD := pkg.Package{Name: "package-d", Type: pkg.PythonPkg} + packageE := pkg.Package{Name: "package-e", Type: pkg.BinaryPkg} + + for _, p := range []*pkg.Package{&packageA, &packageB, &packageC, &packageD, &packageE} { + p.SetID() + } + + tests := []struct { + name string + parent *pkg.Package + child *pkg.Package + expectedID artifact.ID + }{ + { + name: "binary -> binary with JVM installation", + parent: &packageA, + child: &packageC, + expectedID: packageA.ID(), // JVM found, return BinaryPkg ID + }, + { + name: "binary -> binary with binary signature", + parent: &packageA, + child: &packageB, + expectedID: "", // binary signatures only found, no exclusion + }, + { + name: "binary -> python (non-binary child)", + parent: &packageA, + child: &packageD, + expectedID: "", // non-binary child, no exclusion + }, + { + name: "no JVM or signature in binary -> binary", + parent: &packageA, + child: &packageE, + expectedID: "", // no JVM or binary signature, no exclusion + }, + { + name: "non-binary parent", + parent: &packageD, + child: &packageC, + expectedID: "", // non-binary parent, no exclusion + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resultID := identifyOverlappingJVMRelationship(tt.parent, tt.child) + assert.Equal(t, tt.expectedID, resultID) + }) } } diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index 88ce35345b4..fbe76324a89 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -125,6 +125,7 @@ func DefaultPackageTaskFactories() PackageTaskFactories { newSimplePackageTaskFactory(binary.NewELFPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "elf-package"), newSimplePackageTaskFactory(githubactions.NewActionUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"), newSimplePackageTaskFactory(githubactions.NewWorkflowUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"), + newSimplePackageTaskFactory(java.NewJvmDistributionCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "java", "jvm", "jdk", "jre"), newPackageTaskFactory( func(cfg CatalogingFactoryConfig) pkg.Cataloger { return kernel.NewLinuxKernelCataloger(cfg.PackagesConfig.LinuxKernel) diff --git a/schema/json/schema-16.0.16.json b/schema/json/schema-16.0.16.json new file mode 100644 index 00000000000..e8ae0ec7d73 --- /dev/null +++ b/schema/json/schema-16.0.16.json @@ -0,0 +1,2668 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.0.16/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string" + }, + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "packager": { + "type": "string" + }, + "url": { + "type": "string" + }, + "validation": { + "type": "string" + }, + "reason": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ] + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "gid": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "size": { + "type": "string" + }, + "link": { + "type": "string" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object" + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ] + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ] + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "options": { + "$ref": "#/$defs/KeyValues" + }, + "path": { + "type": "string" + }, + "context": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string" + }, + "packageID": { + "type": "string" + }, + "username": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "recipeRevision": { + "type": "string" + }, + "packageRevision": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "CPE": { + "properties": { + "cpe": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "type": "object", + "required": [ + "cpe" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ] + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "checksum" + ] + }, + "Coordinates": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Descriptor": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": true + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array" + }, + "source": { + "$ref": "#/$defs/Source" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor" + }, + "schema": { + "$ref": "#/$defs/Schema" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ] + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ] + }, + "DotnetPortableExecutableEntry": { + "properties": { + "assemblyVersion": { + "type": "string" + }, + "legalCopyright": { + "type": "string" + }, + "comments": { + "type": "string" + }, + "internalName": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "productVersion": { + "type": "string" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ] + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ] + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ] + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean" + }, + "stackCanary": { + "type": "boolean" + }, + "nx": { + "type": "boolean" + }, + "relRO": { + "type": "string" + }, + "pie": { + "type": "boolean" + }, + "dso": { + "type": "boolean" + }, + "safeStack": { + "type": "boolean" + }, + "cfi": { + "type": "boolean" + }, + "fortify": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ] + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "osCPE": { + "type": "string" + }, + "os": { + "type": "string" + }, + "osVersion": { + "type": "string" + }, + "system": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "sourceRepo": { + "type": "string" + }, + "commit": { + "type": "string" + } + }, + "type": "object" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "Executable": { + "properties": { + "format": { + "type": "string" + }, + "hasExports": { + "type": "boolean" + }, + "hasEntrypoint": { + "type": "boolean" + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array" + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures" + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ] + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array" + }, + "executable": { + "$ref": "#/$defs/Executable" + } + }, + "type": "object", + "required": [ + "id", + "location" + ] + }, + "FileLicense": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ] + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "extent": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ] + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ] + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "type": "string" + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array" + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ] + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string" + } + }, + "type": "object" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string" + } + }, + "type": "object" + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string" + }, + "snapshotURL": { + "type": "string" + } + }, + "type": "object" + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "virtualPath" + ] + }, + "JavaJvmInstallation": { + "properties": { + "releaseInfo": { + "$ref": "#/$defs/JvmReleaseInfo" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "releaseInfo", + "files" + ] + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array" + } + }, + "type": "object" + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ] + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ] + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ] + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "private": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ] + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "JvmReleaseInfo": { + "properties": { + "implementor": { + "type": "string" + }, + "implementorVersion": { + "type": "string" + }, + "javaRuntimeVersion": { + "type": "string" + }, + "javaVersion": { + "type": "string" + }, + "javaVersionDate": { + "type": "string" + }, + "libc": { + "type": "string" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array" + }, + "osArch": { + "type": "string" + }, + "osName": { + "type": "string" + }, + "source": { + "type": "string" + }, + "buildSource": { + "type": "string" + }, + "buildSourceRepo": { + "type": "string" + }, + "sourceRepo": { + "type": "string" + }, + "fullVersion": { + "type": "string" + }, + "semanticVersion": { + "type": "string" + }, + "buildInfo": { + "type": "string" + }, + "jvmVariant": { + "type": "string" + }, + "jvmVersion": { + "type": "string" + }, + "imageType": { + "type": "string" + } + }, + "type": "object" + }, + "KeyValue": { + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "key", + "value" + ] + }, + "KeyValues": { + "items": { + "$ref": "#/$defs/KeyValue" + }, + "type": "array" + }, + "License": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ] + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extendedVersion": { + "type": "string" + }, + "buildTime": { + "type": "string" + }, + "author": { + "type": "string" + }, + "format": { + "type": "string" + }, + "rwRootFS": { + "type": "boolean" + }, + "swapDevice": { + "type": "integer" + }, + "rootDevice": { + "type": "integer" + }, + "videoMode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ] + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "license": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "versionMagic": { + "type": "string" + }, + "parameters": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "$ref": "#/$defs/IDLikes" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "versionCodename": { + "type": "string" + }, + "buildID": { + "type": "string" + }, + "imageID": { + "type": "string" + }, + "imageVersion": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + }, + "supportEnd": { + "type": "string" + } + }, + "type": "object" + }, + "Location": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + }, + "accessPath": { + "type": "string" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ] + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "dependencies": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ] + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string" + }, + "kb": { + "type": "string" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ] + }, + "NixStoreEntry": { + "properties": { + "outputHash": { + "type": "string" + }, + "output": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "outputHash", + "files" + ] + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "$ref": "#/$defs/cpes" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmDbEntry" + }, + { + "$ref": "#/$defs/ApkDbEntry" + }, + { + "$ref": "#/$defs/BinarySignature" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ] + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ] + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ] + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "type": "object", + "required": [ + "url" + ] + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo" + }, + "requiresPython": { + "type": "string" + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ] + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + }, + "versionConstraint": { + "type": "string" + }, + "url": { + "type": "string" + }, + "markers": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ] + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "index": { + "type": "string" + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ] + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "markers": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ] + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string" + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array" + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ] + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ] + }, + "RDescription": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repository": { + "type": "string" + }, + "built": { + "type": "string" + }, + "needsCompilation": { + "type": "boolean" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ] + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ] + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ] + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ] + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ] + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ] + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string" + } + }, + "type": "object", + "required": [ + "revision" + ] + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "packager": { + "type": "string" + }, + "packagerEmail": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ] + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorUri": { + "type": "string" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ] + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 1bab78aa6ba..e8ae0ec7d73 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.0.15/document", + "$id": "anchore.io/schema/syft/json/16.0.16/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -955,6 +955,24 @@ "virtualPath" ] }, + "JavaJvmInstallation": { + "properties": { + "releaseInfo": { + "$ref": "#/$defs/JvmReleaseInfo" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "releaseInfo", + "files" + ] + }, "JavaManifest": { "properties": { "main": { @@ -1127,6 +1145,71 @@ "integrity" ] }, + "JvmReleaseInfo": { + "properties": { + "implementor": { + "type": "string" + }, + "implementorVersion": { + "type": "string" + }, + "javaRuntimeVersion": { + "type": "string" + }, + "javaVersion": { + "type": "string" + }, + "javaVersionDate": { + "type": "string" + }, + "libc": { + "type": "string" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array" + }, + "osArch": { + "type": "string" + }, + "osName": { + "type": "string" + }, + "source": { + "type": "string" + }, + "buildSource": { + "type": "string" + }, + "buildSourceRepo": { + "type": "string" + }, + "sourceRepo": { + "type": "string" + }, + "fullVersion": { + "type": "string" + }, + "semanticVersion": { + "type": "string" + }, + "buildInfo": { + "type": "string" + }, + "jvmVariant": { + "type": "string" + }, + "jvmVersion": { + "type": "string" + }, + "imageType": { + "type": "string" + } + }, + "type": "object" + }, "KeyValue": { "properties": { "key": { @@ -1539,6 +1622,9 @@ { "$ref": "#/$defs/JavaArchive" }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, { "$ref": "#/$defs/JavascriptNpmPackage" }, diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier.go b/syft/format/internal/spdxutil/helpers/originator_supplier.go index 478cef377cf..144d5693568 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier.go @@ -62,6 +62,10 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: funlen } } + case pkg.JvmInstallation: + typ = orgType + author = metadata.ReleaseInfo.Implementor + case pkg.LinuxKernelModule: author = metadata.Author diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index 51965925d1c..656f9dc2e0b 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -177,6 +177,18 @@ func Test_OriginatorSupplier(t *testing.T) { }, // note: empty! }, + { + name: "from java -- jvm installation", + input: pkg.Package{ + Metadata: pkg.JvmInstallation{ + ReleaseInfo: pkg.JvmReleaseInfo{ + Implementor: "Oracle", + }, + }, + }, + originator: "Organization: Oracle", + supplier: "Organization: Oracle", + }, { name: "from linux kernel module", input: pkg.Package{ diff --git a/syft/internal/fileresolver/chroot_context.go b/syft/internal/fileresolver/chroot_context.go index a5245952b85..f643411a85b 100644 --- a/syft/internal/fileresolver/chroot_context.go +++ b/syft/internal/fileresolver/chroot_context.go @@ -140,6 +140,40 @@ func (r ChrootContext) ToNativePath(chrootPath string) (string, error) { return responsePath, nil } +func (r ChrootContext) ToNativeGlob(chrootPath string) (string, error) { + // split on any * + parts := strings.Split(chrootPath, "*") + if len(parts) == 0 || parts[0] == "" { + // either this is an empty string or a path that starts with * so there is nothing we can do + return chrootPath, nil + } + + if len(parts) == 1 { + // this has no glob, treat it like a path + return r.ToNativePath(chrootPath) + } + + responsePath := parts[0] + + if filepath.IsAbs(responsePath) { + // don't allow input to potentially hop above root path + responsePath = path.Join(r.root, responsePath) + } else { + // ensure we take into account any relative difference between the root path and the CWD for relative requests + responsePath = path.Join(r.cwdRelativeToRoot, responsePath) + } + + var err error + responsePath, err = filepath.Abs(responsePath) + if err != nil { + return "", err + } + + parts[0] = strings.TrimRight(responsePath, "/") + "/" + + return strings.Join(parts, "*"), nil +} + // ToChrootPath takes a path from the underlying fs domain and converts it to a path that is relative to the current root context. func (r ChrootContext) ToChrootPath(nativePath string) string { responsePath := nativePath diff --git a/syft/internal/fileresolver/chroot_context_test.go b/syft/internal/fileresolver/chroot_context_test.go index 2cd8befe136..245e08b63f4 100644 --- a/syft/internal/fileresolver/chroot_context_test.go +++ b/syft/internal/fileresolver/chroot_context_test.go @@ -479,3 +479,98 @@ func Test_ChrootContext_RequestResponse(t *testing.T) { }) } } + +func TestToNativeGlob(t *testing.T) { + tests := []struct { + name string + chrootContext ChrootContext + chrootPath string + expectedResult string + expectedError error + }{ + { + name: "ignore empty path", + chrootContext: ChrootContext{ + root: "/root", + cwdRelativeToRoot: "/cwd", + }, + chrootPath: "", + expectedResult: "", + expectedError: nil, + }, + { + name: "ignore if just a path", + chrootContext: ChrootContext{ + root: "/root", + cwdRelativeToRoot: "/cwd", + }, + chrootPath: "/some/path/file.txt", + expectedResult: "/root/some/path/file.txt", + expectedError: nil, + }, + { + name: "ignore starting with glob", + chrootContext: ChrootContext{ + root: "/root", + cwdRelativeToRoot: "/cwd", + }, + chrootPath: "*/relative/path/*", + expectedResult: "*/relative/path/*", + expectedError: nil, + }, + { + name: "absolute path with glob", + chrootContext: ChrootContext{ + root: "/root", + cwdRelativeToRoot: "/cwd", + }, + chrootPath: "/some/path/*", + expectedResult: "/root/some/path/*", + expectedError: nil, + }, + { + name: "relative path with glob", + chrootContext: ChrootContext{ + root: "/root", + cwdRelativeToRoot: "/cwd", + }, + chrootPath: "relative/path/*", + expectedResult: "/cwd/relative/path/*", + expectedError: nil, + }, + { + name: "relative path with no root", + chrootContext: ChrootContext{ + root: "", + cwdRelativeToRoot: "/cwd", + }, + chrootPath: "relative/path/*", + expectedResult: "/cwd/relative/path/*", + expectedError: nil, + }, + { + name: "globs everywhere", + chrootContext: ChrootContext{ + root: "/root", + cwdRelativeToRoot: "/cwd", + }, + chrootPath: "relative/path/**/file*.txt", + expectedResult: "/cwd/relative/path/**/file*.txt", + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tt.chrootContext.ToNativeGlob(tt.chrootPath) + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} diff --git a/syft/internal/fileresolver/directory.go b/syft/internal/fileresolver/directory.go index fe1a55a7f8f..a86092f67b5 100644 --- a/syft/internal/fileresolver/directory.go +++ b/syft/internal/fileresolver/directory.go @@ -145,13 +145,21 @@ func (r Directory) FilesByPath(userPaths ...string) ([]file.Location, error) { return references, nil } +func (r Directory) requestGlob(pattern string) (string, error) { + return r.chroot.ToNativeGlob(pattern) +} + // FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image. func (r Directory) FilesByGlob(patterns ...string) ([]file.Location, error) { uniqueFileIDs := stereoscopeFile.NewFileReferenceSet() uniqueLocations := make([]file.Location, 0) for _, pattern := range patterns { - refVias, err := r.searchContext.SearchByGlob(pattern, filetree.FollowBasenameLinks) + requestGlob, err := r.requestGlob(pattern) + if err != nil { + return nil, err + } + refVias, err := r.searchContext.SearchByGlob(requestGlob, filetree.FollowBasenameLinks) if err != nil { return nil, err } diff --git a/syft/internal/packagemetadata/generated.go b/syft/internal/packagemetadata/generated.go index db843261cd6..df94017405f 100644 --- a/syft/internal/packagemetadata/generated.go +++ b/syft/internal/packagemetadata/generated.go @@ -27,6 +27,7 @@ func AllTypes() []any { pkg.HackageStackYamlEntry{}, pkg.HackageStackYamlLockEntry{}, pkg.JavaArchive{}, + pkg.JvmInstallation{}, pkg.LinuxKernel{}, pkg.LinuxKernelModule{}, pkg.LuaRocksPackage{}, diff --git a/syft/internal/packagemetadata/names.go b/syft/internal/packagemetadata/names.go index 52deebce022..ad355bd3fd2 100644 --- a/syft/internal/packagemetadata/names.go +++ b/syft/internal/packagemetadata/names.go @@ -81,6 +81,7 @@ var jsonTypes = makeJSONTypes( jsonNames(pkg.HackageStackYamlLockEntry{}, "haskell-hackage-stack-lock-entry", "HackageMetadataType"), jsonNamesWithoutLookup(pkg.HackageStackYamlEntry{}, "haskell-hackage-stack-entry", "HackageMetadataType"), // the legacy value is split into two types, where the other is preferred jsonNames(pkg.JavaArchive{}, "java-archive", "JavaMetadata"), + jsonNames(pkg.JvmInstallation{}, "java-jvm-installation"), jsonNames(pkg.MicrosoftKbPatch{}, "microsoft-kb-patch", "KbPatchMetadata"), jsonNames(pkg.LinuxKernel{}, "linux-kernel-archive", "LinuxKernel"), jsonNames(pkg.LinuxKernelModule{}, "linux-kernel-module", "LinuxKernelModule"), diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index 11e48b7f5ad..2affb83fd7f 100644 --- a/syft/pkg/cataloger/java/cataloger.go +++ b/syft/pkg/cataloger/java/cataloger.go @@ -43,3 +43,9 @@ func NewGradleLockfileCataloger() pkg.Cataloger { return generic.NewCataloger("java-gradle-lockfile-cataloger"). WithParserByGlobs(parseGradleLockfile, gradleLockfileGlob) } + +// NewJvmDistributionCataloger returns packages representing JDK/JRE installations (of multiple distribution types). +func NewJvmDistributionCataloger() pkg.Cataloger { + return generic.NewCataloger("java-jvm-cataloger"). + WithParserByGlobs(parseJVMRelease, jvmReleaseGlob) +} diff --git a/syft/pkg/cataloger/java/cataloger_test.go b/syft/pkg/cataloger/java/cataloger_test.go index 84ba1ea91db..eac03cf9b46 100644 --- a/syft/pkg/cataloger/java/cataloger_test.go +++ b/syft/pkg/cataloger/java/cataloger_test.go @@ -4,6 +4,9 @@ import ( "testing" "github.com/anchore/syft/syft/cataloging" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) @@ -102,3 +105,63 @@ func Test_POMCataloger_Globs(t *testing.T) { }) } } + +func TestJvmDistributionCataloger(t *testing.T) { + p := pkg.Package{ + Name: "openjdk", + Version: "21.0.4+7", + FoundBy: "java-jvm-cataloger", + Locations: file.NewLocationSet(file.NewLocation("jvm/openjdk/release")), + Licenses: pkg.NewLicenseSet(), + Type: pkg.BinaryPkg, + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:oracle:openjdk:21.0.4:update7:*:*:*:*:*:*", cpe.GeneratedSource)}, + PURL: "pkg:generic/oracle/openjdk@21.0.4%2B7", + Metadata: pkg.JvmInstallation{ + ReleaseInfo: pkg.JvmReleaseInfo{ + Implementor: "Eclipse Adoptium", + ImplementorVersion: "Temurin-21.0.4+7", + JavaRuntimeVersion: "21.0.4+7-LTS", + JavaVersion: "21.0.4", + JavaVersionDate: "2024-07-16", + Libc: "gnu", + Modules: []string{ + "java.base", "java.compiler", "java.datatransfer", "java.xml", "java.prefs", + "java.desktop", "java.instrument", "java.logging", "java.management", + "java.security.sasl", "java.naming", "java.rmi", "java.management.rmi", + "java.net.http", "java.scripting", "java.security.jgss", + "java.transaction.xa", "java.sql", "java.sql.rowset", "java.xml.crypto", "java.se", + "java.smartcardio", "jdk.accessibility", "jdk.internal.jvmstat", "jdk.attach", + "jdk.charsets", "jdk.internal.opt", "jdk.zipfs", "jdk.compiler", "jdk.crypto.ec", + "jdk.crypto.cryptoki", "jdk.dynalink", "jdk.internal.ed", "jdk.editpad", "jdk.hotspot.agent", + "jdk.httpserver", "jdk.incubator.vector", "jdk.internal.le", "jdk.internal.vm.ci", + "jdk.internal.vm.compiler", "jdk.internal.vm.compiler.management", "jdk.jartool", + "jdk.javadoc", "jdk.jcmd", "jdk.management", "jdk.management.agent", "jdk.jconsole", + "jdk.jdeps", "jdk.jdwp.agent", "jdk.jdi", "jdk.jfr", "jdk.jlink", "jdk.jpackage", "jdk.jshell", + "jdk.jsobject", "jdk.jstatd", "jdk.localedata", "jdk.management.jfr", "jdk.naming.dns", + "jdk.naming.rmi", "jdk.net", "jdk.nio.mapmode", "jdk.random", "jdk.sctp", "jdk.security.auth", + "jdk.security.jgss", "jdk.unsupported", "jdk.unsupported.desktop", "jdk.xml.dom", + }, + OsArch: "aarch64", + OsName: "Linux", + Source: ".:git:13710926b798", + BuildSource: "git:1271f10a26c47e1489a814dd2731f936a588d621", + BuildSourceRepo: "https://github.com/adoptium/temurin-build.git", + SourceRepo: "https://github.com/adoptium/jdk21u.git", + FullVersion: "21.0.4+7-LTS", + SemanticVersion: "21.0.4+7", + BuildInfo: "OS: Linux Version: 5.4.0-150-generic", + JvmVariant: "Hotspot", + JvmVersion: "21.0.4+7-LTS", + ImageType: "JDK", + }, + Files: []string{ + "jvm/openjdk/release", + "jvm/openjdk/sibling/child/file1.txt", + }, + }, + } + + p.SetID() + + pkgtest.TestCataloger(t, "test-fixtures/jvm-installs/valid-post-jep223", NewJvmDistributionCataloger(), []pkg.Package{p}, nil) +} diff --git a/syft/pkg/cataloger/java/parse_jvm_release.go b/syft/pkg/cataloger/java/parse_jvm_release.go new file mode 100644 index 00000000000..76a3adcd6f6 --- /dev/null +++ b/syft/pkg/cataloger/java/parse_jvm_release.go @@ -0,0 +1,367 @@ +package java + +import ( + "bufio" + "context" + "fmt" + "io" + "path" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/mitchellh/mapstructure" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +const jvmReleaseGlob = "**/{java,jvm}/*/release" + +var preJep223VersionPattern = regexp.MustCompile(`^1\.(?P\d+)\.(?P\d+)(_(?P\d+))?(-(?P[^b][^-]+))?(-b(?P\d+))?`) + +// the /opt/java/openjdk/release file (and similar paths) is a file that is present in the multiple OpenJDK distributions +// here's an example of the contents of the file: +// +// IMPLEMENTOR="Eclipse Adoptium" +// IMPLEMENTOR_VERSION="Temurin-21.0.4+7" +// JAVA_RUNTIME_VERSION="21.0.4+7-LTS" +// JAVA_VERSION="21.0.4" +// JAVA_VERSION_DATE="2024-07-16" +// LIBC="gnu" +// MODULES="java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.ec jdk.crypto.cryptoki jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.vm.ci jdk.internal.vm.compiler jdk.internal.vm.compiler.management jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jsobject jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.random jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom" +// OS_ARCH="aarch64" +// OS_NAME="Linux" +// SOURCE=".:git:13710926b798" +// BUILD_SOURCE="git:1271f10a26c47e1489a814dd2731f936a588d621" +// BUILD_SOURCE_REPO="https://github.com/adoptium/temurin-build.git" +// SOURCE_REPO="https://github.com/adoptium/jdk21u.git" +// FULL_VERSION="21.0.4+7-LTS" +// SEMANTIC_VERSION="21.0.4+7" +// BUILD_INFO="OS: Linux Version: 5.4.0-150-generic" +// JVM_VARIANT="Hotspot" +// JVM_VERSION="21.0.4+7-LTS" +// IMAGE_TYPE="JDK" +// +// In terms of the temurin flavor, these are controlled by: +// - config: https://github.com/adoptium/temurin-build/blob/v2023.01.03/sbin/common/config_init.sh +// - build script: https://github.com/adoptium/temurin-build/blob/v2023.01.03/sbin/build.sh#L1584-L1796 + +func parseJVMRelease(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + ri, err := parseJvmReleaseInfo(reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to parse JVM release info %q: %w", reader.Path(), err) + } + + if ri == nil { + // TODO: known-unknown: expected JDK installation package + return nil, nil, nil + } + + installDir := path.Dir(reader.Path()) + + version := jvmVersion(ri) + + licenses := jvmLicenses(resolver, ri) + + locations := file.NewLocationSet(reader.Location) + + for _, lic := range licenses.ToSlice() { + locations.Add(lic.Locations.ToSlice()...) + } + + p := pkg.Package{ + Name: jvmPackageNameFromPath(installDir), + Locations: locations, + Version: version, + CPEs: jvmCpes(ri.Implementor, version), + PURL: jvmPurl(version), + Licenses: licenses, + Type: pkg.BinaryPkg, + Metadata: pkg.JvmInstallation{ + ReleaseInfo: *ri, + Files: findJVMFiles(resolver, installDir), + }, + } + p.SetID() + + return []pkg.Package{p}, nil, nil +} + +func jvmLicenses(_ file.Resolver, _ *pkg.JvmReleaseInfo) pkg.LicenseSet { + // TODO: get this from the dir()/legal/**/LICENSE files when we start cataloging license content + // see https://github.com/anchore/syft/issues/656 + return pkg.NewLicenseSet() +} + +func findJVMFiles(resolver file.Resolver, installDir string) []string { + ownedLocations, err := resolver.FilesByGlob(installDir + "/**") + if err != nil { + // TODO: known-unknowns + log.WithFields("path", installDir, "error", err).Trace("unable to find installed JVM files") + } + + var results []string + for _, loc := range ownedLocations { + results = append(results, loc.Path()) + } + + sort.Strings(results) + + return results +} + +func jvmPurl(version string) string { + pURL := packageurl.NewPackageURL( + packageurl.TypeGeneric, + "oracle", + "openjdk", + version, + nil, + "") + return pURL.ToString() +} + +func jvmCpes(implementor, version string) []cpe.CPE { + // see https://github.com/anchore/syft/issues/2422 for more context + + // assume JEP 223 version strings are provided + // example: 9.0.1+20 + fields := strings.Split(version, "+") + var update string + shortVer := version + if len(fields) == 2 { + shortVer = fields[0] + update = "update" + fields[1] + } + + vendor := "oracle" + project := "openjdk" + + if strings.Contains(strings.ToLower(implementor), "azul") { + vendor = "azul" + project = "zulu" + } + + return []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: vendor, + Product: project, + Version: shortVer, + Update: update, + }, + Source: cpe.GeneratedSource, + }, + } +} + +// jvmVersion attempts to extract the correct version value for the JVM given a platter of version strings to choose +// from, and makes special consideration to what a valid version is relative to JEP 223. +// +// example version values (openjdk >8): +// +// IMPLEMENTOR_VERSION "Temurin-21.0.4+7" +// JAVA_RUNTIME_VERSION "21.0.4+7-LTS" +// FULL_VERSION "21.0.4+7-LTS" +// SEMANTIC_VERSION "21.0.4+7" +// JAVA_VERSION "21.0.4" +// +// example version values (openjdk 8): +// +// JAVA_VERSION "1.8.0_422" +// FULL_VERSION "1.8.0_422-b05" +// SEMANTIC_VERSION "8.0.422+5" +// +// example version values (openjdk 8, but older): +// +// JAVA_VERSION "1.8.0_302" +// FULL_VERSION "1.8.0_302-b08" +// SEMANTIC_VERSION "8.0.302+8" +// +// example version values (oracle): +// +// IMPLEMENTOR_VERSION (missing) +// JAVA_RUNTIME_VERSION "22.0.2+9-70" +// JAVA_VERSION "22.0.2" +// +// example version values (mariner): +// +// IMPLEMENTOR_VERSION "Microsoft-9889599" +// JAVA_RUNTIME_VERSION "17.0.12+7-LTS" +// JAVA_VERSION "17.0.12" +// +// example version values (amazon): +// +// IMPLEMENTOR_VERSION "Corretto-17.0.12.7.1" +// JAVA_RUNTIME_VERSION "17.0.12+7-LTS" +// JAVA_VERSION "17.0.12" +// +// JEP 223 changes to JVM version string in the following way: +// +// Pre JEP 223 Post JEP 223 +// Release Type long short long short +// ------------ -------------------- -------------------- +// Early Access 1.9.0-ea-b19 9-ea 9-ea+19 9-ea +// Major 1.9.0-b100 9 9+100 9 +// Security #1 1.9.0_5-b20 9u5 9.0.1+20 9.0.1 +// Security #2 1.9.0_11-b12 9u11 9.0.2+12 9.0.2 +// Minor #1 1.9.0_20-b62 9u20 9.1.2+62 9.1.2 +// Security #3 1.9.0_25-b15 9u25 9.1.3+15 9.1.3 +// Security #4 1.9.0_31-b08 9u31 9.1.4+8 9.1.4 +// Minor #2 1.9.0_40-b45 9u40 9.2.4+45 9.2.4 +// +// What does this mean for us? In terms of the version selected, use semver-compliant strings when available. +// Otherwise, we should attempt to convert pre-JEP 223 version strings. +// +// In terms of where to get the version: +// +// SEMANTIC_VERSION Reasonably prevalent, but most accurate in terms of comparable versions +// JAVA_RUNTIME_VERSION Reasonable prevalent, but difficult to distinguish pre-release info vs aux info (jep 223 sensitive) +// FULL_VERSION Reasonable prevalent, but difficult to distinguish pre-release info vs aux info (jep 223 sensitive) +// JAVA_VERSION Most prevalent, but least specific (jep 223 sensitive) +// IMPLEMENTOR_VERSION Unusable or missing in some cases +func jvmVersion(ri *pkg.JvmReleaseInfo) string { + if ri.SemanticVersion != "" { + return ri.SemanticVersion + } + + var version string + switch { + case ri.FullVersion != "": + version = ri.FullVersion + case ri.JavaRuntimeVersion != "": + version = ri.JavaRuntimeVersion + case ri.JavaVersion != "": + version = ri.JavaVersion + } + + // we don't want to represent 1.8* when we can convert to 8.* (up-convert legacy version strings). We don't want + // to do this for all legacy strings, only those which are 1.8 (when JEP 223 was introduced). Anything less than + // 1,8 should REMAIN as the legacy version string. + majV, minV := getMajorMinorVersions(version) + if majV == 1 && minV == 8 { + version = convertPreJep223Version(version) + } + + return version +} + +func getMajorMinorVersions(v string) (int, int) { + fields := strings.Split(v, ".") + if len(fields) == 0 { + return -1, -1 + } + + var err error + var majV, minV int + + if len(fields) >= 1 { + majV, err = strconv.Atoi(fields[0]) + if err != nil { + log.WithFields("version", v, "error", err).Trace("unable to parse JVM major version") + return -1, -1 + } + } + + if len(fields) >= 2 { + minV, err = strconv.Atoi(fields[1]) + if err != nil { + log.WithFields("version", v, "error", err).Trace("unable to parse JVM minor version") + return -1, -1 + } + } + + return majV, minV +} + +func convertPreJep223Version(version string) string { + // convert the following pre JEP 223 version strings to semvers + // 1.8.0_302-b08 --> 8.0.302+8 + // 1.9.0-ea-b19 --> 9.0.0-ea+19 + version = strings.TrimSpace(version) + + matches := internal.MatchNamedCaptureGroups(preJep223VersionPattern, version) + if len(matches) == 0 { + log.WithFields("version", version).Trace("unable to convert pre-JEP 223 JVM version") + return version + } + + // extract relevant parts from the matches + majorVersion := trim0sFromLeft(matches["major"]) + minorVersion := trim0sFromLeft(matches["minor"]) + patchVersion := trim0sFromLeft(matches["patch"]) + preRelease := trim0sFromLeft(matches["prerelease"]) + build := trim0sFromLeft(matches["build"]) + + // format minor and patch version + if patchVersion == "" { + patchVersion = "0" + } + + // build the semver string + var semver strings.Builder + semver.WriteString(fmt.Sprintf("%s.%s.%s", majorVersion, minorVersion, patchVersion)) + + if preRelease != "" { + semver.WriteString(fmt.Sprintf("-%s", preRelease)) + } + if build != "" { + semver.WriteString(fmt.Sprintf("+%s", build)) + } + + return semver.String() +} + +func trim0sFromLeft(v string) string { + if v == "0" { + return v + } + return strings.TrimLeft(v, "0") +} + +func jvmPackageNameFromPath(installDir string) string { + return path.Base(installDir) +} + +func parseJvmReleaseInfo(r io.ReadCloser) (*pkg.JvmReleaseInfo, error) { + defer r.Close() + + data := make(map[string]any) + scanner := bufio.NewScanner(r) + + for scanner.Scan() { + line := scanner.Text() + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := parts[0] + value := strings.Trim(parts[1], `"`) + + if key == "MODULES" { + data[key] = strings.Split(value, " ") + } else { + data[key] = value + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + var ri pkg.JvmReleaseInfo + if err := mapstructure.Decode(data, &ri); err != nil { + return nil, err + } + + return &ri, nil +} diff --git a/syft/pkg/cataloger/java/parse_jvm_release_test.go b/syft/pkg/cataloger/java/parse_jvm_release_test.go new file mode 100644 index 00000000000..5ba54da5b82 --- /dev/null +++ b/syft/pkg/cataloger/java/parse_jvm_release_test.go @@ -0,0 +1,315 @@ +package java + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/pkg" +) + +func TestJvmCpes(t *testing.T) { + tests := []struct { + name string + version string + implementor string + expected []cpe.CPE + }{ + { + name: "Zulu release", + version: "9.0.1+20", + implementor: "Azul Systems, Inc.", + expected: []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "azul", + Product: "zulu", + Version: "9.0.1", + Update: "update20", + }, + Source: cpe.GeneratedSource, + }, + }, + }, + { + name: "JEP 223 version with update", + version: "9.0.1+20", + expected: []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "oracle", + Product: "openjdk", + Version: "9.0.1", + Update: "update20", + }, + Source: cpe.GeneratedSource, + }, + }, + }, + { + name: "JEP 223 version without update", + version: "11.0.9", + expected: []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "oracle", + Product: "openjdk", + Version: "11.0.9", + Update: "", + }, + Source: cpe.GeneratedSource, + }, + }, + }, + { + name: "Non-JEP version with plus sign but no update", + version: "1.8.0+0", + expected: []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "oracle", + Product: "openjdk", + Version: "1.8.0", + Update: "update0", + }, + Source: cpe.GeneratedSource, + }, + }, + }, + { + name: "No plus sign in version string", + version: "1.8.0", + expected: []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "oracle", + Product: "openjdk", + Version: "1.8.0", + Update: "", + }, + Source: cpe.GeneratedSource, + }, + }, + }, + { + name: "Empty version string", + version: "", + expected: []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "oracle", + Product: "openjdk", + Version: "", + Update: "", + }, + Source: cpe.GeneratedSource, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := jvmCpes(tt.implementor, tt.version) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestJvmVersion(t *testing.T) { + tests := []struct { + name string + input *pkg.JvmReleaseInfo + expected string + }{ + { + name: "SemanticVersion available", + input: &pkg.JvmReleaseInfo{ + SemanticVersion: "21.0.4+7", + }, + expected: "21.0.4+7", + }, + { + name: "FullVersion fallback", + input: &pkg.JvmReleaseInfo{ + FullVersion: "21.0.4+7-LTS", + }, + expected: "21.0.4+7-LTS", + }, + { + name: "JavaRuntimeVersion fallback", + input: &pkg.JvmReleaseInfo{ + JavaRuntimeVersion: "21.0.4+7-LTS", + }, + expected: "21.0.4+7-LTS", + }, + { + name: "JavaVersion fallback", + input: &pkg.JvmReleaseInfo{ + JavaVersion: "21.0.4", + }, + expected: "21.0.4", + }, + { + name: "Legacy version conversion", + input: &pkg.JvmReleaseInfo{ + JavaVersion: "1.8.0_302-b42", + }, + expected: "8.0.302+42", + }, + { + name: "Non-legacy version, no conversion", + input: &pkg.JvmReleaseInfo{ + JavaVersion: "11.0.11", + }, + expected: "11.0.11", + }, + { + name: "Empty input fields", + input: &pkg.JvmReleaseInfo{}, + expected: "", + }, + { + name: "JavaVersion below 1.8, no conversion", + input: &pkg.JvmReleaseInfo{ + JavaVersion: "1.7.0_80", + }, + expected: "1.7.0_80", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := jvmVersion(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestGetMajorMinorVersions(t *testing.T) { + tests := []struct { + name string + version string + expectedMajor int + expectedMinor int + }{ + { + name: "valid version with major and minor", + version: "1.8", + expectedMajor: 1, + expectedMinor: 8, + }, + { + name: "valid version with only major", + version: "11", + expectedMajor: 11, + expectedMinor: 0, + }, + { + name: "invalid version format", + version: "not-a-version", + expectedMajor: -1, + expectedMinor: -1, + }, + { + name: "empty string", + version: "", + expectedMajor: -1, + expectedMinor: -1, + }, + { + name: "extra segments in version", + version: "1.8.0", + expectedMajor: 1, + expectedMinor: 8, + }, + { + name: "non-numeric major", + version: "a.8", + expectedMajor: -1, + expectedMinor: -1, + }, + { + name: "non-numeric minor", + version: "1.b", + expectedMajor: -1, + expectedMinor: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + major, minor := getMajorMinorVersions(tt.version) + assert.Equal(t, tt.expectedMajor, major) + assert.Equal(t, tt.expectedMinor, minor) + }) + } +} + +func TestConvertPreJep223Version(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "valid JDK 8 with patch and build", + input: "1.8.0_302-b08", + expected: "8.0.302+8", + }, + { + name: "valid JDK 9 early access with build", + input: "1.9.0-ea-b19", + expected: "9.0.0-ea+19", + }, + { + name: "valid JDK 7 with patch and build", + input: "1.7.0_75-b13", + expected: "7.0.75+13", + }, + { + name: "no patch and build, JDK 8", + input: "1.8.0", + expected: "8.0.0", + }, + { + name: "valid JDK 10 with patch and build", + input: "1.10.0_25-b01", + expected: "10.0.25+1", + }, + { + name: "no build number, JDK 9", + input: "1.9.0_45", + expected: "9.0.45", + }, + { + name: "JEP 223-like version", + input: "2.0.0", + expected: "2.0.0", + }, + { + name: "JDK 8 with both patch and build", + input: "1.8.0_242-b01", + expected: "8.0.242+1", + }, + { + name: "invalid version string", + input: "not-a-version", + expected: "not-a-version", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertPreJep223Version(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/syft/pkg/cataloger/java/test-fixtures/jvm-installs/valid-post-jep223/jvm/openjdk/release b/syft/pkg/cataloger/java/test-fixtures/jvm-installs/valid-post-jep223/jvm/openjdk/release new file mode 100644 index 00000000000..13cf5f6a54b --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/jvm-installs/valid-post-jep223/jvm/openjdk/release @@ -0,0 +1,19 @@ +IMPLEMENTOR="Eclipse Adoptium" +IMPLEMENTOR_VERSION="Temurin-21.0.4+7" +JAVA_RUNTIME_VERSION="21.0.4+7-LTS" +JAVA_VERSION="21.0.4" +JAVA_VERSION_DATE="2024-07-16" +LIBC="gnu" +MODULES="java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.ec jdk.crypto.cryptoki jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.vm.ci jdk.internal.vm.compiler jdk.internal.vm.compiler.management jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jsobject jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.random jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom" +OS_ARCH="aarch64" +OS_NAME="Linux" +SOURCE=".:git:13710926b798" +BUILD_SOURCE="git:1271f10a26c47e1489a814dd2731f936a588d621" +BUILD_SOURCE_REPO="https://github.com/adoptium/temurin-build.git" +SOURCE_REPO="https://github.com/adoptium/jdk21u.git" +FULL_VERSION="21.0.4+7-LTS" +SEMANTIC_VERSION="21.0.4+7" +BUILD_INFO="OS: Linux Version: 5.4.0-150-generic" +JVM_VARIANT="Hotspot" +JVM_VERSION="21.0.4+7-LTS" +IMAGE_TYPE="JDK" diff --git a/syft/pkg/cataloger/java/test-fixtures/jvm-installs/valid-post-jep223/jvm/openjdk/sibling/child/file1.txt b/syft/pkg/cataloger/java/test-fixtures/jvm-installs/valid-post-jep223/jvm/openjdk/sibling/child/file1.txt new file mode 100644 index 00000000000..ff15cec308a --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/jvm-installs/valid-post-jep223/jvm/openjdk/sibling/child/file1.txt @@ -0,0 +1 @@ +content! \ No newline at end of file diff --git a/syft/pkg/java.go b/syft/pkg/java.go index 2a990b5d497..9e026a1aef3 100644 --- a/syft/pkg/java.go +++ b/syft/pkg/java.go @@ -18,6 +18,74 @@ var jenkinsPluginPomPropertiesGroupIDs = []string{ "com.cloudbees.jenkins.plugins", } +type JvmInstallation struct { + ReleaseInfo JvmReleaseInfo `json:"releaseInfo"` + Files []string `json:"files"` +} + +func (m JvmInstallation) OwnedFiles() []string { + return m.Files +} + +type JvmReleaseInfo struct { + // Implementor is extracted with the `java.vendor` JVM property + Implementor string `mapstructure:"IMPLEMENTOR,omitempty" json:"implementor,omitempty"` + + // ImplementorVersion is extracted with the `java.vendor.version` JVM property + ImplementorVersion string `mapstructure:"IMPLEMENTOR_VERSION,omitempty" json:"implementorVersion,omitempty"` + + // JavaRuntimeVersion is extracted from the 'java.runtime.version' JVM property + JavaRuntimeVersion string `mapstructure:"JAVA_RUNTIME_VERSION,omitempty" json:"javaRuntimeVersion,omitempty"` + + // JavaVersion matches that from `java -version` command output + JavaVersion string `mapstructure:"JAVA_VERSION,omitempty" json:"javaVersion,omitempty"` + + // JavaVersionDate is extracted from the 'java.version.date' JVM property + JavaVersionDate string `mapstructure:"JAVA_VERSION_DATE,omitempty" json:"javaVersionDate,omitempty"` + + // Libc can either be 'glibc' or 'musl' + Libc string `mapstructure:"LIBC,omitempty" json:"libc,omitempty"` + + // Modules is a list of JVM modules that are packaged + Modules []string `mapstructure:"MODULES,omitempty" json:"modules,omitempty"` + + // OsArch is the target CPU architecture + OsArch string `mapstructure:"OS_ARCH,omitempty" json:"osArch,omitempty"` + + // OsName is the target runtime operating system environment + OsName string `mapstructure:"OS_NAME,omitempty" json:"osName,omitempty"` + + // Source refers to the origin repository of OpenJDK source + Source string `mapstructure:"SOURCE,omitempty" json:"source,omitempty"` + + // BuildSource Git SHA of the build repository + BuildSource string `mapstructure:"BUILD_SOURCE,omitempty" json:"buildSource,omitempty"` + + // BuildSourceRepo refers to rhe repository URL for the build source + BuildSourceRepo string `mapstructure:"BUILD_SOURCE_REPO,omitempty" json:"buildSourceRepo,omitempty"` + + // SourceRepo refers to the OpenJDK repository URL + SourceRepo string `mapstructure:"SOURCE_REPO,omitempty" json:"sourceRepo,omitempty"` + + // FullVersion is extracted from the 'java.runtime.version' JVM property + FullVersion string `mapstructure:"FULL_VERSION,omitempty" json:"fullVersion,omitempty"` + + // SemanticVersion is derived from the OpenJDK version + SemanticVersion string `mapstructure:"SEMANTIC_VERSION,omitempty" json:"semanticVersion,omitempty"` + + // BuildInfo contains additional build information + BuildInfo string `mapstructure:"BUILD_INFO,omitempty" json:"buildInfo,omitempty"` + + // JvmVariant specifies the JVM variant (e.g., Hotspot or OpenJ9) + JvmVariant string `mapstructure:"JVM_VARIANT,omitempty" json:"jvmVariant,omitempty"` + + // JvmVersion is extracted from the 'java.vm.version' JVM property + JvmVersion string `mapstructure:"JVM_VERSION,omitempty" json:"jvmVersion,omitempty"` + + // ImageType can be 'JDK' or 'JRE' + ImageType string `mapstructure:"IMAGE_TYPE,omitempty" json:"imageType,omitempty"` +} + // JavaArchive encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship. type JavaArchive struct { VirtualPath string `json:"virtualPath" cyclonedx:"virtualPath"` // we need to include the virtual path in cyclonedx documents to prevent deduplication of jars within jars