From e72d68b0c65febf77798988a3f03dfbee80c6088 Mon Sep 17 00:00:00 2001 From: Morten Linderud Date: Mon, 13 Jun 2022 20:51:37 +0200 Subject: [PATCH] Add pacman (alpm) parser support (#943) Co-authored-by: Christopher Phillips --- go.mod | 1 + go.sum | 3 + internal/constants.go | 2 +- .../formats/common/spdxhelpers/source_info.go | 2 + .../common/spdxhelpers/source_info_test.go | 8 + .../formats/spdx22tagvalue/encoder_test.go | 2 +- .../snapshot/TestSPDXJSONSPDXIDs.golden | 4 +- .../formats/syftjson/model/linux_release.go | 4 + internal/formats/syftjson/model/package.go | 13 +- .../snapshot/TestDirectoryEncoder.golden | 4 +- .../TestEncodeFullJSONDocument.golden | 4 +- .../snapshot/TestImageEncoder.golden | 18 +- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes internal/formats/syftjson/to_format_model.go | 4 + internal/formats/syftjson/to_syft_model.go | 4 + schema/json/generate.go | 1 + schema/json/schema-3.3.0.json | 1417 +++++++++++++++++ syft/linux/identify_release.go | 4 + syft/linux/identify_release_test.go | 3 + syft/linux/release.go | 9 +- syft/pkg/alpm_metadata.go | 80 + syft/pkg/alpm_metadata_test.go | 112 ++ syft/pkg/apk_metadata_test.go | 3 +- syft/pkg/cataloger/alpm/cataloger.go | 48 + syft/pkg/cataloger/alpm/parse_alpm_db.go | 245 +++ syft/pkg/cataloger/alpm/parse_alpm_db_test.go | 195 +++ syft/pkg/cataloger/alpm/test-fixtures/files | 20 + syft/pkg/cataloger/alpm/test-fixtures/mtree | Bin 0 -> 364 bytes syft/pkg/cataloger/cataloger.go | 4 + syft/pkg/metadata.go | 3 + syft/pkg/type.go | 6 + syft/pkg/type_test.go | 4 + syft/pkg/url.go | 25 +- syft/pkg/url_test.go | 18 + test/cli/packages_cmd_test.go | 2 +- .../catalog_packages_cases_test.go | 7 + test/integration/catalog_packages_test.go | 2 - test/integration/convert_test.go | 22 +- .../pkgs/var/lib/pacman/local/ALPM_DB_VERSION | 1 + .../var/lib/pacman/local/pacman-6.0.1-5/desc | 57 + .../var/lib/pacman/local/pacman-6.0.1-5/files | 30 + .../var/lib/pacman/local/pacman-6.0.1-5/mtree | Bin 0 -> 20349 bytes 42 files changed, 2348 insertions(+), 43 deletions(-) create mode 100644 schema/json/schema-3.3.0.json create mode 100644 syft/pkg/alpm_metadata.go create mode 100644 syft/pkg/alpm_metadata_test.go create mode 100644 syft/pkg/cataloger/alpm/cataloger.go create mode 100644 syft/pkg/cataloger/alpm/parse_alpm_db.go create mode 100644 syft/pkg/cataloger/alpm/parse_alpm_db_test.go create mode 100644 syft/pkg/cataloger/alpm/test-fixtures/files create mode 100644 syft/pkg/cataloger/alpm/test-fixtures/mtree create mode 100644 test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/ALPM_DB_VERSION create mode 100644 test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/desc create mode 100644 test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/files create mode 100644 test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/mtree diff --git a/go.mod b/go.mod index e17ab009c..43b1bf54a 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/sigstore/cosign v1.9.0 github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5 + github.com/vbatts/go-mtree v0.5.0 ) require ( diff --git a/go.sum b/go.sum index 7597dd7c1..cc842e035 100644 --- a/go.sum +++ b/go.sum @@ -1871,6 +1871,7 @@ github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5/go.mod h1:OvpZ github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -2040,6 +2041,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vbatts/go-mtree v0.5.0 h1:dM+5XZdqH0j9CSZeerhoN/tAySdwnmevaZHO1XGW2Vc= +github.com/vbatts/go-mtree v0.5.0/go.mod h1:7JbaNHyBMng+RP8C3Q4E+4Ca8JnGQA2R/MB+jb4tSOk= github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= diff --git a/internal/constants.go b/internal/constants.go index 776637fe9..fbf2cc9b1 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 = "3.2.4" + JSONSchemaVersion = "3.3.0" ) diff --git a/internal/formats/common/spdxhelpers/source_info.go b/internal/formats/common/spdxhelpers/source_info.go index 7099ed7dd..7f4a4a33b 100644 --- a/internal/formats/common/spdxhelpers/source_info.go +++ b/internal/formats/common/spdxhelpers/source_info.go @@ -9,6 +9,8 @@ import ( func SourceInfo(p pkg.Package) string { answer := "" switch p.Type { + case pkg.AlpmPkg: + answer = "acquired package info from ALPM DB" case pkg.RpmPkg: answer = "acquired package info from RPM DB" case pkg.ApkPkg: diff --git a/internal/formats/common/spdxhelpers/source_info_test.go b/internal/formats/common/spdxhelpers/source_info_test.go index 591c15db0..4eb652cd0 100644 --- a/internal/formats/common/spdxhelpers/source_info_test.go +++ b/internal/formats/common/spdxhelpers/source_info_test.go @@ -142,6 +142,14 @@ func Test_SourceInfo(t *testing.T) { "from dotnet project assets file", }, }, + { + input: pkg.Package{ + Type: pkg.AlpmPkg, + }, + expected: []string{ + "from ALPM DB", + }, + }, } var pkgTypes []pkg.Type for _, test := range tests { diff --git a/internal/formats/spdx22tagvalue/encoder_test.go b/internal/formats/spdx22tagvalue/encoder_test.go index 8d0afa7fd..83992655d 100644 --- a/internal/formats/spdx22tagvalue/encoder_test.go +++ b/internal/formats/spdx22tagvalue/encoder_test.go @@ -61,7 +61,7 @@ func TestSPDXJSONSPDXIDs(t *testing.T) { }, }, }, - true, + *updateSpdxTagValue, spdxTagValueRedactor, ) } diff --git a/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden b/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden index ad82041e1..e9e540f62 100644 --- a/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden +++ b/internal/formats/spdx22tagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden @@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.2 DataLicense: CC0-1.0 SPDXID: SPDXRef-DOCUMENT DocumentName: . -DocumentNamespace: https://anchore.com/syft/dir/422d92b9-57e8-44ee-8039-f75c1d19be87 +DocumentNamespace: https://anchore.com/syft/dir/bdb67358-651c-4dd8-b5ee-5318936eb16a LicenseListVersion: 3.17 Creator: Organization: Anchore, Inc Creator: Tool: syft-v0.42.0-bogus -Created: 2022-05-24T22:52:02Z +Created: 2022-06-07T19:33:39Z ##### Package: @at-sign diff --git a/internal/formats/syftjson/model/linux_release.go b/internal/formats/syftjson/model/linux_release.go index d8349600e..3aa79a3d7 100644 --- a/internal/formats/syftjson/model/linux_release.go +++ b/internal/formats/syftjson/model/linux_release.go @@ -13,6 +13,10 @@ type LinuxRelease struct { IDLike IDLikes `json:"idLike,omitempty"` Version string `json:"version,omitempty"` VersionID string `json:"versionID,omitempty"` + VersionCodename string `json:"versionCodename,omitempty"` + BuildID string `json:"buildID,omitempty"` + ImageID string `json:"imageID,omitempty"` + ImageVersion string `json:"imageVersion,omitempty"` Variant string `json:"variant,omitempty"` VariantID string `json:"variantID,omitempty"` HomeURL string `json:"homeURL,omitempty"` diff --git a/internal/formats/syftjson/model/package.go b/internal/formats/syftjson/model/package.go index 101df0686..b7bec3ed0 100644 --- a/internal/formats/syftjson/model/package.go +++ b/internal/formats/syftjson/model/package.go @@ -47,7 +47,6 @@ func (p *packageMetadataUnpacker) String() string { } // UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types. -// nolint:funlen func (p *Package) UnmarshalJSON(b []byte) error { var basic PackageBasicData if err := json.Unmarshal(b, &basic); err != nil { @@ -61,9 +60,19 @@ func (p *Package) UnmarshalJSON(b []byte) error { return err } - p.MetadataType = unpacker.MetadataType + return unpackMetadata(p, unpacker) +} +// nolint:funlen +func unpackMetadata(p *Package, unpacker packageMetadataUnpacker) error { + p.MetadataType = unpacker.MetadataType switch p.MetadataType { + case pkg.AlpmMetadataType: + var payload pkg.AlpmMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload case pkg.ApkMetadataType: var payload pkg.ApkMetadata if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index 74e56c2da..cd5ba604f 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -88,7 +88,7 @@ } }, "schema": { - "version": "3.2.4", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.4.json" + "version": "3.3.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.0.json" } } diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index deb9f529c..7c19970eb 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -184,7 +184,7 @@ } }, "schema": { - "version": "3.2.4", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.4.json" + "version": "3.3.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.0.json" } } diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index 6bb76c412..015a80a01 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -9,7 +9,7 @@ "locations": [ { "path": "/somefile-1.txt", - "layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59" + "layerID": "sha256:7ef28e9c2d56471ee090b578a678bdf28c3b5a311ca7b2e28c2a4185e5bb34c0" } ], "licenses": [ @@ -40,7 +40,7 @@ "locations": [ { "path": "/somefile-2.txt", - "layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec" + "layerID": "sha256:86da8aee621161bea2efaf27a2709ddab5e7d44e30ecdfda728b02c03a28fd98" } ], "licenses": [], @@ -67,7 +67,7 @@ "type": "image", "target": { "userInput": "user-image-input", - "imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca", + "imageID": "sha256:5dd5f5f4247e4e946f555f0de7681a631a5240b614e52717d0aed04808e8c65f", "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:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59", + "digest": "sha256:7ef28e9c2d56471ee090b578a678bdf28c3b5a311ca7b2e28c2a4185e5bb34c0", "size": 22 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec", + "digest": "sha256:86da8aee621161bea2efaf27a2709ddab5e7d44e30ecdfda728b02c03a28fd98", "size": 16 } ], - "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19", - "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==", + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1Njo1ZGQ1ZjVmNDI0N2U0ZTk0NmY1NTVmMGRlNzY4MWE2MzFhNTI0MGI2MTRlNTI3MTdkMGFlZDA0ODA4ZThjNjVmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo3ZWYyOGU5YzJkNTY0NzFlZTA5MGI1NzhhNjc4YmRmMjhjM2I1YTMxMWNhN2IyZTI4YzJhNDE4NWU1YmIzNGMwIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2Ojg2ZGE4YWVlNjIxMTYxYmVhMmVmYWYyN2EyNzA5ZGRhYjVlN2Q0NGUzMGVjZGZkYTcyOGIwMmMwM2EyOGZkOTgifV19", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjItMDYtMDJUMTQ6MzQ6MzQuNzE5MTM1MTc0WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIyLTA2LTAyVDE0OjM0OjM0LjY4NjkzMzI2M1oiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjItMDYtMDJUMTQ6MzQ6MzQuNzE5MTM1MTc0WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6N2VmMjhlOWMyZDU2NDcxZWUwOTBiNTc4YTY3OGJkZjI4YzNiNWEzMTFjYTdiMmUyOGMyYTQxODVlNWJiMzRjMCIsInNoYTI1Njo4NmRhOGFlZTYyMTE2MWJlYTJlZmFmMjdhMjcwOWRkYWI1ZTdkNDRlMzBlY2RmZGE3MjhiMDJjMDNhMjhmZDk4Il19fQ==", "repoDigests": [], "architecture": "", "os": "" @@ -111,7 +111,7 @@ } }, "schema": { - "version": "3.2.4", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.2.4.json" + "version": "3.3.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.0.json" } } diff --git a/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index c1b1d2b797ecd34a5276a1aa2fb18c5b0a58c732..3cd33e2b8f5017eba3b30fd98d7a456c3e4515d7 100644 GIT binary patch literal 15360 zcmeHOUvJws5YO{Ig~z?dHc9c1WMB_jQ=kQkrD(GbSWyHNc}%q0k|D`Of*{|0C)r6H zca7u7aZ{9n0h=Q4c)sINcR!w_bJ9yMxX>UV5z0#`J$0Z}%#4bdkzCMJF#(cmrX4ll zsEBC{u~pI!{;i69@BWK2N|g|MbbhUVGcMR3LkVya$u!@ioGLE%dh~v_+v%NEwJA}g zuH07tTvvWOqQ4}jv}aijww{$hM#M~<5@es)BKO%T8C{Z>`4=+!NZuYCzxsVRtIFZH zux320(kve>wQriY9qp}o6MvFV`{eJU{G8>dhgpeJ4RM;I{Et~Sc4U;##^WoTwggiH zwh5=4_o?bre$2!uYCZ$a5*Fba5&t0RSyt6Wd4aNBt|3#gN}`CXs3BX-aFQY=IXFD* z`D_e*HmJ|*-moeru+;vomp0OU&gxbplL_QCQeEF1xWayhvLP6Jq2y|RogjDQ|9?UF ziXij$hJv7`iwXrDXZh?LMK6n@_ITrx)ECpn%42f@WraO1^Zq1r*!PiCXNF5P(%^Xv ziRDfzp&0;8XexDVl#Ww}eKty^i5Rm+ryQ`&jbO0^nWmAj6uB-q#mbr304UCwVksC7 z-gvGJ*EDg?q!P3f0wN05d1o|_Q_3xk43E7_V)E(gYDa$Xvrgj@+{BYqaqGY)8m*aH zNky7)Zxic{ig~In1A`;9wOmOB*sYWk%1OqD4^SMXf@&#O_xJ0t=OLlLBsR^KAmo>j}1CUKHCx{@c9lJ= zRaA9uCQV3+EDnna4!CXqRY8*{V9!Ba8+-P?c9=$tyct)pbg!6UP`-K-N+ey}Jgza; zU;I9rLVlM|+d9fE3b7c|W;{(ovQJ)27P-kjc|9?w3qd~=$Lu8#ogiz{K@&=&A zBcP|k|M0{p;D5VVA)W32H25EjREYm~VgG>t?Ly1XcaF~f-yQ#Bl7Dsnqm>T+zl&9~ z!2h}~0|dRMrktOScZHOY*DaQ~whEw0Sp%xrVwEeX;tzqcZmF(hbC|X%KG_tf{YI(G zcZ6>{ZxLM`|7q}FrdbsB|IWsJ0D~L+x2-dJ0Kp@MtjB-n&;MooR|fk(Lera*w8d`x z2KfDTa}I$bXdq}HXdr0dN7TT?Uud>N>45L_5rL1H$ z#^Q3vweZ@+2_y+%*>FO=!d{Vp<{k@@rPNg8DFm?CQ{ddnw0W!G&Agf&OW3<&54Y(I z{*UeDMp(%H>6eY{va7)*+N#R%J&FvT?9_jq|MT10@MaHa{9mcy|2tU%m!w+bq0L(( eot<>l*_p`I)${bAotP<15;PDr5HzrL4g3cR`;g%P literal 15360 zcmeHO+iu%N5Y=ZaKc z4r#BxuDkuJ7{OBcm#%FzyUOYr8lw^9HIDjR+4F_Fg0dOV`$VGKgH38I>Hgnpur)R2 zl)HY_XM^MD!m4A0dKhDcAS?&*f(4T;Kn>ke#l4gzVN` ze(?Rjwf#RFzkC1s- z9>CiEk8VKl|Mzk`OrxxEw%gtY==qIJx$uy+*ED8?qX(EcDL{I387Ngsb4dh?x$wg9 z*h*rg6@ZQ(w@kA*(GHyphO!?TNJz`YJ>fe4rt;(!W2awfa(sRRC68dzXajVE@y-n>o(^op5E2ZJDVs7 z4hRGU0s;YnXN7=m8LaJu|D{?6{&zpONo)J>g#RTf@W1=9f8c-jgY$EprS<=wcn6J zY-{-4@PCvC{%=1g#vT7ROM=?XoSXT-=K0?$|A*;W&6n z=@JS7fq+0jARzERL||m{EQP9mOdkK&{`^hnzm$9>7qof)M_D-kzZUPOjKl2tmy1l1 zh;8Ft5R3%h!MHdz7xT>W8cHZCSB&8>&90l53=Xp<>E*D>M<2Vo8*8l9D*K-ZL#y zZQ=xy1TbwlA*sSvk$`4t>Jlk6(avMSc$^Yo%t_n4Rq$q>(~gPk+Y4v#cWf^=0@K;g zetL>o_SUyVTU8mpNs&Rdt4TMle|Dc6KFbBi^{-S||GP5Biri^bZCIhryCba^=@v2! WZ*QOH3R;0P6aoSPfq=kwgTOxo35itz diff --git a/internal/formats/syftjson/to_format_model.go b/internal/formats/syftjson/to_format_model.go index ccf881a0d..adff02119 100644 --- a/internal/formats/syftjson/to_format_model.go +++ b/internal/formats/syftjson/to_format_model.go @@ -55,6 +55,10 @@ func toLinuxReleaser(d *linux.Release) model.LinuxRelease { IDLike: d.IDLike, Version: d.Version, VersionID: d.VersionID, + VersionCodename: d.VersionCodename, + BuildID: d.BuildID, + ImageID: d.ImageID, + ImageVersion: d.ImageVersion, Variant: d.Variant, VariantID: d.VariantID, HomeURL: d.HomeURL, diff --git a/internal/formats/syftjson/to_syft_model.go b/internal/formats/syftjson/to_syft_model.go index 9c28c24a6..992c72476 100644 --- a/internal/formats/syftjson/to_syft_model.go +++ b/internal/formats/syftjson/to_syft_model.go @@ -38,6 +38,10 @@ func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release { IDLike: d.IDLike, Version: d.Version, VersionID: d.VersionID, + VersionCodename: d.VersionCodename, + BuildID: d.BuildID, + ImageID: d.ImageID, + ImageVersion: d.ImageVersion, Variant: d.Variant, VariantID: d.VariantID, HomeURL: d.HomeURL, diff --git a/schema/json/generate.go b/schema/json/generate.go index b6c8cf761..96959c105 100644 --- a/schema/json/generate.go +++ b/schema/json/generate.go @@ -28,6 +28,7 @@ can be extended to include specific package metadata struct shapes in the future // not matter as long as it is exported. type artifactMetadataContainer struct { Apk pkg.ApkMetadata + Alpm pkg.AlpmMetadata Dpkg pkg.DpkgMetadata Gem pkg.GemMetadata Java pkg.JavaMetadata diff --git a/schema/json/schema-3.3.0.json b/schema/json/schema-3.3.0.json new file mode 100644 index 000000000..c70be390d --- /dev/null +++ b/schema/json/schema-3.3.0.json @@ -0,0 +1,1417 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Document", + "definitions": { + "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": "#/definitions/Digest" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "AlpmMetadata": { + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "license", + "url", + "validation", + "reason", + "files", + "backup" + ], + "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": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/AlpmFileRecord" + }, + "type": "array" + }, + "backup": { + "items": { + "$ref": "#/definitions/AlpmFileRecord" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "ApkFileRecord": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "ownerUid": { + "type": "string" + }, + "ownerGid": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "digest": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Digest" + } + }, + "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" + }, + "Classification": { + "required": [ + "class", + "metadata" + ], + "properties": { + "class": { + "type": "string" + }, + "metadata": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Coordinates": { + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + }, + "layerID": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DartPubMetadata": { + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "hosted_url": { + "type": "string" + }, + "vcs_url": { + "type": "string" + } + }, + "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" + }, + "Document": { + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "properties": { + "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" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/File" + }, + "type": "array" + }, + "secrets": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Secrets" + }, + "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/LinuxRelease" + }, + "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" + }, + "DotnetDepsMetadata": { + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sha512": { + "type": "string" + }, + "hashPath": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "DpkgFileRecord": { + "required": [ + "path", + "isConfigFile" + ], + "properties": { + "path": { + "type": "string" + }, + "digest": { + "$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" + }, + "File": { + "required": [ + "id", + "location" + ], + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/definitions/Coordinates" + }, + "metadata": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Digest" + }, + "type": "array" + }, + "classifications": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Classification" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "FileMetadataEntry": { + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType" + ], + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + } + }, + "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" + }, + "GolangBinMetadata": { + "required": [ + "goCompiledVersion", + "architecture" + ], + "properties": { + "goBuildSettings": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "goCompiledVersion": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "h1Digest": { + "type": "string" + }, + "mainModule": { + "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" + }, + "pomProject": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomProject" + }, + "digest": { + "items": { + "$ref": "#/definitions/Digest" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object" + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "items": { + "type": "string" + }, + "type": "array" + }, + "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" + } + }, + "additionalProperties": true, + "type": "object" + }, + "NpmPackageJSONMetadata": { + "required": [ + "name", + "version", + "author", + "licenses", + "homepage", + "description", + "url" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "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" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/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": "#/definitions/AlpmMetadata" + }, + { + "$ref": "#/definitions/ApkMetadata" + }, + { + "$ref": "#/definitions/CargoPackageMetadata" + }, + { + "$ref": "#/definitions/DartPubMetadata" + }, + { + "$ref": "#/definitions/DotnetDepsMetadata" + }, + { + "$ref": "#/definitions/DpkgMetadata" + }, + { + "$ref": "#/definitions/GemMetadata" + }, + { + "$ref": "#/definitions/GolangBinMetadata" + }, + { + "$ref": "#/definitions/JavaMetadata" + }, + { + "$ref": "#/definitions/NpmPackageJSONMetadata" + }, + { + "$ref": "#/definitions/PhpComposerJSONMetadata" + }, + { + "$ref": "#/definitions/PythonPackageMetadata" + }, + { + "$ref": "#/definitions/RpmdbMetadata" + } + ] + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerAuthors": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "homepage": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerExternalReference": { + "required": [ + "type", + "url", + "reference" + ], + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "shasum": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PhpComposerJSONMetadata": { + "required": [ + "name", + "version", + "source", + "dist" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "source": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PhpComposerExternalReference" + }, + "dist": { + "$ref": "#/definitions/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": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PhpComposerAuthors" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "time": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomParent": { + "required": [ + "groupId", + "artifactId", + "version" + ], + "properties": { + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object" + }, + "PomProject": { + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "properties": { + "path": { + "type": "string" + }, + "parent": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PomParent" + }, + "groupId": { + "type": "string" + }, + "artifactId": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "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" + }, + "PythonDirectURLOriginInfo": { + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + }, + "commitId": { + "type": "string" + }, + "vcs": { + "type": "string" + } + }, + "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" + }, + "directUrlOrigin": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PythonDirectURLOriginInfo" + } + }, + "additionalProperties": true, + "type": "object" + }, + "Relationship": { + "required": [ + "parent", + "child", + "type" + ], + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": { + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object" + }, + "RpmdbFileRecord": { + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "properties": { + "path": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "digest": { + "$ref": "#/definitions/Digest" + }, + "userName": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "flags": { + "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": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "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/Coordinates" + }, + "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/linux/identify_release.go b/syft/linux/identify_release.go index ec95dabcf..5b644c1a7 100644 --- a/syft/linux/identify_release.go +++ b/syft/linux/identify_release.go @@ -111,6 +111,10 @@ func parseOsRelease(contents string) (*Release, error) { IDLike: idLike, Version: values["VERSION"], VersionID: values["VERSION_ID"], + VersionCodename: values["VERSION_CODENAME"], + BuildID: values["BUILD_ID"], + ImageID: values["IMAGE_ID"], + ImageVersion: values["IMAGE_VERSION"], Variant: values["VARIANT"], VariantID: values["VARIANT_ID"], HomeURL: values["HOME_URL"], diff --git a/syft/linux/identify_release_test.go b/syft/linux/identify_release_test.go index ad65445f2..687af079d 100644 --- a/syft/linux/identify_release_test.go +++ b/syft/linux/identify_release_test.go @@ -125,6 +125,7 @@ func TestIdentifyRelease(t *testing.T) { ID: "ubuntu", IDLike: []string{"debian"}, Version: "20.04 LTS (Focal Fossa)", + VersionCodename: "focal", VersionID: "20.04", HomeURL: "https://www.ubuntu.com/", SupportURL: "https://help.ubuntu.com/", @@ -217,6 +218,7 @@ func TestIdentifyRelease(t *testing.T) { Name: "Arch Linux", ID: "arch", IDLike: nil, + BuildID: "rolling", HomeURL: "https://www.archlinux.org/", SupportURL: "https://bbs.archlinux.org/", BugReportURL: "https://bugs.archlinux.org/", @@ -349,6 +351,7 @@ func TestParseOsRelease(t *testing.T) { IDLike: []string{"debian"}, Version: "20.04 LTS (Focal Fossa)", VersionID: "20.04", + VersionCodename: "focal", HomeURL: "https://www.ubuntu.com/", SupportURL: "https://help.ubuntu.com/", BugReportURL: "https://bugs.launchpad.net/ubuntu/", diff --git a/syft/linux/release.go b/syft/linux/release.go index 8fdb23645..ab68fbc5e 100644 --- a/syft/linux/release.go +++ b/syft/linux/release.go @@ -8,6 +8,10 @@ type Release struct { IDLike []string `cyclonedx:"idLike"` // list of operating system identifiers in the same syntax as the ID= setting. It should list identifiers of operating systems that are closely related to the local operating system in regards to packaging and programming interfaces. Version string // identifies the operating system version, excluding any OS name information, possibly including a release code name, and suitable for presentation to the user. VersionID string `cyclonedx:"versionID"` // identifies the operating system version, excluding any OS name information or release code name, and suitable for processing by scripts or usage in generated filenames. + VersionCodename string `cyclonedx:"versionCodename"` + BuildID string `cyclonedx:"buildID"` // A string uniquely identifying the system image originally used as the installation base. + ImageID string `cyclonedx:"imageID"` + ImageVersion string `cyclonedx:"imageVersion"` Variant string `cyclonedx:"variant"` // identifies a specific variant or edition of the operating system suitable for presentation to the user. VariantID string `cyclonedx:"variantID"` // identifies a specific variant or edition of the operating system. This may be interpreted by other packages in order to determine a divergent default configuration. HomeURL string @@ -30,6 +34,9 @@ func (r *Release) String() string { if r.Version != "" { return r.ID + " " + r.Version } + if r.VersionID != "" { + return r.ID + " " + r.VersionID + } - return r.ID + " " + r.VersionID + return r.ID + " " + r.BuildID } diff --git a/syft/pkg/alpm_metadata.go b/syft/pkg/alpm_metadata.go new file mode 100644 index 000000000..0867915c6 --- /dev/null +++ b/syft/pkg/alpm_metadata.go @@ -0,0 +1,80 @@ +package pkg + +import ( + "sort" + "time" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/linux" + "github.com/scylladb/go-set/strset" +) + +const AlpmDBGlob = "**/var/lib/pacman/local/**/desc" + +type AlpmMetadata struct { + BasePackage string `mapstructure:"base" json:"basepackage"` + Package string `mapstructure:"name" json:"package"` + Version string `mapstructure:"version" json:"version"` + Description string `mapstructure:"desc" json:"description"` + Architecture string `mapstructure:"arch" json:"architecture"` + Size int `mapstructure:"size" json:"size" cyclonedx:"size"` + Packager string `mapstructure:"packager" json:"packager"` + License string `mapstructure:"license" json:"license"` + URL string `mapstructure:"url" json:"url"` + Validation string `mapstructure:"validation" json:"validation"` + Reason int `mapstructure:"reason" json:"reason"` + Files []AlpmFileRecord `mapstructure:"files" json:"files"` + Backup []AlpmFileRecord `mapstructure:"backup" json:"backup"` +} + +type AlpmFileRecord struct { + Path string `mapstruture:"path" json:"path,omitempty"` + Type string `mapstructure:"type" json:"type,omitempty"` + UID string `mapstructure:"uid" json:"uid,omitempty"` + GID string `mapstructure:"gid" json:"gid,omitempty"` + Time time.Time `mapstructure:"time" json:"time,omitempty"` + Size string `mapstructure:"size" json:"size,omitempty"` + Link string `mapstructure:"link" json:"link,omitempty"` + Digests []file.Digest `mapstructure:"digests" json:"digest,omitempty"` +} + +// PackageURL returns the PURL for the specific Arch Linux package (see https://github.com/package-url/purl-spec) +func (m AlpmMetadata) PackageURL(distro *linux.Release) string { + qualifiers := map[string]string{ + PURLQualifierArch: m.Architecture, + } + + if m.BasePackage != "" { + qualifiers[PURLQualifierUpstream] = m.BasePackage + } + + distroID := "" + if distro != nil { + distroID = distro.ID + } + + return packageurl.NewPackageURL( + "alpm", + distroID, + m.Package, + m.Version, + purlQualifiers( + qualifiers, + distro, + ), + "", + ).ToString() +} + +func (m AlpmMetadata) OwnedFiles() (result []string) { + s := strset.New() + for _, f := range m.Files { + if f.Path != "" { + s.Add(f.Path) + } + } + result = s.List() + sort.Strings(result) + return result +} diff --git a/syft/pkg/alpm_metadata_test.go b/syft/pkg/alpm_metadata_test.go new file mode 100644 index 000000000..8adae54b1 --- /dev/null +++ b/syft/pkg/alpm_metadata_test.go @@ -0,0 +1,112 @@ +package pkg + +import ( + "testing" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/linux" + "github.com/sergi/go-diff/diffmatchpatch" +) + +func TestAlpmMetadata_pURL(t *testing.T) { + tests := []struct { + name string + metadata AlpmMetadata + distro linux.Release + expected string + }{ + { + name: "gocase", + metadata: AlpmMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + }, + distro: linux.Release{ + ID: "arch", + BuildID: "rolling", + }, + expected: "pkg:alpm/arch/p@v?arch=a&distro=arch-rolling", + }, + { + name: "missing architecture", + metadata: AlpmMetadata{ + Package: "p", + Version: "v", + }, + distro: linux.Release{ + ID: "arch", + }, + expected: "pkg:alpm/arch/p@v?distro=arch", + }, + { + metadata: AlpmMetadata{ + Package: "python", + Version: "3.10.0", + Architecture: "any", + }, + distro: linux.Release{ + ID: "arch", + BuildID: "rolling", + }, + expected: "pkg:alpm/arch/python@3.10.0?arch=any&distro=arch-rolling", + }, + { + metadata: AlpmMetadata{ + Package: "g plus plus", + Version: "v84", + Architecture: "x86_64", + }, + distro: linux.Release{ + ID: "arch", + BuildID: "rolling", + }, + expected: "pkg:alpm/arch/g%20plus%20plus@v84?arch=x86_64&distro=arch-rolling", + }, + { + name: "add source information as qualifier", + metadata: AlpmMetadata{ + Package: "p", + Version: "v", + Architecture: "a", + BasePackage: "origin", + }, + distro: linux.Release{ + ID: "arch", + BuildID: "rolling", + }, + expected: "pkg:alpm/arch/p@v?arch=a&upstream=origin&distro=arch-rolling", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := test.metadata.PackageURL(&test.distro) + if actual != test.expected { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(test.expected, actual, true) + t.Errorf("diff: %s", dmp.DiffPrettyText(diffs)) + } + // verify packageurl can parse + purl, err := packageurl.FromString(actual) + if err != nil { + t.Errorf("cannot re-parse purl: %s", actual) + } + if purl.Name != test.metadata.Package { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(test.metadata.Package, purl.Name, true) + t.Errorf("invalid purl name: %s", dmp.DiffPrettyText(diffs)) + } + if purl.Version != test.metadata.Version { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(test.metadata.Version, purl.Version, true) + t.Errorf("invalid purl version: %s", dmp.DiffPrettyText(diffs)) + } + if purl.Qualifiers.Map()["arch"] != test.metadata.Architecture { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(test.metadata.Architecture, purl.Qualifiers.Map()["arch"], true) + t.Errorf("invalid purl architecture: %s", dmp.DiffPrettyText(diffs)) + } + }) + } +} diff --git a/syft/pkg/apk_metadata_test.go b/syft/pkg/apk_metadata_test.go index e1531cd56..1e981201f 100644 --- a/syft/pkg/apk_metadata_test.go +++ b/syft/pkg/apk_metadata_test.go @@ -1,10 +1,11 @@ package pkg import ( - "github.com/anchore/syft/syft/linux" "strings" "testing" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/packageurl-go" "github.com/go-test/deep" "github.com/sergi/go-diff/diffmatchpatch" diff --git a/syft/pkg/cataloger/alpm/cataloger.go b/syft/pkg/cataloger/alpm/cataloger.go new file mode 100644 index 000000000..87a7b285e --- /dev/null +++ b/syft/pkg/cataloger/alpm/cataloger.go @@ -0,0 +1,48 @@ +package alpm + +import ( + "fmt" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +const catalogerName = "alpmdb-cataloger" + +type Cataloger struct{} + +// NewAlpmdbCataloger returns a new ALPM DB cataloger object. +func NewAlpmdbCataloger() *Cataloger { + return &Cataloger{} +} + +// Name returns a string that uniquely describes a cataloger +func (c *Cataloger) Name() string { + return catalogerName +} + +// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation. +func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) { + fileMatches, err := resolver.FilesByGlob(pkg.AlpmDBGlob) + if err != nil { + return nil, nil, fmt.Errorf("failed to find rpmdb's by glob: %w", err) + } + + var pkgs []pkg.Package + for _, location := range fileMatches { + dbContentReader, err := resolver.FileContentsByLocation(location) + if err != nil { + return nil, nil, err + } + + discoveredPkgs, err := parseAlpmDB(resolver, location.RealPath, dbContentReader) + internal.CloseAndLogError(dbContentReader, location.VirtualPath) + if err != nil { + return nil, nil, fmt.Errorf("unable to catalog package=%+v: %w", location.RealPath, err) + } + pkgs = append(pkgs, discoveredPkgs...) + } + return pkgs, nil, nil +} diff --git a/syft/pkg/cataloger/alpm/parse_alpm_db.go b/syft/pkg/cataloger/alpm/parse_alpm_db.go new file mode 100644 index 000000000..e74505ffc --- /dev/null +++ b/syft/pkg/cataloger/alpm/parse_alpm_db.go @@ -0,0 +1,245 @@ +package alpm + +import ( + "bufio" + "compress/gzip" + "fmt" + "io" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" + "github.com/mitchellh/mapstructure" + "github.com/vbatts/go-mtree" +) + +var ( + ignoredFiles = map[string]bool{ + "/set": true, + ".BUILDINFO": true, + ".PKGINFO": true, + "": true, + } +) + +func newAlpmDBPackage(d *pkg.AlpmMetadata) *pkg.Package { + return &pkg.Package{ + Name: d.Package, + Version: d.Version, + FoundBy: catalogerName, + Type: "alpm", + Licenses: strings.Split(d.License, " "), + MetadataType: pkg.AlpmMetadataType, + Metadata: *d, + } +} + +func newScanner(reader io.Reader) *bufio.Scanner { + // This is taken from the apk parser + // https://github.com/anchore/syft/blob/v0.47.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L37 + const maxScannerCapacity = 1024 * 1024 + bufScan := make([]byte, maxScannerCapacity) + scanner := bufio.NewScanner(reader) + scanner.Buffer(bufScan, maxScannerCapacity) + onDoubleLF := func(data []byte, atEOF bool) (advance int, token []byte, err error) { + for i := 0; i < len(data); i++ { + if i > 0 && data[i-1] == '\n' && data[i] == '\n' { + return i + 1, data[:i-1], nil + } + } + if !atEOF { + return 0, nil, nil + } + // deliver the last token (which could be an empty string) + return 0, data, bufio.ErrFinalToken + } + + scanner.Split(onDoubleLF) + return scanner +} + +func getFileReader(path string, resolver source.FileResolver) (io.Reader, error) { + locs, err := resolver.FilesByPath(path) + if err != nil { + return nil, err + } + // TODO: Should we maybe check if we found the file + dbContentReader, err := resolver.FileContentsByLocation(locs[0]) + if err != nil { + return nil, err + } + return dbContentReader, nil +} + +// nolint:funlen +func parseDatabase(b *bufio.Scanner) (*pkg.AlpmMetadata, error) { + var entry pkg.AlpmMetadata + var err error + pkgFields := make(map[string]interface{}) + for b.Scan() { + fields := strings.SplitN(b.Text(), "\n", 2) + + // End of File + if len(fields) == 1 { + break + } + + // The alpm database surrounds the keys with %. + key := strings.ReplaceAll(fields[0], "%", "") + key = strings.ToLower(key) + value := strings.TrimSpace(fields[1]) + + switch key { + case "files": + var files []map[string]string + for _, f := range strings.Split(value, "\n") { + path := fmt.Sprintf("/%s", f) + if ok := ignoredFiles[path]; !ok { + files = append(files, map[string]string{"path": path}) + } + } + pkgFields[key] = files + case "backup": + var backup []map[string]interface{} + for _, f := range strings.Split(value, "\n") { + fields := strings.SplitN(f, "\t", 2) + path := fmt.Sprintf("/%s", fields[0]) + if ok := ignoredFiles[path]; !ok { + backup = append(backup, map[string]interface{}{ + "path": path, + "digests": []file.Digest{{ + Algorithm: "md5", + Value: fields[1], + }}}) + } + } + pkgFields[key] = backup + case "reason": + fallthrough + case "size": + pkgFields[key], err = strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse %s to integer", value) + } + default: + pkgFields[key] = value + } + } + if err := mapstructure.Decode(pkgFields, &entry); err != nil { + return nil, fmt.Errorf("unable to parse ALPM metadata: %w", err) + } + if entry.Package == "" && len(entry.Files) == 0 && len(entry.Backup) == 0 { + return nil, nil + } + + if entry.Backup == nil { + entry.Backup = make([]pkg.AlpmFileRecord, 0) + } + return &entry, nil +} + +func parseMtree(r io.Reader) ([]pkg.AlpmFileRecord, error) { + var err error + var entries []pkg.AlpmFileRecord + + r, err = gzip.NewReader(r) + if err != nil { + return nil, err + } + specDh, err := mtree.ParseSpec(r) + if err != nil { + return nil, err + } + for _, f := range specDh.Entries { + var entry pkg.AlpmFileRecord + entry.Digests = make([]file.Digest, 0) + fileFields := make(map[string]interface{}) + if ok := ignoredFiles[f.Name]; ok { + continue + } + path := fmt.Sprintf("/%s", f.Name) + fileFields["path"] = path + for _, kv := range f.Keywords { + kw := string(kv.Keyword()) + switch kw { + case "time": + // All unix timestamps have a .0 suffixs. + v := strings.Split(kv.Value(), ".") + i, _ := strconv.ParseInt(v[0], 10, 64) + tm := time.Unix(i, 0) + fileFields[kw] = tm + case "sha256digest": + entry.Digests = append(entry.Digests, file.Digest{ + Algorithm: "sha256", + Value: kv.Value(), + }) + case "md5digest": + entry.Digests = append(entry.Digests, file.Digest{ + Algorithm: "md5digest", + Value: kv.Value(), + }) + default: + fileFields[kw] = kv.Value() + } + } + if err := mapstructure.Decode(fileFields, &entry); err != nil { + return nil, fmt.Errorf("unable to parse ALPM mtree data: %w", err) + } + entries = append(entries, entry) + } + return entries, nil +} + +func parseAlpmDBEntry(reader io.Reader) (*pkg.AlpmMetadata, error) { + scanner := newScanner(reader) + metadata, err := parseDatabase(scanner) + if err != nil { + return nil, err + } + if metadata == nil { + return nil, nil + } + return metadata, nil +} + +func parseAlpmDB(resolver source.FileResolver, desc string, reader io.Reader) ([]pkg.Package, error) { + metadata, err := parseAlpmDBEntry(reader) + if err != nil { + return nil, err + } + + base := filepath.Dir(desc) + mtree := filepath.Join(base, "mtree") + r, err := getFileReader(mtree, resolver) + if err != nil { + return nil, err + } + pkgFiles, err := parseMtree(r) + if err != nil { + return nil, err + } + // The replace the files found the the pacman database with the files from the mtree These contain more metadata and + // thus more useful. + metadata.Files = pkgFiles + + // We only really do this to get any backup database entries from the files database + files := filepath.Join(base, "files") + _, err = getFileReader(files, resolver) + if err != nil { + return nil, err + } + filesMetadata, err := parseAlpmDBEntry(reader) + if err != nil { + return nil, err + } else if filesMetadata != nil { + metadata.Backup = filesMetadata.Backup + } + + p := *newAlpmDBPackage(metadata) + p.SetID() + return []pkg.Package{p}, nil +} diff --git a/syft/pkg/cataloger/alpm/parse_alpm_db_test.go b/syft/pkg/cataloger/alpm/parse_alpm_db_test.go new file mode 100644 index 000000000..fd5e28a0c --- /dev/null +++ b/syft/pkg/cataloger/alpm/parse_alpm_db_test.go @@ -0,0 +1,195 @@ +package alpm + +import ( + "bufio" + "os" + "testing" + "time" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/go-test/deep" +) + +func TestDatabaseParser(t *testing.T) { + tests := []struct { + name string + expected pkg.AlpmMetadata + }{ + { + name: "test alpm database parsing", + expected: pkg.AlpmMetadata{ + Backup: []pkg.AlpmFileRecord{ + { + Path: "/etc/pacman.conf", + Digests: []file.Digest{{ + Algorithm: "md5", + Value: "de541390e52468165b96511c4665bff4", + }}, + }, + { + Path: "/etc/makepkg.conf", + Digests: []file.Digest{{ + Algorithm: "md5", + Value: "79fce043df7dfc676ae5ecb903762d8b", + }}, + }, + }, + Files: []pkg.AlpmFileRecord{ + { + Path: "/etc/", + }, + { + Path: "/etc/makepkg.conf", + }, + { + Path: "/etc/pacman.conf", + }, + { + Path: "/usr/", + }, + { + Path: "/usr/bin/", + }, + { + Path: "/usr/bin/makepkg", + }, + { + Path: "/usr/bin/makepkg-template", + }, + { + Path: "/usr/bin/pacman", + }, + { + Path: "/usr/bin/pacman-conf", + }, + { + Path: "/var/", + }, + { + Path: "/var/cache/", + }, + { + Path: "/var/cache/pacman/", + }, + { + Path: "/var/cache/pacman/pkg/", + }, + { + Path: "/var/lib/", + }, + { + Path: "/var/lib/pacman/", + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + file, err := os.Open("test-fixtures/files") + if err != nil { + t.Fatal("Unable to read test-fixtures/file: ", err) + } + defer func() { + err := file.Close() + if err != nil { + t.Fatal("closing file failed:", err) + } + }() + + reader := bufio.NewReader(file) + + entry, err := parseAlpmDBEntry(reader) + if err != nil { + t.Fatal("Unable to read file contents: ", err) + } + + if diff := deep.Equal(entry.Files, test.expected.Files); diff != nil { + for _, d := range diff { + t.Errorf("files diff: %+v", d) + } + } + if diff := deep.Equal(entry.Backup, test.expected.Backup); diff != nil { + for _, d := range diff { + t.Errorf("backup diff: %+v", d) + } + } + }) + } +} + +func parseTime(stime string) time.Time { + t, _ := time.Parse(time.RFC3339, stime) + return t +} + +func TestMtreeParse(t *testing.T) { + tests := []struct { + name string + expected []pkg.AlpmFileRecord + }{ + { + name: "test mtree parsing", + expected: []pkg.AlpmFileRecord{ + { + Path: "/etc", + Type: "dir", + Time: parseTime("2022-04-10T14:59:52+02:00"), + Digests: make([]file.Digest, 0), + }, + { + Path: "/etc/pacman.d", + Type: "dir", + Time: parseTime("2022-04-10T14:59:52+02:00"), + Digests: make([]file.Digest, 0), + }, + { + Path: "/etc/pacman.d/mirrorlist", + Size: "44683", + Time: parseTime("2022-04-10T14:59:52+02:00"), + Digests: []file.Digest{ + { + Algorithm: "md5digest", + Value: "81c39827e38c759d7e847f05db62c233", + }, + { + Algorithm: "sha256", + Value: "fc135ab26f2a227b9599b66a2f1ba325c445acb914d60e7ecf6e5997a87abe1e", + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + file, err := os.Open("test-fixtures/mtree") + if err != nil { + t.Fatal("Unable to read test-fixtures/mtree: ", err) + } + defer func() { + err := file.Close() + if err != nil { + t.Fatal("closing file failed:", err) + } + }() + + reader := bufio.NewReader(file) + + entry, err := parseMtree(reader) + if err != nil { + t.Fatal("Unable to read file contents: ", err) + } + + if diff := deep.Equal(entry, test.expected); diff != nil { + for _, d := range diff { + t.Errorf("files diff: %+v", d) + } + } + }) + } + +} diff --git a/syft/pkg/cataloger/alpm/test-fixtures/files b/syft/pkg/cataloger/alpm/test-fixtures/files new file mode 100644 index 000000000..5e1b9326e --- /dev/null +++ b/syft/pkg/cataloger/alpm/test-fixtures/files @@ -0,0 +1,20 @@ +%FILES% +etc/ +etc/makepkg.conf +etc/pacman.conf +usr/ +usr/bin/ +usr/bin/makepkg +usr/bin/makepkg-template +usr/bin/pacman +usr/bin/pacman-conf +var/ +var/cache/ +var/cache/pacman/ +var/cache/pacman/pkg/ +var/lib/ +var/lib/pacman/ + +%BACKUP% +etc/pacman.conf de541390e52468165b96511c4665bff4 +etc/makepkg.conf 79fce043df7dfc676ae5ecb903762d8b diff --git a/syft/pkg/cataloger/alpm/test-fixtures/mtree b/syft/pkg/cataloger/alpm/test-fixtures/mtree new file mode 100644 index 0000000000000000000000000000000000000000..adf8cc4bb08834e2a1f924f04dc0f79ddf3e8e5c GIT binary patch literal 364 zcmV-y0h9h8iwFP!000001D(;qZWS>M13Hlb~~oy$Aitj+}ZSf>2sKFpU(BZ+srAQ&X@Ds zFE=0F-F&?IG#$%obL3R15>+^dX7fr)@g%)6N=8FWl%q7wA*C8)5Qg4J zwK6fu;MvjsAbV;!6Fo1WFWYnS@*r)MZ|D9WBbRmAw)^dF*^j@kiVMCdqr`@_KsGEb zQ|;MG${wgILj%OuWq6B7qQDLUkaav>m07@}3Ies1Xf#n{>KrUvbG8x5QnIK;JN*I- K1b8ud0ssJsJFSfX literal 0 HcmV?d00001 diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 149091a95..de36fa4bf 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -8,6 +8,7 @@ package cataloger import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/alpm" "github.com/anchore/syft/syft/pkg/cataloger/apkdb" "github.com/anchore/syft/syft/pkg/cataloger/dart" "github.com/anchore/syft/syft/pkg/cataloger/deb" @@ -36,6 +37,7 @@ type Cataloger interface { // ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages. func ImageCatalogers(cfg Config) []Cataloger { return []Cataloger{ + alpm.NewAlpmdbCataloger(), ruby.NewGemSpecCataloger(), python.NewPythonPackageCataloger(), php.NewPHPComposerInstalledCataloger(), @@ -52,6 +54,7 @@ func ImageCatalogers(cfg Config) []Cataloger { // DirectoryCatalogers returns a slice of locally implemented catalogers that are fit for detecting packages from index files (and select installations) func DirectoryCatalogers(cfg Config) []Cataloger { return []Cataloger{ + alpm.NewAlpmdbCataloger(), ruby.NewGemFileLockCataloger(), python.NewPythonIndexCataloger(), python.NewPythonPackageCataloger(), @@ -72,6 +75,7 @@ func DirectoryCatalogers(cfg Config) []Cataloger { // AllCatalogers returns all implemented catalogers func AllCatalogers(cfg Config) []Cataloger { return []Cataloger{ + alpm.NewAlpmdbCataloger(), ruby.NewGemFileLockCataloger(), ruby.NewGemSpecCataloger(), python.NewPythonIndexCataloger(), diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index e96f9c8f8..11a383210 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -12,6 +12,7 @@ const ( UnknownMetadataType MetadataType = "UnknownMetadata" ApkMetadataType MetadataType = "ApkMetadata" + AlpmMetadataType MetadataType = "AlpmMetadata" DpkgMetadataType MetadataType = "DpkgMetadata" GemMetadataType MetadataType = "GemMetadata" JavaMetadataType MetadataType = "JavaMetadata" @@ -28,6 +29,7 @@ const ( var AllMetadataTypes = []MetadataType{ ApkMetadataType, + AlpmMetadataType, DpkgMetadataType, GemMetadataType, JavaMetadataType, @@ -44,6 +46,7 @@ var AllMetadataTypes = []MetadataType{ var MetadataTypeByName = map[MetadataType]reflect.Type{ ApkMetadataType: reflect.TypeOf(ApkMetadata{}), + AlpmMetadataType: reflect.TypeOf(AlpmMetadata{}), DpkgMetadataType: reflect.TypeOf(DpkgMetadata{}), GemMetadataType: reflect.TypeOf(GemMetadata{}), JavaMetadataType: reflect.TypeOf(JavaMetadata{}), diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 01f87b161..531c3d25a 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -9,6 +9,7 @@ const ( // the full set of supported packages UnknownPkg Type = "UnknownPackage" ApkPkg Type = "apk" + AlpmPkg Type = "alpm" GemPkg Type = "gem" DebPkg Type = "deb" RpmPkg Type = "rpm" @@ -27,6 +28,7 @@ const ( // AllPkgs represents all supported package types var AllPkgs = []Type{ ApkPkg, + AlpmPkg, GemPkg, DebPkg, RpmPkg, @@ -47,6 +49,8 @@ func (t Type) PackageURLType() string { switch t { case ApkPkg: return "alpine" + case AlpmPkg: + return "alpm" case GemPkg: return packageurl.TypeGem case DebPkg: @@ -90,6 +94,8 @@ func TypeByName(name string) Type { return DebPkg case packageurl.TypeRPM: return RpmPkg + case "alpm": + return AlpmPkg case "alpine": return ApkPkg case packageurl.TypeMaven: diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index 48e25a69c..87a8151a5 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -64,6 +64,10 @@ func TestTypeFromPURL(t *testing.T) { purl: "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?type=zip&classifier=dist", expected: JavaPkg, }, + { + purl: "pkg:alpm/arch/linux@5.10.0?arch=x86_64&distro=arch", + expected: AlpmPkg, + }, } var pkgTypes []string diff --git a/syft/pkg/url.go b/syft/pkg/url.go index 31dc74b34..a2d2d4b8e 100644 --- a/syft/pkg/url.go +++ b/syft/pkg/url.go @@ -1,7 +1,6 @@ package pkg import ( - "fmt" "regexp" "sort" "strings" @@ -86,12 +85,26 @@ func purlQualifiers(vars map[string]string, release *linux.Release) (q packageur }) } - if release != nil && release.ID != "" && release.VersionID != "" { - q = append(q, packageurl.Qualifier{ - Key: PURLQualifierDistro, - Value: fmt.Sprintf("%s-%s", release.ID, release.VersionID), - }) + distroQualifiers := []string{} + + if release == nil { + return q } + if release.ID != "" { + distroQualifiers = append(distroQualifiers, release.ID) + } + + if release.VersionID != "" { + distroQualifiers = append(distroQualifiers, release.VersionID) + } else if release.BuildID != "" { + distroQualifiers = append(distroQualifiers, release.BuildID) + } + + q = append(q, packageurl.Qualifier{ + Key: PURLQualifierDistro, + Value: strings.Join(distroQualifiers, "-"), + }) + return q } diff --git a/syft/pkg/url_test.go b/syft/pkg/url_test.go index 91321e030..3282b73f6 100644 --- a/syft/pkg/url_test.go +++ b/syft/pkg/url_test.go @@ -190,6 +190,24 @@ func TestPackageURL(t *testing.T) { expected: "pkg:maven/g.id/a@v", }, + { + name: "alpm", + distro: &linux.Release{ + ID: "arch", + BuildID: "rolling", + }, + pkg: Package{ + Name: "linux", + Version: "5.10.0", + Type: AlpmPkg, + Metadata: AlpmMetadata{ + Package: "linux", + Version: "5.10.0", + }, + }, + + expected: "pkg:alpm/arch/linux@5.10.0?distro=arch-rolling", + }, } var pkgTypes []string diff --git a/test/cli/packages_cmd_test.go b/test/cli/packages_cmd_test.go index c7ec797bc..d49e74245 100644 --- a/test/cli/packages_cmd_test.go +++ b/test/cli/packages_cmd_test.go @@ -96,7 +96,7 @@ func TestPackagesCmdFlags(t *testing.T) { name: "squashed-scope-flag", args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage}, assertions: []traitAssertion{ - assertPackageCount(32), + assertPackageCount(33), assertSuccessfulReturnCode, }, }, diff --git a/test/integration/catalog_packages_cases_test.go b/test/integration/catalog_packages_cases_test.go index 60b2005ac..8b7f72435 100644 --- a/test/integration/catalog_packages_cases_test.go +++ b/test/integration/catalog_packages_cases_test.go @@ -221,6 +221,13 @@ var dirOnlyTestCases = []testCase{ } var commonTestCases = []testCase{ + { + name: "find alpm packages", + pkgType: pkg.AlpmPkg, + pkgInfo: map[string]string{ + "pacman": "6.0.1-5", + }, + }, { name: "find rpmdb packages", pkgType: pkg.RpmPkg, diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index 695575825..583743086 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -90,7 +90,6 @@ func TestPkgCoverageImage(t *testing.T) { pkgCount := 0 for a := range sbom.Artifacts.PackageCatalog.Enumerate(c.pkgType) { - if a.Language.String() != "" { observedLanguages.Add(a.Language.String()) } @@ -167,7 +166,6 @@ func TestPkgCoverageDirectory(t *testing.T) { actualPkgCount := 0 for actualPkg := range sbom.Artifacts.PackageCatalog.Enumerate(test.pkgType) { - observedLanguages.Add(actualPkg.Language.String()) observedPkgs.Add(string(actualPkg.Type)) diff --git a/test/integration/convert_test.go b/test/integration/convert_test.go index 526d6006e..add2e1fcc 100644 --- a/test/integration/convert_test.go +++ b/test/integration/convert_test.go @@ -42,32 +42,28 @@ func TestConvertCmd(t *testing.T) { f, err := ioutil.TempFile("", "test-convert-sbom-") require.NoError(t, err) defer func() { - err := f.Close() - require.NoError(t, err) os.Remove(f.Name()) }() err = format.Encode(f, sbom) require.NoError(t, err) - stdr, stdw, err := os.Pipe() - require.NoError(t, err) - originalStdout := os.Stdout - os.Stdout = stdw - ctx := context.Background() app := &config.Application{Outputs: []string{format.ID().String()}} + // stdout reduction of test noise + rescue := os.Stdout // keep backup of the real stdout + os.Stdout, _ = os.OpenFile(os.DevNull, os.O_APPEND|os.O_WRONLY, os.ModeAppend) + defer func() { + os.Stdout = rescue + }() + err = convert.Run(ctx, app, []string{f.Name()}) require.NoError(t, err) - stdw.Close() - - out, err := ioutil.ReadAll(stdr) + file, err := ioutil.ReadFile(f.Name()) require.NoError(t, err) - os.Stdout = originalStdout - - formatFound := syft.IdentifyFormat(out) + formatFound := syft.IdentifyFormat(file) if format.ID() == table.ID { require.Nil(t, formatFound) return diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/ALPM_DB_VERSION b/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/ALPM_DB_VERSION new file mode 100644 index 000000000..ec635144f --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/ALPM_DB_VERSION @@ -0,0 +1 @@ +9 diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/desc b/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/desc new file mode 100644 index 000000000..1b49de1e5 --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/desc @@ -0,0 +1,57 @@ +%NAME% +pacman + +%VERSION% +6.0.1-5 + +%BASE% +pacman + +%DESC% +A library-based package manager with dependency support + +%URL% +https://www.archlinux.org/pacman/ + +%ARCH% +x86_64 + +%BUILDDATE% +1652116331 + +%INSTALLDATE% +1654074247 + +%PACKAGER% +Morten Linderud + +%SIZE% +4925474 + +%REASON% +1 + +%GROUPS% +base-devel + +%LICENSE% +GPL + +%VALIDATION% +pgp + +%DEPENDS% +bash +glibc +libarchive +curl +gpgme +pacman-mirrorlist +archlinux-keyring + +%OPTDEPENDS% +perl-locale-gettext: translation support in makepkg-template + +%PROVIDES% +libalpm.so=13-64 + diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/files b/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/files new file mode 100644 index 000000000..9609ce3d7 --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/files @@ -0,0 +1,30 @@ +%FILES% +etc/ +etc/makepkg.conf +etc/pacman.conf +usr/ +usr/bin/ +usr/bin/makepkg +usr/bin/makepkg-template +usr/bin/pacman +usr/bin/pacman-conf +usr/bin/pacman-db-upgrade +usr/bin/pacman-key +usr/bin/repo-add +usr/bin/repo-elephant +usr/bin/repo-remove +usr/bin/testpkg +usr/bin/vercmp +usr/include/ +usr/include/alpm.h +usr/include/alpm_list.h +usr/lib/ +usr/lib/libalpm.so +usr/lib/libalpm.so.13 +usr/lib/libalpm.so.13.0.1 +usr/lib/pkgconfig/ +usr/lib/pkgconfig/libalpm.pc + +%BACKUP% +etc/pacman.conf de541390e52468165b96511c4665bff4 +etc/makepkg.conf 79fce043df7dfc676ae5ecb903762d8b diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/mtree b/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/mtree new file mode 100644 index 0000000000000000000000000000000000000000..005ad8ecebcefd1f7c7765ac819e4f22b6ed913e GIT binary patch literal 20349 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 g{IS=)^2JYU+r9k>cI3N$=p82fA23$qnsn&^0F~