From a86dd3704efdb19aea22774eb7e099d4e85d41e4 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 4 Mar 2022 17:41:38 -0500 Subject: [PATCH] Add platform selection (#866) --- cmd/attest.go | 7 ++++- cmd/cmd.go | 10 +++++-- cmd/packages.go | 15 ++++++---- cmd/power_user.go | 2 +- go.mod | 2 +- go.sum | 4 +-- internal/config/application.go | 1 + .../TestEncodeFullJSONDocument.golden | 4 ++- .../snapshot/TestImageEncoder.golden | 22 +++++++------- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes syft/source/image_metadata.go | 6 ++++ syft/source/source.go | 28 ++++++++++++++---- syft/source/source_test.go | 24 +++++++++++---- test/cli/packages_cmd_test.go | 8 +++++ test/integration/catalog_packages_test.go | 2 +- test/integration/utils_test.go | 4 +-- 16 files changed, 99 insertions(+), 40 deletions(-) diff --git a/cmd/attest.go b/cmd/attest.go index 1a7b430aa..db2df8dae 100644 --- a/cmd/attest.go +++ b/cmd/attest.go @@ -131,7 +131,7 @@ func selectPassFunc(keypath string) (cosign.PassFunc, error) { func attestExec(ctx context.Context, _ *cobra.Command, args []string) error { // can only be an image for attestation or OCI DIR userInput := args[0] - si, err := source.ParseInput(userInput, false) + si, err := source.ParseInput(userInput, appConfig.Platform, false) if err != nil { return fmt.Errorf("could not generate source input for attest command: %w", err) } @@ -304,6 +304,11 @@ func setAttestFlags(flags *pflag.FlagSet) { "output", "o", formatAliases(syftjson.ID)[0], fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", formatAliases(attestFormats...)), ) + + flags.StringP( + "platform", "", "", + "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')", + ) } func bindAttestConfigOptions(flags *pflag.FlagSet) error { diff --git a/cmd/cmd.go b/cmd/cmd.go index 7f178934f..f60b017d6 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -72,8 +72,8 @@ func initCmdAliasBindings() { panic(err) } case attestCmd: - // the --output option is an independently defined flag, but a shared config option - if err = bindSharedOutputConfigOption(attestCmd.Flags()); err != nil { + // the --output and --platform options are independently defined flags, but a shared config option + if err = bindSharedConfigOption(attestCmd.Flags()); err != nil { panic(err) } // even though the root command or packages command is NOT being run, we still need default bindings @@ -90,11 +90,15 @@ func initCmdAliasBindings() { } } -func bindSharedOutputConfigOption(flags *pflag.FlagSet) error { +func bindSharedConfigOption(flags *pflag.FlagSet) error { if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil { return err } + if err := viper.BindPFlag("platform", flags.Lookup("platform")); err != nil { + return err + } + return nil } diff --git a/cmd/packages.go b/cmd/packages.go index b14331833..059173a8b 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -6,17 +6,15 @@ import ( "io/ioutil" "os" - "github.com/anchore/syft/internal/formats/table" - - "github.com/anchore/syft/syft" - "github.com/anchore/stereoscope" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/anchore" "github.com/anchore/syft/internal/bus" + "github.com/anchore/syft/internal/formats/table" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/version" + "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/pkg/cataloger" @@ -112,6 +110,11 @@ func setPackageFlags(flags *pflag.FlagSet) { "file to write the default report output to (default is STDOUT)", ) + flags.StringP( + "platform", "", "", + "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')", + ) + // Upload options ////////////////////////////////////////////////////////// flags.StringP( "host", "H", "", @@ -153,7 +156,7 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error { if err := bindExclusivePackagesConfigOptions(flags); err != nil { return err } - if err := bindSharedOutputConfigOption(flags); err != nil { + if err := bindSharedConfigOption(flags); err != nil { return err } return nil @@ -232,7 +235,7 @@ func packagesExec(_ *cobra.Command, args []string) error { // could be an image or a directory, with or without a scheme userInput := args[0] - si, err := source.ParseInput(userInput, true) + si, err := source.ParseInput(userInput, appConfig.Platform, true) if err != nil { return fmt.Errorf("could not generate source input for packages command: %w", err) } diff --git a/cmd/power_user.go b/cmd/power_user.go index 59c52b262..8b9df4c71 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -114,7 +114,7 @@ func powerUserExecWorker(userInput string, writer sbom.Writer) <-chan error { return } - si, err := source.ParseInput(userInput, true) + si, err := source.ParseInput(userInput, appConfig.Platform, true) if err != nil { errs <- err return diff --git a/go.mod b/go.mod index 31e02eb79..75c536c11 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 - github.com/anchore/stereoscope v0.0.0-20220301220648-8aa8a4a0bf50 + github.com/anchore/stereoscope v0.0.0-20220304014943-22a4c2bb498e github.com/antihax/optional v1.0.0 github.com/bmatcuk/doublestar/v4 v4.0.2 github.com/docker/docker v20.10.12+incompatible diff --git a/go.sum b/go.sum index f10a8a515..1863d69de 100644 --- a/go.sum +++ b/go.sum @@ -284,8 +284,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk= github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0= -github.com/anchore/stereoscope v0.0.0-20220301220648-8aa8a4a0bf50 h1:+Fe67xv6NRLdZ5V9X2kw959XsKQCUx5/6RL/wQZfs44= -github.com/anchore/stereoscope v0.0.0-20220301220648-8aa8a4a0bf50/go.mod h1:QpDHHV2h1NNfu7klzU75XC8RvSlaPK6HHgi0dy8A6sk= +github.com/anchore/stereoscope v0.0.0-20220304014943-22a4c2bb498e h1:+nlnKqlR8UwG/PhTD8qSN7RphXOI9fK77q5p3PQLx0k= +github.com/anchore/stereoscope v0.0.0-20220304014943-22a4c2bb498e/go.mod h1:Juw0DqHmBSAMFVcT/kRM0GhLdIEAaiCQbq9mdCp47dY= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= diff --git a/internal/config/application.go b/internal/config/application.go index 35be6cb54..d24d78bb0 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -46,6 +46,7 @@ type Application struct { Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"` + Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` } // PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index 80bc5162e..f9441d41c 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -163,7 +163,9 @@ ], "manifest": "ZXlKelkyaGxiV0ZXWlhKemFXOXVJam95TENKdFpXUnBZVlI1Y0dVaU9pSmguLi4=", "config": "ZXlKaGNtTm9hWFJsWTNSMWNtVWlPaUpoYldRMk5DSXNJbU52Ym1acC4uLg==", - "repoDigests": [] + "repoDigests": [], + "architecture": "", + "os": "" } }, "distro": { diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index d117183f5..775d2fb05 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -1,7 +1,7 @@ { "artifacts": [ { - "id": "3a6c7061f86ac3f7", + "id": "d9527e708c11f8b9", "name": "package-1", "version": "1.0.1", "type": "python", @@ -9,7 +9,7 @@ "locations": [ { "path": "/somefile-1.txt", - "layerID": "sha256:135c16aca35f76c25a18cb6650c621a46b8b79864ad6f2834167de2679bb587d" + "layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59" } ], "licenses": [ @@ -32,7 +32,7 @@ } }, { - "id": "4e916af2d387ce9", + "id": "73f796c846875b9e", "name": "package-2", "version": "2.0.1", "type": "deb", @@ -40,7 +40,7 @@ "locations": [ { "path": "/somefile-2.txt", - "layerID": "sha256:c751a2f31455b3049bcab3e3af5861c9431116c9f4a46213e44dbeff8ab36985" + "layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec" } ], "licenses": [], @@ -67,7 +67,7 @@ "type": "image", "target": { "userInput": "user-image-input", - "imageID": "sha256:9998e833a66442934ad948e5cbe898630773c369ef16517623254fa46edd171b", + "imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca", "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ @@ -77,18 +77,20 @@ "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:135c16aca35f76c25a18cb6650c621a46b8b79864ad6f2834167de2679bb587d", + "digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59", "size": 22 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:c751a2f31455b3049bcab3e3af5861c9431116c9f4a46213e44dbeff8ab36985", + "digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec", "size": 16 } ], - "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1Njo5OTk4ZTgzM2E2NjQ0MjkzNGFkOTQ4ZTVjYmU4OTg2MzA3NzNjMzY5ZWYxNjUxNzYyMzI1NGZhNDZlZGQxNzFiIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjoxMzVjMTZhY2EzNWY3NmMyNWExOGNiNjY1MGM2MjFhNDZiOGI3OTg2NGFkNmYyODM0MTY3ZGUyNjc5YmI1ODdkIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmM3NTFhMmYzMTQ1NWIzMDQ5YmNhYjNlM2FmNTg2MWM5NDMxMTE2YzlmNGE0NjIxM2U0NGRiZWZmOGFiMzY5ODUifV19", - "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjItMDEtMThUMjA6MzA6MTMuMDQ0Njg1NTg5WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIyLTAxLTE4VDIwOjMwOjEyLjE5OTA3MjQxOVoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjItMDEtMThUMjA6MzA6MTMuMDQ0Njg1NTg5WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6MTM1YzE2YWNhMzVmNzZjMjVhMThjYjY2NTBjNjIxYTQ2YjhiNzk4NjRhZDZmMjgzNDE2N2RlMjY3OWJiNTg3ZCIsInNoYTI1NjpjNzUxYTJmMzE0NTViMzA0OWJjYWIzZTNhZjU4NjFjOTQzMTExNmM5ZjRhNDYyMTNlNDRkYmVmZjhhYjM2OTg1Il19fQ==", - "repoDigests": [] + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==", + "repoDigests": [], + "architecture": "", + "os": "" } }, "distro": { 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 61b0a337edc3d6633121ecd66a95bab972a5cee6..c1b1d2b797ecd34a5276a1aa2fb18c5b0a58c732 100644 GIT binary patch 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 literal 15360 zcmeHOTW{Mo6!!Ceg{Qs7Ht&=S>>+Ckv_P>GZPoz`f`FvRM2jpLl3XM({NH!vTWn{E zm&kTfjDi94dPseT=ZHM|gb7C?;)*-NFu^gBSYYj#LRMKwGVZ80+J+&eA)_jewZx%u zoO47hw!PN&3n7Li=Pfk5Hvj9=7G@YmCYTdwM-U>c)#lqpH*Ya2D^I~;N5QfzAHmIR7TN;3&I`|~yJh-xXtQ-W)7)uhU=s88@3ZegKO3d#QSfd6 z_clv@9H^`LM8D|+bBtwqCP2Y(|PyF z6q&b=$9eGi`0T^4y-`{8(%h)DSL!6|ujH56`-*ANEDOot;w6CgU}ZcPQB|1hq_UZHZk`}@ko)KRED-iB2In#Z+eu~1=yZm`?19`z7 z0gr%3z$5Uc5NLW>+gJRT3LpPH#6oGT|2@TjCB**^5}EYz-$N`jKgJtHpt1cokN?8E z9=!4X2T20{-`oHG7tK8W+jJS!f!Jlt7Dp1*FXM~~I2$vdq2FE8S)7?*Jy8UhU*0;m$U)s7R*xV#leZb8JgBinVQP_=hb7PBo z`_%o8g2~a-6WL@g{p&=g@8MS_GRr@6BEX1QIa3}5MV?mdu4mS`jXQVZ{v=$|%a66ozmGgl{bbM+PD_VEQ`L5_|)=3gJjd85T0OM94^M z9?BS`&LtViu_BHFi=1nUSfq{8)KcYmD6okb#TW|{0o|N|XlfaYwRJ9pRzMWv!Nuf< zBh(zmYdP}|Kp`_KdT@3&UbU(WU!OH09gMQ zJIv42)y$_ z$^@RHi7odv<~I8$_umH9U(wX|FZ}**U)orgI}XY%7FzEb8XM^j80M47+q9r{Jblh1 M;1Tc${3r