diff --git a/app/cli/cmd/attestation_add.go b/app/cli/cmd/attestation_add.go index da095bc9d..55ef9e243 100644 --- a/app/cli/cmd/attestation_add.go +++ b/app/cli/cmd/attestation_add.go @@ -41,6 +41,7 @@ func newAttestationAddCmd() *cobra.Command { var artifactCASConn *grpc.ClientConn var annotationsFlag []string var noStrictValidation bool + var skipSecretRedaction bool var policyInputFromFileFlag []string var policyInputFlag []string var appendFlag bool @@ -102,17 +103,18 @@ func newAttestationAddCmd() *cobra.Command { a, err := action.NewAttestationAdd( &action.AttestationAddOpts{ - ActionsOpts: ActionOpts, - CASURI: viper.GetString(confOptions.CASAPI.viperKey), - CASCAPath: viper.GetString(confOptions.CASCA.viperKey), - ConnectionInsecure: apiInsecure(), - RegistryServer: registryServer, - RegistryUsername: registryUsername, - RegistryPassword: registryPassword, - LocalStatePath: attestationLocalStatePath, - NoStrictValidation: noStrictValidation, - MaxExtractEntries: maxExtractEntries, - MaxExtractSize: int64(maxExtractSizeBytes), + ActionsOpts: ActionOpts, + CASURI: viper.GetString(confOptions.CASAPI.viperKey), + CASCAPath: viper.GetString(confOptions.CASCA.viperKey), + ConnectionInsecure: apiInsecure(), + RegistryServer: registryServer, + RegistryUsername: registryUsername, + RegistryPassword: registryPassword, + LocalStatePath: attestationLocalStatePath, + NoStrictValidation: noStrictValidation, + SkipSecretRedaction: skipSecretRedaction, + MaxExtractEntries: maxExtractEntries, + MaxExtractSize: int64(maxExtractSizeBytes), }, ) if err != nil { @@ -200,6 +202,7 @@ func newAttestationAddCmd() *cobra.Command { flagAttestationID(cmd) cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("kind of the material to be recorded: %q", schemaapi.ListAvailableMaterialKind())) cmd.Flags().BoolVar(&noStrictValidation, "no-strict-validation", false, "skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC, OSSF_SCORECARD_JSON)") + cmd.Flags().BoolVar(&skipSecretRedaction, "skip-secret-redaction", false, "store evidence exactly as captured, without redacting detected secrets first (CHAINLOOP_AI_CODING_SESSION). Recorded in the attestation so policies can reject it") cmd.Flags().StringArrayVar(&policyInputFromFileFlag, "policy-input-from-file", nil, "feed a policy input from a column of a CSV or JSON file, in the format [:]=[:] (e.g. ignored_paths=exception.csv:Path); the values are APPENDED to any contract-declared value; an optional : prefix scopes the input to a single policy (matched by name or ref), otherwise it applies to every declaring policy; is a single top-level column/field name and defaults to the input name; repeatable. The file is also recorded as EVIDENCE.") cmd.Flags().StringArrayVar(&policyInputFlag, "policy-input", nil, "set a policy input to a literal value that REPLACES (overrides) any contract-declared value for the input, in the format [:]= (e.g. min_iterations=10); use this to override a scalar input at run time; an optional : prefix scopes it to a single policy (matched by name or ref), otherwise it applies to every declaring policy; repeatable.") cmd.Flags().BoolVar(&appendFlag, "append", false, "reserved for a future release: will control whether --policy-input and --policy-input-from-file append to (rather than replace) the contract-declared value; has no effect yet") diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index be9aaeb96..09352342d 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -278,6 +278,7 @@ Options --registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD) --registry-server string OCI repository server, ($CHAINLOOP_REGISTRY_SERVER) --registry-username string registry username, ($CHAINLOOP_REGISTRY_USERNAME) +--skip-secret-redaction store evidence exactly as captured, without redacting detected secrets first (CHAINLOOP_AI_CODING_SESSION). Recorded in the attestation so policies can reject it --value string value to be recorded ``` diff --git a/app/cli/pkg/action/attestation_add.go b/app/cli/pkg/action/attestation_add.go index 99c822f28..e6b36634b 100644 --- a/app/cli/pkg/action/attestation_add.go +++ b/app/cli/pkg/action/attestation_add.go @@ -42,6 +42,9 @@ type AttestationAddOpts struct { LocalStatePath string // NoStrictValidation skips strict schema validation NoStrictValidation bool + // SkipSecretRedaction uploads evidence that would normally be scrubbed + // exactly as captured. The bypass is recorded in the attestation. + SkipSecretRedaction bool // MaxExtractEntries limits the number of entries extracted from an archive. // Zero defaults to materials.DefaultArchiveLimits().MaxEntries. MaxExtractEntries int @@ -76,6 +79,10 @@ func NewAttestationAdd(cfg *AttestationAddOpts) (*AttestationAdd, error) { if cfg.NoStrictValidation { opts = append(opts, crafter.WithNoStrictValidation(cfg.NoStrictValidation)) } + if cfg.SkipSecretRedaction { + cfg.Logger.Warn().Msg("secret redaction is disabled, evidence will be stored exactly as captured") + opts = append(opts, crafter.WithSkipSecretRedaction(cfg.SkipSecretRedaction)) + } defaults := materials.DefaultArchiveLimits() maxEntries := cfg.MaxExtractEntries diff --git a/go.mod b/go.mod index 07b7f4c56..2fab64681 100644 --- a/go.mod +++ b/go.mod @@ -97,7 +97,6 @@ require ( github.com/sigstore/timestamp-authority/v2 v2.1.3 github.com/styrainc/regal v0.35.1 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 - github.com/zricethezav/gitleaks/v8 v8.30.1 gitlab.com/gitlab-org/security-products/analyzers/report/v5 v5.13.1 go.step.sm/crypto v0.87.0 google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d @@ -108,6 +107,7 @@ require github.com/vektah/gqlparser/v2 v2.5.36 require ( github.com/XSAM/otelsql v0.43.0 + github.com/betterleaks/betterleaks v1.8.1 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/moby/moby/api v1.55.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 @@ -130,17 +130,14 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect - github.com/BobuSumisu/aho-corasick v1.0.3 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/STARRY-S/zip v0.2.3 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/anchore/go-struct-converter v0.1.0 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect @@ -151,17 +148,16 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.36 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.55.0 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.5.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect - github.com/bodgit/sevenzip v1.6.1 // indirect + github.com/bodgit/sevenzip v1.6.2 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/casbin/govaluate v1.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/charmbracelet/lipgloss v0.5.0 // indirect + github.com/charlievieth/fastwalk v1.0.14 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/containerd/errdefs v1.0.0 // indirect @@ -174,6 +170,7 @@ require ( github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect @@ -181,6 +178,7 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/expr-lang/expr v1.17.8 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fatih/semgroup v1.2.0 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect @@ -206,11 +204,13 @@ require ( github.com/go-openapi/swag/typeutils v0.28.0 // indirect github.com/go-openapi/swag/yamlutils v0.28.0 // indirect github.com/go-playground/assert/v2 v2.2.0 // indirect + github.com/go-sprout/sprout v1.0.3 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/google/cel-go v0.30.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-github/v72 v72.0.0 // indirect github.com/google/go-github/v88 v88.0.0 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/renameio/v2 v2.0.0 // indirect @@ -218,7 +218,6 @@ require ( github.com/h2non/filetype v1.1.3 // indirect github.com/hashicorp/go-version v1.9.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect - github.com/huandu/xstrings v1.5.0 // indirect github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -235,15 +234,14 @@ require ( github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect github.com/lestrrat-go/jwx/v3 v3.1.1 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect - github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect - github.com/mholt/archives v0.1.5 // indirect + github.com/mholt/archives v0.1.6-0.20260429171216-ef71b7a32fae // indirect github.com/miekg/dns v1.1.62 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/highwayhash v1.0.4 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minlz v1.0.1 // indirect + github.com/minio/minlz v1.1.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -251,7 +249,6 @@ require ( github.com/moby/moby/client v0.5.1 // indirect github.com/moby/sys/user v0.4.1 // indirect github.com/moby/sys/userns v0.1.0 // indirect - github.com/muesli/termenv v0.15.1 // indirect github.com/natefinch/atomic v1.0.1 // indirect github.com/nats-io/jwt/v2 v2.8.2 // indirect github.com/nats-io/nkeys v0.4.16 // indirect @@ -265,20 +262,23 @@ require ( github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/philhofer/fwd v1.2.0 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/xattr v0.4.12 // indirect + github.com/pkoukk/tiktoken-go v0.1.8 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/rrethy/ahocorasick v1.0.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/shirou/gopsutil/v4 v4.26.6 // indirect - github.com/shopspring/decimal v1.4.0 // indirect + github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed // indirect + github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf // indirect github.com/sigstore/rekor-tiles/v2 v2.3.0 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect @@ -296,8 +296,6 @@ require ( github.com/transparency-dev/formats v0.1.1 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/valyala/fastjson v1.6.10 // indirect - github.com/wasilibs/go-re2 v1.9.0 // indirect - github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -320,7 +318,7 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect - go4.org v0.0.0-20230225012048-214862532bf5 // indirect + go4.org v0.0.0-20260112195520-a5071408f32f // indirect goa.design/goa/v3 v3.27.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/ini.v1 v1.67.3 // indirect diff --git a/go.sum b/go.sum index 300380037..7ac174972 100644 --- a/go.sum +++ b/go.sum @@ -12,24 +12,14 @@ cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ= cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM= cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= cloud.google.com/go/kms v1.33.0 h1:pG0X78m212b2pv9N4fdMoUO69LuZGQ9kSvn8sHBOFAo= @@ -40,14 +30,10 @@ cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY= cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub/v2 v2.6.1 h1:jX6gnC4n8BgYx6MOYICgbbaXZpr1vKeNOE3Bn17P5zg= cloud.google.com/go/pubsub/v2 v2.6.1/go.mod h1:1y2lZnKfUFPZz0PU4YmXyk4lA11+xmYA42zbC32RkxQ= cloud.google.com/go/secretmanager v1.21.0 h1:e56QQaKWRyzBdUz40AeZaio/ZHAl268cFx3QFAAw9CY= cloud.google.com/go/secretmanager v1.21.0/go.mod h1:+nlV+GYqTD8DM+x7Kk3UF7ZPYgdYMowrkZxAmMXORQ8= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg= cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ= cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E= @@ -94,8 +80,6 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= -github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -110,12 +94,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -144,8 +124,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30= github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op h1:p2zFsAzvhIpFya8AIOHIbWf7NGvO34QpLGclyf7nXj8= @@ -211,8 +191,6 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.45.4 h1:w/AryDYMjSUANSQ2uoZxJovUsMTw github.com/aws/aws-sdk-go-v2/service/sts v1.45.4/go.mod h1:WeBiAa67azG7Su9Vf+ChGDBLiAozJCXzdjXiPBUwtbc= github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= github.com/aws/smithy-go v1.27.7/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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -220,6 +198,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/betterleaks/betterleaks v1.8.1 h1:tTSQAdQe+S4MF69o6VtpRDlml1b73F1xNUUA3PcrHiI= +github.com/betterleaks/betterleaks v1.8.1/go.mod h1:M8pR9QvWFx+iWcRH2HG6Qe1wP9DLXhHt75iMNyZ7Y+s= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= @@ -230,8 +210,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= -github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= -github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= +github.com/bodgit/sevenzip v1.6.2 h1:6/0mwj5KaRXpuf9iSiE+VpG7VpzFJ8D60P53VjxRv34= +github.com/bodgit/sevenzip v1.6.2/go.mod h1:q8DktB7GbvNn0Q6u4Iq6zULE0vo3rWtRHQg5L1XmjuU= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= @@ -255,8 +235,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= -github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg= +github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -329,6 +309,8 @@ github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1G github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= @@ -372,6 +354,8 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= +github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -428,8 +412,6 @@ github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyG github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= github.com/go-git/go-git/v6 v6.0.0-alpha.5 h1:sE+OlkHgYWNMVmN1s9sR7uyFgsWLtxcNWse/vBYKxRE= github.com/go-git/go-git/v6 v6.0.0-alpha.5/go.mod h1:3IjhiZnM+uBmUrOGSeqrJpsmi4Vd0H2NZO/uK2a7d0s= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= @@ -516,6 +498,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-sprout/sprout v1.0.3 h1:LLuz0D3aYazgbVTOwCVuMor3LOUVYinipXRIdjA/D+I= +github.com/go-sprout/sprout v1.0.3/go.mod h1:cFFzpnyGGry3cmN0UNCAM1f7AGok6vPVabeYQzBMBZY= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= @@ -546,14 +530,10 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= @@ -602,6 +582,8 @@ github.com/google/go-containerregistry v0.21.9 h1:F+D4uZ3iA3DLMJLfhaqMdHJbzeqm/2 github.com/google/go-containerregistry v0.21.9/go.mod h1:dP5XNKcL7kMFF/TB3LfvWmVhAcv7iqkHb3oDK8aauTo= github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM= +github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg= github.com/google/go-github/v88 v88.0.0 h1:dZA9IKkPK1eXZj4ypngnpRj5FwdpTv4whix2PrQMP7M= github.com/google/go-github/v88 v88.0.0/go.mod h1:rufTDgn2N45wjhukLTyxmvc9nilSp3mr3Rgtt6b1MPw= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= @@ -611,13 +593,8 @@ github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= @@ -638,8 +615,6 @@ github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= github.com/googleapis/enterprise-certificate-proxy v0.3.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4= github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -723,10 +698,7 @@ github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6 github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= @@ -772,8 +744,6 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -828,9 +798,6 @@ github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= -github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= @@ -848,16 +815,14 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k 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-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= -github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= +github.com/mholt/archives v0.1.6-0.20260429171216-ef71b7a32fae h1:J5ek2lGxYgdh5SMMmlNTSKLmS1x2oJQla/V0NaAH7vo= +github.com/mholt/archives v0.1.6-0.20260429171216-ef71b7a32fae/go.mod h1:IbMrpOL3881/V4qoZRFTSTSRzjjZkD3qoRLX07MitpY= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= @@ -874,8 +839,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw= github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= -github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= -github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM= +github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -919,12 +884,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= 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.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -1021,8 +982,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= @@ -1036,6 +997,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= +github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo= +github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1092,6 +1055,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/rrethy/ahocorasick v1.0.0 h1:YKkCB+E5PXc0xmLfMrWbfNht8vG9Re97IHSWZk/Lk8E= +github.com/rrethy/ahocorasick v1.0.0/go.mod h1:nq8oScE7Vy1rOppoQxpQiiDmPHuKCuk9rXrNcxUV3R0= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -1102,7 +1067,6 @@ github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= @@ -1126,8 +1090,10 @@ github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed h1:KT7hI8vYXgU0s2qaMkrfq9tCA1w/iEPgfredVP+4Tzw= +github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= +github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf h1:o1uxfymjZ7jZ4MsgCErcwWGtVKSiNAXtS59Lhs6uI/g= +github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sigstore/cosign/v3 v3.1.3 h1:001JQRI/PJ/5T+g/kJ1KTvKFbb322+fomc+pHDZ/6sg= github.com/sigstore/cosign/v3 v3.1.3/go.mod h1:DmjtYkWDMdbG26X+QSOPB6QQGkLjRjQCIxxNs6wV6bA= @@ -1264,10 +1230,6 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= -github.com/wasilibs/go-re2 v1.9.0 h1:kjAd8qbNvV4Ve2Uf+zrpTCrDHtqH4dlsRXktywo73JQ= -github.com/wasilibs/go-re2 v1.9.0/go.mod h1:0sRtscWgpUdNA137bmr1IUgrRX0Su4dcn9AEe61y+yI= -github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= -github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -1300,7 +1262,6 @@ github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= @@ -1315,8 +1276,6 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -github.com/zricethezav/gitleaks/v8 v8.30.1 h1:PmEvCfVI7ti9dV3s5aMZUY7sS2GxRvG3yzih7E+cS3w= -github.com/zricethezav/gitleaks/v8 v8.30.1/go.mod h1:rTDwxRjufMKAkhTI/Mijd07nday1yOhf9qywjwz5Irw= gitlab.com/gitlab-org/api/client-go v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr/XTWzNoL24= gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8= gitlab.com/gitlab-org/security-products/analyzers/common/v3 v3.4.0 h1:+1k1NQiBSVeLDePX82ec3nHdrOsDvhMmA09LL25omTk= @@ -1329,10 +1288,7 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -1392,30 +1348,22 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= -go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= +go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= goa.design/goa/v3 v3.27.0 h1:WSb7INd1AgU1BMHUYJoC+NdUgPj9DxOK+C4qJwuuiYE= goa.design/goa/v3 v3.27.0/go.mod h1:+KpTEiO/br2yJ5tub4tttTTd0+CSkIqEAAHzDpKKmSM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= @@ -1425,20 +1373,13 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1453,36 +1394,25 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1492,7 +1422,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1508,9 +1437,6 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1521,10 +1447,8 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1548,9 +1472,7 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -1558,22 +1480,16 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= @@ -1583,35 +1499,20 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1622,36 +1523,16 @@ golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNq gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.292.0 h1:Ewiwo/GTtiaPZSNAZQUcWLh8AYDEoPmIXyJfeoTSMHU= google.golang.org/api v0.292.0/go.mod h1:07kjmMnFGm2RQuCza2EZM/5N68G/fVvFb1xKjWqoFA0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -1669,14 +1550,12 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= @@ -1735,8 +1614,6 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= @@ -1756,9 +1633,6 @@ nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/internal/redaction/betterleaks.go b/internal/redaction/betterleaks.go new file mode 100644 index 000000000..9ba4c7924 --- /dev/null +++ b/internal/redaction/betterleaks.go @@ -0,0 +1,141 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redaction + +import ( + "context" + "fmt" + "sync" + + "github.com/betterleaks/betterleaks/detect" + "github.com/betterleaks/betterleaks/sources" +) + +// betterleaksScanner detects secrets with the betterleaks default ruleset. +type betterleaksScanner struct { + // mu serialises Run. The detector keeps per-run state on itself + // (ValidationCounts is cleared at the start of every Run), so concurrent + // scans on a shared detector would race. Redaction is not on a hot + // concurrent path, so serialising is cheaper than owning a detector per + // caller: constructing one compiles the whole ruleset. + mu sync.Mutex + detector *detect.Detector +} + +var defaultScanner = sync.OnceValues(newBetterleaksScanner) + +// DefaultScanner returns the process-wide betterleaks-backed scanner. +// Constructing a detector compiles several hundred regexes and builds a keyword +// trie, so it is built once, lazily, and only for attestations that need it. +func DefaultScanner() (Scanner, error) { + return defaultScanner() +} + +func newBetterleaksScanner() (Scanner, error) { + // Validation stays off, which is what the default constructor gives us: + // validating would reach out to third-party APIs to check whether a candidate + // credential is live, and crafting a material must not do that. + d, err := detect.NewDetectorDefaultConfig() + if err != nil { + return nil, fmt.Errorf("loading the default secret scanning rules: %w", err) + } + + // A session transcript is text the model was free to write, so an in-band + // "betterleaks:allow" or "gitleaks:allow" must not switch redaction off. + d.IgnoreGitleaksAllow = true + // Findings must carry the verbatim secret: it is what we search for in the + // document in order to replace it. + d.Redact = 0 + // Recursive decoding would report the *decoded* secret, which is not a + // substring of the document and therefore cannot be located and replaced. + // Encoded secrets are a known gap. + d.MaxDecodeDepth = 0 + // Not a size guard: an oversized fragment is silently *skipped*, which would + // mean silent under-redaction. The size cap lives in Redactor and fails + // closed instead. + d.MaxTargetMegaBytes = 0 + // Run otherwise accumulates every finding onto the detector for the benefit + // of a deprecated accessor we do not use. Since this detector is a + // process-wide singleton scanned against repeatedly, that would grow without + // bound. Results are read from the Run iterator instead. + d.SkipFindingAppend = true + + return &betterleaksScanner{detector: d}, nil +} + +func (s *betterleaksScanner) Scan(ctx context.Context, text string) ([]Finding, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + s.mu.Lock() + defer s.mu.Unlock() + + var findings []Finding + for result := range s.detector.Run(ctx, stringSource{text: text}) { + if result.Err != nil { + return nil, fmt.Errorf("scanning for secrets: %w", result.Err) + } + + findings = appendSecret(findings, result.Finding.RuleID, result.Finding.Secret) + + // Composite rules report only their primary match. An AWS access key id, + // for instance, only matches when a secret access key is found near it, + // and the rule for that component is marked as not independently + // reportable — so the primary finding names the harmless public + // identifier while the actual credential arrives only as a component. + // Both halves have to be redacted. + for _, set := range result.Finding.ComponentSets { + for _, component := range set.Components { + if component == nil { + continue + } + findings = appendSecret(findings, component.RuleID, component.Secret) + } + } + } + + // Run stops iterating when the context is cancelled, so a partial result + // here would mean silently under-redacting. + if err := ctx.Err(); err != nil { + return nil, err + } + + return findings, nil +} + +// appendSecret records a locatable secret. A finding without one cannot be +// searched for in the document, so it is dropped rather than reported. +func appendSecret(dst []Finding, ruleID, secret string) []Finding { + if secret == "" { + return dst + } + return append(dst, Finding{RuleID: ruleID, Secret: secret}) +} + +// stringSource adapts an in-memory document to the source interface the scanner +// consumes. Run is the only scanning entry point that is neither deprecated nor +// context-blind, and it takes a source rather than a string. +type stringSource struct { + text string +} + +func (s stringSource) Fragments(ctx context.Context, yield sources.FragmentsFunc) error { + if err := ctx.Err(); err != nil { + return err + } + return yield(sources.Fragment{Raw: s.text}, nil) +} diff --git a/internal/redaction/betterleaks_test.go b/internal/redaction/betterleaks_test.go new file mode 100644 index 000000000..31cc1832e --- /dev/null +++ b/internal/redaction/betterleaks_test.go @@ -0,0 +1,309 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redaction + +import ( + "context" + "strings" + "sync" + "testing" + + betterleaksconfig "github.com/betterleaks/betterleaks/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Synthetic credentials shaped to match the default ruleset. Note that +// AKIAIOSFODNN7EXAMPLE would NOT match: the aws-access-token rule allowlists +// anything ending in "EXAMPLE". The character class after the AKIA prefix is +// base32, so no 0, 1, 8 or 9. +// +// The AWS pair is assembled from fragments so the literal never appears in a +// source file. GitHub's push protection recognises the same AWS patterns +// betterleaks does, and would reject a push containing a realistic-looking key +// even in test data. +const ( + fakeAWSKey = "AKIA" + "4G7TI63VCBIRS4GW" + fakeAWSSecret = "kQ7zXn2VbW9pLm4RtY6" + "uHs3JdF8gA1cE5oPzQwXn" + fakeGitHubPAT = "ghp_erOZlZv0B1e3amrQ" + "ugdwZ8Ro2W4kDql9WPTf" +) + +var fakeAnthropicKey = "sk-ant-api03-" + strings.Repeat("a", 93) + "AA" + +// awsPair is the shape an AWS leak has to take to be detected at all: the +// aws-access-token rule is composite and requires a secret access key nearby. +// The `\n` is the escaped two-character sequence, which is how a newline appears +// inside a JSON string leaf - the real shape of a transcript. +const awsPair = `AWS_ACCESS_KEY_ID=` + fakeAWSKey + `\nAWS_SECRET_ACCESS_KEY=` + fakeAWSSecret + +func TestDefaultScannerDetects(t *testing.T) { + scanner, err := DefaultScanner() + require.NoError(t, err) + + testCases := []struct { + name string + text string + wantRule string + }{ + { + name: "aws access token paired with its secret access key", + text: `"content": "run export ` + awsPair + ` first"`, + wantRule: "aws-access-token", + }, + { + name: "anthropic api key", + text: `"content": "ANTHROPIC_API_KEY=` + fakeAnthropicKey + `"`, + wantRule: "anthropic-api-key", + }, + { + name: "github personal access token", + text: `"repository": "https://oauth2:` + fakeGitHubPAT + `@github.com/example/repo.git"`, + wantRule: "github-pat", + }, + { + // A session transcript is attacker-influenceable text, so neither + // in-band bypass marker may suppress a finding. + name: "gitleaks:allow must not suppress the finding", + text: `"content": "` + awsPair + ` // gitleaks:allow"`, + wantRule: "aws-access-token", + }, + { + name: "betterleaks:allow must not suppress the finding", + text: `"content": "` + awsPair + ` // betterleaks:allow"`, + wantRule: "aws-access-token", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + findings, err := scanner.Scan(context.Background(), tc.text) + require.NoError(t, err) + require.NotEmpty(t, findings, "expected the default ruleset to match") + + var rules []string + for _, f := range findings { + rules = append(rules, f.RuleID) + assert.NotEmpty(t, f.Secret, "a finding without a secret cannot be located") + // The whole redaction strategy depends on this: a secret that is + // not a substring of the scanned text cannot be replaced. + assert.Contains(t, tc.text, f.Secret, "the secret must be a substring of the scanned text") + } + assert.Contains(t, rules, tc.wantRule) + }) + } +} + +func TestDefaultScannerIgnoresCleanText(t *testing.T) { + scanner, err := DefaultScanner() + require.NoError(t, err) + + // Realistic decoys that must survive redaction untouched. + clean := []string{ + `"commit_start": "9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6"`, + `"id": "3bf79921-3c03-81b6-afff-cb246849866f"`, + `"total_tokens": 1234567890`, + `"content": "hello world"`, + `"path": "app/controlplane/internal/service/attestation.go"`, + `"models_used": ["claude-opus-5", "claude-sonnet-5"]`, + } + + for _, text := range clean { + t.Run(text, func(t *testing.T) { + findings, err := scanner.Scan(context.Background(), text) + require.NoError(t, err) + assert.Empty(t, findings) + }) + } +} + +// TestDefaultPlaceholderIsNotDetectable is the gate on the placeholder format. +// The convergence loop in Redact terminates only because a placeholder is not +// itself detected as a secret; if one rule matched it, redaction would replace +// its own output forever and fail with ErrNotConverged. Checked against every +// rule in the shipped ruleset rather than a hand-picked sample, so growing the +// ruleset cannot quietly invalidate the assumption. +func TestDefaultPlaceholderIsNotDetectable(t *testing.T) { + scanner, err := DefaultScanner() + require.NoError(t, err) + + cfg, err := betterleaksconfig.Default() + require.NoError(t, err) + require.NotEmpty(t, cfg.Rules) + t.Logf("checking placeholders for %d rules", len(cfg.Rules)) + + ruleIDs := make([]string, 0, len(cfg.Rules)+1) + for id := range cfg.Rules { + ruleIDs = append(ruleIDs, id) + } + // The empty rule id yields the bare "[REDACTED]" placeholder. + ruleIDs = append(ruleIDs, "") + + for _, id := range ruleIDs { + placeholder := DefaultPlaceholder(id) + // Scan the placeholder bare and in the key/value shape that triggers the + // generic keyword-based rules. + for _, text := range []string{placeholder, `"api_key": "` + placeholder + `"`} { + findings, err := scanner.Scan(context.Background(), text) + require.NoError(t, err) + assert.Empty(t, findings, "placeholder for rule %q is itself detected as a secret in %q", id, text) + } + } +} + +// TestDefaultScannerRepeatedScansDoNotAccumulate guards SkipFindingAppend. The +// scanner is a process-wide singleton and Redact scans the same document more +// than once, so a detector that retained every finding would grow without bound +// across attestations. +func TestDefaultScannerRepeatedScansDoNotAccumulate(t *testing.T) { + scanner, err := DefaultScanner() + require.NoError(t, err) + + text := `"content": "` + awsPair + `"` + + first, err := scanner.Scan(context.Background(), text) + require.NoError(t, err) + require.Len(t, first, 2, "the composite rule reports the key id plus its secret component") + + for range 5 { + again, err := scanner.Scan(context.Background(), text) + require.NoError(t, err) + assert.Equal(t, first, again, "repeated scans must return the same findings, not accumulate") + } +} + +// TestDefaultScannerConcurrentScans exercises the shared detector under -race. +// Run mutates per-run state on the detector itself, so the scanner has to +// serialise access. +func TestDefaultScannerConcurrentScans(t *testing.T) { + scanner, err := DefaultScanner() + require.NoError(t, err) + + texts := []string{ + `"content": "` + awsPair + `"`, + `"content": "nothing to see here"`, + `"repository": "https://oauth2:` + fakeGitHubPAT + `@github.com/example/repo.git"`, + } + + var wg sync.WaitGroup + errs := make([]error, 12) + for i := range errs { + wg.Add(1) + go func(idx int, text string) { + defer wg.Done() + _, errs[idx] = scanner.Scan(context.Background(), text) + }(i, texts[i%len(texts)]) + } + wg.Wait() + + for _, err := range errs { + require.NoError(t, err) + } +} + +func TestDefaultScannerCancelledContext(t *testing.T) { + scanner, err := DefaultScanner() + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err = scanner.Scan(ctx, `"content": "`+awsPair+`"`) + require.ErrorIs(t, err, context.Canceled) +} + +// TestDefaultScannerReportsCompositeComponents pins down the most dangerous +// asymmetry in the ruleset. aws-access-token is a composite rule: it fires only +// when a secret access key is found nearby, it reports the AWS key *id* as its +// secret, and the rule matching the actual credential is marked as not +// independently reportable. Reporting only the primary finding would therefore +// redact the harmless public identifier and leave the real credential in place, +// so the scanner has to flatten the components back out. +func TestDefaultScannerReportsCompositeComponents(t *testing.T) { + scanner, err := DefaultScanner() + require.NoError(t, err) + + findings, err := scanner.Scan(context.Background(), `"content": "`+awsPair+`"`) + require.NoError(t, err) + + secretsByRule := make(map[string]string, len(findings)) + for _, f := range findings { + secretsByRule[f.RuleID] = f.Secret + } + + assert.Equal(t, fakeAWSKey, secretsByRule["aws-access-token"]) + assert.Equal(t, fakeAWSSecret, secretsByRule["aws-secret-access-key"], + "the secret access key must be reported, not just the key id") +} + +// TestRedactCredentialInURIConverges is a regression test for a rule that +// matches a *position* rather than a value. Once the token in a URI's userinfo +// is replaced, the rule for credentials embedded in a URI matches the +// placeholder sitting in its place, reporting it as the secret. Rewriting that +// would satisfy the rule again on the next pass, forever, so the engine has to +// recognise its own output. Uses the real ruleset because the behaviour is a +// property of the rules, not of the engine alone. +func TestRedactCredentialInURIConverges(t *testing.T) { + scanner, err := DefaultScanner() + require.NoError(t, err) + + doc := []byte(`{"repository":"https://oauth2:` + fakeGitHubPAT + `@github.com/example/repo.git"}`) + + once, report, err := New(scanner).Redact(context.Background(), doc) + require.NoError(t, err) + require.True(t, report.Changed()) + assert.NotContains(t, string(once), fakeGitHubPAT) + assert.Contains(t, string(once), "[REDACTED:github-pat]") + // Two passes: one that redacts, one that confirms nothing is left. + assert.Equal(t, 2, report.Passes) + + // Redacting the result again must be a no-op, not a rename. + twice, report, err := New(scanner).Redact(context.Background(), once) + require.NoError(t, err) + assert.Equal(t, string(once), string(twice)) + assert.False(t, report.Changed()) +} + +func BenchmarkDefaultScannerInit(b *testing.B) { + for b.Loop() { + if _, err := newBetterleaksScanner(); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkRedact(b *testing.B) { + scanner, err := DefaultScanner() + require.NoError(b, err) + + // A synthetic multi-megabyte transcript with a secret near the end. + turn := `{"role":"assistant","content":"` + strings.Repeat("some ordinary transcript text ", 40) + `"},` + var sb strings.Builder + sb.WriteString(`{"data":{"raw_session":{"main":[`) + for sb.Len() < 5<<20 { + sb.WriteString(turn) + } + sb.WriteString(`{"role":"user","content":"` + awsPair + `"}]}}}`) + doc := []byte(sb.String()) + + r := New(scanner) + b.SetBytes(int64(len(doc))) + b.ResetTimer() + for b.Loop() { + if _, _, err := r.Redact(context.Background(), doc); err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/redaction/redaction.go b/internal/redaction/redaction.go new file mode 100644 index 000000000..bf6fa8e0c --- /dev/null +++ b/internal/redaction/redaction.go @@ -0,0 +1,544 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package redaction removes detected secrets from JSON documents before they +// leave the machine. +// +// Detection and rewriting are deliberately split. Secrets are detected against +// the document rendered as indented JSON, because the patterns that catch +// generic credentials rely on seeing a key next to its value. The rewrite then +// happens structurally, on the decoded value tree, so the output cannot be +// malformed JSON. A convergence loop re-scans after every rewrite, which makes +// idempotency a property of the algorithm rather than of the placeholder format. +package redaction + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "regexp" + "sort" + "strconv" + "strings" +) + +var ( + // ErrNotConverged is returned when repeated passes keep detecting secrets, + // which in practice means a placeholder is itself matching a rule. + ErrNotConverged = errors.New("secret redaction did not converge") + // ErrInvalidJSON is returned when the document is not a single JSON object. + ErrInvalidJSON = errors.New("document is not a JSON object") + // ErrDuplicateKey is returned when an object repeats a key. Decoding keeps + // only the last value for a repeated key, so a secret in an earlier one would + // never be scanned; redaction refuses the document rather than pass it + // through unexamined. + ErrDuplicateKey = errors.New("document contains a duplicate object key") +) + +// DefaultMaxPasses bounds the convergence loop. A successful redaction costs two +// passes: one that finds the secrets and one that confirms none are left. +// +// Redaction is deliberately unbounded in size and time. A session that cannot be +// scanned cannot be stored, and failing an attestation over a large-but-honest +// transcript is worse than taking a while over it. Callers that need a bound can +// impose one through the context they pass to Redact. +const DefaultMaxPasses = 4 + +// Finding is a located secret. It is deliberately decoupled from any particular +// scanning engine's types. +type Finding struct { + // RuleID names the rule that matched. It ends up in the placeholder, so it + // must never contain secret material. + RuleID string + // Secret is the matched credential, verbatim as it appeared in the scanned + // text. + Secret string +} + +// Scanner detects secrets in a text fragment. +type Scanner interface { + Scan(ctx context.Context, text string) ([]Finding, error) +} + +// PathFilter reports whether the string leaf at the given path may be rewritten. +// Paths look like "/data/raw_session/main/0/content": a leading slash, object +// keys and array indices separated by slashes. +type PathFilter func(path string) bool + +// Report summarises a Redact call. It never contains secret material, so it is +// safe to log and to surface as material annotations. +type Report struct { + // Replacements is the total number of substitutions performed. + Replacements int + // ByRule counts substitutions per rule id. + ByRule map[string]int + // Unlocated counts, per rule id, secrets the scanner reported but which + // could not be attributed to any eligible leaf. That happens when the match + // lies in a path the filter protects, or when it spans the JSON punctuation + // between two adjacent leaves. + Unlocated map[string]int + // Passes is the number of detection passes performed. + Passes int +} + +// Changed reports whether anything was redacted. +func (r *Report) Changed() bool { return r != nil && r.Replacements > 0 } + +// RuleIDs returns the sorted, deduplicated ids of the rules that actually +// redacted something. +func (r *Report) RuleIDs() []string { + if r == nil { + return nil + } + out := make([]string, 0, len(r.ByRule)) + for id := range r.ByRule { + out = append(out, id) + } + sort.Strings(out) + return out +} + +// Redactor rewrites JSON documents, replacing detected secrets with +// placeholders. It is safe for concurrent use as long as the Scanner is. +type Redactor struct { + scanner Scanner + pathFilter PathFilter + maxPasses int + placeholder func(ruleID string) string + isPlaceholder func(string) bool +} + +// Option customises a Redactor. +type Option func(*Redactor) + +// WithPathFilter restricts which string leaves may be rewritten. The default +// allows every leaf. +func WithPathFilter(f PathFilter) Option { + return func(r *Redactor) { + if f != nil { + r.pathFilter = f + } + } +} + +// WithMaxPasses bounds the convergence loop. +func WithMaxPasses(n int) Option { + return func(r *Redactor) { + if n > 0 { + r.maxPasses = n + } + } +} + +// WithPlaceholder overrides the replacement text derived from a rule id. +// +// The two functions are supplied together on purpose. Some rules match a +// position rather than a value, so they report a placeholder as though it were a +// secret; recognising our own output is what stops redaction from rewriting it +// and keeps redacting an already-redacted document a no-op. A format without a +// matching recogniser would silently lose that. +func WithPlaceholder(format func(ruleID string) string, matches func(string) bool) Option { + return func(r *Redactor) { + if format != nil { + r.placeholder = format + } + if matches != nil { + r.isPlaceholder = matches + } + } +} + +// New builds a Redactor around the given Scanner. +func New(s Scanner, opts ...Option) *Redactor { + r := &Redactor{ + scanner: s, + pathFilter: func(string) bool { return true }, + maxPasses: DefaultMaxPasses, + placeholder: DefaultPlaceholder, + isPlaceholder: IsDefaultPlaceholder, + } + for _, o := range opts { + o(r) + } + return r +} + +// DefaultPlaceholder is the replacement text for a redacted secret. It is +// deterministic, so redacting the same document twice yields the same digest, +// and it names the rule so a reviewer can tell what kind of credential was +// present without being able to recover it. +func DefaultPlaceholder(ruleID string) string { + if ruleID == "" { + return "[REDACTED]" + } + return "[REDACTED:" + ruleID + "]" +} + +// defaultPlaceholderPattern recognises the output of DefaultPlaceholder for any +// rule id, including ids this build has never seen. +var defaultPlaceholderPattern = regexp.MustCompile(`^\[REDACTED(?::[^\]\s]*)?\]$`) + +// IsDefaultPlaceholder reports whether s is a placeholder DefaultPlaceholder +// could have produced. +func IsDefaultPlaceholder(s string) bool { + return defaultPlaceholderPattern.MatchString(s) +} + +// Redact returns a copy of doc with every detected secret replaced. When +// nothing is detected the input is returned verbatim, so that a document +// without secrets keeps its original digest. +func (r *Redactor) Redact(ctx context.Context, doc []byte) ([]byte, *Report, error) { + if r.scanner == nil { + return nil, nil, errors.New("no scanner configured") + } + root, err := decodeObject(doc) + if err != nil { + return nil, nil, err + } + + report := &Report{ByRule: map[string]int{}, Unlocated: map[string]int{}} + // Secrets no eligible leaf contains, so that the loop stops chasing them. + skip := make(map[string]struct{}) + converged := false + + for pass := 1; pass <= r.maxPasses; pass++ { + report.Passes = pass + + // Indented rendering is what makes generic key/value rules work: it puts + // each pair on its own line, which is also the shape the scanner's own + // per-line deduplication assumes. + text, err := encode(root, true) + if err != nil { + return nil, nil, fmt.Errorf("rendering document: %w", err) + } + + findings, err := r.scanner.Scan(ctx, text) + if err != nil { + return nil, nil, fmt.Errorf("scanning for secrets: %w", err) + } + + pending := pendingSecrets(findings, skip, r.isPlaceholder) + if len(pending) == 0 { + converged = true + break + } + + w := &rewriter{ + pathFilter: r.pathFilter, + placeholder: r.placeholder, + secrets: pending, + byRule: map[string]int{}, + located: map[string]struct{}{}, + } + w.rewriteMap(root, "") + + for _, s := range pending { + if _, ok := w.located[s.secret]; !ok { + skip[s.secret] = struct{}{} + report.Unlocated[s.ruleID]++ + } + } + for rule, n := range w.byRule { + report.ByRule[rule] += n + } + report.Replacements += w.count + + if w.count == 0 { + // Nothing could be rewritten, so a further pass would see the same + // document and report the same findings. + converged = true + break + } + } + + if !converged { + return nil, nil, fmt.Errorf("%w after %d passes: a placeholder is likely being detected as a secret", ErrNotConverged, r.maxPasses) + } + + if report.Replacements == 0 { + // Hand back the original bytes: re-encoding would reorder object keys and + // change the artifact digest for no benefit. + return doc, report, nil + } + + out, err := encode(root, false) + if err != nil { + return nil, nil, fmt.Errorf("re-encoding redacted document: %w", err) + } + return []byte(out), report, nil +} + +// secretRule pairs a secret with the rule that matched it. +type secretRule struct { + secret string + ruleID string +} + +// pendingSecrets deduplicates findings and drops the ones the loop has decided +// not to chase. The result is ordered longest-secret-first so that a secret +// contained within a longer one cannot partially clobber it. +func pendingSecrets(findings []Finding, skip map[string]struct{}, isPlaceholder func(string) bool) []secretRule { + seen := make(map[string]struct{}, len(findings)) + out := make([]secretRule, 0, len(findings)) + + for _, f := range findings { + if f.Secret == "" { + continue + } + if _, skipped := skip[f.Secret]; skipped { + continue + } + // A finding whose whole secret is a placeholder is a rule matching the + // position it sits in, not a credential. Rewriting it would churn the + // document on every run and never terminate. + if isPlaceholder != nil && isPlaceholder(f.Secret) { + continue + } + if _, dup := seen[f.Secret]; dup { + continue + } + seen[f.Secret] = struct{}{} + out = append(out, secretRule{secret: f.Secret, ruleID: f.RuleID}) + } + + sort.Slice(out, func(i, j int) bool { + if len(out[i].secret) != len(out[j].secret) { + return len(out[i].secret) > len(out[j].secret) + } + return out[i].secret < out[j].secret + }) + return out +} + +// rewriter walks a decoded JSON value tree replacing secrets in eligible string +// leaves. +type rewriter struct { + pathFilter PathFilter + placeholder func(string) string + secrets []secretRule + byRule map[string]int + located map[string]struct{} + count int +} + +func (w *rewriter) rewriteMap(m map[string]any, path string) { + for k, child := range m { + m[k] = w.rewrite(child, path+"/"+k) + } +} + +func (w *rewriter) rewrite(node any, path string) any { + switch v := node.(type) { + case map[string]any: + w.rewriteMap(v, path) + return v + case []any: + for i, child := range v { + v[i] = w.rewrite(child, path+"/"+strconv.Itoa(i)) + } + return v + case string: + if !w.pathFilter(path) { + return v + } + return w.redactLeaf(v) + default: + // Numbers, booleans and null cannot carry a secret the scanner reported + // as a string. + return node + } +} + +// redactLeaf substitutes secrets inside a single string leaf. Matching happens +// against the leaf's JSON-encoded form, because that is the text the scanner +// saw: a secret containing a newline, for instance, reaches us as the two +// characters `\n`. +func (w *rewriter) redactLeaf(s string) string { + body, err := encodeStringBody(s) + if err != nil { + return s + } + + var ( + n int + lastRule string + ) + for _, sr := range w.secrets { + c := strings.Count(body, sr.secret) + if c == 0 { + continue + } + body = strings.ReplaceAll(body, sr.secret, w.placeholder(sr.ruleID)) + n += c + w.byRule[sr.ruleID] += c + w.located[sr.secret] = struct{}{} + lastRule = sr.ruleID + } + if n == 0 { + return s + } + w.count += n + + var out string + if err := json.Unmarshal([]byte(`"`+body+`"`), &out); err != nil { + // A replacement cut through a JSON escape sequence. Drop the whole leaf + // rather than risk leaving part of the secret behind. + return w.placeholder(lastRule) + } + return out +} + +// decodeObject parses doc into a value tree, keeping numbers in their original +// textual form so re-encoding does not reformat them. +func decodeObject(doc []byte) (map[string]any, error) { + // Checked before decoding, because decoding is what loses the information. + if err := rejectDuplicateKeys(doc); err != nil { + return nil, err + } + + dec := json.NewDecoder(bytes.NewReader(doc)) + dec.UseNumber() + + var root any + if err := dec.Decode(&root); err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidJSON, err) + } + + // Decoder.More is not a top-level end-of-input check: it reports false for a + // trailing "]" or "}", which would let a malformed document through. Require + // the input to be exhausted instead. + var trailing json.RawMessage + if err := dec.Decode(&trailing); !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("%w: unexpected trailing content", ErrInvalidJSON) + } + + obj, ok := root.(map[string]any) + if !ok { + return nil, fmt.Errorf("%w: expected an object at the document root", ErrInvalidJSON) + } + return obj, nil +} + +// jsonFrame tracks one open object or array while walking a token stream. +type jsonFrame struct { + isObject bool + keys map[string]struct{} + // expectKey is true when the next token in an object is a key rather than a + // value. + expectKey bool +} + +// rejectDuplicateKeys reports an error if any object in doc repeats a key. +// +// Decoding into a map keeps only the last value for a repeated key. A secret in +// an earlier one would therefore never reach the scanner, and because nothing +// was replaced the original bytes — secret included — would be handed back as +// "clean". Duplicate keys have no legitimate meaning in evidence, so the +// document is refused. +func rejectDuplicateKeys(doc []byte) error { + dec := json.NewDecoder(bytes.NewReader(doc)) + // Matching decodeObject: without this, Token parses numbers into float64 and + // a valid but out-of-range one such as 1e400 would fail the whole document + // here, before redaction ever runs. + dec.UseNumber() + + var stack []*jsonFrame + + top := func() *jsonFrame { + if len(stack) == 0 { + return nil + } + return stack[len(stack)-1] + } + + for { + token, err := dec.Token() + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return fmt.Errorf("%w: %w", ErrInvalidJSON, err) + } + + if delim, ok := token.(json.Delim); ok { + switch delim { + case '{': + stack = append(stack, &jsonFrame{isObject: true, keys: map[string]struct{}{}, expectKey: true}) + case '[': + stack = append(stack, &jsonFrame{}) + case '}', ']': + if len(stack) == 0 { + return fmt.Errorf("%w: unbalanced %q", ErrInvalidJSON, delim) + } + stack = stack[:len(stack)-1] + // The nested value is complete, so the parent object expects the + // next key. + if parent := top(); parent != nil && parent.isObject { + parent.expectKey = true + } + } + continue + } + + frame := top() + if frame == nil || !frame.isObject { + // A top-level scalar or an array element: no keys involved. + continue + } + + if !frame.expectKey { + frame.expectKey = true + continue + } + + key, _ := token.(string) + if _, duplicate := frame.keys[key]; duplicate { + return fmt.Errorf("%w: %q", ErrDuplicateKey, key) + } + frame.keys[key] = struct{}{} + frame.expectKey = false + } +} + +// encode serialises v. HTML escaping is disabled so transcript text keeps its +// angle brackets and ampersands instead of being mangled into \u sequences. +func encode(v any, indent bool) (string, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + if indent { + enc.SetIndent("", " ") + } + if err := enc.Encode(v); err != nil { + return "", err + } + return strings.TrimRight(buf.String(), "\n"), nil +} + +// encodeStringBody returns the JSON encoding of s without the surrounding +// quotes, using the same escaping rules as encode. +func encodeStringBody(s string) (string, error) { + encoded, err := encode(s, false) + if err != nil { + return "", err + } + if len(encoded) < 2 { + return "", fmt.Errorf("unexpected encoding %q for a string leaf", encoded) + } + return encoded[1 : len(encoded)-1], nil +} diff --git a/internal/redaction/redaction_test.go b/internal/redaction/redaction_test.go new file mode 100644 index 000000000..16e246cb8 --- /dev/null +++ b/internal/redaction/redaction_test.go @@ -0,0 +1,303 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redaction + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeScanner reports a fixed set of findings, so the engine's walk, rewrite and +// convergence behaviour can be tested without depending on the real ruleset. +type fakeScanner struct { + findings []Finding + // requirePresent only reports findings whose secret actually appears in the + // scanned text, which is how a real detector behaves. + requirePresent bool + calls int +} + +func (f *fakeScanner) Scan(ctx context.Context, text string) ([]Finding, error) { + f.calls++ + if err := ctx.Err(); err != nil { + return nil, err + } + if !f.requirePresent { + return f.findings, nil + } + var out []Finding + for _, fi := range f.findings { + if strings.Contains(text, fi.Secret) { + out = append(out, fi) + } + } + return out, nil +} + +func TestRedact(t *testing.T) { + testCases := []struct { + name string + doc string + findings []Finding + opts []Option + // reportAlways makes the fake scanner report its findings even when the + // secret does not appear in the scanned text, to exercise the engine's + // handling of findings it cannot attribute to any leaf. + reportAlways bool + wantErr error + wantUnchanged bool // output must be byte-identical to the input + wantReplacements int + wantByRule map[string]int + wantUnlocated map[string]int + mustNotContain []string + mustContain []string + }{ + { + name: "no findings returns the input verbatim", + doc: `{"data":{"a":"hello world","n":1}}`, + wantUnchanged: true, + }, + { + name: "secret in a nested leaf is replaced", + doc: `{"data":{"raw_session":{"main":[{"content":"run FAKE-AWS-KEY-NOT-A-REAL-PATTERN now"}]},"keep":"untouched"}}`, + findings: []Finding{{RuleID: "aws-access-token", Secret: "FAKE-AWS-KEY-NOT-A-REAL-PATTERN"}}, + wantReplacements: 1, + wantByRule: map[string]int{"aws-access-token": 1}, + mustNotContain: []string{"FAKE-AWS-KEY-NOT-A-REAL-PATTERN"}, + mustContain: []string{"[REDACTED:aws-access-token]", "untouched", "run ", " now"}, + }, + { + name: "same secret across three leaves", + doc: `{"a":"x SEC x","b":{"c":"SEC"},"d":["SEC"]}`, + findings: []Finding{{RuleID: "r1", Secret: "SEC"}}, + wantReplacements: 3, + wantByRule: map[string]int{"r1": 3}, + mustNotContain: []string{`"SEC"`}, + }, + { + name: "secret twice in one leaf", + doc: `{"a":"SEC and SEC again"}`, + findings: []Finding{{RuleID: "r1", Secret: "SEC"}}, + wantReplacements: 2, + wantByRule: map[string]int{"r1": 2}, + mustNotContain: []string{"SEC and"}, + }, + { + name: "escapes and non-ascii survive", + doc: `{"a":"line1\nSEC\ttab \"quoted\" café 🚀"}`, + findings: []Finding{{RuleID: "r1", Secret: "SEC"}}, + wantReplacements: 1, + mustNotContain: []string{`\u003c`, `"SEC`}, + mustContain: []string{`line1\n`, `\ttab`, `\"quoted\"`, "", "café", "🚀"}, + }, + { + name: "severed escape sequence drops the whole leaf", + doc: `{"a":"before\nSEC after"}`, + findings: []Finding{{RuleID: "r1", Secret: "nSEC"}}, + wantReplacements: 1, + mustNotContain: []string{"before", "after"}, + mustContain: []string{"[REDACTED:r1]"}, + }, + { + name: "protected path is left alone and recorded", + doc: `{"keepme":"SEC","other":"plain"}`, + findings: []Finding{{RuleID: "r1", Secret: "SEC"}}, + opts: []Option{WithPathFilter(func(p string) bool { + return p != "/keepme" + })}, + wantUnchanged: true, + wantUnlocated: map[string]int{"r1": 1}, + }, + { + name: "finding present nowhere is classified as an artifact", + doc: `{"a":"plain"}`, + findings: []Finding{{RuleID: "r1", Secret: "NOT-IN-DOC"}}, + reportAlways: true, + wantUnchanged: true, + wantUnlocated: map[string]int{"r1": 1}, + }, + { + name: "placeholder that keeps matching does not converge", + doc: `{"a":"SEC"}`, + findings: []Finding{{RuleID: "r1", Secret: "SEC"}}, + // A custom placeholder with no recogniser: the engine cannot tell its + // own output apart from a secret, so it rewrites it forever. This is + // the backstop that stops that being an infinite loop. + opts: []Option{WithPlaceholder(func(string) string { + return "SEC" + }, nil)}, + wantErr: ErrNotConverged, + }, + { + // 1e400 is valid JSON but out of float64 range: every decoder on the + // path has to keep numbers in their textual form, or an honest document + // gets refused over a number nobody was going to scan anyway. + name: "numbers keep their exact representation", + doc: `{"a":"SEC","big":12345678901234567890,"exp":1e10,"f":0.30000000000000004,"huge":1e400}`, + findings: []Finding{{RuleID: "r1", Secret: "SEC"}}, + wantReplacements: 1, + mustContain: []string{"12345678901234567890", "1e10", "0.30000000000000004", "1e400"}, + }, + { + name: "not json", + doc: `not json at all`, + wantErr: ErrInvalidJSON, + }, + { + name: "json array root is rejected", + doc: `["a"]`, + wantErr: ErrInvalidJSON, + }, + { + name: "json null root is rejected", + doc: `null`, + wantErr: ErrInvalidJSON, + }, + { + name: "trailing content is rejected", + doc: `{"a":"b"} trailing`, + wantErr: ErrInvalidJSON, + }, + { + // Decoding keeps only the last value for a repeated key, so the secret + // in the first one would never be scanned — and since nothing was + // replaced, the original bytes would be handed back as clean. + name: "duplicate key hiding a secret is refused", + doc: `{"a":"SEC","a":"clean"}`, + findings: []Finding{{RuleID: "r1", Secret: "SEC"}}, + wantErr: ErrDuplicateKey, + }, + { + name: "duplicate key nested in the transcript is refused", + doc: `{"data":{"raw_session":{"main":[{"content":"SEC","content":"clean"}]}}}`, + wantErr: ErrDuplicateKey, + }, + { + name: "repeating a key in a sibling object is fine", + doc: `{"a":{"k":"x"},"b":{"k":"y"},"c":[{"k":1},{"k":2}]}`, + // Same key name, different objects: nothing is lost, nothing to refuse. + wantUnchanged: true, + }, + { + // Decoder.More reports false for a trailing bracket, so this has to be + // caught by requiring the input to be exhausted. + name: "trailing closing bracket is rejected", + doc: `{"a":"b"}]`, + wantErr: ErrInvalidJSON, + }, + { + name: "trailing closing brace is rejected", + doc: `{"a":"b"}}`, + wantErr: ErrInvalidJSON, + }, + { + name: "a second document is rejected", + doc: `{"a":"b"}{"c":"d"}`, + wantErr: ErrInvalidJSON, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + scanner := &fakeScanner{findings: tc.findings, requirePresent: !tc.reportAlways} + r := New(scanner, tc.opts...) + + got, report, err := r.Redact(context.Background(), []byte(tc.doc)) + + if tc.wantErr != nil { + require.ErrorIs(t, err, tc.wantErr) + assert.Nil(t, got) + return + } + require.NoError(t, err) + require.NotNil(t, report) + + if tc.wantUnchanged { + assert.Equal(t, tc.doc, string(got), "output must be byte-identical") + assert.False(t, report.Changed()) + assert.Zero(t, report.Replacements) + } else { + assert.True(t, report.Changed()) + assert.Equal(t, tc.wantReplacements, report.Replacements) + assert.True(t, json.Valid(got), "output must be valid JSON") + } + + if tc.wantByRule != nil { + assert.Equal(t, tc.wantByRule, report.ByRule) + } + if tc.wantUnlocated != nil { + assert.Equal(t, tc.wantUnlocated, report.Unlocated) + } + for _, s := range tc.mustNotContain { + assert.NotContains(t, string(got), s) + } + for _, s := range tc.mustContain { + assert.Contains(t, string(got), s) + } + }) + } +} + +func TestRedactIsIdempotent(t *testing.T) { + docs := []string{ + `{"data":{"raw_session":{"main":[{"content":"FAKE-AWS-KEY-NOT-A-REAL-PATTERN"}]}}}`, + `{"a":"SEC","b":"no secret here"}`, + `{"a":"line\nSEC","b":["SEC","SEC"]}`, + } + findings := []Finding{ + {RuleID: "aws-access-token", Secret: "FAKE-AWS-KEY-NOT-A-REAL-PATTERN"}, + {RuleID: "r1", Secret: "SEC"}, + } + + for _, doc := range docs { + t.Run(doc, func(t *testing.T) { + r := New(&fakeScanner{findings: findings, requirePresent: true}) + + once, _, err := r.Redact(context.Background(), []byte(doc)) + require.NoError(t, err) + twice, report, err := r.Redact(context.Background(), once) + require.NoError(t, err) + + assert.Equal(t, string(once), string(twice)) + assert.False(t, report.Changed(), "a second pass must find nothing") + }) + } +} + +func TestRedactCancelledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + r := New(&fakeScanner{findings: []Finding{{RuleID: "r1", Secret: "SEC"}}, requirePresent: true}) + got, _, err := r.Redact(ctx, []byte(`{"a":"SEC"}`)) + + require.Error(t, err) + assert.ErrorIs(t, err, context.Canceled) + assert.Nil(t, got) +} + +// RuleIDs reports only the rules that actually redacted something, so it is +// safe to publish as a material annotation. +func TestReportRuleIDs(t *testing.T) { + r := &Report{ByRule: map[string]int{"b": 2, "a": 1}, Unlocated: map[string]int{"c": 1}} + assert.Equal(t, []string{"a", "b"}, r.RuleIDs()) + assert.Nil(t, (*Report)(nil).RuleIDs()) +} diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go index 7cc702a14..e5ae4259e 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go @@ -40,6 +40,10 @@ import ( const AnnotationPrefix = "chainloop." +// AnnotationValueTrue is the value of an annotation that acts as a flag. Shared +// so that whoever sets one and whoever reads it cannot drift apart. +const AnnotationValueTrue = "true" + var ( AnnotationMaterialType = CreateAnnotation("material.type") AnnotationMaterialName = CreateAnnotation("material.name") @@ -53,6 +57,22 @@ var ( AnnotationsSBOMMainComponentName = CreateAnnotation("material.sbom.main_component.name") AnnotationsSBOMMainComponentType = CreateAnnotation("material.sbom.main_component.type") AnnotationsSBOMMainComponentVersion = CreateAnnotation("material.sbom.main_component.version") + + // AnnotationMaterialRedacted marks a material whose stored content was + // transformed by its crafter before upload, to strip secrets out of it. Two + // things follow from it: the recorded digest describes the redacted artifact + // rather than the file on disk, and policy evaluation must read the untouched + // local file instead of the stored content (see GetEvaluableContent). + AnnotationMaterialRedacted = CreateAnnotation("material.redacted") + // AnnotationMaterialRedactionCount is how many secrets were replaced. + AnnotationMaterialRedactionCount = CreateAnnotation("material.redaction.count") + // AnnotationMaterialRedactionRules lists the detection rules that matched, + // so a policy can act on the kind of credential that was present. + AnnotationMaterialRedactionRules = CreateAnnotation("material.redaction.rules") + // AnnotationMaterialRedactionSkipped marks a material uploaded without + // redaction because the operator explicitly asked for it. Recorded so the + // bypass is visible to policies rather than silent. + AnnotationMaterialRedactionSkipped = CreateAnnotation("material.redaction.skipped") ) type NormalizedMaterialOutput struct { @@ -100,13 +120,25 @@ func (m *Attestation_Material) GetEvaluableContent(value string) ([]byte, error) } if artifact != nil { - if m.InlineCas { + // Policies must evaluate the artifact as it was produced. For a redacted + // material the inline bytes are the sanitized copy, so the untouched file + // on disk wins; without a local path there is nothing better to read. + // This keeps the policy input identical whatever CAS backend is in use, + // which matters most in dry runs, where the backend is always inline and + // is exactly where people test their policies. + useInlineContent := m.InlineCas + if m.GetAnnotations()[AnnotationMaterialRedacted] == AnnotationValueTrue && value != "" { + useInlineContent = false + } + + switch { + case useInlineContent: rawMaterial = artifact.GetContent() - } else if value == "" { + case value == "": return nil, errors.New("artifact path required") - } else if m.MaterialType != v1.CraftingSchema_Material_HELM_CHART && + case m.MaterialType != v1.CraftingSchema_Material_HELM_CHART && m.MaterialType != v1.CraftingSchema_Material_JUNIT_XML && - m.MaterialType != v1.CraftingSchema_Material_RADAMSA_CRASHES { + m.MaterialType != v1.CraftingSchema_Material_RADAMSA_CRASHES: // read content from local filesystem (except for tgz charts and // metadata-only materials like radamsa crashes) rawMaterial, err = os.ReadFile(value) diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go index 5e45f6f90..3df00d172 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go @@ -490,3 +490,132 @@ func TestTruffleHogCleanScanIsEvaluable(t *testing.T) { require.True(t, ok, "clean scan must project to an elements array") assert.Empty(t, elements, "a clean scan has zero findings") } + +// TestGetEvaluableContentRedactedPrefersDisk covers the interaction between an +// inline CAS backend and a crafter that redacted the content before storing it. +// Inline materials normally carry their own content, but a redacted material's +// inline bytes are the sanitized copy, so policies have to read the untouched +// file from disk instead. Without that, the policy input would silently differ +// between an inline backend and every other one. +func TestGetEvaluableContentRedactedPrefersDisk(t *testing.T) { + const ( + // Any distinguishable value works here; this test is about which source + // the content is read from, not about detection. + onDiskSecret = "the-unredacted-original" + inlineSecret = "[REDACTED:aws-access-token]" + + onDisk = `{"secret":"` + onDiskSecret + `"}` + inline = `{"secret":"` + inlineSecret + `"}` + ) + + diskPath := filepath.Join(t.TempDir(), "session.json") + require.NoError(t, os.WriteFile(diskPath, []byte(onDisk), 0o600)) + + testCases := []struct { + name string + inlineCas bool + redacted bool + path string + wantSecret string + wantErr bool + }{ + { + name: "inline without the marker keeps reading the inline content", + inlineCas: true, + path: diskPath, + wantSecret: inlineSecret, + }, + { + name: "inline and redacted reads the original from disk", + inlineCas: true, + redacted: true, + path: diskPath, + wantSecret: onDiskSecret, + }, + { + name: "inline and redacted with no path falls back to the inline content", + inlineCas: true, + redacted: true, + wantSecret: inlineSecret, + }, + { + name: "inline and redacted with an unreadable path fails loudly", + inlineCas: true, + redacted: true, + path: filepath.Join(t.TempDir(), "missing.json"), + // Silently falling back to the inline content here would point + // policies at redacted material without saying so. + wantErr: true, + }, + { + name: "non-inline is unaffected by the marker", + redacted: true, + path: diskPath, + wantSecret: onDiskSecret, + }, + { + name: "non-inline without the marker reads from disk as before", + path: diskPath, + wantSecret: onDiskSecret, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + InlineCas: tc.inlineCas, + M: &Attestation_Material_Artifact_{ + Artifact: &Attestation_Material_Artifact{ + Name: "session.json", + Digest: "sha256:deadbeef", + Content: []byte(inline), + }, + }, + } + if tc.redacted { + m.Annotations = map[string]string{AnnotationMaterialRedacted: "true"} + } + + content, err := m.GetEvaluableContent(tc.path) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // GetEvaluableContent also injects a chainloop_metadata block, so + // assert on the field that distinguishes the two sources. + var decoded map[string]any + require.NoError(t, json.Unmarshal(content, &decoded)) + assert.Equal(t, tc.wantSecret, decoded["secret"]) + }) + } +} + +// TestTruffleHogCleanScanIsEvaluableInline is the inline counterpart of +// TestTruffleHogCleanScanIsEvaluable. It pins down that the canonical empty +// content substituted for a zero-byte report projects the same either way, so +// preferring the disk path for redacted materials cannot regress it. +func TestTruffleHogCleanScanIsEvaluableInline(t *testing.T) { + m := &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_TRUFFLEHOG_JSON, + InlineCas: true, + M: &Attestation_Material_Artifact_{ + Artifact: &Attestation_Material_Artifact{ + Name: "secrets", + Digest: "sha256:deadbeef", + Content: []byte("[]"), + }, + }, + } + + content, err := m.GetEvaluableContent("testdata/trufflehog-clean-scan.jsonl") + require.NoError(t, err) + + var decoded map[string]any + require.NoError(t, json.NewDecoder(bytes.NewReader(content)).Decode(&decoded)) + elements, ok := decoded["elements"].([]any) + require.True(t, ok, "clean scan must project to an elements array") + assert.Empty(t, elements) +} diff --git a/pkg/attestation/crafter/crafter.go b/pkg/attestation/crafter/crafter.go index 2bf383ed5..5945ecd7a 100644 --- a/pkg/attestation/crafter/crafter.go +++ b/pkg/attestation/crafter/crafter.go @@ -77,6 +77,9 @@ type Crafter struct { // noStrictValidation skips strict schema validation noStrictValidation bool + // skipSecretRedaction stores evidence exactly as captured, without stripping + // secrets out of it first + skipSecretRedaction bool // collectors are auto-discovery collectors that run during attestation init collectors []Collector @@ -136,6 +139,16 @@ func WithNoStrictValidation(noStrictValidation bool) NewOpt { } } +// WithSkipSecretRedaction disables the redaction of secrets from evidence that +// supports it, storing the content exactly as captured. The bypass is recorded +// in the attestation so a policy can reject it. +func WithSkipSecretRedaction(skipSecretRedaction bool) NewOpt { + return func(c *Crafter) error { + c.skipSecretRedaction = skipSecretRedaction + return nil + } +} + // WorkingDir returns the working directory used for file discovery. func (c *Crafter) WorkingDir() string { return c.workingDir @@ -749,7 +762,8 @@ func (c *Crafter) stageMaterial(ctx context.Context, m *schemaapi.CraftingSchema // 3- Craft resulting material mt, err := materials.Craft(context.Background(), m, value, casBackend, c.ociRegistryAuth, c.Logger, &materials.CraftingOpts{ - NoStrictValidation: c.noStrictValidation, + NoStrictValidation: c.noStrictValidation, + SkipSecretRedaction: c.skipSecretRedaction, }) if err != nil { return nil, err diff --git a/pkg/attestation/crafter/materials/aicodingsession/redact.go b/pkg/attestation/crafter/materials/aicodingsession/redact.go new file mode 100644 index 000000000..bdfbf0499 --- /dev/null +++ b/pkg/attestation/crafter/materials/aicodingsession/redact.go @@ -0,0 +1,138 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aicodingsession + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/chainloop-dev/chainloop/internal/redaction" + "github.com/chainloop-dev/chainloop/internal/schemavalidators" +) + +// protectedPaths are the string leaves redaction must never rewrite: the +// identifiers, enumerations, timestamps and file paths that the platform joins +// records on and that policies read. "*" matches a single array index or object +// key. +// +// Everything not listed is eligible, which notably includes the whole +// raw_session transcript, warnings, the repository URL (a common place for +// embedded credentials), and any field a future schema version introduces. +// +// A deny-list rather than an allow-list is deliberate. An allow-list would +// silently stop scanning every newly added free-form field, and a silent gap in +// a redaction control is far worse than an over-eager match, which is at least +// counted in the report and published as an annotation. +var protectedPaths = []string{ + "/chainloop.material.evidence.id", + "/schema", + "/data/schema_version", + "/data/agent/name", + "/data/agent/version", + "/data/session/id", + "/data/session/slug", + "/data/session/started_at", + "/data/session/ended_at", + "/data/git_context/branch", + "/data/git_context/commit_start", + "/data/git_context/commit_end", + "/data/git_context/commits/*", + "/data/code_changes/files/*/path", + "/data/code_changes/files/*/status", + "/data/code_changes/files/*/attribution", + "/data/code_changes/files/*/session_ids/*", + "/data/model/primary", + "/data/model/provider", + "/data/model/models_used/*", + "/data/tools_used/summary/*/tool_name", + "/data/subagents/*/id", + "/data/subagents/*/type", +} + +// Redact removes detected secrets from an AI coding session evidence document. +// +// It returns the sanitised bytes and a summary of what was replaced. A document +// with no detected secrets is returned verbatim, so that the common case keeps +// its original digest. The result is guaranteed to still validate against the AI +// coding session schema; if it does not, redaction fails rather than uploading +// either an invalid document or an unredacted one. +func Redact(ctx context.Context, evidence []byte) ([]byte, *redaction.Report, error) { + scanner, err := redaction.DefaultScanner() + if err != nil { + return nil, nil, fmt.Errorf("initialising the secret scanner: %w", err) + } + + redacted, report, err := redaction.New(scanner, redaction.WithPathFilter(eligible)).Redact(ctx, evidence) + if err != nil { + return nil, nil, err + } + + if report.Changed() { + if err := validate(redacted); err != nil { + return nil, nil, fmt.Errorf("redacting secrets produced an invalid AI coding session: %w", err) + } + } + + return redacted, report, nil +} + +// eligible reports whether the string leaf at path may be rewritten. +func eligible(path string) bool { + for _, pattern := range protectedPaths { + if matchPath(pattern, path) { + return false + } + } + return true +} + +// matchPath compares a slash-separated path against a pattern in which "*" +// stands for exactly one segment. +func matchPath(pattern, path string) bool { + patternSegments := strings.Split(pattern, "/") + pathSegments := strings.Split(path, "/") + if len(patternSegments) != len(pathSegments) { + return false + } + for i, want := range patternSegments { + if want != "*" && want != pathSegments[i] { + return false + } + } + return true +} + +// validate re-checks the redacted document against the AI coding session schema, +// mirroring what the crafter validates on the way in. +func validate(evidence []byte) error { + var envelope struct { + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(evidence, &envelope); err != nil { + return fmt.Errorf("decoding the evidence envelope: %w", err) + } + + // Decoded generically because that is what the JSON schema validator + // consumes; the crafter keeps using the typed Data struct for everything else. + var data any + if err := json.Unmarshal(envelope.Data, &data); err != nil { + return fmt.Errorf("decoding the data field: %w", err) + } + + return schemavalidators.ValidateAICodingSession(data, schemavalidators.AICodingSessionVersion0_1) +} diff --git a/pkg/attestation/crafter/materials/aicodingsession/redact_test.go b/pkg/attestation/crafter/materials/aicodingsession/redact_test.go new file mode 100644 index 000000000..495447df1 --- /dev/null +++ b/pkg/attestation/crafter/materials/aicodingsession/redact_test.go @@ -0,0 +1,318 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aicodingsession + +import ( + "bytes" + "context" + "encoding/json" + "os" + "strconv" + "strings" + "testing" + + "github.com/chainloop-dev/chainloop/internal/redaction" + "github.com/chainloop-dev/chainloop/internal/schemavalidators" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Synthetic credentials for testdata/session-with-secrets.json. +// +// The fixture holds placeholders rather than the credentials themselves, and +// readFixture substitutes them. Nothing credential-shaped is committed: a +// realistic-looking secret in test data gets flagged by whatever scans the +// repository, and there has been one instance of each kind already — GitHub's +// push protection rejects the AWS patterns, and Checkov's basic-auth check +// rejects a token embedded in a URL. The repository URL is therefore substituted +// whole, since it is the "://user:pass@host" shape that is recognised rather +// than the token in it. +// +// The values are assembled from fragments for the same reason: joined, they +// would be flagged in this file instead. +const ( + fixtureAWSKey = "AKIA" + "4G7TI63VCBIRS4GW" + fixtureAWSSecret = "kQ7zXn2VbW9pLm4RtY6" + "uHs3JdF8gA1cE5oPzQwXn" + fixtureGitHubPAT = "ghp_erOZlZv0B1e3amrQ" + "ugdwZ8Ro2W4kDql9WPTf" + fixtureAnthropicKey = "sk-ant-api03-sT5wsx9DwmaHZDL0dUWKNhAhULxa35sUzyLFK9" + + "5QBTZMDJTYn8p0J7ZQbwpYGYCQeW5eXAAGtVSmhp7UO9vxHJtSBC0xpAA" + fixtureRepository = "https://oauth2:" + fixtureGitHubPAT + "@github.com/example/repo.git" +) + +// fixtureSecrets maps each placeholder in the session fixtures to the value the +// detector has to see. Keep in sync with the copy in the materials package, +// which needs the same substitution to reach the crafter through a real file. +var fixtureSecrets = map[string]string{ + "__AWS_ACCESS_KEY_ID__": fixtureAWSKey, + "__AWS_SECRET_ACCESS_KEY__": fixtureAWSSecret, + "__GITHUB_PAT__": fixtureGitHubPAT, + "__ANTHROPIC_API_KEY__": fixtureAnthropicKey, + "__GIT_REPOSITORY_WITH_CREDENTIALS__": fixtureRepository, +} + +// readFixture loads a session fixture with its credential placeholders resolved. +func readFixture(t *testing.T, path string) []byte { + t.Helper() + + content, err := os.ReadFile(path) + require.NoError(t, err) + + for placeholder, secret := range fixtureSecrets { + content = bytes.ReplaceAll(content, []byte(placeholder), []byte(secret)) + } + return content +} + +func TestEligible(t *testing.T) { + testCases := []struct { + path string + wantEligible bool + }{ + // Protected: identifiers, enums and timestamps the platform joins on. + {"/chainloop.material.evidence.id", false}, + {"/schema", false}, + {"/data/schema_version", false}, + {"/data/agent/name", false}, + {"/data/agent/version", false}, + {"/data/session/id", false}, + {"/data/session/slug", false}, + {"/data/session/started_at", false}, + {"/data/session/ended_at", false}, + {"/data/git_context/branch", false}, + {"/data/git_context/commit_start", false}, + {"/data/git_context/commit_end", false}, + {"/data/git_context/commits/0", false}, + {"/data/git_context/commits/17", false}, + {"/data/code_changes/files/0/path", false}, + {"/data/code_changes/files/3/status", false}, + {"/data/code_changes/files/3/attribution", false}, + {"/data/code_changes/files/3/session_ids/2", false}, + {"/data/model/primary", false}, + {"/data/model/provider", false}, + {"/data/model/models_used/1", false}, + {"/data/tools_used/summary/4/tool_name", false}, + {"/data/subagents/0/id", false}, + {"/data/subagents/0/type", false}, + + // Eligible: free-form text, wherever it lives. + {"/data/git_context/repository", true}, + {"/data/git_context/work_dir", true}, + {"/data/warnings/0", true}, + {"/data/subagents/0/description", true}, + {"/data/raw_session/main/0/message/content", true}, + {"/data/raw_session/reviewer/12/message/content/0/text", true}, + // A field a future schema version might add must be scanned by default. + {"/data/something_new", true}, + {"/data/session/some_new_free_text", true}, + // Near-misses on protected patterns must stay eligible. + {"/data/code_changes/files/0/path/0", true}, + {"/data/git_context/commits", true}, + {"/data/subagents/0", true}, + } + + for _, tc := range testCases { + t.Run(tc.path, func(t *testing.T) { + assert.Equal(t, tc.wantEligible, eligible(tc.path)) + }) + } +} + +func TestRedact(t *testing.T) { + testCases := []struct { + name string + file string + // wantUnchanged asserts the document is handed back byte-for-byte, so a + // session without secrets keeps its digest. + wantUnchanged bool + wantRules []string + wantByRule map[string]int + mustNotContain []string + }{ + { + name: "secrets across transcript threads, warnings and the repository URL", + file: "testdata/session-with-secrets.json", + wantRules: []string{"anthropic-api-key", "aws-access-token", "aws-secret-access-key", "github-pat"}, + // The AWS key id appears in both raw_session threads and in a subagent + // description, and is only detectable at all because its secret access + // key sits next to one of them; the PAT is in the repository URL and in + // a warning. Note that once a secret is detected anywhere it is removed + // everywhere it appears, including the two leaves where the composite + // rule would not have fired on its own. + wantByRule: map[string]int{ + "anthropic-api-key": 1, + "aws-access-token": 3, + "aws-secret-access-key": 1, + "github-pat": 2, + }, + mustNotContain: []string{fixtureAWSKey, fixtureAWSSecret, fixtureGitHubPAT, fixtureAnthropicKey}, + }, + { + name: "false-positive shaped content is left alone", + file: "testdata/session-fp-shaped.json", + wantUnchanged: true, + }, + { + name: "clean session", + file: "../testdata/ai-coding-session.json", + wantUnchanged: true, + }, + { + name: "minimal session without a raw_session", + file: "../testdata/ai-coding-session-minimal.json", + wantUnchanged: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + in := readFixture(t, tc.file) + + got, report, err := Redact(context.Background(), in) + require.NoError(t, err) + require.NotNil(t, report) + + if tc.wantUnchanged { + assert.Equal(t, string(in), string(got), "output must be byte-identical") + assert.False(t, report.Changed()) + return + } + + assert.True(t, report.Changed()) + assert.Equal(t, tc.wantRules, report.RuleIDs()) + assert.Equal(t, tc.wantByRule, report.ByRule) + assert.Empty(t, report.Unlocated) + for _, s := range tc.mustNotContain { + assert.NotContains(t, string(got), s) + } + + // The redacted document must still be a valid AI coding session. + require.NoError(t, validateEvidence(got)) + + // Protected leaves must survive verbatim. + for _, path := range []string{ + "chainloop.material.evidence.id", + "schema", + "data/schema_version", + "data/agent/name", + "data/session/id", + "data/session/started_at", + "data/git_context/commit_start", + "data/git_context/commit_end", + "data/model/primary", + "data/subagents/0/id", + "data/subagents/0/type", + "data/code_changes/files/0/path", + "data/tools_used/summary/0/tool_name", + } { + assert.Equal(t, jsonAt(t, in, path), jsonAt(t, got, path), "path %q must not change", path) + } + + // Surrounding prose in a redacted leaf must be preserved. + assert.Contains(t, string(got), "deploy this, use AWS_ACCESS_KEY_ID=") + assert.Contains(t, string(got), "[REDACTED:aws-access-token]") + assert.Contains(t, string(got), "and a block with \\\"quoted\\\" values. Done ✅") + }) + } +} + +func TestRedactIsIdempotent(t *testing.T) { + in := readFixture(t, "testdata/session-with-secrets.json") + + once, _, err := Redact(context.Background(), in) + require.NoError(t, err) + twice, report, err := Redact(context.Background(), once) + require.NoError(t, err) + + assert.Equal(t, string(once), string(twice)) + assert.False(t, report.Changed(), "a second pass must find nothing") +} + +func TestRedactRejectsInvalidInput(t *testing.T) { + _, _, err := Redact(context.Background(), []byte(`not json`)) + require.Error(t, err) +} + +// A duplicate key would let a secret ride along unscanned: decoding keeps only +// the last value, so the earlier one never reaches the scanner, and with nothing +// replaced the original bytes would be uploaded as clean. Redaction has to fail +// rather than pass the document through. +func TestRedactRejectsDuplicateKeys(t *testing.T) { + doc := []byte(`{ + "chainloop.material.evidence.id": "CHAINLOOP_AI_CODING_SESSION", + "schema": "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", + "data": { + "schema_version": "v1", + "agent": {"name": "claude-code"}, + "session": {"id": "abc", "started_at": "2026-03-25T15:10:49.161Z", "duration_seconds": 1}, + "raw_session": { + "main": [ + {"content": "AWS_ACCESS_KEY_ID=` + fixtureAWSKey + `", "content": "nothing here"} + ] + } + } + }`) + + out, _, err := Redact(context.Background(), doc) + + require.ErrorIs(t, err, redaction.ErrDuplicateKey) + assert.Nil(t, out, "nothing may be returned for a document that cannot be scanned") +} + +// validateEvidence mirrors the crafter's own validation of the data field. +func validateEvidence(doc []byte) error { + var envelope struct { + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(doc, &envelope); err != nil { + return err + } + var raw any + if err := json.Unmarshal(envelope.Data, &raw); err != nil { + return err + } + return schemavalidators.ValidateAICodingSession(raw, schemavalidators.AICodingSessionVersion0_1) +} + +// jsonAt resolves a slash-separated path against a JSON document and returns +// the value's canonical encoding, so two documents can be compared at that path. +// Slashes rather than dots because object keys here contain dots +// ("chainloop.material.evidence.id"). +func jsonAt(t *testing.T, doc []byte, path string) string { + t.Helper() + + var node any + require.NoError(t, json.Unmarshal(doc, &node)) + + for seg := range strings.SplitSeq(path, "/") { + switch v := node.(type) { + case map[string]any: + child, ok := v[seg] + require.True(t, ok, "missing key %q in path %q", seg, path) + node = child + case []any: + idx, err := strconv.Atoi(seg) + require.NoError(t, err, "bad index %q in path %q", seg, path) + require.Less(t, idx, len(v), "index out of range in path %q", path) + node = v[idx] + default: + require.Failf(t, "cannot descend", "into %T at %q in path %q", node, seg, path) + } + } + + out, err := json.Marshal(node) + require.NoError(t, err) + return string(out) +} diff --git a/pkg/attestation/crafter/materials/aicodingsession/testdata/session-fp-shaped.json b/pkg/attestation/crafter/materials/aicodingsession/testdata/session-fp-shaped.json new file mode 100644 index 000000000..2f780f9a3 --- /dev/null +++ b/pkg/attestation/crafter/materials/aicodingsession/testdata/session-fp-shaped.json @@ -0,0 +1,87 @@ +{ + "chainloop.material.evidence.id": "CHAINLOOP_AI_CODING_SESSION", + "schema": "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", + "data": { + "schema_version": "v1", + "agent": { + "name": "claude-code", + "version": "2.1.83" + }, + "session": { + "id": "3bf79921-3c03-81b6-afff-cb246849866f", + "slug": "memoized-purring-valley", + "started_at": "2026-03-25T15:10:49.161Z", + "ended_at": "2026-03-25T16:59:14.988Z", + "duration_seconds": 6505 + }, + "git_context": { + "repository": "git@github.com:chainloop-dev/chainloop.git", + "branch": "jiparis/pfm-6984-redact-secrets-in-session", + "work_dir": "/Users/jiparis/projects/chainloop", + "commit_start": "9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6", + "commit_end": "e25fa85e1c4b7a9d3f0821650cd7e4b39a1f8c2d", + "commits": [ + "9f8e7d6 perf(crafter): de-duplicate AccessChk security descriptors", + "e25fa85 Bump Helm Chart and Dagger Version" + ], + "commit_count": 2 + }, + "code_changes": { + "files_modified": 3, + "lines_added": 1204, + "lines_removed": 87, + "files": [ + {"path": "app/controlplane/internal/service/attestation.go", "status": "modified", "attribution": "ai"}, + {"path": "pkg/attestation/crafter/materials/aicodingsession/redact.go", "status": "created", "attribution": "ai", "session_ids": ["3bf79921-3c03-81b6-afff-cb246849866f"]} + ] + }, + "model": { + "primary": "claude-opus-5", + "provider": "anthropic", + "models_used": ["claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5-20251001"] + }, + "usage": { + "input_tokens": 63, + "output_tokens": 2989, + "total_tokens": 1234567890, + "cache_read_input_tokens": 933233, + "cache_creation_input_tokens": 47538, + "estimated_cost_usd": 0.30000000000000004 + }, + "tools_used": { + "summary": [ + {"tool_name": "Bash", "invocation_count": 13}, + {"tool_name": "Edit", "invocation_count": 4} + ], + "total_invocations": 17 + }, + "conversation": { + "total_messages": 87, + "user_messages": 34, + "assistant_messages": 36 + }, + "subagents": [ + { + "id": "ae0fbd58ba98882bb", + "type": "Explore", + "description": "Explore attestation crafter lifecycle", + "tokens": {"input": 73380, "output": 4096} + } + ], + "raw_session": { + "main": [ + { + "type": "user", + "message": {"role": "user", "content": "the sha256 digest is 9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6f9e8d7c6b5a4938271605f4e"}, + "timestamp": "2026-03-25T15:13:37.330Z" + }, + { + "type": "assistant", + "message": {"role": "assistant", "content": "Reading pkg/attestation/crafter/materials/aicodingsession/aicodingsession.go and running `go test ./... -run TestRedact`. Base64 of hello: aGVsbG8=. Escaped: & \"quotes\" and a tab\there."}, + "timestamp": "2026-03-25T15:13:40.100Z" + } + ] + }, + "warnings": [] + } +} diff --git a/pkg/attestation/crafter/materials/aicodingsession/testdata/session-with-secrets.json b/pkg/attestation/crafter/materials/aicodingsession/testdata/session-with-secrets.json new file mode 100644 index 000000000..3a6037023 --- /dev/null +++ b/pkg/attestation/crafter/materials/aicodingsession/testdata/session-with-secrets.json @@ -0,0 +1,90 @@ +{ + "chainloop.material.evidence.id": "CHAINLOOP_AI_CODING_SESSION", + "schema": "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", + "data": { + "schema_version": "v1", + "agent": { + "name": "claude-code", + "version": "2.1.83" + }, + "session": { + "id": "fa8acbe6-a176-4c2a-b51e-fd4541615eb5", + "slug": "stateful-wobbling-sutherland", + "started_at": "2026-03-25T15:10:49.161Z", + "ended_at": "2026-03-25T16:59:14.988Z", + "duration_seconds": 6505 + }, + "git_context": { + "repository": "__GIT_REPOSITORY_WITH_CREDENTIALS__", + "branch": "main", + "work_dir": "/home/user/repo", + "commit_start": "ae79df1aae6ef53fea636b4aad0a8c3d372178e9", + "commit_end": "5869b91e80fea5ac3d4502c728b118e85d65d825", + "commits": [ + "ae79df1 Add Go hello world application", + "5869b91 Add --bye flag" + ], + "commit_count": 2 + }, + "code_changes": { + "files_created": 2, + "lines_added": 36, + "files": [ + {"path": "go.mod", "status": "created"}, + {"path": "main.go", "status": "created"} + ] + }, + "model": { + "primary": "claude-opus-4-6", + "provider": "anthropic", + "models_used": ["claude-opus-4-6"] + }, + "usage": { + "input_tokens": 63, + "output_tokens": 2989, + "total_tokens": 3052, + "estimated_cost_usd": 0.8388 + }, + "tools_used": { + "summary": [ + {"tool_name": "Bash", "invocation_count": 13} + ], + "total_invocations": 13 + }, + "subagents": [ + { + "id": "agent-1", + "type": "general-purpose", + "description": "deploy with AWS_ACCESS_KEY_ID=__AWS_ACCESS_KEY_ID__", + "tokens": {"input": 10, "output": 20} + } + ], + "raw_session": { + "main": [ + { + "type": "user", + "message": {"role": "user", "content": "deploy this, use AWS_ACCESS_KEY_ID=__AWS_ACCESS_KEY_ID__\nAWS_SECRET_ACCESS_KEY=__AWS_SECRET_ACCESS_KEY__"}, + "timestamp": "2026-03-25T15:13:37.330Z" + }, + { + "type": "assistant", + "message": { + "role": "assistant", + "content": "I read .env and found:\nANTHROPIC_API_KEY=__ANTHROPIC_API_KEY__\nand a block with \"quoted\" values. Done ✅" + }, + "timestamp": "2026-03-25T15:13:40.100Z" + } + ], + "reviewer": [ + { + "type": "user", + "message": {"role": "user", "content": "re-run with AWS_ACCESS_KEY_ID=__AWS_ACCESS_KEY_ID__ please"}, + "timestamp": "2026-03-25T15:20:00.000Z" + } + ] + }, + "warnings": [ + "could not parse token __GITHUB_PAT__ from the environment" + ] + } +} diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go index 1192c238a..2305b6d21 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go @@ -20,8 +20,11 @@ import ( "encoding/json" "fmt" "os" + "strconv" + "strings" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/redaction" "github.com/chainloop-dev/chainloop/internal/schemavalidators" api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/aicodingsession" @@ -34,22 +37,42 @@ var annotationAICodingModel = api.CreateAnnotation("material.aiagent.model") type ChainloopAICodingSessionCrafter struct { *crafterCommon - backend *casclient.CASBackend + backend *casclient.CASBackend + skipRedaction bool +} + +// AICodingSessionCraftOpt tunes how an AI coding session is crafted. +type AICodingSessionCraftOpt func(*ChainloopAICodingSessionCrafter) + +// WithAICodingSessionSkipRedaction uploads the session exactly as captured, +// without stripping secrets out of it first. +func WithAICodingSessionSkipRedaction(skip bool) AICodingSessionCraftOpt { + return func(c *ChainloopAICodingSessionCrafter) { c.skipRedaction = skip } } // NewChainloopAICodingSessionCrafter generates a new CHAINLOOP_AI_CODING_SESSION material. // This material type contains AI coding session telemetry collected during attestation. -func NewChainloopAICodingSessionCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*ChainloopAICodingSessionCrafter, error) { +func NewChainloopAICodingSessionCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger, opts ...AICodingSessionCraftOpt) (*ChainloopAICodingSessionCrafter, error) { if schema.Type != schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION { return nil, fmt.Errorf("material type is not chainloop_ai_coding_session") } craftCommon := &crafterCommon{logger: l, input: schema} - return &ChainloopAICodingSessionCrafter{backend: backend, crafterCommon: craftCommon}, nil + c := &ChainloopAICodingSessionCrafter{backend: backend, crafterCommon: craftCommon} + for _, o := range opts { + o(c) + } + return c, nil } -// Craft validates the AI coding session against the JSON schema, calculates the digest, -// uploads it and returns the material definition. +// Craft validates the AI coding session against the JSON schema, redacts any +// secrets found in it, calculates the digest, uploads it and returns the +// material definition. +// +// Only the stored copy is redacted. The file on disk is left untouched, and it +// is what policies are evaluated against once this material has been staged, so +// a policy that looks for leaked credentials still sees what the agent actually +// captured. func (c *ChainloopAICodingSessionCrafter) Craft(ctx context.Context, artifactPath string) (*api.Attestation_Material, error) { f, err := os.ReadFile(artifactPath) if err != nil { @@ -83,11 +106,18 @@ func (c *ChainloopAICodingSessionCrafter) Craft(ctx context.Context, artifactPat return nil, fmt.Errorf("AI coding session validation failed: %w", err) } - material, err := uploadAndCraft(ctx, c.input, c.backend, artifactPath, c.logger) + craftOpts, report, err := c.redact(ctx, f) + if err != nil { + return nil, err + } + + material, err := uploadAndCraft(ctx, c.input, c.backend, artifactPath, c.logger, craftOpts...) if err != nil { return nil, err } + c.annotateRedaction(material, report) + // Surface agent name as an annotation if data.Agent.Name != "" { material.Annotations[annotationAIAgentName] = data.Agent.Name @@ -100,3 +130,63 @@ func (c *ChainloopAICodingSessionCrafter) Craft(ctx context.Context, artifactPat return material, nil } + +// redact strips secrets out of the session content, returning the options that +// make uploadAndCraft store the sanitized copy instead of the file on disk. +// +// Redaction fails closed: if the content cannot be scanned or the result no +// longer matches the schema, the material is not crafted at all rather than +// uploaded unscanned. The operator's escape hatch is --skip-secret-redaction, +// whose use is recorded in the attestation. +func (c *ChainloopAICodingSessionCrafter) redact(ctx context.Context, content []byte) ([]uploadAndCraftOption, *redaction.Report, error) { + if c.skipRedaction { + c.logger.Warn().Msg("secret redaction is DISABLED: the AI coding session will be stored exactly as captured") + return nil, nil, nil + } + + redacted, report, err := aicodingsession.Redact(ctx, content) + if err != nil { + return nil, nil, fmt.Errorf("redacting secrets from the AI coding session: %w", err) + } + + if !report.Changed() { + // Nothing was replaced, so upload the file as it is on disk and keep the + // digest reproducible from the source file. + return nil, report, nil + } + + return []uploadAndCraftOption{withContentOverride(redacted)}, report, nil +} + +// annotateRedaction records what redaction did, so that it is visible in the +// attestation and actionable by policies rather than an invisible rewrite. +func (c *ChainloopAICodingSessionCrafter) annotateRedaction(material *api.Attestation_Material, report *redaction.Report) { + if c.skipRedaction { + material.Annotations[api.AnnotationMaterialRedactionSkipped] = api.AnnotationValueTrue + return + } + + if report == nil { + return + } + + if len(report.Unlocated) > 0 { + // Detected in the document as a whole but not attributable to any + // rewritable field: either a protected field or a match spanning the + // boundary between two of them. + c.logger.Warn().Interface("rules", report.Unlocated). + Msg("some detected secrets could not be redacted") + } + + if !report.Changed() { + return + } + + rules := report.RuleIDs() + material.Annotations[api.AnnotationMaterialRedacted] = api.AnnotationValueTrue + material.Annotations[api.AnnotationMaterialRedactionCount] = strconv.Itoa(report.Replacements) + material.Annotations[api.AnnotationMaterialRedactionRules] = strings.Join(rules, ",") + + c.logger.Info().Int("count", report.Replacements).Strs("rules", rules). + Msg("redacted secrets from the AI coding session before upload") +} diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session_redaction_test.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session_redaction_test.go new file mode 100644 index 000000000..ca6697b68 --- /dev/null +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session_redaction_test.go @@ -0,0 +1,299 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package materials + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "io" + "os" + "path/filepath" + "strconv" + "testing" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/casclient" + mUploader "github.com/chainloop-dev/chainloop/pkg/casclient/mocks" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// TestUploadAndCraftContentOverride covers the seam redaction relies on: the +// stored bytes are the substituted ones, and every derived field (digest, size, +// uploaded body) describes them rather than the file on disk. +func TestUploadAndCraftContentOverride(t *testing.T) { + const ( + onDisk = `{"original":"content that is definitely longer"}` + redacted = `{"original":"[REDACTED]"}` + ) + + testCases := []struct { + name string + override []byte + skipUpload bool + maxSize int64 + wantErr string + wantStored string + wantUpload bool + }{ + { + name: "no override stores the file on disk", + wantStored: onDisk, + wantUpload: true, + }, + { + name: "override stores the substituted bytes", + override: []byte(redacted), + wantStored: redacted, + wantUpload: true, + }, + { + name: "an empty override is rejected rather than stored", + override: []byte{}, + wantErr: "file is empty", + }, + { + // The CAS stores the substituted content, so its size is the one that + // has to fit the backend. + name: "the size limit applies to the override", + override: []byte(`{"original":"padded out to be much larger than the limit"}`), + maxSize: 20, + wantErr: "too big", + }, + { + name: "skip upload still records the override digest", + override: []byte(redacted), + skipUpload: true, + wantStored: redacted, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logger := zerolog.Nop() + path := filepath.Join(t.TempDir(), "session.json") + require.NoError(t, os.WriteFile(path, []byte(onDisk), 0o600)) + + schema := &schemaapi.CraftingSchema_Material{ + Name: "test", + Type: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + SkipUpload: tc.skipUpload, + } + + backend := &casclient.CASBackend{Name: "test", MaxSize: tc.maxSize} + var uploaded []byte + if tc.wantUpload { + uploader := mUploader.NewUploader(t) + uploader.On("Upload", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + var err error + uploaded, err = io.ReadAll(args.Get(1).(io.Reader)) + require.NoError(t, err) + }). + Return(&casclient.UpDownStatus{Digest: "deadbeef"}, nil) + backend.Uploader = uploader + } + + var opts []uploadAndCraftOption + if tc.override != nil { + opts = append(opts, withContentOverride(tc.override)) + } + + got, err := uploadAndCraft(context.TODO(), schema, backend, path, &logger, opts...) + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + return + } + require.NoError(t, err) + + // The recorded filename must be unaffected by the substitution. + assert.Equal(t, "session.json", got.GetArtifact().Name) + assert.Equal(t, strconv.Itoa(len(tc.wantStored)), got.Annotations[AnnotationMaterialSize]) + assert.Equal(t, sha256Digest(tc.wantStored), got.GetArtifact().Digest) + + if tc.wantUpload { + assert.Equal(t, tc.wantStored, string(uploaded)) + assert.True(t, got.UploadedToCas) + } else { + assert.False(t, got.UploadedToCas) + } + }) + } +} + +// The credentials the session fixture carries as placeholders. Nothing +// credential-shaped is committed, and these are assembled from fragments for the +// same reason: a realistic-looking secret gets flagged by whatever scans the +// repository, whether that is GitHub's push protection or Checkov. See the +// fixtureSecrets comment in the aicodingsession package. +const ( + awsKey = "AKIA" + "4G7TI63VCBIRS4GW" + awsSecret = "kQ7zXn2VbW9pLm4RtY6" + "uHs3JdF8gA1cE5oPzQwXn" + githubPAT = "ghp_erOZlZv0B1e3amrQ" + "ugdwZ8Ro2W4kDql9WPTf" + anthropicKey = "sk-ant-api03-sT5wsx9DwmaHZDL0dUWKNhAhULxa35sUzyLFK9" + "5QBTZMDJTYn8p0J7ZQbwpYGYCQeW5eXAAGtVSmhp7UO9vxHJtSBC0xpAA" + repositoryURL = "https://oauth2:" + githubPAT + "@github.com/example/repo.git" +) + +// Keep in sync with fixtureSecrets in the aicodingsession package. +var fixtureSecrets = map[string]string{ + "__AWS_ACCESS_KEY_ID__": awsKey, + "__AWS_SECRET_ACCESS_KEY__": awsSecret, + "__GITHUB_PAT__": githubPAT, + "__ANTHROPIC_API_KEY__": anthropicKey, + "__GIT_REPOSITORY_WITH_CREDENTIALS__": repositoryURL, +} + +// materializeFixture writes a copy of a session fixture with its credential +// placeholders resolved, and returns its path. The crafter reads the artifact +// from disk, so the substitution has to land in a real file. +func materializeFixture(t *testing.T, src string) string { + t.Helper() + + content, err := os.ReadFile(src) + require.NoError(t, err) + + for placeholder, secret := range fixtureSecrets { + content = bytes.ReplaceAll(content, []byte(placeholder), []byte(secret)) + } + + path := filepath.Join(t.TempDir(), filepath.Base(src)) + require.NoError(t, os.WriteFile(path, content, 0o600)) + return path +} + +// TestChainloopAICodingSessionCrafterRedaction is the end-to-end guarantee: the +// bytes that leave the machine carry no secrets, while the file on disk — what +// policies are evaluated against afterwards — is left untouched. +func TestChainloopAICodingSessionCrafterRedaction(t *testing.T) { + const ( + withSecrets = "./aicodingsession/testdata/session-with-secrets.json" + clean = "./testdata/ai-coding-session.json" + ) + + testCases := []struct { + name string + filePath string + skipRedaction bool + inlineBackend bool + wantRedacted bool + wantCount string + wantRules string + }{ + { + name: "secrets are stripped before upload", + filePath: withSecrets, + wantRedacted: true, + wantCount: "7", + wantRules: "anthropic-api-key,aws-access-token,aws-secret-access-key,github-pat", + }, + { + // An inline backend embeds the content into the attestation itself, + // so it matters most there that the redacted copy is the stored one. + name: "an inline backend embeds the redacted copy", + filePath: withSecrets, + inlineBackend: true, + wantRedacted: true, + wantCount: "7", + wantRules: "anthropic-api-key,aws-access-token,aws-secret-access-key,github-pat", + }, + { + name: "the opt-out is recorded in the attestation", + filePath: withSecrets, + skipRedaction: true, + }, + { + name: "a clean session is not marked as redacted", + filePath: clean, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logger := zerolog.Nop() + schema := &schemaapi.CraftingSchema_Material{ + Name: "test", + Type: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + } + + path := materializeFixture(t, tc.filePath) + original, err := os.ReadFile(path) + require.NoError(t, err) + + // A nil Uploader is what the CLI builds for an inline CAS backend. + backend := &casclient.CASBackend{Name: "not-set"} + var stored []byte + if !tc.inlineBackend { + uploader := mUploader.NewUploader(t) + uploader.On("Upload", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + var readErr error + stored, readErr = io.ReadAll(args.Get(1).(io.Reader)) + require.NoError(t, readErr) + }). + Return(&casclient.UpDownStatus{Digest: "deadbeef"}, nil) + backend.Uploader = uploader + } + + crafter, err := NewChainloopAICodingSessionCrafter(schema, backend, &logger, + WithAICodingSessionSkipRedaction(tc.skipRedaction)) + require.NoError(t, err) + + got, err := crafter.Craft(context.TODO(), path) + require.NoError(t, err) + + if tc.inlineBackend { + require.True(t, got.InlineCas) + stored = got.GetArtifact().Content + } + + assert.Equal(t, tc.wantCount, got.Annotations[api.AnnotationMaterialRedactionCount]) + assert.Equal(t, tc.wantRules, got.Annotations[api.AnnotationMaterialRedactionRules]) + + switch { + case tc.wantRedacted: + assert.Equal(t, "true", got.Annotations[api.AnnotationMaterialRedacted]) + assert.NotContains(t, string(stored), awsKey) + assert.Contains(t, string(stored), "[REDACTED:aws-access-token]") + // The digest describes the redacted artifact, not the source file. + assert.NotEqual(t, sha256Digest(string(original)), got.GetArtifact().Digest) + case tc.skipRedaction: + assert.Equal(t, "true", got.Annotations[api.AnnotationMaterialRedactionSkipped]) + assert.Contains(t, string(stored), awsKey) + assert.Equal(t, sha256Digest(string(original)), got.GetArtifact().Digest) + default: + assert.NotContains(t, got.Annotations, api.AnnotationMaterialRedacted) + assert.NotContains(t, got.Annotations, api.AnnotationMaterialRedactionSkipped) + // Nothing to redact, so the digest stays reproducible from the file. + assert.Equal(t, sha256Digest(string(original)), got.GetArtifact().Digest) + } + + // Redaction must never touch the file the policies will read. + stillOnDisk, err := os.ReadFile(path) + require.NoError(t, err) + assert.Equal(t, string(original), string(stillOnDisk)) + }) + } +} + +func sha256Digest(content string) string { + sum := sha256.Sum256([]byte(content)) + return "sha256:" + hex.EncodeToString(sum[:]) +} diff --git a/pkg/attestation/crafter/materials/gitleaks.go b/pkg/attestation/crafter/materials/gitleaks.go index 68f40018d..a6cbff237 100644 --- a/pkg/attestation/crafter/materials/gitleaks.go +++ b/pkg/attestation/crafter/materials/gitleaks.go @@ -21,11 +21,11 @@ import ( "fmt" "os" + "github.com/betterleaks/betterleaks/report" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" "github.com/chainloop-dev/chainloop/pkg/casclient" "github.com/rs/zerolog" - "github.com/zricethezav/gitleaks/v8/report" ) type GitleaksReportCrafter struct { diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index 89337c6c0..df117989a 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strconv" "strings" "time" @@ -165,6 +166,15 @@ type uploadAndCraftOpts struct { // digest, which would collide across unrelated empty materials of any type. // The original filename is preserved. emptyContentFallback []byte + + // contentOverride, when set, is stored and uploaded instead of the bytes on + // disk. It exists for crafters that must transform an artifact before it + // leaves the machine, such as redacting secrets out of an AI coding session. + // The recorded digest, size and uploaded (or inline) body then all describe + // the transformed content, which is the only content that ever reaches the + // CAS. The file on disk is left untouched, and remains what policies are + // evaluated against. The original filename is preserved. + contentOverride []byte } type uploadAndCraftOption func(*uploadAndCraftOpts) @@ -175,6 +185,12 @@ func withEmptyContentFallback(content []byte) uploadAndCraftOption { return func(o *uploadAndCraftOpts) { o.emptyContentFallback = content } } +// withContentOverride stores and uploads the given bytes instead of the file on +// disk, preserving the artifact's filename. +func withContentOverride(content []byte) uploadAndCraftOption { + return func(o *uploadAndCraftOpts) { o.contentOverride = content } +} + // uploadAndCraft uploads the artifact to CAS and crafts the material // this function is used by all the uploadable artifacts crafters (SBOMs, JUnit, and more in the future) func uploadAndCraft(ctx context.Context, input *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, artifactPath string, l *zerolog.Logger, opts ...uploadAndCraftOption) (*api.Attestation_Material, error) { @@ -183,8 +199,8 @@ func uploadAndCraft(ctx context.Context, input *schemaapi.CraftingSchema_Materia o(options) } - // 1 - Check the file can be stored in the provided CAS backend - result, err := fileStats(artifactPath) + // 1 - Resolve the content to store and check it fits the provided CAS backend + result, err := resolveContent(artifactPath, options.contentOverride) if err != nil { return nil, fmt.Errorf("getting file stats: %w", err) } @@ -209,7 +225,8 @@ func uploadAndCraft(ctx context.Context, input *schemaapi.CraftingSchema_Materia l.Debug().Str("filename", result.filename).Str("digest", result.digest).Str("path", artifactPath). Str("size", bytefmt.ByteSize(uint64(result.size))). Str("max_size", bytefmt.ByteSize(uint64(backend.MaxSize))). - Str("backend", backend.Name).Bool("skip_upload", shouldSkipUpload).Msg("crafting file") + Str("backend", backend.Name).Bool("skip_upload", shouldSkipUpload). + Bool("content_override", options.contentOverride != nil).Msg("crafting file") // If there is a max size set and the file is bigger than that, return an error // Only check size if we're actually going to upload (not skipped) @@ -239,8 +256,8 @@ func uploadAndCraft(ctx context.Context, input *schemaapi.CraftingSchema_Materia case backend.Uploader != nil: l.Debug().Str("backend", backend.Name).Msg("uploading") - // Reuse the already-open, already-hashed reader from fileStats - // to avoid a redundant SHA256 pass inside Uploader.UploadFile. + // Reuse the already-hashed reader resolved above to avoid a + // redundant SHA256 pass inside Uploader.UploadFile. _, err = backend.Uploader.Upload(ctx, result.r, result.filename, result.digest) if err != nil { return nil, fmt.Errorf("%w: %w", ErrBaseUploadAndCraft, fmt.Errorf("uploading material: %w", err)) @@ -272,6 +289,17 @@ type fileInfo struct { r io.ReadCloser } +// resolveContent returns the content to be stored for an artifact: normally the +// file on disk, or the bytes a crafter substituted for it. fileStats derives the +// filename from the path with os.FileInfo.Name, which is already the base name, +// so both branches record the same artifact name. +func resolveContent(artifactPath string, override []byte) (*fileInfo, error) { + if override != nil { + return fileStatsFromBytes(filepath.Base(artifactPath), override) + } + return fileStats(artifactPath) +} + // Returns the sha256 hash of the file, its size and an error func fileStats(filepath string) (*fileInfo, error) { stat, err := os.Stat(filepath) @@ -302,8 +330,9 @@ func fileStats(filepath string) (*fileInfo, error) { } // fileStatsFromBytes builds a fileInfo from in-memory content, preserving the -// given filename. Used to craft a canonical representation when the on-disk -// file is empty (see uploadAndCraft's emptyContentFallback). +// given filename. Used when the stored bytes differ from the bytes on disk: to +// craft a canonical representation of an empty file (emptyContentFallback), or +// to store a transformed copy of the artifact (contentOverride). func fileStatsFromBytes(filename string, content []byte) (*fileInfo, error) { hash, _, err := cr_v1.SHA256(bytes.NewReader(content)) if err != nil { @@ -324,6 +353,9 @@ type Craftable interface { // CraftingOpts contains options for crafting materials type CraftingOpts struct { NoStrictValidation bool + // SkipSecretRedaction stores evidence exactly as captured, without stripping + // secrets out of it first. Only material types that redact honour it. + SkipSecretRedaction bool } //nolint:gocyclo @@ -403,7 +435,7 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia case schemaapi.CraftingSchema_Material_CHAINLOOP_AI_AGENT_CONFIG: crafter, err = NewChainloopAIAgentConfigCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION: - crafter, err = NewChainloopAICodingSessionCrafter(materialSchema, casBackend, logger) + crafter, err = NewChainloopAICodingSessionCrafter(materialSchema, casBackend, logger, WithAICodingSessionSkipRedaction(opts.SkipSecretRedaction)) case schemaapi.CraftingSchema_Material_OPENAPI_SPEC: crafter, err = NewOpenAPICrafter(materialSchema, casBackend, logger, WithOpenAPINoStrictValidation(opts.NoStrictValidation)) case schemaapi.CraftingSchema_Material_ASYNCAPI_SPEC: