diff --git a/internal/constants.go b/internal/constants.go index 40280bcf2b9..7932c185602 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -6,5 +6,5 @@ const ( // JSONSchemaVersion is the current schema version output by the JSON presenter // 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 = "1.0.5" + JSONSchemaVersion = "1.1.0" ) diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestJSONDirsPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestJSONDirsPresenter.golden index 22592df9f47..6f2217d0f59 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestJSONDirsPresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestJSONDirsPresenter.golden @@ -75,7 +75,7 @@ "version": "[not provided]" }, "schema": { - "version": "1.0.5", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.5.json" + "version": "1.1.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestJSONImgsPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestJSONImgsPresenter.golden index 444f0bbead7..2efd1f9c89e 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestJSONImgsPresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestJSONImgsPresenter.golden @@ -9,7 +9,7 @@ "locations": [ { "path": "/somefile-1.txt", - "layerID": "sha256:6c376352c0537f4483e4033e332d7a4ab9433db68c54c297a834d36719aeb6c9" + "layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59" } ], "licenses": [ @@ -40,7 +40,7 @@ "locations": [ { "path": "/somefile-2.txt", - "layerID": "sha256:fc8218a8142ee4952bb8d9b96b3e9838322e9e6eae6477136bcad8fd768949b7" + "layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec" } ], "licenses": [], @@ -67,7 +67,7 @@ "type": "image", "target": { "userInput": "user-image-input", - "imageID": "sha256:1f9cb9dc477f7482856f88ed40c38e260db0526d7a0dad5a0be566bfedde929b", + "imageID": "sha256:5900c94a5bc1e083aa24ad1a223bf6eb9910dc8a6b01cb979ec306cb91709ea1", "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ @@ -77,17 +77,17 @@ "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:6c376352c0537f4483e4033e332d7a4ab9433db68c54c297a834d36719aeb6c9", + "digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59", "size": 22 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:fc8218a8142ee4952bb8d9b96b3e9838322e9e6eae6477136bcad8fd768949b7", + "digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec", "size": 16 } ], - "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNTg2LCJkaWdlc3QiOiJzaGEyNTY6MWY5Y2I5ZGM0NzdmNzQ4Mjg1NmY4OGVkNDBjMzhlMjYwZGIwNTI2ZDdhMGRhZDVhMGJlNTY2YmZlZGRlOTI5YiJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMDQ4LCJkaWdlc3QiOiJzaGEyNTY6NmMzNzYzNTJjMDUzN2Y0NDgzZTQwMzNlMzMyZDdhNGFiOTQzM2RiNjhjNTRjMjk3YTgzNGQzNjcxOWFlYjZjOSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYzgyMThhODE0MmVlNDk1MmJiOGQ5Yjk2YjNlOTgzODMyMmU5ZTZlYWU2NDc3MTM2YmNhZDhmZDc2ODk0OWI3In1dfQ==", - "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpudWxsLCJJbWFnZSI6InNoYTI1NjoyOWQ1YjFjOTkyNjg0MzgwYjQ3NTEyMjliMmNjN2E4MzdkOTBmOWQ1OTJhYmIxZjAyZGYzZGRkMGQ3OWFjMDkxIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNvbnRhaW5lcl9jb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IiIsIkF0dGFjaFN0ZGluIjpmYWxzZSwiQXR0YWNoU3Rkb3V0IjpmYWxzZSwiQXR0YWNoU3RkZXJyIjpmYWxzZSwiVHR5IjpmYWxzZSwiT3BlblN0ZGluIjpmYWxzZSwiU3RkaW5PbmNlIjpmYWxzZSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIl0sIkNtZCI6WyIvYmluL3NoIiwiLWMiLCIjKG5vcCkgQUREIGZpbGU6ZGYzYjc0NGY1NGE5YjE2YjliOWFlZDQwZTNlOThkOWNhMmI0OWY1YTc3ZDlmYThhOTc2OTBkN2JhZjU4ODgyMCBpbiAvc29tZWZpbGUtMi50eHQgIl0sIkltYWdlIjoic2hhMjU2OjI5ZDViMWM5OTI2ODQzODBiNDc1MTIyOWIyY2M3YTgzN2Q5MGY5ZDU5MmFiYjFmMDJkZjNkZGQwZDc5YWMwOTEiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDQtMDFUMTI6NDg6MzIuMjYzNjAzMVoiLCJkb2NrZXJfdmVyc2lvbiI6IjIwLjEwLjIiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0wNC0wMVQxMjo0ODozMi4wODY3MTY2WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTphYzMyZGEyM2Q1MWU4MDFmMDJmOTI0MTIzZWQzMDk5MGViM2YwZmVjMWI5ZWQ0ZjBiMDZjMjRlODhiOWMzNjk1IGluIC9zb21lZmlsZS0xLnR4dCAifSx7ImNyZWF0ZWQiOiIyMDIxLTA0LTAxVDEyOjQ4OjMyLjI2MzYwMzFaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOmRmM2I3NDRmNTRhOWIxNmI5YjlhZWQ0MGUzZTk4ZDljYTJiNDlmNWE3N2Q5ZmE4YTk3NjkwZDdiYWY1ODg4MjAgaW4gL3NvbWVmaWxlLTIudHh0ICJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OjZjMzc2MzUyYzA1MzdmNDQ4M2U0MDMzZTMzMmQ3YTRhYjk0MzNkYjY4YzU0YzI5N2E4MzRkMzY3MTlhZWI2YzkiLCJzaGEyNTY6ZmM4MjE4YTgxNDJlZTQ5NTJiYjhkOWI5NmIzZTk4MzgzMjJlOWU2ZWFlNjQ3NzEzNmJjYWQ4ZmQ3Njg5NDliNyJdfX0=", + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjUsImRpZ2VzdCI6InNoYTI1Njo1OTAwYzk0YTViYzFlMDgzYWEyNGFkMWEyMjNiZjZlYjk5MTBkYzhhNmIwMWNiOTc5ZWMzMDZjYjkxNzA5ZWExIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDQtMDZUMTk6MTM6NTIuNTI0Mzc4WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA0LTA2VDE5OjEzOjUyLjQ1ODIwNjFaIiwiY3JlYXRlZF9ieSI6IkFERCBmaWxlLTEudHh0IC9zb21lZmlsZS0xLnR4dCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifSx7ImNyZWF0ZWQiOiIyMDIxLTA0LTA2VDE5OjEzOjUyLjUyNDM3OFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMi50eHQgL3NvbWVmaWxlLTIudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OmZiNmJlZWNiNzViMzlmNGJiODEzZGJmMTc3ZTUwMWVkZDVkZGIzZTY5YmI0NWNlZGViNzhjNjc2ZWUxYjdhNTkiLCJzaGEyNTY6MzE5YjU4OGNlNjQyNTNhODdiNTMzYzhlZDAxY2YwMDI1ZTBlYWM5OGU3YjUxNmUxMjUzMjk1N2UxMjQ0ZmRlYyJdfX0=", "repoDigests": [], "scope": "Squashed" } @@ -102,7 +102,7 @@ "version": "[not provided]" }, "schema": { - "version": "1.0.5", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.5.json" + "version": "1.1.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } diff --git a/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index b6759cac62b..a5985f95c4f 100644 Binary files a/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden and b/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden b/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden index 29b9416dcf7..25b5635aefa 100644 --- a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden +++ b/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden @@ -205,7 +205,7 @@ } }, "schema": { - "version": "1.0.5", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.5.json" + "version": "1.1.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } diff --git a/schema/json/generate.go b/schema/json/generate.go index dd65c45e6af..fa2c45da1bc 100644 --- a/schema/json/generate.go +++ b/schema/json/generate.go @@ -85,7 +85,7 @@ func build() *jsonschema.Schema { } // set the "anyOf" field for Package.Metadata to be a conjunction of several types - documentSchema.Definitions["Document"].Properties.Set("artifacts.metadata", map[string][]map[string]string{ + documentSchema.Definitions["Package"].Properties.Set("metadata", map[string][]map[string]string{ "anyOf": metadataTypes, }) diff --git a/schema/json/schema-1.1.0.json b/schema/json/schema-1.1.0.json new file mode 100644 index 00000000000..29af14cd44e --- /dev/null +++ b/schema/json/schema-1.1.0.json @@ -0,0 +1,891 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Document", + "definitions": { + "ApkFileRecord": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "checksum": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "ApkMetadata": { + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "license", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "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": { + "type": "string" + }, + "pullChecksum": { + "type": "string" + }, + "gitCommitOfApkPort": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/ApkFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "CargoPackageMetadata": { + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Descriptor": { + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + }, + "Digest": { + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Distribution": { + "required": [ + "name", + "version", + "idLike" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "idLike": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Document": { + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "properties": { + "fileMetadata": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/FileMetadata" + }, + "type": "array" + }, + "secrets": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Secrets" + }, + "type": "array" + }, + "artifacts": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Relationship" + }, + "type": "array" + }, + "source": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Source" + }, + "distro": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Distribution" + }, + "descriptor": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Descriptor" + }, + "schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Schema" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DpkgFileRecord": { + "required": [ + "path", + "isConfigFile" + ], + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Digest" + }, + "isConfigFile": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DpkgMetadata": { + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "properties": { + "package": { + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + }, + "sourceVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "maintainer": { + "type": "string" + }, + "installedSize": { + "type": "integer" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/DpkgFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "FileMetadata": { + "required": [ + "location", + "metadata" + ], + "properties": { + "location": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Location" + }, + "metadata": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/FileMetadataEntry" + } + }, + "additionalProperties": true, + "type": "object" + }, + "FileMetadataEntry": { + "required": [ + "mode", + "type", + "userID", + "groupID" + ], + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "digests": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Digest" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "GemMetadata": { + "required": [ + "name", + "version" + ], + "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" + } + }, + "additionalProperties": true, + "type": "object" + }, + "JavaManifest": { + "properties": { + "main": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "namedSections": { + "patternProperties": { + ".*": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "JavaMetadata": { + "required": [ + "virtualPath" + ], + "properties": { + "virtualPath": { + "type": "string" + }, + "manifest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/JavaManifest" + }, + "pomProperties": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomProperties" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Location": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "NpmPackageJSONMetadata": { + "required": [ + "author", + "licenses", + "homepage", + "description", + "url" + ], + "properties": { + "files": { + "items": { + "type": "string" + }, + "type": "array" + }, + "author": { + "type": "string" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "homepage": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Package": { + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl", + "metadataType", + "metadata" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/definitions/Location" + }, + "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": "#/definitions/ApkMetadata" + }, + { + "$ref": "#/definitions/CargoPackageMetadata" + }, + { + "$ref": "#/definitions/DpkgMetadata" + }, + { + "$ref": "#/definitions/GemMetadata" + }, + { + "$ref": "#/definitions/JavaMetadata" + }, + { + "$ref": "#/definitions/NpmPackageJSONMetadata" + }, + { + "$ref": "#/definitions/PythonPackageMetadata" + }, + { + "$ref": "#/definitions/RpmdbMetadata" + } + ] + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomProperties": { + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version", + "extraFields" + ], + "properties": { + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonFileDigest": { + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonFileRecord": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonFileDigest" + }, + "size": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PythonPackageMetadata": { + "required": [ + "name", + "version", + "license", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "author": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonFileRecord" + }, + "type": "array" + }, + "sitePackagesRootPath": { + "type": "string" + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Relationship": { + "required": [ + "parent", + "child", + "type", + "metadata" + ], + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + }, + "RpmdbFileRecord": { + "required": [ + "path", + "mode", + "size", + "sha256" + ], + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "sha256": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "RpmdbMetadata": { + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "license", + "vendor", + "files" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "epoch": { + "type": "integer" + }, + "architecture": { + "type": "string" + }, + "release": { + "type": "string" + }, + "sourceRpm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "license": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/RpmdbFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Schema": { + "required": [ + "version", + "url" + ], + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "SearchResult": { + "required": [ + "classification", + "lineNumber", + "lineOffset", + "seekPosition", + "length" + ], + "properties": { + "classification": { + "type": "string" + }, + "lineNumber": { + "type": "integer" + }, + "lineOffset": { + "type": "integer" + }, + "seekPosition": { + "type": "integer" + }, + "length": { + "type": "integer" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Secrets": { + "required": [ + "location", + "secrets" + ], + "properties": { + "location": { + "$ref": "#/definitions/Location" + }, + "secrets": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/SearchResult" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Source": { + "required": [ + "type", + "target" + ], + "properties": { + "type": { + "type": "string" + }, + "target": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + } + } +} diff --git a/syft/pkg/cataloger/deb/cataloger.go b/syft/pkg/cataloger/deb/cataloger.go index 34c3df3ac23..905d08727df 100644 --- a/syft/pkg/cataloger/deb/cataloger.go +++ b/syft/pkg/cataloger/deb/cataloger.go @@ -8,14 +8,17 @@ import ( "io" "path" "path/filepath" + "sort" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) const ( - md5sumsExt = ".md5sums" - docsPath = "/usr/share/doc" + md5sumsExt = ".md5sums" + conffilesExt = ".conffiles" + docsPath = "/usr/share/doc" ) type Cataloger struct{} @@ -56,93 +59,166 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error) p.FoundBy = c.Name() p.Locations = []source.Location{dbLocation} - metadata := p.Metadata.(pkg.DpkgMetadata) + // the current entry only has what may have been listed in the status file, however, there are additional + // files that are listed in multiple other locations. We should retrieve them all and merge the file lists + // together. + mergeFileListing(resolver, dbLocation, p) - md5Reader, md5Location, err := fetchMd5Contents(resolver, dbLocation, p) - if err != nil { - return nil, fmt.Errorf("unable to find dpkg md5 contents: %w", err) - } + // fetch additional data from the copyright file to derive the license information + addLicenses(resolver, dbLocation, p) + } - if md5Reader != nil { - // attach the file list - metadata.Files = parseDpkgMD5Info(md5Reader) - - // keep a record of the file where this was discovered - if md5Location != nil { - p.Locations = append(p.Locations, *md5Location) - } - } else { - // ensure the file list is an empty collection (not nil) - metadata.Files = make([]pkg.DpkgFileRecord, 0) - } + results = append(results, pkgs...) + } + return results, nil +} - // persist alterations - p.Metadata = metadata +func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) { + // get license information from the copyright file + copyrightReader, copyrightLocation := fetchCopyrightContents(resolver, dbLocation, p) - // get license information from the copyright file - copyrightReader, copyrightLocation, err := fetchCopyrightContents(resolver, dbLocation, p) - if err != nil { - return nil, fmt.Errorf("unable to find dpkg copyright contents: %w", err) - } + if copyrightReader != nil { + // attach the licenses + p.Licenses = parseLicensesFromCopyright(copyrightReader) + + // keep a record of the file where this was discovered + if copyrightLocation != nil { + p.Locations = append(p.Locations, *copyrightLocation) + } + } +} - if copyrightReader != nil { - // attach the licenses - p.Licenses = parseLicensesFromCopyright(copyrightReader) +func mergeFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) { + metadata := p.Metadata.(pkg.DpkgMetadata) - // keep a record of the file where this was discovered - if copyrightLocation != nil { - p.Locations = append(p.Locations, *copyrightLocation) - } + // get file listing (package files + additional config files) + files, infoLocations := getAdditionalFileListing(resolver, dbLocation, p) +loopNewFiles: + for _, newFile := range files { + for _, existingFile := range metadata.Files { + if existingFile.Path == newFile.Path { + // skip adding this file since it already exists + continue loopNewFiles } } + metadata.Files = append(metadata.Files, newFile) + } - results = append(results, pkgs...) + // sort files by path + sort.SliceStable(metadata.Files, func(i, j int) bool { + return metadata.Files[i].Path < metadata.Files[j].Path + }) + + // persist alterations + p.Metadata = metadata + + // persist location information from each new source of information + p.Locations = append(p.Locations, infoLocations...) +} + +func getAdditionalFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) ([]pkg.DpkgFileRecord, []source.Location) { + // ensure the default value for a collection is never nil since this may be shown as JSON + var files = make([]pkg.DpkgFileRecord, 0) + var locations []source.Location + + md5Reader, md5Location := fetchMd5Contents(resolver, dbLocation, p) + + if md5Reader != nil { + // attach the file list + files = append(files, parseDpkgMD5Info(md5Reader)...) + + // keep a record of the file where this was discovered + if md5Location != nil { + locations = append(locations, *md5Location) + } } - return results, nil + + conffilesReader, conffilesLocation := fetchConffileContents(resolver, dbLocation, p) + + if conffilesReader != nil { + // attach the file list + files = append(files, parseDpkgConffileInfo(md5Reader)...) + + // keep a record of the file where this was discovered + if conffilesLocation != nil { + locations = append(locations, *conffilesLocation) + } + } + + return files, locations } -func fetchMd5Contents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.Reader, *source.Location, error) { +func fetchMd5Contents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) { + var md5Reader io.ReadCloser + var err error + parentPath := filepath.Dir(dbLocation.RealPath) // look for /var/lib/dpkg/info/NAME:ARCH.md5sums name := md5Key(p) - md5SumLocation := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+md5sumsExt)) + location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+md5sumsExt)) - if md5SumLocation == nil { + if location == nil { // the most specific key did not work, fallback to just the name // look for /var/lib/dpkg/info/NAME.md5sums - md5SumLocation = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+md5sumsExt)) + location = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+md5sumsExt)) } // this is unexpected, but not a show-stopper - if md5SumLocation == nil { - return nil, nil, nil + if location != nil { + md5Reader, err = resolver.FileContentsByLocation(*location) + if err != nil { + log.Warnf("failed to fetch deb md5 contents (package=%s): %+v", p.Name, err) + } } - reader, err := resolver.FileContentsByLocation(*md5SumLocation) - if err != nil { - return nil, nil, fmt.Errorf("failed to fetch deb md5 contents (%+v): %w", p, err) + return md5Reader, location +} + +func fetchConffileContents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) { + var reader io.ReadCloser + var err error + + parentPath := filepath.Dir(dbLocation.RealPath) + + // look for /var/lib/dpkg/info/NAME:ARCH.conffiles + name := md5Key(p) + location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+conffilesExt)) + + if location == nil { + // the most specific key did not work, fallback to just the name + // look for /var/lib/dpkg/info/NAME.conffiles + location = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+conffilesExt)) } - return reader, md5SumLocation, nil + + // this is unexpected, but not a show-stopper + if location != nil { + reader, err = resolver.FileContentsByLocation(*location) + if err != nil { + log.Warnf("failed to fetch deb conffiles contents (package=%s): %+v", p.Name, err) + } + } + + return reader, location } -func fetchCopyrightContents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.Reader, *source.Location, error) { +func fetchCopyrightContents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) { // look for /usr/share/docs/NAME/copyright files name := p.Name copyrightPath := path.Join(docsPath, name, "copyright") - copyrightLocation := resolver.RelativeFileByPath(dbLocation, copyrightPath) + location := resolver.RelativeFileByPath(dbLocation, copyrightPath) // we may not have a copyright file for each package, ignore missing files - if copyrightLocation == nil { - return nil, nil, nil + if location == nil { + return nil, nil } - reader, err := resolver.FileContentsByLocation(*copyrightLocation) + reader, err := resolver.FileContentsByLocation(*location) if err != nil { - return nil, nil, fmt.Errorf("failed to fetch deb copyright contents (%+v): %w", p, err) + log.Warnf("failed to fetch deb copyright contents (package=%s): %w", p.Name, err) } - return reader, copyrightLocation, nil + return reader, location } func md5Key(p *pkg.Package) string { diff --git a/syft/pkg/cataloger/deb/cataloger_test.go b/syft/pkg/cataloger/deb/cataloger_test.go index b856a0cb964..84bef7c11fb 100644 --- a/syft/pkg/cataloger/deb/cataloger_test.go +++ b/syft/pkg/cataloger/deb/cataloger_test.go @@ -3,6 +3,8 @@ package deb import ( "testing" + "github.com/anchore/syft/syft/file" + "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" @@ -19,7 +21,12 @@ func TestDpkgCataloger(t *testing.T) { { name: "go-case", sources: map[string][]string{ - "libpam-runtime": {"/var/lib/dpkg/status", "/var/lib/dpkg/info/libpam-runtime.md5sums", "/usr/share/doc/libpam-runtime/copyright"}, + "libpam-runtime": { + "/var/lib/dpkg/status", + "/var/lib/dpkg/info/libpam-runtime.md5sums", + "/var/lib/dpkg/info/libpam-runtime.conffiles", + "/usr/share/doc/libpam-runtime/copyright", + }, }, expected: []pkg.Package{ { @@ -37,10 +44,38 @@ func TestDpkgCataloger(t *testing.T) { Maintainer: "Steve Langasek ", InstalledSize: 1016, Files: []pkg.DpkgFileRecord{ - {Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", MD5: "55f905631797551d4d936a34c7e73474"}, - {Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", MD5: "cede84bda30d2380217f97753c8ccf3a"}, - {Path: "/usr/share/doc/zlib1g/changelog.gz", MD5: "f3c9dafa6da7992c47328b4464f6d122"}, - {Path: "/usr/share/doc/zlib1g/copyright", MD5: "a4fae96070439a5209a62ae5b8017ab2"}, + { + Path: "/etc/pam.conf", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "87fc76f18e98ee7d3848f6b81b3391e5", + }, + IsConfigFile: true, + }, + { + Path: "/etc/pam.d/other", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "31aa7f2181889ffb00b87df4126d1701", + }, + IsConfigFile: true, + }, + {Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", Digest: &file.Digest{ + Algorithm: "md5", + Value: "55f905631797551d4d936a34c7e73474", + }}, + {Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", Digest: &file.Digest{ + Algorithm: "md5", + Value: "cede84bda30d2380217f97753c8ccf3a", + }}, + {Path: "/usr/share/doc/zlib1g/changelog.gz", Digest: &file.Digest{ + Algorithm: "md5", + Value: "f3c9dafa6da7992c47328b4464f6d122", + }}, + {Path: "/usr/share/doc/zlib1g/copyright", Digest: &file.Digest{ + Algorithm: "md5", + Value: "a4fae96070439a5209a62ae5b8017ab2", + }}, }, }, }, diff --git a/syft/pkg/cataloger/deb/parse_dpkg_info_files.go b/syft/pkg/cataloger/deb/parse_dpkg_info_files.go index 0561732c925..105c47da956 100644 --- a/syft/pkg/cataloger/deb/parse_dpkg_info_files.go +++ b/syft/pkg/cataloger/deb/parse_dpkg_info_files.go @@ -5,12 +5,11 @@ import ( "io" "strings" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" ) -func parseDpkgMD5Info(reader io.Reader) []pkg.DpkgFileRecord { - // we must preallocate to ensure the resulting struct does not have null - var findings = make([]pkg.DpkgFileRecord, 0) +func parseDpkgMD5Info(reader io.Reader) (findings []pkg.DpkgFileRecord) { scanner := bufio.NewScanner(reader) for scanner.Scan() { @@ -23,9 +22,53 @@ func parseDpkgMD5Info(reader io.Reader) []pkg.DpkgFileRecord { } findings = append(findings, pkg.DpkgFileRecord{ Path: path, - MD5: strings.TrimSpace(fields[0]), + Digest: &file.Digest{ + Algorithm: "md5", + Value: strings.TrimSpace(fields[0]), + }, }) } } return findings } + +func parseDpkgConffileInfo(reader io.Reader) (findings []pkg.DpkgFileRecord) { + scanner := bufio.NewScanner(reader) + + for scanner.Scan() { + line := strings.Trim(scanner.Text(), " \n") + fields := strings.SplitN(line, " ", 2) + + if line == "" { + continue + } + + var path string + if len(fields) >= 1 { + path = strings.TrimSpace(fields[0]) + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + } + + var digest *file.Digest + if len(fields) >= 2 { + digest = &file.Digest{ + Algorithm: "md5", + Value: strings.TrimSpace(fields[1]), + } + } + + if path != "" { + record := pkg.DpkgFileRecord{ + Path: path, + IsConfigFile: true, + } + if digest != nil { + record.Digest = digest + } + findings = append(findings, record) + } + } + return findings +} diff --git a/syft/pkg/cataloger/deb/parse_dpkg_info_files_test.go b/syft/pkg/cataloger/deb/parse_dpkg_info_files_test.go index df252ddc456..c345c714eab 100644 --- a/syft/pkg/cataloger/deb/parse_dpkg_info_files_test.go +++ b/syft/pkg/cataloger/deb/parse_dpkg_info_files_test.go @@ -4,6 +4,8 @@ import ( "os" "testing" + "github.com/anchore/syft/syft/file" + "github.com/go-test/deep" "github.com/anchore/syft/syft/pkg" @@ -17,10 +19,22 @@ func TestMD5SumInfoParsing(t *testing.T) { { fixture: "test-fixtures/info/zlib1g.md5sums", expected: []pkg.DpkgFileRecord{ - {Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", MD5: "55f905631797551d4d936a34c7e73474"}, - {Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", MD5: "cede84bda30d2380217f97753c8ccf3a"}, - {Path: "/usr/share/doc/zlib1g/changelog.gz", MD5: "f3c9dafa6da7992c47328b4464f6d122"}, - {Path: "/usr/share/doc/zlib1g/copyright", MD5: "a4fae96070439a5209a62ae5b8017ab2"}, + {Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", Digest: &file.Digest{ + Algorithm: "md5", + Value: "55f905631797551d4d936a34c7e73474", + }}, + {Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", Digest: &file.Digest{ + Algorithm: "md5", + Value: "cede84bda30d2380217f97753c8ccf3a", + }}, + {Path: "/usr/share/doc/zlib1g/changelog.gz", Digest: &file.Digest{ + Algorithm: "md5", + Value: "f3c9dafa6da7992c47328b4464f6d122", + }}, + {Path: "/usr/share/doc/zlib1g/copyright", Digest: &file.Digest{ + Algorithm: "md5", + Value: "a4fae96070439a5209a62ae5b8017ab2", + }}, }, }, } @@ -55,3 +69,52 @@ func TestMD5SumInfoParsing(t *testing.T) { }) } } + +func TestConffileInfoParsing(t *testing.T) { + tests := []struct { + fixture string + expected []pkg.DpkgFileRecord + }{ + { + fixture: "test-fixtures/info/util-linux.conffiles", + expected: []pkg.DpkgFileRecord{ + {Path: "/etc/default/hwclock", IsConfigFile: true}, + {Path: "/etc/init.d/hwclock.sh", IsConfigFile: true}, + {Path: "/etc/pam.d/runuser", IsConfigFile: true}, + {Path: "/etc/pam.d/runuser-l", IsConfigFile: true}, + {Path: "/etc/pam.d/su", IsConfigFile: true}, + {Path: "/etc/pam.d/su-l", IsConfigFile: true}, + }, + }, + } + + for _, test := range tests { + t.Run(test.fixture, func(t *testing.T) { + file, err := os.Open(test.fixture) + if err != nil { + t.Fatal("Unable to read: ", err) + } + defer func() { + err := file.Close() + if err != nil { + t.Fatal("closing file failed:", err) + } + }() + + actual := parseDpkgConffileInfo(file) + + if len(actual) != len(test.expected) { + for _, a := range actual { + t.Logf(" %+v", a) + } + t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected)) + } + + diffs := deep.Equal(actual, test.expected) + for _, d := range diffs { + t.Errorf("diff: %+v", d) + } + + }) + } +} diff --git a/syft/pkg/cataloger/deb/parse_dpkg_status.go b/syft/pkg/cataloger/deb/parse_dpkg_status.go index 2c14ac14ada..8992c18361e 100644 --- a/syft/pkg/cataloger/deb/parse_dpkg_status.go +++ b/syft/pkg/cataloger/deb/parse_dpkg_status.go @@ -15,7 +15,10 @@ import ( "github.com/mitchellh/mapstructure" ) -var errEndOfPackages = fmt.Errorf("no more packages to read") +var ( + errEndOfPackages = fmt.Errorf("no more packages to read") + sourceRegexp = regexp.MustCompile(`(?P\S+)( \((?P.*)\))?`) +) // parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed. func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) { @@ -48,20 +51,52 @@ func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) { } // parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader. -// nolint:funlen -func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) { - dpkgFields := make(map[string]interface{}) +func parseDpkgStatusEntry(reader *bufio.Reader) (pkg.DpkgMetadata, error) { var retErr error + dpkgFields, err := extractAllFields(reader) + if err != nil { + if !errors.Is(err, errEndOfPackages) { + return pkg.DpkgMetadata{}, err + } + retErr = err + } + + entry := pkg.DpkgMetadata{ + // ensure the default value for a collection is never nil since this may be shown as JSON + Files: make([]pkg.DpkgFileRecord, 0), + } + err = mapstructure.Decode(dpkgFields, &entry) + if err != nil { + return pkg.DpkgMetadata{}, err + } + + name, version := extractSourceVersion(entry.Source) + if version != "" { + entry.SourceVersion = version + entry.Source = name + } + + // there may be an optional conffiles section that we should persist as files + if conffilesSection, exists := dpkgFields["Conffiles"]; exists && conffilesSection != nil { + if sectionStr, ok := conffilesSection.(string); ok { + entry.Files = parseDpkgConffileInfo(strings.NewReader(sectionStr)) + } + } + + return entry, retErr +} + +func extractAllFields(reader *bufio.Reader) (map[string]interface{}, error) { + dpkgFields := make(map[string]interface{}) var key string for { line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { - retErr = errEndOfPackages - break + return dpkgFields, errEndOfPackages } - return pkg.DpkgMetadata{}, err + return nil, err } line = strings.TrimRight(line, "\n") @@ -79,12 +114,12 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err err case strings.HasPrefix(line, " "): // a field-body continuation if len(key) == 0 { - return pkg.DpkgMetadata{}, fmt.Errorf("no match for continuation: line: '%s'", line) + return nil, fmt.Errorf("no match for continuation: line: '%s'", line) } val, ok := dpkgFields[key] if !ok { - return pkg.DpkgMetadata{}, fmt.Errorf("no previous key exists, expecting: %s", key) + return nil, fmt.Errorf("no previous key exists, expecting: %s", key) } // concatenate onto previous value val = fmt.Sprintf("%s\n %s", val, strings.TrimSpace(line)) @@ -94,36 +129,18 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err err var val interface{} key, val, err = handleNewKeyValue(line) if err != nil { - return pkg.DpkgMetadata{}, err + return nil, err } if _, ok := dpkgFields[key]; ok { - return pkg.DpkgMetadata{}, fmt.Errorf("duplicate key discovered: %s", key) + return nil, fmt.Errorf("duplicate key discovered: %s", key) } dpkgFields[key] = val } } - - err = mapstructure.Decode(dpkgFields, &entry) - if err != nil { - return pkg.DpkgMetadata{}, err - } - - name, version := extractSourceVersion(entry.Source) - if version != "" { - entry.SourceVersion = version - entry.Source = name - } - - return entry, retErr + return dpkgFields, nil } -// match examples: -// "a-thing (1.2.3)" name="a-thing" version="1.2.3" -// "a-thing" name="a-thing" version="" -// "" name="" version="" -var sourceRegexp = regexp.MustCompile(`(?P\S+)( \((?P.*)\))?`) - // If the source entry string is of the form " ()" then parse and return the components, if // of the "" form, then return name and nil func extractSourceVersion(source string) (string, string) { diff --git a/syft/pkg/cataloger/deb/parse_dpkg_status_test.go b/syft/pkg/cataloger/deb/parse_dpkg_status_test.go index 0c4b0720204..f61b23058d6 100644 --- a/syft/pkg/cataloger/deb/parse_dpkg_status_test.go +++ b/syft/pkg/cataloger/deb/parse_dpkg_status_test.go @@ -5,6 +5,8 @@ import ( "os" "testing" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" "github.com/go-test/deep" ) @@ -30,6 +32,40 @@ func TestSinglePackage(t *testing.T) { Architecture: "amd64", InstalledSize: 4064, Maintainer: "APT Development Team ", + Files: []pkg.DpkgFileRecord{ + { + Path: "/etc/apt/apt.conf.d/01autoremove", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "76120d358bc9037bb6358e737b3050b5", + }, + IsConfigFile: true, + }, + { + Path: "/etc/cron.daily/apt-compat", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "49e9b2cfa17849700d4db735d04244f3", + }, + IsConfigFile: true, + }, + { + Path: "/etc/kernel/postinst.d/apt-auto-removal", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "4ad976a68f045517cf4696cec7b8aa3a", + }, + IsConfigFile: true, + }, + { + Path: "/etc/logrotate.d/apt", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "179f2ed4f85cbaca12fa3d69c2a4a1c3", + }, + IsConfigFile: true, + }, + }, }, }, } @@ -74,6 +110,7 @@ func TestMultiplePackages(t *testing.T) { Architecture: "all", InstalledSize: 3036, Maintainer: "GNU Libc Maintainers ", + Files: []pkg.DpkgFileRecord{}, }, { Package: "util-linux", @@ -81,6 +118,56 @@ func TestMultiplePackages(t *testing.T) { Architecture: "amd64", InstalledSize: 4327, Maintainer: "LaMont Jones ", + Files: []pkg.DpkgFileRecord{ + { + Path: "/etc/default/hwclock", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "3916544450533eca69131f894db0ca12", + }, + IsConfigFile: true, + }, + { + Path: "/etc/init.d/hwclock.sh", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "1ca5c0743fa797ffa364db95bb8d8d8e", + }, + IsConfigFile: true, + }, + { + Path: "/etc/pam.d/runuser", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "b8b44b045259525e0fae9e38fdb2aeeb", + }, + IsConfigFile: true, + }, + { + Path: "/etc/pam.d/runuser-l", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "2106ea05877e8913f34b2c77fa02be45", + }, + IsConfigFile: true, + }, + { + Path: "/etc/pam.d/su", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "ce6dcfda3b190a27a455bb38a45ff34a", + }, + IsConfigFile: true, + }, + { + Path: "/etc/pam.d/su-l", + Digest: &file.Digest{ + Algorithm: "md5", + Value: "756fef5687fecc0d986e5951427b0c4f", + }, + IsConfigFile: true, + }, + }, }, }, }, diff --git a/syft/pkg/cataloger/deb/test-fixtures/image-dpkg/var/lib/dpkg/info/libpam-runtime.conffiles b/syft/pkg/cataloger/deb/test-fixtures/image-dpkg/var/lib/dpkg/info/libpam-runtime.conffiles new file mode 100644 index 00000000000..1fe9bc1cd97 --- /dev/null +++ b/syft/pkg/cataloger/deb/test-fixtures/image-dpkg/var/lib/dpkg/info/libpam-runtime.conffiles @@ -0,0 +1,2 @@ +/etc/pam.conf +/etc/pam.d/other \ No newline at end of file diff --git a/syft/pkg/cataloger/deb/test-fixtures/info/util-linux.conffiles b/syft/pkg/cataloger/deb/test-fixtures/info/util-linux.conffiles new file mode 100644 index 00000000000..173ae853c60 --- /dev/null +++ b/syft/pkg/cataloger/deb/test-fixtures/info/util-linux.conffiles @@ -0,0 +1,6 @@ +/etc/default/hwclock +/etc/init.d/hwclock.sh +/etc/pam.d/runuser +/etc/pam.d/runuser-l +/etc/pam.d/su +/etc/pam.d/su-l \ No newline at end of file diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index b50ac49aa60..8f2245c5513 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -3,6 +3,8 @@ package pkg import ( "sort" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/distro" "github.com/package-url/packageurl-go" "github.com/scylladb/go-set/strset" @@ -27,8 +29,9 @@ type DpkgMetadata struct { // DpkgFileRecord represents a single file attributed to a debian package. type DpkgFileRecord struct { - Path string `json:"path"` - MD5 string `json:"md5"` + Path string `json:"path"` + Digest *file.Digest `json:"digest,omitempty"` + IsConfigFile bool `json:"isConfigFile"` } // PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)