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 c1b1d2b79..3cd33e2b8 100644 Binary files a/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden and b/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ 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 000000000..adf8cc4bb Binary files /dev/null and b/syft/pkg/cataloger/alpm/test-fixtures/mtree differ 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 000000000..005ad8ece Binary files /dev/null and b/test/integration/test-fixtures/image-pkg-coverage/pkgs/var/lib/pacman/local/pacman-6.0.1-5/mtree differ