diff --git a/go.mod b/go.mod index 1743235c..a179117a 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.9.1 github.com/go-mysql-org/go-mysql v1.13.0 + github.com/gofrs/flock v0.8.1 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/h2non/filetype v1.1.3 @@ -111,7 +112,6 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.5 // indirect diff --git a/go.sum b/go.sum index f23936e0..95e0e783 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -20,8 +18,6 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= -cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -31,35 +27,25 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0 h1:9x7Bx0A9R5/M9jibeJeZWqjeVEIxYW9fZYqB9a70/bY= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw= cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ= -cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k= -cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/translate v1.10.3 h1:g+B29z4gtRGsiKDoTF+bNeH25bLRokAaElygX2FcZkE= -cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= @@ -68,14 +54,9 @@ github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9 github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8= github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto= github.com/Infisical/turn/v4 v4.0.1 h1:omdelNsnFfzS5cu86W5OBR68by68a8sva4ogR0lQQnw= @@ -86,28 +67,15 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= -github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= -github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -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/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g= @@ -142,11 +110,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b h1:baFN6AnR0SeC194X2D292IUZcHDs4JjStpqtE70fjXE= github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b/go.mod h1:Ram6ngyPDmP+0t6+4T2rymv0w0BS9N8Ch5vvUJccw5o= @@ -154,7 +119,6 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -172,38 +136,23 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= -github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= -github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 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.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= -github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= -github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8 h1:LpMLYGyy67BoAFGda1NeOBQwqlv7nUXpm+rIVHGxZZ4= -github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= -github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 h1:MZRmHqDBd0vxNwenEbKSQqRVT24d3C05ft8kduSwlqM= -github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -233,11 +182,8 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= -github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= @@ -257,17 +203,12 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w= github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0= github.com/gitleaks/go-gitdiff v0.9.1/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -293,8 +234,6 @@ github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrt github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= -github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -323,7 +262,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -350,8 +288,6 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= @@ -370,15 +306,11 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= -github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -393,7 +325,6 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= @@ -415,67 +346,45 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= -github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= -github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -509,37 +418,26 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= -github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= -github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e h1:a+PGEeXb+exwBS3NboqXHyxarD9kaboBbrSp+7GuBuc= -github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -567,22 +465,15 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -591,8 +482,6 @@ github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxd github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= -github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -601,8 +490,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4= @@ -621,9 +508,6 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oiweiwei/go-math v1.0.0 h1:eBxtydohwi0RTfSX8EPQRO7Xt8j4Ck5v7PVv8Hvp7jg= github.com/oiweiwei/go-math v1.0.0/go.mod h1:rWG2PIZRYIjovChTlCcssArtO/s2pVj38c1h2QfH1UM= @@ -643,18 +527,13 @@ github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/oracle/oci-go-sdk/v65 v65.95.2 h1:0HJ0AgpLydp/DtvYrF2d4str2BjXOVAeNbuW7E07g94= github.com/oracle/oci-go-sdk/v65 v65.95.2/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.5-0.20250318082626-8f80e5cb09ec h1:3EiGmeJWoNixU+EwllIn26x6s4njiWRXewdx2zlYa84= github.com/pingcap/errors v0.11.5-0.20250318082626-8f80e5cb09ec/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= -github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE= -github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4= github.com/pingcap/log v1.1.1-0.20241212030209-7e3ff8601a2a h1:WIhmJBlNGmnCWH6TLMdZfNEDaiU8cFpZe3iaqDbQ0M8= github.com/pingcap/log v1.1.1-0.20241212030209-7e3ff8601a2a/go.mod h1:ORfBOFp1eteu2odzsyaxI+b8TzJwgjwyQcGhI+9SfEA= github.com/pingcap/tidb/pkg/parser v0.0.0-20250421232622-526b2c79173d h1:3Ej6eTuLZp25p3aH/EXdReRHY12hjZYs3RrGp7iLdag= @@ -674,51 +553,36 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a h1:Ey0XWvrg6u6hyIn1Kd/jCCmL+bMv9El81tvuGBbxZGg= github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= -github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smallnest/resp3 v0.0.0-20251228151914-4f2fa7427e69 h1:AkDv2coi+ZsMlEp/6V21FWxdswSIEzqflgJ6snIQG+U= github.com/smallnest/resp3 v0.0.0-20251228151914-4f2fa7427e69/go.mod h1:cmfXTZVXEA7xFOYcGnpKp2VeFf6FUHmxdKQHVNE6BXY= @@ -750,8 +614,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -774,13 +636,10 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde h1:AMNpJRc7P+GTwVbl8DkK2I9I8BBUzNiHuH/tlxrpan0= github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde/go.mod h1:MvrEmduDUz4ST5pGZ7CABCnOU5f3ZiOAZzT6b1A6nX8= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -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/wasilibs/go-re2 v1.10.0 h1:vQZEBYZOCA9jdBMmrO4+CvqyCj0x4OomXTJ4a5/urQ0= github.com/wasilibs/go-re2 v1.10.0/go.mod h1:k+5XqO2bCJS+QpGOnqugyfwC04nw0jaglmjrrkG8U6o= @@ -798,7 +657,6 @@ github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= @@ -810,13 +668,9 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/etcd/api/v3 v3.5.0 h1:GsV3S+OfZEOCNXdtNkBSR7kgLobAa/SO6tCxRa0GAYw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0 h1:2aQv6F436YnN7I4VbI8PPYrBhu+SmrTaADcf8Mi/6PU= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= @@ -836,8 +690,6 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= @@ -846,8 +698,6 @@ go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= -go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= @@ -900,7 +750,6 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c= golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 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= @@ -913,10 +762,8 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -929,8 +776,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1149,7 +994,6 @@ golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= @@ -1184,8 +1028,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1231,8 +1073,6 @@ google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH8 google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20260203192932-546029d2fa20 h1:zQTtWukWCqGTV6Pt60SqvPGnEi2CE3PeeIRlu4SYgAc= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20260203192932-546029d2fa20/go.mod h1:Tej9lWiwVvQJP+b43pjJIsr/3mZycXWCIyoiXmbFf40= google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1278,10 +1118,7 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= @@ -1304,7 +1141,6 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.31.4 h1:I2QNzitPVsPeLQvexMEsj945QumYraqv9m74isPDKhM= k8s.io/api v0.31.4/go.mod h1:d+7vgXLvmcdT1BCo79VEgJxHHryww3V5np2OYTr6jdw= @@ -1312,33 +1148,14 @@ k8s.io/apimachinery v0.31.4 h1:8xjE2C4CzhYVm9DGf60yohpNUh5AEBnPxCryPBECmlM= k8s.io/apimachinery v0.31.4/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/client-go v0.31.4 h1:t4QEXt4jgHIkKKlx06+W3+1JOwAFU/2OPiOo7H92eRQ= k8s.io/client-go v0.31.4/go.mod h1:kvuMro4sFYIa8sulL5Gi5GFqUPvfH2O/dXuKstbaaeg= -k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7 h1:cErOOTkQ3JW19o4lo91fFurouhP8NcoBvb7CkvhZZpk= -k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= 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-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= -lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q= -modernc.org/golex v1.1.0 h1:dmSaksHMd+y6NkBsRsCShNPRaSNCNH+abrVm5/gZic8= -modernc.org/golex v1.1.0/go.mod h1:2pVlfqApurXhR1m0N+WDYu6Twnc4QuvO4+U8HnwoiRA= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/parser v1.1.0 h1:XoClYpoz2xHEDIteSQ7tICOTFcNwBI7XRCeghUS6SNI= -modernc.org/parser v1.1.0/go.mod h1:CXl3OTJRZij8FeMpzI3Id/bjupHf0u9HSrCUP4Z9pbA= -modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= -modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/y v1.1.0 h1:JdIvLry+rKeSsVNRCdr6YWYimwwNm0GXtzxid77VfWc= -modernc.org/y v1.1.0/go.mod h1:Iz3BmyIS4OwAbwGaUS7cqRrLsSsfp2sFWtpzX+P4CsE= -rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= diff --git a/packages/agentproxy/ca.go b/packages/agentproxy/ca.go index 6c56502a..8a79e146 100644 --- a/packages/agentproxy/ca.go +++ b/packages/agentproxy/ca.go @@ -27,11 +27,17 @@ const ( leafTTL = 24 * time.Hour leafReuseMargin = 1 * time.Hour maxLeafCacheEntries = 8192 + + // localRootTTL bounds the in-memory self-signed local root; just needs to outlast one session. + localRootTTL = 7 * 24 * time.Hour ) type caManager struct { token func() string + // local: the intermediate fields hold a self-signed root minted at construction, never re-signed. + local bool + mu sync.Mutex intermediateKey *ecdsa.PrivateKey intermediateCert *x509.Certificate @@ -55,7 +61,75 @@ func newCaManager(token func() string) *caManager { } } +// generateLocalRoot mints a self-signed ECDSA P-256 root (P-256 so rustls-based agents accept it). +func generateLocalRoot() (*ecdsa.PrivateKey, *x509.Certificate, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate local root CA key: %w", err) + } + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, err + } + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: "Infisical Agent Proxy Local Root CA"}, + NotBefore: time.Now().Add(-1 * time.Minute), + NotAfter: time.Now().Add(localRootTTL), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + } + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return nil, nil, fmt.Errorf("failed to self-sign local root CA: %w", err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse local root CA certificate: %w", err) + } + return key, cert, nil +} + +// caManagerFromRoot wraps an existing local root key/cert as a caManager. +func caManagerFromRoot(key *ecdsa.PrivateKey, cert *x509.Certificate) *caManager { + return &caManager{ + local: true, + intermediateKey: key, + intermediateCert: cert, + intermediateExp: cert.NotAfter, + leafCache: make(map[string]*leafEntry), + } +} + +// newLocalCaManager builds a fresh in-memory local root. The private key never leaves memory. +func newLocalCaManager() (*caManager, error) { + key, cert, err := generateLocalRoot() + if err != nil { + return nil, err + } + return caManagerFromRoot(key, cert), nil +} + +// RootPEM returns the local root's public certificate (nil outside local mode). +func (c *caManager) RootPEM() []byte { + if !c.local { + return nil + } + c.mu.Lock() + defer c.mu.Unlock() + if c.intermediateCert == nil { + return nil + } + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.intermediateCert.Raw}) +} + func (c *caManager) ensureIntermediate() error { + // The local root is minted once at construction and is never re-signed. + if c.local { + return nil + } c.mu.Lock() defer c.mu.Unlock() diff --git a/packages/agentproxy/cache.go b/packages/agentproxy/cache.go index 39146cb5..abc254b7 100644 --- a/packages/agentproxy/cache.go +++ b/packages/agentproxy/cache.go @@ -202,8 +202,37 @@ func (a *agentCache) evictIfFullLocked(incoming string) { } func (a *agentCache) resolve(jwt string, scope agentScope) ([]*resolvedService, error) { - agentClient := resty.New().SetAuthToken(jwt) - listResp, err := api.CallListProxiedServices(agentClient, api.ListProxiedServicesRequest{ + return resolveServices(scope, resolveParams{ + discoveryToken: jwt, + valueToken: a.proxyToken, + includeNonProxyable: false, + registerDynamic: func(cred api.ProxiedServiceCredential, projectSlug string) *dynamicCredentialRef { + key := leaseKey{ + jwt: jwt, + scope: scope, + secretName: cred.DynamicSecretName, + configHash: canonicalConfigHash(cred.DynamicSecretConfig), + } + a.leases.register(key, leaseSpec{projectSlug: projectSlug, config: cred.DynamicSecretConfig}) + return &dynamicCredentialRef{key: key, field: cred.DynamicSecretField} + }, + }) +} + +// resolveParams holds what differs between the remote (agentCache) and local (localResolver) resolvers. +type resolveParams struct { + discoveryToken string + valueToken func() string + includeNonProxyable bool + // registerDynamic maps a dynamic-secret credential to its lease ref, or returns nil to skip it. + registerDynamic func(cred api.ProxiedServiceCredential, projectSlug string) *dynamicCredentialRef +} + +// resolveServices lists the proxied services for a scope and attaches credential values. Shared by +// both resolvers; the differences live in resolveParams. +func resolveServices(scope agentScope, p resolveParams) ([]*resolvedService, error) { + client := resty.New().SetAuthToken(p.discoveryToken) + listResp, err := api.CallListProxiedServices(client, api.ListProxiedServicesRequest{ ProjectID: scope.projectID, Environment: scope.environment, SecretPath: scope.secretPath, @@ -212,11 +241,11 @@ func (a *agentCache) resolve(jwt string, scope agentScope) ([]*resolvedService, return nil, fmt.Errorf("failed to discover proxied services: %w", err) } - secretValues := a.fetchSecretValues(scope, referencedStaticKeys(listResp.Services)) + secretValues := fetchSecretValues(p.valueToken, scope, referencedStaticKeys(listResp.Services, p.includeNonProxyable)) var services []*resolvedService for _, svc := range listResp.Services { - if !svc.CanProxy { + if !p.includeNonProxyable && !svc.CanProxy { continue } rs := &resolvedService{ @@ -227,13 +256,10 @@ func (a *agentCache) resolve(jwt string, scope agentScope) ([]*resolvedService, } for _, cred := range svc.Credentials { if cred.DynamicSecretName != "" { - key := leaseKey{ - jwt: jwt, - scope: scope, - secretName: cred.DynamicSecretName, - configHash: canonicalConfigHash(cred.DynamicSecretConfig), + ref := p.registerDynamic(cred, listResp.ProjectSlug) + if ref == nil { + continue } - a.leases.register(key, leaseSpec{projectSlug: listResp.ProjectSlug, config: cred.DynamicSecretConfig}) rs.credentials = append(rs.credentials, resolvedCredential{ role: cred.Role, headerName: cred.HeaderName, @@ -241,7 +267,7 @@ func (a *agentCache) resolve(jwt string, scope agentScope) ([]*resolvedService, headerPurpose: cred.HeaderPurpose, placeholder: cred.PlaceholderValue, surfaces: cred.SubstitutionSurfaces, - dynamic: &dynamicCredentialRef{key: key, field: cred.DynamicSecretField}, + dynamic: ref, }) continue } @@ -270,11 +296,11 @@ func (a *agentCache) resolve(jwt string, scope agentScope) ([]*resolvedService, return services, nil } -func referencedStaticKeys(services []api.ProxiedService) []string { +func referencedStaticKeys(services []api.ProxiedService, includeNonProxyable bool) []string { seen := make(map[string]struct{}) var keys []string for _, svc := range services { - if !svc.CanProxy { + if !includeNonProxyable && !svc.CanProxy { continue } for _, cred := range svc.Credentials { @@ -291,12 +317,12 @@ func referencedStaticKeys(services []api.ProxiedService) []string { return keys } -// fetchSecretValues resolves referenced static secrets individually so a key the proxy can't read is -// skipped rather than failing the whole agent. -func (a *agentCache) fetchSecretValues(scope agentScope, keys []string) map[string]string { +// fetchSecretValues resolves referenced static secrets individually so a key the value identity can't +// read is skipped rather than failing the whole agent. +func fetchSecretValues(token func() string, scope agentScope, keys []string) map[string]string { values := make(map[string]string, len(keys)) for _, key := range keys { - secret, _, err := util.GetSinglePlainTextSecretByNameV3(a.proxyToken(), scope.projectID, scope.environment, scope.secretPath, key) + secret, _, err := util.GetSinglePlainTextSecretByNameV3(token(), scope.projectID, scope.environment, scope.secretPath, key) if err != nil { log.Warn().Err(err).Msgf("agent proxy: skipping static secret %q; proxy identity cannot read it", key) continue @@ -306,6 +332,13 @@ func (a *agentCache) fetchSecretValues(scope agentScope, keys []string) map[stri return values } +// close drops every cached entry so credential values become unreachable. Called at proxy shutdown. +func (a *agentCache) close() { + a.mu.Lock() + defer a.mu.Unlock() + a.entries = make(map[string]*agentEntry) +} + func (a *agentCache) refreshActive() { type refreshTarget struct { key string diff --git a/packages/agentproxy/local.go b/packages/agentproxy/local.go new file mode 100644 index 00000000..17f8def1 --- /dev/null +++ b/packages/agentproxy/local.go @@ -0,0 +1,132 @@ +package agentproxy + +import ( + "sync" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/rs/zerolog/log" +) + +// LocalOptions switches the proxy into local coupled mode (`agent-proxy run`): one developer, one +// agent, one scope. Requests carry no Proxy-Authorization; every request uses the scope and token +// fixed here, and API calls use the developer's own token. +type LocalOptions struct { + ProjectID string + Environment string + SecretPath string + + // UserToken returns the developer's current access token; called per request so it can rotate. + UserToken func() string + + // CADir, if set, persists the local root CA there (reused across runs and trustable in the OS + // trust store) instead of a fresh in-memory root. Must be a sandbox-denied path so the agent + // cannot read the key. + CADir string + + // IdentityID/IdentityName label activity records (no wire JWT to decode them from). + IdentityID string + IdentityName string +} + +func (l *LocalOptions) scope() agentScope { + return agentScope{projectID: l.ProjectID, environment: l.Environment, secretPath: l.SecretPath} +} + +// localResolver is the single-snapshot, single-scope counterpart of agentCache (no keyed map, since +// the one caller is fixed at startup). The snapshot refreshes on the poll loop and is dropped +// (fail closed) when the developer's authorization goes away. +type localResolver struct { + opts *LocalOptions + + mu sync.Mutex + services []*resolvedService + valid bool + + dynamicWarned bool +} + +func newLocalResolver(opts *LocalOptions) *localResolver { + return &localResolver{opts: opts} +} + +func (l *localResolver) get(_ string, _ agentScope) ([]*resolvedService, error) { + l.mu.Lock() + if l.valid { + snapshot := l.services + l.mu.Unlock() + return snapshot, nil + } + l.mu.Unlock() + + resolved, err := l.resolveSnapshot() + if err != nil { + return nil, err + } + l.mu.Lock() + l.services = resolved + l.valid = true + l.mu.Unlock() + return resolved, nil +} + +func (l *localResolver) resolveSnapshot() ([]*resolvedService, error) { + // One identity for discovery and value-fetch, and no CanProxy filter: locally the gate is Read + // Value alone. + token := l.opts.UserToken() + return resolveServices(l.opts.scope(), resolveParams{ + discoveryToken: token, + valueToken: func() string { return token }, + includeNonProxyable: true, + registerDynamic: func(cred api.ProxiedServiceCredential, _ string) *dynamicCredentialRef { + l.mu.Lock() + warned := l.dynamicWarned + l.dynamicWarned = true + l.mu.Unlock() + if !warned { + log.Warn().Msgf("proxied service references dynamic secret %q; dynamic secrets are not supported in local mode, skipping those credentials", cred.DynamicSecretName) + } + return nil + }, + }) +} + +func (l *localResolver) identity(_ string, _ agentScope) (string, string, bool) { + return l.opts.IdentityID, l.opts.IdentityName, true +} + +func (l *localResolver) refreshActive() { + l.mu.Lock() + haveSnapshot := l.valid + l.mu.Unlock() + if !haveSnapshot { + return + } + + resolved, err := l.resolveSnapshot() + if err != nil { + if isAuthError(err) { + log.Warn().Err(err).Msg("developer authorization no longer valid; dropping brokered credentials") + l.close() + return + } + log.Warn().Err(err).Msg("failed to refresh brokered credentials; keeping the previous snapshot") + return + } + l.mu.Lock() + l.services = resolved + l.valid = true + l.mu.Unlock() +} + +// activeJWTs is for the lease loop; local mode registers no leases. +func (l *localResolver) activeJWTs() map[string]struct{} { + return map[string]struct{}{} +} + +// close drops the snapshot (fail closed); the next request must re-resolve. +func (l *localResolver) close() { + l.mu.Lock() + defer l.mu.Unlock() + l.services = nil + l.valid = false +} diff --git a/packages/agentproxy/local_ca_store.go b/packages/agentproxy/local_ca_store.go new file mode 100644 index 00000000..be2a90c2 --- /dev/null +++ b/packages/agentproxy/local_ca_store.go @@ -0,0 +1,132 @@ +package agentproxy + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/gofrs/flock" +) + +const ( + localCACertFile = "local-ca.crt" + localCAKeyFile = "local-ca.key" + // localCARenewMargin regenerates the persistent root before it actually expires. + localCARenewMargin = 24 * time.Hour +) + +// LocalCACertPath is the persistent root's public-cert path within a CA dir (what the OS trust store +// and the child's CA bundle reference). +func LocalCACertPath(dir string) string { return filepath.Join(dir, localCACertFile) } + +// newPersistentLocalCaManager loads the local root from dir, or generates and writes one if it is +// missing, unparseable, or within localCARenewMargin of expiry (self-heal). The key stays on disk so +// the same root is reused across runs (and can be trusted once in the OS trust store). dir must be a +// path the sandbox denies, so the agent cannot read the key. +// +// A file lock serializes concurrent first-runs so two processes don't both generate/write. +func newPersistentLocalCaManager(dir string) (*caManager, error) { + if err := os.MkdirAll(dir, 0o700); err != nil { + return nil, fmt.Errorf("failed to create local CA dir: %w", err) + } + unlock, err := lockDir(dir) + if err != nil { + return nil, err + } + defer unlock() + + certPath := filepath.Join(dir, localCACertFile) + keyPath := filepath.Join(dir, localCAKeyFile) + + if key, cert, ok := loadLocalRoot(certPath, keyPath); ok { + return caManagerFromRoot(key, cert), nil + } + + key, cert, err := generateLocalRoot() + if err != nil { + return nil, err + } + if err := writeLocalRoot(certPath, keyPath, key, cert); err != nil { + return nil, err + } + return caManagerFromRoot(key, cert), nil +} + +// loadLocalRoot reads and validates the root; ok is false if anything is missing, unparseable, not a +// CA, or near expiry (caller regenerates). +func loadLocalRoot(certPath, keyPath string) (*ecdsa.PrivateKey, *x509.Certificate, bool) { + certPEM, err := os.ReadFile(certPath) + if err != nil { + return nil, nil, false + } + keyPEM, err := os.ReadFile(keyPath) + if err != nil { + return nil, nil, false + } + cb, _ := pem.Decode(certPEM) + kb, _ := pem.Decode(keyPEM) + if cb == nil || kb == nil { + return nil, nil, false + } + cert, err := x509.ParseCertificate(cb.Bytes) + if err != nil || !cert.IsCA { + return nil, nil, false + } + if time.Now().Add(localCARenewMargin).After(cert.NotAfter) { + return nil, nil, false + } + key, err := x509.ParseECPrivateKey(kb.Bytes) + if err != nil { + return nil, nil, false + } + return key, cert, true +} + +// writeLocalRoot writes the cert (public) and key (0600) atomically via temp-then-rename. +func writeLocalRoot(certPath, keyPath string, key *ecdsa.PrivateKey, cert *x509.Certificate) error { + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + der, err := x509.MarshalECPrivateKey(key) + if err != nil { + return err + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}) + if err := writeFileAtomic(certPath, certPEM, 0o600); err != nil { + return err + } + return writeFileAtomic(keyPath, keyPEM, 0o600) +} + +func writeFileAtomic(path string, data []byte, perm os.FileMode) error { + tmp, err := os.CreateTemp(filepath.Dir(path), ".tmp-*") + if err != nil { + return err + } + tmpName := tmp.Name() + defer os.Remove(tmpName) + if err := tmp.Chmod(perm); err != nil { + tmp.Close() + return err + } + if _, err := tmp.Write(data); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(tmpName, path) +} + +// lockDir takes an exclusive lock on a lockfile in dir; the returned func releases it. Uses gofrs/flock +// so the file is portable (the CLI also builds for Windows) rather than a raw unix.Flock. +func lockDir(dir string) (func(), error) { + fl := flock.New(filepath.Join(dir, ".ca.lock")) + if err := fl.Lock(); err != nil { + return nil, fmt.Errorf("failed to lock CA dir: %w", err) + } + return func() { _ = fl.Unlock() }, nil +} diff --git a/packages/agentproxy/local_ca_store_test.go b/packages/agentproxy/local_ca_store_test.go new file mode 100644 index 00000000..aa06cb36 --- /dev/null +++ b/packages/agentproxy/local_ca_store_test.go @@ -0,0 +1,64 @@ +package agentproxy + +import ( + "os" + "path/filepath" + "testing" +) + +func TestPersistentLocalCAReuseAndHeal(t *testing.T) { + dir := t.TempDir() + + ca1, err := newPersistentLocalCaManager(dir) + if err != nil { + t.Fatal(err) + } + pem1 := ca1.RootPEM() + if len(pem1) == 0 { + t.Fatal("expected a root cert") + } + + // Files exist with the right perms; key is 0600. + keyInfo, err := os.Stat(filepath.Join(dir, localCAKeyFile)) + if err != nil { + t.Fatal(err) + } + if keyInfo.Mode().Perm() != 0o600 { + t.Fatalf("key perms = %v, want 0600", keyInfo.Mode().Perm()) + } + + // Second call reuses the same root (stable across runs). + ca2, err := newPersistentLocalCaManager(dir) + if err != nil { + t.Fatal(err) + } + if string(ca2.RootPEM()) != string(pem1) { + t.Fatal("expected the persistent root to be reused, got a different cert") + } + + // Corrupt the cert -> next call self-heals with a fresh root. + if err := os.WriteFile(filepath.Join(dir, localCACertFile), []byte("garbage"), 0o600); err != nil { + t.Fatal(err) + } + ca3, err := newPersistentLocalCaManager(dir) + if err != nil { + t.Fatal(err) + } + if string(ca3.RootPEM()) == string(pem1) { + t.Fatal("expected a regenerated root after corruption") + } + if len(ca3.RootPEM()) == 0 { + t.Fatal("expected a valid regenerated root") + } +} + +func TestPersistentLocalCAMintsFromStoredRoot(t *testing.T) { + dir := t.TempDir() + ca, err := newPersistentLocalCaManager(dir) + if err != nil { + t.Fatal(err) + } + if _, err := ca.mintLeaf("api.stripe.com"); err != nil { + t.Fatalf("minting a leaf from the persistent root failed: %v", err) + } +} diff --git a/packages/agentproxy/local_test.go b/packages/agentproxy/local_test.go new file mode 100644 index 00000000..7228da3d --- /dev/null +++ b/packages/agentproxy/local_test.go @@ -0,0 +1,322 @@ +package agentproxy + +import ( + "bufio" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/x509" + "encoding/pem" + "fmt" + "net" + "net/http" + "testing" + "time" +) + +// newLocalTestProxy builds a local-mode proxyServer with an injected snapshot, mirroring +// newTestProxy's pipe/one-shot-listener harness for the remote tests. +func newLocalTestProxy(t *testing.T, local *LocalOptions, services []*resolvedService, rt http.RoundTripper) net.Conn { + t.Helper() + resolver := newLocalResolver(local) + resolver.services = services + resolver.valid = true + ps := &proxyServer{ + opts: Options{UnmatchedHost: UnmatchedAllow, Local: local}, + cache: resolver, + transport: rt, + } + client, server := net.Pipe() + l := newOneShotListener(server) + srv := ps.newFrontServer() + srv.ConnState = func(_ net.Conn, s http.ConnState) { + if s == http.StateClosed || s == http.StateHijacked { + _ = l.Close() + } + } + go func() { _ = srv.Serve(l) }() + t.Cleanup(func() { _ = client.Close() }) + return client +} + +func TestLocalCaMintsVerifiableLeaves(t *testing.T) { + ca, err := newLocalCaManager() + if err != nil { + t.Fatal(err) + } + + rootPEM := ca.RootPEM() + if len(rootPEM) == 0 { + t.Fatal("expected a root PEM from the local CA") + } + block, _ := pem.Decode(rootPEM) + if block == nil { + t.Fatal("root PEM did not decode") + } + root, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatal(err) + } + if !root.IsCA { + t.Fatal("local root is not a CA certificate") + } + pub, ok := root.PublicKey.(*ecdsa.PublicKey) + if !ok || pub.Curve != elliptic.P256() { + t.Fatalf("local root must be ECDSA P-256, got %T", root.PublicKey) + } + if err := root.CheckSignatureFrom(root); err != nil { + t.Fatalf("local root is not self-signed: %v", err) + } + + leaf, err := ca.mintLeaf("api.stripe.com") + if err != nil { + t.Fatal(err) + } + parsedLeaf, err := x509.ParseCertificate(leaf.Certificate[0]) + if err != nil { + t.Fatal(err) + } + pool := x509.NewCertPool() + pool.AddCert(root) + if _, err := parsedLeaf.Verify(x509.VerifyOptions{ + Roots: pool, + DNSName: "api.stripe.com", + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }); err != nil { + t.Fatalf("leaf does not verify against the local root: %v", err) + } + + if newCaManager(func() string { return "" }).RootPEM() != nil { + t.Fatal("remote caManager must not expose a root PEM") + } +} + +func TestLocalResolverSnapshotLifecycle(t *testing.T) { + local := &LocalOptions{ + ProjectID: "proj", Environment: "prod", SecretPath: "/", + UserToken: func() string { t.Fatal("token must not be used without a snapshot refresh"); return "" }, + IdentityID: "user-1", IdentityName: "dev", + } + r := newLocalResolver(local) + + // Without a snapshot, refreshActive must be a no-op (it must not call the API). + r.refreshActive() + + r.services = []*resolvedService{{name: "svc"}} + r.valid = true + + got, err := r.get("ignored-wire-credential", agentScope{projectID: "other"}) + if err != nil || len(got) != 1 || got[0].name != "svc" { + t.Fatalf("get should return the snapshot regardless of wire args, got %v, %v", got, err) + } + + id, name, ok := r.identity("", agentScope{}) + if !ok || id != "user-1" || name != "dev" { + t.Fatalf("identity should be the configured user, got %q %q %v", id, name, ok) + } + + if len(r.activeJWTs()) != 0 { + t.Fatal("local mode must register no lease JWTs") + } + + r.close() + if r.valid || r.services != nil { + t.Fatal("close must drop the snapshot (fail closed)") + } +} + +func TestLocalModeServesWithoutProxyAuth(t *testing.T) { + local := &LocalOptions{ + ProjectID: "proj", Environment: "prod", SecretPath: "/", + UserToken: func() string { return "dev-token" }, + } + services := []*resolvedService{{ + name: "stripe", + hostPatterns: parseHostPatterns("example.com"), + isEnabled: true, + credentials: []resolvedCredential{ + {role: roleHeaderRewrite, headerName: "Authorization", headerPrefix: "Bearer", value: "real_secret"}, + }, + }} + stub := &stubRoundTripper{respBody: "ok"} + client := newLocalTestProxy(t, local, services, stub) + _ = client.SetDeadline(time.Now().Add(10 * time.Second)) + + // No Proxy-Authorization header anywhere: local mode must serve and inject regardless. + if _, err := fmt.Fprintf(client, "GET http://example.com/v1 HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"); err != nil { + t.Fatal(err) + } + resp, err := http.ReadResponse(bufio.NewReader(client), nil) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + stub.mu.Lock() + defer stub.mu.Unlock() + if len(stub.gotAuth) != 1 || stub.gotAuth[0] != "Bearer real_secret" { + t.Fatalf("expected injected credential, got %v", stub.gotAuth) + } +} + +func TestRemoteModeChallengesWithoutProxyAuth(t *testing.T) { + cache := newAgentCache(func() string { return "" }, newLeaseStore(func() string { return "" })) + ps := &proxyServer{ + opts: Options{UnmatchedHost: UnmatchedAllow}, + cache: cache, + transport: &http.Transport{}, + } + client, server := net.Pipe() + l := newOneShotListener(server) + srv := ps.newFrontServer() + srv.ConnState = func(_ net.Conn, s http.ConnState) { + if s == http.StateClosed || s == http.StateHijacked { + _ = l.Close() + } + } + go func() { _ = srv.Serve(l) }() + t.Cleanup(func() { _ = client.Close() }) + _ = client.SetDeadline(time.Now().Add(10 * time.Second)) + + if _, err := fmt.Fprintf(client, "GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"); err != nil { + t.Fatal(err) + } + resp, err := http.ReadResponse(bufio.NewReader(client), nil) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusProxyAuthRequired { + t.Fatalf("expected 407 in remote mode, got %d", resp.StatusCode) + } + if resp.Header.Get("Proxy-Authenticate") != "Basic" { + t.Fatal("expected a Proxy-Authenticate challenge") + } +} + +func TestUnmatchedBlockRespectsAllowHost(t *testing.T) { + local := &LocalOptions{ + ProjectID: "proj", Environment: "prod", SecretPath: "/", + UserToken: func() string { return "dev-token" }, + } + resolver := newLocalResolver(local) + resolver.services = nil // no proxied services: every host is "unmatched" + resolver.valid = true + stub := &stubRoundTripper{respBody: "ok"} + ps := &proxyServer{ + opts: Options{UnmatchedHost: UnmatchedBlock, Local: local, AllowedHosts: []string{"docs.internal"}}, + cache: resolver, + transport: stub, + } + client, server := net.Pipe() + l := newOneShotListener(server) + srv := ps.newFrontServer() + srv.ConnState = func(_ net.Conn, s http.ConnState) { + if s == http.StateClosed || s == http.StateHijacked { + _ = l.Close() + } + } + go func() { _ = srv.Serve(l) }() + t.Cleanup(func() { _ = client.Close() }) + _ = client.SetDeadline(time.Now().Add(10 * time.Second)) + + // Allowlisted host passes through (case-insensitive) even under block. + if _, err := fmt.Fprintf(client, "GET http://Docs.Internal/x HTTP/1.1\r\nHost: docs.internal\r\nConnection: close\r\n\r\n"); err != nil { + t.Fatal(err) + } + resp, err := http.ReadResponse(bufio.NewReader(client), nil) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("allowlisted host should pass through under block, got %d", resp.StatusCode) + } +} + +func TestUnmatchedBlockRejectsNonAllowlistedHost(t *testing.T) { + local := &LocalOptions{ + ProjectID: "proj", Environment: "prod", SecretPath: "/", + UserToken: func() string { return "dev-token" }, + } + resolver := newLocalResolver(local) + resolver.valid = true + ps := &proxyServer{ + opts: Options{UnmatchedHost: UnmatchedBlock, Local: local, AllowedHosts: []string{"docs.internal"}}, + cache: resolver, + transport: &stubRoundTripper{respBody: "ok"}, + } + client, server := net.Pipe() + l := newOneShotListener(server) + srv := ps.newFrontServer() + srv.ConnState = func(_ net.Conn, s http.ConnState) { + if s == http.StateClosed || s == http.StateHijacked { + _ = l.Close() + } + } + go func() { _ = srv.Serve(l) }() + t.Cleanup(func() { _ = client.Close() }) + _ = client.SetDeadline(time.Now().Add(10 * time.Second)) + + if _, err := fmt.Fprintf(client, "GET http://evil.example/x HTTP/1.1\r\nHost: evil.example\r\nConnection: close\r\n\r\n"); err != nil { + t.Fatal(err) + } + resp, err := http.ReadResponse(bufio.NewReader(client), nil) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusForbidden { + t.Fatalf("non-allowlisted host should be blocked, got %d", resp.StatusCode) + } +} + +func TestProxyLifecycleServeShutdown(t *testing.T) { + p, err := New(Options{ + UnmatchedHost: UnmatchedAllow, + Local: &LocalOptions{ + ProjectID: "proj", Environment: "prod", SecretPath: "/", + UserToken: func() string { return "" }, + }, + }) + if err != nil { + t.Fatal(err) + } + if len(p.LocalRootPEM()) == 0 { + t.Fatal("expected a local root PEM in local mode") + } + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + done := make(chan error, 1) + go func() { done <- p.Serve(ln) }() + + conn, err := net.DialTimeout("tcp", ln.Addr().String(), 5*time.Second) + if err != nil { + t.Fatalf("proxy is not accepting on %s: %v", ln.Addr(), err) + } + _ = conn.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := p.Shutdown(ctx); err != nil { + t.Fatalf("Shutdown: %v", err) + } + select { + case err := <-done: + if err != nil { + t.Fatalf("Serve returned an error after Shutdown: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("Serve did not return after Shutdown") + } + + // Shutdown is idempotent. + if err := p.Shutdown(ctx); err != nil { + t.Fatal(err) + } +} diff --git a/packages/agentproxy/proxy.go b/packages/agentproxy/proxy.go index c6048b81..f159292b 100644 --- a/packages/agentproxy/proxy.go +++ b/packages/agentproxy/proxy.go @@ -82,12 +82,27 @@ type Options struct { UnmatchedHost string PollInterval time.Duration ProxyToken func() string + + // Local switches the proxy into local coupled mode; nil = remote. + Local *LocalOptions + + // AllowedHosts pass through with no credential even under UnmatchedBlock (run --allow-host). + AllowedHosts []string +} + +// serviceResolver resolves credentials: agentCache (remote) or localResolver (local). +type serviceResolver interface { + get(jwt string, scope agentScope) ([]*resolvedService, error) + identity(jwt string, scope agentScope) (id, name string, ok bool) + refreshActive() + activeJWTs() map[string]struct{} + close() } type proxyServer struct { opts Options ca *caManager - cache *agentCache + cache serviceResolver leases *leaseStore transport http.RoundTripper @@ -97,16 +112,40 @@ type proxyServer struct { usageFlushing atomic.Bool } -func newProxyServer(opts Options) *proxyServer { +func newProxyServer(opts Options) (*proxyServer, error) { + if opts.Local != nil && opts.ProxyToken == nil { + opts.ProxyToken = opts.Local.UserToken + } leases := newLeaseStore(opts.ProxyToken) + + var ca *caManager + var cache serviceResolver + if opts.Local != nil { + var localCa *caManager + var err error + if opts.Local.CADir != "" { + localCa, err = newPersistentLocalCaManager(opts.Local.CADir) + } else { + localCa, err = newLocalCaManager() + } + if err != nil { + return nil, err + } + ca = localCa + cache = newLocalResolver(opts.Local) + } else { + ca = newCaManager(opts.ProxyToken) + cache = newAgentCache(opts.ProxyToken, leases) + } + return &proxyServer{ opts: opts, - ca: newCaManager(opts.ProxyToken), - cache: newAgentCache(opts.ProxyToken, leases), + ca: ca, + cache: cache, leases: leases, transport: newUpstreamTransport(), usage: make(map[string]struct{}), - } + }, nil } func (ps *proxyServer) recordUsage(serviceID string) { @@ -166,17 +205,71 @@ func (ps *proxyServer) newFrontServer() *http.Server { } } -func Start(opts Options) error { - ps := newProxyServer(opts) +// Proxy lets the caller own binding, serving, and shutdown (used by `agent-proxy run`); the remote +// `start` command uses the blocking Start wrapper below. +type Proxy struct { + ps *proxyServer + srv *http.Server + loopStop chan struct{} + stopOnce sync.Once +} +func New(opts Options) (*Proxy, error) { + ps, err := newProxyServer(opts) + if err != nil { + return nil, fmt.Errorf("failed to initialize agent proxy CA: %w", err) + } if err := ps.ca.ensureIntermediate(); err != nil { - return fmt.Errorf("failed to initialize agent proxy CA: %w", err) + return nil, fmt.Errorf("failed to initialize agent proxy CA: %w", err) } + return &Proxy{ + ps: ps, + srv: ps.newFrontServer(), + loopStop: make(chan struct{}), + }, nil +} - go ps.pollLoop() +// Serve runs the poll/lease loops and serves on ln until Shutdown (nil) or a serve error. +func (p *Proxy) Serve(ln net.Listener) error { + go p.ps.pollLoop(p.loopStop) + go p.ps.leases.refreshLoop(p.loopStop, p.ps.opts.PollInterval, p.ps.cache.activeJWTs) - leaseStop := make(chan struct{}) - go ps.leases.refreshLoop(leaseStop, opts.PollInterval, ps.cache.activeJWTs) + err := p.srv.Serve(newLimitListener(ln, maxConcurrentConns)) + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return err +} + +// Shutdown stops the loops, revokes leases, drains in-flight requests (bounded by ctx), and drops +// cached credentials. Idempotent. +func (p *Proxy) Shutdown(ctx context.Context) error { + var err error + p.stopOnce.Do(func() { + close(p.loopStop) + revokeCtx, cancel := context.WithTimeout(context.Background(), leaseRevokeShutdownTimeout) + defer cancel() + p.ps.leases.revokeAll(revokeCtx) + err = p.srv.Shutdown(ctx) + // Flush usage recorded since the last poll tick. `run` is frequently shorter than one poll + // interval, so without a shutdown flush a short session would never report its brokered-service + // usage; the daemon path already flushes each tick. + p.ps.flushUsage() + p.ps.cache.close() + }) + return err +} + +// LocalRootPEM returns the local root CA's public cert (local mode only; nil otherwise). +func (p *Proxy) LocalRootPEM() []byte { + return p.ps.ca.RootPEM() +} + +func Start(opts Options) error { + p, err := New(opts) + if err != nil { + return err + } if addr := portInUse(opts.Port); addr != "" { return fmt.Errorf("port %d is already in use (%s); another process is listening. Choose a free port with --port", opts.Port, addr) @@ -189,25 +282,17 @@ func Start(opts Options) error { log.Info().Msgf("Infisical agent proxy listening on :%d", opts.Port) log.Info().Msg("per-request activity logging on: brokered=info, blocked=warn, error=error, passthrough=debug (use --log-level to filter)") - srv := ps.newFrontServer() - sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) go func() { <-sigCh log.Info().Msg("shutting down agent proxy; revoking active leases") - close(leaseStop) ctx, cancel := context.WithTimeout(context.Background(), leaseRevokeShutdownTimeout) defer cancel() - ps.leases.revokeAll(ctx) - _ = srv.Shutdown(ctx) + _ = p.Shutdown(ctx) }() - err = srv.Serve(newLimitListener(listener, maxConcurrentConns)) - if errors.Is(err, http.ErrServerClosed) { - return nil - } - return err + return p.Serve(listener) } func portInUse(port int) string { @@ -221,17 +306,31 @@ func portInUse(port int) string { return "" } -func (ps *proxyServer) pollLoop() { +func (ps *proxyServer) pollLoop(stop <-chan struct{}) { interval := ps.opts.PollInterval if interval <= 0 { interval = 60 * time.Second } ticker := time.NewTicker(interval) defer ticker.Stop() - for range ticker.C { - ps.cache.refreshActive() - go ps.flushUsage() + for { + select { + case <-ticker.C: + ps.cache.refreshActive() + go ps.flushUsage() + case <-stop: + return + } + } +} + +// requestScope returns the request's scope and wire credential. Remote parses Proxy-Authorization; +// local serves the startup scope with no wire credential. +func (ps *proxyServer) requestScope(r *http.Request) (agentScope, string, bool) { + if l := ps.opts.Local; l != nil { + return l.scope(), "", true } + return parseProxyAuth(r.Header.Get("Proxy-Authorization")) } func (ps *proxyServer) dispatch(w http.ResponseWriter, r *http.Request) { @@ -244,7 +343,7 @@ func (ps *proxyServer) dispatch(w http.ResponseWriter, r *http.Request) { func (ps *proxyServer) handleConnect(w http.ResponseWriter, r *http.Request) { // All authentication and HTTP error responses happen before Hijack: once hijacked, no HTTP status can be sent. - scope, jwt, ok := parseProxyAuth(r.Header.Get("Proxy-Authorization")) + scope, jwt, ok := ps.requestScope(r) if !ok { writeProxyAuthChallenge(w) return @@ -339,7 +438,7 @@ func (ps *proxyServer) handlePlainForward(w http.ResponseWriter, r *http.Request return } - scope, jwt, ok := parseProxyAuth(r.Header.Get("Proxy-Authorization")) + scope, jwt, ok := ps.requestScope(r) if !ok { writeProxyAuthChallenge(w) return @@ -499,7 +598,7 @@ func (ps *proxyServer) forward(req *http.Request, scheme, hostname, port, jwt st svc := bestMatch(services, hostname, port, req.URL.Path) - if svc == nil && ps.opts.UnmatchedHost == UnmatchedBlock { + if svc == nil && ps.opts.UnmatchedHost == UnmatchedBlock && !ps.hostAllowlisted(hostname) { return nil, outcome, fmt.Errorf("host %q has no matching proxied service: %w", hostname, errHostBlocked) } @@ -530,6 +629,16 @@ func (ps *proxyServer) forward(req *http.Request, scheme, hostname, port, jwt st return resp, outcome, nil } +// hostAllowlisted reports whether hostname is in AllowedHosts (case-insensitive). +func (ps *proxyServer) hostAllowlisted(hostname string) bool { + for _, h := range ps.opts.AllowedHosts { + if strings.EqualFold(h, hostname) { + return true + } + } + return false +} + func (ps *proxyServer) materializeCredentials(svc *resolvedService) []resolvedCredential { creds := make([]resolvedCredential, 0, len(svc.credentials)) for _, cred := range svc.credentials { diff --git a/packages/cmd/agent_proxy.go b/packages/cmd/agent_proxy.go index 51a7fe43..70533331 100644 --- a/packages/cmd/agent_proxy.go +++ b/packages/cmd/agent_proxy.go @@ -84,6 +84,14 @@ var credentialEnvKeys = []string{ util.INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME, } +// Authentication-agent handles: not secret values, but a socket the child could use as a signing +// oracle for the developer's SSH/GPG keys. Scrubbed by default; --pass-env re-admits a specific one. +var authAgentEnvKeys = []string{ + "SSH_AUTH_SOCK", + "SSH_AGENT_PID", + "GPG_AGENT_INFO", +} + var requiredNoProxy = []string{"localhost", "127.0.0.1"} func mergeNoProxy(operatorEntries ...string) string { @@ -404,8 +412,11 @@ func runAgentProcess(args, env []string) error { proc.Stderr = os.Stderr proc.Env = env + // Forward only process-directed termination signals; notifying on ALL signals also delivers SIGURG + // (Go's async-preemption signal) and SIGCHLD, which then get spammed at the child and can corrupt + // its exit status. Terminal-generated signals reach the child directly via the foreground pgroup. sigChannel := make(chan os.Signal, 1) - signal.Notify(sigChannel) + signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) if err := proc.Start(); err != nil { return err diff --git a/packages/cmd/agent_proxy_run.go b/packages/cmd/agent_proxy_run.go new file mode 100644 index 00000000..5c30a80c --- /dev/null +++ b/packages/cmd/agent_proxy_run.go @@ -0,0 +1,535 @@ +package cmd + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/url" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + "github.com/Infisical/infisical-merge/packages/agentproxy" + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/sandbox" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/fatih/color" + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var agentProxyRunCmd = &cobra.Command{ + Use: "run [flags] -- [agent start command]", + Short: "Run an untrusted agent locally with secrets brokered on the wire and an OS sandbox as the trust boundary", + Example: "infisical secrets agent-proxy run --env=prod --path=/myapp -- claude", + DisableFlagsInUseLine: true, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("provide the agent command to run after '--', e.g. -- claude") + } + return nil + }, + Run: runAgentProxyRun, +} + +// secretShapedEnvSubstrings: env vars whose name contains any of these are scrubbed from the child +// (coarse on purpose; --pass-env re-admits a specific one). +var secretShapedEnvSubstrings = []string{ + "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "API_KEY", "APIKEY", "PRIVATE_KEY", "ACCESS_KEY", +} + +func runAgentProxyRun(cmd *cobra.Command, args []string) { + if cmd.Flags().Changed("proxy") { + util.HandleError(fmt.Errorf("--proxy is not valid for 'run' (it starts its own ephemeral proxy); use 'agent-proxy connect --proxy=host:port' for a remote proxy")) + } + + // Resolve --env/--path the same way `agent-proxy connect` does (flag > env var > .infisical.json), + // so the two subcommands behave consistently and honor INFISICAL_ENVIRONMENT / INFISICAL_SECRET_PATH. + environment := util.ResolveEnvironmentName(cmd) + if environment == "" { + util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set defaultEnvironment in .infisical.json")) + } + + secretPath := util.ResolveSecretPath(cmd) + + projectID, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "projectId", []string{util.INFISICAL_PROJECT_ID_NAME}, "") + if err != nil { + util.HandleError(err, "Unable to parse --projectId") + } + if projectID == "" { + if workspaceFile, wsErr := util.GetWorkSpaceFromFile(); wsErr == nil { + projectID = workspaceFile.WorkspaceId + } + } + if projectID == "" { + util.HandleError(fmt.Errorf("project id is required; pass --projectId, set INFISICAL_PROJECT_ID, or run inside a project with .infisical.json")) + } + + unmatchedHost, _ := cmd.Flags().GetString("unmatched-host") + if unmatchedHost != agentproxy.UnmatchedAllow && unmatchedHost != agentproxy.UnmatchedBlock { + util.HandleError(fmt.Errorf("--unmatched-host must be 'allow' or 'block', got %q", unmatchedHost)) + } + pollInterval, _ := cmd.Flags().GetInt("poll-interval") + + sandboxEnabled := resolveSandboxEnabled(cmd) + + // The single identity for the run: fetches config and secret values in the parent. The child gets none of it. + src := resolveDeveloperTokenSource(cmd) + + httpClient := resty.New().SetAuthToken(src.token()) + placeholders := fetchLocalProxiedServiceConfig(httpClient, projectID, environment, secretPath) + + local := &agentproxy.LocalOptions{ + ProjectID: projectID, + Environment: environment, + SecretPath: secretPath, + UserToken: src.token, + IdentityID: src.label, + IdentityName: src.label, + } + + // Per-run 0700 tempdir: only the public CA cert (and the unix socket on the Linux hard fence) hit disk. + tempDir, err := os.MkdirTemp("", "infisical-agent-proxy-run-") + if err != nil { + util.HandleError(err, "Failed to create the per-run temp directory") + } + // os.Exit (and util.HandleError) skip deferred funcs, so clean up explicitly at every exit path. + cleanup := func() { _ = os.RemoveAll(tempDir) } + fail := func(e error, messages ...string) { cleanup(); util.HandleError(e, messages...) } + + // Unlike `start` (a daemon whose activity log IS its output), `run` wraps an interactive agent that + // owns the terminal. Route the proxy's per-request activity and poll logs to a file so they don't + // interleave with the agent's output. --log-file picks a stable path (survives teardown); the + // default lives in the per-run tempdir and is removed on exit (tail it live to watch brokering). + // HandleError/PrintWarning write to stderr independently, so errors and one-time notices still show. + logFile, _ := cmd.Flags().GetString("log-file") + logPath := logFile + if logPath == "" { + logPath = filepath.Join(tempDir, "agent-proxy.log") + } + logF, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) + if err != nil { + fail(err, "Failed to open the proxy log file") + } + log.Logger = log.Output(GetLoggerConfig(logF, true)) + + home, _ := os.UserHomeDir() + cwd, _ := os.Getwd() + extraRead, _ := cmd.Flags().GetStringArray("allow-read") + extraWrite, _ := cmd.Flags().GetStringArray("allow-write") + allowHosts, _ := cmd.Flags().GetStringArray("allow-host") + + // On macOS, persist the local root under ~/.infisical (already sandbox-denied, so the agent can't + // read the key) and trust it once in the keychain, so native-trust tools (Go CLIs like gh) can be + // brokered too. Elsewhere the injected CA env var is enough, so we keep an ephemeral in-memory root. + if runtime.GOOS == "darwin" && home != "" { + local.CADir = filepath.Join(home, ".infisical", "agent-proxy") + } + + // Supported agents persist their own state under home; make those dirs writable so interactive + // sessions can save. It's the agent's own data, not the developer's secrets. + writePaths := append(defaultAgentStateWritePaths(home), extraWrite...) + + spec := sandbox.SandboxSpec{ + Sandbox: sandboxEnabled, + ReadPaths: extraRead, + WritePaths: writePaths, + DenyPaths: sandbox.DefaultDenyPaths(home), + Cwd: cwd, + TempDir: tempDir, + // Linux downgrades to SharedNet via Preflight below; macOS always fences to loopback via SBPL. + NetMode: sandbox.HardFence, + } + + // Preflight must run before choosing the listener and building the env (the proxy URL depends on + // whether the bridge is used). + backend := sandbox.NewBackend(spec) + pre, err := backend.Preflight(spec) + if err != nil { + fail(err, "Sandbox preflight failed") + } + if !pre.Supported { + fail(fmt.Errorf("%s", pre.Reason)) + } + if pre.FallbackToSharedNet { + spec.NetMode = sandbox.SharedNet + util.PrintWarning(fmt.Sprintf("network hard-fence unavailable (%s); falling back to shared host networking. Your credentials remain protected (env scrub, keyring block, filesystem deny); only the network isolation is reduced.", pre.Reason)) + } + + proxy, err := agentproxy.New(agentproxy.Options{ + UnmatchedHost: unmatchedHost, + PollInterval: time.Duration(pollInterval) * time.Second, + Local: local, + AllowedHosts: allowHosts, + }) + if err != nil { + fail(err, "Failed to initialize the ephemeral agent proxy") + } + + // macOS: trust the persistent root once so native-trust tools (gh) accept the proxy's certs, and + // allow trustd in the profile for the sandbox. securityd stays blocked, so the token stays + // unreadable. Trust-install is non-fatal: env-CA tools (Claude Code, Codex, curl) work regardless. + if local.CADir != "" { + if sandboxEnabled { + spec.AllowTrustd = true + } + switch installed, terr := ensureCATrusted(agentproxy.LocalCACertPath(local.CADir)); { + case terr != nil: + util.PrintWarning(fmt.Sprintf("could not trust the local CA in your keychain (%v); Go-native tools like gh won't be brokered, but Claude Code, Codex, and curl still will", terr)) + case installed: + util.PrintWarning("added the Infisical agent-proxy local CA to your login keychain (one-time) so native-trust tools can be brokered; it persists for future runs") + } + } + + // Hard fence: proxy on a unix socket in the tempdir, reached via the bridge. Otherwise: TCP loopback. + var listener net.Listener + var childProxyURL string + if pre.UsesBridge { + spec.ProxySocket = filepath.Join(tempDir, "proxy.sock") + listener, err = net.Listen("unix", spec.ProxySocket) + if err != nil { + fail(err, "Failed to bind the ephemeral proxy on its unix socket") + } + spec.LoopbackPort = sandbox.BridgeLoopbackPort + childProxyURL = fmt.Sprintf("http://127.0.0.1:%d", sandbox.BridgeLoopbackPort) + } else { + listener, err = net.Listen("tcp", "127.0.0.1:0") + if err != nil { + fail(err, "Failed to bind the ephemeral proxy on loopback") + } + spec.LoopbackPort = listener.Addr().(*net.TCPAddr).Port + childProxyURL = localProxyURL(listener.Addr().String()) + } + + caPath := filepath.Join(tempDir, "local-ca.pem") + if err := os.WriteFile(caPath, proxy.LocalRootPEM(), 0o600); err != nil { + fail(err, "Failed to write the local CA certificate") + } + + proxyErrCh := make(chan error, 1) + go func() { proxyErrCh <- proxy.Serve(listener) }() + + spec.Env = buildLocalAgentEnv(cmd, childProxyURL, caPath, placeholders) + + if !sandboxEnabled { + util.PrintWarning("running WITHOUT the OS sandbox (--no-sandbox): the agent is uncontained and can read your keyring, credential files, and reach the network directly. Secrets are still brokered on the wire, but the sandbox boundary is off.") + } + // The default log lives in the per-run tempdir and is removed on exit, so its path is only worth + // surfacing when --log-file pins it to a stable location. + if logFile != "" { + fmt.Fprintln(os.Stderr, color.HiBlackString("proxy activity log: "+logPath)) + } + + child, err := backend.Wrap(spec, args) + if err != nil { + shutdownProxy(proxy) + fail(err, "Failed to wrap the agent command in the sandbox") + } + + exitCode := runSandboxedChild(child, proxy, proxyErrCh) + shutdownProxy(proxy) + cleanup() + os.Exit(exitCode) +} + +// runSandboxedChild starts the child, forwards signals, and returns its exit code; if the proxy dies +// first it kills the child. +func runSandboxedChild(child *exec.Cmd, proxy *agentproxy.Proxy, proxyErrCh <-chan error) int { + fmt.Fprintln(os.Stderr, color.GreenString("Starting agent behind the Infisical agent proxy (sandboxed)")) + + if err := child.Start(); err != nil { + fmt.Fprintf(os.Stderr, "failed to start the agent process: %v\n", err) + return 1 + } + + // Forward only process-directed termination signals. Notifying on ALL signals (signal.Notify with + // no filter) also delivers SIGURG (the Go runtime's async-preemption signal, fired constantly) and + // SIGCHLD, which we would then spam at the child and race into its exit path, intermittently + // corrupting its exit status into 255. Terminal-generated signals (Ctrl-C, SIGWINCH, Ctrl-Z) reach + // the child directly: it shares the controlling terminal's foreground process group. + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) + go func() { + for sig := range sigCh { + if child.Process != nil { + _ = child.Process.Signal(sig) + } + } + }() + + waitCh := make(chan error, 1) + go func() { waitCh <- child.Wait() }() + + select { + case err := <-proxyErrCh: + fmt.Fprintf(os.Stderr, "%s\n", color.RedString("the ephemeral proxy stopped unexpectedly (%v); terminating the agent", err)) + if child.Process != nil { + _ = child.Process.Kill() + } + <-waitCh + return 1 + case err := <-waitCh: + signal.Stop(sigCh) + return exitCodeFromWait(err) + } +} + +func exitCodeFromWait(err error) int { + if err == nil { + return 0 + } + if exitErr, ok := err.(*exec.ExitError); ok { + if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok { + return ws.ExitStatus() + } + } + fmt.Fprintf(os.Stderr, "agent process error: %v\n", err) + return 1 +} + +func shutdownProxy(proxy *agentproxy.Proxy) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = proxy.Shutdown(ctx) +} + +// resolveSandboxEnabled reads the toggle from flag or env only, never .infisical.json (a committed +// file must not be able to silently disable the boundary). +func resolveSandboxEnabled(cmd *cobra.Command) bool { + if cmd.Flags().Changed("sandbox") { + v, _ := cmd.Flags().GetBool("sandbox") + return v + } + if cmd.Flags().Changed("no-sandbox") { + v, _ := cmd.Flags().GetBool("no-sandbox") + return !v + } + if v := os.Getenv("INFISICAL_AGENT_PROXY_SANDBOX"); v != "" { + return !(v == "0" || strings.EqualFold(v, "false") || strings.EqualFold(v, "off")) + } + return true +} + +// tokenSource is the parent-held identity for a run. token() is read per request so a future +// refresher can rotate it; label names the identity in activity records. +type tokenSource struct { + token func() string + label string +} + +// resolveDeveloperTokenSource resolves the run's identity: an explicit --token/env, else the keyring +// login. No machine-identity path (that's for services, not a person on a laptop). Neither can be +// refreshed mid-session; we fail fast if the token is already expired but otherwise stay quiet, like +// other auth-bearing CLIs (kubectl, cloud CLIs) that surface an error at use rather than pre-warning. +func resolveDeveloperTokenSource(cmd *cobra.Command) tokenSource { + if token, err := util.GetInfisicalToken(cmd); err == nil && token != nil && token.Token != "" { + failIfTokenExpired(token.Token, "the provided token") + return tokenSource{token: func() string { return token.Token }, label: "token"} + } + + details, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil || !details.IsUserLoggedIn || details.LoginExpired { + details = util.EstablishUserLoginSession() + } + if details.UserCredentials.JTWToken == "" { + util.HandleError(fmt.Errorf("could not resolve your Infisical login; run 'infisical login' or pass --token")) + } + jwt := details.UserCredentials.JTWToken + failIfTokenExpired(jwt, "your login") + return tokenSource{token: func() string { return jwt }, label: details.UserCredentials.Email} +} + +// failIfTokenExpired aborts with a clear error when the token is already expired; silent otherwise +// (no routine "expires in Xh" banner). Tokens that aren't readable JWTs (e.g. service tokens) pass. +func failIfTokenExpired(jwtToken, subject string) { + if exp, ok := jwtExpiry(jwtToken); ok && time.Until(exp) <= 0 { + util.HandleError(fmt.Errorf("%s has expired; brokering would fail. Run 'infisical login' or pass a valid --token", subject)) + } +} + +// jwtExpiry reads the exp claim unverified; false if the token isn't a readable JWT (e.g. a service token). +func jwtExpiry(token string) (time.Time, bool) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return time.Time{}, false + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return time.Time{}, false + } + var claims struct { + Exp int64 `json:"exp"` + } + if err := json.Unmarshal(payload, &claims); err != nil || claims.Exp == 0 { + return time.Time{}, false + } + return time.Unix(claims.Exp, 0), true +} + +// fetchLocalProxiedServiceConfig returns the placeholder env to inject. No CanProxy filter (locally +// the gate is Read Value); disabled services are skipped. Real secret values are never fetched here. +func fetchLocalProxiedServiceConfig(httpClient *resty.Client, projectID, environment, secretPath string) map[string]string { + resp, err := api.CallListProxiedServices(httpClient, api.ListProxiedServicesRequest{ + ProjectID: projectID, + Environment: environment, + SecretPath: secretPath, + }) + if err != nil { + util.HandleError(err, "Failed to list proxied services") + } + + placeholders := map[string]string{} + for _, svc := range resp.Services { + if !svc.IsEnabled { + continue + } + for _, cred := range svc.Credentials { + if cred.Role == "credential-substitution" && cred.PlaceholderKey != "" { + placeholders[cred.PlaceholderKey] = cred.PlaceholderValue + } + } + } + return placeholders +} + +// defaultAgentStateWritePaths returns the supported agents' state paths under home that exist (only +// existing ones: bwrap --bind fails on a missing source). +func defaultAgentStateWritePaths(home string) []string { + if home == "" { + return nil + } + candidates := []string{ + filepath.Join(home, ".claude"), // Claude Code state dir (sessions, history, cache) + filepath.Join(home, ".claude.json"), // Claude Code top-level config + filepath.Join(home, ".codex"), // Codex state dir + } + var out []string + for _, p := range candidates { + if _, err := os.Stat(p); err == nil { + out = append(out, p) + } + } + return out +} + +// localProxyURL is the child's proxy URL: no userinfo, so no credential reaches the child. +func localProxyURL(proxyAddr string) string { + u := url.URL{Scheme: "http", Host: proxyAddr} + return u.String() +} + +// buildLocalAgentEnv builds the child env: a scrubbed parent env (no INFISICAL_TOKEN/DOMAIN, no +// secret-shaped vars) plus the credential-free proxy vars, CA trust vars, and placeholders. +func buildLocalAgentEnv(cmd *cobra.Command, proxy, caPath string, placeholders map[string]string) []string { + passEnv, _ := cmd.Flags().GetStringArray("pass-env") + setEnv, _ := cmd.Flags().GetStringArray("set-env") + + allowlist := map[string]bool{} + for _, k := range passEnv { + allowlist[k] = true + } + + stale := map[string]bool{} + for _, k := range proxyEnvKeys { + stale[k] = true + } + for _, k := range credentialEnvKeys { + stale[k] = true + } + for _, k := range authAgentEnvKeys { + stale[k] = true + } + stale[util.INFISICAL_TOKEN_NAME] = true + stale[util.INFISICAL_DOMAIN_ENV_NAME] = true + stale[util.INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME] = true + + var operatorNoProxy []string + env := map[string]string{} + for _, kv := range os.Environ() { + parts := strings.SplitN(kv, "=", 2) + if len(parts) != 2 { + continue + } + key, val := parts[0], parts[1] + if key == "NO_PROXY" || key == "no_proxy" { + operatorNoProxy = append(operatorNoProxy, val) + continue + } + if allowlist[key] { + env[key] = val + continue + } + if stale[key] || isSecretShapedEnvName(key) { + continue + } + env[key] = val + } + + env["HTTPS_PROXY"] = proxy + env["HTTP_PROXY"] = proxy + env["NO_PROXY"] = mergeNoProxy(operatorNoProxy...) + env["NODE_USE_ENV_PROXY"] = "1" + env["OPENCLAW_PROXY_URL"] = proxy + + for _, k := range caTrustEnvVars { + env[k] = caPath + } + + for k, v := range placeholders { + env[k] = v + } + + // --set-env wins last so operators can force a literal value into the child. + for _, kv := range setEnv { + parts := strings.SplitN(kv, "=", 2) + if len(parts) == 2 { + env[parts[0]] = parts[1] + } + } + + result := make([]string, 0, len(env)) + for k, v := range env { + result = append(result, fmt.Sprintf("%s=%s", k, v)) + } + return result +} + +func isSecretShapedEnvName(name string) bool { + upper := strings.ToUpper(name) + for _, s := range secretShapedEnvSubstrings { + if strings.Contains(upper, s) { + return true + } + } + return false +} + +func init() { + agentProxyRunCmd.Flags().StringP("env", "e", "", "environment slug to fetch proxied services and secrets from") + agentProxyRunCmd.Flags().String("path", "/", "secret path (folder) scope") + agentProxyRunCmd.Flags().String("projectId", "", "project id (falls back to INFISICAL_PROJECT_ID or .infisical.json)") + agentProxyRunCmd.Flags().String("token", "", "run with this token instead of your keyring login (used as-is; not refreshed)") + agentProxyRunCmd.Flags().Bool("sandbox", true, "run the agent inside the OS sandbox (default on)") + agentProxyRunCmd.Flags().Bool("no-sandbox", false, "disable the OS sandbox (agent runs uncontained; prints a warning)") + agentProxyRunCmd.Flags().String("unmatched-host", "allow", "policy for hosts with no proxied service: allow | block") + agentProxyRunCmd.Flags().Int("poll-interval", 60, "seconds between permission/credential refreshes") + agentProxyRunCmd.Flags().String("log-file", "", "write the proxy activity log to this path (default: a per-run temp file, kept off the terminal)") + agentProxyRunCmd.Flags().StringArray("allow-read", nil, "extra path the agent may read (repeatable)") + agentProxyRunCmd.Flags().StringArray("allow-write", nil, "extra path the agent may write (repeatable; implies read)") + agentProxyRunCmd.Flags().StringArray("allow-host", nil, "extra host the agent may reach through the proxy (repeatable)") + agentProxyRunCmd.Flags().StringArray("pass-env", nil, "let a specific host env var through to the agent (repeatable)") + agentProxyRunCmd.Flags().StringArray("set-env", nil, "inject a literal KEY=VALUE into the agent (repeatable)") + // --proxy exists only so we can reject it with a helpful message pointing at connect. + agentProxyRunCmd.Flags().String("proxy", "", "") + _ = agentProxyRunCmd.Flags().MarkHidden("proxy") + + agentProxyCmd.AddCommand(agentProxyRunCmd) +} diff --git a/packages/cmd/agent_proxy_run_test.go b/packages/cmd/agent_proxy_run_test.go new file mode 100644 index 00000000..a961cebb --- /dev/null +++ b/packages/cmd/agent_proxy_run_test.go @@ -0,0 +1,151 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func newRunTestCmd() *cobra.Command { + c := &cobra.Command{Use: "run"} + c.Flags().StringArray("pass-env", nil, "") + c.Flags().StringArray("set-env", nil, "") + return c +} + +func envToMap(env []string) map[string]string { + m := map[string]string{} + for _, kv := range env { + parts := strings.SplitN(kv, "=", 2) + if len(parts) == 2 { + m[parts[0]] = parts[1] + } + } + return m +} + +func TestBuildLocalAgentEnvScrubsSecretsAndToken(t *testing.T) { + t.Setenv("INFISICAL_TOKEN", "dev.jwt.secret") + t.Setenv("INFISICAL_DOMAIN", "https://app.infisical.com/api") + t.Setenv("AWS_SECRET_ACCESS_KEY", "should-be-scrubbed") + t.Setenv("ANTHROPIC_API_KEY", "sk-should-be-scrubbed") + t.Setenv("MY_PASSWORD", "hunter2") + t.Setenv("HARMLESS", "keep-me") + + cmd := newRunTestCmd() + placeholders := map[string]string{"STRIPE_KEY": "__PLACEHOLDER__"} + env := envToMap(buildLocalAgentEnv(cmd, "http://127.0.0.1:51234", "/tmp/ca.pem", placeholders)) + + // The developer token must never reach the child, by name or by value anywhere. + if _, ok := env["INFISICAL_TOKEN"]; ok { + t.Error("INFISICAL_TOKEN must be scrubbed from the child env") + } + if _, ok := env["INFISICAL_DOMAIN"]; ok { + t.Error("INFISICAL_DOMAIN must be scrubbed from the child env") + } + for k, v := range env { + if strings.Contains(v, "dev.jwt.secret") { + t.Errorf("developer token leaked into child env var %q=%q", k, v) + } + } + + for _, k := range []string{"AWS_SECRET_ACCESS_KEY", "ANTHROPIC_API_KEY", "MY_PASSWORD"} { + if _, ok := env[k]; ok { + t.Errorf("secret-shaped var %q must be scrubbed", k) + } + } + + if env["HARMLESS"] != "keep-me" { + t.Error("non-secret vars must be preserved") + } + if env["HTTPS_PROXY"] != "http://127.0.0.1:51234" { + t.Errorf("HTTPS_PROXY = %q; want the credential-free proxy URL", env["HTTPS_PROXY"]) + } + if !strings.Contains(env["HTTPS_PROXY"], "127.0.0.1") || strings.Contains(env["HTTPS_PROXY"], "@") { + t.Errorf("proxy URL must be credential-free loopback, got %q", env["HTTPS_PROXY"]) + } + if env["SSL_CERT_FILE"] != "/tmp/ca.pem" || env["NODE_EXTRA_CA_CERTS"] != "/tmp/ca.pem" { + t.Error("CA trust vars must point at the local CA cert") + } + if env["STRIPE_KEY"] != "__PLACEHOLDER__" { + t.Error("placeholder must be injected") + } + if !strings.Contains(env["NO_PROXY"], "127.0.0.1") { + t.Errorf("NO_PROXY must include localhost, got %q", env["NO_PROXY"]) + } +} + +func TestBuildLocalAgentEnvPassEnvOverridesScrub(t *testing.T) { + t.Setenv("ANTHROPIC_API_KEY", "sk-real") + cmd := newRunTestCmd() + _ = cmd.Flags().Set("pass-env", "ANTHROPIC_API_KEY") + + env := envToMap(buildLocalAgentEnv(cmd, "http://127.0.0.1:1", "/tmp/ca.pem", nil)) + if env["ANTHROPIC_API_KEY"] != "sk-real" { + t.Errorf("--pass-env must re-admit a scrubbed var, got %q", env["ANTHROPIC_API_KEY"]) + } +} + +func TestBuildLocalAgentEnvSetEnvWins(t *testing.T) { + cmd := newRunTestCmd() + _ = cmd.Flags().Set("set-env", "FEATURE_FLAG=on") + env := envToMap(buildLocalAgentEnv(cmd, "http://127.0.0.1:1", "/tmp/ca.pem", nil)) + if env["FEATURE_FLAG"] != "on" { + t.Errorf("--set-env must inject a literal, got %q", env["FEATURE_FLAG"]) + } +} + +func TestLocalProxyURLHasNoCredential(t *testing.T) { + u := localProxyURL("127.0.0.1:51234") + if u != "http://127.0.0.1:51234" { + t.Fatalf("local proxy URL = %q; want a bare loopback URL with no userinfo", u) + } +} + +func TestDefaultAgentStateWritePaths(t *testing.T) { + home := t.TempDir() + // Create ~/.claude (dir) and ~/.claude.json (file); leave ~/.codex absent. + if err := os.MkdirAll(filepath.Join(home, ".claude"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, ".claude.json"), []byte("{}"), 0o600); err != nil { + t.Fatal(err) + } + + got := defaultAgentStateWritePaths(home) + has := func(p string) bool { + for _, g := range got { + if g == filepath.Join(home, p) { + return true + } + } + return false + } + if !has(".claude") || !has(".claude.json") { + t.Errorf("expected existing agent paths to be included, got %v", got) + } + if has(".codex") { + t.Errorf("non-existent ~/.codex must NOT be included (bwrap --bind would fail), got %v", got) + } + if defaultAgentStateWritePaths("") != nil { + t.Error("empty home must return nil") + } +} + +func TestIsSecretShapedEnvName(t *testing.T) { + shaped := []string{"GITHUB_TOKEN", "aws_secret_access_key", "DB_PASSWORD", "X_API_KEY", "MY_CREDENTIAL"} + for _, n := range shaped { + if !isSecretShapedEnvName(n) { + t.Errorf("%q should be treated as secret-shaped", n) + } + } + plain := []string{"HOME", "PATH", "TERM", "LANG", "EDITOR"} + for _, n := range plain { + if isSecretShapedEnvName(n) { + t.Errorf("%q should NOT be treated as secret-shaped", n) + } + } +} diff --git a/packages/cmd/agent_proxy_run_token_test.go b/packages/cmd/agent_proxy_run_token_test.go new file mode 100644 index 00000000..42a07b9d --- /dev/null +++ b/packages/cmd/agent_proxy_run_token_test.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "encoding/base64" + "encoding/json" + "testing" + "time" +) + +func makeJWT(t *testing.T, claims map[string]any) string { + t.Helper() + enc := func(v any) string { + b, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + return base64.RawURLEncoding.EncodeToString(b) + } + return enc(map[string]any{"alg": "HS256", "typ": "JWT"}) + "." + enc(claims) + ".sig" +} + +func TestJwtExpiryReadsExp(t *testing.T) { + want := time.Now().Add(2 * time.Hour).Unix() + exp, ok := jwtExpiry(makeJWT(t, map[string]any{"exp": want})) + if !ok { + t.Fatal("expected exp to be readable") + } + if exp.Unix() != want { + t.Fatalf("exp = %d, want %d", exp.Unix(), want) + } +} + +func TestJwtExpiryRejectsNonJWT(t *testing.T) { + for _, tok := range []string{ + "st.abc.def.ghi", // service token (not a JWT) + "not-a-jwt", // no dots + "a.b", // too few segments + makeJWT(t, map[string]any{}), // JWT without an exp claim + } { + if _, ok := jwtExpiry(tok); ok { + t.Errorf("expected %q to have no readable expiry", tok) + } + } +} diff --git a/packages/cmd/agent_proxy_run_trust_darwin.go b/packages/cmd/agent_proxy_run_trust_darwin.go new file mode 100644 index 00000000..af17e86c --- /dev/null +++ b/packages/cmd/agent_proxy_run_trust_darwin.go @@ -0,0 +1,43 @@ +//go:build darwin + +package cmd + +import ( + "os/exec" +) + +// ensureCATrusted makes sure the local root at certPath is a trusted anchor in the login keychain, so +// native-trust clients (Go CLIs like gh) accept the proxy's leaves. It is silent when the cert is +// already trusted; installing (first run, or after removal/expiry) triggers a one-time macOS password +// prompt. Returns (installed, error): installed=true means an anchor was just added. +// +// This only affects cert TRUST. It never touches keychain secrets: securityd stays blocked in the +// sandbox, so the agent still cannot read the login token. +func ensureCATrusted(certPath string) (bool, error) { + if trustSettingsPresent(certPath) { + return false, nil + } + // add-trusted-cert into the user (login) keychain; -r trustRoot marks it a trusted anchor. + // #nosec G204 -- certPath is a path we control under ~/.infisical + cmd := exec.Command("security", "add-trusted-cert", "-r", "trustRoot", certPath) + if out, err := cmd.CombinedOutput(); err != nil { + return false, &trustInstallError{out: string(out), err: err} + } + return true, nil +} + +// trustSettingsPresent reports whether the cert already has trust settings (i.e. is a trusted anchor), +// without prompting. `security verify-cert` succeeds only if the chain is trusted. +func trustSettingsPresent(certPath string) bool { + // #nosec G204 -- certPath is a path we control + return exec.Command("security", "verify-cert", "-c", certPath).Run() == nil +} + +type trustInstallError struct { + out string + err error +} + +func (e *trustInstallError) Error() string { + return "failed to trust the local CA in the keychain: " + e.err.Error() + " (" + e.out + ")" +} diff --git a/packages/cmd/agent_proxy_run_trust_other.go b/packages/cmd/agent_proxy_run_trust_other.go new file mode 100644 index 00000000..2dc0b737 --- /dev/null +++ b/packages/cmd/agent_proxy_run_trust_other.go @@ -0,0 +1,7 @@ +//go:build !darwin + +package cmd + +// ensureCATrusted is a no-op off macOS: only macOS needs a keychain-trusted anchor for native-trust +// clients. Elsewhere (Linux) the injected CA env var is enough. +func ensureCATrusted(string) (bool, error) { return false, nil } diff --git a/packages/cmd/sandbox_supervisor.go b/packages/cmd/sandbox_supervisor.go new file mode 100644 index 00000000..4b90ab1a --- /dev/null +++ b/packages/cmd/sandbox_supervisor.go @@ -0,0 +1,36 @@ +//go:build linux + +package cmd + +import ( + "os" + + "github.com/Infisical/infisical-merge/packages/sandbox" + "github.com/spf13/cobra" +) + +// sandboxSupervisorCmd is an internal, hidden entry point: the Linux hard-fence path re-execs the CLI +// as this subcommand inside the bwrap netns to run the in-namespace bridge. Not for users. +var sandboxSupervisorCmd = &cobra.Command{ + Use: sandbox.SupervisorSubcommand, + Hidden: true, + DisableFlagParsing: false, + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, + DisableFlagsInUseLine: true, + // No-op override of root's PersistentPreRun: skip the update check / notices / keyring read (a + // network call) that would otherwise run inside the network-less namespace. + PersistentPreRun: func(_ *cobra.Command, _ []string) {}, + Run: func(cmd *cobra.Command, args []string) { + probe, _ := cmd.Flags().GetBool("probe") + port, _ := cmd.Flags().GetInt("port") + socket, _ := cmd.Flags().GetString("socket") + os.Exit(sandbox.RunSupervisor(probe, port, socket, args)) + }, +} + +func init() { + sandboxSupervisorCmd.Flags().Bool("probe", false, "capability probe: bring loopback up and exit") + sandboxSupervisorCmd.Flags().Int("port", 0, "loopback port to bridge from") + sandboxSupervisorCmd.Flags().String("socket", "", "proxy unix socket to bridge to") + RootCmd.AddCommand(sandboxSupervisorCmd) +} diff --git a/packages/sandbox/bridge.go b/packages/sandbox/bridge.go new file mode 100644 index 00000000..ee13cd31 --- /dev/null +++ b/packages/sandbox/bridge.go @@ -0,0 +1,147 @@ +//go:build linux + +package sandbox + +import ( + "errors" + "fmt" + "io" + "net" + "os" + "os/exec" + "os/signal" + "syscall" + + "golang.org/x/sys/unix" +) + +// RunSupervisor runs inside the bwrap netns on the hard-fence path: it brings loopback up, bridges +// 127.0.0.1:port to the parent's proxy unix socket, then execs the agent. probe=true is Preflight's +// capability check: bring loopback up and return without bridging or exec'ing. +func RunSupervisor(probe bool, port int, socket string, argv []string) int { + if err := bringLoopbackUp(); err != nil { + if probe { + return 1 + } + fmt.Fprintf(os.Stderr, "sandbox supervisor: failed to bring up loopback: %v\n", err) + return 1 + } + if probe { + return 0 + } + + if port == 0 || socket == "" || len(argv) == 0 { + fmt.Fprintln(os.Stderr, "sandbox supervisor: --port, --socket and a command are required") + return 1 + } + + // Fail loud: if the port is already taken, abort rather than route the agent's traffic to it. + ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + if err != nil { + fmt.Fprintf(os.Stderr, "sandbox supervisor: failed to bind loopback proxy port %d: %v\n", port, err) + return 1 + } + go runBridge(ln, socket) + + // #nosec G204 -- the wrapped command is provided directly by the operator running the CLI + agent := exec.Command(argv[0], argv[1:]...) + agent.Stdin = os.Stdin + agent.Stdout = os.Stdout + agent.Stderr = os.Stderr + agent.Env = os.Environ() + + if err := agent.Start(); err != nil { + fmt.Fprintf(os.Stderr, "sandbox supervisor: failed to start the agent: %v\n", err) + return 1 + } + + // Forward only process-directed termination signals. Notifying on ALL signals also delivers SIGURG + // (the Go runtime's async-preemption signal) and SIGCHLD, which would be spammed at the agent and + // race its exit path, intermittently corrupting its exit status into 255. Terminal-generated + // signals reach the agent directly through the shared controlling-terminal foreground group. + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) + go func() { + for sig := range sigCh { + if agent.Process != nil { + _ = agent.Process.Signal(sig) + } + } + }() + + err = agent.Wait() + signal.Stop(sigCh) + if err == nil { + return 0 + } + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok { + return ws.ExitStatus() + } + } + fmt.Fprintf(os.Stderr, "sandbox supervisor: agent error: %v\n", err) + return 1 +} + +// runBridge forwards each loopback TCP connection to the proxy's unix socket. The socket must be a +// pathname socket (abstract sockets are netns-scoped and unreachable). +func runBridge(ln net.Listener, socket string) { + defer ln.Close() + for { + conn, err := ln.Accept() + if err != nil { + return + } + go bridgeConn(conn, socket) + } +} + +func bridgeConn(client net.Conn, socket string) { + defer client.Close() + upstream, err := net.Dial("unix", socket) + if err != nil { + return + } + defer upstream.Close() + + done := make(chan struct{}, 2) + go func() { _, _ = io.Copy(upstream, client); done <- struct{}{} }() + go func() { _, _ = io.Copy(client, upstream); done <- struct{}{} }() + <-done +} + +// bringLoopbackUp ensures lo is UP so 127.0.0.1 is reachable inside the netns. bwrap already brings +// loopback up when it creates the new network namespace, so in practice lo is UP by the time we get +// here; we still check and, only if it is down, try to raise it. The SIOCSIFFLAGS write is refused +// with EPERM in bwrap's user-namespaced netns even with CAP_NET_ADMIN, so treating that failure as +// fatal would force every Linux run to fall back to the weaker shared-net path. We therefore return +// success whenever lo is already UP, and surface the error only when it is genuinely still down. +func bringLoopbackUp() error { + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) + if err != nil { + return err + } + defer unix.Close(fd) + + ifr, err := unix.NewIfreq("lo") + if err != nil { + return err + } + if err := unix.IoctlIfreq(fd, unix.SIOCGIFFLAGS, ifr); err != nil { + return err + } + if ifr.Uint16()&unix.IFF_UP != 0 { + return nil + } + flags := ifr.Uint16() | unix.IFF_UP | unix.IFF_RUNNING + ifr.SetUint16(flags) + if err := unix.IoctlIfreq(fd, unix.SIOCSIFFLAGS, ifr); err != nil { + // Re-read: some kernels refuse the write yet lo is up anyway. Trust the observed state. + if unix.IoctlIfreq(fd, unix.SIOCGIFFLAGS, ifr) == nil && ifr.Uint16()&unix.IFF_UP != 0 { + return nil + } + return err + } + return nil +} diff --git a/packages/sandbox/bwrap.go b/packages/sandbox/bwrap.go new file mode 100644 index 00000000..02e6448e --- /dev/null +++ b/packages/sandbox/bwrap.go @@ -0,0 +1,119 @@ +//go:build linux + +package sandbox + +import ( + "fmt" + "os" + "os/exec" +) + +func osBackend() Backend { return bwrapBackend{} } + +// statDenyPath reports whether a deny path exists and, if so, whether it is a regular file. A missing +// path is skipped by buildBwrapArgv (nothing to hide, and the mountpoint can't be created under the +// read-only root bind). +func statDenyPath(path string) (exists, isFile bool) { + info, err := os.Stat(path) + if err != nil { + return false, false + } + return true, info.Mode().IsRegular() +} + +type bwrapBackend struct{} + +// Preflight checks bwrap is present and probes (by executing an empty-netns bwrap) whether the hard +// fence works here; if not (e.g. Ubuntu 24.04 userns restriction) it falls back to shared net. +func (bwrapBackend) Preflight(spec SandboxSpec) (PreflightResult, error) { + bwrapPath, err := exec.LookPath("bwrap") + if err != nil { + return PreflightResult{ + Supported: false, + Reason: "bubblewrap (bwrap) is not installed; install it (e.g. `apt install bubblewrap`) or re-run with --no-sandbox", + }, nil + } + + if spec.NetMode == SharedNet { + // Caller already asked for shared net; nothing to probe. + return PreflightResult{Supported: true, UsesBridge: false}, nil + } + + if hardFenceWorks(bwrapPath) { + return PreflightResult{Supported: true, UsesBridge: true}, nil + } + + // The hard fence probe failed. Before downgrading to shared net, check whether bwrap can create a + // user namespace at all: if it cannot (e.g. Ubuntu 24.04 with unprivileged userns fully restricted), + // the shared-net fallback would also die at "setting up uid map", so falling back would only trade a + // clear error for a raw bwrap crash. In that case report unsupported with actionable guidance. + if !sharedNetWorks(bwrapPath) { + return PreflightResult{ + Supported: false, + Reason: "the OS sandbox cannot start: unprivileged user namespaces are restricted on this host " + + "(e.g. Ubuntu 24.04 AppArmor). Allow them with `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0` " + + "(or install an AppArmor profile permitting bwrap user namespaces), or re-run with --no-sandbox to skip the sandbox.", + }, nil + } + + // User namespaces work but the empty-netns hard fence does not. Fall back to shared host networking. + // Credential controls are unaffected; only the network fence weakens. + return PreflightResult{ + Supported: true, + FallbackToSharedNet: true, + UsesBridge: false, + Reason: "the empty-netns hard fence is unavailable on this host. To restore it, install an AppArmor profile allowing bwrap user namespaces or set kernel.apparmor_restrict_unprivileged_userns=0", + }, nil +} + +// sharedNetWorks probes whether bwrap can start a user-namespaced sandbox that shares host networking. +// It mirrors the shared-net fallback's own bwrap flags, so a success here means the fallback can run. +func sharedNetWorks(bwrapPath string) bool { + // #nosec G204 -- fixed argv, no user input + cmd := exec.Command(bwrapPath, + "--unshare-all", "--share-net", "--die-with-parent", + "--ro-bind", "/", "/", + "--dev", "/dev", "--proc", "/proc", "--tmpfs", "/tmp", + "--", "true", + ) + return cmd.Run() == nil +} + +// hardFenceWorks probes whether an empty-netns bwrap can start and bring loopback up. +func hardFenceWorks(bwrapPath string) bool { + self, err := os.Executable() + if err != nil { + return false + } + // #nosec G204 -- fixed argv, no user input + cmd := exec.Command(bwrapPath, + "--unshare-all", "--die-with-parent", + "--ro-bind", "/", "/", + "--dev", "/dev", "--proc", "/proc", "--tmpfs", "/tmp", + "--", self, SupervisorSubcommand, "--probe", + ) + return cmd.Run() == nil +} + +func (bwrapBackend) Wrap(spec SandboxSpec, argv []string) (*exec.Cmd, error) { + if len(argv) == 0 { + return nil, fmt.Errorf("sandbox: empty command") + } + bwrapPath, err := exec.LookPath("bwrap") + if err != nil { + return nil, fmt.Errorf("bubblewrap (bwrap) is not installed: %w", err) + } + self, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("cannot resolve own executable for the sandbox supervisor: %w", err) + } + + full := buildBwrapArgv(spec, self, argv, statDenyPath) + // #nosec G204 -- the wrapped command is provided directly by the operator running the CLI + cmd := exec.Command(bwrapPath, full[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = spec.Env + return cmd, nil +} diff --git a/packages/sandbox/bwrap_args.go b/packages/sandbox/bwrap_args.go new file mode 100644 index 00000000..89afbb56 --- /dev/null +++ b/packages/sandbox/bwrap_args.go @@ -0,0 +1,100 @@ +package sandbox + +// BridgeLoopbackPort is the loopback port the in-namespace bridge listens on. Fixed is safe: each +// hard-fence run has its own empty netns, so it never collides across concurrent runs. +const BridgeLoopbackPort = 17321 + +// SupervisorSubcommand re-enters this binary as the in-namespace supervisor. Exported so the cmd +// package can register the matching hidden command. +const SupervisorSubcommand = "__sandbox-supervisor" + +// buildBwrapArgv builds the bwrap argv for a spec. Pure (its filesystem dependency is injected via +// classify, which reports whether a deny path exists and, if so, whether it is a regular file), so it +// is golden-testable. +// +// Deny paths are masked by type: /dev/null bind for files, empty tmpfs for directories. This is +// load-bearing, not cosmetic: mounting a tmpfs onto an existing file aborts bwrap at startup (ENOTDIR). +// A deny path that does not exist is skipped entirely: there is no credential there to hide, and bwrap +// cannot create the mountpoint under the read-only root bind, so masking a missing path would abort the +// whole sandbox at startup ("Can't mkdir ...: Read-only file system"). +// Deny mounts are emitted after the cwd/tempdir/write binds so they always win: otherwise a cwd bind +// that is an ancestor of a deny path (running the agent from $HOME) would re-expose ~/.aws, ~/.ssh, etc. +// Omits --new-session (setsid breaks the interactive TTY). On the hard fence the proxy's unix socket +// is bind-mounted and the supervisor bridges to it; shared net reaches host loopback directly. +func buildBwrapArgv(spec SandboxSpec, selfExe string, argv []string, classify func(string) (exists, isFile bool)) []string { + args := []string{ + "bwrap", + "--unshare-all", + "--die-with-parent", + "--ro-bind", "/", "/", + "--dev", "/dev", + "--proc", "/proc", + "--tmpfs", "/tmp", + } + + if spec.NetMode == SharedNet { + args = append(args, "--share-net") + } + + // Read/write binds first. Bound after --tmpfs /tmp so a tempdir under /tmp is not shadowed. + if spec.Cwd != "" { + args = append(args, "--bind", spec.Cwd, spec.Cwd) + } + if spec.TempDir != "" { + args = append(args, "--bind", spec.TempDir, spec.TempDir) + } + for _, p := range dedupeSorted(spec.WritePaths) { + args = append(args, "--bind", p, p) + } + + // Deny mounts LAST so they always win: a cwd or write bind that is an ancestor of a deny path + // (e.g. running the agent from $HOME, whose bind would otherwise re-expose ~/.aws, ~/.ssh, ...) + // must not be able to re-expose a credential path masked earlier. + for _, p := range dedupeSorted(spec.DenyPaths) { + exists, isFile := classify(p) + if !exists { + continue + } + if isFile { + args = append(args, "--ro-bind", "/dev/null", p) + } else { + args = append(args, "--tmpfs", p) + } + } + + args = append(args, "--") + + if spec.NetMode == HardFence && spec.ProxySocket != "" { + args = append(args, + selfExe, SupervisorSubcommand, + "--port", itoa(spec.LoopbackPort), + "--socket", spec.ProxySocket, + "--", + ) + } + + return append(args, argv...) +} + +// itoa keeps this file dependency-free (no strconv). +func itoa(n int) string { + if n == 0 { + return "0" + } + neg := n < 0 + if neg { + n = -n + } + var buf [20]byte + i := len(buf) + for n > 0 { + i-- + buf[i] = byte('0' + n%10) + n /= 10 + } + if neg { + i-- + buf[i] = '-' + } + return string(buf[i:]) +} diff --git a/packages/sandbox/bwrap_args_test.go b/packages/sandbox/bwrap_args_test.go new file mode 100644 index 00000000..48c173ba --- /dev/null +++ b/packages/sandbox/bwrap_args_test.go @@ -0,0 +1,183 @@ +package sandbox + +import ( + "strings" + "testing" +) + +func bwrapSpec(net NetMode) SandboxSpec { + return SandboxSpec{ + Sandbox: true, + Cwd: "/home/dev/project", + TempDir: "/tmp/infisical-run-xyz", + LoopbackPort: BridgeLoopbackPort, + ProxySocket: "/tmp/infisical-run-xyz/proxy.sock", + DenyPaths: []string{"/home/dev/.aws", "/home/dev/.ssh", "/home/dev/.infisical"}, + WritePaths: []string{"/home/dev/.cache"}, + NetMode: net, + } +} + +// allExistingDirs classifies every deny path as an existing directory (never a file, never missing), +// so the default specs mask with --tmpfs. Tests that care about the file or missing branch pass their +// own classifier. +func allExistingDirs(string) (bool, bool) { return true, false } + +// argIndex returns the index of the first occurrence of tok, or -1. +func argIndex(args []string, tok string) int { + for i, a := range args { + if a == tok { + return i + } + } + return -1 +} + +func TestBwrapArgvHardFence(t *testing.T) { + args := buildBwrapArgv(bwrapSpec(HardFence), "/proc/self/exe", []string{"claude", "--flag"}, allExistingDirs) + joined := strings.Join(args, " ") + + for _, want := range []string{ + "--unshare-all", "--die-with-parent", + "--tmpfs /home/dev/.aws", "--tmpfs /home/dev/.ssh", "--tmpfs /home/dev/.infisical", + "--bind /home/dev/project /home/dev/project", + "--bind /tmp/infisical-run-xyz /tmp/infisical-run-xyz", + "--bind /home/dev/.cache /home/dev/.cache", + SupervisorSubcommand, + "--socket /tmp/infisical-run-xyz/proxy.sock", + } { + if !strings.Contains(joined, want) { + t.Errorf("hard-fence argv missing %q\n%s", want, joined) + } + } + + // Must NOT share host net on the hard fence, and must NOT setsid. + if strings.Contains(joined, "--share-net") { + t.Error("hard fence must not pass --share-net") + } + if strings.Contains(joined, "--new-session") { + t.Error("must never pass --new-session (breaks the interactive TTY)") + } + + // The supervisor + agent must appear after the -- separator, in order. + sep := argIndex(args, "--") + sup := argIndex(args, SupervisorSubcommand) + agent := argIndex(args, "claude") + if sep == -1 || sup < sep || agent < sup { + t.Fatalf("expected `-- ... claude`; sep=%d sup=%d agent=%d\n%v", sep, sup, agent, args) + } + // tempdir bind must come after the /tmp tmpfs so it is not shadowed. + tmpfsTmp := lastArgPairIndex(args, "--tmpfs", "/tmp") + bindTemp := lastArgPairIndex(args, "--bind", "/tmp/infisical-run-xyz") + if tmpfsTmp == -1 || bindTemp == -1 || bindTemp < tmpfsTmp { + t.Fatalf("tempdir bind must follow the /tmp tmpfs (tmpfs=%d bind=%d)", tmpfsTmp, bindTemp) + } +} + +func TestBwrapArgvSharedNet(t *testing.T) { + args := buildBwrapArgv(bwrapSpec(SharedNet), "/proc/self/exe", []string{"claude"}, allExistingDirs) + joined := strings.Join(args, " ") + + if !strings.Contains(joined, "--share-net") { + t.Error("shared-net argv must pass --share-net") + } + // No bridge/supervisor on the shared-net path: the agent runs directly. + if strings.Contains(joined, SupervisorSubcommand) { + t.Error("shared-net path must not insert the supervisor") + } + if args[len(args)-1] != "claude" { + t.Errorf("agent must be the final arg on the shared-net path, got %q", args[len(args)-1]) + } +} + +// lastArgPairIndex finds the index of `flag` immediately followed by `val`. +func lastArgPairIndex(args []string, flag, val string) int { + idx := -1 + for i := 0; i+1 < len(args); i++ { + if args[i] == flag && args[i+1] == val { + idx = i + } + } + return idx +} + +func TestBwrapArgvMasksFilesWithDevNull(t *testing.T) { + spec := bwrapSpec(HardFence) + spec.DenyPaths = []string{"/home/dev/.aws", "/home/dev/.docker/config.json", "/home/dev/.netrc"} + // Classify the two dotfiles as files; the .aws dir stays a directory. All three exist. + classify := func(p string) (bool, bool) { + return true, p == "/home/dev/.docker/config.json" || p == "/home/dev/.netrc" + } + joined := strings.Join(buildBwrapArgv(spec, "/proc/self/exe", []string{"claude"}, classify), " ") + + // Files masked by binding /dev/null over them (tmpfs onto a file aborts bwrap). + for _, want := range []string{ + "--ro-bind /dev/null /home/dev/.docker/config.json", + "--ro-bind /dev/null /home/dev/.netrc", + "--tmpfs /home/dev/.aws", // directory still uses tmpfs + } { + if !strings.Contains(joined, want) { + t.Errorf("expected %q\n%s", want, joined) + } + } + // A file deny path must NOT be masked with tmpfs. + if strings.Contains(joined, "--tmpfs /home/dev/.netrc") || strings.Contains(joined, "--tmpfs /home/dev/.docker/config.json") { + t.Errorf("file deny paths must not be masked with tmpfs\n%s", joined) + } +} + +func TestBwrapArgvSkipsMissingDenyPaths(t *testing.T) { + spec := bwrapSpec(HardFence) + spec.DenyPaths = []string{"/home/dev/.aws", "/home/dev/.ssh", "/home/dev/.netrc"} + // Only .aws exists (as a dir); .ssh and .netrc are missing on this account. + classify := func(p string) (bool, bool) { + return p == "/home/dev/.aws", false + } + joined := strings.Join(buildBwrapArgv(spec, "/proc/self/exe", []string{"claude"}, classify), " ") + + // The existing path is masked... + if !strings.Contains(joined, "--tmpfs /home/dev/.aws") { + t.Errorf("existing deny dir must be masked with tmpfs\n%s", joined) + } + // ...but missing paths must NOT appear at all: bwrap can't create a mountpoint under the + // read-only root bind, so a --tmpfs/--ro-bind for a missing path aborts the whole sandbox. + for _, missing := range []string{"/home/dev/.ssh", "/home/dev/.netrc"} { + if strings.Contains(joined, missing) { + t.Errorf("missing deny path %q must be skipped, not mounted\n%s", missing, joined) + } + } +} + +func TestBwrapArgvDenyMountsWinOverCwdBind(t *testing.T) { + // Agent launched from $HOME: the cwd bind is an ancestor of every deny path. + spec := bwrapSpec(HardFence) + spec.Cwd = "/home/dev" + spec.WritePaths = []string{"/home/dev/.claude"} + spec.DenyPaths = []string{"/home/dev/.aws", "/home/dev/.ssh"} + args := buildBwrapArgv(spec, "/proc/self/exe", []string{"claude"}, allExistingDirs) + + // Every deny mount must come AFTER the cwd bind and the write bind, or the bind re-exposes it. + cwdBind := lastArgPairIndex(args, "--bind", "/home/dev") + writeBind := lastArgPairIndex(args, "--bind", "/home/dev/.claude") + for _, deny := range []string{"/home/dev/.aws", "/home/dev/.ssh"} { + denyIdx := lastArgPairIndex(args, "--tmpfs", deny) + if denyIdx == -1 { + t.Fatalf("deny path %q not masked\n%v", deny, args) + } + if denyIdx < cwdBind { + t.Errorf("deny mount %q (idx %d) must come after the cwd bind (idx %d), else the cwd bind re-exposes it", deny, denyIdx, cwdBind) + } + if denyIdx < writeBind { + t.Errorf("deny mount %q (idx %d) must come after the write bind (idx %d)", deny, denyIdx, writeBind) + } + } +} + +func TestItoa(t *testing.T) { + cases := map[int]string{0: "0", 7: "7", 17321: "17321", -42: "-42"} + for in, want := range cases { + if got := itoa(in); got != want { + t.Errorf("itoa(%d) = %q, want %q", in, got, want) + } + } +} diff --git a/packages/sandbox/sandbox.go b/packages/sandbox/sandbox.go new file mode 100644 index 00000000..9492b68f --- /dev/null +++ b/packages/sandbox/sandbox.go @@ -0,0 +1,52 @@ +package sandbox + +import ( + "errors" + "fmt" + "os" + "os/exec" +) + +var errUnsupportedPlatform = errors.New("sandbox: unsupported platform") + +type PreflightResult struct { + Supported bool // false => no OS sandbox here (e.g. Windows, bwrap missing) + FallbackToSharedNet bool // Linux: empty-netns hard fence unavailable, downgrade to shared net + UsesBridge bool // Linux hard fence only: proxy on a unix socket, reached via the bridge + Reason string // user-facing explanation for an unsupported result or a fallback +} + +// Backend applies an OS sandbox to a command. Wrap returns an *exec.Cmd ready to Start (stdio +// inherited, Env from the spec) but does not start it. +type Backend interface { + Preflight(spec SandboxSpec) (PreflightResult, error) + Wrap(spec SandboxSpec, argv []string) (*exec.Cmd, error) +} + +// NewBackend returns the platform backend, or the uncontained passthrough backend for --no-sandbox. +func NewBackend(spec SandboxSpec) Backend { + if !spec.Sandbox { + return passthroughBackend{} + } + return osBackend() +} + +// passthroughBackend runs the command uncontained (--no-sandbox): proxy and scrubbed env still apply. +type passthroughBackend struct{} + +func (passthroughBackend) Preflight(SandboxSpec) (PreflightResult, error) { + return PreflightResult{Supported: true}, nil +} + +func (passthroughBackend) Wrap(spec SandboxSpec, argv []string) (*exec.Cmd, error) { + if len(argv) == 0 { + return nil, fmt.Errorf("sandbox: empty command") + } + // #nosec G204 -- the command is provided directly by the operator running the CLI + cmd := exec.Command(argv[0], argv[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = spec.Env + return cmd, nil +} diff --git a/packages/sandbox/sandbox_unsupported.go b/packages/sandbox/sandbox_unsupported.go new file mode 100644 index 00000000..a87260e1 --- /dev/null +++ b/packages/sandbox/sandbox_unsupported.go @@ -0,0 +1,21 @@ +//go:build !darwin && !linux + +package sandbox + +import "os/exec" + +func osBackend() Backend { return unsupportedBackend{} } + +// unsupportedBackend is used on platforms without an OS sandbox (e.g. Windows). +type unsupportedBackend struct{} + +func (unsupportedBackend) Preflight(SandboxSpec) (PreflightResult, error) { + return PreflightResult{ + Supported: false, + Reason: "the OS sandbox is not available on this platform (macOS and Linux only in v1); re-run with --no-sandbox to run uncontained", + }, nil +} + +func (unsupportedBackend) Wrap(SandboxSpec, []string) (*exec.Cmd, error) { + return nil, errUnsupportedPlatform +} diff --git a/packages/sandbox/seatbelt.go b/packages/sandbox/seatbelt.go new file mode 100644 index 00000000..5948ce9b --- /dev/null +++ b/packages/sandbox/seatbelt.go @@ -0,0 +1,41 @@ +//go:build darwin + +package sandbox + +import ( + "fmt" + "os" + "os/exec" +) + +const sandboxExecPath = "/usr/bin/sandbox-exec" + +func osBackend() Backend { return seatbeltBackend{} } + +type seatbeltBackend struct{} + +func (seatbeltBackend) Preflight(SandboxSpec) (PreflightResult, error) { + if _, err := os.Stat(sandboxExecPath); err != nil { + return PreflightResult{ + Supported: false, + Reason: fmt.Sprintf("%s not found; cannot sandbox on this macOS host", sandboxExecPath), + }, nil + } + return PreflightResult{Supported: true}, nil +} + +// Wrap builds `sandbox-exec -p `; the profile is passed inline, never on disk. +func (seatbeltBackend) Wrap(spec SandboxSpec, argv []string) (*exec.Cmd, error) { + if len(argv) == 0 { + return nil, fmt.Errorf("sandbox: empty command") + } + profile := generateSeatbeltProfile(spec) + args := append([]string{"-p", profile}, argv...) + // #nosec G204 -- the command is provided directly by the operator running the CLI + cmd := exec.Command(sandboxExecPath, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = spec.Env + return cmd, nil +} diff --git a/packages/sandbox/seatbelt_live_test.go b/packages/sandbox/seatbelt_live_test.go new file mode 100644 index 00000000..25cc5b4d --- /dev/null +++ b/packages/sandbox/seatbelt_live_test.go @@ -0,0 +1,136 @@ +//go:build darwin + +package sandbox + +import ( + "fmt" + "net" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" +) + +// These tests actually spawn sandbox-exec on the host to prove the profile enforces what we claim. +// They are gated behind INFISICAL_SANDBOX_LIVE=1 so ordinary `go test` (and CI) skip them. +func requireLive(t *testing.T) { + t.Helper() + if os.Getenv("INFISICAL_SANDBOX_LIVE") != "1" { + t.Skip("set INFISICAL_SANDBOX_LIVE=1 to run live sandbox-exec tests") + } + if _, err := os.Stat(sandboxExecPath); err != nil { + t.Skipf("%s not present", sandboxExecPath) + } +} + +func liveSpec(t *testing.T, port int) SandboxSpec { + t.Helper() + home, _ := os.UserHomeDir() + return SandboxSpec{ + Sandbox: true, + Cwd: t.TempDir(), + TempDir: t.TempDir(), + LoopbackPort: port, + DenyPaths: DefaultDenyPaths(home), + Env: os.Environ(), + NetMode: HardFence, + } +} + +func runInSandbox(t *testing.T, spec SandboxSpec, argv ...string) (string, error) { + t.Helper() + cmd, err := seatbeltBackend{}.Wrap(spec, argv) + if err != nil { + t.Fatal(err) + } + // Wrap sets stdio for interactive use; clear it so CombinedOutput can capture instead. + cmd.Stdin = nil + cmd.Stdout = nil + cmd.Stderr = nil + out, err := cmd.CombinedOutput() + return string(out), err +} + +// The credential boundary: reading a credential file must fail inside the box. +func TestLiveDeniesCredentialFileRead(t *testing.T) { + requireLive(t) + home, _ := os.UserHomeDir() + probe := home + "/.aws/credentials" + if _, err := os.Stat(probe); err != nil { + // Create a throwaway file under a denied path so the test is meaningful even without real creds. + probe = home + "/.infisical/.sandbox-probe" + _ = os.MkdirAll(home+"/.infisical", 0o700) + if werr := os.WriteFile(probe, []byte("secret"), 0o600); werr != nil { + t.Skipf("cannot stage a probe file: %v", werr) + } + defer os.Remove(probe) + } + out, err := runInSandbox(t, liveSpec(t, 51234), "/bin/cat", probe) + if err == nil { + t.Fatalf("expected reading %s to fail inside the sandbox; got output:\n%s", probe, out) + } + t.Logf("denied as expected: %v\n%s", err, out) +} + +// The network fence: a raw outbound connection to a non-loopback host must fail. +func TestLiveDeniesExternalEgress(t *testing.T) { + requireLive(t) + // curl to a well-known host; expect a non-zero exit (connection denied by the sandbox). + out, err := runInSandbox(t, liveSpec(t, 51234), "/usr/bin/curl", "-sS", "--max-time", "5", "https://example.com") + if err == nil { + t.Fatalf("expected external egress to be denied; got:\n%s", out) + } + t.Logf("egress denied as expected: %v\n%s", err, out) +} + +// Loopback to the proxy port must be reachable: start a listener on that port and curl it. +func TestLiveAllowsLoopbackToProxy(t *testing.T) { + requireLive(t) + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + port := ln.Addr().(*net.TCPAddr).Port + srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, "reached-proxy") + })} + go func() { _ = srv.Serve(ln) }() + defer srv.Close() + + url := "http://127.0.0.1:" + strconv.Itoa(port) + "/" + out, err := runInSandbox(t, liveSpec(t, port), "/usr/bin/curl", "-sS", "--max-time", "5", url) + if err != nil { + t.Fatalf("expected loopback to the proxy port to succeed, got %v:\n%s", err, out) + } + if !strings.Contains(out, "reached-proxy") { + t.Fatalf("did not reach the loopback listener; output:\n%s", out) + } +} + +// The sandbox must not stop an ordinary command from running (baseline: profile boots a process). +func TestLiveAllowsBasicExec(t *testing.T) { + requireLive(t) + out, err := runInSandbox(t, liveSpec(t, 51234), "/bin/echo", "hello") + if err != nil { + t.Fatalf("basic exec failed inside the sandbox: %v\n%s", err, out) + } + if !strings.Contains(out, "hello") { + t.Fatalf("unexpected output: %q", out) + } + _ = exec.Command // keep import if trimmed +} + +func TestLiveKeychainReadDenied(t *testing.T) { + requireLive(t) + // `security find-generic-password` talks to the keychain mach services we omit; it must fail. + out, err := runInSandbox(t, liveSpec(t, 51234), "/usr/bin/security", "find-generic-password", "-s", "infisical") + if err == nil { + t.Fatalf("expected keychain access to be denied; got:\n%s", out) + } + t.Logf("keychain denied as expected: %v", err) + _ = time.Second +} diff --git a/packages/sandbox/seatbelt_profile.go b/packages/sandbox/seatbelt_profile.go new file mode 100644 index 00000000..4fa6896e --- /dev/null +++ b/packages/sandbox/seatbelt_profile.go @@ -0,0 +1,158 @@ +package sandbox + +import ( + "sort" + "strconv" + "strings" +) + +// generateSeatbeltProfile builds the SBPL profile for a spec. Pure (no OS calls), golden-testable. +// The keychain services (securityd.xpc, SecurityServer) and trustd are deliberately omitted from the +// allow-list, so (deny default) blocks keyring reads while injected-CA TLS still works. +func generateSeatbeltProfile(spec SandboxSpec) string { + var b []string + add := func(lines ...string) { b = append(b, lines...) } + + add( + "(version 1)", + "(deny default)", + "", + "; Process", + "(allow process-exec)", + "(allow process-fork)", + "(allow process-info* (target same-sandbox))", + "(allow signal (target same-sandbox))", + "(allow mach-priv-task-port (target same-sandbox))", + "", + "(allow user-preference-read)", + "", + "; Baseline mach services (keychain services deliberately omitted; trustd gated on AllowTrustd)", + "(allow mach-lookup", + ` (global-name "com.apple.audio.systemsoundserver")`, + ` (global-name "com.apple.distributed_notifications@Uv3")`, + ` (global-name "com.apple.FontObjectsServer")`, + ` (global-name "com.apple.fonts")`, + ` (global-name "com.apple.logd")`, + ` (global-name "com.apple.lsd.mapdb")`, + ` (global-name "com.apple.system.logger")`, + ` (global-name "com.apple.system.notification_center")`, + ` (global-name "com.apple.system.opendirectoryd.libinfo")`, + ` (global-name "com.apple.system.opendirectoryd.membership")`, + ` (global-name "com.apple.bsd.dirhelper")`, + ` (global-name "com.apple.coreservices.launchservicesd")`, + ")", + "", + "; POSIX IPC", + "(allow ipc-posix-shm)", + "(allow ipc-posix-sem)", + "", + "; IOKit", + "(allow iokit-open", + ` (iokit-registry-entry-class "IOSurfaceRootUserClient")`, + ` (iokit-registry-entry-class "RootDomainUserClient")`, + ` (iokit-user-client-class "IOSurfaceSendRight")`, + ")", + "(allow iokit-get-properties)", + "", + "; System info", + "(allow sysctl-read)", + "(allow system-socket (require-all (socket-domain AF_SYSTEM) (socket-protocol 2)))", + "", + "; Device files", + `(allow file-ioctl (literal "/dev/null") (literal "/dev/zero") (literal "/dev/random") (literal "/dev/urandom") (literal "/dev/dtracehelper") (literal "/dev/tty"))`, + "", + "; Pseudo-terminal (interactive TUIs need a real pty)", + "(allow pseudo-tty)", + `(allow file-ioctl (literal "/dev/ptmx") (regex #"^/dev/ttys"))`, + `(allow file-read* file-write* (literal "/dev/ptmx") (regex #"^/dev/ttys"))`, + "", + ) + + if spec.AllowTrustd { + // Cert-trust evaluation only. securityd/SecurityServer (keychain secret reads) stay omitted, so + // the login token remains unreadable; this just lets native-trust clients verify certs. + add( + "; trustd: cert-trust evaluation (keychain secret access still denied)", + `(allow mach-lookup (global-name "com.apple.trustd") (global-name "com.apple.trustd.agent"))`, + "", + ) + } + + // Egress must be filtered on `remote ip`, not `local ip` (a local filter matches the any-address + // and would admit all egress). Bind/inbound are wildcarded so agents can run local dev servers. + add("; Network") + port := strconv.Itoa(spec.LoopbackPort) + add( + `(allow network-bind (local ip "*:*"))`, + `(allow network-inbound (local ip "*:*"))`, + `(allow network-outbound (remote ip "localhost:`+port+`"))`, + ) + add("") + + // Broad read, then subtract the credential deny paths (SBPL is last-match-wins). + add("; File read") + add("(allow file-read*)") + for _, p := range dedupeSorted(spec.DenyPaths) { + add(`(deny file-read* (subpath ` + escapeSBPL(p) + `))`) + } + add("") + + add("; File write") + writePaths := dedupeSorted(append([]string{spec.Cwd, spec.TempDir, "/tmp", "/private/tmp", "/dev/null"}, spec.WritePaths...)) + for _, p := range writePaths { + if p == "" { + continue + } + if p == "/dev/null" { + add(`(allow file-write* (literal "/dev/null"))`) + continue + } + add(`(allow file-write* (subpath ` + escapeSBPL(p) + `))`) + } + add("") + + // Re-deny writes to credential paths after the write allows (SBPL is last-match-wins), so a writable + // cwd or write path that contains a denied path (e.g. running the agent from $HOME) can't write into + // it, for example appending to ~/.ssh/authorized_keys. + add("; Credential paths: deny writes too (not just reads)") + for _, p := range dedupeSorted(spec.DenyPaths) { + add(`(deny file-write* (subpath ` + escapeSBPL(p) + `))`) + } + + return strings.Join(b, "\n") + "\n" +} + +func dedupeSorted(in []string) []string { + seen := make(map[string]struct{}, len(in)) + var out []string + for _, s := range in { + if s == "" { + continue + } + if _, ok := seen[s]; ok { + continue + } + seen[s] = struct{}{} + out = append(out, s) + } + sort.Strings(out) + return out +} + +// escapeSBPL quotes a path as an SBPL (C-style) string literal. +func escapeSBPL(p string) string { + var sb strings.Builder + sb.WriteByte('"') + for _, r := range p { + switch r { + case '\\': + sb.WriteString(`\\`) + case '"': + sb.WriteString(`\"`) + default: + sb.WriteRune(r) + } + } + sb.WriteByte('"') + return sb.String() +} diff --git a/packages/sandbox/seatbelt_profile_test.go b/packages/sandbox/seatbelt_profile_test.go new file mode 100644 index 00000000..66c883fe --- /dev/null +++ b/packages/sandbox/seatbelt_profile_test.go @@ -0,0 +1,141 @@ +package sandbox + +import ( + "strings" + "testing" +) + +func testSpec() SandboxSpec { + return SandboxSpec{ + Sandbox: true, + Cwd: "/Users/dev/project", + TempDir: "/private/tmp/infisical-run-abc", + LoopbackPort: 51234, + DenyPaths: []string{ + "/Users/dev/.aws", + "/Users/dev/.infisical", + "/Users/dev/.ssh", + }, + WritePaths: []string{"/Users/dev/.cache"}, + NetMode: HardFence, + } +} + +func TestSeatbeltProfileStructure(t *testing.T) { + p := generateSeatbeltProfile(testSpec()) + + mustContain := []string{ + "(version 1)", + "(deny default)", + "(allow pseudo-tty)", + // Egress must be filtered on remote + the exact loopback port, never local. + `(allow network-outbound (remote ip "localhost:51234"))`, + // Credential deny paths (sorted). + `(deny file-read* (subpath "/Users/dev/.aws"))`, + `(deny file-read* (subpath "/Users/dev/.infisical"))`, + `(deny file-read* (subpath "/Users/dev/.ssh"))`, + // Writable cwd + tempdir + operator path. + `(allow file-write* (subpath "/Users/dev/project"))`, + `(allow file-write* (subpath "/private/tmp/infisical-run-abc"))`, + `(allow file-write* (subpath "/Users/dev/.cache"))`, + } + for _, s := range mustContain { + if !strings.Contains(p, s) { + t.Errorf("profile missing expected line:\n %s\n---\n%s", s, p) + } + } + + mustNotContain := []string{ + // The two keychain services must be omitted so (deny default) blocks the keyring. + "com.apple.securityd.xpc", + "com.apple.SecurityServer", + // trustd omitted so the injected CA bundle is the trust path. + "com.apple.trustd", + // Never filter egress on local ip (silently admits all egress). + "(allow network-outbound (local ip", + // No blanket network allow. + "(allow network*)", + } + for _, s := range mustNotContain { + if strings.Contains(p, s) { + t.Errorf("profile must NOT contain %q\n---\n%s", s, p) + } + } +} + +func TestSeatbeltProfileTrustdToggle(t *testing.T) { + spec := testSpec() + + // Default: trustd omitted so native-trust TLS falls back to the injected CA bundle. + if strings.Contains(generateSeatbeltProfile(spec), "com.apple.trustd") { + t.Error("trustd must be omitted when AllowTrustd is false") + } + + // AllowTrustd: trustd allowed, but keychain secret services stay omitted (token stays unreadable). + spec.AllowTrustd = true + p := generateSeatbeltProfile(spec) + if !strings.Contains(p, `(global-name "com.apple.trustd")`) { + t.Errorf("expected trustd allowed when AllowTrustd is true\n%s", p) + } + for _, s := range []string{"com.apple.securityd.xpc", "com.apple.SecurityServer"} { + if strings.Contains(p, s) { + t.Errorf("keychain secret service %q must stay omitted even with AllowTrustd", s) + } + } +} + +func TestSeatbeltProfileEscaping(t *testing.T) { + spec := testSpec() + spec.DenyPaths = []string{`/Users/dev/weird "dir"\path`} + p := generateSeatbeltProfile(spec) + if !strings.Contains(p, `(subpath "/Users/dev/weird \"dir\"\\path")`) { + t.Errorf("path not escaped correctly:\n%s", p) + } +} + +func TestSeatbeltProfileDenyBeforeAllowWrite(t *testing.T) { + // A denied read path that also appears as a write path: the write allow is emitted, but the read + // deny must still be present (SBPL is last-match-wins, and this documents the ordering intent). + p := generateSeatbeltProfile(testSpec()) + readAllow := strings.Index(p, "(allow file-read*)") + firstDeny := strings.Index(p, "(deny file-read*") + if readAllow == -1 || firstDeny == -1 || firstDeny < readAllow { + t.Fatalf("expected broad read allow before the deny subtractions (allow=%d deny=%d)", readAllow, firstDeny) + } +} + +func TestPassthroughBackendWrap(t *testing.T) { + spec := SandboxSpec{Sandbox: false, Env: []string{"FOO=bar"}} + b := NewBackend(spec) + if _, ok := b.(passthroughBackend); !ok { + t.Fatalf("expected passthrough backend when Sandbox=false, got %T", b) + } + cmd, err := b.Wrap(spec, []string{"echo", "hi"}) + if err != nil { + t.Fatal(err) + } + if cmd.Args[0] != "echo" || cmd.Args[1] != "hi" { + t.Fatalf("unexpected argv: %v", cmd.Args) + } + if len(cmd.Env) != 1 || cmd.Env[0] != "FOO=bar" { + t.Fatalf("env not set from spec: %v", cmd.Env) + } +} + +func TestDefaultDenyPaths(t *testing.T) { + got := DefaultDenyPaths("/Users/dev") + want := map[string]bool{ + "/Users/dev/.infisical": true, + "/Users/dev/.aws": true, + "/Users/dev/.ssh": true, + } + for _, p := range got { + delete(want, p) + } + if len(want) != 0 { + t.Fatalf("DefaultDenyPaths missing entries: %v", want) + } + if DefaultDenyPaths("") != nil { + t.Fatal("DefaultDenyPaths(\"\") should be nil") + } +} diff --git a/packages/sandbox/spec.go b/packages/sandbox/spec.go new file mode 100644 index 00000000..28d02e31 --- /dev/null +++ b/packages/sandbox/spec.go @@ -0,0 +1,69 @@ +// Package sandbox applies an OS-level jail (macOS Seatbelt, Linux bubblewrap) around the agent +// spawned by `agent-proxy run`. The profile/argv generators are pure functions of a SandboxSpec so +// they are golden-testable on any platform; only the exec wiring is platform-specific. +package sandbox + +type NetMode int + +const ( + // HardFence: no external egress, the proxy is the only route out (macOS (deny default) SBPL; + // Linux empty netns bridged to the proxy socket). + HardFence NetMode = iota + // SharedNet: host network shared. macOS (SBPL still fences to loopback) and the Linux fallback + // when unprivileged user namespaces are restricted. Only the network fence weakens, not the + // credential controls. + SharedNet +) + +// SandboxSpec is the in-memory policy for one wrapped command. Nothing here is persisted. +type SandboxSpec struct { + Sandbox bool // false => --no-sandbox: run uncontained + + ReadPaths []string + WritePaths []string + DenyPaths []string // read-denied credential paths, subtracted from the broad read allow + + Cwd string + TempDir string // per-run 0700 dir: CA cert, and the unix socket on the Linux hard fence + + // LoopbackPort is the proxy's TCP port (macOS / Linux shared-net) or the in-namespace bridge port + // (Linux hard fence); ProxySocket is the proxy's unix socket, set only on the hard fence. + LoopbackPort int + ProxySocket string + + Env []string // fully-prepared, scrubbed child environment + NetMode NetMode + + // AllowTrustd allows the macOS cert-trust evaluation service so native-trust tools (Go CLIs like + // gh) can verify the proxy's leaves against a CA trusted in the keychain. securityd (keychain + // secret reads) stays blocked regardless, so the login token remains unreadable. macOS only. + AllowTrustd bool +} + +// DefaultDenyPaths returns the credential paths denied by default, resolved against home. +func DefaultDenyPaths(home string) []string { + if home == "" { + return nil + } + rel := []string{ + ".infisical", + "infisical-keyring", // file-vault backend store (JWT + backup key); default on headless Linux + ".aws", + ".ssh", + ".config/gcloud", + ".azure", + ".kube", + ".netrc", + ".docker/config.json", + ".config/infisical", + ".config/gh", // GitHub CLI token + ".git-credentials", // git stored credentials + ".npmrc", // npm auth token + ".gnupg", // GPG private keys + } + paths := make([]string, 0, len(rel)) + for _, r := range rel { + paths = append(paths, home+"/"+r) + } + return paths +}