diff --git a/go.mod b/go.mod index 75b98686..01293ab0 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,19 @@ module github.com/overmindtech/cli -go 1.26.2 +go 1.26.3 replace github.com/anthropics/anthropic-sdk-go => github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 -// Address an incompatibility between buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go and the kubernetes modules. -// See https://github.com/overmindtech/workspace/pull/1124 and https://github.com/kubernetes/apiserver/issues/116 -replace github.com/google/cel-go => github.com/google/cel-go v0.22.1 - // Carry the pool.acquire/prepare span removal patch on our fork while exaring/otelpgx#76 is in review. // Drop this once upstream merges and tags a release. -replace github.com/exaring/otelpgx => github.com/overmindtech/otelpgx v0.10.0 +// +// Pinned to a pseudo-version on the `remove-acquire-prepare-spans-upstream` branch — DO NOT bump +// to a tag. Tags on the fork mirror upstream `main` and do NOT contain the patch. See the +// Renovate `enabled: false` rule for this module in .github/renovate.json. +replace github.com/exaring/otelpgx => github.com/overmindtech/otelpgx v0.0.0-20260518092812-9a74fcacfd49 + +// Fix security issue; force upgrade even though the terraform libraries have not been updated yet. +replace github.com/go-git/go-git/v5 => github.com/go-git/go-git/v5 v5.19.1 require ( atomicgo.dev/keyboard v0.2.10 @@ -23,11 +26,11 @@ require ( cloud.google.com/go/bigquery v1.77.0 cloud.google.com/go/bigtable v1.47.0 cloud.google.com/go/certificatemanager v1.14.0 - cloud.google.com/go/compute v1.62.0 + cloud.google.com/go/compute v1.63.0 cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/container v1.51.0 + cloud.google.com/go/container v1.52.0 cloud.google.com/go/dataplex v1.34.0 - cloud.google.com/go/dataproc/v2 v2.21.0 + cloud.google.com/go/dataproc/v2 v2.22.0 cloud.google.com/go/eventarc v1.23.0 cloud.google.com/go/filestore v1.15.0 cloud.google.com/go/functions v1.24.0 @@ -43,9 +46,9 @@ require ( cloud.google.com/go/secretmanager v1.20.0 cloud.google.com/go/securitycentermanagement v1.6.0 cloud.google.com/go/spanner v1.91.0 - cloud.google.com/go/storage v1.62.1 + cloud.google.com/go/storage v1.62.2 cloud.google.com/go/storagetransfer v1.18.0 - connectrpc.com/connect v1.18.1 // v1.19.0 was faulty, wait until it is above this version + connectrpc.com/connect v1.20.0 // v1.19.0 was faulty, wait until it is above this version github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 @@ -66,28 +69,28 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3 v3.0.0 github.com/Masterminds/semver/v3 v3.5.0 github.com/MrAlias/otel-schema-utils v0.4.0-alpha - github.com/auth0/go-jwt-middleware/v3 v3.1.0 + github.com/auth0/go-jwt-middleware/v3 v3.2.0 github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.17 github.com/aws/aws-sdk-go-v2/credentials v1.19.16 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 - github.com/aws/aws-sdk-go-v2/service/apigateway v1.39.3 + github.com/aws/aws-sdk-go-v2/service/apigateway v1.40.0 github.com/aws/aws-sdk-go-v2/service/autoscaling v1.66.2 - github.com/aws/aws-sdk-go-v2/service/cloudfront v1.63.0 + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.64.0 github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.57.0 github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.17 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.301.0 - github.com/aws/aws-sdk-go-v2/service/ecs v1.79.1 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.303.0 + github.com/aws/aws-sdk-go-v2/service/ecs v1.80.0 github.com/aws/aws-sdk-go-v2/service/efs v1.41.16 - github.com/aws/aws-sdk-go-v2/service/eks v1.83.0 + github.com/aws/aws-sdk-go-v2/service/eks v1.84.0 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.25 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.12 github.com/aws/aws-sdk-go-v2/service/iam v1.53.10 - github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 + github.com/aws/aws-sdk-go-v2/service/kms v1.52.0 github.com/aws/aws-sdk-go-v2/service/lambda v1.90.1 github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.60.1 - github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.10 + github.com/aws/aws-sdk-go-v2/service/networkmanager v1.42.0 github.com/aws/aws-sdk-go-v2/service/rds v1.118.2 github.com/aws/aws-sdk-go-v2/service/route53 v1.62.7 github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 @@ -103,6 +106,7 @@ require ( github.com/getsentry/sentry-go v0.46.2 github.com/go-jose/go-jose/v4 v4.1.4 github.com/google/btree v1.1.3 + github.com/google/cel-go v0.28.1 // indirect github.com/google/uuid v1.6.0 github.com/googleapis/gax-go/v2 v2.22.0 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e @@ -112,6 +116,7 @@ require ( github.com/hashicorp/terraform-plugin-framework v1.19.0 github.com/hashicorp/terraform-plugin-go v0.31.0 github.com/hashicorp/terraform-plugin-testing v1.16.0 + github.com/jackc/pgx/v5 v5.9.2 github.com/jedib0t/go-pretty/v6 v6.7.10 github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/micahhausler/aws-iam-policy v0.4.4 @@ -119,11 +124,9 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/reflow v0.3.0 github.com/nats-io/jwt/v2 v2.8.1 - github.com/nats-io/nats-server/v2 v2.14.0 + github.com/nats-io/nats-server/v2 v2.14.1 github.com/nats-io/nats.go v1.52.0 github.com/nats-io/nkeys v0.4.15 - github.com/onsi/ginkgo/v2 v2.28.3 // indirect - github.com/onsi/gomega v1.40.0 // indirect github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c @@ -144,25 +147,27 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 + go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/net v0.53.0 + golang.org/x/net v0.55.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/text v0.36.0 + golang.org/x/text v0.37.0 gonum.org/v1/gonum v0.17.0 - google.golang.org/api v0.278.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 - google.golang.org/grpc v1.81.0 - google.golang.org/protobuf v1.36.11 + google.golang.org/api v0.280.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 + google.golang.org/grpc v1.81.1 + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/ini.v1 v1.67.2 - k8s.io/api v0.35.4 - k8s.io/apimachinery v0.35.4 - k8s.io/client-go v0.35.4 + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/client-go v0.36.1 sigs.k8s.io/kind v0.31.0 sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect ) @@ -222,7 +227,7 @@ require ( github.com/containerd/console v1.0.5 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect @@ -239,7 +244,6 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.28.0 // indirect github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -269,9 +273,11 @@ require ( github.com/hashicorp/terraform-svchost v0.2.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect @@ -312,7 +318,6 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect - github.com/stoewer/go-strcase v1.3.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/valyala/fastjson v1.6.7 // indirect @@ -330,17 +335,16 @@ require ( go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect go.opentelemetry.io/otel/log v0.11.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/schema v0.0.12 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/sys v0.43.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect - golang.org/x/term v0.42.0 // indirect + golang.org/x/term v0.43.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect @@ -349,9 +353,8 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go.sum b/go.sum index d39414c7..fd447e54 100644 --- a/go.sum +++ b/go.sum @@ -30,18 +30,18 @@ cloud.google.com/go/bigtable v1.47.0 h1:NGLgDSr/i79BTGCjxH/maPKxyvl5q8/SsBsyLK52 cloud.google.com/go/bigtable v1.47.0/go.mod h1:GUM6PdkG3rrDse9kugqvX5+ktwo3ldfLtLi1VFn5Wj4= cloud.google.com/go/certificatemanager v1.14.0 h1:31fCXgMFDLSXh9HeF2M6hLE+dPF/1UFyIJXLmqpr41g= cloud.google.com/go/certificatemanager v1.14.0/go.mod h1:QOA8qRoM6/Ik03+srLnBykenGTy0fk78dnPcx5ZWOW8= -cloud.google.com/go/compute v1.62.0 h1:tJ7lKJ8YEVa6vZX03Jc8o1YePbjKDOQhDw1BscMZ1bs= -cloud.google.com/go/compute v1.62.0/go.mod h1:Xm6PbsLgBpAg4va77ljbBdpMjzuU+uPp5Ze2dnZq7lw= +cloud.google.com/go/compute v1.63.0 h1:KsBourH0wajM4RhzwPwRMKbxHVdvzGsk7StvACoWXD8= +cloud.google.com/go/compute v1.63.0/go.mod h1:Xm6PbsLgBpAg4va77ljbBdpMjzuU+uPp5Ze2dnZq7lw= 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/container v1.51.0 h1:KdeWHqwlOvABdhkhSTSb4QFySMOuXnc3UUoQMmq9lic= -cloud.google.com/go/container v1.51.0/go.mod h1:EvqoT2eXfxLweXXUlhAMGR0sOAB00XPzEjoL01esSDs= +cloud.google.com/go/container v1.52.0 h1:oAkZciqdQ+xpP29pa5UJfsBBvcWrNNgKsWvraoz9Ajk= +cloud.google.com/go/container v1.52.0/go.mod h1:EvqoT2eXfxLweXXUlhAMGR0sOAB00XPzEjoL01esSDs= cloud.google.com/go/datacatalog v1.27.0 h1:AnghhtHKCqYIe62gTPHcn9nJr5jtxjZHV4D/Fob23gg= cloud.google.com/go/datacatalog v1.27.0/go.mod h1:YTI11pFlC5HCj4CphEf+qWCy/z9udd7o0HVN6c2Povg= cloud.google.com/go/dataplex v1.34.0 h1:WXf+qC/Qhrq6B91HoXYcZJEv1nrLkFpM0HV+JX2SdPs= cloud.google.com/go/dataplex v1.34.0/go.mod h1:sOazL+Bs/PTxiMHQ5yBboBvEW9qPrpGogx3+RAgfIt8= -cloud.google.com/go/dataproc/v2 v2.21.0 h1:sSyWnDNpcPlSJpNYqfdCAC/i9J4WzO5aLn+9624I6hg= -cloud.google.com/go/dataproc/v2 v2.21.0/go.mod h1:oARVSa38kAHvSuG+cozsrY2sE6UajGuvOOf9vS+ADHI= +cloud.google.com/go/dataproc/v2 v2.22.0 h1:ypUlQKOHMHGv8FQCCNYd0XyM6tAaMDdbcSFBcjYWhbg= +cloud.google.com/go/dataproc/v2 v2.22.0/go.mod h1:oARVSa38kAHvSuG+cozsrY2sE6UajGuvOOf9vS+ADHI= cloud.google.com/go/eventarc v1.23.0 h1:/EUAdoBWSlqQRbpQYTV2Msmg4esw3Mum3tEU7zkhLi4= cloud.google.com/go/eventarc v1.23.0/go.mod h1:tIJL0hoWtZXVa5MjcAep/4xB+AXz4AbqQV14ogX5VwU= cloud.google.com/go/filestore v1.15.0 h1:ZYFAnP4elMogIQAXFwPx4nKpcvY0dJOZV+zl2l50MGQ= @@ -74,14 +74,14 @@ cloud.google.com/go/securitycentermanagement v1.6.0 h1:1oH0hTRrwS0Er8QVhrUR2ScQN cloud.google.com/go/securitycentermanagement v1.6.0/go.mod h1:2vT5sKJSeclefx8yku77inS/bAyEiLH9n1CHpshtDMQ= cloud.google.com/go/spanner v1.91.0 h1:XwXfcZ0kc1NT9Uu2IsThFiWtYptB+WgLn/KZEZcyzRg= cloud.google.com/go/spanner v1.91.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= -cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8= -cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= +cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4= +cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= cloud.google.com/go/storagetransfer v1.18.0 h1:Y8kA7TiPPjiQH7Xsuf2KlBAJd7Jcn5J8aR5ABO81p/g= cloud.google.com/go/storagetransfer v1.18.0/go.mod h1:AbGutEym/KNasoiDpSj/CYbigp5yhgosSgwlhGvQNs4= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= -connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= -connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= @@ -181,8 +181,8 @@ github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/auth0/go-jwt-middleware/v3 v3.1.0 h1:1aqVJA9K0+B6hP6qqMjTsJUk/L14sJSUjiTGW2/mY64= -github.com/auth0/go-jwt-middleware/v3 v3.1.0/go.mod h1:BBZCQAXmqC/QfwzWyHOqF/kwN4C66eMeayy9QS6TgT4= +github.com/auth0/go-jwt-middleware/v3 v3.2.0 h1:OP0/YH89A+w03zOjuRPPgKh5S+1+uAmY/vtllYUSWCM= +github.com/auth0/go-jwt-middleware/v3 v3.2.0/go.mod h1:/f0hy3exUWxL7/4XJ1oSHBDSBf2Os2C1VT2RkQ9frs0= github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= @@ -199,26 +199,26 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueO github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= -github.com/aws/aws-sdk-go-v2/service/apigateway v1.39.3 h1:7MJlB7KGFd+KNKtnPgoFWYf52PGO1pd+1VHp10lNKhI= -github.com/aws/aws-sdk-go-v2/service/apigateway v1.39.3/go.mod h1:MwilTAruv11x8EFjsk1R0VfjMdCxB6JHVtanCqsTR5o= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.40.0 h1:+s2yzvSERu63KCzhRQopwqfKcOYKMbP1LAw0RgR2PRM= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.40.0/go.mod h1:MwilTAruv11x8EFjsk1R0VfjMdCxB6JHVtanCqsTR5o= github.com/aws/aws-sdk-go-v2/service/autoscaling v1.66.2 h1:pPd+/Ujqf2+DmPOdB47EN7ox1iC21lu2zlOccUlfHeo= github.com/aws/aws-sdk-go-v2/service/autoscaling v1.66.2/go.mod h1:b3XHAIEe5I9cmeZ9MLvUqj5DRWcBuh1/hpKDPb7T6KE= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.63.0 h1:YNpvY2mAGhoTHPEvsqPuDb8K5JBjorbVMaitP8i4OXI= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.63.0/go.mod h1:brhMG/gR2xEB5lezxL2Cx+hqsEzGUn4LhNUtu7+ePFE= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.64.0 h1:a6XmNe8cAvfrXVKwjXzWl9HHtuyE/n4kBroNm2mSOyo= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.64.0/go.mod h1:brhMG/gR2xEB5lezxL2Cx+hqsEzGUn4LhNUtu7+ePFE= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.57.0 h1:dlkFtYOrwOuM7IIBD6FPLtt0Xvnph+8hqmmbzyowkCk= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.57.0/go.mod h1:7900IH3EvTrwNGLNx3QDKnQwPF/Cw+pD9cuvBDQ4org= github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.17 h1:fkeDjhbAy9ddanOVlxP2vnY2dbTxA8HL+DdV9HezVSs= github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.17/go.mod h1:kzj2OFWYl3uGXBkincAArVPtSG8QwXJRfCL8+Ztsw9o= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3 h1:XgjzLEE8CrNYnr4Xmi1W5PfKsKMjp4Pu1rWkJNO43JI= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3/go.mod h1:r7sfLXEN8RUA89tAHy1E7lCtVOOWIkqVy/FbnUdxW1E= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.301.0 h1:U+cZAMc8mN0jne8/ae7KrrFuILTXrZReAvc6BIpXGls= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.301.0/go.mod h1:Y95W0Hm6FYLPa6o0hbnJ+sWgmdc4ifcLFjGkdobWVhY= -github.com/aws/aws-sdk-go-v2/service/ecs v1.79.1 h1:tQNU4tC4cMoZo1e+7J8j3/GWM7PJFdXCN0VzEFwFqUE= -github.com/aws/aws-sdk-go-v2/service/ecs v1.79.1/go.mod h1:TIKZ9zIFS6W2k9FeW+r5sGVnlxp+aUt9oQ/St3Suj1o= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.303.0 h1:qkTLlFVQDSk0tbOqn49pxZjIVY2jy3n0FBXh+PphNkk= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.303.0/go.mod h1:Y95W0Hm6FYLPa6o0hbnJ+sWgmdc4ifcLFjGkdobWVhY= +github.com/aws/aws-sdk-go-v2/service/ecs v1.80.0 h1:orZYOYtvYU7A45XTWC/UchWSkkdWjOyXi8MYksnmQf0= +github.com/aws/aws-sdk-go-v2/service/ecs v1.80.0/go.mod h1:TIKZ9zIFS6W2k9FeW+r5sGVnlxp+aUt9oQ/St3Suj1o= github.com/aws/aws-sdk-go-v2/service/efs v1.41.16 h1:qHmh61/S6g+scI9M4U3XYivCiEp1tUadKgyrczuLJpM= github.com/aws/aws-sdk-go-v2/service/efs v1.41.16/go.mod h1:Q7WcY1H6krqZEnFyxyuzfLAnEad1Q69U4CrBbY4P2Fg= -github.com/aws/aws-sdk-go-v2/service/eks v1.83.0 h1:mS5rkyFt+NYryy0p4n8o80tJjBmXiQrRCQjP8jZcSLY= -github.com/aws/aws-sdk-go-v2/service/eks v1.83.0/go.mod h1:JQcyECIV9iZHm+GMrWn1pTPTJYRavOVsqPvlCbjt+Fg= +github.com/aws/aws-sdk-go-v2/service/eks v1.84.0 h1:U9HMTDPdZtkCOTE8ACbHQJmXGBKP7/mBds7M1JbUZH0= +github.com/aws/aws-sdk-go-v2/service/eks v1.84.0/go.mod h1:JQcyECIV9iZHm+GMrWn1pTPTJYRavOVsqPvlCbjt+Fg= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.25 h1:VzmoYPRbNSUqk3pA04ZyGZUg52yfX259XXRqwr1lns4= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.25/go.mod h1:r7chQGimOmFs4oqawhO+i+o3ez2l69rzAco5KTb7bjY= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.12 h1:TJXv7kZjdXA2maPDaJFFEQPBrPmvPtMybN3qYDOpJ4Y= @@ -235,14 +235,14 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/ku github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= -github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY= -github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI= +github.com/aws/aws-sdk-go-v2/service/kms v1.52.0 h1:QNtg+Mtj1zmepk568+UKBD5DFfqh+ESTUUqQT27JkQc= +github.com/aws/aws-sdk-go-v2/service/kms v1.52.0/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI= github.com/aws/aws-sdk-go-v2/service/lambda v1.90.1 h1:odCeJgHXfQoXEWQUIzPkKvsJTWcLMsaOWowNpovPFFw= github.com/aws/aws-sdk-go-v2/service/lambda v1.90.1/go.mod h1:NbtJVztitG7JkuoI4GSrDUlsB32zeXqKBvXj6bUxcMo= github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.60.1 h1:acbBwzoZSM3oet/FcUNddED5V7zBauXiRxsD2NJcD70= github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.60.1/go.mod h1:oWCet/AjsuKhMkvcXOGEeS2QmssLJX1UmX2SiKCEsFM= -github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.10 h1:fZdjuh4szziSdwiDhUT2xexjJ21sehyDU88mkUjw0KQ= -github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.10/go.mod h1:x0O7AHep2gwquyfW6gmNql2OM4LEloyJGFflJfEJV+U= +github.com/aws/aws-sdk-go-v2/service/networkmanager v1.42.0 h1:xWEOUBxKqNCR3qkUsmYOPUyW7ZlyWzKX6s0f81PW60A= +github.com/aws/aws-sdk-go-v2/service/networkmanager v1.42.0/go.mod h1:x0O7AHep2gwquyfW6gmNql2OM4LEloyJGFflJfEJV+U= github.com/aws/aws-sdk-go-v2/service/rds v1.118.2 h1:pkEeQneYFpTAnGhyqSbyp/DlCPPJTGt0GkWahlLYzMA= github.com/aws/aws-sdk-go-v2/service/rds v1.118.2/go.mod h1:7gS+cGrKF0mH253QHFlStmx79ws+DlNk+04ZRfmw3U0= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.7 h1:twRRMmtSITnt/rrp+D7UDLzE5pKMZe759aalkUdN+OY= @@ -324,8 +324,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= @@ -357,8 +357,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= -github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= -github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= 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= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -372,8 +372,6 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= @@ -391,8 +389,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40= -github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= +github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= +github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -406,8 +404,6 @@ 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/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-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -485,6 +481,14 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -501,8 +505,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -581,8 +585,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= -github.com/nats-io/nats-server/v2 v2.14.0 h1:+8q0HrDFotwLLcGH/legOEOnowunhK+aZ4GYBIWpQlM= -github.com/nats-io/nats-server/v2 v2.14.0/go.mod h1:ImVUUDvfClJbb6cuJQRc1VmgDCXKM5ds0OoiG9MVOKo= +github.com/nats-io/nats-server/v2 v2.14.1 h1:wXs/a5fw9Hzm3CvuzLxGeIwpjPulSa7gMT3eSuhGkcg= +github.com/nats-io/nats-server/v2 v2.14.1/go.mod h1:4N17zLpuS7WMbG8T9gsE2B7z9hC9PraPyulVBfpK6nU= github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= @@ -591,10 +595,6 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= -github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= -github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= -github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= -github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd h1:UuQycBx6K0lB0/IfHePshOYjlrptkF4FoApFP2Y4s3k= github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd/go.mod h1:391Ww1JbjG4FHOlvQqCd6n25CCCPE64JzC5cCYPxhyM= github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297 h1:ih4bqBMHTCtg3lMwJszNkMGO9n7Uoe0WX5be1/x+s+g= @@ -656,8 +656,6 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= -github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= -github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -665,10 +663,10 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -764,21 +762,21 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= 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= @@ -801,23 +799,23 @@ 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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= 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.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -832,8 +830,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 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.278.0 h1:W7jiRvRi53VYFfZ/HoZjQBtJk7gOFbHD8ot1RzVZU6E= -google.golang.org/api v0.278.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= +google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk= +google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= @@ -841,14 +839,14 @@ google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgn google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= -google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -865,16 +863,16 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988= -k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU= -k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds= -k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= -k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8= -k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= diff --git a/go/auth/middleware.go b/go/auth/middleware.go index 12b45b49..397b46d7 100644 --- a/go/auth/middleware.go +++ b/go/auth/middleware.go @@ -167,9 +167,17 @@ func WithAnyScope(scopes []string, next http.Handler) http.Handler { }) } -var ErrNoClaims = errors.New("error extracting claims from token") +var ( + ErrNoClaims = errors.New("error extracting claims from token") + ErrEmptyAccountName = errors.New("account_name custom claim is empty") +) -// ExtractAccount Extracts the account name from a context +// ExtractAccount Extracts the account name from a context. Returns +// ErrNoClaims if the context has no custom claims attached, and +// ErrEmptyAccountName if the claim is present but blank. An empty +// account_name is a valid SQL value and would silently land tenant data in a +// "no-tenant" bucket, so callers must treat it as an authentication failure +// rather than a usable identifier. func ExtractAccount(ctx context.Context) (string, error) { claims := ctx.Value(CustomClaimsContextKey{}) @@ -177,7 +185,12 @@ func ExtractAccount(ctx context.Context) (string, error) { return "", ErrNoClaims } - return claims.(*CustomClaims).AccountName, nil + accountName := claims.(*CustomClaims).AccountName + if accountName == "" { + return "", ErrEmptyAccountName + } + + return accountName, nil } // NewAuthMiddleware Creates new auth middleware. The options allow you to diff --git a/go/auth/middleware_test.go b/go/auth/middleware_test.go index f6359e27..7351060e 100644 --- a/go/auth/middleware_test.go +++ b/go/auth/middleware_test.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "crypto/rsa" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -568,6 +569,56 @@ func TestWithSubject(t *testing.T) { }) } +func TestExtractAccount(t *testing.T) { + t.Parallel() + + t.Run("returns ErrNoClaims when no claims on context", func(t *testing.T) { + t.Parallel() + + accountName, err := ExtractAccount(context.Background()) + if !errors.Is(err, ErrNoClaims) { + t.Errorf("expected ErrNoClaims, got %v", err) + } + if accountName != "" { + t.Errorf("expected empty account name, got %q", accountName) + } + }) + + t.Run("returns ErrEmptyAccountName when claim is present but blank", func(t *testing.T) { + t.Parallel() + + ctx := context.WithValue(context.Background(), CustomClaimsContextKey{}, &CustomClaims{ + Scope: "api:read", + AccountName: "", + }) + + accountName, err := ExtractAccount(ctx) + if !errors.Is(err, ErrEmptyAccountName) { + t.Errorf("expected ErrEmptyAccountName, got %v", err) + } + if accountName != "" { + t.Errorf("expected empty account name, got %q", accountName) + } + }) + + t.Run("returns account_name when claim is populated", func(t *testing.T) { + t.Parallel() + + ctx := context.WithValue(context.Background(), CustomClaimsContextKey{}, &CustomClaims{ + Scope: "api:read", + AccountName: "tenant-a", + }) + + accountName, err := ExtractAccount(ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if accountName != "tenant-a" { + t.Errorf("expected %q, got %q", "tenant-a", accountName) + } + }) +} + func TestOverrideAuth(t *testing.T) { tests := []struct { Name string diff --git a/go/auth/principal.go b/go/auth/principal.go new file mode 100644 index 00000000..b6b7d6a2 --- /dev/null +++ b/go/auth/principal.go @@ -0,0 +1,82 @@ +package auth + +import ( + "context" + "errors" + "fmt" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/overmindtech/cli/go/tracing" + "go.opentelemetry.io/otel/attribute" +) + +// PrincipalResolver maps the calling Auth0 subject + account_name to the +// caller's principal_id UUID. It is the one method ResolvePrincipalID needs +// from the principals aggregate; brent-backend's *principals.Store satisfies +// this interface via a thin wrapper around the sqlc-generated query. +// +// The interface is single-method by design so test fakes can satisfy it +// without dragging in the full sqlc query surface. +type PrincipalResolver interface { + ResolvePrincipalIDBySubject(ctx context.Context, accountName, subject string) (uuid.UUID, error) +} + +var ( + // ErrSubjectMissing is returned when the caller's context has no Auth0 + // subject. Treat as an authentication failure (the auth middleware should + // have populated this before the handler ran). + ErrSubjectMissing = errors.New("auth: no Auth0 subject on context") + + // ErrAccountMissing is returned when the caller's context has no + // account_name. Same severity as ErrSubjectMissing. + ErrAccountMissing = errors.New("auth: no account_name on context") + + // ErrPrincipalNotProvisioned is returned when the caller is authenticated + // but has no principal_identities row in the calling account. Handlers + // map this to the voice-guide "please visit brent.ai to finish setup" + // error so the user is nudged through ProvisionCurrentPrincipal. + ErrPrincipalNotProvisioned = errors.New("auth: caller has no principal yet (visit brent.ai to finish setup)") +) + +// ResolvePrincipalID converts the calling JWT's Auth0 subject + account +// into the caller's principal_id UUID. The missing-principal branch returns +// ErrPrincipalNotProvisioned; the handler maps that to the voice-guide +// "please visit brent.ai to finish setup" error. +// +// This is the single funnel every authenticated MCP / Connect handler in +// brent-backend uses to convert auth.CurrentSubjectContextKey{} to the +// stable principal_id UUID before calling any aggregate. +// +// In-process system callers (workflow-agent tool wrappers, etc.) must seed +// a real principal_identities row for their synthetic subject (e.g. the +// brent agent's `'brent-agent'` subject is created during onboarding in +// production and by test helpers in integration tests). +func ResolvePrincipalID(ctx context.Context, r PrincipalResolver) (uuid.UUID, error) { + ctx, span := tracing.Tracer().Start(ctx, "auth.resolve_principal_id") + defer span.End() + + subject, _ := ctx.Value(CurrentSubjectContextKey{}).(string) + if subject == "" { + return uuid.Nil, ErrSubjectMissing + } + account, _ := ctx.Value(AccountNameContextKey{}).(string) + if account == "" { + return uuid.Nil, ErrAccountMissing + } + + span.SetAttributes( + attribute.String("ovm.auth.accountName", account), + attribute.String("ovm.auth.subject", subject), + ) + + id, err := r.ResolvePrincipalIDBySubject(ctx, account, subject) + switch { + case errors.Is(err, pgx.ErrNoRows): + return uuid.Nil, ErrPrincipalNotProvisioned + case err != nil: + return uuid.Nil, fmt.Errorf("resolve principal id: %w", err) + } + span.SetAttributes(attribute.String("ovm.auth.principalId", id.String())) + return id, nil +} diff --git a/go/auth/principal_test.go b/go/auth/principal_test.go new file mode 100644 index 00000000..2922d4b5 --- /dev/null +++ b/go/auth/principal_test.go @@ -0,0 +1,141 @@ +package auth + +import ( + "context" + "errors" + "testing" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" +) + +// fakePrincipalResolver is a (account, subject) → principal_id map. It +// satisfies the PrincipalResolver interface so ResolvePrincipalID can be +// exercised without a real database. +type fakePrincipalResolver struct { + byAccountSubject map[string]map[string]uuid.UUID + err error +} + +func (f *fakePrincipalResolver) ResolvePrincipalIDBySubject(_ context.Context, accountName, subject string) (uuid.UUID, error) { + if f.err != nil { + return uuid.Nil, f.err + } + if subs, ok := f.byAccountSubject[accountName]; ok { + if id, ok := subs[subject]; ok { + return id, nil + } + } + return uuid.Nil, pgx.ErrNoRows +} + +// ctxWithIdentity wires the two values ResolvePrincipalID reads off +// context: the Auth0 subject and the account_name. +func ctxWithIdentity(subject, account string) context.Context { + ctx := context.Background() + if subject != "" { + ctx = context.WithValue(ctx, CurrentSubjectContextKey{}, subject) + } + if account != "" { + ctx = context.WithValue(ctx, AccountNameContextKey{}, account) + } + return ctx +} + +func TestResolvePrincipalID_CrossTenantIsolation(t *testing.T) { + t.Parallel() + + // The same Auth0 subject lands in two different accounts. The + // resolver must return two different principal IDs — the central + // guarantee that powers every tenant-scoped read/write in + // brent-backend (see .cursor/skills/sql-multi-tenant-safety). + sharedSubject := "auth0|alice" + tenantAPID := uuid.New() + tenantBPID := uuid.New() + + resolver := &fakePrincipalResolver{ + byAccountSubject: map[string]map[string]uuid.UUID{ + "tenant-a": {sharedSubject: tenantAPID}, + "tenant-b": {sharedSubject: tenantBPID}, + }, + } + + gotA, err := ResolvePrincipalID(ctxWithIdentity(sharedSubject, "tenant-a"), resolver) + if err != nil { + t.Fatalf("tenant-a resolve: %v", err) + } + if gotA != tenantAPID { + t.Fatalf("tenant-a: got %s, want %s", gotA, tenantAPID) + } + + gotB, err := ResolvePrincipalID(ctxWithIdentity(sharedSubject, "tenant-b"), resolver) + if err != nil { + t.Fatalf("tenant-b resolve: %v", err) + } + if gotB != tenantBPID { + t.Fatalf("tenant-b: got %s, want %s", gotB, tenantBPID) + } + + if gotA == gotB { + t.Fatalf("cross-tenant leak: same principal_id %s returned for both accounts", gotA) + } +} + +func TestResolvePrincipalID_MissingSubjectOrAccount(t *testing.T) { + t.Parallel() + + resolver := &fakePrincipalResolver{} + + cases := []struct { + name string + ctx context.Context + wantErr error + }{ + {"no subject", ctxWithIdentity("", "tenant-a"), ErrSubjectMissing}, + {"no account", ctxWithIdentity("auth0|alice", ""), ErrAccountMissing}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, err := ResolvePrincipalID(tc.ctx, resolver) + if !errors.Is(err, tc.wantErr) { + t.Fatalf("got %v, want %v", err, tc.wantErr) + } + }) + } +} + +func TestResolvePrincipalID_NotProvisioned(t *testing.T) { + t.Parallel() + + // pgx.ErrNoRows on the underlying query means the caller authenticated + // successfully but has no principal_identities row in this account + // yet — the canonical "please visit brent.ai to finish setup" branch. + resolver := &fakePrincipalResolver{ + byAccountSubject: map[string]map[string]uuid.UUID{}, // empty: every lookup misses. + } + + _, err := ResolvePrincipalID(ctxWithIdentity("auth0|alice", "tenant-a"), resolver) + if !errors.Is(err, ErrPrincipalNotProvisioned) { + t.Fatalf("got %v, want ErrPrincipalNotProvisioned", err) + } +} + +func TestResolvePrincipalID_UnderlyingError(t *testing.T) { + t.Parallel() + + // Non-ErrNoRows errors from the resolver propagate wrapped so the + // handler can log them — they're real database failures, not a + // missing-principal condition. + boom := errors.New("connection refused") + resolver := &fakePrincipalResolver{err: boom} + + _, err := ResolvePrincipalID(ctxWithIdentity("auth0|alice", "tenant-a"), resolver) + if err == nil { + t.Fatalf("expected wrapped error, got nil") + } + if !errors.Is(err, boom) { + t.Fatalf("expected %v in chain, got %v", boom, err) + } +} diff --git a/go/sdp-go/sdpws/messagehandler.go b/go/sdp-go/sdpws/messagehandler.go index 3e9d348a..c6b5a484 100644 --- a/go/sdp-go/sdpws/messagehandler.go +++ b/go/sdp-go/sdpws/messagehandler.go @@ -188,18 +188,76 @@ func (s *StoreEverythingHandler) NewEdge(ctx context.Context, edge *sdp.Edge) { s.Edges = append(s.Edges, edge) } -var _ GatewayMessageHandler = (*WaitForAllQueriesHandler)(nil) +// StoreItemsOnlyHandler stores items but discards edges. Use when only the +// candidate item list is consumed after the wait; this avoids retaining the +// edge slice, which can be tens of MB at ThousandEyes-shaped loads. +// +// Items-only by design — if you need edges, add a callback or pick +// StoreEverythingHandler. +type StoreItemsOnlyHandler struct { + Items []*sdp.Item + + NoopGatewayMessageHandler +} + +var _ GatewayMessageHandler = (*StoreItemsOnlyHandler)(nil) + +func (s *StoreItemsOnlyHandler) NewItem(ctx context.Context, item *sdp.Item) { + s.Items = append(s.Items, item) +} -// A Handler that waits for all queries to be done then calls a callback +// WaitForAllQueriesHandler signals completion when all queries are done but +// retains no items or edges. Use this when the caller observes items and edges +// via callbacks (e.g. an atomic counter or a per-event override). This is the +// safe default; pick a storing variant deliberately when you need the slices. type WaitForAllQueriesHandler struct { - // A callback that will be called when all queries are done + // A callback that will be called when all queries are done. DoneCallback func() - StoreEverythingHandler + NoopGatewayMessageHandler } +var _ GatewayMessageHandler = (*WaitForAllQueriesHandler)(nil) + func (w *WaitForAllQueriesHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { if status.Done() { w.DoneCallback() } } + +// WaitForAllQueriesItemsOnlyHandler signals completion when all queries are +// done and stores the items the gateway sends, but discards edges. +type WaitForAllQueriesItemsOnlyHandler struct { + // A callback that will be called when all queries are done. + DoneCallback func() + + StoreItemsOnlyHandler +} + +var _ GatewayMessageHandler = (*WaitForAllQueriesItemsOnlyHandler)(nil) + +func (w *WaitForAllQueriesItemsOnlyHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { + if status.Done() { + w.DoneCallback() + } +} + +// WaitForAllQueriesStoreEverythingHandler signals completion when all queries +// are done and retains every item and edge until the caller reads them after +// the wait. Memory cost is O(items + edges) per client; at ThousandEyes-shaped +// loads this is tens of MB per change-analysis job. Use only when the caller +// actually consumes the full slices. +type WaitForAllQueriesStoreEverythingHandler struct { + // A callback that will be called when all queries are done. + DoneCallback func() + + StoreEverythingHandler +} + +var _ GatewayMessageHandler = (*WaitForAllQueriesStoreEverythingHandler)(nil) + +func (w *WaitForAllQueriesStoreEverythingHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { + if status.Done() { + w.DoneCallback() + } +} diff --git a/go/sdp-go/sdpws/messagehandler_test.go b/go/sdp-go/sdpws/messagehandler_test.go new file mode 100644 index 00000000..0d48125e --- /dev/null +++ b/go/sdp-go/sdpws/messagehandler_test.go @@ -0,0 +1,140 @@ +package sdpws + +import ( + "context" + "sync/atomic" + "testing" + + "github.com/overmindtech/cli/go/sdp-go" +) + +const benchEventCount = 1000 + +// doneStatus returns a GatewayRequestStatus whose Done() returns true. +func doneStatus() *sdp.GatewayRequestStatus { + return &sdp.GatewayRequestStatus{ + Summary: &sdp.GatewayRequestStatus_Summary{ + Working: 0, + Complete: 1, + Responders: 1, + }, + PostProcessingComplete: true, + } +} + +// notDoneStatus returns a GatewayRequestStatus whose Done() returns false. +func notDoneStatus() *sdp.GatewayRequestStatus { + return &sdp.GatewayRequestStatus{ + Summary: &sdp.GatewayRequestStatus_Summary{ + Working: 1, + Complete: 0, + Responders: 1, + }, + PostProcessingComplete: false, + } +} + +func feedItemsAndEdges(h GatewayMessageHandler, n int) { + ctx := context.Background() + for range n { + h.NewItem(ctx, &sdp.Item{Type: "test", UniqueAttribute: "name"}) + h.NewEdge(ctx, &sdp.Edge{}) + } +} + +// TestStoreItemsOnlyHandler asserts items accumulate and edges are dropped. +func TestStoreItemsOnlyHandler(t *testing.T) { + h := &StoreItemsOnlyHandler{} + feedItemsAndEdges(h, benchEventCount) + + if got := len(h.Items); got != benchEventCount { + t.Errorf("expected %d items retained, got %d", benchEventCount, got) + } + // StoreItemsOnlyHandler has no Edges field by design — the absence is + // enforced by the type system. The test exists to lock in the + // no-edges-stored invariant for future readers. +} + +// TestWaitForAllQueriesHandler_NoStorage asserts the safe default does not +// retain items or edges. The absence of the slices is enforced by the type +// system (no fields named Items/Edges); this test verifies the runtime path +// is also free of side effects and that the DoneCallback semantics are +// preserved. +func TestWaitForAllQueriesHandler_NoStorage(t *testing.T) { + var calls atomic.Int32 + h := &WaitForAllQueriesHandler{DoneCallback: func() { calls.Add(1) }} + + feedItemsAndEdges(h, benchEventCount) + + // Status with Done() == false: no callback. + h.Status(context.Background(), notDoneStatus()) + if got := calls.Load(); got != 0 { + t.Errorf("DoneCallback fired %d times on non-done status; expected 0", got) + } + + // Status with Done() == true: exactly one callback. + h.Status(context.Background(), doneStatus()) + if got := calls.Load(); got != 1 { + t.Errorf("DoneCallback fired %d times on done status; expected 1", got) + } +} + +func TestWaitForAllQueriesItemsOnlyHandler(t *testing.T) { + var calls atomic.Int32 + h := &WaitForAllQueriesItemsOnlyHandler{DoneCallback: func() { calls.Add(1) }} + + feedItemsAndEdges(h, benchEventCount) + + if got := len(h.Items); got != benchEventCount { + t.Errorf("expected %d items retained, got %d", benchEventCount, got) + } + + h.Status(context.Background(), notDoneStatus()) + if got := calls.Load(); got != 0 { + t.Errorf("DoneCallback fired %d times on non-done status; expected 0", got) + } + + h.Status(context.Background(), doneStatus()) + if got := calls.Load(); got != 1 { + t.Errorf("DoneCallback fired %d times on done status; expected 1", got) + } +} + +func TestWaitForAllQueriesStoreEverythingHandler(t *testing.T) { + var calls atomic.Int32 + h := &WaitForAllQueriesStoreEverythingHandler{DoneCallback: func() { calls.Add(1) }} + + feedItemsAndEdges(h, benchEventCount) + + if got := len(h.Items); got != benchEventCount { + t.Errorf("expected %d items retained, got %d", benchEventCount, got) + } + if got := len(h.Edges); got != benchEventCount { + t.Errorf("expected %d edges retained, got %d", benchEventCount, got) + } + + h.Status(context.Background(), notDoneStatus()) + if got := calls.Load(); got != 0 { + t.Errorf("DoneCallback fired %d times on non-done status; expected 0", got) + } + + h.Status(context.Background(), doneStatus()) + if got := calls.Load(); got != 1 { + t.Errorf("DoneCallback fired %d times on done status; expected 1", got) + } +} + +// TestStoreEverythingHandler asserts the pre-existing handler still works +// post-rename. It's not new code, but we test it here to keep the four wait +// variants symmetric and guard against accidental future drift. +func TestStoreEverythingHandler(t *testing.T) { + h := &StoreEverythingHandler{} + feedItemsAndEdges(h, benchEventCount) + + if got := len(h.Items); got != benchEventCount { + t.Errorf("expected %d items retained, got %d", benchEventCount, got) + } + if got := len(h.Edges); got != benchEventCount { + t.Errorf("expected %d edges retained, got %d", benchEventCount, got) + } +} diff --git a/stdlib-source/adapters/http.go b/stdlib-source/adapters/http.go index 306eb9fa..5e9bb44f 100644 --- a/stdlib-source/adapters/http.go +++ b/stdlib-source/adapters/http.go @@ -19,62 +19,17 @@ import ( const USER_AGENT_VERSION = "0.1" -// linkLocalRange represents the IPv4 link-local address range (169.254.0.0/16) -// This includes the EC2 metadata service IP (169.254.169.254) and is blocked -// to prevent DNS rebinding attacks and unauthorized metadata service access. -var linkLocalRange = &net.IPNet{ - IP: net.IPv4(169, 254, 0, 0), - Mask: net.CIDRMask(16, 32), -} - -// isLinkLocalIP checks if an IP address is in the link-local range (169.254.0.0/16) -func isLinkLocalIP(ip net.IP) bool { - if ip == nil { - return false - } - // Convert IPv4-mapped IPv6 addresses to IPv4 - ip = ip.To4() - if ip == nil { - return false - } - return linkLocalRange.Contains(ip) +type HTTPAdapter struct { + cache sdpcache.Cache // This is mandatory + ipPolicy IPPolicy // nil → defaultIPPolicy (production); tests inject allowLoopbackPolicy + resolver *net.Resolver // nil → net.DefaultResolver; tests inject a stub } -// validateHostname checks if a hostname resolves to a link-local IP address. -// This prevents DNS rebinding attacks where a hostname resolves to the EC2 -// metadata service or other link-local addresses. -func validateHostname(ctx context.Context, hostname string) error { - // First check if the hostname is already an IP address - if ip := net.ParseIP(hostname); ip != nil { - if isLinkLocalIP(ip) { - return fmt.Errorf("access to link-local address range (169.254.0.0/16) is blocked for security reasons") - } - return nil - } - - // Resolve the hostname to check if it resolves to a link-local IP - resolver := net.DefaultResolver - ips, err := resolver.LookupIPAddr(ctx, hostname) - if err != nil { - // If DNS resolution fails, we can't validate, but we should still - // allow the request to proceed (it will fail later if needed) - // This prevents blocking legitimate requests due to transient DNS issues - //nolint:nilerr // Intentionally allowing request to proceed if DNS resolution fails - return nil - } - - // Check all resolved IPs - for _, ipAddr := range ips { - if isLinkLocalIP(ipAddr.IP) { - return fmt.Errorf("hostname %s resolves to link-local address %s (169.254.0.0/16), which is blocked for security reasons", hostname, ipAddr.IP) - } +func (s *HTTPAdapter) policy() IPPolicy { + if s.ipPolicy == nil { + return defaultIPPolicy{} } - - return nil -} - -type HTTPAdapter struct { - cache sdpcache.Cache // This is mandatory + return s.ipPolicy } const httpCacheDuration = 5 * time.Minute @@ -148,18 +103,20 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor } } - // Validate hostname to prevent access to link-local addresses (including EC2 metadata service) + // Pre-validate hostname against the SSRF policy. DialContext is the + // authoritative enforcement point; this early check lets us return a + // clean error without opening a socket. hostname := parsedURL.Hostname() if hostname != "" { - if err := validateHostname(ctx, hostname); err != nil { + if err := validateHost(ctx, hostname, s.policy(), s.resolver); err != nil { ck := sdpcache.CacheKeyFromParts(s.Name(), sdp.QueryMethod_GET, scope, s.Type(), query) - err = &sdp.QueryError{ + qe := &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: err.Error(), Scope: scope, } - s.cache.StoreUnavailableItem(ctx, err, httpCacheDuration, ck) - return nil, err + s.cache.StoreUnavailableItem(ctx, qe, httpCacheDuration, ck) + return nil, qe } } @@ -182,16 +139,8 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor return nil, nil } - // Create a client that skips TLS verification since we will want to get the - // details of the TLS connection rather than stop if it's not trusted. Since - // we are only running a HEAD request this is unlikely to be a problem - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //nolint:gosec // G402 (TLS skip verify): intentional—adapter inspects TLS certificate details via HEAD request, not trusting the content - }, - } client := &http.Client{ - Transport: tr, + Transport: newSecureTransport(s.policy(), s.resolver), // Don't follow redirects, just return the status code directly CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse @@ -215,7 +164,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor var res *http.Response res, err = client.Do(req) - if err != nil { err = &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, @@ -262,7 +210,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor "headers": headersMap, "transferEncoding": res.Request.TransferEncoding, }) - if err != nil { err = &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, @@ -367,20 +314,15 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor // Resolve relative URLs against the original request URL resolvedURL := parsedURL.ResolveReference(locURL) - // Validate redirect target to prevent redirects to link-local addresses redirectHostname := resolvedURL.Hostname() if redirectHostname != "" { - if err := validateHostname(ctx, redirectHostname); err != nil { - // Don't fail the entire request, but mark the redirect as invalid + if err := validateHost(ctx, redirectHostname, s.policy(), s.resolver); err != nil { item.Attributes.AttrStruct.Fields["location-error"] = &structpb.Value{ Kind: &structpb.Value_StringValue{ StringValue: fmt.Sprintf("redirect blocked: %v", err), }, } } else { - // Don't include query string and fragment in the linked item. - // This leads to too many items, like auth redirect errors, that - // do not provide value. resolvedURL.Fragment = "" resolvedURL.RawQuery = "" item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ diff --git a/stdlib-source/adapters/http_ssrf.go b/stdlib-source/adapters/http_ssrf.go new file mode 100644 index 00000000..f5d53b29 --- /dev/null +++ b/stdlib-source/adapters/http_ssrf.go @@ -0,0 +1,146 @@ +package adapters + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/http" + "time" +) + +// ErrIPBlocked is returned by IPPolicy implementations when an IP is in a +// disallowed range. Callers use errors.Is to detect policy rejections vs. +// transport errors. +var ErrIPBlocked = errors.New("ip address blocked by SSRF policy") + +// IPPolicy decides whether an IP address may be dialed. Implementations are +// expected to be cheap, allocation-free, and safe for concurrent use. +type IPPolicy interface { + CheckIP(ip net.IP) error +} + +// cgnatRange is RFC 6598 100.64.0.0/10 — not covered by net.IP.IsPrivate. +var cgnatRange = &net.IPNet{ + IP: net.IPv4(100, 64, 0, 0), + Mask: net.CIDRMask(10, 32), +} + +// defaultIPPolicy is the production policy. It blocks the IP ranges that +// ENG-4205 enumerates: loopback, link-local, RFC1918 private, RFC6598 CGNAT, +// and the IPv6 equivalents (loopback, link-local, ULA fc00::/7). +type defaultIPPolicy struct{} + +func (defaultIPPolicy) CheckIP(ip net.IP) error { + if ip == nil { + return fmt.Errorf("%w: nil ip", ErrIPBlocked) + } + // Normalise IPv4-mapped IPv6 (::ffff:a.b.c.d) so a single check covers both. + if v4 := ip.To4(); v4 != nil { + ip = v4 + } + switch { + case ip.IsUnspecified(): + return fmt.Errorf("%w: %s is unspecified", ErrIPBlocked, ip) + case ip.IsLoopback(): + return fmt.Errorf("%w: %s is loopback", ErrIPBlocked, ip) + case ip.IsLinkLocalUnicast(): + return fmt.Errorf("%w: %s is link-local", ErrIPBlocked, ip) + case ip.IsPrivate(): + return fmt.Errorf("%w: %s is private", ErrIPBlocked, ip) + case ip.To4() != nil && cgnatRange.Contains(ip): + return fmt.Errorf("%w: %s is carrier-grade NAT", ErrIPBlocked, ip) + } + return nil +} + +// allowLoopbackPolicy is a test-only policy that wraps defaultIPPolicy but +// permits loopback (127.0.0.0/8 and ::1). This lets test files construct +// HTTPAdapter instances that can still talk to httptest.NewServer on localhost. +type allowLoopbackPolicy struct{} + +func (allowLoopbackPolicy) CheckIP(ip net.IP) error { + if ip == nil { + return fmt.Errorf("%w: nil ip", ErrIPBlocked) + } + if v4 := ip.To4(); v4 != nil { + ip = v4 + } + if ip.IsLoopback() { + return nil + } + return (defaultIPPolicy{}).CheckIP(ip) +} + +// validateHost checks a hostname against the given IPPolicy. If hostname is an +// IP literal it checks directly; otherwise it resolves via the supplied +// resolver and checks every returned address. +func validateHost(ctx context.Context, hostname string, policy IPPolicy, resolver *net.Resolver) error { + if ip := net.ParseIP(hostname); ip != nil { + return policy.CheckIP(ip) + } + + if resolver == nil { + resolver = net.DefaultResolver + } + ips, err := resolver.LookupIPAddr(ctx, hostname) + if err != nil { + return fmt.Errorf("dns resolution failed for %s: %w", hostname, err) + } + for _, ipAddr := range ips { + if err := policy.CheckIP(ipAddr.IP); err != nil { + return fmt.Errorf("hostname %s resolves to blocked address: %w", hostname, err) + } + } + return nil +} + +// newSecureTransport builds an *http.Transport with a DialContext hook that +// enforces the given IPPolicy at connection time (preventing DNS rebinding). +func newSecureTransport(policy IPPolicy, resolver *net.Resolver) *http.Transport { + if resolver == nil { + resolver = net.DefaultResolver + } + base := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + return &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // adapter inspects TLS certificate details via HEAD request, not trusting the content + }, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + // Resolve once here, validate each candidate, and dial the + // validated IP literal — not the hostname — so DNS cannot + // rebind between validation and connect. + ips, err := resolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, err + } + var lastErr error + for _, ipAddr := range ips { + if perr := policy.CheckIP(ipAddr.IP); perr != nil { + lastErr = perr + continue + } + conn, derr := base.DialContext(ctx, network, net.JoinHostPort(ipAddr.IP.String(), port)) + if derr == nil { + return conn, nil + } + lastErr = derr + if errors.Is(derr, context.Canceled) || errors.Is(derr, context.DeadlineExceeded) { + return nil, derr + } + } + if lastErr == nil { + lastErr = fmt.Errorf("no addresses resolved for %s", host) + } + return nil, fmt.Errorf("failed to dial %s: %w", host, lastErr) + }, + } +} diff --git a/stdlib-source/adapters/http_ssrf_test.go b/stdlib-source/adapters/http_ssrf_test.go new file mode 100644 index 00000000..7e3f7cf6 --- /dev/null +++ b/stdlib-source/adapters/http_ssrf_test.go @@ -0,0 +1,176 @@ +package adapters + +import ( + "context" + "errors" + "net" + "testing" +) + +func TestDefaultIPPolicy(t *testing.T) { + t.Parallel() + policy := defaultIPPolicy{} + + tests := []struct { + name string + ip string + blocked bool + reason string + }{ + // Unspecified (0.0.0.0 reaches localhost on Linux/macOS) + {"ipv4 unspecified 0.0.0.0", "0.0.0.0", true, "unspecified"}, + {"ipv6 unspecified ::", "::", true, "unspecified"}, + + // IPv4 loopback + {"ipv4 loopback 127.0.0.1", "127.0.0.1", true, "loopback"}, + {"ipv4 loopback 127.255.255.254", "127.255.255.254", true, "loopback"}, + + // IPv4 link-local + {"ipv4 link-local 169.254.0.1", "169.254.0.1", true, "link-local"}, + {"ipv4 link-local metadata 169.254.169.254", "169.254.169.254", true, "link-local"}, + + // IPv4 private (RFC1918) + {"ipv4 private 10.0.0.1", "10.0.0.1", true, "private"}, + {"ipv4 private 172.16.0.1", "172.16.0.1", true, "private"}, + {"ipv4 private 172.31.255.254", "172.31.255.254", true, "private"}, + {"ipv4 private 192.168.1.1", "192.168.1.1", true, "private"}, + + // IPv4 carrier-grade NAT (RFC6598) + {"ipv4 CGNAT 100.64.0.1", "100.64.0.1", true, "carrier-grade NAT"}, + {"ipv4 CGNAT 100.127.255.254", "100.127.255.254", true, "carrier-grade NAT"}, + + // IPv4 public (should be allowed) + {"ipv4 public 8.8.8.8", "8.8.8.8", false, ""}, + {"ipv4 public 1.1.1.1", "1.1.1.1", false, ""}, + {"ipv4 public 100.128.0.1", "100.128.0.1", false, ""}, + {"ipv4 just-outside-private 172.32.0.1", "172.32.0.1", false, ""}, + + // IPv6 loopback + {"ipv6 loopback ::1", "::1", true, "loopback"}, + + // IPv6 link-local + {"ipv6 link-local fe80::1", "fe80::1", true, "link-local"}, + + // IPv6 unique-local (ULA fc00::/7) + {"ipv6 ULA fd00::1", "fd00::1", true, "private"}, + {"ipv6 ULA fc00::1", "fc00::1", true, "private"}, + + // IPv6 public (should be allowed) + {"ipv6 public 2001:4860:4860::8888", "2001:4860:4860::8888", false, ""}, + + // IPv4-mapped IPv6 — should be unwrapped and checked against v4 rules + {"ipv4-mapped-ipv6 loopback ::ffff:127.0.0.1", "::ffff:127.0.0.1", true, "loopback"}, + {"ipv4-mapped-ipv6 private ::ffff:10.0.0.1", "::ffff:10.0.0.1", true, "private"}, + {"ipv4-mapped-ipv6 link-local ::ffff:169.254.169.254", "::ffff:169.254.169.254", true, "link-local"}, + {"ipv4-mapped-ipv6 CGNAT ::ffff:100.64.0.1", "::ffff:100.64.0.1", true, "carrier-grade NAT"}, + {"ipv4-mapped-ipv6 public ::ffff:8.8.8.8", "::ffff:8.8.8.8", false, ""}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ip := net.ParseIP(tc.ip) + if ip == nil { + t.Fatalf("failed to parse IP %q", tc.ip) + } + err := policy.CheckIP(ip) + if tc.blocked { + if err == nil { + t.Errorf("expected IP %s to be blocked (%s), got nil error", tc.ip, tc.reason) + } else if !errors.Is(err, ErrIPBlocked) { + t.Errorf("expected ErrIPBlocked for %s, got: %v", tc.ip, err) + } + } else { + if err != nil { + t.Errorf("expected IP %s to be allowed, got error: %v", tc.ip, err) + } + } + }) + } + + t.Run("nil IP", func(t *testing.T) { + t.Parallel() + err := policy.CheckIP(nil) + if !errors.Is(err, ErrIPBlocked) { + t.Errorf("expected ErrIPBlocked for nil IP, got: %v", err) + } + }) +} + +func TestAllowLoopbackPolicy(t *testing.T) { + t.Parallel() + policy := allowLoopbackPolicy{} + + tests := []struct { + name string + ip string + blocked bool + }{ + // Loopback is allowed + {"ipv4 loopback 127.0.0.1", "127.0.0.1", false}, + {"ipv6 loopback ::1", "::1", false}, + + // Everything else still blocked + {"ipv4 private 10.0.0.1", "10.0.0.1", true}, + {"ipv4 link-local 169.254.169.254", "169.254.169.254", true}, + {"ipv4 CGNAT 100.64.0.1", "100.64.0.1", true}, + {"ipv6 ULA fd00::1", "fd00::1", true}, + + // Public still allowed + {"ipv4 public 8.8.8.8", "8.8.8.8", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ip := net.ParseIP(tc.ip) + if ip == nil { + t.Fatalf("failed to parse IP %q", tc.ip) + } + err := policy.CheckIP(ip) + if tc.blocked && err == nil { + t.Errorf("expected IP %s to be blocked, got nil error", tc.ip) + } + if !tc.blocked && err != nil { + t.Errorf("expected IP %s to be allowed, got error: %v", tc.ip, err) + } + }) + } + + t.Run("nil IP", func(t *testing.T) { + t.Parallel() + err := policy.CheckIP(nil) + if !errors.Is(err, ErrIPBlocked) { + t.Errorf("expected ErrIPBlocked for nil IP, got: %v", err) + } + }) +} + +func TestValidateHost(t *testing.T) { + t.Parallel() + policy := defaultIPPolicy{} + + t.Run("IP literal blocked", func(t *testing.T) { + t.Parallel() + err := validateHost(context.Background(), "10.0.0.1", policy, nil) + if !errors.Is(err, ErrIPBlocked) { + t.Errorf("expected ErrIPBlocked for 10.0.0.1, got: %v", err) + } + }) + + t.Run("IP literal allowed", func(t *testing.T) { + t.Parallel() + err := validateHost(context.Background(), "8.8.8.8", policy, nil) + if err != nil { + t.Errorf("expected 8.8.8.8 to be allowed, got: %v", err) + } + }) + + t.Run("unresolvable hostname returns error", func(t *testing.T) { + t.Parallel() + err := validateHost(context.Background(), "this-hostname-does-not-exist.invalid", policy, nil) + if err == nil { + t.Error("expected error for unresolvable hostname, got nil") + } + }) +} diff --git a/stdlib-source/adapters/http_test.go b/stdlib-source/adapters/http_test.go index 6ecc61d2..40bb0660 100644 --- a/stdlib-source/adapters/http_test.go +++ b/stdlib-source/adapters/http_test.go @@ -18,6 +18,10 @@ import ( const TestHTTPTimeout = 3 * time.Second +func newTestAdapter(cache sdpcache.Cache) HTTPAdapter { + return HTTPAdapter{cache: cache, ipPolicy: allowLoopbackPolicy{}} +} + type TestHTTPServer struct { TLSServer *httptest.Server HTTPServer *httptest.Server @@ -116,9 +120,7 @@ func (t *TestHTTPServer) Close() { } func TestHTTPGet(t *testing.T) { - src := HTTPAdapter{ - cache: sdpcache.NewNoOpCache(), - } + src := newTestAdapter(sdpcache.NewNoOpCache()) server, err := NewTestServer() if err != nil { t.Fatal(err) @@ -204,7 +206,7 @@ func TestHTTPGet(t *testing.T) { srv := httptest.NewTLSServer(mux) defer srv.Close() - cachedSrc := HTTPAdapter{cache: sdpcache.NewMemoryCache()} + cachedSrc := newTestAdapter(sdpcache.NewMemoryCache()) url404 := srv.URL + "/404" // First call: 404 is cached as NOTFOUND @@ -253,7 +255,7 @@ func TestHTTPGet(t *testing.T) { srv := httptest.NewTLSServer(mux) defer srv.Close() - cachedSrc := HTTPAdapter{cache: sdpcache.NewMemoryCache()} + cachedSrc := newTestAdapter(sdpcache.NewMemoryCache()) url404 := srv.URL + "/404" first, err1 := cachedSrc.Search(context.Background(), "global", url404, false) @@ -449,7 +451,6 @@ func TestHTTPGet(t *testing.T) { }) t.Run("With link-local IP address should be blocked", func(t *testing.T) { - // Test direct access to EC2 metadata service IP metadataURL := "http://169.254.169.254/latest/meta-data/" _, err := src.Get(context.Background(), "global", metadataURL, false) @@ -458,27 +459,20 @@ func TestHTTPGet(t *testing.T) { t.Error("Expected error for link-local IP address, got nil") } - // Verify the error message mentions link-local blocking - if err != nil { - errStr := err.Error() - if errStr == "" { - t.Error("Expected error message, got empty string") + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if qErr.GetErrorType() != sdp.QueryError_OTHER { + t.Errorf("expected error type OTHER, got %v", qErr.GetErrorType()) } - // Check that it's a QueryError with the right error type - var qErr *sdp.QueryError - if errors.As(err, &qErr) { - if qErr.GetErrorType() != sdp.QueryError_OTHER { - t.Errorf("Expected error type OTHER, got %v", qErr.GetErrorType()) - } - if !strings.Contains(qErr.GetErrorString(), "link-local") { - t.Errorf("Expected error message to mention 'link-local', got: %s", qErr.GetErrorString()) - } + if !strings.Contains(qErr.GetErrorString(), "link-local") { + t.Errorf("expected error message to mention 'link-local', got: %s", qErr.GetErrorString()) } + } else { + t.Errorf("expected QueryError, got: %v", err) } }) t.Run("With other link-local IP addresses should be blocked", func(t *testing.T) { - // Test other IPs in the 169.254.0.0/16 range testIPs := []string{ "http://169.254.0.1/", "http://169.254.1.1/", @@ -487,21 +481,95 @@ func TestHTTPGet(t *testing.T) { for _, testIP := range testIPs { _, err := src.Get(context.Background(), "global", testIP, false) + if err == nil { + t.Errorf("expected error for link-local IP %s, got nil", testIP) + } + } + }) + t.Run("With private IP address should be blocked", func(t *testing.T) { + privateURLs := []string{ + "http://10.0.0.1/", + "http://172.16.0.1/", + "http://192.168.1.1/", + } + + for _, u := range privateURLs { + _, err := src.Get(context.Background(), "global", u, false) if err == nil { - t.Errorf("Expected error for link-local IP %s, got nil", testIP) + t.Errorf("expected error for private IP %s, got nil", u) + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "private") { + t.Errorf("expected error for %s to mention 'private', got: %s", u, qErr.GetErrorString()) + } + } + } + }) + + t.Run("With unspecified address 0.0.0.0 should be blocked", func(t *testing.T) { + _, err := src.Get(context.Background(), "global", "http://0.0.0.0/", false) + if err == nil { + t.Error("expected error for unspecified address 0.0.0.0, got nil") + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "unspecified") { + t.Errorf("expected error to mention 'unspecified', got: %s", qErr.GetErrorString()) + } + } + }) + + t.Run("With CGNAT IP address should be blocked", func(t *testing.T) { + _, err := src.Get(context.Background(), "global", "http://100.64.0.1/", false) + if err == nil { + t.Error("expected error for CGNAT IP, got nil") + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "carrier-grade NAT") { + t.Errorf("expected error to mention 'carrier-grade NAT', got: %s", qErr.GetErrorString()) + } + } + }) + + t.Run("With IPv6 loopback should be blocked", func(t *testing.T) { + strictSrc := HTTPAdapter{ + cache: sdpcache.NewNoOpCache(), + ipPolicy: defaultIPPolicy{}, + } + _, err := strictSrc.Get(context.Background(), "global", "http://[::1]/", false) + if err == nil { + t.Error("expected error for IPv6 loopback, got nil") + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "loopback") { + t.Errorf("expected error to mention 'loopback', got: %s", qErr.GetErrorString()) + } + } + }) + + t.Run("With IPv6 ULA address should be blocked", func(t *testing.T) { + _, err := src.Get(context.Background(), "global", "http://[fd00::1]/", false) + if err == nil { + t.Error("expected error for IPv6 ULA, got nil") + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "private") { + t.Errorf("expected error to mention 'private', got: %s", qErr.GetErrorString()) } } }) t.Run("With redirect to link-local address should be blocked", func(t *testing.T) { - // Test that redirects to link-local addresses are blocked item, err := src.Get(context.Background(), "global", server.RedirectPageLinkLocal, false) if err != nil { t.Fatal(err) } - // The request should succeed, but the redirect should be marked as blocked var locationError any locationError, err = item.GetAttributes().Get("location-error") if err != nil { @@ -510,28 +578,176 @@ func TestHTTPGet(t *testing.T) { locationErrorStr := locationError.(string) if !strings.Contains(locationErrorStr, "redirect blocked") { - t.Errorf("Expected location-error to contain 'redirect blocked', got: %s", locationErrorStr) + t.Errorf("expected location-error to contain 'redirect blocked', got: %s", locationErrorStr) } if !strings.Contains(locationErrorStr, "link-local") { - t.Errorf("Expected location-error to mention 'link-local', got: %s", locationErrorStr) + t.Errorf("expected location-error to mention 'link-local', got: %s", locationErrorStr) } - // Verify that no linked item query was created for the blocked redirect liqs := item.GetLinkedItemQueries() for _, liq := range liqs { if liq.GetQuery().GetType() == "http" { if strings.Contains(liq.GetQuery().GetQuery(), "169.254") { - t.Errorf("Expected no linked item query for blocked link-local redirect, got: %s", liq.GetQuery().GetQuery()) + t.Errorf("expected no linked item query for blocked link-local redirect, got: %s", liq.GetQuery().GetQuery()) } } } }) + + t.Run("With IPv6 link-local should be blocked", func(t *testing.T) { + strictSrc := HTTPAdapter{ + cache: sdpcache.NewNoOpCache(), + ipPolicy: defaultIPPolicy{}, + } + _, err := strictSrc.Get(context.Background(), "global", "http://[fe80::1]/", false) + if err == nil { + t.Error("expected error for IPv6 link-local, got nil") + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "link-local") { + t.Errorf("expected error to mention 'link-local', got: %s", qErr.GetErrorString()) + } + } + }) + + t.Run("With IPv4-mapped IPv6 should be blocked", func(t *testing.T) { + _, err := src.Get(context.Background(), "global", "http://[::ffff:10.0.0.1]/", false) + if err == nil { + t.Error("expected error for IPv4-mapped IPv6 private address, got nil") + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "private") { + t.Errorf("expected error to mention 'private', got: %s", qErr.GetErrorString()) + } + } + }) + + t.Run("defaultIPPolicy blocks loopback at dial time", func(t *testing.T) { + strictSrc := HTTPAdapter{ + cache: sdpcache.NewNoOpCache(), + ipPolicy: defaultIPPolicy{}, + } + _, err := strictSrc.Get(context.Background(), "global", server.OKPage, false) + if err == nil { + t.Error("expected defaultIPPolicy to block loopback test server, got nil") + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "loopback") { + t.Errorf("expected error to mention 'loopback', got: %s", qErr.GetErrorString()) + } + } + }) + + t.Run("DNS rebinding to private IP is blocked at dial time", func(t *testing.T) { + // Start a minimal UDP DNS server that always responds with 10.0.0.1, + // simulating a DNS rebinding attack. + stubAddr := newStubDNSServer(t, net.IPv4(10, 0, 0, 1)) + + rebindResolver := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{} + return d.DialContext(ctx, "udp", stubAddr) + }, + } + + rebindSrc := HTTPAdapter{ + cache: sdpcache.NewNoOpCache(), + ipPolicy: defaultIPPolicy{}, + resolver: rebindResolver, + } + + _, err := rebindSrc.Get(context.Background(), "global", "http://attacker.test/", false) + if err == nil { + t.Error("expected DNS-rebinding to private IP to be blocked, got nil") + } + var qErr *sdp.QueryError + if errors.As(err, &qErr) { + if !strings.Contains(qErr.GetErrorString(), "private") { + t.Errorf("expected error to mention 'private', got: %s", qErr.GetErrorString()) + } + } + }) } -func TestHTTPSearch(t *testing.T) { - src := HTTPAdapter{ - cache: sdpcache.NewNoOpCache(), +// newStubDNSServer starts a UDP DNS server on localhost that responds to all +// A queries with the given IPv4 address. It returns the listen address and +// registers cleanup with t.Cleanup. +func newStubDNSServer(t *testing.T, ip net.IP) string { + t.Helper() + v4 := ip.To4() + if v4 == nil { + t.Fatal("newStubDNSServer only supports IPv4") + } + + lc := net.ListenConfig{} + conn, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to start stub DNS server: %v", err) } + t.Cleanup(func() { conn.Close() }) + + go func() { + buf := make([]byte, 512) + for { + n, addr, err := conn.ReadFrom(buf) + if err != nil { + return + } + if n < 12 { + continue + } + query := buf[:n] + + // Walk past the QNAME labels to find QTYPE. + off := 12 + for off < n { + labelLen := int(query[off]) + if labelLen == 0 { + off++ // skip the zero-length root label + break + } + off += 1 + labelLen + } + if off+4 > n { + continue + } + qtype := uint16(query[off])<<8 | uint16(query[off+1]) + qEnd := off + 4 // end of question section (QTYPE + QCLASS) + + var resp []byte + resp = append(resp, query[:2]...) // transaction ID + resp = append(resp, 0x81, 0x80) // flags: response, recursion available + resp = append(resp, 0x00, 0x01) // QDCOUNT = 1 + if qtype == 1 { // A record query + resp = append(resp, 0x00, 0x01) // ANCOUNT = 1 + } else { + resp = append(resp, 0x00, 0x00) // ANCOUNT = 0 + } + resp = append(resp, 0x00, 0x00, 0x00, 0x00) // NSCOUNT, ARCOUNT = 0 + resp = append(resp, query[12:qEnd]...) // question section only + + if qtype == 1 { + resp = append(resp, 0xc0, 0x0c) // name pointer + resp = append(resp, 0x00, 0x01) // TYPE A + resp = append(resp, 0x00, 0x01) // CLASS IN + resp = append(resp, 0x00, 0x00, 0x00, 0x3c) // TTL 60 + resp = append(resp, 0x00, 0x04) // RDLENGTH 4 + resp = append(resp, v4...) + } + + _, _ = conn.WriteTo(resp, addr) + } + }() + + return conn.LocalAddr().String() +} + +func TestHTTPSearch(t *testing.T) { + src := newTestAdapter(sdpcache.NewNoOpCache()) server, err := NewTestServer() if err != nil { t.Fatal(err)