diff --git a/.devcontainer/devcontainer.env b/.devcontainer/devcontainer.env new file mode 100644 index 000000000..474710071 --- /dev/null +++ b/.devcontainer/devcontainer.env @@ -0,0 +1 @@ +IMIX_SERVER_PUBKEY=PLACE_HOLDER diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b458eac7..0fee96f70 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,6 +33,8 @@ jobs: implants: runs-on: ${{ matrix.os }} timeout-minutes: 60 + env: + IMIX_SERVER_PUBKEY: "pR56vDJZb9b3BL3ZvCXIvgK0r2vCk7FiZ1RjeEhJVyU=" strategy: matrix: os: @@ -71,7 +73,7 @@ jobs: cd ./bin/reflective_loader/ cargo +nightly-2025-01-31 build --release -Z build-std=core,compiler_builtins -Z build-std-features=compiler-builtins-mem - name: Install latest nextest & cargo-llvm-cov release - uses: taiki-e/install-action@v2.52.7 + uses: taiki-e/install-action@v2 with: tool: nextest,cargo-llvm-cov - name: 🔎 Run tests diff --git a/.vscode/settings.json b/.vscode/settings.json index 4fad2d189..72430bbe1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,9 @@ "--profile", "rust-analyzer" ], + "rust-analyzer.server.extraEnv": { + "IMIX_SERVER_PUBKEY": "v3NEg3eB9/e3wi4HHSoIIgPq3BEi6xrurKOSuOVj72g=" + }, "rust-analyzer.check.command": "clippy", "rust-analyzer.showUnlinkedFileNotification": false, } diff --git a/docs/_docs/admin-guide/tavern.md b/docs/_docs/admin-guide/tavern.md index 563af380d..2dd23ff29 100644 --- a/docs/_docs/admin-guide/tavern.md +++ b/docs/_docs/admin-guide/tavern.md @@ -117,6 +117,15 @@ By default, Tavern does not export metrics. You may use the below environment co | ENABLE_METRICS | Set to any value to enable the "/metrics" endpoint. | Disabled | No | | HTTP_METRICS_LISTEN_ADDR | Listen address for the metrics HTTP server, it must be different than the value of `HTTP_LISTEN_ADDR`. | `127.0.0.1:8080` | No | +### Secrets + +By default, Tavern wants to use a GCP KMS for secrets management. The secrets engine is used to generate keypairs when communicating with agents. +If you're running locally make suer to set the secrets manager to a local file path using: + +```bash +SECRETS_FILE_PATH="/tmp/secrets" go run ./tavern/ +``` + ### MySQL By default, Tavern operates an in-memory SQLite database. To persist data, a MySQL backend is supported. In order to configure Tavern to use MySQL, the `MYSQL_ADDR` environment variable must be set to the `host[:port]` of the database (e.g. `127.0.0.1`, `mydb.com`, or `mydb.com:3306`). You can reference the [mysql.Config](https://pkg.go.dev/github.com/go-sql-driver/mysql#Config) for additional information about Tavern's MySQL configuration. diff --git a/docs/_docs/user-guide/imix.md b/docs/_docs/user-guide/imix.md index 81a64f363..f1da0ef25 100644 --- a/docs/_docs/user-guide/imix.md +++ b/docs/_docs/user-guide/imix.md @@ -83,6 +83,11 @@ This isn't ideal as in the UI each new beacon will appear as thought it were on ## Static cross compilation +**We strongly recommend building agents inside the provided devcontainer `.devcontainer`** +Building in the dev container limits variables that might cause issues and is the most tested way to compile. + +**Imix requires a server public key so it can encrypt messsages to and from the server check the server log for `level=INFO msg="public key: "`. This base64 encoded string should be passed to the agent using the environment variable `IMIX_SERVER_PUBKEY`** + ### Linux ```bash @@ -91,7 +96,7 @@ rustup target add x86_64-unknown-linux-musl sudo apt update sudo apt install musl-tools cd realm/implants/imix/ -cargo build --release --bin imix --target=x86_64-unknown-linux-musl +IMIX_SERVER_PUBKEY="" cargo build --release --bin imix --target=x86_64-unknown-linux-musl ``` ### MacOS @@ -113,10 +118,11 @@ sudo apt install gcc-mingw-w64 # Build imix cd realm/implants/imix/ + # Build imix.exe -cargo build --release --target=x86_64-pc-windows-gnu +IMIX_SERVER_PUBKEY="" cargo build --release --target=x86_64-pc-windows-gnu # Build imix.svc.exe -cargo build --release --features win_service --target=x86_64-pc-windows-gnu +IMIX_SERVER_PUBKEY="" cargo build --release --features win_service --target=x86_64-pc-windows-gnu # Build imix.dll -cargo build --release --lib --target=x86_64-pc-windows-gnu +IMIX_SERVER_PUBKEY="" cargo build --release --lib --target=x86_64-pc-windows-gnu ``` diff --git a/go.mod b/go.mod index 695b64b31..33f1d08a8 100644 --- a/go.mod +++ b/go.mod @@ -4,38 +4,39 @@ go 1.23.4 require ( cloud.google.com/go/pubsub v1.45.3 + cloud.google.com/go/secretmanager v1.14.2 entgo.io/contrib v0.6.0 entgo.io/ent v0.14.1 github.com/99designs/gqlgen v0.17.62 + github.com/cloudflare/circl v1.5.0 github.com/go-sql-driver/mysql v1.8.1 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-cmp v0.6.0 github.com/hashicorp/go-multierror v1.1.1 - github.com/mattn/go-sqlite3 v1.14.24 - github.com/prometheus/client_golang v1.20.5 + github.com/mattn/go-sqlite3 v1.14.16 + github.com/prometheus/client_golang v1.20.4 github.com/stretchr/testify v1.10.0 - github.com/urfave/cli v1.22.16 + github.com/urfave/cli v1.22.5 github.com/vektah/gqlparser/v2 v2.5.21 - golang.org/x/crypto v0.31.0 - golang.org/x/net v0.33.0 - golang.org/x/oauth2 v0.25.0 - golang.org/x/sync v0.10.0 - google.golang.org/grpc v1.69.2 - google.golang.org/protobuf v1.36.1 + golang.org/x/crypto v0.38.0 + golang.org/x/net v0.40.0 + golang.org/x/oauth2 v0.26.0 + golang.org/x/sync v0.14.0 + google.golang.org/api v0.210.0 + google.golang.org/grpc v1.72.1 + google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 ) require ( - cloud.google.com/go v0.117.0 // indirect - cloud.google.com/go/auth v0.13.0 // indirect + cloud.google.com/go v0.116.0 // indirect + cloud.google.com/go/auth v0.11.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect - cloud.google.com/go/iam v1.3.1 // indirect - dario.cat/mergo v1.0.1 // indirect + cloud.google.com/go/iam v1.2.2 // indirect + dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.3 // indirect - github.com/bmatcuk/doublestar v1.3.4 // indirect - github.com/cloudflare/circl v1.5.0 // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -48,7 +49,7 @@ require ( github.com/google/s2a-go v0.1.8 // indirect github.com/google/wire v0.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/googleapis/gax-go/v2 v2.14.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.11 // indirect @@ -60,21 +61,20 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/api v0.214.0 // indirect - google.golang.org/genproto v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + golang.org/x/time v0.8.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) require ( - ariga.io/atlas v0.29.1 // indirect + ariga.io/atlas v0.25.1-0.20240717145915-af51d3945208 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/agnivade/levenshtein v1.2.0 // indirect @@ -99,12 +99,12 @@ require ( github.com/sosodev/duration v1.3.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.16.0 // indirect - gocloud.dev v0.40.0 - golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 - golang.org/x/mod v0.22.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.28.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + gocloud.dev v0.37.0 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/mod v0.20.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/tools v0.24.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect ) diff --git a/go.sum b/go.sum index 3c91376cd..db564a4b4 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,26 @@ -ariga.io/atlas v0.29.1 h1:7gB8XRFTnJeZ7ZiccNCJqwBtUv3yjFyxRFDMzu0AmRg= -ariga.io/atlas v0.29.1/go.mod h1:lkLAw/t2/P7g5CFYlYmHvNuShlmGujwm3OGsW00xowI= +ariga.io/atlas v0.25.1-0.20240717145915-af51d3945208 h1:ixs1c/fAXGS3mTdalyKQrtvfkFjgChih/unX66YTzYk= +ariga.io/atlas v0.25.1-0.20240717145915-af51d3945208/go.mod h1:KPLc7Zj+nzoXfWshrcY1RwlOh94dsATQEy4UPrF2RkM= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.117.0 h1:Z5TNFfQxj7WG2FgOGX1ekC5RiXrYgms6QscOm32M/4s= -cloud.google.com/go v0.117.0/go.mod h1:ZbwhVTb1DBGt2Iwb3tNO6SEK4q+cplHZmLWH+DelYYc= -cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= -cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.11.0 h1:Ic5SZz2lsvbYcWT5dfjNWgw6tTlGi2Wc8hyQSC9BstA= +cloud.google.com/go/auth v0.11.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.3.1 h1:KFf8SaT71yYq+sQtRISn90Gyhyf4X8RGgeAVC8XGf3E= -cloud.google.com/go/iam v1.3.1/go.mod h1:3wMtuyT4NcbnYNPLMBzYRFiEfjKfJlLVLrisE7bwm34= -cloud.google.com/go/kms v1.20.3 h1:a61yIN5LN8ozWxOC6tjUx5V5SEzfkS+b69kYMQfzGzE= -cloud.google.com/go/kms v1.20.3/go.mod h1:YvX+xhp2E2Sc3vol5IcRlBhH14Ecl3kegUY/DtH7EWQ= -cloud.google.com/go/longrunning v0.6.3 h1:A2q2vuyXysRcwzqDpMMLSI6mb6o39miS52UEG/Rd2ng= -cloud.google.com/go/longrunning v0.6.3/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= +cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/kms v1.20.1 h1:og29Wv59uf2FVaZlesaiDAqHFzHaoUyHI3HYp9VUHVg= +cloud.google.com/go/kms v1.20.1/go.mod h1:LywpNiVCvzYNJWS9JUcGJSVTNSwPwi0vBAotzDqn2nc= +cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= +cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= cloud.google.com/go/pubsub v1.45.3 h1:prYj8EEAAAwkp6WNoGTE4ahe0DgHoyJd5Pbop931zow= cloud.google.com/go/pubsub v1.45.3/go.mod h1:cGyloK/hXC4at7smAtxFnXprKEFTqmMXNNd9w+bd94Q= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +cloud.google.com/go/secretmanager v1.14.2 h1:2XscWCfy//l/qF96YE18/oUaNJynAx749Jg3u0CjQr8= +cloud.google.com/go/secretmanager v1.14.2/go.mod h1:Q18wAPMM6RXLC/zVpWTlqq2IBSbbm7pKBlM3lCKsmjw= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= entgo.io/contrib v0.6.0 h1:xfo4TbJE7sJZWx7BV7YrpSz7IPFvS8MzL3fnfzZjKvQ= entgo.io/contrib v0.6.0/go.mod h1:3qWIseJ/9Wx2Hu5zVh15FDzv7d/UvKNcYKdViywWCQg= entgo.io/ent v0.14.1 h1:fUERL506Pqr92EPHJqr8EYxbPioflJo6PudkrEA8a/s= @@ -28,7 +30,6 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 github.com/99designs/gqlgen v0.17.62 h1:Wovt1+XJN9dTWYh92537Y9a5FuMVSkrQL4bn0a8v5Rg= github.com/99designs/gqlgen v0.17.62/go.mod h1:sVCM2iwIZisJjTI/DEC3fpH+HFgxY1496ZJ+jbT9IjA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -54,38 +55,36 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= -github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= -github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= -github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= +github.com/aws/aws-sdk-go v1.50.36 h1:PjWXHwZPuTLMR1NIb8nEjLucZBMzmf84TLoLbD8BZqk= +github.com/aws/aws-sdk-go v1.50.36/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.25.3 h1:xYiLpZTQs1mzvz5PaI6uR0Wh57ippuEthxS4iK5v0n0= +github.com/aws/aws-sdk-go-v2 v1.25.3/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I= +github.com/aws/aws-sdk-go-v2/config v1.27.7 h1:JSfb5nOQF01iOgxFI5OIKWwDiEXWTyTgg1Mm1mHi0A4= +github.com/aws/aws-sdk-go-v2/config v1.27.7/go.mod h1:PH0/cNpoMO+B04qET699o5W92Ca79fVtbUnvMIZro4I= +github.com/aws/aws-sdk-go-v2/credentials v1.17.7 h1:WJd+ubWKoBeRh7A5iNMnxEOs982SyVKOJD+K8HIezu4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.7/go.mod h1:UQi7LMR0Vhvs+44w5ec8Q+VS+cd10cjwgHwiVkE0YGU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3 h1:p+y7FvkK2dxS+FEwRIDHDe//ZX+jDhP8HHE50ppj4iI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3/go.mod h1:/fYB+FZbDlwlAiynK9KDXlzZl3ANI9JkD0Uhz5FjNT4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3 h1:ifbIbHZyGl1alsAhPIYsHOg5MuApgqOvVeI8wIugXfs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3/go.mod h1:oQZXg3c6SNeY6OZrDY+xHcF4VGIEoNotX2B4PrDeoJI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3 h1:Qvodo9gHG9F3E8SfYOspPeBt0bjSbsevK8WhRAUHcoY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3/go.mod h1:vCKrdLXtybdf/uQd/YfVR2r5pcbNuEYKzMQpcxmeSJw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= -github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= -github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.5 h1:K/NXvIftOlX+oGgWGIa3jDyYLDNsdVhsjHmsBH2GLAQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.5/go.mod h1:cl9HGLV66EnCmMNzq4sYOti+/xo8w34CsgzVtm2GgsY= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.2 h1:XOPfar83RIRPEzfihnp+U6udOveKZJvPQ76SKWrLRHc= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.2/go.mod h1:Vv9Xyk1KMHXrR3vNQe8W5LMFdTjSeWk0gBZBzvf3Qa0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2 h1:pi0Skl6mNl2w8qWZXcdOyg197Zsf4G97U7Sso9JXGZE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2/go.mod h1:JYzLoEVeLXk+L4tn1+rrkfhkxl6mLDEVaDSvGq9og90= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.4 h1:Ppup1nVNAOWbBOrcoOxaxPeEnSFB2RnnQdguhXpmeQk= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.4/go.mod h1:+K1rNPVyGxkRuv9NNiaZ4YhBFuyw2MMA9SlIJ1Zlpz8= +github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= +github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 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/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= -github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -93,7 +92,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= @@ -162,8 +161,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo= -github.com/google/go-replayers/grpcreplay v1.3.0/go.mod h1:v6NgKtkijC0d3e3RW8il6Sy5sqRVUwoQa4mHOGEy8DI= +github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= +github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk= github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= @@ -178,8 +177,8 @@ github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= +github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -208,8 +207,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= -github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY= @@ -224,8 +223,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= @@ -235,10 +234,12 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +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/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= @@ -254,12 +255,10 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= -github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vektah/gqlparser/v2 v2.5.21 h1:Zw1rG2dr1pRR4wqwbVq4d6+xk2f4ut/yo+hwr4QjE08= github.com/vektah/gqlparser/v2 v2.5.21/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -269,8 +268,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w= -github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= +github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.einride.tech/aip v0.68.0 h1:4seM66oLzTpz50u4K1zlJyOXQ3tCzcJN7I22tKkjipw= @@ -279,33 +278,33 @@ 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.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng= -gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro= +gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= -golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -313,8 +312,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -329,11 +328,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -341,8 +340,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -359,16 +358,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -377,10 +376,10 @@ 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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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= @@ -391,32 +390,32 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.214.0 h1:h2Gkq07OYi6kusGOaT/9rnNljuXmqPnaig7WGPmKbwA= -google.golang.org/api v0.214.0/go.mod h1:bYPpLG8AyeMWwDU6NXoB00xC0DFkikVvd5MfwoxjLqE= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.210.0 h1:HMNffZ57OoZCRYSbdWVRoqOa8V8NIHLL0CzdBPLztWk= +google.golang.org/api v0.210.0/go.mod h1:B9XDZGnx2NtyjzVkOVTGrFSAVZgPcbedzKg/gTLwqBs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20250102185135-69823020774d h1:3NH+6ZtWWhXDpEJEAtzF1Gp/zA87pKkIB4gO1Ag8VSI= -google.golang.org/genproto v0.0.0-20250102185135-69823020774d/go.mod h1:zhXVSAeuPiprFfMSrt7Jo1Uighv2Nfu3HAZrw83tcYE= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -426,8 +425,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/implants/Cargo.toml b/implants/Cargo.toml index 44d958f74..01a42f2a9 100644 --- a/implants/Cargo.toml +++ b/implants/Cargo.toml @@ -24,6 +24,7 @@ async-recursion = "1.0.0" async-trait = "0.1.68" base64 = "0.21.4" chrono = "0.4.34" +const-decoder = "0.3.0" clap = "3.2.23" netdev = "0.33.0" derive_more = "=0.99.17" @@ -79,7 +80,7 @@ tokio-stream = { version = "0.1.9", default-features = false } tokio-test = "*" tokio-util = { version = "0.7.10", features = ["io"] } tonic = { git = "https://github.com/hyperium/tonic.git", rev = "07e4ee1" } -tonic-build = "0.10" +tonic-build = { git = "https://github.com/hyperium/tonic.git", rev = "c783652" } # Needed git for `.codec_path` in build.rs - previous version of codec setting is really gross. https://github.com/hyperium/tonic/blob/ea8cd3f384e953e177f20a62aa156a75676853f4/examples/build.rs#L44 trait-variant = "0.1.1" uuid = "1.5.0" static_vcruntime = "2.0" @@ -88,6 +89,10 @@ whoami = { version = "1.5.1", default-features = false } windows-service = "0.6.0" windows-sys = "0.45.0" winreg = "0.51.0" +chacha20poly1305 = "0.10.1" +bytes = "1.6.0" +x25519-dalek = "2.0.1" +lru = "0.16.0" [profile.release] diff --git a/implants/lib/pb/Cargo.toml b/implants/lib/pb/Cargo.toml index d8faf9c7a..79764df2e 100644 --- a/implants/lib/pb/Cargo.toml +++ b/implants/lib/pb/Cargo.toml @@ -4,17 +4,26 @@ version = "0.0.5" edition = "2021" [dependencies] +anyhow = { workspace = true } host_unique = { workspace = true } log = { workspace = true } netdev = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } +rand_chacha = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tokio-stream = { workspace = true } tonic = { workspace = true, features = ["tls-roots"] } +const-decoder = { workspace = true } +chacha20poly1305 = { workspace = true } +bytes = { workspace = true } +rand = { workspace = true } +x25519-dalek = { workspace = true } +lru = { workspace = true } + uuid = { workspace = true, features = ["v4", "fast-rng"] } whoami = { workspace = true } [build-dependencies] -tonic-build = { workspace = true } +tonic-build = { workspace = true, features = ["prost"] } which = { workspace = true } diff --git a/implants/lib/pb/build.rs b/implants/lib/pb/build.rs index e1f40b772..9c163c0a7 100644 --- a/implants/lib/pb/build.rs +++ b/implants/lib/pb/build.rs @@ -18,6 +18,7 @@ fn main() -> Result<(), Box> { // Build Eldritch Proto match tonic_build::configure() .out_dir("./src/generated/") + .codec_path("crate::xchacha::ChachaCodec") .build_client(false) .build_server(false) .compile(&["eldritch.proto"], &["../../../tavern/internal/c2/proto"]) @@ -32,6 +33,7 @@ fn main() -> Result<(), Box> { // Build C2 Protos match tonic_build::configure() .out_dir("./src/generated") + .codec_path("crate::xchacha::ChachaCodec") .build_server(false) .extern_path(".eldritch", "crate::eldritch") .compile(&["c2.proto"], &["../../../tavern/internal/c2/proto/"]) diff --git a/implants/lib/pb/src/generated/c2.rs b/implants/lib/pb/src/generated/c2.rs index c388d9b63..f1f0ec87e 100644 --- a/implants/lib/pb/src/generated/c2.rs +++ b/implants/lib/pb/src/generated/c2.rs @@ -338,7 +338,7 @@ pub mod c2_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = crate::xchacha::ChachaCodec::default(); let path = http::uri::PathAndQuery::from_static("/c2.C2/ClaimTasks"); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ClaimTasks")); @@ -368,7 +368,7 @@ pub mod c2_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = crate::xchacha::ChachaCodec::default(); let path = http::uri::PathAndQuery::from_static("/c2.C2/FetchAsset"); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new("c2.C2", "FetchAsset")); @@ -392,7 +392,7 @@ pub mod c2_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = crate::xchacha::ChachaCodec::default(); let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportCredential"); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportCredential")); @@ -421,7 +421,7 @@ pub mod c2_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = crate::xchacha::ChachaCodec::default(); let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportFile"); let mut req = request.into_streaming_request(); req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportFile")); @@ -446,7 +446,7 @@ pub mod c2_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = crate::xchacha::ChachaCodec::default(); let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportProcessList"); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportProcessList")); @@ -470,7 +470,7 @@ pub mod c2_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = crate::xchacha::ChachaCodec::default(); let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportTaskOutput"); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportTaskOutput")); @@ -496,7 +496,7 @@ pub mod c2_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = crate::xchacha::ChachaCodec::default(); let path = http::uri::PathAndQuery::from_static("/c2.C2/ReverseShell"); let mut req = request.into_streaming_request(); req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReverseShell")); diff --git a/implants/lib/pb/src/lib.rs b/implants/lib/pb/src/lib.rs index 8a886c241..f65dc9036 100644 --- a/implants/lib/pb/src/lib.rs +++ b/implants/lib/pb/src/lib.rs @@ -5,3 +5,4 @@ pub mod c2 { include!("generated/c2.rs"); } pub mod config; +pub mod xchacha; diff --git a/implants/lib/pb/src/xchacha.rs b/implants/lib/pb/src/xchacha.rs new file mode 100644 index 000000000..a87ede72f --- /dev/null +++ b/implants/lib/pb/src/xchacha.rs @@ -0,0 +1,284 @@ +use anyhow::{Context, Result}; +use bytes::{Buf, BufMut}; +use chacha20poly1305::{aead::generic_array::GenericArray, aead::Aead, AeadCore, KeyInit}; +use const_decoder::Decoder as const_decode; +use lru::LruCache; +use prost::Message; +use rand::rngs::OsRng; +use rand_chacha::rand_core::SeedableRng; +use std::{ + io::{Read, Write}, + marker::PhantomData, + num::NonZeroUsize, + sync::{Mutex, OnceLock}, +}; +use tonic::{ + codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}, + Status, +}; +use x25519_dalek::{EphemeralSecret, PublicKey}; + +/* Compile-time constant for the server pubkey, derived from the IMIX_SERVER_PUBKEY environment variable during compilation. + * To find the servers pubkey check the startup messages on the server look for `[INFO] Public key: ` + */ +static SERVER_PUBKEY: [u8; 32] = const_decode::Base64.decode(env!("IMIX_SERVER_PUBKEY").as_bytes()); + +// ------------ + +const KEY_CACHE_SIZE: usize = 1024; + +// The client and server may have multiple connections open and they may not resolve in order or sequentially. +// To handle this ephemeral public keys and shared secrets are stored in a hashmap key_history where they can be looked up +// by public key. +fn key_history() -> &'static Mutex> { + static ARRAY: OnceLock>> = OnceLock::new(); + ARRAY.get_or_init(|| Mutex::new(LruCache::new(NonZeroUsize::new(KEY_CACHE_SIZE).unwrap()))) +} + +fn add_key_history(pub_key: [u8; 32], shared_secret: [u8; 32]) { + key_history().lock().unwrap().put(pub_key, shared_secret); // Mutex's must unwrap +} + +fn get_key(pub_key: [u8; 32]) -> Result<[u8; 32]> { + // Lookup the shared secret based on the public key + let res = *key_history() + .lock() + .unwrap() // Mutex's must unwrap + .get(&pub_key) + .context("Unable to find shared secret for the public key recieved")?; + Ok(res) +} + +fn del_key(pub_key: [u8; 32]) -> Option<[u8; 32]> { + key_history().lock().unwrap().pop(&pub_key) +} + +// ------------ + +#[derive(Debug, Clone, Default)] +pub struct ChaChaSvc {} + +#[derive(Debug, Clone)] +pub struct ChachaCodec(PhantomData<(T, U)>, ChaChaSvc); + +impl Default for ChachaCodec { + fn default() -> Self { + #[cfg(debug_assertions)] + log::debug!("Loaded custom codec with xchacha encryption"); + Self(PhantomData, ChaChaSvc::default()) + } +} + +impl Codec for ChachaCodec +where + T: Message + Send + 'static, + U: Message + Default + Send + 'static, +{ + type Encode = T; + type Decode = U; + type Encoder = ChachaEncrypt; + type Decoder = ChachaDecrypt; + + fn encoder(&mut self) -> Self::Encoder { + ChachaEncrypt(PhantomData, ChaChaSvc::default()) + } + + fn decoder(&mut self) -> Self::Decoder { + ChachaDecrypt(PhantomData, ChaChaSvc::default()) + } +} + +// --- + +#[derive(Debug)] +pub struct ChachaEncrypt(PhantomData<(T, U)>, ChaChaSvc); + +impl Encoder for ChachaEncrypt +where + T: Message + Send + 'static, + U: Message + Default + Send + 'static, +{ + type Item = T; + type Error = Status; + + fn encode(&mut self, item: Self::Item, buf: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { + if !buf.has_remaining_mut() { + // This should never happen but if it does the agent will be unable to queue new messages to the buffer until it's drained. + #[cfg(debug_assertions)] + log::debug!("DANGER can't add to the buffer."); + } + + // Store server pubkey + let server_public = PublicKey::from(SERVER_PUBKEY); + + // Generate ephemeral keys + let rng = rand_chacha::ChaCha20Rng::from_entropy(); + let client_secret = EphemeralSecret::random_from_rng(rng); + let client_public = PublicKey::from(&client_secret); + + // Generate shared secret + let shared_secret = client_secret.diffie_hellman(&server_public); + add_key_history(*client_public.as_bytes(), *shared_secret.as_bytes()); + + // Generate nonce + let cipher = chacha20poly1305::XChaCha20Poly1305::new(GenericArray::from_slice( + shared_secret.as_bytes(), + )); + let nonce = chacha20poly1305::XChaCha20Poly1305::generate_nonce(&mut OsRng); + + // Encrypt data + let pt_vec = item.encode_to_vec(); + let ciphertext = match cipher.encrypt(&nonce, pt_vec.as_slice()) { + Ok(ct) => ct, + Err(err) => { + #[cfg(debug_assertions)] + log::debug!( + "encode error unable to read bytes while encrypting: {:?}", + err + ); + return Err(Status::new(tonic::Code::Internal, err.to_string())); + } + }; + + // Write pubkey + nonce + cipher text + buf.writer().write_all(client_public.as_bytes())?; + buf.writer().write_all(nonce.as_slice())?; + buf.writer().write_all(ciphertext.as_slice())?; + + Ok(()) + } +} + +// --- +// +const DEFAULT_CODEC_BUFFER_SIZE: usize = 8 * 1024; +const PUBKEY_LEN: usize = 32; +const NONCE_LEN: usize = 24; + +#[derive(Debug)] +pub struct ChachaDecrypt(PhantomData<(T, U)>, ChaChaSvc); + +impl Decoder for ChachaDecrypt +where + T: Message + Send + 'static, + U: Message + Default + Send + 'static, +{ + type Item = U; + type Error = Status; + + fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { + // public key + xchacha nonce + ciphertext + let mut reader = buf.reader(); + let mut bytes_in = vec![0; DEFAULT_CODEC_BUFFER_SIZE]; + let bytes_read = match reader.read(&mut bytes_in) { + Ok(n) => n, + Err(err) => { + #[cfg(debug_assertions)] + log::debug!( + "decode error unable to read bytes from decode reader: {:?}", + err + ); + return Err(Status::new(tonic::Code::Internal, err.to_string())); + } + }; + + if bytes_read < PUBKEY_LEN + NONCE_LEN { + let err = + anyhow::anyhow!("Message from server is too small to contain public key and nonce"); + log::debug!( + "Input buffer from server during decode faild validation: {:?}", + err + ); + return Err(Status::new(tonic::Code::Internal, err.to_string())); + } + let buf = bytes_in + .get(0..bytes_read) + .context("Bytes read doesn't match buffer size") + .map_err(from_anyhow_error)?; + + let client_public = buf + .get(0..PUBKEY_LEN) + .context("Input buffer doesn't have enough bytes for public key") + .map_err(from_anyhow_error)?; + + let nonce = buf + .get(PUBKEY_LEN..PUBKEY_LEN + NONCE_LEN) + .context("Input buffer doesn't have enough bytes for nonce") + .map_err(from_anyhow_error)?; + + let ciphertext = buf + .get(PUBKEY_LEN + NONCE_LEN..) + .context("Input buffer doesn't have enough bytes for ciphertext") + .map_err(from_anyhow_error)?; + + // Get private key based on messages public key + let tmp_client_public_bytes = client_public.to_vec(); + let client_public_bytes = match tmp_client_public_bytes.try_into() { + Ok(arr) => arr, + Err(err) => { + #[cfg(debug_assertions)] + log::debug!("Unable to cast public key bytes from vector to slice during decode function: {:?}", err); + + return Err(Status::new( + tonic::Code::Internal, + "Unable to cast public key bytes from vector to slice during decode function", + )); + } + }; + + let client_private_bytes = get_key(client_public_bytes).map_err(from_anyhow_error)?; + // Shouldn't need private key again once the message has been decrypted + del_key(client_public_bytes); + + let cipher = chacha20poly1305::XChaCha20Poly1305::new(GenericArray::from_slice( + &client_private_bytes, + )); + + // Decrypt message + let plaintext = match cipher.decrypt(GenericArray::from_slice(nonce), ciphertext.as_ref()) { + Ok(pt) => pt, + Err(err) => { + #[cfg(debug_assertions)] + log::debug!("Error decrypting response: {:?}", err); + return Err(Status::new(tonic::Code::Internal, err.to_string())); + } + }; + + // Serialize + let item = Message::decode(bytes::Bytes::from(plaintext)) + .map(Option::Some) + .map_err(from_decode_error)?; + + Ok(item) + } +} + +fn from_anyhow_error(error: anyhow::Error) -> Status { + Status::new(tonic::Code::Internal, error.to_string()) +} + +fn from_decode_error(error: prost::DecodeError) -> Status { + // Map Protobuf parse errors to an INTERNAL status code, as per + // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md + Status::new(tonic::Code::Internal, error.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_key_history_bounded() { + let mut key_history = key_history().lock().unwrap(); + for i in 0..KEY_CACHE_SIZE * 2 { + let mut pub_key = [0u8; 32]; + let mut shared_secret = [0u8; 32]; + pub_key[0] = i as u8; + pub_key[1] = (i >> 8) as u8; + shared_secret[0] = i as u8; + shared_secret[1] = (i >> 8) as u8; + key_history.put(pub_key, shared_secret); + } + assert_eq!(key_history.len(), KEY_CACHE_SIZE); + } +} diff --git a/implants/lib/transport/src/grpc.rs b/implants/lib/transport/src/grpc.rs index d7200d1a4..d220e4db6 100644 --- a/implants/lib/transport/src/grpc.rs +++ b/implants/lib/transport/src/grpc.rs @@ -4,7 +4,6 @@ use hyper::Uri; use pb::c2::*; use std::str::FromStr; use std::sync::mpsc::{Receiver, Sender}; -use tonic::codec::ProstCodec; use tonic::GrpcMethod; use tonic::Request; @@ -200,9 +199,7 @@ impl GRPC { format!("Service was not ready: {}", e), ) })?; - let codec: ProstCodec = - tonic::codec::ProstCodec::default(); - + let codec = pb::xchacha::ChachaCodec::default(); let path = tonic::codegen::http::uri::PathAndQuery::from_static(CLAIM_TASKS_PATH); let mut req = request.into_request(); req.extensions_mut() @@ -237,8 +234,7 @@ impl GRPC { format!("Service was not ready: {}", e), ) })?; - let codec: ProstCodec = - tonic::codec::ProstCodec::default(); + let codec = pb::xchacha::ChachaCodec::default(); let path = tonic::codegen::http::uri::PathAndQuery::from_static(FETCH_ASSET_PATH); let mut req = request.into_request(); req.extensions_mut() @@ -268,9 +264,7 @@ impl GRPC { format!("Service was not ready: {}", e), ) })?; - let codec: ProstCodec = - tonic::codec::ProstCodec::default(); - + let codec = pb::xchacha::ChachaCodec::default(); let path = tonic::codegen::http::uri::PathAndQuery::from_static(REPORT_CREDENTIAL_PATH); let mut req = request.into_request(); req.extensions_mut() @@ -302,8 +296,7 @@ impl GRPC { format!("Service was not ready: {}", e), ) })?; - let codec: ProstCodec = - tonic::codec::ProstCodec::default(); + let codec = pb::xchacha::ChachaCodec::default(); let path = tonic::codegen::http::uri::PathAndQuery::from_static(REPORT_FILE_PATH); let mut req = request.into_streaming_request(); req.extensions_mut() @@ -334,8 +327,7 @@ impl GRPC { format!("Service was not ready: {}", e), ) })?; - let codec: ProstCodec = - tonic::codec::ProstCodec::default(); + let codec = pb::xchacha::ChachaCodec::default(); let path = tonic::codegen::http::uri::PathAndQuery::from_static(REPORT_PROCESS_LIST_PATH); let mut req = request.into_request(); req.extensions_mut() @@ -361,8 +353,7 @@ impl GRPC { format!("Service was not ready: {}", e), ) })?; - let codec: ProstCodec = - tonic::codec::ProstCodec::default(); + let codec = pb::xchacha::ChachaCodec::default(); let path = tonic::codegen::http::uri::PathAndQuery::from_static(REPORT_TASK_OUTPUT_PATH); let mut req = request.into_request(); req.extensions_mut() @@ -389,8 +380,7 @@ impl GRPC { format!("Service was not ready: {}", e), ) })?; - let codec: ProstCodec = - tonic::codec::ProstCodec::default(); + let codec = pb::xchacha::ChachaCodec::default(); let path = tonic::codegen::http::uri::PathAndQuery::from_static(REVERSE_SHELL_PATH); let mut req = request.into_streaming_request(); req.extensions_mut() diff --git a/tavern/app.go b/tavern/app.go index d6f9df240..049b70748 100644 --- a/tavern/app.go +++ b/tavern/app.go @@ -2,8 +2,11 @@ package main import ( "context" + "crypto/ecdh" "crypto/ed25519" "crypto/rand" + "crypto/x509" + "encoding/base64" "fmt" "log" "log/slog" @@ -25,11 +28,13 @@ import ( "realm.pub/tavern/internal/c2" "realm.pub/tavern/internal/c2/c2pb" "realm.pub/tavern/internal/cdn" + "realm.pub/tavern/internal/cryptocodec" "realm.pub/tavern/internal/ent" "realm.pub/tavern/internal/ent/migrate" "realm.pub/tavern/internal/graphql" tavernhttp "realm.pub/tavern/internal/http" "realm.pub/tavern/internal/http/stream" + "realm.pub/tavern/internal/secrets" "realm.pub/tavern/internal/www" "realm.pub/tavern/tomes" ) @@ -340,9 +345,88 @@ func newGraphQLHandler(client *ent.Client, repoImporter graphql.RepoImporter) ht }) } +func generateKeyPair() (*ecdh.PublicKey, *ecdh.PrivateKey, error) { + curve := ecdh.X25519() + privateKey, err := curve.GenerateKey(rand.Reader) + if err != nil { + slog.Error(fmt.Sprintf("failed to generate private key: %v\n", err)) + return nil, nil, err + } + publicKey, err := curve.NewPublicKey(privateKey.PublicKey().Bytes()) + if err != nil { + slog.Error(fmt.Sprintf("failed to generate public key: %v\n", err)) + return nil, nil, err + } + + return publicKey, privateKey, nil +} + +func getKeyPair() (*ecdh.PublicKey, *ecdh.PrivateKey) { + curve := ecdh.X25519() + + var secretsManager secrets.SecretsManager + var err error + + if EnvSecretsManagerPath.String() == "" { + secretsManager, err = secrets.NewGcp("") + } else { + secretsManager, err = secrets.NewDebugFileSecrets(EnvSecretsManagerPath.String()) + } + if err != nil { + slog.Error("unable to setup secrets manager") + slog.Error("if you're running locally try setting `export SECRETS_FILE_PATH='/tmp/secrets'` \n") + panic("unable to connect to secrets manager") + } + + // Check if we already have a key + privateKeyString, err := secretsManager.GetValue("tavern_encryption_private_key") + if err != nil { + // Generate a new one if it doesn't exist + pubKey, privateKey, err := generateKeyPair() + if err != nil { + slog.Error(fmt.Sprintf("key generation failed: %v", err)) + return nil, nil + } + + privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + slog.Error(fmt.Sprintf("unable to marshal private key: %v", err)) + return nil, nil + } + _, err = secretsManager.SetValue("tavern_encryption_private_key", privateKeyBytes) + if err != nil { + slog.Error(fmt.Sprintf("unable to set 'tavern_encryption_private_key' using secrets manager: %v", err)) + return nil, nil + } + return pubKey, privateKey + } + + // Parse private key bytes + tmp, err := x509.ParsePKCS8PrivateKey(privateKeyString) + if err != nil { + slog.Error(fmt.Sprintf("unable to parse private key %v", err)) + } + privateKey := tmp.(*ecdh.PrivateKey) + + publicKey, err := curve.NewPublicKey(privateKey.PublicKey().Bytes()) + if err != nil { + slog.Error(fmt.Sprintf("failed to generate public key: %v\n", err)) + panic("failed to generate public key") + } + + return publicKey, privateKey +} + func newGRPCHandler(client *ent.Client, grpcShellMux *stream.Mux) http.Handler { + pub, priv := getKeyPair() + slog.Info(fmt.Sprintf("public key: %s", base64.StdEncoding.EncodeToString(pub.Bytes()))) + c2srv := c2.New(client, grpcShellMux) + xchacha := cryptocodec.StreamDecryptCodec{ + Csvc: cryptocodec.NewCryptoSvc(priv), + } grpcSrv := grpc.NewServer( + grpc.ForceServerCodecV2(xchacha), grpc.UnaryInterceptor(grpcWithUnaryMetrics), grpc.StreamInterceptor(grpcWithStreamMetrics), ) diff --git a/tavern/config.go b/tavern/config.go index bdd4565e3..6626f6c88 100644 --- a/tavern/config.go +++ b/tavern/config.go @@ -59,11 +59,11 @@ var ( // EnvMySQLUser defines the MySQL user to authenticate as. // EnvMySQLPasswd defines the password for the MySQL user to authenticate with. // EnvMySQLDB defines the name of the MySQL database to use. - EnvMySQLAddr = EnvString{"MYSQL_ADDR", ""} - EnvMySQLNet = EnvString{"MYSQL_NET", "tcp"} - EnvMySQLUser = EnvString{"MYSQL_USER", "root"} - EnvMySQLPasswd = EnvString{"MYSQL_PASSWD", ""} - EnvMySQLDB = EnvString{"MYSQL_DB", "tavern"} + EnvMySQLAddr = EnvString{"MYSQL_ADDR", ""} + EnvMySQLNet = EnvString{"MYSQL_NET", "tcp"} + EnvMySQLUser = EnvString{"MYSQL_USER", "root"} + EnvMySQLPasswd = EnvString{"MYSQL_PASSWD", ""} + EnvMySQLDB = EnvString{"MYSQL_DB", "tavern"} // EnvDBMaxIdleConns defines the maximum number of idle db connections to allow. // EnvDBMaxOpenConns defines the maximum number of open db connections to allow. @@ -89,6 +89,8 @@ var ( // EnvEnableMetrics enables the /metrics endpoint and HTTP server. It is unauthenticated and should be used carefully. EnvEnablePProf = EnvBool{"ENABLE_PPROF"} EnvEnableMetrics = EnvBool{"ENABLE_METRICS"} + + EnvSecretsManagerPath = EnvString{"SECRETS_FILE_PATH", ""} ) // Config holds information that controls the behaviour of Tavern diff --git a/tavern/internal/cryptocodec/cryptocodec.go b/tavern/internal/cryptocodec/cryptocodec.go new file mode 100644 index 000000000..61b390e91 --- /dev/null +++ b/tavern/internal/cryptocodec/cryptocodec.go @@ -0,0 +1,251 @@ +package cryptocodec + +import ( + "bytes" + "crypto/ecdh" + "crypto/rand" + "errors" + "fmt" + "log" + "log/slog" + "runtime" + "strconv" + "sync" + + "github.com/cloudflare/circl/dh/x25519" + "golang.org/x/crypto/chacha20poly1305" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/mem" +) + +// TODO: Switch to a gomap and mutex. +var session_pub_keys = NewSyncMap() + +type SyncMap struct { + Mutex sync.RWMutex // Read Write Mutex to allow for multiple readers + Map map[int][]byte // Example data map +} + +func NewSyncMap() *SyncMap { + return &SyncMap{Mutex: sync.RWMutex{}, Map: make(map[int][]byte)} +} + +func (s *SyncMap) Load(key int) ([]byte, bool) { + defer s.Mutex.Unlock() + s.Mutex.Lock() + res, ok := s.Map[key] + return res, ok +} + +func (s *SyncMap) Store(key int, value []byte) { + defer s.Mutex.Unlock() + s.Mutex.Lock() + s.Map[key] = value +} + +func (s *SyncMap) Delete(key int) { + defer s.Mutex.Unlock() + s.Mutex.Lock() + delete(s.Map, key) +} + +// TODO: Should we make this a random long byte array in case it gets used anywhere to avoid encrypting data with a weak key? - Sliver handles errors in this way. +var FAILURE_BYTES = []byte{} + +func castBytesToBufSlice(buf []byte) (mem.BufferSlice, error) { + r := bytes.NewBuffer(buf) + res, e := mem.ReadAll(r, mem.DefaultBufferPool()) + if e != nil { + slog.Error(fmt.Sprintf("failed to read failure_bytes %s", e)) + return res, e + } + return res, nil +} + +func init() { + log.Println("[INFO] Loading xchacha20-poly1305") + encoding.RegisterCodecV2(StreamDecryptCodec{}) +} + +type StreamDecryptCodec struct { + Csvc CryptoSvc +} + +func NewStreamDecryptCodec() StreamDecryptCodec { + return StreamDecryptCodec{} +} + +func (s StreamDecryptCodec) Marshal(v any) (mem.BufferSlice, error) { + id, err := goid() + if err != nil { + slog.Error(fmt.Sprintf("unable to find GOID %d", id)) + return castBytesToBufSlice(FAILURE_BYTES) + } + proto := encoding.GetCodecV2("proto") + res, err := proto.Marshal(v) + if err != nil { + slog.Error("Unable to marshall data") + return res, err + } + enc_res := s.Csvc.Encrypt(res.Materialize()) + byte_enc_res, err := castBytesToBufSlice(enc_res) + + return byte_enc_res, err +} + +func (s StreamDecryptCodec) Unmarshal(buf mem.BufferSlice, v any) error { + id, err := goid() + if err != nil { + slog.Error(fmt.Sprintf("unable to find GOID %d", id)) + return err + } + dec_buf, pub_key := s.Csvc.Decrypt(buf.Materialize()) + + session_pub_keys.Store(id, pub_key) + + proto := encoding.GetCodecV2("proto") + if proto == nil { + slog.Error("'proto' codec is not registered") + return errors.New("'proto' codec is not registered") + } + dec_mem_slice, err := castBytesToBufSlice(dec_buf) + if err != nil { + slog.Error("Unable to cast decrypted bytes to mem.BufferSlice") + return err + } + return proto.Unmarshal(dec_mem_slice, v) +} + +func (s StreamDecryptCodec) Name() string { + return "xchacha20-poly1305" +} + +type CryptoSvc struct { + // Aead cipher.AEAD + priv_key *ecdh.PrivateKey +} + +func NewCryptoSvc(priv_key *ecdh.PrivateKey) CryptoSvc { + return CryptoSvc{ + priv_key: priv_key, + } +} + +func (csvc *CryptoSvc) generate_shared_key(client_pub_key_bytes []byte) []byte { + x22519_curve := ecdh.X25519() + client_pub_key, err := x22519_curve.NewPublicKey(client_pub_key_bytes) + if err != nil { + slog.Error(fmt.Sprintf("failed to create public key %v", err)) + return FAILURE_BYTES + } + + shared_key, err := csvc.priv_key.ECDH(client_pub_key) + if err != nil { + slog.Error(fmt.Sprintf("failed to get shared secret %v", err)) + return FAILURE_BYTES + } + + return shared_key +} + +func (csvc *CryptoSvc) Decrypt(in_arr []byte) ([]byte, []byte) { + // Read in pub key + if len(in_arr) < x25519.Size { + slog.Error(fmt.Sprintf("input bytes to short %d expected at least %d", len(in_arr), x25519.Size)) + return FAILURE_BYTES, FAILURE_BYTES + } + + client_pub_key_bytes := in_arr[:x25519.Size] + + id, err := goid() + if err != nil { + slog.Error("failed to get goid") + return FAILURE_BYTES, FAILURE_BYTES + } + session_pub_keys.Store(id, client_pub_key_bytes) + + // Generate shared secret + derived_key := csvc.generate_shared_key(client_pub_key_bytes) + + aead, err := chacha20poly1305.NewX(derived_key) + if err != nil { + slog.Error(fmt.Sprintf("failed to create xchacha key %v", err)) + return FAILURE_BYTES, FAILURE_BYTES + } + + // Progress in_arr buf + in_arr = in_arr[x25519.Size:] + + // Read nonce & cipher text + if len(in_arr) < aead.NonceSize() { + slog.Error(fmt.Sprintf("input bytes to short %d expected at least %d", len(in_arr), aead.NonceSize())) + return FAILURE_BYTES, FAILURE_BYTES + } + nonce, ciphertext := in_arr[:aead.NonceSize()], in_arr[aead.NonceSize():] + + // Decrypt + plaintext, err := aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + slog.Error(fmt.Sprintf("failed to decrypt %v", err)) + return FAILURE_BYTES, FAILURE_BYTES + } + + return plaintext, client_pub_key_bytes +} + +// TODO: Don't use [] ref. +func (csvc *CryptoSvc) Encrypt(in_arr []byte) []byte { + // Get the client pub key? + id, err := goid() + if err != nil { + slog.Error(fmt.Sprintf("unable to find GOID %d", id)) + return FAILURE_BYTES + } + + client_pub_key_bytes, ok := session_pub_keys.Load(id) + if !ok { + slog.Error("Public key not found") + return FAILURE_BYTES + } + + // We should only need to use these once so delete it after use + session_pub_keys.Delete(id) + + // Generate shared secret + shared_key := csvc.generate_shared_key(client_pub_key_bytes) + aead, err := chacha20poly1305.NewX(shared_key) + if err != nil { + slog.Error(fmt.Sprintf("failed to create xchacha key %v", err)) + return FAILURE_BYTES + } + + nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(in_arr)+aead.Overhead()) + if _, err := rand.Read(nonce); err != nil { + slog.Error(fmt.Sprintf("Failed to encrypt %v", err)) + return FAILURE_BYTES + } + encryptedMsg := aead.Seal(nonce, nonce, in_arr, nil) + return append(client_pub_key_bytes, encryptedMsg...) +} + +// TODO: Find a better way +// This is terrible, slow, and should never be used. +func goid() (int, error) { + buf := make([]byte, 32) + n := runtime.Stack(buf, false) + buf = buf[:n] + // goroutine 1 [running]: ... + var goroutinePrefix = []byte("goroutine ") + var errBadStack = errors.New("invalid runtime.Stack output") + buf, ok := bytes.CutPrefix(buf, goroutinePrefix) + if !ok { + return 0, errBadStack + } + + i := bytes.IndexByte(buf, ' ') + if i < 0 { + return 0, errBadStack + } + + return strconv.Atoi(string(buf[:i])) +} diff --git a/tavern/internal/secrets/debug_file.go b/tavern/internal/secrets/debug_file.go new file mode 100644 index 000000000..77b6df8d9 --- /dev/null +++ b/tavern/internal/secrets/debug_file.go @@ -0,0 +1,172 @@ +package secrets + +import ( + "errors" + "fmt" + "log/slog" + "os" + + "gopkg.in/yaml.v3" +) + +const DEFAULT_PERMS = 0644 +const DELIMITER = "=" +const MEGABYTES = 1000000 +const MAX_FILE_SIZE = 128 * MEGABYTES + +type Secret struct { + Key string + Value string +} + + +type Secrets []Secret + + +type DebugFileSecrets struct { + Name string + Path string +} + +func NewDebugFileSecrets(path string) (SecretsManager, error) { + return DebugFileSecrets{ + Name: "DebugFileSecrets", + Path: path, + }, nil +} + +func (s DebugFileSecrets) GetName() string { + return s.Name +} + +func (s DebugFileSecrets) SetValue(key string, value []byte) ([]byte, error) { + path, err := s.ensureSecretsFileExist() + if err != nil { + slog.Error("failed to create secrets file %s: %v", path, err) + return []byte{}, err + } + + secrets, err := s.getYamlStruct(path) + if err != nil { + slog.Error("failed to parse YAML file %s: %v", path, err) + return []byte{}, err + } + + var old_value []byte = []byte{} + + // If the value exists update it + for idx, k := range secrets { + if k.Key == key { + secrets[idx].Value = string(value) + old_value = []byte(k.Value) + } + } + + // If the value doesn't exist create it + if len(old_value) == 0 { + secrets = append( + secrets, + Secret{ + Key: key, + Value: string(value), + }, + ) + } + + err = s.setYamlStruct(path, secrets) + if err != nil { + slog.Error("failed to update YAML file %s: %v", path, err) + return []byte{}, err + } + + return old_value, nil +} + +func (s DebugFileSecrets) GetValue(key string) ([]byte, error) { + path := s.Path + + secrets, err := s.getYamlStruct(path) + if err != nil { + slog.Error("failed to parse YAML file %s: %v", path, err) + return []byte{}, err + } + + for _, k := range secrets { + if k.Key == key { + return []byte(k.Value), nil + } + } + + return []byte{}, nil +} + +func (s DebugFileSecrets) setYamlStruct(path string, secrets Secrets) error { + data, err := yaml.Marshal(secrets) + if err != nil { + slog.Error("failed to parse file YAML %s: %v", path, err) + return err + } + + file, err := os.OpenFile(path, os.O_RDWR, DEFAULT_PERMS) + if err != nil { + slog.Error("failed to open secrets file %s: %v", path, err) + return err + } + defer file.Close() + + _, err = file.Write(data) + if err != nil { + slog.Error("failed to read file %s: %v", path, err) + return err + } + + return nil +} + +func (s DebugFileSecrets) getYamlStruct(path string) (Secrets, error) { + file, err := os.OpenFile(path, os.O_RDWR, DEFAULT_PERMS) + if err != nil { + slog.Error("failed to open secrets file %s: %v", path, err) + return Secrets{}, err + } + defer file.Close() + + data := make([]byte, MAX_FILE_SIZE) + n, err := file.Read(data) + if err != nil { + slog.Error("failed to read file %s: %v", path, err) + return Secrets{}, err + } + + data = data[0:n] + + var secrets Secrets + err = yaml.Unmarshal(data, &secrets) + if err != nil { + slog.Error("failed to parse file YAML %s: %v", path, err) + return Secrets{}, err + } + + return secrets, nil +} + +func (s DebugFileSecrets) ensureSecretsFileExist() (string, error) { + _, err := os.Stat(s.Path) + if errors.Is(err, os.ErrNotExist) { + // Create file + f, err := os.OpenFile(s.Path, os.O_CREATE, DEFAULT_PERMS) + if err != nil { + slog.Error(fmt.Sprintf("failed to create file %s", s.Path)) + return s.Path, err + } + defer f.Close() + + // Write empty struct to file + err = s.setYamlStruct(s.Path, Secrets{}) + if err != nil { + slog.Error("failed to set yaml struct") + return s.Path, err + } + } + return s.Path, nil +} diff --git a/tavern/internal/secrets/debug_file_test.go b/tavern/internal/secrets/debug_file_test.go new file mode 100644 index 000000000..add301533 --- /dev/null +++ b/tavern/internal/secrets/debug_file_test.go @@ -0,0 +1,93 @@ +package secrets_test + +import ( + "log" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "realm.pub/tavern/internal/secrets" +) + +func createTestSecrets(t *testing.T) string { + // TODO: How do I clean up this test file 🫠 + tmpDir := t.TempDir() + secretsPath := path.Join(tmpDir, "secrets.yaml") + data := []byte(` +- key: TAVERN_PRIVATE_KEY + value: !!binary Fe8E/SVf7MLgCHzBWvjdnavejsJijJZD9mPvLQbgybE= +- key: super_secret_test_data + value: hello world! +`) + err := os.WriteFile(secretsPath, data, 0644) + if err != nil { + log.Fatalf("Failed to write test file %s: %v", secretsPath, err) + } + + return secretsPath +} + +func TestGetSecretsFile(t *testing.T) { + path := createTestSecrets(t) + + secretsManager, err := secrets.NewDebugFileSecrets(path) + assert.Nil(t, err) + res, err := secretsManager.GetValue("super_secret_test_data") + assert.Nil(t, err) + assert.Equal(t, []byte("hello world!"), res) + + res, err = secretsManager.GetValue("TAVERN_PRIVATE_KEY") + assert.Nil(t, err) + assert.Equal(t, []byte{0x15, 0xef, 0x04, 0xfd, 0x25, 0x5f, 0xec, 0xc2, 0xe0, 0x08, 0x7c, 0xc1, 0x5a, 0xf8, 0xdd, 0x9d, 0xab, 0xde, 0x8e, 0xc2, 0x62, 0x8c, 0x96, 0x43, 0xf6, 0x63, 0xef, 0x2d, 0x06, 0xe0, 0xc9, 0xb1}, res) +} + +func TestSetSecretsAddFile(t *testing.T) { + path := createTestSecrets(t) + + secretsManager, err := secrets.NewDebugFileSecrets(path) + assert.Nil(t, err) + res, err := secretsManager.SetValue("WOAH!", []byte("This works!")) + assert.Nil(t, err) + assert.Equal(t, []byte{}, res) + + res, err = secretsManager.GetValue("WOAH!") + assert.Nil(t, err) + assert.Equal(t, []byte("This works!"), res) + + // --- + + res, err = secretsManager.SetValue("WOAH!", []byte("This works too!")) + assert.Nil(t, err) + assert.Equal(t, []byte("This works!"), res) + + res, err = secretsManager.GetValue("WOAH!") + assert.Nil(t, err) + assert.Equal(t, []byte("This works too!"), res) + + // --- + + res, err = secretsManager.GetValue("TAVERN_PRIVATE_KEY") + assert.Nil(t, err) + assert.Equal(t, []byte{0x15, 0xef, 0x04, 0xfd, 0x25, 0x5f, 0xec, 0xc2, 0xe0, 0x08, 0x7c, 0xc1, 0x5a, 0xf8, 0xdd, 0x9d, 0xab, 0xde, 0x8e, 0xc2, 0x62, 0x8c, 0x96, 0x43, 0xf6, 0x63, 0xef, 0x2d, 0x06, 0xe0, 0xc9, 0xb1}, res) +} + +func TestSetSecretsNewFile(t *testing.T) { + tmpDir := t.TempDir() + path := path.Join(tmpDir, "secrets.yaml") + + secretsManager, err := secrets.NewDebugFileSecrets(path) + assert.Nil(t, err) + res, err := secretsManager.GetValue("super_secret_test_data") + // Make sure this fails + assert.NotNil(t, err) + assert.Equal(t, []byte{}, res) + + res, err = secretsManager.SetValue("super_secret_test_data", []byte{0x99, 0x99}) + assert.Nil(t, err) + assert.Equal(t, []byte{}, res) + + res, err = secretsManager.GetValue("super_secret_test_data") + assert.Nil(t, err) + assert.Equal(t, []byte{0x99, 0x99}, res) +} diff --git a/tavern/internal/secrets/gcp.go b/tavern/internal/secrets/gcp.go new file mode 100644 index 000000000..89c10a49d --- /dev/null +++ b/tavern/internal/secrets/gcp.go @@ -0,0 +1,140 @@ +package secrets + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "log/slog" + "strings" + + secretmanager "cloud.google.com/go/secretmanager/apiv1" + "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" + "golang.org/x/oauth2/google" + "google.golang.org/api/compute/v1" +) + +type Gcp struct { + Name string + projectID string + prefix string + client *secretmanager.Client + clientctx context.Context +} + +// GetName implements SecretsManager. +func (g Gcp) GetName() string { + return g.Name +} + +// GetValue implements SecretsManager. +func (g Gcp) GetValue(key string) ([]byte, error) { + name := fmt.Sprintf("projects/%s/secrets/%s_%s/versions/latest", g.projectID, g.prefix, key) + + // Build the request. + accessRequest := &secretmanagerpb.AccessSecretVersionRequest{ + Name: name, + } + + // Call the API. + result, err := g.client.AccessSecretVersion(g.clientctx, accessRequest) + if err != nil { + slog.Error(fmt.Sprintf("failed to access secret version: %v", err)) + return []byte{}, err + } + + return result.Payload.Data, nil +} + +type credentialsJson struct { + ProjectID string `json:"quota_project_id"` +} + +func GetCurrentGcpProject(ctx context.Context) (string, error) { + respMesg, err := google.FindDefaultCredentials(ctx, compute.ComputeScope) + if err != nil { + return "", err + } + + if respMesg.ProjectID != "" { + return respMesg.ProjectID, nil + } + + // respMesg.ProjectID can be empty so instead we grab from the creds JSON file + credJSON := credentialsJson{} + err = json.Unmarshal(respMesg.JSON, &credJSON) + if err != nil { + return "", err + } + ProjectID := credJSON.ProjectID + + if ProjectID == "" { + return "", errors.New("project id is empty") + } + + return ProjectID, nil +} + +// SetValue implements SecretsManager. +func (g Gcp) SetValue(key string, value []byte) ([]byte, error) { + // Create the request to create the secret. + parent := fmt.Sprintf("projects/%s", g.projectID) + + old_value, err := g.GetValue(key) + if err != nil && !strings.Contains(err.Error(), "code = NotFound") { + slog.Error(fmt.Sprintf("failed to get old secret: %v", err)) + return []byte{}, err + } + + // Declare the payload to store. + path := fmt.Sprintf("%s/secrets/%s_%s", parent, g.prefix, key) + payload := []byte(value) + + // Build the request. + addSecretVersionReq := &secretmanagerpb.AddSecretVersionRequest{ + Parent: path, + Payload: &secretmanagerpb.SecretPayload{ + Data: payload, + }, + } + + // Call the API. + _, err = g.client.AddSecretVersion(g.clientctx, addSecretVersionReq) + if err != nil { + log.Fatalf("failed to add secret version: %v", err) + } + + return old_value, nil +} + +func NewGcp(projectID string) (SecretsManager, error) { + ctx := context.Background() + + // If unset try to figure out the current GCP + if projectID == "" { + // GCP project in which to store secrets in Secret Manager. + tmp, err := GetCurrentGcpProject(ctx) + projectID = tmp + if err != nil { + slog.Error(fmt.Sprintf("failed to get current project ID: %v\n", err)) + return nil, err + } + } + log.Printf("[DEBUG] Using projectID: %s\n", projectID) + + // Create the client. + client, err := secretmanager.NewClient(ctx) + if err != nil { + slog.Error(fmt.Sprintf("failed to setup client: %v\n", err)) + return nil, err + } + + return Gcp{ + Name: "Gcp", + projectID: projectID, + prefix: "REALM", + client: client, + clientctx: ctx, + }, nil +} diff --git a/tavern/internal/secrets/gcp_test.go b/tavern/internal/secrets/gcp_test.go new file mode 100644 index 000000000..a2f7080cd --- /dev/null +++ b/tavern/internal/secrets/gcp_test.go @@ -0,0 +1,37 @@ +package secrets_test + +// No way to test this in CI but worked in dev + +// import ( +// "testing" + +// "github.com/stretchr/testify/assert" +// "realm.pub/tavern/internal/secrets" +// ) + +// func TestSetSecretsGcp(t *testing.T) { +// test_key := "super_secret_test_data" +// // Create manager +// secretsManager, err := secrets.NewGcp("") +// assert.NotNil(t, secretsManager) +// assert.Nil(t, err) +// // Get a non existent value +// res, err := secretsManager.GetValue(test_key) +// assert.NotNil(t, err) +// assert.Equal(t, []byte(""), res) + +// // Create a value +// res, err = secretsManager.SetValue(test_key, []byte("This should work")) +// assert.Nil(t, err) +// assert.Equal(t, []byte(""), res) + +// // Update the value +// res, err = secretsManager.SetValue(test_key, []byte{0x99, 0x99}) +// assert.Nil(t, err) +// assert.Equal(t, []byte("This should work"), res) + +// // Get the value +// res, err = secretsManager.GetValue(test_key) +// assert.Nil(t, err) +// assert.Equal(t, []byte{0x99, 0x99}, res) +// } diff --git a/tavern/internal/secrets/secrets.go b/tavern/internal/secrets/secrets.go new file mode 100644 index 000000000..0cfb8c22d --- /dev/null +++ b/tavern/internal/secrets/secrets.go @@ -0,0 +1,7 @@ +package secrets + +type SecretsManager interface { + GetName() string + SetValue(string, []byte) ([]byte, error) + GetValue(string) ([]byte, error) +} diff --git a/tavern/main_test.go b/tavern/main_test.go index c72d524f8..809ae7ffc 100644 --- a/tavern/main_test.go +++ b/tavern/main_test.go @@ -2,17 +2,22 @@ package main import ( "os" + "path" "testing" ) // TestMainFunc runs main after configuring the application to immediately exit. // This validates our default configurations are successful. func TestMainFunc(t *testing.T) { + tmpDir := t.TempDir() + path := path.Join(tmpDir, "secrets.yaml") + os.Setenv(EnvEnableTestRunAndExit.Key, "1") os.Setenv(EnvHTTPListenAddr.Key, "127.0.0.1:8080") os.Setenv(EnvHTTPMetricsListenAddr.Key, "127.0.0.1:8081") os.Setenv(EnvEnablePProf.Key, "1") os.Setenv(EnvEnableMetrics.Key, "1") + os.Setenv(EnvSecretsManagerPath.Key, path) defer func() { unsetList := []string{ EnvEnableTestRunAndExit.Key, diff --git a/terraform/main.tf b/terraform/main.tf index 2eef33f59..2542a0583 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -42,6 +42,11 @@ variable "gcp_project" { error_message = "Must provide a valid gcp_project" } } + +data "google_project" "project" { + project_id = var.gcp_project +} + variable "gcp_region" { type = string description = "GCP Region for deployment" @@ -134,6 +139,11 @@ resource "google_project_service" "cloud_run_api" { disable_on_destroy = false } +resource "google_project_service" "secret_manager" { + service = "secretmanager.googleapis.com" + disable_on_destroy = false +} + resource "google_project_service" "cloud_sqladmin_api" { service = "sqladmin.googleapis.com" disable_on_destroy = false @@ -176,6 +186,48 @@ locals { prometheus_container_name = "prometheus-sidecar" } +resource "google_service_account" "svctavern" { + account_id = "svctavern" + description = "The service account Realm's Tavern uses to connect to GCP based services. Managed by Terraform." +} + +resource "google_secret_manager_secret" "tavern-grpc-priv-key" { + secret_id = "REALM_tavern_encryption_private_key" + + replication { + auto { + } + } +} + +resource "google_secret_manager_secret_iam_binding" "tavern-secrets-binding" { + project = var.gcp_project + secret_id = google_secret_manager_secret.tavern-grpc-priv-key.secret_id + role = "roles/secretmanager.secretAccessor" + members = [ + "serviceAccount:${google_service_account.svctavern.email}", + ] +} + +resource "google_project_iam_member" "tavern-sqlclient-binding" { + project = var.gcp_project + role = "roles/cloudsql.client" + member = "serviceAccount:${google_service_account.svctavern.email}" +} + +resource "google_project_iam_member" "tavern-metricwriter-binding" { + project = var.gcp_project + role = "roles/monitoring.metricWriter" + member = "serviceAccount:${google_service_account.svctavern.email}" +} + +resource "google_project_iam_member" "tavern-logwriter-binding" { + project = var.gcp_project + role = "roles/logging.logWriter" + member = "serviceAccount:${google_service_account.svctavern.email}" +} + + resource "google_pubsub_topic" "shell_input" { count = var.disable_gcp_pubsub ? 0 : 1 name = var.gcp_pubsub_topic_shell_input @@ -206,6 +258,7 @@ resource "google_cloud_run_service" "tavern" { template { spec { + service_account_name = google_service_account.svctavern.email // Controls request timeout, must be long-lived to enable reverse shell support timeout_seconds = var.tavern_request_timeout_seconds @@ -335,6 +388,10 @@ resource "google_cloud_run_service" "tavern" { autogenerate_revision_name = true depends_on = [ + google_project_iam_member.tavern-sqlclient-binding, + google_secret_manager_secret_iam_binding.tavern-secrets-binding, + google_project_iam_member.tavern-metricwriter-binding, + google_project_iam_member.tavern-logwriter-binding, google_project_service.cloud_run_api, google_project_service.cloud_sqladmin_api, google_sql_user.tavern-user,