From f910fd1996f3f490000d9e70bbfc081917a82dce Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:53:30 -0700 Subject: [PATCH 1/6] Replace Quill CLI with focused Mach-O tool --- Herebyfile.mjs | 22 +--- tools/cmd/machotool/main.go | 103 +++++++++++++++ tools/go.mod | 96 +------------- tools/go.sum | 248 ------------------------------------ 4 files changed, 107 insertions(+), 362 deletions(-) create mode 100644 tools/cmd/machotool/main.go diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 444aed6179fb5..30dfa528ea055 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1523,24 +1523,6 @@ ${entries} `; } -/** - * @param {string} filePath - */ -async function verifyTypeScriptMacEntitlements(filePath) { - const { stdout } = await $pipe`go -C ./tools tool quill describe --quiet --output json ${filePath}`; - const details = JSON.parse(stdout); - const entitlements = details[0]?.superBlob?.entitlements?.entitlements; - if (typeof entitlements !== "string") { - throw new Error(`Signed file has no macOS entitlements: ${filePath}`); - } - for (const entitlement of typescriptMacEntitlements) { - const escapedEntitlement = entitlement.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - if (!new RegExp(`\\s*${escapedEntitlement}\\s*\\s*`).test(entitlements)) { - throw new Error(`Signed file is missing macOS entitlement '${entitlement}': ${filePath}`); - } - } -} - /** * @typedef {"win32" | "linux" | "darwin" | "aix" | "android" | "freebsd" | "netbsd" | "openbsd" | "sunos"} OS * @typedef {"x64" | "arm" | "arm64" | "ia32" | "ppc64" | "loong64" | "mips64el" | "riscv64" | "s390x"} Arch @@ -2096,7 +2078,7 @@ async function runSignNativePreviewPackages() { // along with a notarization step. for (const p of filelistPaths) { // ESRP preserves entitlements from an existing ad-hoc signature. - await $pipe`go -C ./tools tool quill sign --quiet --ad-hoc --identity ${path.basename(p.path)} --entitlements ${typescriptMacEntitlementsPath} ${p.path}`; + await $pipe`go -C ./tools run ./cmd/machotool sign ${typescriptMacEntitlementsPath} ${p.path}`; const unsignedZipPath = path.join(tmp, `${p.tmpName}.unsigned.zip`); const signedZipPath = path.join(tmp, `${p.tmpName}.signed.zip`); @@ -2157,7 +2139,7 @@ async function runSignNativePreviewPackages() { for (const p of macZips) { await fs.promises.chmod(p.path, 0o755); - await verifyTypeScriptMacEntitlements(p.path); + await $pipe`go -C ./tools run ./cmd/machotool verify ${typescriptMacEntitlementsPath} ${p.path}`; } } } diff --git a/tools/cmd/machotool/main.go b/tools/cmd/machotool/main.go new file mode 100644 index 0000000000000..d2a189d73a38b --- /dev/null +++ b/tools/cmd/machotool/main.go @@ -0,0 +1,103 @@ +// Portions of this file are derived from github.com/anchore/quill v0.7.1. +// The original code is licensed under the Apache License, Version 2.0. +// This version is modified to support only the operations used by TypeScript. + +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/anchore/quill/quill/macho" + "github.com/anchore/quill/quill/pki" + "github.com/anchore/quill/quill/sign" + blacktopmacho "github.com/blacktop/go-macho" +) + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(args []string) error { + if len(args) != 3 { + return errors.New("usage: machotool ") + } + + entitlements, err := os.ReadFile(args[1]) + if err != nil { + return fmt.Errorf("read entitlements: %w", err) + } + switch args[0] { + case "sign": + return signBinary(args[2], string(entitlements)) + case "verify": + return verifyBinary(args[2], string(entitlements)) + default: + return fmt.Errorf("unknown command %q", args[0]) + } +} + +func verifyBinary(path, expectedEntitlements string) error { + file, err := blacktopmacho.Open(path) + if err != nil { + return err + } + defer file.Close() + + codeSignature := file.CodeSignature() + if codeSignature == nil || codeSignature.Entitlements == "" { + return fmt.Errorf("signed file has no macOS entitlements: %s", path) + } + if codeSignature.Entitlements != expectedEntitlements { + return fmt.Errorf("signed file has unexpected macOS entitlements: %s", path) + } + return nil +} + +func signBinary(path, entitlements string) error { + file, err := macho.NewFile(path) + if err != nil { + return err + } + defer file.Close() + + if file.HasCodeSigningCmd() { + err = file.RemoveSigningContent() + if err != nil { + return fmt.Errorf("remove existing signature: %w", err) + } + } + signingMaterial := pki.SigningMaterial{} + + err = file.AddEmptyCodeSigningCmd() + if err != nil { + return err + } + identity := filepath.Base(path) + superBlobSize, superBlob, err := sign.GenerateSigningSuperBlob(identity, file, signingMaterial, entitlements, 0) + if err != nil { + return fmt.Errorf("generate initial signature: %w", err) + } + err = sign.UpdateSuperBlobOffsetReferences(file, uint64(len(superBlob))) + if err != nil { + return fmt.Errorf("update signature offsets: %w", err) + } + _, superBlob, err = sign.GenerateSigningSuperBlob(identity, file, signingMaterial, entitlements, superBlobSize) + if err != nil { + return fmt.Errorf("generate final signature: %w", err) + } + + command, _, err := file.CodeSigningCmd() + if err != nil { + return err + } + if err := file.Patch(superBlob, len(superBlob), uint64(command.DataOffset)); err != nil { + return fmt.Errorf("write signature: %w", err) + } + return nil +} diff --git a/tools/go.mod b/tools/go.mod index 95eb3e0c70eea..59e6b959ec355 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -3,6 +3,8 @@ module github.com/microsoft/TypeScript/tools go 1.26 require ( + github.com/anchore/quill v0.7.1 + github.com/blacktop/go-macho v1.1.263 github.com/golangci/plugin-module-register v0.1.2 golang.org/x/mod v0.37.0 golang.org/x/tools v0.47.0 @@ -10,110 +12,16 @@ require ( ) require ( - dario.cat/mergo v1.0.2 // indirect - github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/adrg/xdg v0.5.3 // indirect - github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf // indirect - github.com/anchore/clio v0.0.0-20251028140840-721eea5390a4 // indirect - github.com/anchore/fangs v0.0.0-20251204220743-df7ac3652565 // indirect - github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d // indirect github.com/anchore/go-logger v0.0.0-20251106021608-a5b0513fa9a9 // indirect github.com/anchore/go-macholibre v0.0.0-20250826193721-3cd206ca93aa // indirect - github.com/anchore/quill v0.7.1 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect - github.com/aws/smithy-go v1.24.2 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/blacktop/go-dwarf v1.0.14 // indirect - github.com/blacktop/go-macho v1.1.263 // indirect - github.com/charmbracelet/bubbles v0.21.0 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.11.3 // indirect - github.com/charmbracelet/x/cellbuf v0.0.14 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.6.2 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/erikgeiser/promptkit v0.9.0 // indirect - github.com/felixge/fgprof v0.9.5 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/github/smimesign v0.2.0 // indirect - github.com/go-openapi/errors v0.22.5 // indirect - github.com/go-openapi/strfmt v0.25.0 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gookit/color v1.6.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/iancoleman/strcase v0.3.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect - github.com/klauspost/compress v1.18.4 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/oklog/ulid v1.3.1 // indirect - github.com/pborman/indent v1.2.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/profile v1.7.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect - github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.mongodb.org/mongo-driver v1.17.6 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect - golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect ) - -tool github.com/anchore/quill/cmd/quill diff --git a/tools/go.sum b/tools/go.sum index 4e02665f65614..370dd96892f4a 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1,324 +1,76 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= -github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf h1:UY7SQkfVVaeGUpPZrJxqmTc8M0ZSWc5ChiKF6I6aL3I= -github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf/go.mod h1:w8Br1ZKk1Nk82YRSh10pcD7LO7avPyFmNnaY1TRPgs0= -github.com/anchore/clio v0.0.0-20251028140840-721eea5390a4 h1:ou5Co2rLHT5mqVdVmVE7QukGe2cCFTryAaaVzEEDnb0= -github.com/anchore/clio v0.0.0-20251028140840-721eea5390a4/go.mod h1:Qps+CdrdcMx1k7GhKaJ1ZkKYZvRaIfbNmW6B8/5ONPs= -github.com/anchore/fangs v0.0.0-20251204220743-df7ac3652565 h1:w7AFkXXSlBSgY/+NbuQIg5qp8hBq2VTliVL1y9p3QzU= -github.com/anchore/fangs v0.0.0-20251204220743-df7ac3652565/go.mod h1:yYsgDLrzipKlyDAzrjtd35lMcXyZ60b8Ko8vTaMwqoU= -github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= -github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= github.com/anchore/go-logger v0.0.0-20251106021608-a5b0513fa9a9 h1:k+D5tVys8MC+CxfzXIOGm3bZ0FEkmn8udlKACPm4PPo= github.com/anchore/go-logger v0.0.0-20251106021608-a5b0513fa9a9/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw= github.com/anchore/go-macholibre v0.0.0-20250826193721-3cd206ca93aa h1:KPEP8f3enFJeus3Wo51I+riVuCvlf4OEYl2B4IfycbQ= github.com/anchore/go-macholibre v0.0.0-20250826193721-3cd206ca93aa/go.mod h1:7YJA6tAfRm4SzIF93b32pR4xnbf8g2nJIeQnp+2vzzI= github.com/anchore/quill v0.7.1 h1:/K2Yvcjs3ipmP+hjxXmy+3IL/nZ0Tnj8S+OLZvtXRYI= github.com/anchore/quill v0.7.1/go.mod h1:1ExdNP//bdjTRROE/MJg8MxLoDzIJH5HvsbMW4chpBo= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= -github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= -github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= -github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= -github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 h1:1hWFp+52Vq8Fevy/KUhbW/1MEApMz7uitCF/PQXRJpk= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1/go.mod h1:sIec8j802/rCkCKgZV678HFR0s7lhQUYXT77tIvlaa4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/blacktop/go-dwarf v1.0.14 h1:OjmzfSgg/qAKckn2tWFebcgKgJ7HOqCj7bS+CiE1lrY= github.com/blacktop/go-dwarf v1.0.14/go.mod h1:4W2FKgSFYcZLDwnR7k+apv5i3nrau4NGl9N6VQ9DSTo= github.com/blacktop/go-macho v1.1.263 h1:wzcLdip1PD2UwOZ5liKijF4YlYbFFF7XdqjLp3VQ1d0= github.com/blacktop/go-macho v1.1.263/go.mod h1:Hc5E2Lvt/U1VT+jOxr1O5l/LNFJeMYK4eAmDfazTiGc= github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= -github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI= -github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI= -github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= -github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= -github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= -github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/erikgeiser/promptkit v0.9.0 h1:3qL1mS/ntCrXdb8sTP/ka82CJ9kEQaGuYXNrYJkWYBc= -github.com/erikgeiser/promptkit v0.9.0/go.mod h1:pU9dtogSe3Jlc2AY77EP7R4WFP/vgD4v+iImC83KsCo= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= -github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= -github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= -github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= -github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/github/smimesign v0.2.0 h1:Hho4YcX5N1I9XNqhq0fNx0Sts8MhLonHd+HRXVGNjvk= github.com/github/smimesign v0.2.0/go.mod h1:iZiiwNT4HbtGRVqCQu7uJPEZCuEE5sfSSttcnePkDl4= -github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= -github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= -github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= -github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= -github.com/gkampitakis/go-snaps v0.5.7 h1:uVGjHR4t4pPHU944udMx7VKHpwepZXmvDMF+yDmI0rg= -github.com/gkampitakis/go-snaps v0.5.7/go.mod h1:ZABkO14uCuVxBHAXAfKG+bqNz+aa1bGPAg8jkI0Nk8Y= -github.com/go-openapi/errors v0.22.5 h1:Yfv4O/PRYpNF3BNmVkEizcHb3uLVVsrDt3LNdgAKRY4= -github.com/go-openapi/errors v0.22.5/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f h1:HU1RgM6NALf/KW9HEY6zry3ADbDKcmpQ+hJedoNGQYQ= -github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= -github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= -github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= -github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= -github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= -github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= -github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= -github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= -github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= -github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= -github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= -github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= -github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8= -github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= -golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= From eb21093c0307a9656dbfafda35107a6e07a0590f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:41:49 -0700 Subject: [PATCH 2/6] Compare Mach-O entitlements semantically --- tools/cmd/machotool/main.go | 82 +++++++++++++++++++++++++++++++- tools/cmd/machotool/main_test.go | 49 +++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 tools/cmd/machotool/main_test.go diff --git a/tools/cmd/machotool/main.go b/tools/cmd/machotool/main.go index d2a189d73a38b..ce7d7f33a4c6f 100644 --- a/tools/cmd/machotool/main.go +++ b/tools/cmd/machotool/main.go @@ -5,10 +5,13 @@ package main import ( + "encoding/xml" "errors" "fmt" + "io" "os" "path/filepath" + "strings" "github.com/anchore/quill/quill/macho" "github.com/anchore/quill/quill/pki" @@ -53,12 +56,87 @@ func verifyBinary(path, expectedEntitlements string) error { if codeSignature == nil || codeSignature.Entitlements == "" { return fmt.Errorf("signed file has no macOS entitlements: %s", path) } - if codeSignature.Entitlements != expectedEntitlements { - return fmt.Errorf("signed file has unexpected macOS entitlements: %s", path) + if err := verifyEntitlements(codeSignature.Entitlements, expectedEntitlements); err != nil { + return fmt.Errorf("verify entitlements in %s: %w", path, err) } return nil } +func verifyEntitlements(actual, expected string) error { + expectedValues, err := parseBooleanEntitlements(expected) + if err != nil { + return fmt.Errorf("parse expected entitlements: %w", err) + } + if len(expectedValues) == 0 { + return errors.New("expected plist contains no boolean entitlements") + } + + actualValues, err := parseBooleanEntitlements(actual) + if err != nil { + return fmt.Errorf("parse signed entitlements: %w", err) + } + for key, expectedValue := range expectedValues { + if actualValue, ok := actualValues[key]; !ok || actualValue != expectedValue { + return fmt.Errorf("entitlement %q does not have expected value %t", key, expectedValue) + } + } + return nil +} + +func parseBooleanEntitlements(contents string) (map[string]bool, error) { + decoder := xml.NewDecoder(strings.NewReader(contents)) + values := map[string]bool{} + dictDepth := 0 + foundDict := false + pendingKey := "" + + for { + token, err := decoder.Token() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + + switch token := token.(type) { + case xml.StartElement: + switch token.Name.Local { + case "dict": + if dictDepth == 0 { + foundDict = true + } else if dictDepth == 1 { + pendingKey = "" + } + dictDepth++ + case "key": + if dictDepth == 1 { + if err := decoder.DecodeElement(&pendingKey, &token); err != nil { + return nil, err + } + } + case "true", "false": + if dictDepth == 1 && pendingKey != "" { + values[pendingKey] = token.Name.Local == "true" + pendingKey = "" + } + default: + if dictDepth == 1 { + pendingKey = "" + } + } + case xml.EndElement: + if token.Name.Local == "dict" { + dictDepth-- + } + } + } + if !foundDict { + return nil, errors.New("plist contains no dictionary") + } + return values, nil +} + func signBinary(path, entitlements string) error { file, err := macho.NewFile(path) if err != nil { diff --git a/tools/cmd/machotool/main_test.go b/tools/cmd/machotool/main_test.go new file mode 100644 index 0000000000000..c36bb9c90c34c --- /dev/null +++ b/tools/cmd/machotool/main_test.go @@ -0,0 +1,49 @@ +package main + +import "testing" + +func TestVerifyEntitlements(t *testing.T) { + t.Parallel() + + expected := ` + first + second + ` + + for _, test := range []struct { + name string + actual string + wantErr bool + }{ + { + name: "equivalent formatting and order", + actual: ` + + second + + extra + first + + + `, + }, + { + name: "missing", + actual: `first`, + wantErr: true, + }, + { + name: "wrong value", + actual: `firstsecond`, + wantErr: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + err := verifyEntitlements(test.actual, expected) + if (err != nil) != test.wantErr { + t.Fatalf("verifyEntitlements() error = %v, wantErr %v", err, test.wantErr) + } + }) + } +} From 09a19b79b8d5e7aac42547c0f26a0bcd61d47aaa Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:43:36 -0700 Subject: [PATCH 3/6] Simplify Mach-O entitlement verification --- Herebyfile.mjs | 2 +- tools/cmd/machotool/main.go | 103 +++++++------------------------ tools/cmd/machotool/main_test.go | 13 ++-- 3 files changed, 27 insertions(+), 91 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 30dfa528ea055..013dd88fa9e6d 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -2139,7 +2139,7 @@ async function runSignNativePreviewPackages() { for (const p of macZips) { await fs.promises.chmod(p.path, 0o755); - await $pipe`go -C ./tools run ./cmd/machotool verify ${typescriptMacEntitlementsPath} ${p.path}`; + await $pipe`go -C ./tools run ./cmd/machotool verify ${p.path} ${typescriptMacEntitlements}`; } } } diff --git a/tools/cmd/machotool/main.go b/tools/cmd/machotool/main.go index ce7d7f33a4c6f..99e56ff97b5ce 100644 --- a/tools/cmd/machotool/main.go +++ b/tools/cmd/machotool/main.go @@ -5,13 +5,11 @@ package main import ( - "encoding/xml" "errors" "fmt" - "io" "os" "path/filepath" - "strings" + "regexp" "github.com/anchore/quill/quill/macho" "github.com/anchore/quill/quill/pki" @@ -27,25 +25,31 @@ func main() { } func run(args []string) error { - if len(args) != 3 { - return errors.New("usage: machotool ") + if len(args) == 0 { + return errors.New("usage: machotool sign | machotool verify ...") } - entitlements, err := os.ReadFile(args[1]) - if err != nil { - return fmt.Errorf("read entitlements: %w", err) - } switch args[0] { case "sign": + if len(args) != 3 { + return errors.New("usage: machotool sign ") + } + entitlements, err := os.ReadFile(args[1]) + if err != nil { + return fmt.Errorf("read entitlements: %w", err) + } return signBinary(args[2], string(entitlements)) case "verify": - return verifyBinary(args[2], string(entitlements)) + if len(args) < 3 { + return errors.New("usage: machotool verify ...") + } + return verifyBinary(args[1], args[2:]) default: return fmt.Errorf("unknown command %q", args[0]) } } -func verifyBinary(path, expectedEntitlements string) error { +func verifyBinary(path string, requiredEntitlements []string) error { file, err := blacktopmacho.Open(path) if err != nil { return err @@ -56,87 +60,22 @@ func verifyBinary(path, expectedEntitlements string) error { if codeSignature == nil || codeSignature.Entitlements == "" { return fmt.Errorf("signed file has no macOS entitlements: %s", path) } - if err := verifyEntitlements(codeSignature.Entitlements, expectedEntitlements); err != nil { + if err := verifyEntitlements(codeSignature.Entitlements, requiredEntitlements); err != nil { return fmt.Errorf("verify entitlements in %s: %w", path, err) } return nil } -func verifyEntitlements(actual, expected string) error { - expectedValues, err := parseBooleanEntitlements(expected) - if err != nil { - return fmt.Errorf("parse expected entitlements: %w", err) - } - if len(expectedValues) == 0 { - return errors.New("expected plist contains no boolean entitlements") - } - - actualValues, err := parseBooleanEntitlements(actual) - if err != nil { - return fmt.Errorf("parse signed entitlements: %w", err) - } - for key, expectedValue := range expectedValues { - if actualValue, ok := actualValues[key]; !ok || actualValue != expectedValue { - return fmt.Errorf("entitlement %q does not have expected value %t", key, expectedValue) +func verifyEntitlements(actual string, required []string) error { + for _, entitlement := range required { + pattern := `\s*` + regexp.QuoteMeta(entitlement) + `\s*\s*(|\s*)` + if !regexp.MustCompile(pattern).MatchString(actual) { + return fmt.Errorf("required entitlement %q is not enabled", entitlement) } } return nil } -func parseBooleanEntitlements(contents string) (map[string]bool, error) { - decoder := xml.NewDecoder(strings.NewReader(contents)) - values := map[string]bool{} - dictDepth := 0 - foundDict := false - pendingKey := "" - - for { - token, err := decoder.Token() - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return nil, err - } - - switch token := token.(type) { - case xml.StartElement: - switch token.Name.Local { - case "dict": - if dictDepth == 0 { - foundDict = true - } else if dictDepth == 1 { - pendingKey = "" - } - dictDepth++ - case "key": - if dictDepth == 1 { - if err := decoder.DecodeElement(&pendingKey, &token); err != nil { - return nil, err - } - } - case "true", "false": - if dictDepth == 1 && pendingKey != "" { - values[pendingKey] = token.Name.Local == "true" - pendingKey = "" - } - default: - if dictDepth == 1 { - pendingKey = "" - } - } - case xml.EndElement: - if token.Name.Local == "dict" { - dictDepth-- - } - } - } - if !foundDict { - return nil, errors.New("plist contains no dictionary") - } - return values, nil -} - func signBinary(path, entitlements string) error { file, err := macho.NewFile(path) if err != nil { diff --git a/tools/cmd/machotool/main_test.go b/tools/cmd/machotool/main_test.go index c36bb9c90c34c..567f2bf88c810 100644 --- a/tools/cmd/machotool/main_test.go +++ b/tools/cmd/machotool/main_test.go @@ -5,10 +5,7 @@ import "testing" func TestVerifyEntitlements(t *testing.T) { t.Parallel() - expected := ` - first - second - ` + required := []string{"first", "second"} for _, test := range []struct { name string @@ -20,7 +17,7 @@ func TestVerifyEntitlements(t *testing.T) { actual: ` second - + extra first @@ -33,14 +30,14 @@ func TestVerifyEntitlements(t *testing.T) { wantErr: true, }, { - name: "wrong value", - actual: `firstsecond`, + name: "disabled", + actual: `firstsecond`, wantErr: true, }, } { t.Run(test.name, func(t *testing.T) { t.Parallel() - err := verifyEntitlements(test.actual, expected) + err := verifyEntitlements(test.actual, required) if (err != nil) != test.wantErr { t.Fatalf("verifyEntitlements() error = %v, wantErr %v", err, test.wantErr) } From d6274d701533203012ebb5272bf7e986d841eb7d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:47:12 -0700 Subject: [PATCH 4/6] Parse Mach-O entitlements as XML --- Herebyfile.mjs | 2 +- tools/cmd/machotool/main.go | 84 ++++++++++++++++++++++++-------- tools/cmd/machotool/main_test.go | 19 +++++++- 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 013dd88fa9e6d..30dfa528ea055 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -2139,7 +2139,7 @@ async function runSignNativePreviewPackages() { for (const p of macZips) { await fs.promises.chmod(p.path, 0o755); - await $pipe`go -C ./tools run ./cmd/machotool verify ${p.path} ${typescriptMacEntitlements}`; + await $pipe`go -C ./tools run ./cmd/machotool verify ${typescriptMacEntitlementsPath} ${p.path}`; } } } diff --git a/tools/cmd/machotool/main.go b/tools/cmd/machotool/main.go index 99e56ff97b5ce..b8e7073569e77 100644 --- a/tools/cmd/machotool/main.go +++ b/tools/cmd/machotool/main.go @@ -5,11 +5,12 @@ package main import ( + "encoding/xml" "errors" "fmt" "os" "path/filepath" - "regexp" + "strings" "github.com/anchore/quill/quill/macho" "github.com/anchore/quill/quill/pki" @@ -25,31 +26,25 @@ func main() { } func run(args []string) error { - if len(args) == 0 { - return errors.New("usage: machotool sign | machotool verify ...") + if len(args) != 3 { + return errors.New("usage: machotool ") } + entitlements, err := os.ReadFile(args[1]) + if err != nil { + return fmt.Errorf("read entitlements: %w", err) + } switch args[0] { case "sign": - if len(args) != 3 { - return errors.New("usage: machotool sign ") - } - entitlements, err := os.ReadFile(args[1]) - if err != nil { - return fmt.Errorf("read entitlements: %w", err) - } return signBinary(args[2], string(entitlements)) case "verify": - if len(args) < 3 { - return errors.New("usage: machotool verify ...") - } - return verifyBinary(args[1], args[2:]) + return verifyBinary(args[2], string(entitlements)) default: return fmt.Errorf("unknown command %q", args[0]) } } -func verifyBinary(path string, requiredEntitlements []string) error { +func verifyBinary(path, expectedEntitlements string) error { file, err := blacktopmacho.Open(path) if err != nil { return err @@ -60,17 +55,64 @@ func verifyBinary(path string, requiredEntitlements []string) error { if codeSignature == nil || codeSignature.Entitlements == "" { return fmt.Errorf("signed file has no macOS entitlements: %s", path) } - if err := verifyEntitlements(codeSignature.Entitlements, requiredEntitlements); err != nil { + if err := verifyEntitlements(codeSignature.Entitlements, expectedEntitlements); err != nil { return fmt.Errorf("verify entitlements in %s: %w", path, err) } return nil } -func verifyEntitlements(actual string, required []string) error { - for _, entitlement := range required { - pattern := `\s*` + regexp.QuoteMeta(entitlement) + `\s*\s*(|\s*)` - if !regexp.MustCompile(pattern).MatchString(actual) { - return fmt.Errorf("required entitlement %q is not enabled", entitlement) +type plistElement struct { + XMLName xml.Name `xml:""` + Value string `xml:",chardata"` +} + +type entitlementsPlist struct { + XMLName xml.Name `xml:"plist"` + Dict struct { + Elements []plistElement `xml:",any"` + } `xml:"dict"` +} + +func parseBooleanEntitlements(contents string) (map[string]bool, error) { + var plist entitlementsPlist + if err := xml.Unmarshal([]byte(contents), &plist); err != nil { + return nil, err + } + + entitlements := map[string]bool{} + key := "" + for _, element := range plist.Dict.Elements { + switch element.XMLName.Local { + case "key": + key = strings.TrimSpace(element.Value) + case "true", "false": + if key != "" { + entitlements[key] = element.XMLName.Local == "true" + } + key = "" + default: + key = "" + } + } + return entitlements, nil +} + +func verifyEntitlements(actual, expected string) error { + expectedValues, err := parseBooleanEntitlements(expected) + if err != nil { + return fmt.Errorf("parse expected entitlements: %w", err) + } + if len(expectedValues) == 0 { + return errors.New("expected plist contains no boolean entitlements") + } + + actualValues, err := parseBooleanEntitlements(actual) + if err != nil { + return fmt.Errorf("parse signed entitlements: %w", err) + } + for key, expectedValue := range expectedValues { + if actualValue, ok := actualValues[key]; !ok || actualValue != expectedValue { + return fmt.Errorf("entitlement %q does not have expected value %t", key, expectedValue) } } return nil diff --git a/tools/cmd/machotool/main_test.go b/tools/cmd/machotool/main_test.go index 567f2bf88c810..8409825d9df56 100644 --- a/tools/cmd/machotool/main_test.go +++ b/tools/cmd/machotool/main_test.go @@ -5,7 +5,12 @@ import "testing" func TestVerifyEntitlements(t *testing.T) { t.Parallel() - required := []string{"first", "second"} + expected := ` + + + first + second + ` for _, test := range []struct { name string @@ -37,10 +42,20 @@ func TestVerifyEntitlements(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { t.Parallel() - err := verifyEntitlements(test.actual, required) + err := verifyEntitlements(test.actual, expected) if (err != nil) != test.wantErr { t.Fatalf("verifyEntitlements() error = %v, wantErr %v", err, test.wantErr) } }) } } + +func TestVerifyEntitlementsRejectsNestedValue(t *testing.T) { + t.Parallel() + + expected := `first` + actual := `otherfirst` + if err := verifyEntitlements(actual, expected); err == nil { + t.Fatal("verifyEntitlements() succeeded for a nested entitlement") + } +} From 3473bdc07530864575f3c36f204508aff7ff3108 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:38:32 -0700 Subject: [PATCH 5/6] Keep macOS entitlements in Mach-O tool --- Herebyfile.mjs | 24 ++---------------------- tools/cmd/machotool/main.go | 25 ++++++++++++++++--------- tools/cmd/machotool/main_test.go | 17 +++++------------ 3 files changed, 23 insertions(+), 43 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 30dfa528ea055..7aeb135283de5 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1506,23 +1506,6 @@ const mainNativePreviewPackage = { npmTarball: path.join(builtNpm, publishAsTypescript ? "typescript.tgz" : "native-preview.tgz"), }; -const typescriptMacEntitlements = [ - "com.apple.security.cs.allow-dyld-environment-variables", - "com.apple.security.cs.disable-library-validation", -]; - -function createTypeScriptMacEntitlementsPlist() { - const entries = typescriptMacEntitlements.map(entitlement => ` ${entitlement}\n `).join("\n"); - return ` - - - -${entries} - - -`; -} - /** * @typedef {"win32" | "linux" | "darwin" | "aix" | "android" | "freebsd" | "netbsd" | "openbsd" | "sunos"} OS * @typedef {"x64" | "arm" | "arm64" | "ia32" | "ppc64" | "loong64" | "mips64el" | "riscv64" | "s390x"} Arch @@ -2044,9 +2027,6 @@ async function runSignNativePreviewPackages() { } const tmp = await getSignTempDir(); - const typescriptMacEntitlementsPath = path.join(tmp, "typescript-macos-entitlements.plist"); - await fs.promises.writeFile(typescriptMacEntitlementsPath, createTypeScriptMacEntitlementsPlist()); - /** @type {DDSignFileList} */ const filelist = { SignFileRecordList: [], @@ -2078,7 +2058,7 @@ async function runSignNativePreviewPackages() { // along with a notarization step. for (const p of filelistPaths) { // ESRP preserves entitlements from an existing ad-hoc signature. - await $pipe`go -C ./tools run ./cmd/machotool sign ${typescriptMacEntitlementsPath} ${p.path}`; + await $pipe`go -C ./tools run ./cmd/machotool sign ${p.path}`; const unsignedZipPath = path.join(tmp, `${p.tmpName}.unsigned.zip`); const signedZipPath = path.join(tmp, `${p.tmpName}.signed.zip`); @@ -2139,7 +2119,7 @@ async function runSignNativePreviewPackages() { for (const p of macZips) { await fs.promises.chmod(p.path, 0o755); - await $pipe`go -C ./tools run ./cmd/machotool verify ${typescriptMacEntitlementsPath} ${p.path}`; + await $pipe`go -C ./tools run ./cmd/machotool verify ${p.path}`; } } } diff --git a/tools/cmd/machotool/main.go b/tools/cmd/machotool/main.go index b8e7073569e77..b25ece27837f8 100644 --- a/tools/cmd/machotool/main.go +++ b/tools/cmd/machotool/main.go @@ -18,6 +18,18 @@ import ( blacktopmacho "github.com/blacktop/go-macho" ) +const typescriptMacEntitlements = ` + + + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + +` + func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err) @@ -26,19 +38,14 @@ func main() { } func run(args []string) error { - if len(args) != 3 { - return errors.New("usage: machotool ") - } - - entitlements, err := os.ReadFile(args[1]) - if err != nil { - return fmt.Errorf("read entitlements: %w", err) + if len(args) != 2 { + return errors.New("usage: machotool ") } switch args[0] { case "sign": - return signBinary(args[2], string(entitlements)) + return signBinary(args[1], typescriptMacEntitlements) case "verify": - return verifyBinary(args[2], string(entitlements)) + return verifyBinary(args[1], typescriptMacEntitlements) default: return fmt.Errorf("unknown command %q", args[0]) } diff --git a/tools/cmd/machotool/main_test.go b/tools/cmd/machotool/main_test.go index 8409825d9df56..510c58b082418 100644 --- a/tools/cmd/machotool/main_test.go +++ b/tools/cmd/machotool/main_test.go @@ -5,13 +5,6 @@ import "testing" func TestVerifyEntitlements(t *testing.T) { t.Parallel() - expected := ` - - - first - second - ` - for _, test := range []struct { name string actual string @@ -21,28 +14,28 @@ func TestVerifyEntitlements(t *testing.T) { name: "equivalent formatting and order", actual: ` - second + com.apple.security.cs.disable-library-validation extra - first + com.apple.security.cs.allow-dyld-environment-variables `, }, { name: "missing", - actual: `first`, + actual: `com.apple.security.cs.allow-dyld-environment-variables`, wantErr: true, }, { name: "disabled", - actual: `firstsecond`, + actual: `com.apple.security.cs.allow-dyld-environment-variablescom.apple.security.cs.disable-library-validation`, wantErr: true, }, } { t.Run(test.name, func(t *testing.T) { t.Parallel() - err := verifyEntitlements(test.actual, expected) + err := verifyEntitlements(test.actual, typescriptMacEntitlements) if (err != nil) != test.wantErr { t.Fatalf("verifyEntitlements() error = %v, wantErr %v", err, test.wantErr) } From 9a01f7153558170e7e0c9c9d6ad7681a6e7640e7 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:22:03 -0700 Subject: [PATCH 6/6] Revert "Keep macOS entitlements in Mach-O tool" This reverts commit 3473bdc07530864575f3c36f204508aff7ff3108. --- Herebyfile.mjs | 24 ++++++++++++++++++++++-- tools/cmd/machotool/main.go | 25 +++++++++---------------- tools/cmd/machotool/main_test.go | 17 ++++++++++++----- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 7aeb135283de5..30dfa528ea055 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1506,6 +1506,23 @@ const mainNativePreviewPackage = { npmTarball: path.join(builtNpm, publishAsTypescript ? "typescript.tgz" : "native-preview.tgz"), }; +const typescriptMacEntitlements = [ + "com.apple.security.cs.allow-dyld-environment-variables", + "com.apple.security.cs.disable-library-validation", +]; + +function createTypeScriptMacEntitlementsPlist() { + const entries = typescriptMacEntitlements.map(entitlement => ` ${entitlement}\n `).join("\n"); + return ` + + + +${entries} + + +`; +} + /** * @typedef {"win32" | "linux" | "darwin" | "aix" | "android" | "freebsd" | "netbsd" | "openbsd" | "sunos"} OS * @typedef {"x64" | "arm" | "arm64" | "ia32" | "ppc64" | "loong64" | "mips64el" | "riscv64" | "s390x"} Arch @@ -2027,6 +2044,9 @@ async function runSignNativePreviewPackages() { } const tmp = await getSignTempDir(); + const typescriptMacEntitlementsPath = path.join(tmp, "typescript-macos-entitlements.plist"); + await fs.promises.writeFile(typescriptMacEntitlementsPath, createTypeScriptMacEntitlementsPlist()); + /** @type {DDSignFileList} */ const filelist = { SignFileRecordList: [], @@ -2058,7 +2078,7 @@ async function runSignNativePreviewPackages() { // along with a notarization step. for (const p of filelistPaths) { // ESRP preserves entitlements from an existing ad-hoc signature. - await $pipe`go -C ./tools run ./cmd/machotool sign ${p.path}`; + await $pipe`go -C ./tools run ./cmd/machotool sign ${typescriptMacEntitlementsPath} ${p.path}`; const unsignedZipPath = path.join(tmp, `${p.tmpName}.unsigned.zip`); const signedZipPath = path.join(tmp, `${p.tmpName}.signed.zip`); @@ -2119,7 +2139,7 @@ async function runSignNativePreviewPackages() { for (const p of macZips) { await fs.promises.chmod(p.path, 0o755); - await $pipe`go -C ./tools run ./cmd/machotool verify ${p.path}`; + await $pipe`go -C ./tools run ./cmd/machotool verify ${typescriptMacEntitlementsPath} ${p.path}`; } } } diff --git a/tools/cmd/machotool/main.go b/tools/cmd/machotool/main.go index b25ece27837f8..b8e7073569e77 100644 --- a/tools/cmd/machotool/main.go +++ b/tools/cmd/machotool/main.go @@ -18,18 +18,6 @@ import ( blacktopmacho "github.com/blacktop/go-macho" ) -const typescriptMacEntitlements = ` - - - - com.apple.security.cs.allow-dyld-environment-variables - - com.apple.security.cs.disable-library-validation - - - -` - func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err) @@ -38,14 +26,19 @@ func main() { } func run(args []string) error { - if len(args) != 2 { - return errors.New("usage: machotool ") + if len(args) != 3 { + return errors.New("usage: machotool ") + } + + entitlements, err := os.ReadFile(args[1]) + if err != nil { + return fmt.Errorf("read entitlements: %w", err) } switch args[0] { case "sign": - return signBinary(args[1], typescriptMacEntitlements) + return signBinary(args[2], string(entitlements)) case "verify": - return verifyBinary(args[1], typescriptMacEntitlements) + return verifyBinary(args[2], string(entitlements)) default: return fmt.Errorf("unknown command %q", args[0]) } diff --git a/tools/cmd/machotool/main_test.go b/tools/cmd/machotool/main_test.go index 510c58b082418..8409825d9df56 100644 --- a/tools/cmd/machotool/main_test.go +++ b/tools/cmd/machotool/main_test.go @@ -5,6 +5,13 @@ import "testing" func TestVerifyEntitlements(t *testing.T) { t.Parallel() + expected := ` + + + first + second + ` + for _, test := range []struct { name string actual string @@ -14,28 +21,28 @@ func TestVerifyEntitlements(t *testing.T) { name: "equivalent formatting and order", actual: ` - com.apple.security.cs.disable-library-validation + second extra - com.apple.security.cs.allow-dyld-environment-variables + first `, }, { name: "missing", - actual: `com.apple.security.cs.allow-dyld-environment-variables`, + actual: `first`, wantErr: true, }, { name: "disabled", - actual: `com.apple.security.cs.allow-dyld-environment-variablescom.apple.security.cs.disable-library-validation`, + actual: `firstsecond`, wantErr: true, }, } { t.Run(test.name, func(t *testing.T) { t.Parallel() - err := verifyEntitlements(test.actual, typescriptMacEntitlements) + err := verifyEntitlements(test.actual, expected) if (err != nil) != test.wantErr { t.Fatalf("verifyEntitlements() error = %v, wantErr %v", err, test.wantErr) }