diff --git a/cmd/syft/internal/test/integration/go_compiler_detection_test.go b/cmd/syft/internal/test/integration/go_compiler_detection_test.go index fbeec5ba0..8c440a030 100644 --- a/cmd/syft/internal/test/integration/go_compiler_detection_test.go +++ b/cmd/syft/internal/test/integration/go_compiler_detection_test.go @@ -19,7 +19,7 @@ func TestGolangCompilerDetection(t *testing.T) { name: "syft can detect a single golang compiler given the golang base image", image: "image-golang-compiler", expectedCompilers: []string{"go1.18.10"}, - expectedCPE: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.10:-:*:*:*:*:*:*")}, + expectedCPE: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.10:-:*:*:*:*:*:*", cpe.GeneratedSource)}, expectedPURL: []string{"pkg:golang/stdlib@1.18.10"}, }, } diff --git a/internal/constants.go b/internal/constants.go index e52ca7f89..47e3d3594 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 = "15.0.0" + JSONSchemaVersion = "16.0.0" ) diff --git a/internal/task/package_task_factory.go b/internal/task/package_task_factory.go index 2c5c7325d..17b613d77 100644 --- a/internal/task/package_task_factory.go +++ b/internal/task/package_task_factory.go @@ -113,7 +113,7 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string // we might have binary classified CPE already with the package so we want to append here dictionaryCPE, ok := cpe.DictionaryFind(p) if ok { - log.Tracef("used CPE dictionary to find CPE for %s package %q: %s", p.Type, p.Name, dictionaryCPE.BindToFmtString()) + log.Tracef("used CPE dictionary to find CPE for %s package %q: %s", p.Type, p.Name, dictionaryCPE.Attributes.BindToFmtString()) p.CPEs = append(p.CPEs, dictionaryCPE) } else { p.CPEs = append(p.CPEs, cpe.Generate(p)...) diff --git a/schema/json/schema-16.0.0.json b/schema/json/schema-16.0.0.json new file mode 100644 index 000000000..d282375e0 --- /dev/null +++ b/schema/json/schema-16.0.0.json @@ -0,0 +1,2164 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.0.0/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" + } + }, + "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" + ] + }, + "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" + ] + }, + "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" + ] + }, + "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" + } + }, + "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" + } + }, + "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" + ] + }, + "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" + ] + }, + "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" + ] + }, + "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/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + } + ] + } + }, + "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" + ] + }, + "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" + } + }, + "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" + ] + }, + "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" + }, + "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" + }, + "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" + ] + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/syft/cpe/by_source_then_specificity.go b/syft/cpe/by_source_then_specificity.go new file mode 100644 index 000000000..85948449c --- /dev/null +++ b/syft/cpe/by_source_then_specificity.go @@ -0,0 +1,39 @@ +package cpe + +import "sort" + +type BySourceThenSpecificity []CPE + +func (b BySourceThenSpecificity) Len() int { + return len(b) +} + +func (b BySourceThenSpecificity) Less(i, j int) bool { + sourceOrder := map[Source]int{ + NVDDictionaryLookupSource: 1, + DeclaredSource: 2, + GeneratedSource: 3, + } + + getRank := func(source Source) int { + if rank, exists := sourceOrder[source]; exists { + return rank + } + return 4 // Sourced we don't know about can't be assigned special priority, so + // are considered ties. + } + iSource := b[i].Source + jSource := b[j].Source + rankI, rankJ := getRank(iSource), getRank(jSource) + if rankI != rankJ { + return rankI < rankJ + } + + return isMoreSpecific(b[i].Attributes, b[j].Attributes) +} + +func (b BySourceThenSpecificity) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +var _ sort.Interface = (*BySourceThenSpecificity)(nil) diff --git a/syft/cpe/by_source_then_specificity_test.go b/syft/cpe/by_source_then_specificity_test.go new file mode 100644 index 000000000..9687447f9 --- /dev/null +++ b/syft/cpe/by_source_then_specificity_test.go @@ -0,0 +1,74 @@ +package cpe + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBySourceThenSpecificity(t *testing.T) { + tests := []struct { + name string + input []CPE + want []CPE + }{ + { + name: "empty case", + }, + { + name: "nvd before generated", + input: []CPE{ + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", GeneratedSource), + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", NVDDictionaryLookupSource), + }, + want: []CPE{ + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", NVDDictionaryLookupSource), + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", GeneratedSource), + }, + }, + { + name: "declared before generated", + input: []CPE{ + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", GeneratedSource), + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", DeclaredSource), + }, + want: []CPE{ + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", DeclaredSource), + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", GeneratedSource), + }, + }, + { + name: "most specific attributes of equal sources", + input: []CPE{ + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", NVDDictionaryLookupSource), + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", NVDDictionaryLookupSource), + Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*", NVDDictionaryLookupSource), + }, + want: []CPE{ + Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*", NVDDictionaryLookupSource), + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", NVDDictionaryLookupSource), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", NVDDictionaryLookupSource), + }, + }, + { + name: "most specific attributes of unknown sources", + input: []CPE{ + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", ""), + Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*", "some-other-unknown-source"), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", "some-unknown-source"), + }, + want: []CPE{ + Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*", "some-other-unknown-source"), + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", ""), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", "some-unknown-source"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sort.Sort(BySourceThenSpecificity(tt.input)) + assert.Equal(t, tt.want, tt.input) + }) + } +} diff --git a/syft/cpe/by_specificity.go b/syft/cpe/by_specificity.go index 2ab457a68..016d94ec8 100644 --- a/syft/cpe/by_specificity.go +++ b/syft/cpe/by_specificity.go @@ -6,15 +6,22 @@ import ( var _ sort.Interface = (*BySpecificity)(nil) -type BySpecificity []CPE +type BySpecificity []Attributes func (c BySpecificity) Len() int { return len(c) } func (c BySpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c BySpecificity) Less(i, j int) bool { - iScore := weightedCountForSpecifiedFields(c[i]) - jScore := weightedCountForSpecifiedFields(c[j]) + return isMoreSpecific(c[i], c[j]) +} + +// Returns true if i is more specific than j, with some +// tie breaking mechanisms to make sorting equally-specific cpe Attributes +// deterministic. +func isMoreSpecific(i, j Attributes) bool { + iScore := weightedCountForSpecifiedFields(i) + jScore := weightedCountForSpecifiedFields(j) // check weighted sort first if iScore != jScore { @@ -22,28 +29,28 @@ func (c BySpecificity) Less(i, j int) bool { } // sort longer fields to top - if countFieldLength(c[i]) != countFieldLength(c[j]) { - return countFieldLength(c[i]) > countFieldLength(c[j]) + if countFieldLength(i) != countFieldLength(j) { + return countFieldLength(i) > countFieldLength(j) } // if score and length are equal then text sort // note that we are not using String from the syft pkg - // as we are not encoding/decoding this CPE string so we don't - // need the proper quoted version of the CPE. - return c[i].BindToFmtString() < c[j].BindToFmtString() + // as we are not encoding/decoding this Attributes string so we don't + // need the proper quoted version of the Attributes. + return i.BindToFmtString() < j.BindToFmtString() } -func countFieldLength(cpe CPE) int { +func countFieldLength(cpe Attributes) int { return len(cpe.Part + cpe.Vendor + cpe.Product + cpe.Version + cpe.TargetSW) } -func weightedCountForSpecifiedFields(cpe CPE) int { - checksForSpecifiedField := []func(cpe CPE) (bool, int){ - func(cpe CPE) (bool, int) { return cpe.Part != "", 2 }, - func(cpe CPE) (bool, int) { return cpe.Vendor != "", 3 }, - func(cpe CPE) (bool, int) { return cpe.Product != "", 4 }, - func(cpe CPE) (bool, int) { return cpe.Version != "", 1 }, - func(cpe CPE) (bool, int) { return cpe.TargetSW != "", 1 }, +func weightedCountForSpecifiedFields(cpe Attributes) int { + checksForSpecifiedField := []func(cpe Attributes) (bool, int){ + func(cpe Attributes) (bool, int) { return cpe.Part != "", 2 }, + func(cpe Attributes) (bool, int) { return cpe.Vendor != "", 3 }, + func(cpe Attributes) (bool, int) { return cpe.Product != "", 4 }, + func(cpe Attributes) (bool, int) { return cpe.Version != "", 1 }, + func(cpe Attributes) (bool, int) { return cpe.TargetSW != "", 1 }, } weightedCount := 0 diff --git a/syft/cpe/by_specificity_test.go b/syft/cpe/by_specificity_test.go index 33c0d3336..24df8db71 100644 --- a/syft/cpe/by_specificity_test.go +++ b/syft/cpe/by_specificity_test.go @@ -10,81 +10,81 @@ import ( func Test_BySpecificity(t *testing.T) { tests := []struct { name string - input []CPE - expected []CPE + input []Attributes + expected []Attributes }{ { name: "sort strictly by wfn *", - input: []CPE{ - Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), - Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), - Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + input: []Attributes{ + MustAttributes("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), + MustAttributes("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), + MustAttributes("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), }, - expected: []CPE{ - Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), - Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), - Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + expected: []Attributes{ + MustAttributes("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), + MustAttributes("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), + MustAttributes("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), }, }, { name: "sort strictly by field length", - input: []CPE{ - Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), + input: []Attributes{ + MustAttributes("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), }, - expected: []CPE{ - Must("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), - Must("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + expected: []Attributes{ + MustAttributes("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), + MustAttributes("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), }, }, { name: "sort by mix of field length and specificity", - input: []CPE{ - Must("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), - Must("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + input: []Attributes{ + MustAttributes("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), + MustAttributes("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), }, - expected: []CPE{ - Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), - Must("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), - Must("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), + expected: []Attributes{ + MustAttributes("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), + MustAttributes("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), + MustAttributes("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), }, }, { name: "sort by mix of field length, specificity, dash", - input: []CPE{ - Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + input: []Attributes{ + MustAttributes("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), }, - expected: []CPE{ - Must("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + expected: []Attributes{ + MustAttributes("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + MustAttributes("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), }, }, } diff --git a/syft/cpe/cpe.go b/syft/cpe/cpe.go index 99a048636..fe678977d 100644 --- a/syft/cpe/cpe.go +++ b/syft/cpe/cpe.go @@ -8,9 +8,29 @@ import ( "github.com/facebookincubator/nvdtools/wfn" ) +// CPE contains the attributes of an NVD Attributes and a string +// describing where Syft got the Attributes, e.g. generated by heuristics +// vs looked up in the NVD Attributes dictionary +type CPE struct { + Attributes Attributes + Source Source +} + +type Source string + +func (c Source) String() string { + return string(c) +} + +const ( + GeneratedSource Source = "syft-generated" + NVDDictionaryLookupSource Source = "nvd-cpe-dictionary" + DeclaredSource Source = "declared" +) + const Any = "" -type CPE struct { +type Attributes struct { Part string Vendor string Product string @@ -24,19 +44,19 @@ type CPE struct { Language string } -func (c CPE) asAttributes() wfn.Attributes { +func (c Attributes) asAttributes() wfn.Attributes { return wfn.Attributes(c) } -func fromAttributes(a wfn.Attributes) CPE { - return CPE(a) +func fromAttributes(a wfn.Attributes) Attributes { + return Attributes(a) } -func (c CPE) BindToFmtString() string { +func (c Attributes) BindToFmtString() string { return c.asAttributes().BindToFmtString() } -func NewWithAny() CPE { +func NewWithAny() Attributes { return fromAttributes(*(wfn.NewAttributesWithAny())) } @@ -46,36 +66,55 @@ const ( // This regex string is taken from // https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd which has the official cpe spec -// This first part matches CPE urls and the second part matches binding strings +// This first part matches Attributes urls and the second part matches binding strings const cpeRegexString = ((`^([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})`) + - // Or match the CPE binding string + // Or match the Attributes binding string // Note that we had to replace '`' with '\x60' to escape the backticks `|(cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){4})$`) var cpeRegex = regexp.MustCompile(cpeRegexString) -// New will parse a formatted CPE string and return a CPE object. Some input, such as the existence of whitespace -// characters is allowed, however, a more strict validation is done after this sanitization process. -func New(cpeStr string) (CPE, error) { - // get a CPE object based on the given string --don't validate yet since it may be possible to escape select cases on the callers behalf - c, err := newWithoutValidation(cpeStr) +func New(value string, source Source) (CPE, error) { + attributes, err := NewAttributes(value) if err != nil { - return CPE{}, fmt.Errorf("unable to parse CPE string: %w", err) - } - - // ensure that this CPE can be validated after being fully sanitized - if ValidateString(c.String()) != nil { return CPE{}, err } + return CPE{ + Attributes: attributes, + Source: source, + }, nil +} - // we don't return the sanitized string, as this is a concern for later when creating CPE strings. In fact, since +// NewAttributes will parse a formatted Attributes string and return a Attributes object. Some input, such as the existence of whitespace +// characters is allowed, however, a more strict validation is done after this sanitization process. +func NewAttributes(cpeStr string) (Attributes, error) { + // get a Attributes object based on the given string --don't validate yet since it may be possible to escape select cases on the callers behalf + c, err := newWithoutValidation(cpeStr) + if err != nil { + return Attributes{}, fmt.Errorf("unable to parse Attributes string: %w", err) + } + + // ensure that this Attributes can be validated after being fully sanitized + if ValidateString(c.String()) != nil { + return Attributes{}, err + } + + // we don't return the sanitized string, as this is a concern for later when creating Attributes strings. In fact, since // sanitization is lossy (whitespace is replaced, not escaped) it's important that the raw values are left as. return c, nil } // Must returns a CPE or panics if the provided string is not valid -func Must(cpeStr string) CPE { - c, err := New(cpeStr) +func Must(cpeStr string, source Source) CPE { + c := MustAttributes(cpeStr) + return CPE{ + Attributes: c, + Source: source, + } +} + +func MustAttributes(cpeStr string) Attributes { + c, err := NewAttributes(cpeStr) if err != nil { panic(err) } @@ -83,22 +122,22 @@ func Must(cpeStr string) CPE { } func ValidateString(cpeStr string) error { - // We should filter out all CPEs that do not match the official CPE regex - // The facebook nvdtools parser can sometimes incorrectly parse invalid CPE strings + // We should filter out all CPEs that do not match the official Attributes regex + // The facebook nvdtools parser can sometimes incorrectly parse invalid Attributes strings if !cpeRegex.MatchString(cpeStr) { - return fmt.Errorf("failed to parse CPE=%q as it doesn't match the regex=%s", cpeStr, cpeRegexString) + return fmt.Errorf("failed to parse Attributes=%q as it doesn't match the regex=%s", cpeStr, cpeRegexString) } return nil } -func newWithoutValidation(cpeStr string) (CPE, error) { +func newWithoutValidation(cpeStr string) (Attributes, error) { value, err := wfn.Parse(cpeStr) if err != nil { - return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err) + return Attributes{}, fmt.Errorf("failed to parse Attributes=%q: %w", cpeStr, err) } if value == nil { - return CPE{}, fmt.Errorf("failed to parse CPE=%q", cpeStr) + return Attributes{}, fmt.Errorf("failed to parse Attributes=%q", cpeStr) } syftCPE := fromAttributes(*value) @@ -120,7 +159,7 @@ func newWithoutValidation(cpeStr string) (CPE, error) { } func normalizeField(field string) string { - // replace spaces with underscores (per section 5.3.2 of the CPE spec v 2.3) + // replace spaces with underscores (per section 5.3.2 of the Attributes spec v 2.3) field = strings.ReplaceAll(field, " ", "_") // keep dashes and forward slashes unescaped @@ -144,8 +183,8 @@ func stripSlashes(s string) string { return sb.String() } -func (c CPE) String() string { - output := CPE{} +func (c Attributes) String() string { + output := Attributes{} output.Vendor = sanitize(c.Vendor) output.Product = sanitize(c.Product) output.Language = sanitize(c.Language) diff --git a/syft/cpe/cpe_test.go b/syft/cpe/cpe_test.go index d6ed7a505..4b3f60ae0 100644 --- a/syft/cpe/cpe_test.go +++ b/syft/cpe/cpe_test.go @@ -12,38 +12,38 @@ import ( "github.com/stretchr/testify/require" ) -func Test_New(t *testing.T) { +func Test_NewAttributes(t *testing.T) { tests := []struct { name string input string - expected CPE + expected Attributes }{ { name: "gocase", input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`, - expected: Must(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`), + expected: MustAttributes(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`), }, { name: "dashes", input: `cpe:/a:7-zip:7-zip:4.56:beta:~~~windows~~`, - expected: Must(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`), + expected: MustAttributes(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`), }, { name: "URL escape characters", input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`, - expected: Must(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`), + expected: MustAttributes(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual, err := New(test.input) + actual, err := NewAttributes(test.input) if err != nil { - t.Fatalf("got an error while creating CPE: %+v", err) + t.Fatalf("got an error while creating Attributes: %+v", err) } if d := cmp.Diff(actual, test.expected); d != "" { - t.Errorf("CPE mismatch (-want +got):\n%s", d) + t.Errorf("Attributes mismatch (-want +got):\n%s", d) } }) @@ -82,9 +82,9 @@ func Test_normalizeCpeField(t *testing.T) { func Test_CPEParser(t *testing.T) { var testCases []struct { - CPEString string `json:"cpe-string"` - CPEUrl string `json:"cpe-url"` - WFN CPE `json:"wfn"` + CPEString string `json:"cpe-string"` + CPEUrl string `json:"cpe-url"` + WFN Attributes `json:"wfn"` } out, err := os.ReadFile("test-fixtures/cpe-data.json") require.NoError(t, err) @@ -92,9 +92,9 @@ func Test_CPEParser(t *testing.T) { for _, test := range testCases { t.Run(test.CPEString, func(t *testing.T) { - c1, err := New(test.CPEString) + c1, err := NewAttributes(test.CPEString) assert.NoError(t, err) - c2, err := New(test.CPEUrl) + c2, err := NewAttributes(test.CPEUrl) assert.NoError(t, err) assert.Equal(t, c1, c2) assert.Equal(t, c1, test.WFN) @@ -161,11 +161,11 @@ func Test_InvalidCPE(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - c, err := New(test.in) + c, err := NewAttributes(test.in) if test.expectedErr { assert.Error(t, err) if t.Failed() { - t.Logf("got CPE: %q details: %+v", c, c) + t.Logf("got Attributes: %q details: %+v", c, c) } return } @@ -179,12 +179,12 @@ func Test_RoundTrip(t *testing.T) { tests := []struct { name string cpe string - parsedCPE CPE + parsedCPE Attributes }{ { name: "normal", cpe: "cpe:2.3:a:some-vendor:name:3.2:*:*:*:*:*:*:*", - parsedCPE: CPE{ + parsedCPE: Attributes{ Part: "a", Vendor: "some-vendor", Product: "name", @@ -194,7 +194,7 @@ func Test_RoundTrip(t *testing.T) { { name: "escaped colon", cpe: "cpe:2.3:a:some-vendor:name:1\\:3.2:*:*:*:*:*:*:*", - parsedCPE: CPE{ + parsedCPE: Attributes{ Part: "a", Vendor: "some-vendor", Product: "name", @@ -204,7 +204,7 @@ func Test_RoundTrip(t *testing.T) { { name: "escaped forward slash", cpe: "cpe:2.3:a:test\\/some-vendor:name:3.2:*:*:*:*:*:*:*", - parsedCPE: CPE{ + parsedCPE: Attributes{ Part: "a", Vendor: "test/some-vendor", Product: "name", @@ -215,13 +215,13 @@ func Test_RoundTrip(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - // CPE string must be preserved through a round trip - assert.Equal(t, test.cpe, Must(test.cpe).String()) - // The parsed CPE must be the same after a round trip - assert.Equal(t, Must(test.cpe), Must(Must(test.cpe).String())) - // The test case parsed CPE must be the same after parsing the input string - assert.Equal(t, test.parsedCPE, Must(test.cpe)) - // The test case parsed CPE must produce the same string as the input cpe + // Attributes string must be preserved through a round trip + assert.Equal(t, test.cpe, MustAttributes(test.cpe).String()) + // The parsed Attributes must be the same after a round trip + assert.Equal(t, MustAttributes(test.cpe), MustAttributes(MustAttributes(test.cpe).String())) + // The test case parsed Attributes must be the same after parsing the input string + assert.Equal(t, test.parsedCPE, MustAttributes(test.cpe)) + // The test case parsed Attributes must produce the same string as the input cpe assert.Equal(t, test.parsedCPE.String(), test.cpe) }) } diff --git a/syft/cpe/merge_cpes.go b/syft/cpe/merge_cpes.go index 5c5c7daf2..951869145 100644 --- a/syft/cpe/merge_cpes.go +++ b/syft/cpe/merge_cpes.go @@ -1,25 +1,27 @@ package cpe import ( + "fmt" "sort" ) -func Merge(a, b []CPE) (result []CPE) { - aCPEs := make(map[string]CPE) - - // keep all CPEs from a and create a quick string-based lookup - for _, aCPE := range a { - aCPEs[aCPE.BindToFmtString()] = aCPE - result = append(result, aCPE) +// Merge returns unique SourcedCPEs that are found in A or B +// Two SourcedCPEs are identical if their source and normalized string are identical +func Merge(a, b []CPE) []CPE { + var result []CPE + dedupe := make(map[string]CPE) + key := func(scpe CPE) string { + return fmt.Sprintf("%s:%s", scpe.Source.String(), scpe.Attributes.BindToFmtString()) } - - // keep all unique CPEs from b - for _, bCPE := range b { - if _, exists := aCPEs[bCPE.BindToFmtString()]; !exists { - result = append(result, bCPE) - } + for _, s := range a { + dedupe[key(s)] = s } - - sort.Sort(BySpecificity(result)) + for _, s := range b { + dedupe[key(s)] = s + } + for _, val := range dedupe { + result = append(result, val) + } + sort.Sort(BySourceThenSpecificity(result)) return result } diff --git a/syft/cpe/merge_cpes_test.go b/syft/cpe/merge_cpes_test.go index 80ef32e7a..747affcf4 100644 --- a/syft/cpe/merge_cpes_test.go +++ b/syft/cpe/merge_cpes_test.go @@ -16,18 +16,20 @@ func Test_Merge(t *testing.T) { name: "merge, removing duplicates and ordered", input: [][]CPE{ { - Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*", NVDDictionaryLookupSource), + Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*", DeclaredSource), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", GeneratedSource), }, { - Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", DeclaredSource), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", GeneratedSource), }, }, expected: []CPE{ - Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), - Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*", NVDDictionaryLookupSource), + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", DeclaredSource), + Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*", DeclaredSource), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", GeneratedSource), }, }, } diff --git a/syft/format/common/cyclonedxhelpers/to_format_model.go b/syft/format/common/cyclonedxhelpers/to_format_model.go index d408b6b45..15f34a71d 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model.go @@ -103,7 +103,7 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component { } func formatCPE(cpeString string) string { - c, err := cpe.New(cpeString) + c, err := cpe.NewAttributes(cpeString) if err != nil { log.Debugf("skipping invalid CPE: %s", cpeString) return "" diff --git a/syft/format/common/spdxhelpers/to_syft_model.go b/syft/format/common/spdxhelpers/to_syft_model.go index 3a59ca7ac..fe045887d 100644 --- a/syft/format/common/spdxhelpers/to_syft_model.go +++ b/syft/format/common/spdxhelpers/to_syft_model.go @@ -630,7 +630,7 @@ func findPURLValue(p *spdx.Package) string { func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) { for _, r := range p.PackageExternalReferences { if r.RefType == string(helpers.Cpe23ExternalRefType) { - c, err := cpe.New(r.Locator) + c, err := cpe.New(r.Locator, cpe.DeclaredSource) if err != nil { log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err) continue diff --git a/syft/format/internal/cyclonedxutil/helpers/cpe.go b/syft/format/internal/cyclonedxutil/helpers/cpe.go index 439aeb39f..5eb1082a3 100644 --- a/syft/format/internal/cyclonedxutil/helpers/cpe.go +++ b/syft/format/internal/cyclonedxutil/helpers/cpe.go @@ -12,7 +12,7 @@ func encodeSingleCPE(p pkg.Package) string { // Since the CPEs in a package are sorted by specificity // we can extract the first CPE as the one to output in cyclonedx if len(p.CPEs) > 0 { - return p.CPEs[0].String() + return p.CPEs[0].Attributes.String() } return "" } @@ -25,7 +25,7 @@ func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) { } out = append(out, cyclonedx.Property{ Name: "syft:cpe23", - Value: c.String(), + Value: c.Attributes.String(), }) } return @@ -33,7 +33,7 @@ func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) { func decodeCPEs(c *cyclonedx.Component) (out []cpe.CPE) { if c.CPE != "" { - cp, err := cpe.New(c.CPE) + cp, err := cpe.New(c.CPE, cpe.DeclaredSource) if err != nil { log.Warnf("invalid CPE: %s", c.CPE) } else { @@ -44,7 +44,7 @@ func decodeCPEs(c *cyclonedx.Component) (out []cpe.CPE) { if c.Properties != nil { for _, p := range *c.Properties { if p.Name == "syft:cpe23" { - cp, err := cpe.New(p.Value) + cp, err := cpe.New(p.Value, cpe.DeclaredSource) if err != nil { log.Warnf("invalid CPE: %s", p.Value) } else { diff --git a/syft/format/internal/cyclonedxutil/helpers/cpe_test.go b/syft/format/internal/cyclonedxutil/helpers/cpe_test.go index 192508de5..40a67dd4c 100644 --- a/syft/format/internal/cyclonedxutil/helpers/cpe_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/cpe_test.go @@ -10,8 +10,8 @@ import ( ) func Test_encodeCPE(t *testing.T) { - testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") - testCPE2 := cpe.Must("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*") + testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "test-source") + testCPE2 := cpe.Must("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*", "test-source-2") tests := []struct { name string input pkg.Package @@ -26,7 +26,7 @@ func Test_encodeCPE(t *testing.T) { expected: "", }, { - name: "single CPE", + name: "single Attributes", input: pkg.Package{ CPEs: []cpe.CPE{ testCPE, diff --git a/syft/format/internal/cyclonedxutil/helpers/decoder_test.go b/syft/format/internal/cyclonedxutil/helpers/decoder_test.go index c13198c79..af08bac2d 100644 --- a/syft/format/internal/cyclonedxutil/helpers/decoder_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/decoder_test.go @@ -222,7 +222,7 @@ func Test_decode(t *testing.T) { if e.cpe != "" { foundCPE := false for _, c := range p.CPEs { - cstr := c.BindToFmtString() + cstr := c.Attributes.BindToFmtString() if e.cpe == cstr { foundCPE = true break diff --git a/syft/format/internal/spdxutil/helpers/external_refs.go b/syft/format/internal/spdxutil/helpers/external_refs.go index 0c41f3e2c..25b89b5eb 100644 --- a/syft/format/internal/spdxutil/helpers/external_refs.go +++ b/syft/format/internal/spdxutil/helpers/external_refs.go @@ -10,7 +10,7 @@ func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) { for _, c := range p.CPEs { externalRefs = append(externalRefs, ExternalRef{ ReferenceCategory: SecurityReferenceCategory, - ReferenceLocator: c.String(), + ReferenceLocator: c.Attributes.String(), ReferenceType: Cpe23ExternalRefType, }) } diff --git a/syft/format/internal/spdxutil/helpers/external_refs_test.go b/syft/format/internal/spdxutil/helpers/external_refs_test.go index c3aa900a8..489957c4c 100644 --- a/syft/format/internal/spdxutil/helpers/external_refs_test.go +++ b/syft/format/internal/spdxutil/helpers/external_refs_test.go @@ -10,7 +10,7 @@ import ( ) func Test_ExternalRefs(t *testing.T) { - testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") + testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", cpe.Source("")) tests := []struct { name string input pkg.Package @@ -27,7 +27,7 @@ func Test_ExternalRefs(t *testing.T) { expected: []ExternalRef{ { ReferenceCategory: SecurityReferenceCategory, - ReferenceLocator: testCPE.String(), + ReferenceLocator: testCPE.Attributes.String(), ReferenceType: Cpe23ExternalRefType, }, { diff --git a/syft/format/internal/testutil/directory_input.go b/syft/format/internal/testutil/directory_input.go index 27b4ab3d5..38279c3d4 100644 --- a/syft/format/internal/testutil/directory_input.go +++ b/syft/format/internal/testutil/directory_input.go @@ -123,7 +123,7 @@ func newDirectoryCatalog() *pkg.Collection { }, PURL: "a-purl-2", // intentionally a bad pURL for test fixtures CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.Source("")), }, }) catalog.Add(pkg.Package{ @@ -140,7 +140,7 @@ func newDirectoryCatalog() *pkg.Collection { }, PURL: "pkg:deb/debian/package-2@2.0.1", CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.Source("")), }, }) @@ -175,7 +175,7 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection { }, PURL: "a-purl-2", // intentionally a bad pURL for test fixtures CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }) catalog.Add(pkg.Package{ @@ -192,7 +192,7 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection { }, PURL: "pkg:deb/debian/package-2@2.0.1", CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", "another-test-source"), }, }) diff --git a/syft/format/internal/testutil/image_input.go b/syft/format/internal/testutil/image_input.go index 099a622fd..a7757f7d7 100644 --- a/syft/format/internal/testutil/image_input.go +++ b/syft/format/internal/testutil/image_input.go @@ -119,7 +119,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) { }, PURL: "a-purl-1", // intentionally a bad pURL for test fixtures CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }) } @@ -139,7 +139,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) { }, PURL: "pkg:deb/debian/package-2@2.0.1", CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), }, }) } diff --git a/syft/format/syftjson/decoder_test.go b/syft/format/syftjson/decoder_test.go index 9e3a98729..75f53eca3 100644 --- a/syft/format/syftjson/decoder_test.go +++ b/syft/format/syftjson/decoder_test.go @@ -159,11 +159,14 @@ func Test_encodeDecodeFileMetadata(t *testing.T) { Type: "type", CPEs: []cpe.CPE{ { - Part: "a", - Vendor: "vendor", - Product: "product", - Version: "version", - Update: "update", + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "vendor", + Product: "product", + Version: "version", + Update: "update", + }, + Source: "test-source", }, }, PURL: "pkg:generic/pkg@version", diff --git a/syft/format/syftjson/encoder_test.go b/syft/format/syftjson/encoder_test.go index 643462173..58f356c9f 100644 --- a/syft/format/syftjson/encoder_test.go +++ b/syft/format/syftjson/encoder_test.go @@ -152,7 +152,7 @@ func TestEncodeFullJSONDocument(t *testing.T) { }, PURL: "a-purl-1", CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource), }, } @@ -173,7 +173,7 @@ func TestEncodeFullJSONDocument(t *testing.T) { }, PURL: "a-purl-2", CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.GeneratedSource), }, } diff --git a/syft/format/syftjson/model/package.go b/syft/format/syftjson/model/package.go index f655ad121..503709d1f 100644 --- a/syft/format/syftjson/model/package.go +++ b/syft/format/syftjson/model/package.go @@ -32,10 +32,17 @@ type PackageBasicData struct { Locations []file.Location `json:"locations"` Licenses licenses `json:"licenses"` Language pkg.Language `json:"language"` - CPEs []string `json:"cpes"` + CPEs cpes `json:"cpes"` PURL string `json:"purl"` } +type cpes []CPE + +type CPE struct { + Value string `json:"cpe"` + Source string `json:"source,omitempty"` +} + type licenses []License type License struct { @@ -74,6 +81,29 @@ func (f *licenses) UnmarshalJSON(b []byte) error { return nil } +func sourcedCPESfromSimpleCPEs(simpleCPEs []string) []CPE { + var result []CPE + for _, s := range simpleCPEs { + result = append(result, CPE{ + Value: s, + }) + } + return result +} + +func (c *cpes) UnmarshalJSON(b []byte) error { + var cs []CPE + if err := json.Unmarshal(b, &cs); err != nil { + var simpleCPEs []string + if err := json.Unmarshal(b, &simpleCPEs); err != nil { + return fmt.Errorf("unable to unmarshal cpes: %w", err) + } + cs = sourcedCPESfromSimpleCPEs(simpleCPEs) + } + *c = cs + return nil +} + // PackageCustomData contains ambiguous values (type-wise) from pkg.Package. type PackageCustomData struct { MetadataType string `json:"metadataType,omitempty"` diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index 24b06d655..a5eae3e3e 100644 --- a/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -23,7 +23,9 @@ ], "language": "python", "cpes": [ - "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + { + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + } ], "purl": "a-purl-2", "metadataType": "python-package", @@ -56,7 +58,9 @@ "licenses": [], "language": "", "cpes": [ - "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + { + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + } ], "purl": "pkg:deb/debian/package-2@2.0.1", "metadataType": "dpkg-db-entry", diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index e70e147bb..00abfc0d1 100644 --- a/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -23,7 +23,10 @@ ], "language": "python", "cpes": [ - "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + { + "cpe": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", + "source": "nvd-cpe-dictionary" + } ], "purl": "a-purl-1", "metadataType": "python-package", @@ -51,7 +54,10 @@ "licenses": [], "language": "", "cpes": [ - "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + { + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "source": "syft-generated" + } ], "purl": "a-purl-2", "metadataType": "dpkg-db-entry", diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index ab42e1183..104e5a749 100644 --- a/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -24,7 +24,10 @@ ], "language": "python", "cpes": [ - "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + { + "cpe": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", + "source": "syft-generated" + } ], "purl": "a-purl-1", "metadataType": "python-package", @@ -53,7 +56,10 @@ "licenses": [], "language": "", "cpes": [ - "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + { + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "source": "nvd-cpe-dictionary" + } ], "purl": "pkg:deb/debian/package-2@2.0.1", "metadataType": "dpkg-db-entry", diff --git a/syft/format/syftjson/to_format_model.go b/syft/format/syftjson/to_format_model.go index 017d997bd..eef3eca85 100644 --- a/syft/format/syftjson/to_format_model.go +++ b/syft/format/syftjson/to_format_model.go @@ -229,9 +229,13 @@ func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) { // toPackageModel crates a new Package from the given pkg.Package. func toPackageModel(p pkg.Package, cfg EncoderConfig) model.Package { - var cpes = make([]string, len(p.CPEs)) + var cpes = make([]model.CPE, len(p.CPEs)) for i, c := range p.CPEs { - cpes[i] = c.String() + convertedCPE := model.CPE{ + Value: c.Attributes.String(), + Source: c.Source.String(), + } + cpes[i] = convertedCPE } // we want to make sure all catalogers are diff --git a/syft/format/syftjson/to_syft_model.go b/syft/format/syftjson/to_syft_model.go index 8807688e3..fda73d5fc 100644 --- a/syft/format/syftjson/to_syft_model.go +++ b/syft/format/syftjson/to_syft_model.go @@ -301,9 +301,9 @@ func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Colle func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package { var cpes []cpe.CPE for _, c := range p.CPEs { - value, err := cpe.New(c) + value, err := cpe.New(c.Value, cpe.Source(c.Source)) if err != nil { - log.Warnf("excluding invalid CPE %q: %v", c, err) + log.Warnf("excluding invalid Attributes %q: %v", c, err) continue } diff --git a/syft/pkg/cataloger/binary/cataloger_test.go b/syft/pkg/cataloger/binary/cataloger_test.go index 8625615e6..e59f5a9de 100644 --- a/syft/pkg/cataloger/binary/cataloger_test.go +++ b/syft/pkg/cataloger/binary/cataloger_test.go @@ -1282,7 +1282,7 @@ func TestCatalogerConfig_MarshalJSON(t *testing.T) { Qualifiers: nil, Subpath: "subpath", }, - CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*")}, + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource)}, }, }, }, diff --git a/syft/pkg/cataloger/binary/classifier.go b/syft/pkg/cataloger/binary/classifier.go index d3aeb56d4..b8261c44d 100644 --- a/syft/pkg/cataloger/binary/classifier.go +++ b/syft/pkg/cataloger/binary/classifier.go @@ -56,7 +56,7 @@ func (cfg Classifier) MarshalJSON() ([]byte, error) { var marshalledCPEs []string for _, c := range cfg.CPEs { - marshalledCPEs = append(marshalledCPEs, c.BindToFmtString()) + marshalledCPEs = append(marshalledCPEs, c.Attributes.BindToFmtString()) } m := marshalled{ @@ -225,10 +225,11 @@ func getContents(resolver file.Resolver, location file.Location) ([]byte, error) return contents, nil } -// singleCPE returns a []pkg.CPE based on the cpe string or panics if the CPE is invalid +// singleCPE returns a []cpe.CPE with Source: Generated based on the cpe string or panics if the +// cpe string cannot be parsed into valid CPE Attributes func singleCPE(cpeString string) []cpe.CPE { return []cpe.CPE{ - cpe.Must(cpeString), + cpe.Must(cpeString, cpe.GeneratedSource), } } diff --git a/syft/pkg/cataloger/binary/classifier_test.go b/syft/pkg/cataloger/binary/classifier_test.go index eb37dd2c8..2a2a7b16b 100644 --- a/syft/pkg/cataloger/binary/classifier_test.go +++ b/syft/pkg/cataloger/binary/classifier_test.go @@ -30,14 +30,14 @@ func Test_ClassifierCPEs(t *testing.T) { cpes: nil, }, { - name: "one CPE", + name: "one Attributes", fixture: "test-fixtures/version.txt", classifier: Classifier{ Package: "some-app", FileGlob: "**/version.txt", EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P[0-9.]+)`), CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }, cpes: []string{ @@ -52,8 +52,8 @@ func Test_ClassifierCPEs(t *testing.T) { FileGlob: "**/version.txt", EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P[0-9.]+)`), CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"), - cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource), + cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }, cpes: []string{ @@ -79,7 +79,7 @@ func Test_ClassifierCPEs(t *testing.T) { var cpes []string for _, c := range p.CPEs { - cpes = append(cpes, c.String()) + cpes = append(cpes, c.Attributes.String()) } require.Equal(t, test.cpes, cpes) }) @@ -109,7 +109,7 @@ func TestClassifier_MarshalJSON(t *testing.T) { Qualifiers: nil, Subpath: "subpath", }, - CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*")}, + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource)}, }, want: `{"class":"class","fileGlob":"glob","package":"pkg","purl":"pkg:type/namespace/name@version#subpath","cpes":["cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"]}`, }, diff --git a/syft/pkg/cataloger/binary/default_classifiers.go b/syft/pkg/cataloger/binary/default_classifiers.go index ca4992919..381d08df2 100644 --- a/syft/pkg/cataloger/binary/default_classifiers.go +++ b/syft/pkg/cataloger/binary/default_classifiers.go @@ -23,8 +23,8 @@ func DefaultClassifiers() []Classifier { Package: "python", PURL: mustPURL("pkg:generic/python@version"), CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*"), - cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*", cpe.GeneratedSource), + cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }, { @@ -34,8 +34,8 @@ func DefaultClassifiers() []Classifier { Package: "python", PURL: mustPURL("pkg:generic/python@version"), CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*"), - cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*", cpe.GeneratedSource), + cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }, { @@ -93,7 +93,7 @@ func DefaultClassifiers() []Classifier { `(?m)\x00openjdk\x00java\x00(?P[0-9]+[.0-9]*)\x00(?P[0-9]+[^\x00]+)\x00`), Package: "java", PURL: mustPURL("pkg:generic/java@version"), - // TODO the updates might need to be part of the CPE, like: 1.8.0:update152 + // TODO the updates might need to be part of the CPE Attributes, like: 1.8.0:update152 CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:*:*:*:*:*:*:*:*"), }, { @@ -255,8 +255,8 @@ func DefaultClassifiers() []Classifier { Package: "percona-server", PURL: mustPURL("pkg:generic/percona-server@version"), CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*"), - cpe.Must("cpe:2.3:a:percona:percona_server:*:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*", cpe.GeneratedSource), + cpe.Must("cpe:2.3:a:percona:percona_server:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }, { @@ -267,9 +267,9 @@ func DefaultClassifiers() []Classifier { Package: "percona-xtradb-cluster", PURL: mustPURL("pkg:generic/percona-xtradb-cluster@version"), CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*"), - cpe.Must("cpe:2.3:a:percona:percona_server:*:*:*:*:*:*:*:*"), - cpe.Must("cpe:2.3:a:percona:xtradb_cluster:*:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*", cpe.GeneratedSource), + cpe.Must("cpe:2.3:a:percona:percona_server:*:*:*:*:*:*:*:*", cpe.GeneratedSource), + cpe.Must("cpe:2.3:a:percona:xtradb_cluster:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }, { @@ -363,8 +363,8 @@ func DefaultClassifiers() []Classifier { Package: "nginx", PURL: mustPURL("pkg:generic/nginx@version"), CPEs: []cpe.CPE{ - cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*"), - cpe.Must("cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*", cpe.GeneratedSource), + cpe.Must("cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }, { diff --git a/syft/pkg/cataloger/binary/package.go b/syft/pkg/cataloger/binary/package.go index 125918c84..f3326e2b6 100644 --- a/syft/pkg/cataloger/binary/package.go +++ b/syft/pkg/cataloger/binary/package.go @@ -21,8 +21,8 @@ func newPackage(classifier Classifier, location file.Location, matchMetadata map var cpes []cpe.CPE for _, c := range classifier.CPEs { - c.Version = version - c.Update = update + c.Attributes.Version = version + c.Attributes.Update = update cpes = append(cpes, c) } diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go index 9b1131a1a..183594256 100644 --- a/syft/pkg/cataloger/golang/cataloger.go +++ b/syft/pkg/cataloger/golang/cataloger.go @@ -127,5 +127,5 @@ func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) { cpeString = fmt.Sprintf("cpe:2.3:a:golang:go:%s:%s:*:*:*:*:*:*", vr, candidate) } - return cpe.New(cpeString) + return cpe.New(cpeString, cpe.GeneratedSource) } diff --git a/syft/pkg/cataloger/golang/cataloger_test.go b/syft/pkg/cataloger/golang/cataloger_test.go index 067efd11d..240944539 100644 --- a/syft/pkg/cataloger/golang/cataloger_test.go +++ b/syft/pkg/cataloger/golang/cataloger_test.go @@ -81,7 +81,7 @@ func Test_Binary_Cataloger_Stdlib_Cpe(t *testing.T) { t.Run(tc.name, func(t *testing.T) { got, err := generateStdlibCpe(tc.candidate) assert.NoError(t, err, "expected no err; got %v", err) - assert.Equal(t, got.String(), tc.want) + assert.Equal(t, got.Attributes.String(), tc.want) }) } } diff --git a/syft/pkg/cataloger/internal/cpegenerate/apk.go b/syft/pkg/cataloger/internal/cpegenerate/apk.go index 337bb1494..fa5f78cb1 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/apk.go +++ b/syft/pkg/cataloger/internal/cpegenerate/apk.go @@ -23,7 +23,7 @@ type upstreamCandidate struct { } func upstreamCandidates(m pkg.ApkDBEntry) (candidates []upstreamCandidate) { - // Do not consider OriginPackage variations when generating CPE candidates for the child package + // Do not consider OriginPackage variations when generating CPE Attributes candidates for the child package // because doing so will result in false positives when matching to vulnerabilities in Grype since // it won't know to lookup apk fix entries using the OriginPackage name. diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/index-generator/generate.go b/syft/pkg/cataloger/internal/cpegenerate/dictionary/index-generator/generate.go index 2d7f975ae..23868e30f 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/index-generator/generate.go +++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/index-generator/generate.go @@ -90,7 +90,7 @@ func filterCpeList(cpeList CpeList) CpeList { return processedCpeList } -// normalizeCPE removes the version and update parts of a CPE. +// normalizeCPE removes the version and update parts of CPE Attributes. func normalizeCPE(cpe *wfn.Attributes) *wfn.Attributes { cpeCopy := *cpe diff --git a/syft/pkg/cataloger/internal/cpegenerate/filter.go b/syft/pkg/cataloger/internal/cpegenerate/filter.go index f84ce93e9..8423c320b 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/filter.go +++ b/syft/pkg/cataloger/internal/cpegenerate/filter.go @@ -9,8 +9,8 @@ import ( const jenkinsName = "jenkins" -// filterFn instances should return true if the given CPE should be removed from a collection for the given package -type filterFn func(cpe cpe.CPE, p pkg.Package) bool +// filterFn instances should return true if the given CPE attributes should be removed from a collection for the given package +type filterFn func(cpe cpe.Attributes, p pkg.Package) bool var cpeFilters = []filterFn{ disallowJiraClientServerMismatch, @@ -19,7 +19,7 @@ var cpeFilters = []filterFn{ disallowNonParseableCPEs, } -func filter(cpes []cpe.CPE, p pkg.Package, filters ...filterFn) (result []cpe.CPE) { +func filter(cpes []cpe.Attributes, p pkg.Package, filters ...filterFn) (result []cpe.Attributes) { cpeLoop: for _, c := range cpes { for _, fn := range filters { @@ -33,9 +33,9 @@ cpeLoop: return result } -func disallowNonParseableCPEs(c cpe.CPE, _ pkg.Package) bool { +func disallowNonParseableCPEs(c cpe.Attributes, _ pkg.Package) bool { v := c.String() - _, err := cpe.New(v) + _, err := cpe.NewAttributes(v) cannotParse := err != nil @@ -43,15 +43,15 @@ func disallowNonParseableCPEs(c cpe.CPE, _ pkg.Package) bool { } // jenkins plugins should not match against jenkins -func disallowJenkinsServerCPEForPluginPackage(c cpe.CPE, p pkg.Package) bool { +func disallowJenkinsServerCPEForPluginPackage(c cpe.Attributes, p pkg.Package) bool { if p.Type == pkg.JenkinsPluginPkg && c.Product == jenkinsName { return true } return false } -// filter to account that packages that are not for jenkins but have a CPE generated that will match against jenkins -func disallowJenkinsCPEsNotAssociatedWithJenkins(c cpe.CPE, p pkg.Package) bool { +// filter to account that packages that are not for jenkins but have a Attributes generated that will match against jenkins +func disallowJenkinsCPEsNotAssociatedWithJenkins(c cpe.Attributes, p pkg.Package) bool { // jenkins server should only match against a product with the name jenkins if c.Product == jenkinsName && !strings.Contains(strings.ToLower(p.Name), jenkinsName) { if c.Vendor == cpe.Any || c.Vendor == jenkinsName || c.Vendor == "cloudbees" { @@ -61,8 +61,8 @@ func disallowJenkinsCPEsNotAssociatedWithJenkins(c cpe.CPE, p pkg.Package) bool return false } -// filter to account for packages which are jira client packages but have a CPE that will match against jira -func disallowJiraClientServerMismatch(c cpe.CPE, p pkg.Package) bool { +// filter to account for packages which are jira client packages but have a Attributes that will match against jira +func disallowJiraClientServerMismatch(c cpe.Attributes, p pkg.Package) bool { // jira / atlassian should not apply to clients if c.Product == "jira" && strings.Contains(strings.ToLower(p.Name), "client") { if c.Vendor == cpe.Any || c.Vendor == "jira" || c.Vendor == "atlassian" { diff --git a/syft/pkg/cataloger/internal/cpegenerate/filter_test.go b/syft/pkg/cataloger/internal/cpegenerate/filter_test.go index f5c252193..0e39a0371 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/filter_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/filter_test.go @@ -18,7 +18,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) { }{ { name: "go case (filter out)", - cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Type: pkg.JenkinsPluginPkg, }, @@ -26,7 +26,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) { }, { name: "ignore jenkins plugins with unique name", - cpe: cpe.Must("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Type: pkg.JenkinsPluginPkg, }, @@ -34,7 +34,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) { }, { name: "ignore java packages", - cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Type: pkg.JavaPkg, }, @@ -43,7 +43,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, disallowJenkinsServerCPEForPluginPackage(test.cpe, test.pkg)) + assert.Equal(t, test.expected, disallowJenkinsServerCPEForPluginPackage(test.cpe.Attributes, test.pkg)) }) } } @@ -57,7 +57,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }{ { name: "filter out mismatched name (cloudbees vendor)", - cpe: cpe.Must("cpe:2.3:a:cloudbees:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:cloudbees:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "not-j*nkins", Type: pkg.JavaPkg, @@ -66,7 +66,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }, { name: "filter out mismatched name (jenkins vendor)", - cpe: cpe.Must("cpe:2.3:a:jenkins:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:jenkins:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "not-j*nkins", Type: pkg.JavaPkg, @@ -75,7 +75,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }, { name: "filter out mismatched name (any vendor)", - cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "not-j*nkins", Type: pkg.JavaPkg, @@ -84,7 +84,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }, { name: "ignore packages with the name jenkins", - cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "jenkins-thing", Type: pkg.JavaPkg, @@ -93,7 +93,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }, { name: "ignore product names that are not exactly 'jenkins'", - cpe: cpe.Must("cpe:2.3:a:*:jenkins-something-else:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jenkins-something-else:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "not-j*nkins", Type: pkg.JavaPkg, @@ -103,7 +103,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, disallowJenkinsCPEsNotAssociatedWithJenkins(test.cpe, test.pkg)) + assert.Equal(t, test.expected, disallowJenkinsCPEsNotAssociatedWithJenkins(test.cpe.Attributes, test.pkg)) }) } } @@ -117,7 +117,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }{ { name: "filter out mismatched name (atlassian vendor)", - cpe: cpe.Must("cpe:2.3:a:atlassian:jira:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:atlassian:jira:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "something-client", Type: pkg.JavaPkg, @@ -126,7 +126,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }, { name: "filter out mismatched name (jira vendor)", - cpe: cpe.Must("cpe:2.3:a:jira:jira:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:jira:jira:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "something-client", Type: pkg.JavaPkg, @@ -135,7 +135,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }, { name: "filter out mismatched name (any vendor)", - cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "something-client", Type: pkg.JavaPkg, @@ -144,7 +144,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }, { name: "ignore package names that do not have 'client'", - cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "jira-thing", Type: pkg.JavaPkg, @@ -153,7 +153,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }, { name: "ignore product names that are not exactly 'jira'", - cpe: cpe.Must("cpe:2.3:a:*:jira-something-else:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jira-something-else:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource), pkg: pkg.Package{ Name: "not-j*ra", Type: pkg.JavaPkg, @@ -163,7 +163,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, disallowJiraClientServerMismatch(test.cpe, test.pkg)) + assert.Equal(t, test.expected, disallowJiraClientServerMismatch(test.cpe.Attributes, test.pkg)) }) } } diff --git a/syft/pkg/cataloger/internal/cpegenerate/generate.go b/syft/pkg/cataloger/internal/cpegenerate/generate.go index 0199f3ec7..6c3a40050 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/generate.go +++ b/syft/pkg/cataloger/internal/cpegenerate/generate.go @@ -22,7 +22,7 @@ import ( // the CPE database, so they will be preferred over other candidates: var knownVendors = strset.New("apache") -func newCPE(product, vendor, version, targetSW string) *cpe.CPE { +func newCPE(product, vendor, version, targetSW string) *cpe.Attributes { c := cpe.NewWithAny() c.Part = "a" c.Product = product @@ -61,7 +61,7 @@ func GetIndexedDictionary() (_ *dictionary.Indexed, err error) { func FromDictionaryFind(p pkg.Package) (cpe.CPE, bool) { dict, err := GetIndexedDictionary() if err != nil { - log.Debugf("dictionary CPE lookup not available: %+v", err) + log.Debugf("CPE dictionary lookup not available: %+v", err) return cpe.CPE{}, false } @@ -96,12 +96,12 @@ func FromDictionaryFind(p pkg.Package) (cpe.CPE, bool) { return cpe.CPE{}, false } - parsedCPE, err := cpe.New(cpeString) + parsedCPE, err := cpe.New(cpeString, cpe.NVDDictionaryLookupSource) if err != nil { return cpe.CPE{}, false } - parsedCPE.Version = p.Version + parsedCPE.Attributes.Version = p.Version return parsedCPE, true } @@ -117,7 +117,7 @@ func FromPackageAttributes(p pkg.Package) []cpe.CPE { } keys := strset.New() - cpes := make([]cpe.CPE, 0) + cpes := make([]cpe.Attributes, 0) for _, product := range products { for _, vendor := range vendors { // prevent duplicate entries... @@ -137,8 +137,12 @@ func FromPackageAttributes(p pkg.Package) []cpe.CPE { cpes = filter(cpes, p, cpeFilters...) sort.Sort(cpe.BySpecificity(cpes)) + var result []cpe.CPE + for _, c := range cpes { + result = append(result, cpe.CPE{Attributes: c, Source: cpe.GeneratedSource}) + } - return cpes + return result } func candidateVendors(p pkg.Package) []string { diff --git a/syft/pkg/cataloger/internal/cpegenerate/generate_test.go b/syft/pkg/cataloger/internal/cpegenerate/generate_test.go index 04b8c82bb..7674b6abd 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/generate_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/generate_test.go @@ -723,11 +723,14 @@ func TestGeneratePackageCPEs(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := FromPackageAttributes(test.p) + expectedCpeSet := set.NewStringSet() + for _, cpeStr := range test.expected { + expectedCpeSet.Add("syft-generated:" + cpeStr) + } - expectedCpeSet := set.NewStringSet(test.expected...) actualCpeSet := set.NewStringSet() for _, a := range actual { - actualCpeSet.Add(a.String()) + actualCpeSet.Add(fmt.Sprintf("%s:%s", a.Source.String(), a.Attributes.String())) } extra := strset.Difference(actualCpeSet, expectedCpeSet).List() @@ -1007,7 +1010,7 @@ func TestDictionaryFindIsWired(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, gotExists := FromDictionaryFind(tt.pkg) - assert.Equal(t, tt.want, got.BindToFmtString()) + assert.Equal(t, tt.want, got.Attributes.BindToFmtString()) assert.Equal(t, tt.wantExists, gotExists) }) } diff --git a/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go b/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go index c21650252..8c4fc7959 100644 --- a/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go +++ b/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go @@ -117,9 +117,9 @@ func (c *nativeImageCataloger) Name() string { func getPackage(component nativeImageComponent) pkg.Package { var cpes []cpe.CPE for _, property := range component.Properties { - c, err := cpe.New(property.Value) + c, err := cpe.New(property.Value, cpe.DeclaredSource) if err != nil { - log.Debugf("unable to parse CPE: %v", err) + log.Debugf("unable to parse Attributes: %v", err) continue } cpes = append(cpes, c) diff --git a/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go b/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go index 6680c960b..73893402a 100644 --- a/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go +++ b/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go @@ -80,22 +80,31 @@ func TestParseNativeImageSbom(t *testing.T) { }, CPEs: []cpe.CPE{ { - Part: "a", - Vendor: "codec", - Product: "codec", - Version: "4.1.73.Final", + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "codec", + Product: "codec", + Version: "4.1.73.Final", + }, + Source: "declared", }, { - Part: "a", - Vendor: "codec", - Product: "netty-codec-http2", - Version: "4.1.73.Final", + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "codec", + Product: "netty-codec-http2", + Version: "4.1.73.Final", + }, + Source: "declared", }, { - Part: "a", - Vendor: "codec", - Product: "netty_codec_http2", - Version: "4.1.73.Final", + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "codec", + Product: "netty_codec_http2", + Version: "4.1.73.Final", + }, + Source: "declared", }, }, }, diff --git a/syft/pkg/cataloger/sbom/cataloger_test.go b/syft/pkg/cataloger/sbom/cataloger_test.go index dbaf37b30..ad6aea71d 100644 --- a/syft/pkg/cataloger/sbom/cataloger_test.go +++ b/syft/pkg/cataloger/sbom/cataloger_test.go @@ -14,21 +14,11 @@ import ( func mustCPEs(s ...string) (c []cpe.CPE) { for _, i := range s { - c = append(c, mustCPE(i)) + c = append(c, cpe.Must(i, "")) } return } -func mustCPE(c string) cpe.CPE { - return must(cpe.New(c)) -} -func must(c cpe.CPE, e error) cpe.CPE { - if e != nil { - panic(e) - } - return c -} - func Test_parseSBOM(t *testing.T) { expectedPkgs := []pkg.Package{ { diff --git a/syft/pkg/collection_test.go b/syft/pkg/collection_test.go index cfde34d5d..65d74c33c 100644 --- a/syft/pkg/collection_test.go +++ b/syft/pkg/collection_test.go @@ -368,7 +368,7 @@ func TestCatalog_MergeRecords(t *testing.T) { name: "multiple Locations with shared path", pkgs: []Package{ { - CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*")}, + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*", cpe.GeneratedSource)}, Locations: file.NewLocationSet( file.NewVirtualLocationFromCoordinates( file.Coordinates{ @@ -381,7 +381,7 @@ func TestCatalog_MergeRecords(t *testing.T) { Type: RpmPkg, }, { - CPEs: []cpe.CPE{cpe.Must("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*")}, + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource)}, Locations: file.NewLocationSet( file.NewVirtualLocationFromCoordinates( file.Coordinates{ diff --git a/syft/pkg/package_test.go b/syft/pkg/package_test.go index 20e2031ef..94896d652 100644 --- a/syft/pkg/package_test.go +++ b/syft/pkg/package_test.go @@ -34,7 +34,7 @@ func TestIDUniqueness(t *testing.T) { Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ - cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), }, PURL: "pkg:pypi/pi@3.14", Metadata: PythonPackage{ @@ -256,7 +256,7 @@ func TestPackage_Merge(t *testing.T) { Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ - cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), }, PURL: "pkg:pypi/pi@3.14", Metadata: PythonPackage{ @@ -278,7 +278,7 @@ func TestPackage_Merge(t *testing.T) { Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ - cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`), // NOTE: difference + cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: difference }, PURL: "pkg:pypi/pi@3.14", Metadata: PythonPackage{ @@ -301,8 +301,8 @@ func TestPackage_Merge(t *testing.T) { Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ - cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), - cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`), // NOTE: merge! + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), + cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: merge! }, PURL: "pkg:pypi/pi@3.14", Metadata: PythonPackage{ @@ -327,7 +327,7 @@ func TestPackage_Merge(t *testing.T) { Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ - cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), }, PURL: "pkg:pypi/pi@3.14", Metadata: PythonPackage{ @@ -349,7 +349,7 @@ func TestPackage_Merge(t *testing.T) { Language: "math", Type: PythonPkg, CPEs: []cpe.CPE{ - cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), }, PURL: "pkg:pypi/pi@3.14", Metadata: PythonPackage{