From 12d91f47dc8e4fbddc9cc5fef8424e08f6dd76e9 Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Tue, 13 May 2025 13:01:41 -0400 Subject: [PATCH] Add a homebrew cataloger (#3724) * Cataloger homebrew (#4) * homebrew cataloger * uptd * fixed test * fixed test * fixed tests * fixed lint * inc schema ver * upt schema * fixed integration test * fixed integration tst * fixed test Signed-off-by: Rez Moss * Update parse_homebrew_test.go Signed-off-by: Rez Moss * Update parse_homebrew_test.go fixed DCO Signed-off-by: Rez Moss Signed-off-by: Rez Moss * Update parse_homebrew_test.go add evd anno to test Signed-off-by: Rez Moss * lint Signed-off-by: Rez Moss * fixed test Signed-off-by: Rez Moss * with PR refactors Signed-off-by: Alex Goodman * regenerate json schema Signed-off-by: Alex Goodman * fix tests Signed-off-by: Alex Goodman * regenerate jsonschema Signed-off-by: Alex Goodman * refactor homebrew parser + add tests Signed-off-by: Alex Goodman * more resiliant variable extraction Signed-off-by: Alex Goodman --------- Signed-off-by: Rez Moss Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- .../catalog_packages_cases_test.go | 7 + .../Cellar/afflib/1.2.3/.brew/afflib.rb | 2 + internal/constants.go | 2 +- internal/task/package_tasks.go | 3 +- schema/json/schema-16.0.30.json | 3010 +++++++++++++++++ schema/json/schema-latest.json | 19 +- .../spdxutil/helpers/originator_supplier.go | 1 + .../helpers/originator_supplier_test.go | 1 + .../internal/spdxutil/helpers/source_info.go | 2 + .../spdxutil/helpers/source_info_test.go | 8 + syft/internal/packagemetadata/generated.go | 1 + syft/internal/packagemetadata/names.go | 1 + syft/pkg/cataloger/homebrew/cataloger.go | 17 + syft/pkg/cataloger/homebrew/cataloger_test.go | 82 + syft/pkg/cataloger/homebrew/package.go | 44 + syft/pkg/cataloger/homebrew/package_test.go | 37 + .../homebrew/parse_homebrew_formula.go | 149 + .../homebrew/parse_homebrew_formula_test.go | 303 ++ .../formulas/crazy/1.0.0/.brew/crazy.rb | 501 +++ .../formulas/syft/1.23.1/.brew/syft.rb | 52 + .../homebrew/Cellar/foo/1.2.3/.brew/foo.rb | 3 + .../Taps/testorg/sometap/Formula/bar.rb | 6 + syft/pkg/homebrew.go | 7 + syft/pkg/type.go | 6 + syft/pkg/type_test.go | 1 + test/cli/scan_cmd_test.go | 2 +- 26 files changed, 4263 insertions(+), 4 deletions(-) create mode 100644 cmd/syft/internal/test/integration/test-fixtures/image-pkg-coverage/pkgs/homebrew/Cellar/afflib/1.2.3/.brew/afflib.rb create mode 100644 schema/json/schema-16.0.30.json create mode 100644 syft/pkg/cataloger/homebrew/cataloger.go create mode 100644 syft/pkg/cataloger/homebrew/cataloger_test.go create mode 100644 syft/pkg/cataloger/homebrew/package.go create mode 100644 syft/pkg/cataloger/homebrew/package_test.go create mode 100644 syft/pkg/cataloger/homebrew/parse_homebrew_formula.go create mode 100644 syft/pkg/cataloger/homebrew/parse_homebrew_formula_test.go create mode 100644 syft/pkg/cataloger/homebrew/test-fixtures/formulas/crazy/1.0.0/.brew/crazy.rb create mode 100644 syft/pkg/cataloger/homebrew/test-fixtures/formulas/syft/1.23.1/.brew/syft.rb create mode 100644 syft/pkg/cataloger/homebrew/test-fixtures/install-example/opt/homebrew/Cellar/foo/1.2.3/.brew/foo.rb create mode 100644 syft/pkg/cataloger/homebrew/test-fixtures/install-example/opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb create mode 100644 syft/pkg/homebrew.go diff --git a/cmd/syft/internal/test/integration/catalog_packages_cases_test.go b/cmd/syft/internal/test/integration/catalog_packages_cases_test.go index f7b6bbcb6..3014be2a0 100644 --- a/cmd/syft/internal/test/integration/catalog_packages_cases_test.go +++ b/cmd/syft/internal/test/integration/catalog_packages_cases_test.go @@ -501,6 +501,13 @@ var commonTestCases = []testCase{ "Akismet Anti-spam: Spam Protection": "5.3", }, }, + { + name: "find homebrew", + pkgType: pkg.HomebrewPkg, + pkgInfo: map[string]string{ + "afflib": "1.2.3", + }, + }, { name: "find php pear/pecl package", pkgType: pkg.PhpPearPkg, diff --git a/cmd/syft/internal/test/integration/test-fixtures/image-pkg-coverage/pkgs/homebrew/Cellar/afflib/1.2.3/.brew/afflib.rb b/cmd/syft/internal/test/integration/test-fixtures/image-pkg-coverage/pkgs/homebrew/Cellar/afflib/1.2.3/.brew/afflib.rb new file mode 100644 index 000000000..e5f225e99 --- /dev/null +++ b/cmd/syft/internal/test/integration/test-fixtures/image-pkg-coverage/pkgs/homebrew/Cellar/afflib/1.2.3/.brew/afflib.rb @@ -0,0 +1,2 @@ +desc "Advanced Forensic Format" +homepage "https://github.com/sshock/AFFLIBv3" \ No newline at end of file diff --git a/internal/constants.go b/internal/constants.go index f494cd277..950f06a14 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -3,5 +3,5 @@ package internal const ( // JSONSchemaVersion is the current schema version output by the JSON encoder // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. - JSONSchemaVersion = "16.0.29" + JSONSchemaVersion = "16.0.30" ) diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index 0c7ebe388..5b616c452 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -17,6 +17,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/githubactions" "github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/haskell" + "github.com/anchore/syft/syft/pkg/cataloger/homebrew" "github.com/anchore/syft/syft/pkg/cataloger/java" "github.com/anchore/syft/syft/pkg/cataloger/javascript" "github.com/anchore/syft/syft/pkg/cataloger/kernel" @@ -166,6 +167,7 @@ func DefaultPackageTaskFactories() Factories { newSimplePackageTaskFactory(bitnamiSbomCataloger.NewCataloger, "bitnami", pkgcataloging.InstalledTag, pkgcataloging.ImageTag), newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"), newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"), + newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"), // deprecated catalogers //////////////////////////////////////// // these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible) @@ -173,6 +175,5 @@ func DefaultPackageTaskFactories() Factories { newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0 newSimplePackageTaskFactory(php.NewPeclCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0 newSimplePackageTaskFactory(nix.NewStoreCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0 - } } diff --git a/schema/json/schema-16.0.30.json b/schema/json/schema-16.0.30.json new file mode 100644 index 000000000..e57d0628d --- /dev/null +++ b/schema/json/schema-16.0.30.json @@ -0,0 +1,3010 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.0.30/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string" + }, + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "packager": { + "type": "string" + }, + "url": { + "type": "string" + }, + "validation": { + "type": "string" + }, + "reason": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ] + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "gid": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "size": { + "type": "string" + }, + "link": { + "type": "string" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object" + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "installedSize": { + "type": "integer" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ] + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ] + }, + "BitnamiSbomEntry": { + "properties": { + "name": { + "type": "string" + }, + "arch": { + "type": "string" + }, + "distro": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "arch", + "distro", + "revision", + "version", + "path", + "files" + ] + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "options": { + "$ref": "#/$defs/KeyValues" + }, + "path": { + "type": "string" + }, + "context": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string" + }, + "packageID": { + "type": "string" + }, + "username": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "recipeRevision": { + "type": "string" + }, + "packageRevision": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "CPE": { + "properties": { + "cpe": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "type": "object", + "required": [ + "cpe" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ] + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "checksum" + ] + }, + "Coordinates": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Descriptor": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": true + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array" + }, + "source": { + "$ref": "#/$defs/Source" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor" + }, + "schema": { + "$ref": "#/$defs/Schema" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ] + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + }, + "executables": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ] + }, + "DotnetPackagesLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "contentHash": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "contentHash", + "type" + ] + }, + "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" + ] + }, + "DpkgArchiveEntry": { + "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" + ] + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ] + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ] + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean" + }, + "stackCanary": { + "type": "boolean" + }, + "nx": { + "type": "boolean" + }, + "relRO": { + "type": "string" + }, + "pie": { + "type": "boolean" + }, + "dso": { + "type": "boolean" + }, + "safeStack": { + "type": "boolean" + }, + "cfi": { + "type": "boolean" + }, + "fortify": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ] + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "osCPE": { + "type": "string" + }, + "os": { + "type": "string" + }, + "osVersion": { + "type": "string" + }, + "system": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "sourceRepo": { + "type": "string" + }, + "commit": { + "type": "string" + } + }, + "type": "object" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "Executable": { + "properties": { + "format": { + "type": "string" + }, + "hasExports": { + "type": "boolean" + }, + "hasEntrypoint": { + "type": "boolean" + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array" + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures" + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ] + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array" + }, + "executable": { + "$ref": "#/$defs/Executable" + }, + "unknowns": { + "items": { + "type": "string" + }, + "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" + ] + }, + "GithubActionsUseStatement": { + "properties": { + "value": { + "type": "string" + }, + "comment": { + "type": "string" + } + }, + "type": "object", + "required": [ + "value" + ] + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "type": "string" + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array" + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ] + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string" + } + }, + "type": "object" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string" + } + }, + "type": "object" + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string" + }, + "snapshotURL": { + "type": "string" + } + }, + "type": "object" + }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "virtualPath" + ] + }, + "JavaJvmInstallation": { + "properties": { + "release": { + "$ref": "#/$defs/JavaVMRelease" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "release", + "files" + ] + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array" + } + }, + "type": "object" + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ] + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ] + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ] + }, + "JavaVMRelease": { + "properties": { + "implementor": { + "type": "string" + }, + "implementorVersion": { + "type": "string" + }, + "javaRuntimeVersion": { + "type": "string" + }, + "javaVersion": { + "type": "string" + }, + "javaVersionDate": { + "type": "string" + }, + "libc": { + "type": "string" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array" + }, + "osArch": { + "type": "string" + }, + "osName": { + "type": "string" + }, + "osVersion": { + "type": "string" + }, + "source": { + "type": "string" + }, + "buildSource": { + "type": "string" + }, + "buildSourceRepo": { + "type": "string" + }, + "sourceRepo": { + "type": "string" + }, + "fullVersion": { + "type": "string" + }, + "semanticVersion": { + "type": "string" + }, + "buildInfo": { + "type": "string" + }, + "jvmVariant": { + "type": "string" + }, + "jvmVersion": { + "type": "string" + }, + "imageType": { + "type": "string" + }, + "buildType": { + "type": "string" + } + }, + "type": "object" + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "private": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ] + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "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" + }, + "fullText": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "contents": { + "type": "string" + } + }, + "type": "object", + "required": [ + "value", + "fullText", + "spdxExpression", + "type", + "urls", + "locations" + ] + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extendedVersion": { + "type": "string" + }, + "buildTime": { + "type": "string" + }, + "author": { + "type": "string" + }, + "format": { + "type": "string" + }, + "rwRootFS": { + "type": "boolean" + }, + "swapDevice": { + "type": "integer" + }, + "rootDevice": { + "type": "integer" + }, + "videoMode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ] + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "license": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "versionMagic": { + "type": "string" + }, + "parameters": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "$ref": "#/$defs/IDLikes" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "versionCodename": { + "type": "string" + }, + "buildID": { + "type": "string" + }, + "imageID": { + "type": "string" + }, + "imageVersion": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + }, + "supportEnd": { + "type": "string" + } + }, + "type": "object" + }, + "Location": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + }, + "accessPath": { + "type": "string" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ] + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "dependencies": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ] + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string" + }, + "kb": { + "type": "string" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ] + }, + "NixDerivation": { + "properties": { + "path": { + "type": "string" + }, + "system": { + "type": "string" + }, + "inputDerivations": { + "items": { + "$ref": "#/$defs/NixDerivationReference" + }, + "type": "array" + }, + "inputSources": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "NixDerivationReference": { + "properties": { + "path": { + "type": "string" + }, + "outputs": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "NixStoreEntry": { + "properties": { + "path": { + "type": "string" + }, + "output": { + "type": "string" + }, + "outputHash": { + "type": "string" + }, + "derivation": { + "$ref": "#/$defs/NixDerivation" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "outputHash" + ] + }, + "OpamPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "url": { + "type": "string" + }, + "checksum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "licenses", + "url", + "checksum", + "homepage", + "dependencies" + ] + }, + "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/BitnamiSbomEntry" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPackagesLockEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgArchiveEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GithubActionsUseStatement" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/HomebrewFormula" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/OpamPackage" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPearEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/TerraformLockProviderEntry" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ] + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name" + ] + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ] + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference" + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ] + }, + "PhpPearEntry": { + "properties": { + "name": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer" + }, + "licenses": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ] + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "type": "object", + "required": [ + "url" + ] + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ] + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo" + }, + "requiresPython": { + "type": "string" + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ] + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + }, + "versionConstraint": { + "type": "string" + }, + "url": { + "type": "string" + }, + "markers": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ] + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "index": { + "type": "string" + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ] + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "markers": { + "type": "string" + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ] + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string" + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array" + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ] + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ] + }, + "RDescription": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repository": { + "type": "string" + }, + "built": { + "type": "string" + }, + "needsCompilation": { + "type": "boolean" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ] + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ] + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ] + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ] + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ] + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ] + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string" + } + }, + "type": "object", + "required": [ + "revision" + ] + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "packager": { + "type": "string" + }, + "packagerEmail": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ] + }, + "TerraformLockProviderEntry": { + "properties": { + "url": { + "type": "string" + }, + "constraints": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "url", + "constraints", + "version", + "hashes" + ] + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorUri": { + "type": "string" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ] + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index ac872a8c7..e57d0628d 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.0.29/document", + "$id": "anchore.io/schema/syft/json/16.0.30/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -1072,6 +1072,20 @@ }, "type": "object" }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, "IDLikes": { "items": { "type": "string" @@ -1879,6 +1893,9 @@ { "$ref": "#/$defs/HaskellHackageStackLockEntry" }, + { + "$ref": "#/$defs/HomebrewFormula" + }, { "$ref": "#/$defs/JavaArchive" }, diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier.go b/syft/format/internal/spdxutil/helpers/originator_supplier.go index 95bfe5f76..8e6e9b97b 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier.go @@ -127,6 +127,7 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f // it seems that the vast majority of the time the author is an org, not a person typ = orgType author = metadata.Author + case pkg.SwiplPackEntry: author = formatPersonOrOrg(metadata.Author, metadata.AuthorEmail) } diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index d9b966833..26f9ae2d6 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -26,6 +26,7 @@ func Test_OriginatorSupplier(t *testing.T) { pkg.ErlangRebarLockEntry{}, pkg.GolangBinaryBuildinfoEntry{}, pkg.GolangModuleEntry{}, + pkg.HomebrewFormula{}, pkg.HackageStackYamlLockEntry{}, pkg.HackageStackYamlEntry{}, pkg.LinuxKernel{}, diff --git a/syft/format/internal/spdxutil/helpers/source_info.go b/syft/format/internal/spdxutil/helpers/source_info.go index fbe82a1f5..5232acbc3 100644 --- a/syft/format/internal/spdxutil/helpers/source_info.go +++ b/syft/format/internal/spdxutil/helpers/source_info.go @@ -76,6 +76,8 @@ func SourceInfo(p pkg.Package) string { answer = "acquired package info from GitHub Actions workflow file or composite action file" case pkg.WordpressPluginPkg: answer = "acquired package info from found wordpress plugin PHP source files" + case pkg.HomebrewPkg: + answer = "acquired package info from Homebrew formula" case pkg.TerraformPkg: answer = "acquired package info from Terraform dependency lock file" default: diff --git a/syft/format/internal/spdxutil/helpers/source_info_test.go b/syft/format/internal/spdxutil/helpers/source_info_test.go index a2c6e7587..726ebcb61 100644 --- a/syft/format/internal/spdxutil/helpers/source_info_test.go +++ b/syft/format/internal/spdxutil/helpers/source_info_test.go @@ -327,6 +327,14 @@ func Test_SourceInfo(t *testing.T) { "acquired package info from found wordpress plugin PHP source files", }, }, + { + input: pkg.Package{ + Type: pkg.HomebrewPkg, + }, + expected: []string{ + "acquired package info from Homebrew formula", + }, + }, { input: pkg.Package{ Type: pkg.TerraformPkg, diff --git a/syft/internal/packagemetadata/generated.go b/syft/internal/packagemetadata/generated.go index b30bd4cf4..63d13a993 100644 --- a/syft/internal/packagemetadata/generated.go +++ b/syft/internal/packagemetadata/generated.go @@ -30,6 +30,7 @@ func AllTypes() []any { pkg.GolangModuleEntry{}, pkg.HackageStackYamlEntry{}, pkg.HackageStackYamlLockEntry{}, + pkg.HomebrewFormula{}, pkg.JavaArchive{}, pkg.JavaVMInstallation{}, pkg.LinuxKernel{}, diff --git a/syft/internal/packagemetadata/names.go b/syft/internal/packagemetadata/names.go index 0cbee1bde..978eb0034 100644 --- a/syft/internal/packagemetadata/names.go +++ b/syft/internal/packagemetadata/names.go @@ -112,6 +112,7 @@ var jsonTypes = makeJSONTypes( jsonNames(pkg.RustCargoLockEntry{}, "rust-cargo-lock-entry", "RustCargoPackageMetadata"), jsonNamesWithoutLookup(pkg.RustBinaryAuditEntry{}, "rust-cargo-audit-entry", "RustCargoPackageMetadata"), // the legacy value is split into two types, where the other is preferred jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"), + jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"), jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"), jsonNames(pkg.TerraformLockProviderEntry{}, "terraform-lock-provider-entry"), jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"), diff --git a/syft/pkg/cataloger/homebrew/cataloger.go b/syft/pkg/cataloger/homebrew/cataloger.go new file mode 100644 index 000000000..d99195c00 --- /dev/null +++ b/syft/pkg/cataloger/homebrew/cataloger.go @@ -0,0 +1,17 @@ +package homebrew + +import ( + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +func NewCataloger() pkg.Cataloger { + return generic.NewCataloger("homebrew-cataloger"). + WithParserByGlobs( + parseHomebrewFormula, + // forumulas are located at $(brew --repository)/Cellar + "**/Cellar/*/*/.brew/*.rb", + // taps are located at $(brew --repository)/Library/Taps + "**/Library/Taps/*/*/Formula/*.rb", + ) +} diff --git a/syft/pkg/cataloger/homebrew/cataloger_test.go b/syft/pkg/cataloger/homebrew/cataloger_test.go new file mode 100644 index 000000000..77e9e7560 --- /dev/null +++ b/syft/pkg/cataloger/homebrew/cataloger_test.go @@ -0,0 +1,82 @@ +package homebrew + +import ( + "testing" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func Test_HomebrewCataloger_Globs(t *testing.T) { + fixture := "test-fixtures/install-example" + + expected := []string{ + "opt/homebrew/Cellar/foo/1.2.3/.brew/foo.rb", + "opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb", + } + + pkgtest.NewCatalogTester(). + FromDirectory(t, fixture). + ExpectsResolverContentQueries(expected). + TestCataloger(t, NewCataloger()) +} + +func Test_HomebrewCataloger(t *testing.T) { + + tests := []struct { + name string + path string + expected []pkg.Package + expectedRels []artifact.Relationship + }{ + { + name: "go case", + path: "test-fixtures/install-example", + expected: []pkg.Package{ + { + Name: "bar", + Version: "4.5.6", + Type: pkg.HomebrewPkg, + Locations: file.NewLocationSet( + file.NewLocation("opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("MIT")...), + FoundBy: "homebrew-cataloger", + PURL: "pkg:brew/bar@4.5.6", + Metadata: pkg.HomebrewFormula{ + Tap: "testorg/sometap", + Homepage: "https://example.com/bar", + Description: "A test Homebrew formula for bar", + }, + }, + { + Name: "foo", + Version: "1.2.3", + Type: pkg.HomebrewPkg, + Locations: file.NewLocationSet( + file.NewLocation("opt/homebrew/Cellar/foo/1.2.3/.brew/foo.rb").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("Apache 2.0")...), + FoundBy: "homebrew-cataloger", + PURL: "pkg:brew/foo@1.2.3", + Metadata: pkg.HomebrewFormula{ + Homepage: "https://example.com/foo", + Description: "A test Homebrew formula for Foo", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkgtest.NewCatalogTester(). + FromDirectory(t, tt.path). + Expects(tt.expected, tt.expectedRels). + TestCataloger(t, NewCataloger()) + }) + } + +} diff --git a/syft/pkg/cataloger/homebrew/package.go b/syft/pkg/cataloger/homebrew/package.go new file mode 100644 index 000000000..24199c8f6 --- /dev/null +++ b/syft/pkg/cataloger/homebrew/package.go @@ -0,0 +1,44 @@ +package homebrew + +import ( + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" +) + +func newHomebrewPackage(pd parsedHomebrewData, formulaLocation file.Location) pkg.Package { + var licenses []string + if pd.License != "" { + licenses = append(licenses, pd.License) + } + + p := pkg.Package{ + Name: pd.Name, + Version: pd.Version, + Type: pkg.HomebrewPkg, + Locations: file.NewLocationSet(formulaLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues(licenses...)...), + FoundBy: "homebrew-cataloger", + PURL: packageURL(pd.Name, pd.Version), + Metadata: pkg.HomebrewFormula{ + Tap: pd.Tap, + Homepage: pd.Homepage, + Description: pd.Desc, + }, + } + + p.SetID() + return p +} + +func packageURL(name, version string) string { + purl := packageurl.NewPackageURL( + "brew", + "", + name, + version, + nil, + "", + ) + return purl.ToString() +} diff --git a/syft/pkg/cataloger/homebrew/package_test.go b/syft/pkg/cataloger/homebrew/package_test.go new file mode 100644 index 000000000..6875fd7e5 --- /dev/null +++ b/syft/pkg/cataloger/homebrew/package_test.go @@ -0,0 +1,37 @@ +package homebrew + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_packageURL(t *testing.T) { + tests := []struct { + name string + packageName string + packageVersion string + expected string + }{ + // preemptive based on https://github.com/package-url/purl-spec/pull/281 + { + name: "standard homebrew package URL", + packageName: "foo", + packageVersion: "1.2.3", + expected: "pkg:brew/foo@1.2.3", + }, + { + name: "another example", + packageName: "bar", + packageVersion: "9.8.7", + expected: "pkg:brew/bar@9.8.7", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := packageURL(test.packageName, test.packageVersion) + assert.Equal(t, test.expected, actual, "expected package URL to match") + }) + } +} diff --git a/syft/pkg/cataloger/homebrew/parse_homebrew_formula.go b/syft/pkg/cataloger/homebrew/parse_homebrew_formula.go new file mode 100644 index 000000000..a2d95a3e9 --- /dev/null +++ b/syft/pkg/cataloger/homebrew/parse_homebrew_formula.go @@ -0,0 +1,149 @@ +package homebrew + +import ( + "bufio" + "context" + "path" + "strings" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +type parsedHomebrewData struct { + Tap string + Name string + Version string + Desc string + Homepage string + License string +} + +func parseHomebrewFormula(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + pd, err := parseFormulaFile(reader) + if err != nil { + log.WithFields("path", reader.RealPath).Trace("failed to parse formula") + return nil, nil, err + } + + if pd == nil { + return nil, nil, nil + } + + return []pkg.Package{ + newHomebrewPackage( + *pd, + reader.Location, + ), + }, nil, nil +} + +func parseFormulaFile(reader file.LocationReadCloser) (*parsedHomebrewData, error) { + pd := parsedHomebrewData{} + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.Contains(line, "class ") && strings.Contains(line, " < Formula") { + // this is the start of the class declaration, ignore anything before this + pd = parsedHomebrewData{} + continue + } + + switch { + case matchesVariable(line, "desc"): + pd.Desc = getQuotedValue(line) + case matchesVariable(line, "homepage"): + pd.Homepage = getQuotedValue(line) + case matchesVariable(line, "license"): + pd.License = getQuotedValue(line) + case matchesVariable(line, "name"): + pd.Name = getQuotedValue(line) + case matchesVariable(line, "version"): + pd.Version = getQuotedValue(line) + } + } + + pd.Tap = getTapFromPath(reader.RealPath) + + if err := scanner.Err(); err != nil { + return nil, err + } + + if pd.Name != "" && pd.Version != "" { + return &pd, nil + } + + pd.Name, pd.Version = getNameAndVersionFromPath(reader.RealPath) + + return &pd, nil +} + +func matchesVariable(line, name string) bool { + // should return true if the line starts with "name" or "name" + return strings.HasPrefix(line, name+" ") || strings.HasPrefix(line, name+"\t") +} + +func getNameAndVersionFromPath(p string) (string, string) { + if p == "" { + return "", "" + } + + pathParts := strings.Split(p, "/") + + // extract from a formula path... + // e.g. /opt/homebrew/Cellar/foo/1.0.0/.brew/foo.rb + var name, ver string + for i := len(pathParts) - 1; i >= 0; i-- { + if pathParts[i] == ".brew" && i-2 >= 0 { + name = pathParts[i-2] + ver = pathParts[i-1] + break + } + } + + if name == "" { + // get it from the filename + name = strings.TrimSuffix(path.Base(p), ".rb") + } + + return name, ver +} + +func getTapFromPath(path string) string { + // get testorg/sometap from opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb + // key off of Library/Taps/ as the path just before the org/tap name + + paths := strings.Split(path, "Library/Taps/") + if len(paths) < 2 { + return "" + } + + paths = strings.Split(paths[1], "/") + if len(paths) < 2 { + return "" + } + return strings.Join(paths[0:2], "/") +} + +func getQuotedValue(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "" + } + + start := strings.Index(s, "\"") + if start == -1 { + return "" + } + + end := strings.LastIndex(s, "\"") + if end == -1 || end <= start { + return "" + } + + return s[start+1 : end] +} diff --git a/syft/pkg/cataloger/homebrew/parse_homebrew_formula_test.go b/syft/pkg/cataloger/homebrew/parse_homebrew_formula_test.go new file mode 100644 index 000000000..a18a4d0d4 --- /dev/null +++ b/syft/pkg/cataloger/homebrew/parse_homebrew_formula_test.go @@ -0,0 +1,303 @@ +package homebrew + +import ( + "testing" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func Test_ParseHomebrewPackage(t *testing.T) { + + tests := []struct { + name string + fixture string + expected pkg.Package + }{ + { + name: "syft example", + fixture: "test-fixtures/formulas/syft/1.23.1/.brew/syft.rb", + expected: pkg.Package{ + Name: "syft", + Version: "1.23.1", + Type: pkg.HomebrewPkg, + Locations: file.NewLocationSet( + file.NewLocation("test-fixtures/formulas/syft/1.23.1/.brew/syft.rb").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("Apache License 2.0")...), + FoundBy: "homebrew-cataloger", + PURL: "pkg:brew/syft@1.23.1", + Metadata: pkg.HomebrewFormula{ + Homepage: "https://github.com/anchore/syft", + Description: "A tool that generates a Software Bill Of Materials (SBOM) from container images and filesystems", + }, + }, + }, + { + name: "crazy example", + fixture: "test-fixtures/formulas/crazy/1.0.0/.brew/crazy.rb", + expected: pkg.Package{ + Name: "crazy", + Version: "1.0.0", + Type: pkg.HomebrewPkg, + Locations: file.NewLocationSet( + file.NewLocation("test-fixtures/formulas/crazy/1.0.0/.brew/crazy.rb").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ), + FoundBy: "homebrew-cataloger", + PURL: "pkg:brew/crazy@1.0.0", + Metadata: pkg.HomebrewFormula{ + Homepage: "https://www.example.com", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pkgtest.TestFileParser(t, test.fixture, parseHomebrewFormula, []pkg.Package{test.expected}, nil) + }) + } +} + +func TestGetTapFromPath(t *testing.T) { + tests := []struct { + name string + path string + expected string + }{ + { + name: "valid path", + path: "/opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb", + expected: "testorg/sometap", + }, + { + name: "valid path with different prefix", + path: "/usr/local/Library/Taps/otherorg/anothertap/Formula/foo.rb", + expected: "otherorg/anothertap", + }, + { + name: "missing Library/Taps", + path: "/opt/homebrew/Cellar/formula.rb", + expected: "", + }, + { + name: "incomplete path after Taps", + path: "/opt/homebrew/Library/Taps/testorg", + expected: "", + }, + { + name: "empty path", + path: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getTapFromPath(tt.path) + if result != tt.expected { + t.Errorf("getTapFromPath(%q) = %q, want %q", tt.path, result, tt.expected) + } + }) + } +} + +func TestGetNameAndVersionFromPath(t *testing.T) { + tests := []struct { + name string + path string + expectedName string + expectedVer string + }{ + { + name: "formula path", + path: "/opt/homebrew/Cellar/foo/1.0.0/.brew/foo.rb", + expectedName: "foo", + expectedVer: "1.0.0", + }, + { + name: "formula path with different version", + path: "/opt/homebrew/Cellar/bar/2.3.4/.brew/bar.rb", + expectedName: "bar", + expectedVer: "2.3.4", + }, + { + name: "path without .brew directory", + path: "/opt/homebrew/Formula/baz.rb", + expectedName: "baz", + expectedVer: "", + }, + { + name: "path with file extension different than filename", + path: "/opt/homebrew/Cellar/qux-tool/5.0.1/.brew/qux.rb", + expectedName: "qux-tool", + expectedVer: "5.0.1", + }, + { + name: "empty path", + path: "", + expectedName: "", + expectedVer: "", + }, + { + name: "path with no extension", + path: "/opt/homebrew/Formula/quux", + expectedName: "quux", + expectedVer: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + name, ver := getNameAndVersionFromPath(tt.path) + if name != tt.expectedName { + t.Errorf("getNameAndVersionFromPath(%q) name = %q, want %q", tt.path, name, tt.expectedName) + } + if ver != tt.expectedVer { + t.Errorf("getNameAndVersionFromPath(%q) version = %q, want %q", tt.path, ver, tt.expectedVer) + } + }) + } +} + +func TestGetQuotedValue(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "simple quoted string", + input: "\"hello\"", + expected: "hello", + }, + { + name: "quoted string with whitespace outside", + input: " \"hello world\" ", + expected: "hello world", + }, + { + name: "quoted string with content before and after", + input: "prefix \"extracted value\" suffix", + expected: "extracted value", + }, + { + name: "multiple quotes - extract first to last", + input: "\"first\" something \"last\"", + expected: "first\" something \"last", + }, + { + name: "nested quotes", + input: "\"outer \"inner\" outer\"", + expected: "outer \"inner\" outer", + }, + { + name: "empty quoted string", + input: "\"\"", + expected: "", + }, + { + name: "only opening quote", + input: "\"unbalanced", + expected: "", + }, + { + name: "only closing quote", + input: "unbalanced\"", + expected: "", + }, + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "whitespace only", + input: " ", + expected: "", + }, + { + name: "no quotes", + input: "hello world", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getQuotedValue(tt.input) + if result != tt.expected { + t.Errorf("getQuotedValue(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestMatchesVariable(t *testing.T) { + tests := []struct { + name string + line string + variableName string + expected bool + }{ + { + name: "matches with space", + line: "foo = bar", + variableName: "foo", + expected: true, + }, + { + name: "matches with tab", + line: "bar\tvalue", + variableName: "bar", + expected: true, + }, + { + name: "no match - different variable", + line: "baz = value", + variableName: "foo", + expected: false, + }, + { + name: "no match - substring", + line: "foobar = value", + variableName: "foo", + expected: false, + }, + { + name: "no match - no space or tab", + line: "foo=value", + variableName: "foo", + expected: false, + }, + { + name: "no match - empty line", + line: "", + variableName: "foo", + expected: false, + }, + { + name: "matches with space and complex value", + line: "complex_var complex value with spaces", + variableName: "complex_var", + expected: true, + }, + { + name: "case sensitive - different case", + line: "FOO = value", + variableName: "foo", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := matchesVariable(tt.line, tt.variableName) + if result != tt.expected { + t.Errorf("matchesVariable(%q, %q) = %v, want %v", + tt.line, tt.variableName, result, tt.expected) + } + }) + } +} diff --git a/syft/pkg/cataloger/homebrew/test-fixtures/formulas/crazy/1.0.0/.brew/crazy.rb b/syft/pkg/cataloger/homebrew/test-fixtures/formulas/crazy/1.0.0/.brew/crazy.rb new file mode 100644 index 000000000..d8db055ba --- /dev/null +++ b/syft/pkg/cataloger/homebrew/test-fixtures/formulas/crazy/1.0.0/.brew/crazy.rb @@ -0,0 +1,501 @@ +# source: https://github.com/syhw/homebrew/blob/174cc1183a7d45e4c87efbc7715ea1016234715c/Library/Contributions/example-formula.rb + +# This is a non-functional example formula to showcase all features and +# therefore, it's overly complex and dupes stuff just to comment on it. +# You may want to use `brew create` to start your own new formula! +# Documentation: https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Formula-Cookbook.md + +## Naming -- Every Homebrew formula is a class of the type `Formula`. +# Ruby classes have to start Upper case and dashes are not allowed. +# So we transform: `example-formula.rb` into `ExampleFormula`. Further, +# Homebrew does enforce that the name of the file and the class correspond. +# Check with `brew search` that the name is free. +# name = "FALSE POSITIVE comment" +# version = "ALSO FALSE POSITIVE comment" + +name = "FALSE POSITIVE global" +version = "ALSO FALSE POSITIVE global" + +class ExampleFormula < Formula + homepage "https://www.example.com" # used by `brew home example-formula`. + revision 1 # This is used when there's no new version but it needs recompiling for another reason. + # 0 is default & unwritten. + + # The url of the archive. Prefer https (security and proxy issues): + url "https://packed.sources.and.we.prefer.https.example.com/archive-1.2.3.tar.bz2" + mirror "https://in.case.the.host.is.down.example.com" # `mirror` is optional. + mirror "https://in.case.the.mirror.is.down.example.com" # Mirrors are limitless, but don't go too wild. + + # Optionally specify the download strategy `:using => ...` + # `:git`, `:hg`, `:svn`, `:bzr`, `:cvs`, + # `:curl` (normal file download. Will also extract.) + # `:nounzip` (without extracting) + # `:post` (download via an HTTP POST) + # `S3DownloadStrategy` (download from S3 using signed request) + url "https://some.dont.provide.archives.example.com", :using => :git, :tag => "1.2.3" + + # version is seldom needed, because it's usually autodetected from the URL/tag. + # version "1.2-final" + + # For integrity and security, we verify the hash (`openssl dgst -sha1 `) + # You may also use sha256 if the software uses sha256 on their homepage. Do not use md5. + # Either generate the sha locally or leave it empty & `brew install` will tell you the expected. + sha1 "cafebabe78901234567890123456789012345678" + + # Stable-only dependencies should be nested inside a `stable` block rather than + # using a conditional. It is preferrable to also pull the URL and checksum into + # the block if one is necessary. + stable do + url "https://example.com/foo-1.0.tar.gz" + sha1 "cafebabe78901234567890123456789012345678" + + depends_on "libxml2" + depends_on "libffi" + end + + # Optionally, specify a repository to be used. Brew then generates a + # `--HEAD` option. Remember to also test it. + # The download strategies (:using =>) are the same as for `url`. + # "master" is the default branch and doesn't need stating with a :branch conditional + head "https://we.prefer.https.over.git.example.com/.git" + head "https://example.com/.git", :branch => "name_of_branch", :revision => "abc123" + head "https://hg.is.awesome.but.git.has.won.example.com/", :using => :hg # If autodetect fails. + + head do + url "https://example.com/repo.git" + + depends_on "autoconf" => :build + depends_on "automake" => :build + depends_on "libtool" => :build + end + + # The optional devel block is only executed if the user passes `--devel`. + # Use this to specify a not-yet-released version of a software. + devel do + url "https://example.com/archive-2.0-beta.tar.gz" + sha1 "1234567890123456789012345678901234567890" + + depends_on "cairo" + depends_on "pixman" + end + + + ## Options + + # Options can be used as arguments to `brew install`. + # To switch features on/off: `"with-something"` or `"with-otherthing"`. + # To use another software: `"with-other-software"` or `"without-foo"` + # Note, that for dependencies that are `:optional` or `:recommended`, options + # are generated automatically. + # Build a universal (On newer intel Macs this means a combined 32bit and + # 64bit binary/library). LATER: better explain what this means for PPC. + option :universal + option "with-spam", "The description goes here without a dot at the end" + option "with-qt", "Text here overwrites the autogenerated one from `depends_on 'qt'`" + + ## Bottles + + # Bottles are pre-built and added by the Homebrew maintainers for you. + # If you maintain your own repository, you can add your own bottle links. + # https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Bottles.md + # You can ignore this block entirely if submitting to Homebrew/Homebrew, It'll be + # handled for you by the Brew Test Bot. + bottle do + root_url "http://mikemcquaid.com" # Optional root to calculate bottle URLs + prefix "/opt/homebrew" # Optional HOMEBREW_PREFIX in which the bottles were built. + cellar "/opt/homebrew/Cellar" # Optional HOMEBREW_CELLAR in which the bottles were built. + revision 1 # Making the old bottle outdated without bumping the version of the formula. + sha1 "d3d13fe6f42416765207503a946db01378131d7b" => :yosemite + sha1 "cdc48e79de2dee796bb4ba1ad987f6b35ce1c1ee" => :mavericks + sha1 "a19b544c8c645d7daad1d39a070a0eb86dfe9b9c" => :mountain_lion + end + + def pour_bottle? + # Only needed if this formula has to check if using the pre-built + # bottle is fine. + true + end + + ## keg_only + + # Software that will not be sym-linked into the `brew --prefix` will only + # live in it's Cellar. Other formulae can depend on it and then brew will + # add the necessary includes and libs (etc.) during the brewing of that + # other formula. But generally, keg_only formulae are not in your PATH + # and not seen by compilers if you build your own software outside of + # Homebrew. This way, we don't shadow software provided by OS X. + keg_only :provided_by_osx + keg_only "because I want it so" + + + ## Dependencies + + # The dependencies for this formula. Use strings for the names of other + # formulae. Homebrew provides some :special dependencies for stuff that + # requires certain extra handling (often changing some ENV vars or + # deciding if to use the system provided version or not.) + + # `:build` means this dep is only needed during build. + depends_on "cmake" => :build + # Explictly name formulae in other taps. Non-optional tap dependencies won't + # be accepted in core. + depends_on "homebrew/dupes/tcl-tk" => :optional + # `:recommended` dependencies are built by default. But a `--without-...` + # option is generated to opt-out. + depends_on "readline" => :recommended + # `:optional` dependencies are NOT built by default but a `--with-...` + # options is generated. + depends_on "glib" => :optional + # If you need to specify that another formula has to be built with/out + # certain options (note, no `--` needed before the option): + depends_on "zeromq" => "with-pgm" + depends_on "qt" => ["with-qtdbus", "developer"] # Multiple options. + # Optional and enforce that boost is built with `--with-c++11`. + depends_on "boost" => [:optional, "with-c++11"] + # If a dependency is only needed in certain cases: + depends_on "sqlite" if MacOS.version == :leopard + depends_on :xcode # If the formula really needs full Xcode. + depends_on :tex # Homebrew does not provide a Tex Distribution. + depends_on :fortran # Checks that `gfortran` is available or `FC` is set. + depends_on :mpi => :cc # Needs MPI with `cc` + depends_on :mpi => [:cc, :cxx, :optional] # Is optional. MPI with `cc` and `cxx`. + depends_on :macos => :lion # Needs at least Mac OS X "Lion" aka. 10.7. + depends_on :apr # If a formula requires the CLT-provided apr library to exist. + depends_on :arch => :intel # If this formula only builds on intel architecture. + depends_on :arch => :x86_64 # If this formula only build on intel x86 64bit. + depends_on :arch => :ppc # Only builds on PowerPC? + depends_on :ld64 # Sometimes ld fails on `MacOS.version < :leopard`. Then use this. + depends_on :x11 # X11/XQuartz components. Non-optional X11 deps should go in Homebrew/Homebrew-x11 + depends_on :osxfuse # Permits the use of the upstream signed binary or our source package. + depends_on :tuntap # Does the same thing as above. This is vital for Yosemite and above. + depends_on :mysql => :recommended + # It is possible to only depend on something if + # `build.with?` or `build.without? "another_formula"`: + depends_on :mysql # allows brewed or external mysql to be used + depends_on :postgresql if build.without? "sqlite" + depends_on :hg # Mercurial (external or brewed) is needed + + # If any Python >= 2.7 < 3.x is okay (either from OS X or brewed): + depends_on :python + # to depend on Python >= 2.7 but use system Python where possible + depends_on :python if MacOS.version <= :snow_leopard + # Python 3.x if the `--with-python3` is given to `brew install example` + depends_on :python3 => :optional + + # Modules/Packages from other languages, such as :chicken, :jruby, :lua, + # :node, :ocaml, :perl, :python, :rbx, :ruby, can be specified by + depends_on "some_module" => :lua + + ## Conflicts + + # If this formula conflicts with another one: + conflicts_with "imagemagick", :because => "because this is just a stupid example" + + + ## Failing with a certain compiler? + + # If it is failing for certain compiler: + fails_with :llvm do # :llvm is really llvm-gcc + build 2334 + cause "Segmentation fault during linking." + end + + fails_with :clang do + build 600 + cause "multiple configure and compile errors" + end + + ## Resources + + # Additional downloads can be defined as resources and accessed in the + # install method. Resources can also be defined inside a stable, devel, or + # head block. This mechanism replaces ad-hoc "subformula" classes. + resource "additional_files" do + url "https://example.com/additional-stuff.tar.gz" + sha1 "deadbeef7890123456789012345678901234567890" + end + + + ## Patches + + # External patches can be declared using resource-style blocks. + patch do + url "https://example.com/example_patch.diff" + sha1 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + end + + # A strip level of -p1 is assumed. It can be overridden using a symbol + # argument: + patch :p0 do + url "https://example.com/example_patch.diff" + sha1 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + end + + # Patches can be declared in stable, devel, and head blocks. This form is + # preferred over using conditionals. + stable do + patch do + url "https://example.com/example_patch.diff" + sha1 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + end + end + + # Embedded (__END__) patches are declared like so: + patch :DATA + patch :p0, :DATA + + # Patches can also be embedded by passing a string. This makes it possible + # to provide multiple embedded patches while making only some of them + # conditional. + patch :p0, "..." + + ## The install method. + + def install + # Now the sources (from `url`) are downloaded, hash-checked and + # Homebrew has changed into a temporary directory where the + # archive has been unpacked or the repository has been cloned. + + # Print a warning (do this rarely) + opoo "Dtrace features are experimental!" if build.with? "dtrace" + + # Sometimes we have to change a bit before we install. Mostly we + # prefer a patch but if you need the `prefix` of this formula in the + # patch you have to resort to `inreplace`, because in the patch + # you don't have access to any var defined by the formula. Only + # HOMEBREW_PREFIX is available in the embedded patch. + # inreplace supports regular expressions. + inreplace "somefile.cfg", /look[for]what?/, "replace by #{bin}/tool" + + # To call out to the system, we use the `system` method and we prefer + # you give the args separately as in the line below, otherwise a subshell + # has to be opened first. + system "./bootstrap.sh", "--arg1", "--prefix=#{prefix}" + + # For Cmake, we have some necessary defaults in `std_cmake_args`: + system "cmake", ".", *std_cmake_args + + # If the arguments given to configure (or make or cmake) are depending + # on options defined above, we usually make a list first and then + # use the `args << if ` to append to: + args = ["--option1", "--option2"] + args << "--i-want-spam" if build.with? "spam" + args << "--qt-gui" if build.with? "qt" # "--with-qt" ==> build.with? "qt" + args << "--some-new-stuff" if build.head? # if head is used instead of url. + args << "--universal-binary" if build.universal? + + # If there are multiple conditional arguments use a block instead of lines. + if build.head? + args << "--i-want-pizza" + args << "--and-a-cold-beer" if build.with? "cold-beer" + end + + # If a formula presents a user with a choice, but the choice must be fulfilled: + if build.with? "example2" + args << "--with-example2" + else + args << "--with-example1" + end + + # The `build.with?` and `build.without?` are smart enough to do the + # right thing with respect to defaults defined via `:optional` and + # `:recommended` dependencies. + + # If you need to give the path to lib/include of another brewed formula + # please use the `opt_prefix` instead of the `prefix` of that other + # formula. The reasoning behind this is that `prefix` has the exact + # version number and if you update that other formula, things might + # break if they remember that exact path. In contrast to that, the + # `$(brew --prefix)/opt/formula` is the same path for all future + # versions of the formula! + args << "--with-readline=#{Formula["readline"].opt_prefix}" if build.with? "readline" + + # Most software still uses `configure` and `make`. + # Check with `./configure --help` what our options are. + system "./configure", "--disable-debug", "--disable-dependency-tracking", + "--disable-silent-rules", "--prefix=#{prefix}", + *args # our custom arg list (needs `*` to unpack) + + # If your formula's build system is not thread safe: + ENV.deparallelize + + # A general note: The commands here are executed line by line, so if + # you change some variable or call a method like ENV.deparallelize, it + # only affects the lines after that command. + + # Do something only for clang + if ENV.compiler == :clang + # modify CFLAGS CXXFLAGS OBJCFLAGS OBJCXXFLAGS in one go: + ENV.append_to_cflags "-I ./missing/includes" + end + + # Overwriting any env var: + ENV["LDFLAGS"] = "--tag CC" + # Is the formula struggling to find the pkgconfig file? Point it to it. + # This is done automatically for `keg_only` formulae. + ENV.prepend_path "PKG_CONFIG_PATH", "#{Formula["glib"].opt_lib}/pkgconfig" + + # Need to install into the bin but the makefile doesn't mkdir -p prefix/bin? + bin.mkpath + # A custom directory? + mkdir_p share/"example" + # And then move something from the buildpath to that directory? + mv "ducks.txt", share/"example/ducks.txt" + # No "make", "install" available? + bin.install "binary1" + include.install "example.h" + lib.install "example.dylib" + man1.install "example.1" + man3.install "example.3" + # All that README/LICENSE/NOTES/CHANGELOG stuff? Use "metafiles" + prefix.install_metafiles + # Maybe you'd like to remove a broken or unnecessary element? + # Empty directories will be removed by Homebrew automatically post-install! + rm "bin/example" + rm_rf "share/pointless" + + # If there is a "make", "install" available, please use it! + system "make", "install" + + # We are in a temporary directory and don't have to care about cleanup. + + # Instead of `system "cp"` or something, call `install` on the Pathname + # objects as they are smarter with respect to correcting access rights. + # (`install` is a Homebrew mixin into Ruby's Pathname) + + # The pathnames defined in the formula + prefix # == HOMEBREW_PREFIX+"Cellar"+name+version + bin # == prefix+"bin" + doc # == share+"doc"+name + include # == prefix+"include" + info # == share+"info" + lib # == prefix+"lib" + libexec # == prefix+"libexec" + buildpath # The temporary directory where build occurs. + + man # share+"man" + man1 # man+"man1" + man2 # man+"man2" + man3 # man+"man3" + man4 # man+"man4" + man5 # man+"man5" + man6 # man+"man6" + man7 # man+"man7" + man8 # man+"man8" + sbin # prefix+"sbin" + share # prefix+"share" + frameworks # prefix+"Frameworks" + kext_prefix # prefix+"Library/Extensions" + # Configuration stuff that will survive formula updates + etc # HOMEBREW_PREFIX+"etc" + # Generally we don't want var stuff inside the keg + var # HOMEBREW_PREFIX+"var" + bash_completion # prefix+"etc/bash_completion.d" + zsh_completion # share+"zsh/site-functions" + # Further possibilities with the pathnames: + # http://www.ruby-doc.org/stdlib-1.8.7/libdoc/pathname/rdoc/Pathname.html + + # Copy `./example_code/simple/ones` to share/demos + (share/"demos").install "example_code/simple/ones" + # Copy `./example_code/simple/ones` to share/demos/examples + (share/"demos").install "example_code/simple/ones" => "examples" + + # Additional downloads can be defined as resources (see above). + # The stage method will create a temporary directory and yield + # to a block. + resource("additional_files").stage { bin.install "my/extra/tool" } + + # `name` and `version` are accessible too, if you need them. + end + + + ## Caveats + + def caveats + "Are optional. Something the user should know?" + end + + def caveats + s = <<-EOS.undent + Print some important notice to the user when `brew info ` is + called or when brewing a formula. + This is optional. You can use all the vars like #{version} here. + EOS + s += "Some issue only on older systems" if MacOS.version < :mountain_lion + s + end + + + ## Test (is optional but makes us happy) + + test do + # `test do` will create, run in, and delete a temporary directory. + + # We are fine if the executable does not error out, so we know linking + # and building the software was ok. + system bin/"foobar", "--version" + + (testpath/"Test.file").write <<-EOS.undent + writing some test file, if you need to + EOS + # To capture the output of a command, we use backtics: + assert_equal "OK", ` test.file`.strip + + # Need complete control over stdin, stdout? + require "open3" + Open3.popen3("#{bin}/example", "argument") do |stdin, stdout, _| + stdin.write("some text") + stdin.close + assert_equal "result", stdout.read + end + + # The test will fail if it returns false, or if an exception is raised. + # Failed assertions and failed `system` commands will raise exceptions. + end + + + ## Plist handling + + # Does your plist need to be loaded at startup? + plist_options :startup => true + # Or only when necessary or desired by the user? + plist_options :manual => "foo" + # Or perhaps you'd like to give the user a choice? Ooh fancy. + plist_options :startup => "true", :manual => "foo start" + + # Define this method to provide a plist. + # Looking for another example? Check out Apple's handy manpage => + # https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man5/plist.5.html + def plist; <<-EOS.undent + + + + + Label + #{plist_name} + ProgramArguments + + #{bin}/example + --do-this + + RunAtLoad + + KeepAlive + + StandardErrorPath + /dev/null + StandardOutPath + /dev/null + + EOS + end +end + +__END__ +# Room for a patch after the `__END__` +# Read about how to get a patch in here: +# https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Formula-Cookbook.md +# In short, `brew install --interactive --git ` and make your edits. +# Then `git diff >> path/to/your/formula.rb` +# Note, that HOMEBREW_PREFIX will be replaced in the path before it is +# applied. A patch can consit of several hunks. \ No newline at end of file diff --git a/syft/pkg/cataloger/homebrew/test-fixtures/formulas/syft/1.23.1/.brew/syft.rb b/syft/pkg/cataloger/homebrew/test-fixtures/formulas/syft/1.23.1/.brew/syft.rb new file mode 100644 index 000000000..15216c1fd --- /dev/null +++ b/syft/pkg/cataloger/homebrew/test-fixtures/formulas/syft/1.23.1/.brew/syft.rb @@ -0,0 +1,52 @@ +# typed: false +# frozen_string_literal: true + +# This file was generated by GoReleaser. DO NOT EDIT. +class Syft < Formula + desc "A tool that generates a Software Bill Of Materials (SBOM) from container images and filesystems" + homepage "https://github.com/anchore/syft" + version "1.23.1" + license "Apache License 2.0" + + on_macos do + if Hardware::CPU.intel? + url "https://github.com/anchore/syft/releases/download/v1.23.1/syft_1.23.1_darwin_amd64.tar.gz" + sha256 "76fed9a16fec65c2b13f30e2db6128f625aaf54b82302b427a0e2bbb554c6ab7" + + def install + bin.install "syft" + end + end + if Hardware::CPU.arm? + url "https://github.com/anchore/syft/releases/download/v1.23.1/syft_1.23.1_darwin_arm64.tar.gz" + sha256 "099f506860bcb5d85d4a981b4fca7a732978d0eeff79876648cc1a5350974f33" + + def install + bin.install "syft" + end + end + end + + on_linux do + if Hardware::CPU.intel? + if Hardware::CPU.is_64_bit? + url "https://github.com/anchore/syft/releases/download/v1.23.1/syft_1.23.1_linux_amd64.tar.gz" + sha256 "42f3e01b64f054d0caee42073cb94e3ac3e61be6f0100e7ecda96e6a2abf7e22" + + def install + bin.install "syft" + end + end + end + if Hardware::CPU.arm? + if Hardware::CPU.is_64_bit? + url "https://github.com/anchore/syft/releases/download/v1.23.1/syft_1.23.1_linux_arm64.tar.gz" + sha256 "6172794c95aebb5c3e84760d6489d1c149762822e254a2e3d413923c1b4263e4" + + def install + bin.install "syft" + end + end + end + end +end \ No newline at end of file diff --git a/syft/pkg/cataloger/homebrew/test-fixtures/install-example/opt/homebrew/Cellar/foo/1.2.3/.brew/foo.rb b/syft/pkg/cataloger/homebrew/test-fixtures/install-example/opt/homebrew/Cellar/foo/1.2.3/.brew/foo.rb new file mode 100644 index 000000000..0091b3aa0 --- /dev/null +++ b/syft/pkg/cataloger/homebrew/test-fixtures/install-example/opt/homebrew/Cellar/foo/1.2.3/.brew/foo.rb @@ -0,0 +1,3 @@ +desc "A test Homebrew formula for Foo" +homepage "https://example.com/foo" +license "Apache 2.0" \ No newline at end of file diff --git a/syft/pkg/cataloger/homebrew/test-fixtures/install-example/opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb b/syft/pkg/cataloger/homebrew/test-fixtures/install-example/opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb new file mode 100644 index 000000000..2bd16a79d --- /dev/null +++ b/syft/pkg/cataloger/homebrew/test-fixtures/install-example/opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb @@ -0,0 +1,6 @@ + +desc "A test Homebrew formula for bar" +homepage "https://example.com/bar" +license "MIT" +name "bar" +version "4.5.6" \ No newline at end of file diff --git a/syft/pkg/homebrew.go b/syft/pkg/homebrew.go new file mode 100644 index 000000000..8abc567ee --- /dev/null +++ b/syft/pkg/homebrew.go @@ -0,0 +1,7 @@ +package pkg + +type HomebrewFormula struct { + Tap string `json:"tap,omitempty"` + Homepage string `json:"homepage,omitempty"` + Description string `json:"description,omitempty"` +} diff --git a/syft/pkg/type.go b/syft/pkg/type.go index d9090ca24..04c08cd40 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -48,6 +48,7 @@ const ( SwiplPackPkg Type = "swiplpack" TerraformPkg Type = "terraform" WordpressPluginPkg Type = "wordpress-plugin" + HomebrewPkg Type = "homebrew" ) // AllPkgs represents all supported package types @@ -90,6 +91,7 @@ var AllPkgs = []Type{ SwiplPackPkg, TerraformPkg, WordpressPluginPkg, + HomebrewPkg, } // PackageURLType returns the PURL package type for the current package. @@ -162,6 +164,8 @@ func (t Type) PackageURLType() string { return "terraform" case WordpressPluginPkg: return "wordpress-plugin" + case HomebrewPkg: + return "homebrew" default: // TODO: should this be a "generic" purl type instead? return "" @@ -246,6 +250,8 @@ func TypeByName(name string) Type { return TerraformPkg case "wordpress-plugin": return WordpressPluginPkg + case "homebrew": + return HomebrewPkg default: return UnknownPkg } diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index d5ccd9469..abb1ba569 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -147,6 +147,7 @@ func TestTypeFromPURL(t *testing.T) { expectedTypes.Remove(string(LinuxKernelModulePkg)) expectedTypes.Remove(string(GithubActionPkg), string(GithubActionWorkflowPkg)) expectedTypes.Remove(string(WordpressPluginPkg)) + expectedTypes.Remove(string(HomebrewPkg)) expectedTypes.Remove(string(TerraformPkg)) expectedTypes.Remove(string(GraalVMNativeImagePkg)) expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package diff --git a/test/cli/scan_cmd_test.go b/test/cli/scan_cmd_test.go index 6b6c197c0..42aad73db 100644 --- a/test/cli/scan_cmd_test.go +++ b/test/cli/scan_cmd_test.go @@ -9,7 +9,7 @@ import ( const ( // this is the number of packages that should be found in the image-pkg-coverage fixture image // when analyzed with the squashed scope. - coverageImageSquashedPackageCount = 42 + coverageImageSquashedPackageCount = 43 ) func TestPackagesCmdFlags(t *testing.T) {