diff --git a/README.md b/README.md index 66268f789..5b6aaf0a4 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http - Java (jar, ear, war, par, sar, native-image) - JavaScript (npm, yarn) - Jenkins Plugins (jpi, hpi) +- Nix (outputs in /nix/store) - PHP (composer) - Python (wheel, egg, poetry, requirements.txt) - Red Hat (rpm) diff --git a/internal/constants.go b/internal/constants.go index a58edec07..1fda74223 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -6,5 +6,5 @@ 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 = "7.1.0" + JSONSchemaVersion = "7.1.1" ) diff --git a/schema/json/generate.go b/schema/json/generate.go index 0c7e422d7..3f4efaf4a 100644 --- a/schema/json/generate.go +++ b/schema/json/generate.go @@ -45,6 +45,7 @@ type artifactMetadataContainer struct { Hackage pkg.HackageMetadata Java pkg.JavaMetadata KbPackage pkg.KbPackageMetadata + Nix pkg.NixStoreMetadata NpmPackage pkg.NpmPackageJSONMetadata NpmPackageLock pkg.NpmPackageLockJSONMetadata MixLock pkg.MixLockMetadata diff --git a/schema/json/schema-7.1.1.json b/schema/json/schema-7.1.1.json new file mode 100644 index 000000000..e64509eb3 --- /dev/null +++ b/schema/json/schema-7.1.1.json @@ -0,0 +1,1663 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/anchore/syft/syft/formats/syftjson/model/document", + "$ref": "#/$defs/Document", + "$defs": { + "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" + }, + "AlpmMetadata": { + "properties": { + "basepackage": { + "type": "string" + }, + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "packager": { + "type": "string" + }, + "license": { + "type": "string" + }, + "url": { + "type": "string" + }, + "validation": { + "type": "string" + }, + "reason": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "license", + "url", + "validation", + "reason", + "files", + "backup" + ] + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "ApkMetadata": { + "properties": { + "package": { + "type": "string" + }, + "originPackage": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "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", + "license", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ] + }, + "BinaryMetadata": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ] + }, + "CargoPackageMetadata": { + "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" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ] + }, + "CocoapodsMetadata": { + "properties": { + "checksum": { + "type": "string" + } + }, + "type": "object", + "required": [ + "checksum" + ] + }, + "ConanLockMetadata": { + "properties": { + "ref": { + "type": "string" + }, + "package_id": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "requires": { + "type": "string" + }, + "build_requires": { + "type": "string" + }, + "py_requires": { + "type": "string" + }, + "options": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "path": { + "type": "string" + }, + "context": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "ConanMetadata": { + "properties": { + "ref": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ref" + ] + }, + "Coordinates": { + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "DartPubMetadata": { + "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" + }, + "secrets": { + "items": { + "$ref": "#/$defs/Secrets" + }, + "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" + ] + }, + "DotnetDepsMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ] + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ] + }, + "DpkgMetadata": { + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ] + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "id", + "location" + ] + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType" + ] + }, + "GemMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "GolangBinMetadata": { + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "type": "string" + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ] + }, + "GolangModMetadata": { + "properties": { + "h1Digest": { + "type": "string" + } + }, + "type": "object" + }, + "HackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "snapshotURL": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version" + ] + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "JavaMetadata": { + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest" + }, + "pomProperties": { + "$ref": "#/$defs/PomProperties" + }, + "pomProject": { + "$ref": "#/$defs/PomProject" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "virtualPath" + ] + }, + "KbPackageMetadata": { + "properties": { + "product_id": { + "type": "string" + }, + "kb": { + "type": "string" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ] + }, + "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" + }, + "virtualPath": { + "type": "string" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "MixLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "NixStoreMetadata": { + "properties": { + "outputHash": { + "type": "string" + }, + "output": { + "type": "string" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "outputHash", + "files" + ] + }, + "NpmPackageJSONMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "author": { + "type": "string" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "private": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "licenses", + "homepage", + "description", + "url", + "private" + ] + }, + "NpmPackageLockJSONMetadata": { + "properties": { + "resolved": { + "type": "string" + }, + "integrity": { + "type": "string" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ] + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Coordinates" + }, + "type": "array" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "language": { + "type": "string" + }, + "cpes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmMetadata" + }, + { + "$ref": "#/$defs/ApkMetadata" + }, + { + "$ref": "#/$defs/BinaryMetadata" + }, + { + "$ref": "#/$defs/CargoPackageMetadata" + }, + { + "$ref": "#/$defs/CocoapodsMetadata" + }, + { + "$ref": "#/$defs/ConanLockMetadata" + }, + { + "$ref": "#/$defs/ConanMetadata" + }, + { + "$ref": "#/$defs/DartPubMetadata" + }, + { + "$ref": "#/$defs/DotnetDepsMetadata" + }, + { + "$ref": "#/$defs/DpkgMetadata" + }, + { + "$ref": "#/$defs/GemMetadata" + }, + { + "$ref": "#/$defs/GolangBinMetadata" + }, + { + "$ref": "#/$defs/GolangModMetadata" + }, + { + "$ref": "#/$defs/HackageMetadata" + }, + { + "$ref": "#/$defs/JavaMetadata" + }, + { + "$ref": "#/$defs/KbPackageMetadata" + }, + { + "$ref": "#/$defs/MixLockMetadata" + }, + { + "$ref": "#/$defs/NixStoreMetadata" + }, + { + "$ref": "#/$defs/NpmPackageJSONMetadata" + }, + { + "$ref": "#/$defs/NpmPackageLockJSONMetadata" + }, + { + "$ref": "#/$defs/PhpComposerJSONMetadata" + }, + { + "$ref": "#/$defs/PortageMetadata" + }, + { + "$ref": "#/$defs/PythonPackageMetadata" + }, + { + "$ref": "#/$defs/PythonPipfileLockMetadata" + }, + { + "$ref": "#/$defs/RebarLockMetadata" + }, + { + "$ref": "#/$defs/RpmMetadata" + } + ] + } + }, + "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" + ] + }, + "PhpComposerJSONMetadata": { + "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" + }, + "type": { + "type": "string" + }, + "notification-url": { + "type": "string" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "license": { + "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" + ] + }, + "PomParent": { + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ] + }, + "PomProject": { + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$ref": "#/$defs/PomParent" + }, + "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" + ] + }, + "PomProperties": { + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ] + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/Digest" + } + }, + "type": "object", + "required": [ + "path" + ] + }, + "PortageMetadata": { + "properties": { + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ] + }, + "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" + ] + }, + "PythonPackageMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ] + }, + "PythonPipfileLockMetadata": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "index": { + "type": "string" + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ] + }, + "RebarLockMetadata": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "pkgHash": { + "type": "string" + }, + "pkgHashExt": { + "type": "string" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ] + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmMetadata": { + "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" + }, + "license": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "modularityLabel": { + "type": "string" + }, + "files": { + "items": { + "$ref": "#/$defs/RpmdbFileRecord" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "license", + "vendor", + "modularityLabel", + "files" + ] + }, + "RpmdbFileRecord": { + "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" + ] + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "SearchResult": { + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ] + }, + "Secrets": { + "properties": { + "location": { + "$ref": "#/$defs/Coordinates" + }, + "secrets": { + "items": { + "$ref": "#/$defs/SearchResult" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "location", + "secrets" + ] + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "target": true + }, + "type": "object", + "required": [ + "id", + "type", + "target" + ] + } + } +} diff --git a/syft/formats/common/spdxhelpers/source_info.go b/syft/formats/common/spdxhelpers/source_info.go index f13187e5e..2222f757f 100644 --- a/syft/formats/common/spdxhelpers/source_info.go +++ b/syft/formats/common/spdxhelpers/source_info.go @@ -45,6 +45,8 @@ func SourceInfo(p pkg.Package) string { answer = "acquired package info from cabal or stack manifest files" case pkg.HexPkg: answer = "acquired package info from rebar3 or mix manifest file" + case pkg.NixPkg: + answer = "acquired package info from nix store path" default: answer = "acquired package info from the following paths" } diff --git a/syft/formats/common/spdxhelpers/source_info_test.go b/syft/formats/common/spdxhelpers/source_info_test.go index f26d0744d..f70e00892 100644 --- a/syft/formats/common/spdxhelpers/source_info_test.go +++ b/syft/formats/common/spdxhelpers/source_info_test.go @@ -199,6 +199,14 @@ func Test_SourceInfo(t *testing.T) { "from rebar3 or mix manifest file", }, }, + { + input: pkg.Package{ + Type: pkg.NixPkg, + }, + expected: []string{ + "from nix store path", + }, + }, } var pkgTypes []pkg.Type for _, test := range tests { diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index ce9102d5f..8d30145dd 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -89,7 +89,7 @@ } }, "schema": { - "version": "7.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json" + "version": "7.1.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json" } } diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index 353e1d7d5..7b4ccd02b 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -185,7 +185,7 @@ } }, "schema": { - "version": "7.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json" + "version": "7.1.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json" } } diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index 61fd33299..7e922e6ac 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -112,7 +112,7 @@ } }, "schema": { - "version": "7.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json" + "version": "7.1.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json" } } diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index dfc91bb55..9bab7043e 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -23,6 +23,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/haskell" "github.com/anchore/syft/syft/pkg/cataloger/java" "github.com/anchore/syft/syft/pkg/cataloger/javascript" + "github.com/anchore/syft/syft/pkg/cataloger/nix" "github.com/anchore/syft/syft/pkg/cataloger/php" "github.com/anchore/syft/syft/pkg/cataloger/portage" "github.com/anchore/syft/syft/pkg/cataloger/python" @@ -51,6 +52,7 @@ func ImageCatalogers(cfg Config) []pkg.Cataloger { golang.NewGoModuleBinaryCataloger(cfg.Go()), dotnet.NewDotnetDepsCataloger(), portage.NewPortageCataloger(), + nix.NewStoreCataloger(), sbom.NewSBOMCataloger(), binary.NewCataloger(), }, cfg.Catalogers) @@ -85,6 +87,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger { binary.NewCataloger(), elixir.NewMixLockCataloger(), erlang.NewRebarLockCataloger(), + nix.NewStoreCataloger(), }, cfg.Catalogers) } @@ -121,6 +124,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger { binary.NewCataloger(), elixir.NewMixLockCataloger(), erlang.NewRebarLockCataloger(), + nix.NewStoreCataloger(), }, cfg.Catalogers) } diff --git a/syft/pkg/cataloger/nix/cataloger.go b/syft/pkg/cataloger/nix/cataloger.go new file mode 100644 index 000000000..4343844b1 --- /dev/null +++ b/syft/pkg/cataloger/nix/cataloger.go @@ -0,0 +1,101 @@ +package nix + +import ( + "fmt" + + "github.com/bmatcuk/doublestar/v4" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +const ( + catalogerName = "nix-store-cataloger" + nixStoreGlob = "**/nix/store/*" +) + +// StoreCataloger finds package outputs installed in the Nix store location (/nix/store/*). +type StoreCataloger struct{} + +func NewStoreCataloger() *StoreCataloger { + return &StoreCataloger{} +} + +func (c *StoreCataloger) Name() string { + return catalogerName +} + +func (c *StoreCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) { + // we want to search for only directories, which isn't possible via the stereoscope API, so we need to apply the glob manually on all returned paths + var pkgs []pkg.Package + var filesByPath = make(map[string]*source.LocationSet) + for location := range resolver.AllLocations() { + matchesStorePath, err := doublestar.Match(nixStoreGlob, location.RealPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to match nix store path: %w", err) + } + + parentStorePath := findParentNixStorePath(location.RealPath) + if parentStorePath != "" { + if _, ok := filesByPath[parentStorePath]; !ok { + s := source.NewLocationSet() + filesByPath[parentStorePath] = &s + } + filesByPath[parentStorePath].Add(location) + } + + if !matchesStorePath { + continue + } + + storePath := parseNixStorePath(location.RealPath) + + if storePath == nil || !storePath.isValidPackage() { + continue + } + + p := newNixStorePackage(*storePath, location) + pkgs = append(pkgs, p) + } + + // add file sets to packages + for i := range pkgs { + p := &pkgs[i] + locations := p.Locations.ToSlice() + if len(locations) == 0 { + log.WithFields("package", p.Name).Warn("nix package has no evidence locations associated") + continue + } + parentStorePath := locations[0].RealPath + files, ok := filesByPath[parentStorePath] + if !ok { + log.WithFields("path", parentStorePath, "nix-store-path", parentStorePath).Warn("found a nix store file for a non-existent package") + continue + } + appendFiles(p, files.ToSlice()...) + } + + return pkgs, nil, nil +} + +func appendFiles(p *pkg.Package, location ...source.Location) { + metadata, ok := p.Metadata.(pkg.NixStoreMetadata) + if !ok { + log.WithFields("package", p.Name).Warn("nix package metadata missing") + return + } + + for _, l := range location { + metadata.Files = append(metadata.Files, l.RealPath) + } + + if metadata.Files == nil { + // note: we always have an allocated collection for output + metadata.Files = []string{} + } + + p.Metadata = metadata + p.SetID() +} diff --git a/syft/pkg/cataloger/nix/cataloger_test.go b/syft/pkg/cataloger/nix/cataloger_test.go new file mode 100644 index 000000000..10b544fc0 --- /dev/null +++ b/syft/pkg/cataloger/nix/cataloger_test.go @@ -0,0 +1,55 @@ +package nix + +import ( + "testing" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" +) + +func TestCataloger_Catalog(t *testing.T) { + + tests := []struct { + fixture string + wantPkgs []pkg.Package + wantRel []artifact.Relationship + }{ + { + fixture: "test-fixtures/fixture-1", + wantPkgs: []pkg.Package{ + { + Name: "glibc", + Version: "2.34-210", + PURL: "pkg:nix/glibc@2.34-210?output=bin&outputhash=h0cnbmfcn93xm5dg2x27ixhag1cwndga", + Locations: source.NewLocationSet(source.NewLocation("nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin")), + FoundBy: catalogerName, + Type: pkg.NixPkg, + MetadataType: pkg.NixStoreMetadataType, + Metadata: pkg.NixStoreMetadata{ + OutputHash: "h0cnbmfcn93xm5dg2x27ixhag1cwndga", + Output: "bin", + Files: []string{ + "nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib", + "nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib/glibc.so", + "nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share", + "nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man", + "nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man/glibc.1", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.fixture, func(t *testing.T) { + c := NewStoreCataloger() + + pkgtest.NewCatalogTester(). + FromDirectory(t, tt.fixture). + Expects(tt.wantPkgs, tt.wantRel). + TestCataloger(t, c) + }) + } +} diff --git a/syft/pkg/cataloger/nix/package.go b/syft/pkg/cataloger/nix/package.go new file mode 100644 index 000000000..6e473d6fd --- /dev/null +++ b/syft/pkg/cataloger/nix/package.go @@ -0,0 +1,59 @@ +package nix + +import ( + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func newNixStorePackage(storePath nixStorePath, locations ...source.Location) pkg.Package { + p := pkg.Package{ + Name: storePath.name, + Version: storePath.version, + FoundBy: catalogerName, + Locations: source.NewLocationSet(locations...), + Type: pkg.NixPkg, + PURL: packageURL(storePath), + MetadataType: pkg.NixStoreMetadataType, + Metadata: pkg.NixStoreMetadata{ + OutputHash: storePath.outputHash, + Output: storePath.output, + }, + } + + p.SetID() + + return p +} + +func packageURL(storePath nixStorePath) string { + var qualifiers packageurl.Qualifiers + if storePath.output != "" { + // since there is no nix pURL type yet, this is a guess, however, it is reasonable to assume that + // if only a single output is installed the pURL should be able to express this. + qualifiers = append(qualifiers, + packageurl.Qualifier{ + Key: "output", + Value: storePath.output, + }, + ) + } + if storePath.outputHash != "" { + // it's not immediately clear if the hash found in the store path should be encoded in the pURL + qualifiers = append(qualifiers, + packageurl.Qualifier{ + Key: "outputhash", + Value: storePath.outputHash, + }, + ) + } + pURL := packageurl.NewPackageURL( + // TODO: nix pURL type has not been accepted yet (only proposed at this time) + "nix", + "", + storePath.name, + storePath.version, + qualifiers, + "") + return pURL.ToString() +} diff --git a/syft/pkg/cataloger/nix/package_test.go b/syft/pkg/cataloger/nix/package_test.go new file mode 100644 index 000000000..df7d3ca40 --- /dev/null +++ b/syft/pkg/cataloger/nix/package_test.go @@ -0,0 +1,49 @@ +package nix + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_packageURL(t *testing.T) { + + tests := []struct { + name string + storePath nixStorePath + want string + }{ + { + name: "name + version", + storePath: nixStorePath{ + name: "glibc", + version: "2.34", + }, + want: "pkg:nix/glibc@2.34", + }, + { + name: "hash qualifier", + storePath: nixStorePath{ + name: "glibc", + version: "2.34", + outputHash: "h0cnbmfcn93xm5dg2x27ixhag1cwndga", + }, + want: "pkg:nix/glibc@2.34?outputhash=h0cnbmfcn93xm5dg2x27ixhag1cwndga", + }, + { + name: "output qualifier", + storePath: nixStorePath{ + name: "glibc", + version: "2.34", + outputHash: "h0cnbmfcn93xm5dg2x27ixhag1cwndga", + output: "bin", + }, + want: "pkg:nix/glibc@2.34?output=bin&outputhash=h0cnbmfcn93xm5dg2x27ixhag1cwndga", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, packageURL(tt.storePath)) + }) + } +} diff --git a/syft/pkg/cataloger/nix/parse_nix_store_path.go b/syft/pkg/cataloger/nix/parse_nix_store_path.go new file mode 100644 index 000000000..dc70e989d --- /dev/null +++ b/syft/pkg/cataloger/nix/parse_nix_store_path.go @@ -0,0 +1,134 @@ +package nix + +import ( + "fmt" + "path" + "regexp" + "strings" +) + +var ( + numericPattern = regexp.MustCompile(`\d`) + + // attempts to find the right-most example of something that appears to be a version (semver or otherwise) + // example input: h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin + // example output: + // version: "2.34-210" + // major: "2" + // minor: "34" + // patch: "210" + // (there are other capture groups, but they can be ignored) + rightMostVersionIshPattern = regexp.MustCompile(`-(?P(?P[0-9][a-zA-Z0-9]*)(\.(?P[0-9][a-zA-Z0-9]*))?(\.(?P0|[1-9][a-zA-Z0-9]*)){0,3}(?:-(?P\d*[.a-zA-Z-][.0-9a-zA-Z-]*)*)?(?:\+(?P[.0-9a-zA-Z-]+(?:\.[.0-9a-zA-Z-]+)*))?)`) + + unstableVersion = regexp.MustCompile(`-(?Punstable-\d{4}-\d{2}-\d{2})$`) +) + +// checkout the package naming conventions here: https://nixos.org/manual/nixpkgs/stable/#sec-package-naming + +type nixStorePath struct { + outputHash string + name string + version string + output string +} + +func (p nixStorePath) isValidPackage() bool { + return p.name != "" && p.version != "" +} + +func findParentNixStorePath(source string) string { + source = strings.TrimRight(source, "/") + indicator := "nix/store/" + start := strings.Index(source, indicator) + if start == -1 { + return "" + } + + startOfHash := start + len(indicator) + nextField := strings.Index(source[startOfHash:], "/") + if nextField == -1 { + return "" + } + startOfSubPath := startOfHash + nextField + + return source[0:startOfSubPath] +} + +func parseNixStorePath(source string) *nixStorePath { + if strings.HasSuffix(source, ".drv") { + // ignore derivations + return nil + } + + source = path.Base(source) + + versionStartIdx, versionIsh, prerelease := findVersionIsh(source) + if versionStartIdx == -1 { + return nil + } + + hashName := strings.TrimSuffix(source[0:versionStartIdx], "-") + hashNameFields := strings.Split(hashName, "-") + if len(hashNameFields) < 2 { + return nil + } + hash, name := hashNameFields[0], strings.Join(hashNameFields[1:], "-") + + prereleaseFields := strings.Split(prerelease, "-") + lastPrereleaseField := prereleaseFields[len(prereleaseFields)-1] + + var version = versionIsh + var output string + if !hasNumeric(lastPrereleaseField) { + // this last prerelease field is probably a nix output + version = strings.TrimSuffix(versionIsh, fmt.Sprintf("-%s", lastPrereleaseField)) + output = lastPrereleaseField + } + + return &nixStorePath{ + outputHash: hash, + name: name, + version: version, + output: output, + } +} + +func hasNumeric(s string) bool { + return numericPattern.MatchString(s) +} + +func findVersionIsh(input string) (int, string, string) { + // we want to return the index of the start of the "version" group (the first capture group). + // note that the match indices are in the form of [start, end, start, end, ...]. Also note that the + // capture group for version in both regexes are the same index, but if the regexes are changed + // this code will start to fail. + versionGroup := 1 + + match := unstableVersion.FindAllStringSubmatchIndex(input, -1) + if len(match) > 0 && len(match[0]) > 0 { + return match[0][versionGroup*2], input[match[0][versionGroup*2]:match[0][(versionGroup*2)+1]], "" + } + + match = rightMostVersionIshPattern.FindAllStringSubmatchIndex(input, -1) + if len(match) == 0 || len(match[0]) == 0 { + return -1, "", "" + } + + var version string + versionStart, versionStop := match[0][versionGroup*2], match[0][(versionGroup*2)+1] + if versionStart != -1 || versionStop != -1 { + version = input[versionStart:versionStop] + } + + prereleaseGroup := 7 + + var prerelease string + prereleaseStart, prereleaseStop := match[0][prereleaseGroup*2], match[0][(prereleaseGroup*2)+1] + if prereleaseStart != -1 && prereleaseStop != -1 { + prerelease = input[prereleaseStart:prereleaseStop] + } + + return versionStart, + version, + prerelease +} diff --git a/syft/pkg/cataloger/nix/parse_nix_store_path_test.go b/syft/pkg/cataloger/nix/parse_nix_store_path_test.go new file mode 100644 index 000000000..6ea877543 --- /dev/null +++ b/syft/pkg/cataloger/nix/parse_nix_store_path_test.go @@ -0,0 +1,304 @@ +package nix + +import ( + "path" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_findVersionIsh(t *testing.T) { + // note: only the package version fields are tested here, the name is tested in parseNixStorePath below. + tests := []struct { + name string + input string + wantIdx int + wantVersion string + wantPreRelease string + }{ + { + name: "no version", + input: "5q7vxm9lc4b9hifc3br4sr8dy7f2h0qa-source", + wantIdx: -1, + wantVersion: "", + wantPreRelease: "", + }, + { + name: "semver with overbite into output", + input: "/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin", + wantIdx: 50, + wantVersion: "2.34-210-bin", + wantPreRelease: "210-bin", + }, + { + name: "multiple versions", + input: "5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33", + wantIdx: 53, + wantVersion: "2.33", + wantPreRelease: "", + }, + { + name: "name ends with number", + input: "55nswyz8335lk954y1ccx6as2jbq1z8f-libfido2-1.10.0", + wantIdx: 42, + wantVersion: "1.10.0", + wantPreRelease: "", + }, + { + name: "major-minor only", + input: "q8gnp7r8475p52k9gmdzsrcddw5hirbn-gdbm-1.23", + wantIdx: 38, + wantVersion: "1.23", + wantPreRelease: "", + }, + { + name: "0-prefixed version field", + input: "r705jm2icczpnmfccby3fzfrckfjakx3-perl5.34.1-URI-5.05", + wantIdx: 48, + wantVersion: "5.05", + wantPreRelease: "", + }, + { + name: "prerelease with alpha prefix", + input: "v48s6iddb518j9lc1pk3rcn3x8c2ff0j-bash-interactive-5.1-p16", + wantIdx: 50, + wantVersion: "5.1-p16", + wantPreRelease: "p16", + }, + { + + name: "0-major version", + input: "x2f9x5q6qrs6cssx09ylxqyg9q2isi1z-aws-c-http-0.6.15", + wantIdx: 44, + wantVersion: "0.6.15", + wantPreRelease: "", + }, + { + + name: "several version fields", + // note: this package version is fictitious + input: "z24qs6f5d1mmwdp73n1jfc3swj4v2c5s-krb5-1.19.3.9.10", + wantIdx: 38, + wantVersion: "1.19.3.9.10", + wantPreRelease: "", + }, + { + + name: "skip drv + major only version", + input: "z0fqylhisz47krxv8fd0izm1i2qbswfr-readline63-006.drv", + wantIdx: 44, + wantVersion: "006", + wantPreRelease: "", + }, + { + + name: "prerelease with multiple dashes", + input: "zkgyp2vra0bgqm0dv1qi514l5fd0aksx-bash-interactive-5.1-p16-man", + wantIdx: 50, + wantVersion: "5.1-p16-man", + wantPreRelease: "p16-man", + }, + { + + name: "date as major version", + input: "0amf0d1dymv9gqcyhhjb9j0l8sn00c56-libedit-20210910-3.1", + wantIdx: 41, + wantVersion: "20210910-3.1", + wantPreRelease: "3.1", + }, + { + + name: "long name", + input: "0296qxvn30z9b2ah1g5p97k5wr9k8y78-busybox-static-x86_64-unknown-linux-musl-1.35.0", + wantIdx: 74, + wantVersion: "1.35.0", + wantPreRelease: "", + }, + { + // this accounts for https://nixos.org/manual/nixpkgs/stable/#sec-package-naming + // > If a package is not a release but a commit from a repository, then the version attribute must + // > be the date of that (fetched) commit. The date must be in "unstable-YYYY-MM-DD" format. + // example: https://github.com/NixOS/nixpkgs/blob/798e23beab9b5cba4d6f05e8b243e1d4535770f3/pkgs/servers/webdav-server-rs/default.nix#L14 + name: "unstable version", + input: "q5dhwzcn82by5ndc7g0q83wsnn13qkqw-webdav-server-rs-unstable-2021-08-16", + wantIdx: 50, + wantVersion: "unstable-2021-08-16", + wantPreRelease: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotIdx, gotVersion, gotPreRelease := findVersionIsh(tt.input) + assert.Equal(t, tt.wantIdx, gotIdx) + assert.Equal(t, tt.wantVersion, gotVersion) + assert.Equal(t, tt.wantPreRelease, gotPreRelease) + }) + } +} + +func Test_parseNixStorePath(t *testing.T) { + + tests := []struct { + source string + want *nixStorePath + }{ + { + source: "/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin", + want: &nixStorePath{ + outputHash: "h0cnbmfcn93xm5dg2x27ixhag1cwndga", + name: "glibc", + version: "2.34-210", + output: "bin", + }, + }, + { + source: "/nix/store/0296qxvn30z9b2ah1g5p97k5wr9k8y78-busybox-static-x86_64-unknown-linux-musl-1.35.0", + want: &nixStorePath{ + outputHash: "0296qxvn30z9b2ah1g5p97k5wr9k8y78", + name: "busybox-static-x86_64-unknown-linux-musl", + version: "1.35.0", + }, + }, + { + source: "/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33", + want: &nixStorePath{ + outputHash: "5zzrvdmlkc5rh3k5862krd3wfb3pqhyf", + name: "perl5.34.1-TimeDate", + version: "2.33", + }, + }, + { + source: "/nix/store/q38q8ng57zwjg1h15ry5zx0lb0xyax4b-libcap-2.63-lib", + want: &nixStorePath{ + outputHash: "q38q8ng57zwjg1h15ry5zx0lb0xyax4b", + name: "libcap", + version: "2.63", + output: "lib", + }, + }, + { + source: "/nix/store/p0y8fbpbqr2jm5zfrdll0rgyg2lvp5g2-util-linux-minimal-2.37.4-bin", + want: &nixStorePath{ + outputHash: "p0y8fbpbqr2jm5zfrdll0rgyg2lvp5g2", + name: "util-linux-minimal", + version: "2.37.4", + output: "bin", + }, + }, + { + source: "/nix/store/z24qs6f5d1mmwdp73n1jfc3swj4v2c5s-krb5-1.19.3.9.10", + want: &nixStorePath{ + outputHash: "z24qs6f5d1mmwdp73n1jfc3swj4v2c5s", + name: "krb5", + version: "1.19.3.9.10", + }, + }, + { + source: "/nix/store/zkgyp2vra0bgqm0dv1qi514l5fd0aksx-bash-interactive-5.1-p16-man", + want: &nixStorePath{ + outputHash: "zkgyp2vra0bgqm0dv1qi514l5fd0aksx", + name: "bash-interactive", + version: "5.1-p16", + output: "man", + }, + }, + { + source: "/nix/store/nwf2y0nc48ybim56308cr5ccvwkabcqc-openssl-1.1.1q", + want: &nixStorePath{ + outputHash: "nwf2y0nc48ybim56308cr5ccvwkabcqc", + name: "openssl", + version: "1.1.1q", + }, + }, + { + source: "/nix/store/nwv742f1bxv6g78hy9yc6slxdbxlmqhb-kmod-29", + want: &nixStorePath{ + outputHash: "nwv742f1bxv6g78hy9yc6slxdbxlmqhb", + name: "kmod", + version: "29", + }, + }, + { + source: "/nix/store/n83qx7m848kg51lcjchwbkmlgdaxfckf-tzdata-2022a", + want: &nixStorePath{ + outputHash: "n83qx7m848kg51lcjchwbkmlgdaxfckf", + name: "tzdata", + version: "2022a", + }, + }, + { + source: "'/nix/store/q5dhwzcn82by5ndc7g0q83wsnn13qkqw-webdav-server-rs-unstable-2021-08-16", + want: &nixStorePath{ + outputHash: "q5dhwzcn82by5ndc7g0q83wsnn13qkqw", + name: "webdav-server-rs", + version: "unstable-2021-08-16", + }, + }, + // negative cases... + { + source: "'z33yk02rsr6b4rb56lgb80bnvxx6yw39-?id=21ee35dde73aec5eba35290587d479218c6dd824.drv'", + }, + { + source: "/nix/store/yzahni8aig6mdrvcsccgwm2515lcpi5q-git-minimal-2.36.0.drv", + }, + { + source: "/nix/store/z9yvxs0s3xdkp5jgmzis4g50bfq3dgvm-0018-pkg-config-derive-prefix-from-prefix.patch", + }, + { + source: "/nix/store/w3hl7zrmc9qvzadc0k7cp9ysxiyz88j6-base-system", + }, + { + source: "/nix/store/zz1lc28x25fcx6al6xwk3dk8kp7wx47y-Test-RequiresInternet-0.05.tar.gz.drv", + }, + } + for _, tt := range tests { + t.Run(path.Base(tt.source), func(t *testing.T) { + assert.Equal(t, tt.want, parseNixStorePath(tt.source)) + }) + } +} + +func Test_parentNixStorePath(t *testing.T) { + + tests := []struct { + name string + source string + want string + }{ + { + name: "exact path from absolute root", + source: "/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33", + want: "", + }, + { + name: "exact path from relative root", + source: "nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33", + want: "", + }, + { + name: "clean paths", + source: "//nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33///", + want: "", + }, + { + name: "relative root with subdir file", + source: "nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33/bin/perl-timedate", + want: "nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33", + }, + { + name: "absolute root with with subdir file", + source: "/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33/bin/perl-timedate", + want: "/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33", + }, + { + name: "nexted root with with subdir file", + source: "/somewhere/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33/bin/perl-timedate", + want: "/somewhere/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, findParentNixStorePath(tt.source)) + }) + } +} diff --git a/syft/pkg/cataloger/nix/test-fixtures/fixture-1/.gitignore b/syft/pkg/cataloger/nix/test-fixtures/fixture-1/.gitignore new file mode 100644 index 000000000..723bcdad3 --- /dev/null +++ b/syft/pkg/cataloger/nix/test-fixtures/fixture-1/.gitignore @@ -0,0 +1,2 @@ +# this is not a real binary, just a small text file +!nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib/glibc.so \ No newline at end of file diff --git a/syft/pkg/cataloger/nix/test-fixtures/fixture-1/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib/glibc.so b/syft/pkg/cataloger/nix/test-fixtures/fixture-1/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib/glibc.so new file mode 100644 index 000000000..77e9738ab --- /dev/null +++ b/syft/pkg/cataloger/nix/test-fixtures/fixture-1/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib/glibc.so @@ -0,0 +1 @@ +the binary \ No newline at end of file diff --git a/syft/pkg/cataloger/nix/test-fixtures/fixture-1/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man/glibc.1 b/syft/pkg/cataloger/nix/test-fixtures/fixture-1/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man/glibc.1 new file mode 100644 index 000000000..fd4ed835d --- /dev/null +++ b/syft/pkg/cataloger/nix/test-fixtures/fixture-1/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man/glibc.1 @@ -0,0 +1 @@ +the man pages \ No newline at end of file diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index 3fff9e5d7..6e79d0c3c 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -9,6 +9,7 @@ type MetadataType string const ( // this is the full set of data shapes that can be represented within the pkg.Package.Metadata field + UnknownMetadataType MetadataType = "UnknownMetadata" AlpmMetadataType MetadataType = "AlpmMetadata" ApkMetadataType MetadataType = "ApkMetadata" @@ -26,6 +27,7 @@ const ( JavaMetadataType MetadataType = "JavaMetadata" KbPackageMetadataType MetadataType = "KbPackageMetadata" MixLockMetadataType MetadataType = "MixLockMetadataType" + NixStoreMetadataType MetadataType = "NixStoreMetadata" NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata" NpmPackageLockJSONMetadataType MetadataType = "NpmPackageLockJsonMetadata" PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata" @@ -54,6 +56,7 @@ var AllMetadataTypes = []MetadataType{ JavaMetadataType, KbPackageMetadataType, MixLockMetadataType, + NixStoreMetadataType, NpmPackageJSONMetadataType, NpmPackageLockJSONMetadataType, PhpComposerJSONMetadataType, @@ -82,6 +85,7 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{ JavaMetadataType: reflect.TypeOf(JavaMetadata{}), KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}), MixLockMetadataType: reflect.TypeOf(MixLockMetadata{}), + NixStoreMetadataType: reflect.TypeOf(NixStoreMetadata{}), NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}), NpmPackageLockJSONMetadataType: reflect.TypeOf(NpmPackageLockJSONMetadata{}), PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}), diff --git a/syft/pkg/nix_store_metadata.go b/syft/pkg/nix_store_metadata.go new file mode 100644 index 000000000..964447f49 --- /dev/null +++ b/syft/pkg/nix_store_metadata.go @@ -0,0 +1,25 @@ +package pkg + +import ( + "sort" + + "github.com/scylladb/go-set/strset" +) + +type NixStoreMetadata struct { + // OutputHash is the prefix of the nix store basename path + OutputHash string `mapstructure:"outputHash" json:"outputHash"` + + // Output allows for optionally specifying the specific nix package output this package represents (for packages that support multiple outputs). + // Note: the default output for a package is an empty string, so will not be present in the output. + Output string `mapstructure:"output" json:"output,omitempty"` + + // Files is a listing a files that are under the nix/store path for this package + Files []string `mapstructure:"files" json:"files"` +} + +func (m NixStoreMetadata) OwnedFiles() (result []string) { + result = strset.New(m.Files...).List() + sort.Strings(result) + return +} diff --git a/syft/pkg/type.go b/syft/pkg/type.go index a275ed140..b9493553f 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -26,6 +26,7 @@ const ( JavaPkg Type = "java-archive" JenkinsPluginPkg Type = "jenkins-plugin" KbPkg Type = "msrc-kb" + NixPkg Type = "nix" NpmPkg Type = "npm" PhpComposerPkg Type = "php-composer" PortagePkg Type = "portage" @@ -51,6 +52,7 @@ var AllPkgs = []Type{ JavaPkg, JenkinsPluginPkg, KbPkg, + NixPkg, NpmPkg, PhpComposerPkg, PortagePkg, @@ -92,6 +94,8 @@ func (t Type) PackageURLType() string { return packageurl.TypePyPi case PortagePkg: return "portage" + case NixPkg: + return "nix" case NpmPkg: return packageurl.TypeNPM case RpmPkg: @@ -151,6 +155,8 @@ func TypeByName(name string) Type { return PortagePkg case packageurl.TypeHex: return HexPkg + case "nix": + return NixPkg default: return UnknownPkg } diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index 7f2afb33a..76127f5cd 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -83,6 +83,10 @@ func TestTypeFromPURL(t *testing.T) { purl: "pkg:hex/hpax/hpax@0.1.1", expected: HexPkg, }, + { + purl: "pkg:nix/glibc@2.34?hash=h0cnbmfcn93xm5dg2x27ixhag1cwndga", + expected: NixPkg, + }, } var pkgTypes []string diff --git a/test/cli/packages_cmd_test.go b/test/cli/packages_cmd_test.go index 20ec9fa7e..2e3749749 100644 --- a/test/cli/packages_cmd_test.go +++ b/test/cli/packages_cmd_test.go @@ -97,7 +97,7 @@ func TestPackagesCmdFlags(t *testing.T) { name: "squashed-scope-flag", args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage}, assertions: []traitAssertion{ - assertPackageCount(34), + assertPackageCount(35), assertSuccessfulReturnCode, }, }, @@ -214,7 +214,7 @@ func TestPackagesCmdFlags(t *testing.T) { // the application config in the log matches that of what we expect to have been configured. assertInOutput("parallelism: 2"), assertInOutput("parallelism=2"), - assertPackageCount(34), + assertPackageCount(35), assertSuccessfulReturnCode, }, }, @@ -225,7 +225,7 @@ func TestPackagesCmdFlags(t *testing.T) { // the application config in the log matches that of what we expect to have been configured. assertInOutput("parallelism: 1"), assertInOutput("parallelism=1"), - assertPackageCount(34), + assertPackageCount(35), assertSuccessfulReturnCode, }, }, @@ -239,7 +239,7 @@ func TestPackagesCmdFlags(t *testing.T) { assertions: []traitAssertion{ assertNotInOutput("secret_password"), assertNotInOutput("secret_key_path"), - assertPackageCount(34), + assertPackageCount(35), assertSuccessfulReturnCode, }, }, diff --git a/test/integration/catalog_packages_cases_test.go b/test/integration/catalog_packages_cases_test.go index aef3ab23c..cfa02072f 100644 --- a/test/integration/catalog_packages_cases_test.go +++ b/test/integration/catalog_packages_cases_test.go @@ -390,4 +390,11 @@ var commonTestCases = []testCase{ "example-jenkins-plugin": "1.0-SNAPSHOT", }, }, + { + name: "find nix store packages", + pkgType: pkg.NixPkg, + pkgInfo: map[string]string{ + "glibc": "2.34-210", + }, + }, } diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man/glibc.1 b/test/integration/test-fixtures/image-pkg-coverage/pkgs/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man/glibc.1 new file mode 100644 index 000000000..fd4ed835d --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/pkgs/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man/glibc.1 @@ -0,0 +1 @@ +the man pages \ No newline at end of file