From dec2f2e4d989c7f2adabfa6025a08914f3df3394 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 05:50:33 -0500 Subject: [PATCH 001/291] chore(deps): update Hermes to v0.3.6 and libc to v1.80.0 --- backend/go.mod | 4 ++-- backend/go.sum | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index bc339111b..d368450b1 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,7 +4,7 @@ go 1.27.3 require ( filippo.io/age v1.3.2 - github.com/Wikid82/Hermes v0.3.5 + github.com/Wikid82/Hermes v0.3.6 github.com/gin-contrib/gzip v1.2.9 github.com/gin-gonic/gin v1.12.0 github.com/glebarez/sqlite v1.11.0 @@ -110,7 +110,7 @@ require ( golang.org/x/sys v0.49.0 // indirect google.golang.org/protobuf v1.36.12 // indirect gopkg.in/ini.v1 v1.67.3 // indirect - modernc.org/libc v1.79.1 // indirect + modernc.org/libc v1.80.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.12.1 // indirect modernc.org/sqlite v1.61.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 631f39119..99e7f7c32 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -6,8 +6,8 @@ filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g= github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U= -github.com/Wikid82/Hermes v0.3.5 h1:wVlHNaRaHMyVqtXnW3mHsEcUwoFmun81+3ooSY2KfzI= -github.com/Wikid82/Hermes v0.3.5/go.mod h1:A2fFibmMT+lFy68VeBK8fWl1Ry0tCqqiDoK+w0XwcLI= +github.com/Wikid82/Hermes v0.3.6 h1:FCl8YV4O8AmfQ927JI1X//hh52rJ7yrMlAVm6/1PsPA= +github.com/Wikid82/Hermes v0.3.6/go.mod h1:wiiWSIxUhw2rIbHbLwAyN24C0jl9QSaT/qgccJ3p2Z4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= @@ -262,10 +262,10 @@ gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys= -modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4= -modernc.org/ccgo/v4 v4.37.0/go.mod h1:/8627VhwWtAbqUXmPORxFQU5SgLrAx2jl9seLw9GBhY= +modernc.org/cc/v4 v4.30.5 h1:s086/M3aHHBJVrk95Dq+ciWXLOwcH6EruIoTb8XWTAY= +modernc.org/cc/v4 v4.30.5/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.38.0 h1:bvjlNLdV3ysX2TcKFPmGnzYtPi69647Q3HffqSs9XQE= +modernc.org/ccgo/v4 v4.38.0/go.mod h1:YGey7f3gDAm8DkKXi1CF1/tbQsu+Cbco6wj5FIU5Ah8= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= @@ -274,8 +274,8 @@ modernc.org/gc/v3 v3.2.0 h1:/Y3LGDMQaoYf55PebP1qsZ5FDGgAZyG1RYKEHeQzh1o= modernc.org/gc/v3 v3.2.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.79.1 h1:GgKnSYDT0WctfCKddw03DPUiSAiL2LZIF/fGxqqLkN0= -modernc.org/libc v1.79.1/go.mod h1:uT3kR61NsbeKJSLqq83GJwSnZMVLlSjyAz4DfTaBy1M= +modernc.org/libc v1.80.0 h1:EHPhhOouLWob4DZRY7VIcaIoyzT4MKGEz25jRecd8KE= +modernc.org/libc v1.80.0/go.mod h1:KaEO/N1pWq08YxZ+wfdA4+d/WeE7TvRRnoLaFI46YMc= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= From 7b69888b41278248019ca235215fc635b2a48a5a Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 05:56:32 -0500 Subject: [PATCH 002/291] chore(deps): update typescript to version 7.1.0-dev.20261102.1 --- frontend/package-lock.json | 74 +++++++++++++++++++------------------- frontend/package.json | 2 +- package-lock.json | 64 ++++++++++++++++----------------- package.json | 2 +- 4 files changed, 71 insertions(+), 71 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f42f584e9..b109485c4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -57,7 +57,7 @@ "@typescript-eslint/eslint-plugin": "^8.72.1", "@typescript-eslint/parser": "^8.72.1", "@typescript-eslint/utils": "^8.72.1", - "@typescript/native": "npm:typescript@^7.1.0-dev.20261101.1", + "@typescript/native": "npm:typescript@^7.1.0-dev.20261102.1", "@vitejs/plugin-react": "^6.1.2", "@vitest/coverage-istanbul": "^5.0.5", "@vitest/coverage-v8": "^5.0.5", @@ -3947,9 +3947,9 @@ }, "node_modules/@typescript/native": { "name": "typescript", - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-fYEA8Z7NHFRhbSYFYGFSbmAcNbqNQM1w6OBlPj00+axX/fnDPJ1F9yr3c17G09lvVdw0aepa/QWjY2ANXdcbaA==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-oSEOA23BPTy2rri0alGxC2XU7zyEQw4W05lETiRgvXidZ8iFCCqAxwguX4MHzeobqmI7dgHOdIsdmUIkTeoOSg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3959,13 +3959,13 @@ "node": ">=16.20.0" }, "optionalDependencies": { - "@typescript/typescript-darwin-arm64": "7.1.0-dev.20261101.1", - "@typescript/typescript-darwin-x64": "7.1.0-dev.20261101.1", - "@typescript/typescript-linux-arm": "7.1.0-dev.20261101.1", - "@typescript/typescript-linux-arm64": "7.1.0-dev.20261101.1", - "@typescript/typescript-linux-x64": "7.1.0-dev.20261101.1", - "@typescript/typescript-win32-arm64": "7.1.0-dev.20261101.1", - "@typescript/typescript-win32-x64": "7.1.0-dev.20261101.1" + "@typescript/typescript-darwin-arm64": "7.1.0-dev.20261102.1", + "@typescript/typescript-darwin-x64": "7.1.0-dev.20261102.1", + "@typescript/typescript-linux-arm": "7.1.0-dev.20261102.1", + "@typescript/typescript-linux-arm64": "7.1.0-dev.20261102.1", + "@typescript/typescript-linux-x64": "7.1.0-dev.20261102.1", + "@typescript/typescript-win32-arm64": "7.1.0-dev.20261102.1", + "@typescript/typescript-win32-x64": "7.1.0-dev.20261102.1" } }, "node_modules/@typescript/old": { @@ -3984,9 +3984,9 @@ } }, "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-p1IwuiIoUZOQ6nAx6FG0qKZytR/3ho3uSfdw2RQbX0pNRi+ELWaeovHxJH8r5UxXE4CnOUDumVnatKdJZu0Z7w==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-Lqujbb8jso9SuMF8Jv8lUagiihuuJCP2LDqfQsA70BWIvnQojcwVmvofVZ3mjFNTW2OKhShGYNlU/LZzNz25jg==", "cpu": [ "arm64" ], @@ -4001,9 +4001,9 @@ } }, "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-gdehNZ0iyLK5Oq6cOf9NuPubgue5A+zFTE1bECQ+c1wZKNPdx56raG7XUDNkY95Xh0NGRtE+tMMUnpxu95cs2g==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-rOkgtv9E7TP0Tgl1jaskq8EJFvcG6FO6wFM16IbC+0S4S+iBFTVkou2X3O+ns/ex2so5BErIs3AXtKV/Q5ySIQ==", "cpu": [ "x64" ], @@ -4018,9 +4018,9 @@ } }, "node_modules/@typescript/typescript-linux-arm": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-4Ib6Nr6OzhCqUqQxiDRk9s46mcSbD4ccdt1jbHrQ/UOr8PUxBAQumsatt7ZqIu1VhJGEYUBXtaH7VNPi/bugHw==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-lHpGaqIS3g6Z9SvhYcvkWh7ciRRWFBAT05Ii23wDvciHkIDv0kkG9ubqHNEqjIH3HDyjwhxFB/0tdbxgxNjKww==", "cpu": [ "arm" ], @@ -4035,9 +4035,9 @@ } }, "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-UGE9UCPtg//ITH1jtsldUPhkt4WsedCvj9P/kFiV5Jze2c67QV03Pp+3L64ie52oC54hpgdYBGcxzUD62CuOag==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-Sp9rfaw3UhXaT2+KwIa0SU5ar7g4kl+mRggfD/PQzPKgJiL9YrAWvbBiq1lpXXt5exdGLexiSOut//2O/LplJQ==", "cpu": [ "arm64" ], @@ -4052,9 +4052,9 @@ } }, "node_modules/@typescript/typescript-linux-x64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-XkVVJMu/LiHxovdRPO+TAAxjxxhYMTCNx41jUkfliYQ+DUiEj4g+uf1ucJHUzK4uEOCsyimEloOuBxvCgAuhJw==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-tJzZqgoAiz6k+mN/OxRmSeEf1OmblugASGXZBTzn6A4+BlVOLu1N4rvF65jCwMBWMCEtIjSJEJwcQ17XobTSwg==", "cpu": [ "x64" ], @@ -4069,9 +4069,9 @@ } }, "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-PZVaaGCK9F9CBvcPq8RP1kwmU3LoQiHotgDIoFuFoYY2G2xKNE64IITGc4zXrahuxhv0Qk4XO+oMbx2MI1zbxQ==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-bPLL+0+M38TL/2JYM2MwkthuKPvB1+5lXdRrf7EO9YOp8StoN8i/QtPEOBG8g9HxUdQl74LKnFoTr/OKC+Faog==", "cpu": [ "arm64" ], @@ -4086,9 +4086,9 @@ } }, "node_modules/@typescript/typescript-win32-x64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-nqD53qpSdtmN/dWSEx+enjFGTXHwW/S58JXsVgNnsUzkHfAKN8jcr0D245X4fgfzZfAvuTgjExZ4z8krCBJFnw==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-JruaLShE1/9BSBTndgwkARyDEtuYMejsfTF4WBTcry6eE95PstXDLT69slICds/2ihkuBD2pngHuoai/r3OXeQ==", "cpu": [ "x64" ], @@ -4427,9 +4427,9 @@ ] }, "node_modules/@vitejs/plugin-react": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", - "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.2.1.tgz", + "integrity": "sha512-9KyWCij3r0+qLG3Q62vBmv0VyevmYibAtxaCIVK9o0k6CXgaajZRIe8tpfchQcdZArJ5b/7OrIz4W3b2uxVRDA==", "dev": true, "license": "MIT", "dependencies": { @@ -4439,9 +4439,9 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0 || ^0.3.0", "babel-plugin-react-compiler": "^1.0.0", - "oxc-transform-react": "^0.152.0", + "oxc-transform-react": ">=0.152.0 <0.157.0", "vite": "^8.0.0" }, "peerDependenciesMeta": { diff --git a/frontend/package.json b/frontend/package.json index b1c8fc456..661d48c28 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -77,7 +77,7 @@ "@typescript-eslint/eslint-plugin": "^8.72.1", "@typescript-eslint/parser": "^8.72.1", "@typescript-eslint/utils": "^8.72.1", - "@typescript/native": "npm:typescript@^7.1.0-dev.20261101.1", + "@typescript/native": "npm:typescript@^7.1.0-dev.20261102.1", "@vitejs/plugin-react": "^6.1.2", "@vitest/coverage-istanbul": "^5.0.5", "@vitest/coverage-v8": "^5.0.5", diff --git a/package-lock.json b/package-lock.json index 9d3cd2c38..0802c1b29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@playwright/test": "^1.64.1", "@types/eslint-plugin-jsx-a11y": "6.10.0", "@types/node": "^26.6.6", - "@typescript/native": "npm:typescript@^7.1.0-dev.20261101.1", + "@typescript/native": "npm:typescript@^7.1.0-dev.20261102.1", "audit-ci": "^7.1.0", "dotenv": "^18.0.9", "eslint": "^10.14.0", @@ -491,9 +491,9 @@ }, "node_modules/@typescript/native": { "name": "typescript", - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-fYEA8Z7NHFRhbSYFYGFSbmAcNbqNQM1w6OBlPj00+axX/fnDPJ1F9yr3c17G09lvVdw0aepa/QWjY2ANXdcbaA==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-oSEOA23BPTy2rri0alGxC2XU7zyEQw4W05lETiRgvXidZ8iFCCqAxwguX4MHzeobqmI7dgHOdIsdmUIkTeoOSg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -503,13 +503,13 @@ "node": ">=16.20.0" }, "optionalDependencies": { - "@typescript/typescript-darwin-arm64": "7.1.0-dev.20261101.1", - "@typescript/typescript-darwin-x64": "7.1.0-dev.20261101.1", - "@typescript/typescript-linux-arm": "7.1.0-dev.20261101.1", - "@typescript/typescript-linux-arm64": "7.1.0-dev.20261101.1", - "@typescript/typescript-linux-x64": "7.1.0-dev.20261101.1", - "@typescript/typescript-win32-arm64": "7.1.0-dev.20261101.1", - "@typescript/typescript-win32-x64": "7.1.0-dev.20261101.1" + "@typescript/typescript-darwin-arm64": "7.1.0-dev.20261102.1", + "@typescript/typescript-darwin-x64": "7.1.0-dev.20261102.1", + "@typescript/typescript-linux-arm": "7.1.0-dev.20261102.1", + "@typescript/typescript-linux-arm64": "7.1.0-dev.20261102.1", + "@typescript/typescript-linux-x64": "7.1.0-dev.20261102.1", + "@typescript/typescript-win32-arm64": "7.1.0-dev.20261102.1", + "@typescript/typescript-win32-x64": "7.1.0-dev.20261102.1" } }, "node_modules/@typescript/old": { @@ -528,9 +528,9 @@ } }, "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-p1IwuiIoUZOQ6nAx6FG0qKZytR/3ho3uSfdw2RQbX0pNRi+ELWaeovHxJH8r5UxXE4CnOUDumVnatKdJZu0Z7w==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-Lqujbb8jso9SuMF8Jv8lUagiihuuJCP2LDqfQsA70BWIvnQojcwVmvofVZ3mjFNTW2OKhShGYNlU/LZzNz25jg==", "cpu": [ "arm64" ], @@ -545,9 +545,9 @@ } }, "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-gdehNZ0iyLK5Oq6cOf9NuPubgue5A+zFTE1bECQ+c1wZKNPdx56raG7XUDNkY95Xh0NGRtE+tMMUnpxu95cs2g==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-rOkgtv9E7TP0Tgl1jaskq8EJFvcG6FO6wFM16IbC+0S4S+iBFTVkou2X3O+ns/ex2so5BErIs3AXtKV/Q5ySIQ==", "cpu": [ "x64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@typescript/typescript-linux-arm": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-4Ib6Nr6OzhCqUqQxiDRk9s46mcSbD4ccdt1jbHrQ/UOr8PUxBAQumsatt7ZqIu1VhJGEYUBXtaH7VNPi/bugHw==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-lHpGaqIS3g6Z9SvhYcvkWh7ciRRWFBAT05Ii23wDvciHkIDv0kkG9ubqHNEqjIH3HDyjwhxFB/0tdbxgxNjKww==", "cpu": [ "arm" ], @@ -579,9 +579,9 @@ } }, "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-UGE9UCPtg//ITH1jtsldUPhkt4WsedCvj9P/kFiV5Jze2c67QV03Pp+3L64ie52oC54hpgdYBGcxzUD62CuOag==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-Sp9rfaw3UhXaT2+KwIa0SU5ar7g4kl+mRggfD/PQzPKgJiL9YrAWvbBiq1lpXXt5exdGLexiSOut//2O/LplJQ==", "cpu": [ "arm64" ], @@ -596,9 +596,9 @@ } }, "node_modules/@typescript/typescript-linux-x64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-XkVVJMu/LiHxovdRPO+TAAxjxxhYMTCNx41jUkfliYQ+DUiEj4g+uf1ucJHUzK4uEOCsyimEloOuBxvCgAuhJw==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-tJzZqgoAiz6k+mN/OxRmSeEf1OmblugASGXZBTzn6A4+BlVOLu1N4rvF65jCwMBWMCEtIjSJEJwcQ17XobTSwg==", "cpu": [ "x64" ], @@ -613,9 +613,9 @@ } }, "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-PZVaaGCK9F9CBvcPq8RP1kwmU3LoQiHotgDIoFuFoYY2G2xKNE64IITGc4zXrahuxhv0Qk4XO+oMbx2MI1zbxQ==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-bPLL+0+M38TL/2JYM2MwkthuKPvB1+5lXdRrf7EO9YOp8StoN8i/QtPEOBG8g9HxUdQl74LKnFoTr/OKC+Faog==", "cpu": [ "arm64" ], @@ -630,9 +630,9 @@ } }, "node_modules/@typescript/typescript-win32-x64": { - "version": "7.1.0-dev.20261101.1", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.1.0-dev.20261101.1.tgz", - "integrity": "sha512-nqD53qpSdtmN/dWSEx+enjFGTXHwW/S58JXsVgNnsUzkHfAKN8jcr0D245X4fgfzZfAvuTgjExZ4z8krCBJFnw==", + "version": "7.1.0-dev.20261102.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.1.0-dev.20261102.1.tgz", + "integrity": "sha512-JruaLShE1/9BSBTndgwkARyDEtuYMejsfTF4WBTcry6eE95PstXDLT69slICds/2ihkuBD2pngHuoai/r3OXeQ==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 397044eed..795520c14 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@playwright/test": "^1.64.1", "@types/eslint-plugin-jsx-a11y": "6.10.0", "@types/node": "^26.6.6", - "@typescript/native": "npm:typescript@^7.1.0-dev.20261101.1", + "@typescript/native": "npm:typescript@^7.1.0-dev.20261102.1", "audit-ci": "^7.1.0", "dotenv": "^18.0.9", "eslint": "^10.14.0", From 63f724e956404005364a93dd38157926c44846f1 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 05:59:08 -0500 Subject: [PATCH 003/291] chore(deps): update @vitejs/plugin-react to version 6.2.1 --- frontend/package-lock.json | 2 +- frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b109485c4..2bcf70f92 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -58,7 +58,7 @@ "@typescript-eslint/parser": "^8.72.1", "@typescript-eslint/utils": "^8.72.1", "@typescript/native": "npm:typescript@^7.1.0-dev.20261102.1", - "@vitejs/plugin-react": "^6.1.2", + "@vitejs/plugin-react": "^6.2.1", "@vitest/coverage-istanbul": "^5.0.5", "@vitest/coverage-v8": "^5.0.5", "@vitest/eslint-plugin": "^1.6.28", diff --git a/frontend/package.json b/frontend/package.json index 661d48c28..bd44f8884 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -78,7 +78,7 @@ "@typescript-eslint/parser": "^8.72.1", "@typescript-eslint/utils": "^8.72.1", "@typescript/native": "npm:typescript@^7.1.0-dev.20261102.1", - "@vitejs/plugin-react": "^6.1.2", + "@vitejs/plugin-react": "^6.2.1", "@vitest/coverage-istanbul": "^5.0.5", "@vitest/coverage-v8": "^5.0.5", "@vitest/eslint-plugin": "^1.6.28", From 8ee6b1171f162317d77383446391295b123bf3db Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 09:29:10 -0500 Subject: [PATCH 004/291] chore(assets): update banner and favicon images --- docs-site/assets/banner.webp | Bin 311174 -> 48066 bytes docs-site/assets/favicon.png | Bin 7000 -> 13940 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs-site/assets/banner.webp b/docs-site/assets/banner.webp index 21e353934a9347f721eb210b350d3450a1b067ff..2840bf4a8db791d274f2d207a267e9557ccaaefd 100644 GIT binary patch literal 48066 zcmcHgcU)7+7e5ZqO(66pB9ZQ*V5NviCsB80ZGgKX79b$2U_n4cI=NO<5bPi=aV;RA zpsaL~C?zUjP&$Ex-XVbi38eilyPwzh`+NR){&@a)`~zq1oqOiYnN!~9%;b-Kd-qmU z0^s@EZug_^wg=xKZF6ZY2{d^a{em(8A>gDo3nn~Ve>VM}IOJdOg(Sb77HAk9A*g{O zLL#u4Xf&-v1%QwcTU8L@iv`Nq5i4jwx`UEOH z#24aGX{>k00)-=V9@QHSu}$EF+GpyqgOm5@{Gtc99R3mfDUkl&NCRvhJ=nbKkGE^a zOM@mmPE41+tvh!<=ym)e`bK>7NME{H>GmvHUGWZr=gP*bnVk>Fj8n^p2rG zVzemWQKVx^6zR&F8Ba+cGSYEcALle!&DL8gr1XmSMCiuNeb!eF+^kML><~ZHUrUhG zEtHA`pHf9N4a5OcQ|TZ|9nzxa5F8wSO4}CaEtY}H#jPLMu z&uMXp6G)H;(c{ec4@>m#@H(LeiD^7(ddESK1L>fAP*ouJg&UDo%>)y=Asw`!i0VN+vg zD*9G;#4=a>BzS4Ig47H0S5L-YsYC{|#%uWS^Mx7~e|nrcA2#Dl+l-0eB26ChXq&Hm z7B*SfMlg_2)M~-8VWV3$Y<_6%A*X#2o36HbVLIx?5D~BSMlu zq>5cWbCfij$PST1AN4D{&JffEr?tWBID4c`3E{X(=;+yY@zj+u>Jq?C7|fPW(};Yi z=x$gaRcJ8~rt5oKm`0Fld4Qv#xdSUL-@q7NN|qCCGXoJ<47_f-h9%wRt2I`{Cn{mT z8Hn%X+{48~1*jm=Y{S1>Rm5MS>H9tjB&qd4x8BtS&tH*>p`!i&^b(1Py1?ru^>K6f z94Pn56Ti@)xKJ=TBd z68XC!{X0dHdmVN7G0x=$4uvYH{rt!LPzB$f+eJR2dTqiM0|>H zkoqNl2KB9}g zHz66YmJ)RpfVjE$c2H zB4&xFr8flahN$#`FQ#Js6{ch7*>W-)T!~GRL}^m=52?)BX3^ei0sk3aVpGhi>pcTCogEsKz-62c zxF|%j*J+dp?Z`RImQqoljY$jCPrA)XdwZ6y#V-k+XrUeKZD)x z{K-Z-A+u}M%|d?|lb4haK%eVggQkF7a3fI}&s$50mX+@i=OeEg#Xq-g2G1*q620cw zTh_feF;?AV?@W#lD!sWUr5b%>Try4<5kHFutnkNji=_QHl|a`TzfLNb z0X~0_E|5ZS3#*!6o{>)AhFOakbydm4xA;*KYDfOMnpiRC)FJ%F_P4}tF)sjqZOvD6 zE@ds`06$ym0)3hw)yHdQh*{E7MDc9N(fkzFvqGUa-b#1x$a!f2j}))#{#JOWV8$1t@yjHuU{OajMWU#_nizgs{Dovm6fg!0J!yUW?k-$l_nIfvwcN0k8XLIr=zk8( z-mo>)7>yI1wPM;@`vyAkU8G;^43`?D)q@d<;j;4A6omxIzQH@hPm%>Tr2KHQ%X!`R z(&ZYZS&J<*YnFO*7Uv(A5I+zas*=)QS3Isrk8o(M~KRM`ekWq1cy%Py!5nVzxlDkC2u~11Y!34 zp*J;JIoRJ)CaleVciP|;>#6~y2R0wRUw^U>J;r+z@R4X3!Kf44&D#FFg17DHjH2L{ zFP)<`PgGf%!lrK-+0}!LtVNPEF<(Nb3cCX&^X3F$vK6$k?u)olNk|aw&QnUhAIfC1O8|>GmR(kT>qn(U^x&>6(r}XC8;{5&|(yEkeIu`Rz(s?a1<5`-J zD0!c7QR--!{&s0lfS^k~EvAg} zSq3gn=EyOQo7sa$80Bo{^6+pOHLKhbbEB??)XM3Ky5zVWYjMMg)2QL@H`V1rBY%BU zxR4+{$6SN{ob>6}iRs8rWb5{Hu;Uu6$)7u!-{DV}&R15At7*1w1-lLI8Fo0^89;m5 zx+;?zz2(T6n}g@?n9_`*VlzqagLZ(=0e-(M{KGc&fS->?1-QbBFPy{sRaLNhg?AG2 z(@+5Om(9{|xZpkqBy35>jgc9OQ{Y_>R;$(4%?2d;s6Z=Wmg!0H#K6AkMsujk)42y6+Tvj^@roR0V(YeEUn5Y(p>L&+ zlAQlHr(k-}ZPZ-`pd5^p^tVsGj}k}egR9x!^Kq?Bc1gqX>VewJw{6k4?t7n=#HGNB{)}wsy7zuw>|KCY z*Ls)jXcX+6k2d1^NA*-EnFcA3kw5;B!^4LK9Em*E{@;~YEDr|Qt6+!fyN1|99&dAL_Q+I>MO+%csN52PyG=Mow0<}0t3_)gH~ zr1EKH>`=)R2JdC%hp;Bt3Ai&XUEJaRW_Z)SD*7u-9xEGm1i) zTZ0Zd4Z8MUg!QfY>yM1*CVJTe6>lse`Qm^QtW!KLSjC6#5wL#Yk9)aJ^s83X;c zccOehCGK9ex37t7Fwf52k`_EM7u4g_&(B%*->>N|UWi|6%oa&T+~EBRZC$gr#xv9p zyoJUW<5^=WA&P&~P;$RqreUk(;!ttg!cqzMR1awc2?a&=7g^6V|8tDHW`1Sb0RhfM zRQeA1iF+m=t)~$qbkx@rQq~&;EDdhR3$j!KhHFadwGS7IW}ikLV_v}@vs~z!72IMI zb92HxD*M(0RC5vJoJM(OM>jOQBa|kZ(r8EwR1-^dMr;qFjBeBnj(3%jPLFQN>m6E+ zkqNx+G<@I)VR`9mr0o|5Tr>{3XCLvNR3ixoe`|f3hJn@On1g5Yj!PmBIlkNh<@b77 zN+~Y*n};L<$l{CGP=(96@f8X4oRJesfUBQiXKj>OGkEqjE_44Y3nO>saZaQ=ANDdYzZcZA(1$DhJN{Gu@`H zrgHJ}WBX*NE%}qpc6O4mSB#R6dO2+_9Q=fY5%tA;If!;|9@z+v*y&vKrD*YEmNbnH z{sH#OEL@)r*=BVQx_Z(teWJX~@Qh|rbkM(hT3<@~y3;qrA<~2<4Ja#gg)Kd@G^lLw>5IOOL&GeuC9k z<;4v2v#0zmjBYe>iT|)Q)UN5M8#saGjuP<(Vh_m#xHp*zv|_sJ&Ed9lJU7CM7ZW31 z?8<#elHzKQHfk)LI46q|B)@{%Og$LOQ76~BpVsXu6sN>vj%fag!>V;ywqVs)skpo$ zEYR;=K;NLhF)Ybhur#`nC7ydtQhUA++}K@18n+Sqouq=@#Qi|@-+wD;9 zoO!6Tmx}x=t(kxBt%iM#gNVPT_G8sA=}1nW+^?zr{pQpTkVn!>3CRB)+}L09TVB|J zhPvt|sH#OAYN(_BO}?xq8{{{QWUW94rx=X#C0!+$^T+%yZ|GcXTGQ8qDA0c)*_4Sk zQqPHRHZun5%Nt|!anSBAo9vQWc*1mfaP4XczQ50wWWVVLXUDlLh7Y*TOx_Gj zO=ZaSbY?tIkv{}0U67Np?PMe%9#%Dw46B*})T$7JKi$!JqqE6a_k}?AySBf81<>pH z;>EavmtVhp9XD-;-aSbhUDotn=&Z6rct$EFCKy`ZP91ctcVw{~xt`JPiZ-f>o3$@N z$J#Bd*HVCuFtJx>GHmai=Gzry*h~&j=T>>!1lzo8d0y;ex1{sH5bV13sr(A&i1cB} zMvzB2baEAd!jIjc?6y+iZ21Q}4&>A7OS@#%Q8!J`Jp}R`#_AkQ!J3SSWtm)y#VKRo z`zO`Vv7WStKERr?1@oB6b*efjdQ=sAX_ddu;K*0gJI8;Q#p4Y~fetkOR-(=w2#W5f z+|geyBeD*@oebce^LcB6Drj_zKa$`uaruHz%H-ta*R2Rrd3VOb=(4sW%)Tip)56Gn zqiGI>Zq;pDSd1zTX6ZfbU{Ruqy;&EIbkG@**BXIQv#J15(j!y@)*AG;ZD|QUdcD@6 zT|?^;B)M(KN|W&X7l3-*Ux?J0RExcFJ)?>%0No1VUkM$nZH#~WC)ntHYbLYvec_cR zrsmx82ghEI9J#s3V1vT^mS<7b1lEP49jW(u8b+P0)wGm9rB5(2X}hM{>2JcT3*^Xu z?+pDjJD%-HQU_91!Jm%M?tlEuqSI_s-%AVpCw6QN?s&)i5#AlAO3XODeUNge6MOcS-e-<3XoXjoY2(Ychq`57X~@`Oq6FkWI9~|Th8gVXH~Q$< zZDrYxCWW5!Kw!oh<~qjR01~MSwEI!Bv1a8$KR+sLmk%zTZa#58L&kPb{3V~ag&l3; zsLT@`^pOSj9LC+g?TQ9~&a9$x2AiUOngXqbI~lxxJPA#D5Ndtxj!KaC8ghS_kiXWU zy(v8W1WSqRh>MEJy7G>xYibQ-x)e>1z-_*~@0mcWz9-(K9{zOi(>B2M-i~UmOBrkS zdDryl4secWAHVIBiZ%j}@GL4;^)SMBT5Z`TH`vI;{&{kPIxWHxb7Sc9`jPd9)JZ4UGe*OXZTPbE?>=s@mvW)C8a`7Y(50-zzE7i z&`Ti((5Fj}N*@CDjtggWjaF-!ozf$%%#3nvbw$&p)Y|c=X=NCE4~h21?E#|Wv9FP; z2Z@ZHoR518q-qlrWyfP6bzk9G+JyO*`I@-o|78(9C$N3gzfkUSxW8XaY<57a|K;;` zw9x|Uqpky+rOlJC!G$Z$%#^iZ8c4d9OjP z8r~B27RLnFy)u-!(r}sFI4xI_HNwTzy^82tKu|j9w-b88dt}}^`@vEFSqF2dX-&A} zlVRt%xPSD#!2BkQa!)+>nS7svB6|16&*Q~a9-c7GwCg*dmCCtJU$+7%``B zGzcFts^DICJxYXuViN|DP~tv)scc&cOiPT8QUj8VbEu;c&l2NBBS`Y_KEM#VuI~V1 z4}n(6`Ct7|>Y}r&`q<7G)1@J_CPBUIhrc&^hU|weV(EVZJg-P``nOKaQrQV(@Ln zKPtn)k@wp$f_Hf78)I@_4)2^LN^mg|^04Rrs)pAx@7M*w!Iu!a)|~nO6+JcwV$QTf z?oDcFnkNmu+WosMcLQ)=n3N&ky?5cHHaeEQ`#Z?Jh=I8JcWgeWxc8b{P(ATlCd+4K zxLPv4;T``=u91kGeGgF9gL4u&lzZDL8SX1Oe0BMSqPk?>0N%X#VIQFJ5ls8UjS&L% zxzIR4s63$oly_n)6#LB5*E;q64>wP51p2MuhUgJ&UxP2c$$T%I7ykrMUdmv}#sjhP zm>bzQm#R^DoqFil%4T7>Z0@CtZxc0W5lYIr7k|jRdzhjG4_EnGcc}60POQU6F;fGU z4>u#KA{CQ;A&9A5MV_Ve8=_(d9eg@>ekO}ow_G8_9 zFDbY`*#VB0Hc>hMlT)9mF>DUid_#Ag&yjN$77Js9Qn7D<0O>FiayBq;k;CG7k)t<83w-a zsflUPrC&1{_vlFS`Xe&3h7&+XQeR?SNyf9BKcp_f*JOe6Z9B{njX!fhpF@C5?sC|w zKBnS6Eppxf$W`>G1v`5Jo%5mEIJOW8n2kXJ_}+4qd&HK#K&r8w+^!E=+cz>LYevrx z^xuh$#LZ~e_Nqy^4@;QOwB3m>B>3QUYG@2VkNpmBuJ3k5(HJ)w0nibrd^HRZ$#>S8 zkT+|Cd`)5*V3*|SuVW`x!BC1YM>v|F@) z4@w4{i_u@(w`G^78ourO{awi5bF|<{x=dgJ&y|EkjBF?L1j7=V`2K4Y5WKq%smuYK z)XLyYome?&)V=bnJ|2(_e(s>zB!G28pfAG!9bZ$4YCiT~i3*WrF3uB7m?K;~z&6V> zgM9kc2z9;a^n@SA7UYYk5XHve<$nV1J%yEN!Hy$AhKcVS0itRvpVC1qyHJJ}u^zL7 zlD2$;DCqo7^ej|?qwgxhg^IBHYK1;sePl+a8GesgGVuagyPA;qb~s-)7sr&%r4leO zC*=3DD4pK^+4<+VgYJnS*IWlsOu%l$zaLOS=465$tx^B_Kcu1*9;gd13wq&``@aQpMQ4#GQbQE=-`WnqY7z*8L-j{ao9{(-k@EWR5gNg^R-q;S&!xz=Z%- zzXA2-E5mGV{V#aqtcVc=gN~G#;+=F5Is$8yU{%QDL-;SF^@Bglcm(x*)E_k>_ql!*TdtV_vR%YEXv&m2CXe^$6@wUMuZ^Ev|)6D!qc;nyn+Qf-7I zO|wy9XR{5O2769GAJf(X%3Hi5ejy0k$Fv6VHIvA$xi4Mwy2L<}GqEX1Mw;sqrwiN1 zQr}e|P(?;>Z2-P(5cCPHP_0v4C3K+vQZCy#nPmC;qT07frMYE^g!x^BJVkJVL#>r+l zuk;~A*>ZnH0T)zYj6J7O;a}`cHbc76vF}r|02i0~)fXRGb75v5I`;JZeq_gc?_mTJ zA*%j=_?*{-D+IEDlHrGM8w`Xwr-0~b$D%@D=1rxrt~avouFA+EuT1N!5;we)eT_1z z52xtHbHPWBcK-%oPAb_AoD1KI5wI+mju=25_d)I}S;RJ#UGsp)rHLk}Pw0daJ@w?H zc6^V@aeF7Tk-&E||7@eR;`ZNGnsL<ry_;v`#=yD zV#N%}fbYL+GmwRY4YCW+3G4-(%R_p8%Ysbg-d+oZUR15B1 z^^d^mf0$4ZR~a;n)H9BJOrCZ?)WPQWXa8fgrjhWN%8G*?KbpRWlgQ_~EmVQlQDL~X zCWN|>Y{+-nr6S*77%EB7=?ku@b^_KJ>HsH4nUY0HEm-%HhU$QTaV^z%Z(CdP8=}I_ zkl+xZ%m`8mV~s!B0)2CdA6X@y#Iy$VUz2Ga4=+9jHzG$ga5jnk~lo`)|1iBs*9(_$Q-vgDMAAfn-gOJ=(oe6^O9D7Ua9dv6{gBB#yt_ zCf}!fv*9C@SN3@)B5MNqw`vgIqHT5%UxV!1O30f=D6c{K)jr!3e1ObxLtK8t8$>*> zdq@0dL!5s@8_|>p9@&F)p1%Rys*)lV^;lzO@@w#MF=KKyxsM>(!k31EC z)C&+~OujXGr~+`AB#aED%doT#P7k|9H-#j>P&}EDY;d(+c!h){ct|nL7TsqPFL6aT z8~t~%uo3}VL|GVeG9!J@8t@6*nHqnXsT)tcio5uiE_wT=#l;-vKz~DIS-PDL`dZwb zi@BQQr#dB3SI6Up!?UzEkaOnpTx6tsj3OGPk{SI=z~9T}*uQA~$IG29PQ7(-{;ZK|^c}akcuP zAUb_*AC*dCFVfM0_#hUEeq3H1zWHeUnIzK3(ZmB*+|26q~&i6L^vNMA-gmA_j2^bA082qy07Ly>|0#b6u($B z9(zs(BlDpDZy$&%j+t=;_d@^Akz@39IS;h*H-(GMP|8zbwZq{ED0Q3Qq5&95r*r`)B?aqe@)xO>ai)em1Cuafs2<2MixU zwzgWYV0k39IwMA^N$&2cQ^*Ytuzl@nXhg$P_GhMdz}>_vtJDLXY_Cz0?Yx<4lA;|e z2RYlR!cH@__oSR|-BO7BI=Er*yu5{xxjKJDqe#anCKU?2{j4&1=D-@fT^5V`NE?68 z_H|F);Ajk?DZ05SBPB*M&#$iHFOhtN=?pW`9gaw>O|c&yrKF2qn_rBr6Ut=^N2qS% zp)401NerJq1&(A?H-Of1=h;;I>Z<_F9~cu?+`lBQt-;t7M7s(WtsN~3y(ritOnh{BjfYED%o_jQL2Wk5V+6Bjzc7S6qlkhaT~#`DuV z;;(RcxHtsVQf37y6hTP_>0QM{Ij_hYu%9B(@yW>FqO4W!($SHHS1%RetL}*P{Ef(p z{wON;Cb+SnY+0QQr+c*znpaDd;rrTTV_|0-y66N=tE*BOZ~R|OUPP27tt4Oga5zDz zhl}A(Cz-~JhFSVHL^~Zp_;+W*OtS@k{h63s_epuCKx~qm(J)@mFWk^Z+T1m<<{}{EJJzm3!TpWl% zPB??9l!aFuv-07@JDlaY8wAnYF!pTk7UEEGrO*8MAR~5ZV!@C&*UXgVJDr)aZ~P5b ze@NJLN;!9XSO*j+Piplmw0GLABd`8W*6luWGd5)std~NEmgQ-+pD4FFEyhTqrE6=3MS8W(^9MfRj$dad8mCxnjb;QFJDD^Xw z(k`g&3M&ZcT;Cb$lbxJ4yhTQy=P>WtUOe~mAeb1-Igy-R!;p6uM~pGzj{QKQcy}#= zvX@>afpz;Vo`vrB$&fGVCQn{OMEIZULvS_MT06Wq=y(Ww&{VBDZecma5ZS{OA!*^3 zm+Wv$)td3Mr6!cgh65Lu21*R$g(MD9{4=6jz;(!ui=np6#T3+e1I+8`1_yHYyGj-O zi?rhjal8?|dNq0V_Df|qdW?fvU(sX=rEno7d;vlo5w2PG&w9#7<&IZQT&5LRDd4eY zRW4Hwo@zsgyLis~snsm-`oEU|UVU47KR*{fBX;9BO)T-;mWJ4js5o8&>rmXFc8;_$ zbw%-b?8V-E0>7)*Tr~eRhnhKCo3f&&DeTxM)eS}7FytB+h02?*x-7dRnLbx`fR$Mz z&?yJy*Uy+)@VJ>}F( z?kJ;<O)Q^SQj%Rp7Q&)rb30BuxiTiR zgpua&<-xAYk=puJ4eP?KD%Id+L4*PSTgILY{gGz&{SFvzh(6dbIn7(_JI=`v=nborAEFz6Fkj@Gluiz6MDe1%p>M(Y zei~M}Jb40jX5(d$hw6D^TGBaQ|_Q&=)kY<`UJX^lL`( z#4^tw|ojplTf{~A=aiUr) z9|=zmSB>mR7JreGEyynXE*Lk@5%-#3j2f>@xEL$`%&!vkX)q||BmYdbo^ybs$t`b* z$KlPy6@o8MJ0aQgQdHbA*3^BQ7jsvN?G1KJDp-9#<@F`4vFvuJ>LABTvZG-!&icc} zl18R@Zsc(l<%MhUG6)2Q9e(G$4e#6d&z+$=XF3kv{aR@vIpnUct93y(opw67x9DB(4B|jKxoU4D9gbvX6 z=wOaaAC8I(%09CI6-A#y(#~e=KXF?f!UZGgEpjRUZ~q)@UOvpTieGNfWb@j}IRe%b z+}x74d9)A_4Q26V3NG%;G~d(ED#E)!o&2s_ddiduZ%)lR0aUhH4L-83@}is03ET}h zj6o_-aLj;^J17kQn5!KCc044-#8HxwBagWX%0@RN^hJd`Z7MAM+CrTZM0=k;1164S z2+CC5hGyFxy|Z%?=PvtXS0R6+vM)9aK8g|*s4mO+F4R4YF#rowY0>5eN}TYt1#*Us zA^(RJkvAO25YAV4QmJEa*>+1D;z9~ZdhTZiwXFXMha+_e&K9!VSn-X#LW9WSv2R<( zUB~ys%X*8Wy5#N+eB=gpQEIP0m}u_&b@Ww(sD++F6)!&Fhy*_Zsw)QTg3YDx8I1T* zzH2PQGeFYHH>+MsWD@2U9|TaNm%bVj=ebM{Wq5wFw`0HzEKu6Dcj7-b%vgdwod?Vb z-cYW#=R@-^$UKeZi_5)}*;39Xl4E2`Muv~%;SrajqOYCf!|P_uM19I} zbDAF3O_7cz23IbP=jCTRs4-;&vFF8Is<}V45TekY&I#uV^R=)}skc*v0WO}`HcA{E zSul;AM=l|-c-PztqoxB!sneB2=}mJtVV8N1NT6*~E$XF`c*{!&df^;qjCjRV|99PD z2l*ql2+EtH{FN&N%BRgKr(|2tglZ!xwRq-avZ0e`aoCh7`ch2jU91Tq*%u`C(w<;L;mJaurSB;?$s!Xu-tdk%nRE!~U@Z+KODgWe16z*L?k^4m zBiZIxt);~Jzqb@pqWhYdDN&Lhe*7gVJ1VSZ3)?N4r!5sM6}n1<1n>EFju(G|F%!ee zVThVigPmtym{S+d^jE(__WvE&@wvpD@ns)65Aj=aE3~&9#j6~9-sqjmDRhoUkc4NU zR4^MrshIpNxKQ+f!HAIkK@li6E{@&uU3=B|$L$$c<<>c*ihqu`k1L zCaf*)>#3VSC3=YD2<3T44l81R&QSjPUE9RyFZ$!kNKq^&|F~L36fcxPl2B6l&eE3i z1YXxo+-w$A`q?u_)Z_VxJzsaPFj7hjV~Y^aT*Vt3EjA1hH)th%zKLk^Ug`|#BE0#P zQnBzMgzff0rjVz1=JI}?mW&S~xY(82KZtnniDYeRwTSMF6K1g;W}OIG3xBdJr&LI6 z$-~kVN8!S}u5R|Ml61U<&7ZqRC(hTE_KGgIc`&jP zrcuhy?+ZPcsS)@mh~jD?zqg?0tHLzC6Q`-IP&idpO60tZAcik9kc$BK2tA??5z_eY z8Kb1RuQ4gn&U{JEt5n0sJWY$Vd&{jzQe;?Y4{ntat0wvs@y$pgp)?cI*b|~Sso(#;ssm``!4FnOHX30)uJGJ=0lFXEpl&4dBvDD$#qMfDxxSNayeCznraxKCfp zF9ds7h{ZfGQ^wOiN-m(4AHKjL*Ma`1u!u)c1a|I9T|_`K46hh5R`1Si8j}HUraFXO zmh~MIR$6(&@HWD;_k~)%?s}4?ZL@6(xpON|*Ks+nhS08#r72m9jTh(W0r-z>C_j)!wmrG*hr0GL_H?qS5rdsC5lY!m-ea>y z!@>%b@&ku@Pc=sfl`O4^q50Z~c&SKX7s1NTi*iWCKc2}GNkcp*r^ifm=n4C zI6E4p{ESgP5_myIekbw^u5LmnxUOr-nx*zac{_Hdhc#(f{Pix_dNkeGs4v-&f>yq$ z8$b6(Y<>m*B_)i4lgIL}>jQ&3Crs-*Y!^aom(nbibIbenf%)CN zJ7H+4A+&%Aakt(-EyqYkMNV4(fcoy`f&vqX;5j0clj2W%CJC-IqY^qts^dOQSF6Uf zIzXm|yogCM)Hgi*s@=y26Df1yMAXRFRWGHOVZsFP$$}^)>M-q9UGEFbmnP!4ns$p%KqCWx3s^l1qB<2_gSA zsr!O?&|*eu##Io9AL@JHk54n!MbZgT?N58umoKug=dU)wrKzdYUz--Wuc7|?HT!Aj zT!Rc(G*qUC$9LwA>R1Gd`_YD=Ek17gFjJ2F{diSF`pOa_g3mpW8590iL44DZs^53- zt)4ZeHBHITF1SnyskJl$?%W47{D(rdLE~jFP+%tMbGQgWFNnP{5>_E}-J`+tyfB3Y z<%w6GFHp`Vx**zpW)r@vp+89nPg|n}*Y^Y8@cE7ogJB+8ohyy!Gm&VAd4r4XLuX-* zY$E;3W@`*nzVCD^2KAU)-I(GZy`>S}4sjl3UTREK^PlrWG}NWw{b61A2RE|PHIt)0 z`5m-dtF2eBK<_ChTaqu&M}F2}#Ur!sY`S4x)vJV;YUG;~^oL?C-o?#X=42)vP;GdQ zXg2+$X+i(A2P#s*(N85f;~<=xo>o-h?4TK(aSeu%e6r_X4LCl@>x1o}C z$drkygRTBECs$dJrJdF$1@ApoVbdh!|2ee{_+iUwgmR1tywN$Zt(+eE)$iGr?LYnT zUpp5G?Y8{;kJJYLt`P{+V@x!ZH`<-LhYUWM3Jq^{zPQqSfx+*SbN;KX*5B1$nKM6V z8|dMHc^tF@VVjvR(>Hr{?Wn=r!$+3We<|ytmycJ)jK8!tcTC!3I{3N9_H0wHq7Hg< zP@7&v5>L%;$->MwNi(@TY?nVC-eZLbZG#2AHle^?_#YF9c~z!jl{YdSo)z7)IR}#T z3}ccK)dqh)noaRX?f<>=ja`2lvJ;OVg?{qO3~9_#S_L*(#`ahul!Ds8{+2<7^94P* zDqK}YTG6diZ)l@we;Oi={(A=K&C;lq^$!Ue=^VHsT5`&w(R7W#SI&?+!!%vW&Q@F-w|*g*8HB+LC;b zCVcJcG*b5;l_8D%omlj@Uy%toTn~V{VxASM*Wy~g!PH;4-B+tlF;3Kc9#2E=7;>b@ z?PJ^7(da_8cyL5GyQdI2=|xH^ks^8be&f+Qud3aY@#ERA-k2tcJHjsvpU~IWpNyEw z989X|pZ9-V56^{8pEVu)Qe*$_(yuPU+S|#YPh9jm-k3JDpT_TtIUlPUJgA88vVS#e zS9MqmmNadqk8%SAlRBn)su@R1++Lozqv1PzLUS-IQuiou^uv;BPEyf*8}<%{73g)L zA{*{CS?@9@|+YeVB)6@3D z+PjcBdx}u*<#9Wl6Ku|18WQ~d=yaji-lH#11ZgUR<)4uQp_!L#FOdBI3UHx!>t~!s z=19#fsMLlf8W5a$@TJ-E z8bUjAkhIoU*{6Slco+H`sYv)7-N<=PXFaJUv?qeuIfeHsYv?BvjqK) zM74$re|*=M*_#QWJ$}7;_4Zl%`o{M49tNRaIaymz+5qXH&Gn<)ziaBJ6W!XKE*+;9 zzwn<2D*qsUsl92ZUHxe1 z*06GLgjH}UR@Hp_6v9DMW!3X4lbuDEQuP9|3#7KMVd!*d>^Dm{ z2fdPp_ZI_J6m! z*z#?ov(Q%6$xPSdUq#aJ^%xer^q{WINFCTF6np6UPmZ)jPU3~mi<4@U!WD$SF_F`n z%v5NXUBDXqR;=1^!XH1*xjU$u(5?Un_#H2?pdD}(1v!Z*;7F-U z{S`aV_WPJzFkI?kLcSn~j5MVQH!7-+{Fppes_DM-T2V4xSrSJ#Y!-Ub2UmI;dj^d42gxJu&(hoH}E> zRP>q-KYTykqB_Y{g^_dE0wuU(`z&Hm0Vw%Z*Zot@Uy!l5v6Tt94__!G)U4R*+aFga zcdNl4*jjXb)wh~}@%h6AKe)r*ex_)~2P~70=eeldPEjRjHcKl{UEV;ebql(p4OopE zVVkNj#Gzf7JP7lvOOpU)@I*|Z(w!aUo+St%dfQ|E@le=U=rG1*X!x4)v2q`}B^}zh z*4cToD0yZ*=-a((#v=pU_qHQ+;_ENI__Yw-fR2D3r6XM$AVpu*-poM@et4)mC#zuc zGRjeIbhxS;1E7o}*_W^~VE=%i@zLoE8+>1Fb_8})?~~cuu%gSm<-yQl7vT0t>BDtM z=7Lw$h~G2~o_mqvU#&>n+=QezPqR+|$k;S)Oo;q`KIs?JY7S8Va<7cP8D2@PDx{zO z*{#A&wlz>aQFjXFaPE4CL6PjJ-Vc>tWlU4f;yz)+feh~&xVH8^{U*Rm_tt!(KSAMB z7l)3aobqM6fodxs}_hd;go!ws+Q2%BQg z6r-z1Z%t-OpSW$s6o@{X(wMo|=#ewQ^K*5 zM1JbmO7S&uJacBQ+J?s5mO9-4KjHnhSH;MWu82R2Dr75go$sLL^~*;vSwPqMZdBdO zrP+&oge~X0y`SI(`(#JNKK>u}-ZQGH?t2&ANg#BjNC!a>L691Xh(J_|FQ9-TAiV~p zO79Rm9Z`yaXaqr2dM}|VMS7JUKtVui5C}*;E5848$GG>y{c`U;=Zt%PUow(F_FikQ zx#pbDGv{3UVH_zEe=uG`z}K2r)K21SeQ2;8O;mgwEHHM{>9Sh$l8M5x9v#1Zz6qUp zE+!tndPa0O-O;jVYz(CG`$hWdgF}VD$~O%YYZI2qrig171V&=VK~vf(W6!doy`$Bi z2Ly$?l~!d92Og#Eve+kT{K*wSDHnLpm^y9m`qc*cSKEdMx|Sl0;!g30n4g1u?i@ST zW>ij#tdcfK`$FkDDctBL{t@3UXi+@VQDG3)xXYbh4ikT|3-VMmb#+}WEY?5u1`3CP zckY4`H8+4eTQ3#G9-mqs5LYXyHZPlaze+l{-`Q4-!1H;nG)SK5A_caPWPL03rdAc# zB-Mgo`0HYd+|XgTl4LP+-R{=t<>vb?@Qz_IH_Nf?F#B4I zq6TnTk#$K_GNBoQA9?UmKzo9_xw9RovG=XaNbUP>OYU!K(qE8L6jOk#eU-zl;EBB6 zoWE$WLaLF0Kaafn-8TzHEikQXGokS=pe)mMXW&LPg!ia(pR$mgTz%oc-?HzvSHdPE zpmdQ`!~}`PGN<2O{PA#fecbY2&8qi97~Po)9g+}Obm!me-g^@Ur@s<>+;-x}2%Y|F zZF~)-9M-Yo76-&l>iGHl#dei^#a$EnT+XKb0=mXvDN?XPxjsCAJLpU3u^)=UdNq!q zjXk;=_Ym3WwVYnL&+!BSQDL|g-FC8vAzG3d6;8}=YpX=1=+~I0rs?TJG1!B!@6YX_ zXg^(;w&Ev>0IPk9N9}^Q>%A*gYIlP_n3;~gQ zpcxCht)baqYSl1d#UPbjGH~S|Ey$xGhQx@rtaq!dV>`**xc^U$H>zP9&QzpVfUPgg zf=ywP!0r=Yg!Q={4MTa8_wc?9ScZ6y%J<1@$&jT{kn=`#CnK@qqq!r!Xl#s;$#l_# zB@O#OckU(_szV-~$rp1L${ea5a;6U(-g?l+;^b_H7J z8I+hfPi7VSyf5e=zPj1TYJxpiV-^Ahb4ciaD&sLWEYeDQ?jgGl zHi?R#gB$A1_n2C<;$(z)cPD)Z-L0G%a8IwsA^MK6AO|Y5N(j{Mv5vd(%|EE{srwA=x3aenwh&!n#4vw6s5I#o1`%Mobt)@>ODy#D`Vk-t8Y?mOf>-5Ct_*C6Y|p7J&*T<218>RI@fREnjUAGY!ih_155hxDWvL@@kcZ9WsW@QW6Iw(9==_iG-uVRg$eMiUHleZ*vP`$>KyN z9{P_(Q}-Q5mUe-##u&a8>YzL25${^LknHxFMqhT2Zl?S?wCCRgLLK~+CnUUnQ7r)0 z;nx`<*eLkvSOS@NUOIGMxpAU^MW3`+A_a<|6$Tz}v^>0l#Vv>L-FuZ7MxhkqDG%>_i%9%p5COLF-weeC#79vnX64E2S@vj%+UD@+ z8!8#`h8e-|t0>svm$om; zFoh6K29tjpHMt-j%y~-D{YjBVfS&Zzih-Z+FyxwHSdXwpx0}xz)ATj?Qlg-+B2zJ# zcFMKrd=SSNz=+4~&eas6hmGgHLdaMtnD#<2tfSQ!j_1cGU&9VqWm8T814NH*h(`9|m5b!GzgSIdFI^beq%xvT52n`Oum1VtAj{=a*mvcTeKhGrv@AOx`>Vnk|2gs$)wY z-z=oZ%_M%Q(L479_BNMOU*RKS=FpZ>)ao5F;MYIsX$}Km&}h-ej9dWvkbsdF*NO|I zSghzdjUF>sY}6@-vc;f|g)|AMS!?0-`P!4`k%rs=>(b@Je(K~wQ$_5J$08o+RJ!Oo z*wjTn?7#zG9pT|mW9ETfVu7-s!$b$A8?zw&)Gs^?TKP0Y zYG9qGix9Yo1{CB_%14LLe(DRkzO+Lo`o6W~LN^65;MA%3U`D?6%HG@LbiY1nROI+! zAvg)^p3)L1B)x(7c(}dR>F-zo#eROQl+(sxRi0tICOMemmQYCtd5xYW_A63F}2 zIS-+ESHsNzyQ7-Kg)TAtX5{b)g>KRgZ(NdNXSNVdbjmvV-N0k@8NFMwcSH7f&V zm;?os+QRpqltY#!c1!J|YMb_+Fm%=hC8ZS*jS4$LiJ&3k z{+9{1r;dOx{6}PV+`Dsofzk0*p4kGFn8Vb#oMZtd-nqttea_Cy1yPs>l<2B({rmwD zO_AtGahOq{`Nq7RJM8|*@z&t?iajILP3=C_W#*Tn zF%0%HFTkeac8Eryr(5cjeoz2CWK@D^?4(0?hs+IATIa_l*fn-+fRPtmM~BDvyA{j; z$?I9fyGer0oZw{Arl54>V=k#&`P!2=s|{Opfr8j<5R{2`pdgCE7QJW8-asDd+KnBt z6K)b%!Eg+6YXc&hT+=fYfMz@)VtCkwc&B9|?9{{IUz0K^4W~xL^%zA?*^P4c^)f(G zGTM5HZJJR06)2ny(E+L%O`zR>hZk4~1*|}#zB6?WaHF1>(8m;_{@=+r`vh*r(8|XL zT|XLSpiXUiiZS;rITdcQ^VMOn<2zq5vl8S=glLZ===pLEw%myD)qnF6M#4~w>s@l^DJlW&hnyG zrqfFVEdrdoTkzNLs<}M~Yd*nDeokLi67g{WW zUy@<5!5bwty~buwLA~ld^0Rhztecc!Y>CV})33?){uM9jJnV`WY-amois|ixZ}f}D zqT`f}t*P{XZ7hhe&r>XV!)DkvZhq=uhv0bj`jE=Hk`=XpPqNm+%B}{puU`0WD&Tr4 zu_O7G&bMW<;Jbs6C}_@6f{4Cnuv#=|B^Brr2SWP}e&ixxl)r)6^T#K$EFf1yt|rK1 zUl*&`+kwX3yBpGK1N75|f+&Zd40=7%HTs6&YP=#DMW1{2Nw4bpiTN2MR-{uWzkYj9 zfFzORA~?y9@%-GOEA$L+eN!;6MRS0Fjf9 z1{ibq9zLB0*={*rD%Cg|;PD$fc&ng$?|3SF4=cpN93Uh1yV#Ud(Cy3cM|*cqHcq^Q z&#ao*GFd11i@@n|kyHL_V?#rPud)<-sVP{n>N74XiN}R~>u39ZG6^wQ@zW@ch2slq4j$_;r@M z37f=?4B(d}k43eZbLI^%tbsCv_H2PQIxU_yC(`|=v*I!m=&}t81*>nt4kbxWsz$ePO z;W+iH&*`oEzRFm$x&8X!uqLmCCXyOPdk-GJDQZK~GbX$s>)`bY*zMlB`KqlUxDy3( zJgnPdk1j}nT)A7p(Wny(eGggogB4kd#KRnTk%{4)3ewhwlr-ta z5WcVu66QMh2}#V#=SFkIs-TOuIQ93ci@!1eCxlI+n}j@kC}CJxMg-i={2vHchB)B? z!>`xD>gn>KsZnk8AD~qpZ{=}nQ74oO=AZyI46Kfw2aA0;Qcj8JQ1e=b7g-8NSLRX# zl-5Bu(L&grYz|zR+xZyiTLhwUUw~eePz6gAbE*cjRH4GTrto4*guz-ZYonE(!af-d z7KStC+Ggop%GjGK{2*(szq?#`h@XvC#_jzYQKOQh;0W751}I~qIQ56``+tQI+077p zs`2oEiFr;1WMUjwfs+4)V`)IKZiTKmz^qMND&hJyE_Ww*pYo1G-(Zvje3S-+A9b3L z#o2FS)TwaV)>th;boPl9Kd9*P!Y+F(wCS2gEp7cYFU`+HQpJDtErn<{G{Xh&TL4y- z0`1%RE_)dJ_1;%*Y)Lq8inv}f>j*Cj(l#t>fzMaj)lFx4-9FE z3b|W(VRK4DH338~Bnp2e=~deA{%Ol#S&0~t}wd?4Hg_FT=jdycy3ii!~Z?ofFn z9Uz%Pd9hv$5=HPL=>qwW2z+( zu~+F#`ug}Pk5LY2)c&DvtKP`m#@vKGnr2(}gX1ZOu}O^hS|BJ#3z8UY@9w4VoWCN6 zhuP(QKo7o0!R$Ep=%KM&0w55hXo%~{^)WO=;1tXhut%QVr`srJLfJ}LVS5#B63Jp;tOLb_@lbd3`9SR7XUNxYflV=MqLMFzj$)gN?KrJ&8N6&aN4P*!hAlz4Ge{)vi4WGK9+WkG^noL3cJcbzpmH5i~^GEYywR z!T8yT+gkaaYo4#}T_9#jg;E(-H|E)3r7!Lj@m4HF+*X#ymfY!w;H`#mqnF{*FyvZ( zt|0Qv*&db3Q+3YoZ9idlel*)190trF(eZmK9zeT>92jsjWwU$lW4@qBH=x@gZXxWf z=*CmaFM~`Nt;${QF8r7on)@>92Jlzhi2qsiFWy`6^yH#~P4QrfLqJ4sf+yTB%}zcBg{x z)Aa2m2iJ38A3Tif)OhN7&pE|*{|#sC9$H%bIN8VBgVsn!27Ibw`k?L(0e~=92iPP@D1d%qU*7xH-^o6@+)7N^<2DD z*?ze(I{kVA%t6oH?>01kw&$YKtB|fbAe_4AptxYTeOwmKN&HcQF#4bxvg<~YMCf;u z`0JDpRn+X_8Jvj4h2QR|9owz-VV6q%x%gEN*!x6JneENKl!T}``-?;4)bcvlM;y|5 z0pIJ`B%XdKuZXj+_BnR%?1X|w%YI{Lz2-bsoGjYDSyQ16E)^5P*#YZAs3jPiAimnn zb{xiczKLHBTQ2bwroqPS)pzFrifO-RK?Zh;GE3aA$Dxf^NF4Gwzh8(!Cn1Ae#$Z#X ze6>GIyd@{*fJyw3PYc1YW2!L3sDw)S7(GOEdTJhbsi2K-_-gzzyF8#!#gBt)c>w=2 zF3U%Mt=!L|ZGE^nR08XK%4!ennZo?-T62I;;w}^f7wBkFHJZ(Azv-q(?A@Rdph6e! ziL>Gkyl2O{-65>j#N34di}*_g`}v)Vv~Z9UI848;0UdkjqHO&sa1ORR=0{$ac!MwT z2AC^+JOdRC?S1cN6s-vWSX}@Dv$_RR`LOK|>M9C6@L(dPY5!8ofvxh*@NRgq#KdZhhtyW43iMs}b zmAY{1ch{~}1KNQQhZy$#{QcgF4I$X#Nx9EPGsYj%AAamU;l5dC4h5WfMs3~nDLpg? zCTb<7Qbahzh5nM0M3uLk(6){9j#-wn||7wlme1m1~lP)i4J&?UB_AJ=+m! z-1O&ZbwW*Dhz+ET8eo_>B#b(6^%Z_yZwWheKfVo5R4v)^PM5GlZXG%w?#|1q(C6-L z&ordh86)$|KT(RJ`Y)w}3dWb%ll^SAREvX?~wb_Jd}+H{gbQebv7He}+5Z=ms%ND?Y4J zA_=-|$)HwMePFu^$Eis*)UPOd0RA(Pb8+puO6^$95cauMKL3RK@tB-IA3`Ve3%6AlUQxFh-JTt_cHQaeI8NOmY{@6ilI?#cw&UWE=i5P%`+YL+Hd)U za(xwo`-05DA%9ri253d4eoXgcb=e)pFvl~vnx0nDaH))tBQ;^xd1SOAOA_533sZ!4!)2mH-{&tFE&KKUm*oorf?`u#5ja| zmJ&+I8NyzPVP@Qc%0kp?*w3TijU=yQlGoUpSGQl-5*$CPJLxg?Fdez2N9YD=N;AMu zT6u%y<5XQb2mzJ>W9Knyaca_i$6ab$|ED*q~Bxq6u79D_dzD>Na(SE;qZzgSQN8z1cgxC#OQmBV)i#p6`m5V-`>%MMp$AIDp@Ecj zDLoG$Zq?9IVCAlldGxCDQ0wpYUcU~+1bmV(>!JTfA|Y&@^!o=Uy%;Q7MQ}yRx!S{q zek6P=eC9P6E=bA3S#ev&zd-sD$W7`A%t3aPbLO*_0Tw;zx8Hm?e|R3IEjs|-SQJgb zviUENrLl~GYfpLRtdjOCQj8V$CyGH%#@|EA*mYq9Ia5L)iydID>8Cy5XM~arY{|kG zat66?02{_(Nv}t+II1I9nvp8)!ta(&*VLLfxTnNqYyMx4T(zN+LB-6CyvVN@Ks}&^ZT9Yhtm^o9c#Y}ujfAlSn2O7kGo-S z3x$3%e!a!3PbgBGmJD4!(Rx@&h5mB3we4U@5mZ|;GPR291r>NuI*HQm4ZLK=Glkh~ zq_0(J{qc z|Do1btzexAJw?g62;ZBO|DvLDki2j9mCBXH$*|5g z9PCMv4(hBBpY?m9x#Gi-oCk_7@0Fpzz+{+-{6r0iwYV5^D;e52PPIx>g6#GKN^D%p zKiJ!7H~vD^4JDkm?S+}?Fy&t&;X@lOe0>p--(OlC{^3_8!k9Wf>4g-mBBU4TcH@_6 ziS;pF;E4SSW7!fo?#GEbm~X``@9}n+M1RR1e19;|`**-s40@uC-)zxKe_bDX<;@Eszr+{5CUnnI9Mxe) zO=nNF{d$TgN*`4fK1(iox@O@)f6wY<(7pjBv<1)6yHUl#g8-U(?AaQ}hj1WQQ&kw>-66a#=(P zqWyh!!tF7IiG0W^O)o761#XXFEudyjB+O^BFyzJV5{2BrwpwWQLAM&`;RZ_J)o03$ zan{!`lk?;N&7A-57K@Le8{TOXV$O2X624N z?o8p4C<*w25SWR^5NYGHe<%g@eb;qRhg^Te5bguGwsw%l$~yDpczQN1kemoLVU3#Z)ZRZdn#JTtp^eLuLZS4+-I+--*-ksP} zY^eFOJ*~E)7|sQ{)46TiICi4LmbA2vqOJPwqOXCD_YwXbdg;J0qp`JzOnZ}Rrsu#!Jhfz?A?2R0@P49i{E(*{R4L&$3eC%-&mtgRd zpt`lZJVn%cia$2)az;CW|6W)7!QLk~!x^u#$%+o(ncN_+mG}OqhHz5vv3<(%?{q-7 z_2C3X4uQH>tpV{KNQDpXrLZ#nu)XdZjLA}0BPu=|PcDWWB)nwNj@SPK@-%bLZ z5PanMf%A6>hb3zGt$ei$^TrKht5+MwX5vIZ*%`OSZ*lLZ@sk-uz54o9`S1&1r z?<|8#%Pg_Ci31 z)Zxlv_|fL!F`>|Jzpt?J5C2a3asMnhd=+$S+&$D&%ij@6sWCquOo>(XoLaGqR}PtT z2mCmxuz|D*BE+`Jy7~Re`uTnGE)Lsy6aE5pfqxF$NvMo2G=X(f>V;0XEvZ5X@U`1+ z`+GiBugc!=UE)IccIRyPpLEpJ;UM`cFT#Fn4Z0tG&-OU{?%?s~vW4Z|Fu!i#etgey zD*v0`I%>*G$A>Mz2H9+?1-bs|prFUr#J{g!w?n_U?_;C7U&oI?jyX2$*!}uUA;*J1 zs&XW&!ew7jjJ4gSf{^1I?RltJ586JMd_nU}eHkBmvk5N;&f z)p1UW5w*0a>DzsepkK7;($538Iya}cd8I{Fk1cvrK&4nWU2phKTf?L`CH({N8hTW# zhLn>T?NR9scBq9OzFw_a2rP7^>gy^Zc5JtZs8P`eNvZz%16rZb-e1BS7P4B5^RSoQ zH`tcJ2O;!}hiomMZMqjDb`Gz)H&cx(tyU@ikDd;+Di^b$oCO9IlQNUiZ z=1`*$93m;FM;^-V*c%A>_UW6$cJfeAEoSlchl@6f*vku9PYL+#MKiQ_%VF;ydvg1X zK|L|`@^pQnS}p-=7L7Gvg!rP?4$`<_q`16-iUl(2gx~HXkC>G`UX0YIL};^vTzUY z7YAo>IeuMOxGoi%4{jt1ZuBd=@aHCLUtaOt@9iFrdM}_W_gXTfLdpRZ*N5NuiO|!# zv})HVKx`gVJ^FoL1dVC~vfWzC0@3v`NDkHee9^+r$5FW-%uRSG!MP7Ar@caETJ@mo zr;HYpQ}C94&M>(xhxf^5mcPeoVQDIfn?Lqa8rrAN!5A)uzsDc_^hn%<_Z{^6R{q>G z#-CdQM^Q)DRvoc<)T~uNX_v$5N4^hpG*s}RA68_TSsm;x1oJ`Nr%Ttv4@Ry_S-_mW z7VLsIj?>4~PV9kT#lUbWai`Tb$~e|lezW&o`aQtVYjxDKfJGks_E!!6^<)8ydEWNs z=*Qp8)*2|n0w>ae1r`2#9ko!RJgzBhZ)Q21urfKROp8X8y~-b54R>MaY`B!ZSsXjKHrnXai=BmUUl=|-Z<|=a-VYdtCq?=I4rB- z+%uoWM?P8Z8xg2*ff`Cu1wpG5Z`MK~IXrkJ|5Fl>URUNT4&#RT!M}f~BL87e{*4$2 zJE}DZZMO^m_%wZ^LSHqzxBdO&NXW-EMBhs0>TH^E-Qj$g789h5o$P4u zXEpIWmPhksyEXI^Y&V}ToqrREbV{wWddIs39C%p;_)tc1zlBu5_U@pXA3d72|LKW_ z>MgBlUn^P2cQ~}Y3cL^9CGw!X`+)}i;}LS)eHe$W*#nCWJ~R+J*!o-zs{{%{f2OB2 zY*vdyzOzx_%^$LARtZ4eS{2K{8C0!4wRG8sh9WECC&CuDfxot?g|7eeMs+u+quYaQ z;eCs@hGc)vSAws&`|`>TPAw;zO#xrWgXRm_C8nRN#9M{M15@(O!*_(vk`O+q1I{~X z?bg_t-uewDG61wozHWPQ;)7xXI8p%|3;`bLaLXfXXE@6P)_nuGQ;|c0KdRvnss9DM zVg{x|D|e&V`VQW$&c^YF_)dFUElof}x%IL8ulIk0Z?abreJ8iUTHJCEtDXBzssBPp z(kf^2fRq88R`*TN(foe$_fc%yR1I9;yC7h;82n>k!xpf1mmFNTW@4p0LdJN23JksU zv6jI;mbm-9~jBKg$abw$ys}n$0P4 zxxD+sUvIaAZ_0KuDdeoGe|!^BP2XzWA%_O?CA(3+13c3Wd(EJ9M9u+_f4bE_3DJ6C zYn)i_dodhu4njI4*2zWI-KIv1z>ZfPYeI(-hfV zHy_ZHKHab}3MTMqElTm!{>p#Va1 zc5`;Xj-T`cM?YJDHwT{;>)!Z-A-iH+w{{1ubnE`CeilE0?PIqLd>OPk0W9@rApS=c z#EvgR>C>uvT^xHxSpBX1Q=#+GKxaUcGpz3wrZ&gw_<%f&&|@KiM`Jr`mc;oTkmPd* z+Uf%O!wW*cy{|m%TLw*}`VQ+FffFa{>PffX_z~P-a^<1FpDlZP9FoDM5)wz-_Htv_ zabaq2Oyop0Qm*#He!$|s8hN9tO)o>#2b*<_gHyz{x9i91z$VkIy$|Nknf$#5=55eJ zhZgt=j_YaFBYy+_0CjtkRjjusaAE+UX{)@m z-EEVv4@te`Di}LR`~UJejX1YC3x4|wC?MJDY=r`A1Qbw>_$uZ1ADGjCSx8F|Sfi=E zA0w%?jO)PH)`R8auquY9?W%?nw*x&6yY{!5MzHg$c0wnL=EED)j$tlGRC#~+|L;Fo zYY7HfBmJ6V1@pt)o8?y`xcU*1)Q}uEOFsftB;0(eR{*=$)IejxrGLWE3r@1w;JhJB6u`R4<{pBMwwBWR4mMpliZn1DvY((r0BrX zf~NVGxR=Z-&mtgn)@f!HX@v3*%>*Mg#UI>$%`uDj)hQruhRDVHT%*j2QDuA8%p*lY z81)lp1ff2sYw9Pu8sn-A-hJzht$M;IBNQaS48tNXLn={akBl-C^ISWfth`6lR$a;* za%9a=&$%Sn4%=hw9b7cUvLs4FW8>uOd@y3D)P!6n}J%NFKH&I>2HYd9$QM zA}h4p#-*aLKRLC-wfh>T%y3Ht^*kw`>qAUM7fJr&@YX=;hc0`~%quGq6;;eCf{^|Q zMPJr69VcP!_Ku1elKj*Bv3gB2j996>P-_3p{x@CMj4CkiWXEMEMixy5bsTeKUvNdz z77LC4(A)w7uwX^U3rf6A5)~GokR2c4t-$PayMbE_$JlG?XoSlO-5ELHNifQW~x-2r& zU?B>91nQnpy>Rjmw)U8cm^k?f)GhJ~pG>vZq=1&>2&%Kahu3zel`9Y zL?M9XetDl~L!$U(A<#1ipS-cxsU6o0BeK6zpnzKWQ%Gkyi5vVKi5XCZya($u+(GJa za47sxBmHEAM`U(@hHREZmR&cg%Md8!d5yC#!Ct5l!g6VtMMnJ{iL9xZEn5iHNS_dR z00K1*iJ8$1(K+8D{+#Qt;D9o+u-TrIZHfWAfDhIsIu6Pe$RkPazsdY*M!D+AsRW1` z4arL*6n~i6p0iBo`YSjUR1(-fIterwU?V))Ki3A+Pwmk)YQDJ3PF@D7E`hS=Nvke7vOm~DqEErDoTkD6C5cNm6a(}1 z!ITINH&v459%O@jo0R8jg-T*BoH5&?W};P+23p})_UR2>EWu)ds_!1?^lC^w!*$J415nYF7@m+Qm_@gU~ zU0~h$Bt94dVgBl}*T{S5Aww1=)GWUd1q75@Oj~doVp5R5C-F8h_ZoRFmRZ?X&U*?u zLqivsMw-RP!PQ#Ds-8gT(V@ojM>jJC$ubOzO{Ohwjs+DOvuJbHLMJc_u1_=EaLDEs zVuVFIGV1RQ7;=*hd0eC;tlV5_@m<_XGXQlaxpu4ZPc2;JdwOMo~p&}x|4 zYRL*^jJ+O9WBwEovk24wwDK(1srQq}S zHV%A7KRXTfewO|_VZSq|fz;L1e6HfCuMwx9@{a%cL*QBk9;JKEY!0HHqvlEfEbZgn zKUvuM#FhG1#UlhLUH*EXk0w7E_`EoIGZc9rf^lmy_QzFp4eQoxFk;bhMaAdnuPSnM zAJ({jeIo6ka{;2yjp7|m=yPs&>Rd4a?hkAXATO6WrvKdR|G5~wH&|4l176p!AFF2w z%So|4>b{!38V<=ln%en)g+r1`z+i{&2{xDtelgml`lO# z0cUy%`JG7w(L*YT9x$!I)T=7|22%ec=Kld=zBo)_=<`UjbGUl3AiM6hQ@fcA z2g9w3$Dj~Wa`bazH0IlRvxU0gOq79ma)T8fP^?%plqzx9}Jki+u zXP-KKk`PL2s-%YgbJ73E^#5g*N`&!vHD$!EMRjO&x?A{33(%V_K5;pSK_ zzS1_o6PQ;atXn4Ezft$i)W|5OXuhv7Y5lpz%I9pqIszzF;D&I8xd+NT1h%B;WJ1jf zcm`rcAH%ov)!zN7xjoq>27w+ee|20Fnow8%7udX$Wo7GUSAK@D#(ll-AuK+Sv_Tt0 zkxSwFmC|3KMqG?TqSkk;2`laTmgnOguLZDOfCi$c$qtI?XXTwpfsBUbZsq zK!x~R^w>QY)1DCxyp&{8Y)7#8a1F5M((uu<;|YJUmrzn_ZbZfCeO?Hm!5AWbC!rw5 zxK9WzY<*R-syA>mpAY>Hpu2G4g1y>8?XDf8-2AI|H zEXr26zJ;Cwx`NTXqUE9)teN@1g&Bc+AnxNmRV!X2Vp^==fc`4^@(POMuR0nV>Lt$Q zr(XXK7hr;QHCFy$%Lid6fC-v;&6O|jL+Gzv*A|_+LGWsLPXZjveebb*Pq07ml;XoV z^weXNx18o5{FQXnQsi^IeCavx++IN}*RByhQ@&IM0F$nWBmjz-AnH^u8oqq4Re*8} zH0ZIk1%2It>jjIiom$2dVW(@YX}ituk+spnK*ivBB}U;f9TrU?jhK!yF2htf0w}~) z%rb3@ghQuMLT|I=vs8o(n#vJ?4y08K%tfj93-B3?x={MRB37HZwSr&E*#I z(aM8!a^6jUl~H3`{hz6{v6JAzgv^cfA>x+(X#jQi=wh0+0n`LYrYq)A+;9vjMxAm* z5Q(NVUYy7K8~f;$*}8RLpIJyh#z2PL5nt)-#wiIsFhD4>Tx4PU076&tyaLXR|KN6p z9$gl6Ka>2Ni;Uz=BKcr2u{$Mm85Yhw78p&I^N?-2&PG&K)!?v2Rxf(c5rW97LZeME^Ezb7~A& z#@ejSsgLD0KtP%dlh+on8NQQMCbI*mAK6aPF1au!WMox>i8fgL99dgoKMIOm`}XJl zt>O#$seCX_Q7z6Z@(XpPz%u%4E!5q1`2ut(hPx`EF9E<~KQh$EH0$)UkLV)==vh34 ziW9q>0AJLsy+Tc`-~L>9J;u1l45bX0){m2Z{ng zC`%=e?iK1rw9;p$k4ozqu}kJP$`{x3O?jTsVXcQ6V;=!KBvwi$CuD^zqbjPNa6>lF zB6uRR?KR1imXR%pnq)cTyg@{7Di5aC*O;7_D(oL`yl|61c0+E|91!foeI5;Ez-V>& z%ohO#$p4qKj4FG{EYAcHMUc+cnhBatWW66BAwK7_rBW26LTmizs{fI||0fb?>B;^* zl|!G)r30LluO0}!{#Dhlz>?nr`2#XJ*?%*k#io|0=UNq$217FRw{+jhW69@%TS zIEbO@fck5J?UmGjlj{=*KA0n+u;gcxsUU_shj4CXNvpOdkfQ^C;M6^NO6ahSLyd8N ziR3b1bOwO^Vl)HkfeK(lT>xJso0XlZcBkJIQ1XTSug4OADFH-uD>=|o7sS31KI4B@ zY#q3bwBkn?Ns}EA34P-O0nFr+6}W)3e9^y*u>{)(KtHvmeHnJLDwP03O9OO&m(zzu zJ)<(C9N9saGDkoA9AG0Ql8VVeWdik#BTaioWJhqiEDm!XILKiYVB37PK(JPMx)!3= zWmx%_8rIPe1H4r+*uCQ;uAN|Lm3%2~E4_#Z_tEqe-I55=IamWL4kJcIZ z?wu=Mj+y@v^Zx)bBbPgX!tJ^D-^0%c3}Z|1I`MWG9Q+6$uadW2bv zT#CDy&j-tu|995_9)w^}*B>?Z417l$K@nuFx$T%zh^Uazl9#eiV&v0Ml$ z$Ix_}7p+CEnSWINsAwv~Qz8W2h9#g+bVcy}S89%ZLK^Y-D~#zYb6T~?M`i7DKMLIN zzgpQ(*2-;7am-clotOX%$mbRRSE6skmIs4lQi9-0S5RPIXnh!tgND+CL6BA>Qsez$ zEqBVP#r10(WJswFSCJO-`>(X2sp}W|lfcUeXgSTl&RrAzHqMMSE&X?M`Gd{%jVJ#l z0|<;hVz1!aLJLm!2syqao70CHmmATU1~ z!-GKfz`niOT#D)m<^j(rNfzy(Tz?I4lrDf|6_ROg^qd!{4840kiCki;cGB*E3ziSMg!)arQ zd@$;k=kldp7qTTt2)?u~neNS(-~bfGyh-_%DFp zdE(&G?b;CsJ+f_21M$A5zeLjJ&8{41D4H%O54aAc^}i&>;#83Q$*=%2rX(wPEC zKHJMOYV0I7$g?R?-n;4I!oT>Nz>}O0aG^4sDN5J{(+=2-a>E`SnRK|ml;q12;N=$Z z|8fw)UrNd`3JzM(m16sD09mqXlRE;>VQxW7EtI#0!Pa}_{}(>{c$icGTnDU}C^-gQ zcl8;$sU+3nN{bGyw(igmtWr3OuhfrwG7Mh|#3P-D#K-(AsN%H$T7@V~G~f zPzGI&xnbuT06{h40N1ZAnwR}0izU-Pvx5-hf^~b%+dE+M*VSf`Z7%=-U;$wI{*o`f z3?ym+QURNmY)riS|FYLLuC!(VvCLqFm!DuPh=7rfBU@1rrKepVqXbWz{r~ag>S}m05UHHPR2r~&;;ZZ))rqGPvyUi#^?Zv zj!2r6NPfbMU0er%UDE+9{qLIOwK$M*P+!1%f$YFx_2TPASKu{A2E>z>Oo31rlKjQz zGsm8TWh|#l&g=Ymr=ll#0o;MSt?&f^L6!j4MXAu-r_euKvPJ}C3HZL1Z$)L-B!iU< z{7HzacB-%ky0iK~c)7$mIe&Ai8r%UW=($3dUuz(9py$&g&Oulvyop|m1i($pSEA1L zLjifYEdwiUCDSCpnp)KUZ#nIU_nMlLr5&ygGgwHb?fZ^ByKX~6h2V~*W2|^|j;F+Z z(d*yDBCXB^&ORsedqu*!W6ylArbN$&St!`wGWpzu)tr6sP{O9;Nghv)x!lfj$?5dZ zER3KTfrHnT=9Ypzq3>sAwN(nk*vaI3(zyPr8Ec)FC8w94NdB99%qe>G5Ek1?NoDpq zk>QS_Pqow9h{IKd$HKhyPI8hLMp<%Pzt+6%)bMVQev5CDZtOp&?FDw!fL*-=Y&rdwaLG|vb!G2!}|BGiW>PW7|uMssb;NCeFp z2(d6zQ};Phs|z}-$1l?4w8ZSaWLNo}%N$8fLE$7vA;>JBskKEhXsa2j8+|HpC_7e z5q_HD`cO7>aM^&L|7*)h} ztmZ5r5KPUfrq3Bh$^S@=4YO5iGF{&d=imMz5b z-NOu=NG_l;Em#rq(h>y^*H4PIaD%~tBOA}(0@r^W<>XjaA~k-_Fv!;7JnD7o*O-xr zZ%pEk|66!tyr4yF;u^q35F(Kk8N4HoVk@iLQ#E^@M7yiDYQJpl)a=gq z5vtxd&wI{!&U601bKb=IYT%eU_LjqV7^d;u_Xk{S9q*|3SWDTJu(+sAfiiD~wUips z6Ufv%y}v`AkSDtuqo9M{IBz z!jnsBO`dmM6!Ycz*3yP#m!XQCs{G^ck-^dzHL9-i6 z1`Aq}wMiWmz@}785PP+!`>1tiKm+Usc|~p)g%@rsDr|~O{XN~~Jf8gCS{1QWM7L~= z91cH4WUgdf-#x&ylpc;%MN|SCUpFp(ZtwFz$|ilKRsr?2HW?S!iF3or1A_%s$xE98 z#gD}*UB3FNh!&OK%iCZC{}i&%rv9 zB+?@DDVXV5gQXL>o?|$LJ^4W}JE&xEi`P_hB(Q5I-awuuyzALneLkH9;IEn77o*3wBKbY2J+21VOngrpZ36viAEeQ9ac5iYKrS0Y|{U zp@ESm$K6?(c%7`FT`d;InR(z%<+woHGciLfU2yAkrE*Rk5ylbOm~r#Xvmvu}Y|g2J zD{y8Wf}F;v)bf{jGVHCoQQEnK*@w*)YI=2>RZ>14 zG?Nd*4qb4(@6CV|5O7kOYH=G>r{Xs8DzU+v5xMaxnEBdji^2gVY{-B#qRd6{aAU1w zFot)0#e(}nOlQE@=ZC~5_8Wq45zlG%NTjfzhqFj`)IB#$=+EDXUy651q=)Sa!TlZD z11r9xTuo?)OvQ;LU$kq5*&62QsG_J%@W0$!V1<&cc5lS|h2v~aU*y_ArZ&%yl7ksv z52kq#TtZVFgbp|n7bwXM5BWN1HW=Ind*3rmF-E)^*zkosGsF_>{Mj=9li-Iy1`+;K zk?ZHM%5>c4|NjkE-;r>vrGj!ymaE@0|6a$9UKHrnzDp9LFq>x~wBg)>P#)99{9sAv zV|OW%Tj^SrB|sImS|zZG-f*+^-*!UP*UhoG>`Zqzc_V%)u6#+z`@K>3Dg#lcAsUg-U2UxST7v! zbUhrq%;e8*8QQYEQKqu`igoBuaB=Axee^aP`;D-c0&2V;1qneR=3L*64gzj*xTwC# z{=Umu+$Y2SRSY{~B7a)k;>#pY$0w@6K`GC5t$Gg)scsJ(qDy{%j)>BVn< z)mD(LyQIuk0ckP4Fk{KuaI@@EmLkPF&HTiBh$BJVVK)EJXaCVZ7=_b%vX_$D4 zj@$FabpmKB6LxfrNANW)iO;*lzV-Zjch^q=hB8UrRYEz&{bEOtt{soI*>LeTP4rK? zn=$~eqF-ZETxX~t`b7@?vErbC7!}L}NPLsQShJJ}0y32H8)3Z=0vhA{*)`eL?j~O* zpCL(7?+k@KKMQ-48x3qb%oGl0i6M;+#26}r^nR;!uK`!~>M1ysBb?TPu@+>_*2qxI z-VHZZfQ>LtZW%+Zdb*P$h^#>grW;wgFR!}=cBRL!yOO=yP&WC^U^^VxOZ|@sp$=)b zk%6%aqT`UExC+)WE(QjnFWFX`g}d;NfcQ{Q+z_#ZKp zL(aQQC#^cmYPBjT(nigrAt&{<$dRGCb&9Rs6u7A0JZiEldjnYbCT#&_fVIfV-`*3n zL1bO?U-}ciy>~bMG7O&(*P>^|zbxLSg=|^) zT!(y-j-`u_&xB}PzmMsxewpd@6vojHc#4EpP)`+^2Vs1eIit+QYPJvL)|PyK4_io{ z-~;bz^ApwE^wDp13KS!1%SQVU^&UykvN|^DzBcw243?WkV5XM&JP3Yg4Zrc!^RQVt zPtqjzG(1>biyC^JI_MaGT9lulYf zb#?d#)Yq~3#Vc?ylVdt5qq~f#ljsluBqs%Dop?=bELnJ6?Ieuep+l zj1K;*z*u)WKv79N45jUH(9WJ1KRUL9(iG5e@O&dgX0$JJC@nrlgVyf5q~wp5(co;d z=JHI!YslvYqhn!;3U5?!o(b&_L-baK;h6MB%XAi%v&rJC&zFiMveX{gl~c76wDuh^ zumtC(yqqD~qdxsGVKM#+GP)&Fy zclF2$6CY+57X@4{p9Nz))~pQ^kfp$^5Ixz`WqR2GlLI0-ZfceckB>x<=a@Q%j%wx@ zgA{2v`0`W~81|D%{)T+{{*Uvf>5Zw7niMJ8 zwD)WDr@0)t5L)ie!M7Yz4Pq2QAsiw=fCyd!QHb1uS9g2$|rq;|si2?jOET_@yg5y^p?0Ktl5VXKpZUF&8fKV9rF56G zhi@Qs@!CKiv*U<4Fc>Ftlj9Kh1f2%D7Let}r&gv=$z;?%0;e~@4>QcD{D$j$-AJ0X z;gEz;e*nA6qOh31K%jEkBRvtj&>9e1YeeM@;EI@>#z+I*D;Z3#ENG?Uo;7-*nex&M z8AM|leB-%j>Y5*aI;RHTZ4YNJ>Q~tR;0Aj_lVmCFbp0N|3z7jB)&gR&FvXbMIN<7! zNTt0Gi1RvX#HoV~fDuOtpA7lGV#qj2TZ-P99%udOd+7iymZW0=U0cauwvK`R_ z9A!fMWXne|K34vbi_>ApgR(-wDwd7{o1Q{RZsy5M^L8~PjjzLQel`h*7JeNPmv5o! zMRPlVbPTL@6vz*R9q|_gyzV0brY<$`V?)F;A_2AwfGJssP8=*}m_tgdGzof;4hL#{ z-ziX+6HxG@K_dW8lK`cY!huUc>{b23w6A zc#I88#w!lOVNYAA26+jUi$Du)OTb}lR;tBUxEFXbqC?9*Li#r!8NK(oJtF*WD0bPL z9^mBpC`Z2wzK{j#;*)XzTE;b(2U{FNrRIn1q=cZAR~cGPH@^PRaVl(t782q|KeiVT znsecJWbW>0gStJwJ2D|Ha7;24seM zDanjC@wff>ySUbrUd9#cIHVD~#~}Y%hE1O9Fo?j2Y#ls6UQ{jj9`mhF_?MvEZqgu? z5iuE;uNkFTOA0HXQRkjfpU0m#Yfl0_wVn)F35HQ#SQ`aX4Oa+8+*6XRQ*1v^tpSxY z)}Gr<0>z~wuQll2xOyen;okp|h0I+es06hj$&%Ek@ko-D>ss&1cEEIvp%_Z5K04dN zg5M8FOU)Q6y9kn&P(6$T<351c6#@EV&1IMrEHquljCFB|b_>-!X zwES_il1KU(I6-I-`3vDtzv$9#wQQ^M8$}^f@!J;L3it*HJq~~=3Aoy%qYT`xJ0qP3 zJ^%A~yDd1+Z0=wG-X3VeoR!Qo!oF{T)hE2JJ5=3l9oKbq_R&ZFO-}m=z?;DxN4YJC znO!i2p`FQV@gL~u5U|JUk)j$r0Qf8Tb=#EjSN(NWs+0m;b#_LEM$ zU*mnop0p2I2Ol~Uy|bi`tqD-Z1=$P)THzH{7!#tbRT>#)@BDT^MZ-~F#u+*VuM^1= zXW@za2);NUn(6n= zht^ov&ibtvN7l1=(JIvDx|Jo`F`v$Y29~jrH7*kj+2#8;j$$rPLQ_oJkiFqi=xsq8 z1%Np;xK5hncODc%FV5JLi)VFFLMAn$NV9o1YKQG>)z|zS)7_%VH1z5!mrQExRGjF= i7-Kh1s8!9UJDKVJ{7=b0{Vx6HUqnHFL4Rb?u>T2!P7I*{ literal 311174 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

4s`&VR9lSJydTJVQrPj! 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�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 z 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 Date: Mon, 2 Nov 2026 10:37:59 -0500 Subject: [PATCH 005/291] fix: make private IP forward host warning neutral for LAN and VPN addresses --- .../proxy_host_forward_warnings_test.go | 41 +++++++++++++++++++ .../api/handlers/proxy_host_handler.go | 2 +- frontend/src/components/ProxyHostForm.tsx | 4 +- 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 backend/internal/api/handlers/proxy_host_forward_warnings_test.go diff --git a/backend/internal/api/handlers/proxy_host_forward_warnings_test.go b/backend/internal/api/handlers/proxy_host_forward_warnings_test.go new file mode 100644 index 000000000..600d1ad48 --- /dev/null +++ b/backend/internal/api/handlers/proxy_host_forward_warnings_test.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + dockerBridgeWarningMessage = "This looks like a Docker container IP address. Docker IPs can change when containers restart. Consider using the container name for more reliable connections." + privateIPWarningMessage = "Using a private IP address. Make sure Charon can reach it (for example over your LAN or VPN). If this is a Docker container, its IP may change on restart; container names are more reliable." +) + +func TestGenerateForwardHostWarnings_Branches(t *testing.T) { + tests := []struct { + name string + forwardHost string + wantMessage string // empty means no warning expected + }{ + {name: "docker bridge IP", forwardHost: "172.17.0.1", wantMessage: dockerBridgeWarningMessage}, + {name: "RFC1918 192.168 LAN", forwardHost: "192.168.1.100", wantMessage: privateIPWarningMessage}, + {name: "RFC1918 10/8 VPN-style address", forwardHost: "10.8.0.5", wantMessage: privateIPWarningMessage}, + {name: "public IP has no warning", forwardHost: "8.8.8.8", wantMessage: ""}, + {name: "hostname has no warning", forwardHost: "my-app", wantMessage: ""}, + {name: "empty host has no warning", forwardHost: "", wantMessage: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warnings := generateForwardHostWarnings(tt.forwardHost) + if tt.wantMessage == "" { + assert.Empty(t, warnings) + return + } + require.Len(t, warnings, 1) + assert.Equal(t, "forward_host", warnings[0].Field) + assert.Equal(t, tt.wantMessage, warnings[0].Message) + }) + } +} diff --git a/backend/internal/api/handlers/proxy_host_handler.go b/backend/internal/api/handlers/proxy_host_handler.go index bc46a62dc..9244bf9f9 100644 --- a/backend/internal/api/handlers/proxy_host_handler.go +++ b/backend/internal/api/handlers/proxy_host_handler.go @@ -125,7 +125,7 @@ func generateForwardHostWarnings(forwardHost string) []ProxyHostWarning { } else if ip := net.ParseIP(forwardHost); ip != nil && network.IsPrivateIP(ip) { warnings = append(warnings, ProxyHostWarning{ Field: "forward_host", - Message: "Using a private IP address. If this is a Docker container, the IP may change on restart. Container names are more reliable for Docker services.", + Message: "Using a private IP address. Make sure Charon can reach it (for example over your LAN or VPN). If this is a Docker container, its IP may change on restart; container names are more reliable.", }) } diff --git a/frontend/src/components/ProxyHostForm.tsx b/frontend/src/components/ProxyHostForm.tsx index 2b99c44dc..7a0751ebe 100644 --- a/frontend/src/components/ProxyHostForm.tsx +++ b/frontend/src/components/ProxyHostForm.tsx @@ -531,8 +531,8 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor ) } else if (isPrivateOrDockerIP(host)) { setForwardHostWarning( - 'Using a private IP address. If this is a Docker container, the IP may change on restart. ' + - 'Container names are more reliable for Docker services.' + 'Using a private IP address. Make sure Charon can reach it (for example over your LAN or VPN). ' + + 'If this is a Docker container, its IP may change on restart; container names are more reliable.' ) } else { setForwardHostWarning(null) From 52d4175f55f811faec4da470b1393ddc6a0c5e6f Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 10:38:43 -0500 Subject: [PATCH 006/291] docs: add guide for proxying S3-compatible storage (Garage, MinIO) --- docs-site/zensical.toml | 1 + docs/guides/s3-object-storage.md | 167 +++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 docs/guides/s3-object-storage.md diff --git a/docs-site/zensical.toml b/docs-site/zensical.toml index 70386dd37..0ddc753f2 100644 --- a/docs-site/zensical.toml +++ b/docs-site/zensical.toml @@ -34,6 +34,7 @@ nav = [ "guides/dns-providers.md", { "DNS provider setup" = ["guides/dns-providers/cloudflare.md", "guides/dns-providers/route53.md", "guides/dns-providers/digitalocean.md", "guides/dns-providers/azure-dns.md", "guides/dns-providers/google-cloud-dns.md"] }, "guides/manual-dns-provider.md", + "guides/s3-object-storage.md", "guides/local-key-management.md", "guides/supply-chain-security-user-guide.md", "guides/supply-chain-security-developer-guide.md", diff --git a/docs/guides/s3-object-storage.md b/docs/guides/s3-object-storage.md new file mode 100644 index 000000000..fba085a1d --- /dev/null +++ b/docs/guides/s3-object-storage.md @@ -0,0 +1,167 @@ +--- +title: Sharing S3 Storage (Garage or MinIO) Through Charon +description: A beginner-friendly guide to making your home S3 storage (Garage or MinIO) reachable from the internet using a normal Charon Proxy Host. +--- + +# Sharing S3 Storage (Garage or MinIO) Through Charon + +Do you run **Garage** or **MinIO** at home and want to reach it from outside your house? This guide shows you how to do that with Charon. + +Good news: you do **not** need a special "S3" button. Charon treats your storage like any other website or app on your home network. You make a normal **Proxy Host**, and that's it. + +--- + +## The Short Version + +1. Make sure Charon can reach your storage machine (for example, over your VPN). +2. In Charon, click **Proxy Hosts**, then click **+ Add**. +3. Fill in the boxes as shown below and click **Save**. +4. Point your apps at your new public address. + +--- + +## Words You Will See + +| Word | What it means in plain English | +| --- | --- | +| **S3 storage** | A "filing cabinet" for files that apps can read and write over the internet. Garage and MinIO are two programs that provide one. | +| **Proxy Host** | A "doorman" that Charon sets up. People knock on the public address, and Charon walks them to the right machine inside your home. | +| **Domain name** | The public address people type, such as `s3.example.com`. | +| **IP address** | The number that identifies a machine on a network, such as `10.8.0.5`. | +| **Port** | A numbered "door" on a machine. Garage uses door `3900` for S3 and MinIO uses door `9000`. | +| **VPN** | A private tunnel that links your machines together, even when they are in different places. | + +--- + +## Before You Start + +You need: + +- Charon installed and running, and you can open its web page. +- A domain name (such as `example.com`) that you control, and the ability to add a new record for it at your domain provider. If you don't have one yet, see the [Getting Started](../getting-started.md) page. +- Garage or MinIO running on your storage machine. +- Your storage machine reachable from the Charon machine. A VPN link works well for this. + +> **Tip:** Find the storage machine's address on your VPN. It often looks like `10.x.x.x` or `100.x.x.x`. Use that address in Charon, not your home's public internet address. + +--- + +## Step 1: Create the Proxy Host + +1. Open Charon in your web browser. +2. Click **Proxy Hosts**, then click **+ Add**. +3. Fill in the boxes like this: + +| Box | What to type | Example | +| --- | --- | --- | +| **Domain** | The public address you want to use | `s3.example.com` | +| **Forward To** | The address of your storage machine, reachable from Charon | `10.8.0.5` | +| **Port** | `3900` for Garage, `9000` for MinIO | `3900` | +| **Scheme** | `http` | `http` | + +4. Keep the secure (HTTPS) option turned on. Charon gets your certificate automatically. +5. Click **Save**. + +Charon will now answer on `https://s3.example.com` and pass every request to your storage machine. + +> **About the "private IP address" warning:** Charon may show a warning because `10.8.0.5` is a private address. This warning is only a reminder. If the address is a VPN address that Charon can reach, you can safely ignore it. + +--- + +## Step 2: Point Your Domain at Charon + +Go to your domain provider and create an **A record** (a record that says "this name lives at this address"): + +| Name | Points to | +| --- | --- | +| `s3` | The public internet address of your Charon machine | + +After a few minutes, `s3.example.com` will lead to Charon. + +--- + +## Step 3: Connect Your Apps + +Use these settings in your app (or in a tool such as rclone or the AWS command-line tool): + +| Setting | Value | +| --- | --- | +| **Endpoint URL** | `https://s3.example.com` | +| **Region** | The region name you chose when setting up Garage or MinIO | +| **Access key and secret key** | The keys you created inside Garage or MinIO | +| **Addressing style** | **Path-style** (this is the simplest choice and needs no extra steps) | + +That's it. Your apps can now upload, download, and list files through your new address. + +--- + +## Optional: Use Bucket-Style Addresses + +Some apps prefer addresses like `my-photos.s3.example.com` instead of `s3.example.com/my-photos`. If you want that, you need a few extra steps. If the simple path-style setup above works for you, skip this section. + +1. **Add a wildcard name to your Proxy Host.** In the **Domain** box, type both names separated by a comma: + + ```text + s3.example.com,*.s3.example.com + ``` + + The `*` means "anything before this part," so every bucket gets its own name. + +2. **Get a wildcard certificate.** A wildcard certificate covers every name that starts with `*.`. Charon gets one by proving you own your domain through your DNS provider. Follow the [DNS Challenge](../features/dns-challenge.md) page and the [DNS provider setup](dns-providers.md) guide for your provider. The wildcard name is only covered by a DNS Challenge certificate, so you must turn that on for this host. + +3. **Tell Garage about your domain.** In Garage's settings file, set the root domain to match your public name: + + ```text + root_domain = ".s3.example.com" + ``` + + Restart Garage after you change this. + +4. **Add a wildcard record at your domain provider.** Create an **A record** with the name `*.s3` that points to the same address as `s3`. + +--- + +## Keeping Your Storage Safe + +Your storage is now reachable from the internet, so take these steps seriously: + +- **Keep the VPN link private.** Only your own machines should be on that tunnel. Do not open the storage machine's own ports to the internet. +- **Only share the public address through Charon.** Let Charon be the one door to the outside world. +- **Use strong keys.** Create long, random access keys inside Garage or MinIO. Don't reuse a password you use elsewhere. +- **Consider Charon's protection features for this host.** Charon has [Access Control Lists](../features/access-control.md) that limit who can reach a site, and a [Security overview](../features/security.md) that explains the other protections it provides. Turn on what fits your needs. + +--- + +## Troubleshooting + +### My upload or download works in one tool but fails in another + +Some tools are picky about how they talk to Garage. If you see an error that mentions **"signed header content-length is not present"** while using the AWS command-line tool to download files from Garage, this is a known issue between that tool and Garage. Charon is not the cause, and Charon cannot change it. Try one of these fixes: + +- **Use rclone.** It works well with both Garage and MinIO through Charon. +- **Use the classic transfer mode in the AWS tool.** Run this once in a terminal: + + ```bash + aws configure set default.s3.preferred_transfer_client classic + ``` + +- **Use the single-file download command.** Use `aws s3api get-object` instead of `aws s3 cp`. + +MinIO does not have this problem. + +### The AWS command-line tool says "invalid checksum algorithm" + +Newer versions of the AWS tool add extra checksum data that older Garage versions (such as Garage 1.0.1) reject. This happens even without Charon. To fix it, set these two settings before running the tool: + +```bash +export AWS_REQUEST_CHECKSUM_CALCULATION=when_required +export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required +``` + +On Windows PowerShell, use `$env:AWS_REQUEST_CHECKSUM_CALCULATION="when_required"` and `$env:AWS_RESPONSE_CHECKSUM_VALIDATION="when_required"` instead. + +### The page does not load at all + +- Check that **Forward To** and **Port** match where Garage or MinIO is actually running. +- Check that your VPN is connected on the Charon machine. +- Check that your domain's A record points to the right public address. From d5462fb13feddc70213d7b73c03c300734d7aaf8 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 11:24:15 -0500 Subject: [PATCH 007/291] chore(deps): update vitest dependencies to version 5.1.0 --- frontend/package-lock.json | 1113 +++++++++++------------------------- frontend/package.json | 8 +- 2 files changed, 324 insertions(+), 797 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2bcf70f92..0a2f9d7a8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -59,10 +59,10 @@ "@typescript-eslint/utils": "^8.72.1", "@typescript/native": "npm:typescript@^7.1.0-dev.20261102.1", "@vitejs/plugin-react": "^6.2.1", - "@vitest/coverage-istanbul": "^5.0.5", - "@vitest/coverage-v8": "^5.0.5", + "@vitest/coverage-istanbul": "^5.1.0", + "@vitest/coverage-v8": "^5.1.0", "@vitest/eslint-plugin": "^1.6.28", - "@vitest/ui": "^5.0.5", + "@vitest/ui": "^5.1.0", "audit-ci": "^7.1.0", "eslint": "^10.14.0", "eslint-formatter-compact": "^9.0.1", @@ -87,7 +87,7 @@ "typescript": "npm:@typescript/typescript6@^6.0.2", "typescript-eslint": "^8.72.1", "vite": "^8.3.5", - "vitest": "^5.0.5", + "vitest": "^5.1.0", "zod-validation-error": "^5.0.0" }, "engines": { @@ -131,6 +131,16 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@asamuzakjp/dom-selector": { "version": "10.0.5", "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-10.0.5.tgz", @@ -147,6 +157,16 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -163,149 +183,57 @@ } }, "node_modules/@babel/compat-data": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz", - "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.10.tgz", - "integrity": "sha512-T6dEcoMJ3L1hyLuNfRXcVlqQrN79kU/SB5bDygVgQNIOUgStdyEGzigtRA9K+9Coh7SZUSt543oWRBxvqYdyuQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^8.0.6", - "@babel/generator": "^8.0.10", - "@babel/helper-compilation-targets": "^8.0.7", - "@babel/helpers": "^8.0.10", - "@babel/parser": "^8.0.10", - "@babel/template": "^8.0.0", - "@babel/traverse": "^8.0.10", - "@babel/types": "^8.0.10", - "@types/gensync": "^1.0.5", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", - "empathic": "^2.0.1", + "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "import-meta-resolve": "^4.2.0", "json5": "^2.2.3", - "obug": "^3.0.0", - "verkit": "^0.5.0" + "semver": "^6.3.1" }, "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.9.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/@babel/code-frame": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz", - "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^8.0.6", - "js-tokens": "^10.0.0" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz", - "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@babel/core/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/generator": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.10.tgz", - "integrity": "sha512-iC9p3U4Y3vp0qKjRcfFeNzpj+kkK8BVYZTbQepBWVD7vOBLH6ji0drNzKPTPtumFd2w2r/hbEKYjG4pscKa4bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^8.0.10", - "@babel/types": "^8.0.10", - "@jridgewell/gen-mapping": "0.4.0-beta.0", - "@jridgewell/trace-mapping": "^0.3.31", - "@types/jsesc": "^2.5.0", - "jsesc": "^3.0.2" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.4.0-beta.0", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz", - "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0-beta.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.7.tgz", - "integrity": "sha512-ilAZCdOOlOVA7dQbaFNw2f/59p6GW33NxTopc3DvEE3efp3ZXTrGX/yZScaKw8/vd9bq31Ag0EcXiLw32bAgJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^8.0.5", - "@babel/helper-validator-option": "^8.0.0", - "browserslist": "^4.24.0", - "flru": "1.0.2", - "verkit": "^0.5.0" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.6.tgz", - "integrity": "sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/generator": { "version": "7.29.12", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", @@ -322,98 +250,83 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/helper-globals": { + "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/helper-string-parser": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", - "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/parser": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", - "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.11" - }, + "license": "ISC", "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" + "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/template": { + "node_modules/@babel/helper-globals": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/traverse": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", - "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.12", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.12", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.11", - "debug": "^4.3.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", - "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.29.11", - "@babel/helper-validator-identifier": "^7.29.7" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.8.tgz", - "integrity": "sha512-fVM+gSuKbdv9cpmyRNh5EvsDHUZCdxQH3FdnFKSkNYHpbsn95bbrxm+ES3MVeQjcqwi2O5ur/DjLfmTQaO60ng==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", "dev": true, "license": "MIT", "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { @@ -427,43 +340,43 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz", - "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.10.tgz", - "integrity": "sha512-tBG9yuaR26JJWFafcl/Dk4eib6bkRJ9H5e6lENcwZqFudA9ImNCuMXLjB3/t+Ub7/DOZv2Rmi5LCOi8q2DCekQ==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^8.0.0", - "@babel/types": "^8.0.10" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" }, "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.10.tgz", - "integrity": "sha512-h+ah2Kg0ne1Vii892dShj9gky9I5jPzkd8x4s/3xgN/P2bMWmHS3qq59tum3mCVjCBXI8uqfYyYtrfp1Mz6+1w==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", + "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^8.0.10" + "@babel/types": "^7.29.11" }, "bin": { "parser": "bin/babel-parser.js" }, "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.0.0" } }, "node_modules/@babel/runtime": { @@ -476,123 +389,51 @@ } }, "node_modules/@babel/template": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", - "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^8.0.0", - "@babel/parser": "^8.0.0", - "@babel/types": "^8.0.0" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz", - "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^8.0.6", - "js-tokens": "^10.0.0" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz", - "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.9.0" } }, - "node_modules/@babel/template/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/traverse": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.10.tgz", - "integrity": "sha512-t5DaGzMi9WlhMSFT2YavI8gl8yGihoQymSQUkIq9+MUh5tO3Vx9KHwr5gMCzeBdYkvs00xNvr18PxtQTRayucA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^8.0.6", - "@babel/generator": "^8.0.10", - "@babel/helper-globals": "^8.0.6", - "@babel/parser": "^8.0.10", - "@babel/template": "^8.0.0", - "@babel/types": "^8.0.10", - "obug": "^3.0.0" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz", - "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", + "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^8.0.6", - "js-tokens": "^10.0.0" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.12", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.12", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.11", + "debug": "^4.3.1" }, "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz", - "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/types": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.10.tgz", - "integrity": "sha512-KV8Y+teoRsmXeEPS4sCJY1kwV+hcPzHP1qQqgCxrjKOehLdBaxpsFBfoJ8MXWFV80y6Jw56+TM95wR0Per8dYw==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^8.0.8", - "@babel/helper-validator-identifier": "^8.0.6" + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/types/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz", - "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" + "node": ">=6.9.0" } }, "node_modules/@bcoe/v8-coverage": { @@ -3341,9 +3182,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.105.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.105.1.tgz", - "integrity": "sha512-B/rNlZoZ54w1dlddiUt9oEe1SLM+37c00SKLJEg9YH+7as3Y54P81o9Fh+2ujZ36tXPnQJ5oVe7AcyEWkyFhPg==", + "version": "5.105.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.105.2.tgz", + "integrity": "sha512-cyZGOtI1ZZ79mm905PJ42ng43Mwbc9CeqB83XuJmj9ADj/C+dzUXcgMQucRQ8yZs7atHykMzqavAXXhjxAw50g==", "license": "MIT", "funding": { "type": "github", @@ -3351,12 +3192,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.105.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.105.1.tgz", - "integrity": "sha512-qcu+/+PtBOjW8qU4/0l47QVmNG7hPOWsCLzP0bTRhE1X7HYa757C4732wXTUH0QhCjTV1yedX8onsCiEwlNYrA==", + "version": "5.105.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.105.2.tgz", + "integrity": "sha512-K7MMGdVIgsQ41o6TI0fXD75aRCDNBLEKP80FvhvuwFkgiC36+rd8/JI5BYufLpfbZ3/sFTcYnotWxJqDxV/9dQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.105.1" + "@tanstack/query-core": "5.105.2" }, "funding": { "type": "github", @@ -3614,13 +3455,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/gensync": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz", - "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/hast": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", @@ -3631,13 +3465,6 @@ "@types/unist": "*" } }, - "node_modules/@types/jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4457,18 +4284,16 @@ } }, "node_modules/@vitest/coverage-istanbul": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-5.0.5.tgz", - "integrity": "sha512-nmbE0+e1JSH9kYEoHGp/iKCQq6+TqwvQ4EcH2zKPULOIrY7rPHxDHsipYGGr+0xMDdzCy46F8zj3edz8dsqvdg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-5.1.0.tgz", + "integrity": "sha512-XRAUYsfdho+Az4K2eEWOVPedtIzUZbCl2eO9pOjNH5L1/niuy8cYwlK2W9RUtfwYZumoqic+4pRGqAvch/JSzg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/trace-mapping": "0.3.31", - "@vitest/istanbul-lib-coverage": "^1.0.2", - "@vitest/istanbul-lib-instrument": "^1.0.2", - "@vitest/istanbul-lib-report": "^1.0.2", - "@vitest/istanbul-lib-source-maps": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^2.1.0", + "@vitest/istanbul-lib-instrument": "^2.1.0", + "@vitest/istanbul-lib-report": "^2.1.0", + "@vitest/istanbul-lib-source-maps": "^2.1.0", "magicast": "^0.5.5", "obug": "^3.0.0", "tinyrainbow": "^3.2.0" @@ -4477,19 +4302,19 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "5.0.5" + "vitest": "5.1.0" } }, "node_modules/@vitest/coverage-v8": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.5.tgz", - "integrity": "sha512-4KpkGdIrSqBihx/JyP/e+S+IKB98etg1XDQiLAxksLfcShnwi1VReGeOT9NI43hLHbknosKp/1CCLn9Zgcs8mg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.1.0.tgz", + "integrity": "sha512-OY9aShBQG5KOIw2zCo9fDoCHI9RHliNHJ//nM7m6jGg41Hyke9JOGWmeMlK74xYErsxMoK8mIB3gAeEyFVA7LA==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/istanbul-lib-coverage": "^1.0.2", - "@vitest/istanbul-lib-report": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^2.1.0", + "@vitest/istanbul-lib-report": "^2.1.0", "ast-v8-to-istanbul": "^1.0.7", "magicast": "^0.5.5", "obug": "^3.0.0", @@ -4500,8 +4325,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "5.0.5", - "vitest": "5.0.5" + "@vitest/browser": "5.1.0", + "vitest": "5.1.0" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4541,9 +4366,9 @@ } }, "node_modules/@vitest/istanbul-lib-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.2.tgz", - "integrity": "sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-2.1.0.tgz", + "integrity": "sha512-WOjuS+xcjSIy7ruYBQ+9BUcV7XZQ0FyVcbWbWxW9TRx/kLGx5SQaeFRIFDJxjNplb6YsT7SpKo2zuXXUo91OIQ==", "dev": true, "license": "MIT", "engines": { @@ -4551,73 +4376,70 @@ } }, "node_modules/@vitest/istanbul-lib-instrument": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-instrument/-/istanbul-lib-instrument-1.0.2.tgz", - "integrity": "sha512-KFlvK0uYxYjdNoQJR+6F1B45u0ZOmYgOZndqfwLiGBjYHACc8q1Wyuw/ZQeGP3UKc7XsSZ51NKQDKt0IIbGMQg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-instrument/-/istanbul-lib-instrument-2.1.0.tgz", + "integrity": "sha512-lMhFnfKPkueJQealWtkRWv0E95gO14OQg/ZzVT4tqDRXLxn6teAZKBe1uXg9jNB2AI5WPk4vF52R+59LAUgQUA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^8.0.1", - "@babel/parser": "^8.0.4", - "@istanbuljs/schema": "^0.1.3", - "@jridgewell/trace-mapping": "^0.3.23", - "@vitest/istanbul-lib-coverage": "1.0.2" + "@istanbuljs/schema": "^0.1.6", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/trace-mapping": "^0.3.31", + "@vitest/istanbul-lib-coverage": "2.1.0", + "magic-string": "^1.4.3", + "rolldown": "^1.0.0" }, "engines": { "node": ">=22" } }, - "node_modules/@vitest/istanbul-lib-report": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.2.tgz", - "integrity": "sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==", + "node_modules/@vitest/istanbul-lib-instrument/node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/istanbul-lib-coverage": "1.0.2" - }, - "engines": { - "node": ">=22" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, - "node_modules/@vitest/istanbul-lib-source-maps": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.0.2.tgz", - "integrity": "sha512-Ia3+9WDB5UKN940ztJ5LyhqDaS/It05Uh2VQ70KIPDWlOed2ZsJTJTyMb7x6OIW4eAwWSBFknpqJafJetTTXLw==", + "node_modules/@vitest/istanbul-lib-report": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-2.1.0.tgz", + "integrity": "sha512-+ZFta25nWtGCOBG9/IET8w1RO5Oawsd6H8Uf6AJ9EA/StFuKVtKAEJEJ6uRt84OH+T4YkI0ASmIHAhRA+0KVxw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "@vitest/istanbul-lib-coverage": "1.0.2", - "obug": "^2.1.4" + "@vitest/istanbul-lib-coverage": "2.1.0" }, "engines": { "node": ">=22" } }, - "node_modules/@vitest/istanbul-lib-source-maps/node_modules/obug": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", - "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "node_modules/@vitest/istanbul-lib-source-maps": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-source-maps/-/istanbul-lib-source-maps-2.1.0.tgz", + "integrity": "sha512-X+fmmI1SEiJZc/nmMUllliP8qaLgi4xMcDd0HwTBMdaanbRcCu/+IKo+iq7wbjJXtAEBWQrGyI18wfSWIqyDpw==", "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "@vitest/istanbul-lib-coverage": "2.1.0", + "obug": "^3.0.0" + }, "engines": { - "node": ">=12.20.0" + "node": ">=22" } }, "node_modules/@vitest/mocker": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", - "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.1.0.tgz", + "integrity": "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.5", + "@vitest/spy": "5.1.0", "estree-walker": "^3.0.3", "magic-string": "^1.4.3" }, @@ -4648,9 +4470,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.0.5.tgz", - "integrity": "sha512-24sSdIlMA9DVPXnYiB9siwU0bTd887Y9jX3+vabztCSsvHwo7ipvJ/a2L3Rljw8+2rK1f5fCj3h5U9I/Jsqgbw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.1.0.tgz", + "integrity": "sha512-IRx5mc30CrFzyMNni6SZKQ1r53kv049Mcvvk0WbWcD6PUz/bFjG4zYv0KosQAeonPpGp5WxqYBMqdSOfJfvVWw==", "dev": true, "license": "MIT", "dependencies": { @@ -4661,9 +4483,9 @@ } }, "node_modules/@vitest/spy": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", - "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.1.0.tgz", + "integrity": "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==", "dev": true, "license": "MIT", "funding": { @@ -4671,13 +4493,13 @@ } }, "node_modules/@vitest/ui": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-5.0.5.tgz", - "integrity": "sha512-dCoGUOdgmCaSB2g+WQ/spPCBkytAWdNUkFSXY4z+eYp2d8JEOY8v0uDRaJTG9qf/kTlTVZ4CuYMYMz/f1HS/qQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-5.1.0.tgz", + "integrity": "sha512-00S3x4NRmqIBUiTVWF9/xLaA9EP9KeKv0+1sBO7e2Y4luBISlW+2ttnhlk/zjbTqEvVhJWCCo7MXzu6zNbefqQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "5.0.5", + "@vitest/utils": "5.1.0", "fflate": "^0.8.3", "flatted": "^3.4.4", "pathe": "^2.0.3", @@ -4688,17 +4510,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "5.0.5" + "vitest": "5.1.0" } }, "node_modules/@vitest/utils": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.0.5.tgz", - "integrity": "sha512-/rBu7gzLDQI/SC79Tn98b5DwOgaHU/Gt4I9OVeP3YofzNaYtbImRCOJQmT1S6vOJg9gEIlWC+r3QxKkjuSa1nQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.1.0.tgz", + "integrity": "sha512-V+qgCwkvVw+s1t1S5TaoBHsLi5Sz/YmohCMoOu0mZ8ZQst2bKhRYARAQucWpVg8D0mv0x5MHcWXY5QdOsoiGBQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "5.0.5", + "@vitest/pretty-format": "5.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.2.0" }, @@ -5885,16 +5707,6 @@ "dev": true, "license": "MIT" }, - "node_modules/empathic": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz", - "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/enhanced-resolve": { "version": "5.26.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", @@ -6335,362 +6147,141 @@ "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-import-x" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^8.56.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "eslint-import-resolver-node": "*" - }, - "peerDependenciesMeta": { - "@typescript-eslint/utils": { - "optional": true - }, - "eslint-import-resolver-node": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-no-unsanitized": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz", - "integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==", - "dev": true, - "license": "MPL-2.0", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz", - "integrity": "sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/generator": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", - "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.12", - "@babel/types": "^7.29.11", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "funding": { + "url": "https://opencollective.com/eslint-plugin-import-x" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } } }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=4.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/helper-string-parser": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", - "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=6.9.0" + "node": ">= 0.4" } }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/helpers": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", - "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8" - }, - "engines": { - "node": ">=6.9.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/parser": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", - "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@babel/types": "^7.29.11" - }, - "bin": { - "parser": "bin/babel-parser.js" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=6.0.0" + "node": "*" } }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "node_modules/eslint-plugin-no-unsanitized": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz", + "integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" + "license": "MPL-2.0", + "peerDependencies": { + "eslint": "^9 || ^10" } }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/traverse": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", - "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", + "node_modules/eslint-plugin-promise": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz", + "integrity": "sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.12", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.12", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.11", - "debug": "^4.3.1" + "@eslint-community/eslint-utils": "^4.4.0" }, "engines": { - "node": ">=6.9.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" } }, - "node_modules/eslint-plugin-react-hooks/node_modules/@babel/types": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", - "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.29.11", - "@babel/helper-validator-identifier": "^7.29.7" + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-plugin-react-hooks/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/eslint-plugin-react-hooks/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": { @@ -7153,16 +6744,6 @@ "dev": true, "license": "ISC" }, - "node_modules/flru": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", - "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/follow-redirects": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz", @@ -7743,17 +7324,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8378,6 +7948,16 @@ } } }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8924,13 +8504,13 @@ } }, "node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" } }, "node_modules/lucide-react": { @@ -8980,46 +8560,6 @@ "source-map-js": "^1.2.1" } }, - "node_modules/magicast/node_modules/@babel/helper-string-parser": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", - "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/magicast/node_modules/@babel/parser": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", - "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.11" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/magicast/node_modules/@babel/types": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", - "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.11", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/map-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", @@ -12323,19 +11863,6 @@ "dev": true, "license": "MIT" }, - "node_modules/verkit": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", - "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, "node_modules/victory-vendor": { "version": "37.4.1", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.4.1.tgz", @@ -12710,14 +12237,14 @@ } }, "node_modules/vitest": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", - "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.1.0.tgz", + "integrity": "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.5", + "@vitest/mocker": "5.1.0", "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", @@ -12743,12 +12270,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.5", - "@vitest/browser-preview": "5.0.5", + "@vitest/browser-playwright": "5.1.0", + "@vitest/browser-preview": "5.1.0", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.5", - "@vitest/coverage-v8": "5.0.5", - "@vitest/ui": "5.0.5", + "@vitest/coverage-istanbul": "5.1.0", + "@vitest/coverage-v8": "5.1.0", + "@vitest/ui": "5.1.0", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" diff --git a/frontend/package.json b/frontend/package.json index bd44f8884..bda3a11a7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -79,10 +79,10 @@ "@typescript-eslint/utils": "^8.72.1", "@typescript/native": "npm:typescript@^7.1.0-dev.20261102.1", "@vitejs/plugin-react": "^6.2.1", - "@vitest/coverage-istanbul": "^5.0.5", - "@vitest/coverage-v8": "^5.0.5", + "@vitest/coverage-istanbul": "^5.1.0", + "@vitest/coverage-v8": "^5.1.0", "@vitest/eslint-plugin": "^1.6.28", - "@vitest/ui": "^5.0.5", + "@vitest/ui": "^5.1.0", "audit-ci": "^7.1.0", "eslint": "^10.14.0", "eslint-formatter-compact": "^9.0.1", @@ -107,7 +107,7 @@ "typescript": "npm:@typescript/typescript6@^6.0.2", "typescript-eslint": "^8.72.1", "vite": "^8.3.5", - "vitest": "^5.0.5", + "vitest": "^5.1.0", "zod-validation-error": "^5.0.0" }, "engines": { From 3c24d1b21cd49eb2d5abe5ae2ed12ed0000c7dc4 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 11:27:42 -0500 Subject: [PATCH 008/291] chore(deps): update @tanstack/react-query to version 5.105.2 --- frontend/package-lock.json | 2 +- frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0a2f9d7a8..2ddd5e7a8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,7 +16,7 @@ "@radix-ui/react-select": "^2.4.1", "@radix-ui/react-tabs": "^1.2.1", "@radix-ui/react-tooltip": "^1.4.1", - "@tanstack/react-query": "^5.105.1", + "@tanstack/react-query": "^5.105.2", "axios": "^1.21.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/frontend/package.json b/frontend/package.json index bda3a11a7..4137b0625 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "@radix-ui/react-select": "^2.4.1", "@radix-ui/react-tabs": "^1.2.1", "@radix-ui/react-tooltip": "^1.4.1", - "@tanstack/react-query": "^5.105.1", + "@tanstack/react-query": "^5.105.2", "axios": "^1.21.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", From 0498a99ea39136d2e79992638d3ce662580bfd34 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 16:02:25 -0500 Subject: [PATCH 009/291] chore(deps): update @tanstack/react-query to version 5.105.3 --- frontend/package-lock.json | 34 +++++++++++++++++----------------- frontend/package.json | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2ddd5e7a8..337f8387c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,7 +16,7 @@ "@radix-ui/react-select": "^2.4.1", "@radix-ui/react-tabs": "^1.2.1", "@radix-ui/react-tooltip": "^1.4.1", - "@tanstack/react-query": "^5.105.2", + "@tanstack/react-query": "^5.105.3", "axios": "^1.21.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -224,9 +224,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -268,9 +268,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -3182,9 +3182,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.105.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.105.2.tgz", - "integrity": "sha512-cyZGOtI1ZZ79mm905PJ42ng43Mwbc9CeqB83XuJmj9ADj/C+dzUXcgMQucRQ8yZs7atHykMzqavAXXhjxAw50g==", + "version": "5.105.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.105.3.tgz", + "integrity": "sha512-v1rbNy7MB/4Akth7CQH+CerV13K7Ok9hAusEsfz4sCjDdu3/kOSJPoWfoB+YjdEX1eBFBWKNAGE5nqZCgdlHDw==", "license": "MIT", "funding": { "type": "github", @@ -3192,12 +3192,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.105.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.105.2.tgz", - "integrity": "sha512-K7MMGdVIgsQ41o6TI0fXD75aRCDNBLEKP80FvhvuwFkgiC36+rd8/JI5BYufLpfbZ3/sFTcYnotWxJqDxV/9dQ==", + "version": "5.105.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.105.3.tgz", + "integrity": "sha512-dj5hGWZmwoc9NG4enoXuYbiYwb5FxgJYPVv06eUgicMTfEMTRXn08Xv9D/r1V8TfjAv+BpxdvZFzJ1h3hSWZcQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.105.2" + "@tanstack/query-core": "5.105.3" }, "funding": { "type": "github", @@ -10878,9 +10878,9 @@ } }, "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { diff --git a/frontend/package.json b/frontend/package.json index 4137b0625..b6665ebfb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "@radix-ui/react-select": "^2.4.1", "@radix-ui/react-tabs": "^1.2.1", "@radix-ui/react-tooltip": "^1.4.1", - "@tanstack/react-query": "^5.105.2", + "@tanstack/react-query": "^5.105.3", "axios": "^1.21.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", From 788400e42d0d73d89f7a873fef4463f67d453ae1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:05:00 +0000 Subject: [PATCH 010/291] chore: update dependency anchore/grype to v0.121.1 --- .github/skills/security-scan-docker-image-scripts/run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/skills/security-scan-docker-image-scripts/run.sh b/.github/skills/security-scan-docker-image-scripts/run.sh index eef07baa5..b74a42a1b 100755 --- a/.github/skills/security-scan-docker-image-scripts/run.sh +++ b/.github/skills/security-scan-docker-image-scripts/run.sh @@ -35,7 +35,7 @@ fi # Check Grype if ! command -v grype >/dev/null 2>&1; then log_error "Grype not found - install from: https://github.com/anchore/grype" - log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.121.0" + log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.121.1" error_exit "Grype is required for vulnerability scanning" 2 fi @@ -51,7 +51,7 @@ GRYPE_INSTALLED_VERSION=$(grype version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9] # Set defaults matching CI workflow set_default_env "SYFT_VERSION" "v1.55.0" -set_default_env "GRYPE_VERSION" "v0.121.0" +set_default_env "GRYPE_VERSION" "v0.121.1" set_default_env "IMAGE_TAG" "charon:local" set_default_env "FAIL_ON_SEVERITY" "Critical,High" From 4c5d7909f722ce9773fa35dbcfef222a674523cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:05:08 +0000 Subject: [PATCH 011/291] chore: update dependency github/codeql-cli-binaries to v2.27.4 --- scripts/install-codeql.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install-codeql.sh b/scripts/install-codeql.sh index ffb6a7d63..d81a2f8f1 100755 --- a/scripts/install-codeql.sh +++ b/scripts/install-codeql.sh @@ -41,7 +41,7 @@ NC='\033[0m' # deliberately and re-validate `lefthook run codeql` locally before raising # this — an exact pin (not "latest") keeps local scans reproducible, per # CLAUDE.md's "use exact dependency versions" convention. -CODEQL_VERSION="${CODEQL_VERSION:-v2.27.3}" +CODEQL_VERSION="${CODEQL_VERSION:-v2.27.4}" # Where to install the `codeql` PATH shim. Defaults to the first writable, # user-owned directory already on PATH so no sudo is required. From 2d94330a6ce53975f9b50b94331c27a014f98b4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:40:50 +0000 Subject: [PATCH 012/291] chore: update dependency anchore/syft to v1.56.0 --- .github/skills/security-scan-docker-image-scripts/run.sh | 2 +- .github/workflows/docker-build.yml | 2 +- .github/workflows/nightly-build.yml | 2 +- .github/workflows/supply-chain-pr.yml | 2 +- .github/workflows/supply-chain-verify.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/skills/security-scan-docker-image-scripts/run.sh b/.github/skills/security-scan-docker-image-scripts/run.sh index b74a42a1b..40da3beea 100755 --- a/.github/skills/security-scan-docker-image-scripts/run.sh +++ b/.github/skills/security-scan-docker-image-scripts/run.sh @@ -50,7 +50,7 @@ SYFT_INSTALLED_VERSION=$(syft version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\ GRYPE_INSTALLED_VERSION=$(grype version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown") # Set defaults matching CI workflow -set_default_env "SYFT_VERSION" "v1.55.0" +set_default_env "SYFT_VERSION" "v1.56.0" set_default_env "GRYPE_VERSION" "v0.121.1" set_default_env "IMAGE_TAG" "charon:local" set_default_env "FAIL_ON_SEVERITY" "Critical,High" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 5b07fafe1..c329d9de8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -57,7 +57,7 @@ permissions: contents: read env: - SYFT_VERSION: 'v1.55.0' + SYFT_VERSION: 'v1.56.0' GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: wikid82/charon diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 798228a27..09154bb5d 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -18,7 +18,7 @@ env: GO_VERSION: '1.27.3' NODE_VERSION: '26.13.0' GOTOOLCHAIN: auto - SYFT_VERSION: 'v1.55.0' + SYFT_VERSION: 'v1.56.0' GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: wikid82/charon diff --git a/.github/workflows/supply-chain-pr.yml b/.github/workflows/supply-chain-pr.yml index 5f9110c75..b78610037 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -22,7 +22,7 @@ concurrency: cancel-in-progress: true env: - SYFT_VERSION: 'v1.55.0' + SYFT_VERSION: 'v1.56.0' permissions: contents: read diff --git a/.github/workflows/supply-chain-verify.yml b/.github/workflows/supply-chain-verify.yml index 6117ba172..31c4d5de5 100644 --- a/.github/workflows/supply-chain-verify.yml +++ b/.github/workflows/supply-chain-verify.yml @@ -15,7 +15,7 @@ on: - prereleased env: - SYFT_VERSION: 'v1.55.0' + SYFT_VERSION: 'v1.56.0' permissions: contents: read From 9429a9d74452c0f7bc3c03f5b9792f8298e23247 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 16:51:39 -0500 Subject: [PATCH 013/291] chore(deps): update tailwindcss and related packages to version 4.3.4 and add installation script --- frontend/package-lock.json | 585 ++++++--------------------- frontend/package.json | 5 +- scripts/deps/frontend/tailwindcss.sh | 18 + 3 files changed, 155 insertions(+), 453 deletions(-) create mode 100644 scripts/deps/frontend/tailwindcss.sh diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 337f8387c..a2aa81789 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -45,7 +45,8 @@ "@eslint/json": "^2.1.1", "@eslint/markdown": "^8.0.3", "@playwright/test": "^1.64.1", - "@tailwindcss/postcss": "^4.3.3", + "@tailwindcss/postcss": "^4.3.4", + "@tailwindcss/vite": "^4.3.4", "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", @@ -83,7 +84,7 @@ "knip": "^6.44.1", "playwright": "^1.64.1", "postcss": "^8.5.32", - "tailwindcss": "^4.3.3", + "tailwindcss": "^4.3.4", "typescript": "npm:@typescript/typescript6@^6.0.2", "typescript-eslint": "^8.72.1", "vite": "^8.3.5", @@ -2899,49 +2900,49 @@ "license": "MIT" }, "node_modules/@tailwindcss/node": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", - "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.4.tgz", + "integrity": "sha512-b7QK/QG04v3j04NbpG82XLCedRyulnkuwUDwRa9oK/ypnJIzMapz2Wg+0TltVczjUzDaJJHIa1+jj9aMUOFMTQ==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.24.1", + "enhanced-resolve": "^5.24.5", "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", + "lightningcss": "1.33.0", + "magic-string": "^1.1.0", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.3" + "tailwindcss": "4.3.4" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", - "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.4.tgz", + "integrity": "sha512-riqsRw2sLF7B1sNCCkTl3KYXEqvhTiABzZ/4oxxHfZ3c3Td60bCBfOrUuUGOiHagd3E7V+zG46c+XBDWZGFBcg==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-x64": "4.3.3", - "@tailwindcss/oxide-freebsd-x64": "4.3.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-x64-musl": "4.3.3", - "@tailwindcss/oxide-wasm32-wasi": "4.3.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + "@tailwindcss/oxide-android-arm64": "4.3.4", + "@tailwindcss/oxide-darwin-arm64": "4.3.4", + "@tailwindcss/oxide-darwin-x64": "4.3.4", + "@tailwindcss/oxide-freebsd-x64": "4.3.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.4", + "@tailwindcss/oxide-linux-x64-musl": "4.3.4", + "@tailwindcss/oxide-wasm32-wasi": "4.3.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.4" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", - "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.4.tgz", + "integrity": "sha512-qkjxt+EFocOi3mtsUS3ndlj6yj4SHubwBdvBynLnS8T4mvSzX0Enn0CpypJKmBEtczUAXOHVZKA2zTGzSNU2Sg==", "cpu": [ "arm64" ], @@ -2956,9 +2957,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", - "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.4.tgz", + "integrity": "sha512-i/awUA/7mJQAbA3AzCW8aQtUh5goMieUdF1zOoclLaJUCxDvsXQ8VJqpQZY3dfcHS6PJjd3tw25nNneD/mEviQ==", "cpu": [ "arm64" ], @@ -2973,9 +2974,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", - "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.4.tgz", + "integrity": "sha512-q56V9U6AnoYz/yj/alkDLhSQ9AZoXQSEl93f7WETa0+32CudbqM57BXdYXfMYPKudueXKaA+MG6L/0KRseF7Jg==", "cpu": [ "x64" ], @@ -2990,9 +2991,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", - "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.4.tgz", + "integrity": "sha512-r0W7A+Z9HTHh7bwihJl45E/zZKLjxD50/jToVF/E/RZHSl8+EWO+4yKKJpPMcUooT9hdoip3Zx1uWnbMg4N79g==", "cpu": [ "x64" ], @@ -3007,9 +3008,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", - "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.4.tgz", + "integrity": "sha512-3aih/sRxzeYfi2Bkh+bGYNf2YU9Llamb2mpA7BNF1obYzdgEeWDAMQzK6BSwzLhs/eTdOaSur92qrGfchagUtQ==", "cpu": [ "arm" ], @@ -3024,9 +3025,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", - "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.4.tgz", + "integrity": "sha512-ktJSrPGQou4OqL88rDEx5CkeM9BjzrwZvYo8u7B7VL1zkBa2XBm351o4MQrek95qikb2M++26Y7w7vbH6Mgl8g==", "cpu": [ "arm64" ], @@ -3044,9 +3045,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", - "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.4.tgz", + "integrity": "sha512-rt3hjUxT7utAtcB+df6wHyaXDyxhE7Zd1x7L/nvB5Jb2qBb8dZwFUoWqeRja1BaF5OlSqkKSjBmVXj9OsevmCQ==", "cpu": [ "arm64" ], @@ -3064,9 +3065,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", - "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.4.tgz", + "integrity": "sha512-XE3jO2yOOWjttpeGvpwf3/b274NMi3QapQSAI/EzsgrmYNan1Tw8dXT9JQkN7K0OgvuqiC8mcCyNvGzEkpVJPQ==", "cpu": [ "x64" ], @@ -3084,9 +3085,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", - "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.4.tgz", + "integrity": "sha512-PaAZ2FjJaZk9SQLYtKIfQWiFm/xtl7pX2MKPgCzn0mS/VU54yu+vP457AJdB2XFLxU3gO2abLCt8N7Lg4EbKNg==", "cpu": [ "x64" ], @@ -3103,40 +3104,10 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", - "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", - "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.4.tgz", + "integrity": "sha512-jCrwCvWVtt7ruMXp6dFPd5KBNU8o1tWtEdZbTL/w/hvD3lUYZA0E2mmfvTzW4QLHIx5wGQU9HeY7997R1QTrEg==", "cpu": [ "arm64" ], @@ -3151,9 +3122,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", - "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.4.tgz", + "integrity": "sha512-cge1wcedn1FrRSXL22lF5sNDYr8c9+VImk7zFYLPs12jkDmchSrZNfzWqOKub+5Hb/aYRdDEqERGGDKONBlOzQ==", "cpu": [ "x64" ], @@ -3168,17 +3139,32 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", - "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.4.tgz", + "integrity": "sha512-6O0QLY+1CseO8ivlYK2Y7IxNPaQ8D5ypxrV+ws34mbxknWH824J+gEWtJ0gyFiYAudmdEeXdF8kqwgXIaZRyHw==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "postcss": "^8.5.16", - "tailwindcss": "4.3.3" + "@tailwindcss/node": "4.3.4", + "@tailwindcss/oxide": "4.3.4", + "postcss": "^8.5.25", + "tailwindcss": "4.3.4" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.4.tgz", + "integrity": "sha512-wGukGWb8SVuN7+FJvSS7M5eSJWDIWwVSvUvj1q41eTIhGPsS2DAgFWjA7YURJNwjHSgty59Hz8iMUiyO+YMHBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.4", + "@tailwindcss/oxide": "4.3.4", + "tailwindcss": "4.3.4" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "node_modules/@tanstack/query-core": { @@ -4393,16 +4379,6 @@ "node": ">=22" } }, - "node_modules/@vitest/istanbul-lib-instrument/node_modules/magic-string": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", - "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0" - } - }, "node_modules/@vitest/istanbul-lib-report": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-2.1.0.tgz", @@ -4459,16 +4435,6 @@ } } }, - "node_modules/@vitest/mocker/node_modules/magic-string": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", - "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0" - } - }, "node_modules/@vitest/pretty-format": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.1.0.tgz", @@ -8158,9 +8124,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -8174,23 +8140,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -8209,9 +8175,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -8230,9 +8196,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -8251,9 +8217,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -8272,9 +8238,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -8293,9 +8259,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -8317,9 +8283,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -8341,9 +8307,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -8365,9 +8331,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -8389,9 +8355,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -8410,9 +8376,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -8539,13 +8505,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/magicast": { @@ -11307,9 +11273,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.4.tgz", + "integrity": "sha512-PCJZTFFdG+sL3gvnP1Do9o6GbkvH2q0p04xSs3f7cSvmAeK+sttG+/7ncK1fdRwKFfC7n+wDDJaOMBSefkI9ow==", "dev": true, "license": "MIT" }, @@ -11963,279 +11929,6 @@ } } }, - "node_modules/vite/node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/vite/node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/vitest": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.1.0.tgz", @@ -12319,16 +12012,6 @@ } } }, - "node_modules/vitest/node_modules/magic-string": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", - "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b6665ebfb..aadec7e7e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -65,7 +65,8 @@ "@eslint/json": "^2.1.1", "@eslint/markdown": "^8.0.3", "@playwright/test": "^1.64.1", - "@tailwindcss/postcss": "^4.3.3", + "@tailwindcss/postcss": "^4.3.4", + "@tailwindcss/vite": "^4.3.4", "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", @@ -103,7 +104,7 @@ "knip": "^6.44.1", "playwright": "^1.64.1", "postcss": "^8.5.32", - "tailwindcss": "^4.3.3", + "tailwindcss": "^4.3.4", "typescript": "npm:@typescript/typescript6@^6.0.2", "typescript-eslint": "^8.72.1", "vite": "^8.3.5", diff --git a/scripts/deps/frontend/tailwindcss.sh b/scripts/deps/frontend/tailwindcss.sh new file mode 100644 index 000000000..85272ec88 --- /dev/null +++ b/scripts/deps/frontend/tailwindcss.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + +NPM_MODULES=( + "$REPO_ROOT/frontend" +) + +for MODULE in "${NPM_MODULES[@]}"; do + echo "============================================================================" + echo "Updating: $MODULE" + echo "============================================================================" + + cd "$MODULE" || exit 1 + rm -rf node_modules package-lock.json + npm install tailwindcss @tailwindcss/postcss @tailwindcss/vite --save-dev +done From 8b7636e0ea0b64bb068a0667e79e826ddc5bf7c9 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 17:04:48 -0500 Subject: [PATCH 014/291] chore(deps): update tailwindcss installation script to preserve package-lock.json --- frontend/package-lock.json | 96 ++++++++++++++++++++++++++++ scripts/deps/frontend/tailwindcss.sh | 4 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a2aa81789..45bbddb35 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3104,6 +3104,102 @@ "node": ">= 20" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.4.tgz", + "integrity": "sha512-bjO9WmoM687p8AoCIyw1DUqzpAs4JICLvNmIPxIWDmhw8IOsdubtWQLShyJmISnDKtRy1BBApHVOUl6sNFQQJA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.3", + "@emnapi/runtime": "^1.11.3", + "@emnapi/wasi-threads": "^1.2.3", + "@napi-rs/wasm-runtime": "^1.2.2", + "@tybys/wasm-util": "^0.10.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.4.tgz", diff --git a/scripts/deps/frontend/tailwindcss.sh b/scripts/deps/frontend/tailwindcss.sh index 85272ec88..31eae7e81 100644 --- a/scripts/deps/frontend/tailwindcss.sh +++ b/scripts/deps/frontend/tailwindcss.sh @@ -13,6 +13,8 @@ for MODULE in "${NPM_MODULES[@]}"; do echo "============================================================================" cd "$MODULE" || exit 1 - rm -rf node_modules package-lock.json + # Keep the existing package-lock.json: regenerating it from scratch drops the + # optional @tailwindcss/oxide-wasm32-wasi entry, which breaks `npm ci` in Docker. + rm -rf node_modules npm install tailwindcss @tailwindcss/postcss @tailwindcss/vite --save-dev done From ec06c68e28073bff9a839f18fa3cb53c97a2ab49 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 17:40:14 +0000 Subject: [PATCH 015/291] test: add E2E spec for CrowdSec key robustness --- .../security/crowdsec-key-robustness.spec.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/security/crowdsec-key-robustness.spec.ts diff --git a/tests/security/crowdsec-key-robustness.spec.ts b/tests/security/crowdsec-key-robustness.spec.ts new file mode 100644 index 000000000..d4de23ad3 --- /dev/null +++ b/tests/security/crowdsec-key-robustness.spec.ts @@ -0,0 +1,40 @@ +/** + * CrowdSec bouncer key robustness - E2E (API level) + * + * The key-status and bouncer endpoints report how the bouncer key was checked and where it is + * held. The additive contract is: + * - `GET /admin/crowdsec/key-status` returns `key_check` (accepted | rejected | unknown | none); + * `valid` is true exactly when `key_check` is `accepted`. + * - `GET /admin/crowdsec/bouncer` returns `key_source` (env_var | file | memory | none). + * + * Written as `test.fixme` in commit 1 of the CrowdSec key robustness PR, before the + * `key_check` field and the `memory` key source exist. Commit 8 of that PR removes the fixme. + * + * @see /projects/Charon/docs/plans/crowdsec_hardening_batch5_spec.md (section 3t.5, 3t.6, 3t.14) + */ + +import { test, expect } from '@playwright/test'; + +const ADMIN = '/api/v1/admin/crowdsec'; + +const KEY_CHECK_VALUES = ['accepted', 'rejected', 'unknown', 'none']; +const KEY_SOURCE_VALUES = ['env_var', 'file', 'memory', 'none']; + +test.describe('CrowdSec key robustness: key check and key source (API)', () => { + test.fixme('key-status reports a key_check whose value agrees with valid', async ({ request }) => { + const response = await request.get(`${ADMIN}/key-status`); + expect(response.status()).toBe(200); + + const body = await response.json(); + expect(KEY_CHECK_VALUES).toContain(body.key_check); + expect(body.valid).toBe(body.key_check === 'accepted'); + }); + + test.fixme('bouncer info reports a key_source from the allowed set', async ({ request }) => { + const response = await request.get(`${ADMIN}/bouncer`); + expect(response.status()).toBe(200); + + const body = await response.json(); + expect(KEY_SOURCE_VALUES).toContain(body.key_source); + }); +}); From 12c82322c6ca13425a5a9bf7ad7dd50c670a7c31 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 18:12:31 +0000 Subject: [PATCH 016/291] refactor: prepare the CrowdSec key package for three-valued checks --- .../internal/api/handlers/crowdsec_handler.go | 16 +- .../api/handlers/crowdsec_handler_test.go | 10 +- .../crowdsec/bouncerkey/bouncerkey.go | 60 +++- .../crowdsec/bouncerkey/bouncerkey_test.go | 28 +- .../internal/crowdsec/bouncerkey/hold_test.go | 150 ++++++++++ .../internal/crowdsec/bouncerkey/verdict.go | 118 ++++++++ .../crowdsec/bouncerkey/verdict_test.go | 256 ++++++++++++++++++ backend/internal/services/crowdsec_startup.go | 11 +- 8 files changed, 633 insertions(+), 16 deletions(-) create mode 100644 backend/internal/crowdsec/bouncerkey/hold_test.go create mode 100644 backend/internal/crowdsec/bouncerkey/verdict.go create mode 100644 backend/internal/crowdsec/bouncerkey/verdict_test.go diff --git a/backend/internal/api/handlers/crowdsec_handler.go b/backend/internal/api/handlers/crowdsec_handler.go index 015dd71c3..316097233 100644 --- a/backend/internal/api/handlers/crowdsec_handler.go +++ b/backend/internal/api/handlers/crowdsec_handler.go @@ -1691,12 +1691,22 @@ type KeyStatusResponse struct { KeyFilePath string `json:"key_file_path"` } -// testKeyAgainstLAPI validates an API key by making an authenticated request to LAPI. +// testKeyAgainstLAPI reports the LAPI verdict for an API key. The probe itself is +// keyAcceptedByLAPI; a false result is mapped to VerdictRejected until the three-valued +// Check replaces it. +func (h *CrowdsecHandler) testKeyAgainstLAPI(ctx context.Context, apiKey string) bouncerkey.Verdict { + if h.keyAcceptedByLAPI(ctx, apiKey) { + return bouncerkey.VerdictAccepted + } + return bouncerkey.VerdictRejected +} + +// keyAcceptedByLAPI validates an API key by making an authenticated request to LAPI. // Uses /v1/decisions/stream endpoint which requires authentication. // Returns true if the key is accepted (200 OK), false otherwise. // Implements retry logic with exponential backoff for LAPI startup (connection refused). // Fails fast on 403 Forbidden (invalid key - no retries). -func (h *CrowdsecHandler) testKeyAgainstLAPI(ctx context.Context, apiKey string) bool { +func (h *CrowdsecHandler) keyAcceptedByLAPI(ctx context.Context, apiKey string) bool { if apiKey == "" { return false } @@ -1892,7 +1902,7 @@ func ignoredEnvKeyMessage(keyPath string) string { func (h *CrowdsecHandler) keyStillValid(c *gin.Context, key string) bool { ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() - return h.testKeyAgainstLAPI(ctx, key) + return h.testKeyAgainstLAPI(ctx, key) == bouncerkey.VerdictAccepted } // ensureBouncerRegistration decides which bouncer key to use and makes the key file match it. diff --git a/backend/internal/api/handlers/crowdsec_handler_test.go b/backend/internal/api/handlers/crowdsec_handler_test.go index 84c2c5cd3..837af2caf 100644 --- a/backend/internal/api/handlers/crowdsec_handler_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_test.go @@ -3884,7 +3884,7 @@ func TestTestKeyAgainstLAPI_ValidKey(t *testing.T) { ctx := context.Background() result := handler.testKeyAgainstLAPI(ctx, "valid-key-123") - require.True(t, result, "Valid key should return true") + require.Equal(t, bouncerkey.VerdictAccepted, result, "Valid key should be accepted") } // TestTestKeyAgainstLAPI_InvalidKey verifies that invalid API keys are rejected by LAPI. @@ -3916,7 +3916,7 @@ func TestTestKeyAgainstLAPI_InvalidKey(t *testing.T) { ctx := context.Background() result := handler.testKeyAgainstLAPI(ctx, "invalid-key-456") - require.False(t, result, "Invalid key should return false immediately (no retries)") + require.Equal(t, bouncerkey.VerdictRejected, result, "Invalid key should be rejected immediately (no retries)") } // TestTestKeyAgainstLAPI_EmptyKey verifies that empty keys are rejected without making requests. @@ -3929,7 +3929,7 @@ func TestTestKeyAgainstLAPI_EmptyKey(t *testing.T) { ctx := context.Background() result := handler.testKeyAgainstLAPI(ctx, "") - require.False(t, result, "Empty key should return false without making request") + require.Equal(t, bouncerkey.VerdictRejected, result, "Empty key should be rejected without making request") } // TestTestKeyAgainstLAPI_Timeout verifies that LAPI requests timeout appropriately. @@ -3957,7 +3957,7 @@ func TestTestKeyAgainstLAPI_Timeout(t *testing.T) { ctx := context.Background() result := handler.testKeyAgainstLAPI(ctx, "test-key") - require.False(t, result, "Should return false after timeout") + require.Equal(t, bouncerkey.VerdictRejected, result, "Should be rejected after timeout") } // TestTestKeyAgainstLAPI_NonOKStatus verifies that non-200/403 status codes are handled. @@ -3987,7 +3987,7 @@ func TestTestKeyAgainstLAPI_NonOKStatus(t *testing.T) { ctx := context.Background() result := handler.testKeyAgainstLAPI(ctx, "test-key") - require.False(t, result, "Should return false for non-OK status") + require.Equal(t, bouncerkey.VerdictRejected, result, "Should be rejected for non-OK status") } // TestEnsureBouncerRegistration_ValidEnvKey verifies that valid environment keys are used. diff --git a/backend/internal/crowdsec/bouncerkey/bouncerkey.go b/backend/internal/crowdsec/bouncerkey/bouncerkey.go index ea56543c5..217b400af 100644 --- a/backend/internal/crowdsec/bouncerkey/bouncerkey.go +++ b/backend/internal/crowdsec/bouncerkey/bouncerkey.go @@ -9,12 +9,61 @@ import ( "context" "fmt" "os" + "path/filepath" "strings" + "sync" ) // DefaultFile is the saved bouncer key location inside the container. const DefaultFile = "/app/data/crowdsec/bouncer_key" +// PathEnvName is the environment variable that overrides the key file location. +const PathEnvName = "CHARON_CROWDSEC_BOUNCER_KEY_PATH" + +// keyFileName is the key file name inside a data directory. +const keyFileName = "bouncer_key" + +// PathFor returns the key file location: the PathEnvName override when set, else +// /bouncer_key when dataDir is set, else DefaultFile. +func PathFor(dataDir string) string { + if override := strings.TrimSpace(os.Getenv(PathEnvName)); override != "" { + return filepath.Clean(override) + } + if dataDir != "" { + return filepath.Join(dataDir, keyFileName) + } + return DefaultFile +} + +// heldKeys maps a key file path to a key that could not be saved to that path. +// It lives for the process; see Hold. +var ( + heldMu sync.RWMutex + heldKeys = map[string]string{} +) + +// Hold remembers key for path after a save to path failed, so readers in this +// process keep using the key that CrowdSec accepted. +func Hold(path, key string) { + heldMu.Lock() + defer heldMu.Unlock() + heldKeys[path] = key +} + +// Held returns the key held for path, or "" when none is held. +func Held(path string) string { + heldMu.RLock() + defer heldMu.RUnlock() + return heldKeys[path] +} + +// Release drops any key held for path. +func Release(path string) { + heldMu.Lock() + defer heldMu.Unlock() + delete(heldKeys, path) +} + // envNames lists the environment variables that may carry the bouncer key, // in lookup order (the first non-empty value wins). var envNames = [...]string{ @@ -63,6 +112,8 @@ const ( SourceEnv Source = "env" // SourceNone means no key is available. SourceNone Source = "none" + // SourceMemory means the key is held in memory because it could not be saved. + SourceMemory Source = "memory" ) // Resolve returns the key the proxy uses: the saved key file first, then the @@ -110,6 +161,9 @@ type EnsureResult struct { // Ensure decides which bouncer key to use and makes the key file match it. // +// Only a VerdictAccepted from validate keeps or adopts a key. VerdictRejected and +// VerdictUnknown are both treated as "not accepted" for now. +// // Rules, in order: // 1. A saved key that validates is kept. The env value is not tested. // 2. Otherwise an env value that differs from the saved key and validates is saved. @@ -122,7 +176,7 @@ type EnsureResult struct { func Ensure( ctx context.Context, path string, - validate func(context.Context, string) bool, + validate func(context.Context, string) Verdict, register func(context.Context) (string, error), save func(path, key string) error, ) (EnsureResult, error) { @@ -130,14 +184,14 @@ func Ensure( saved := ReadFile(path) res := EnsureResult{EnvKey: env} - if saved != "" && validate(ctx, saved) { + if saved != "" && validate(ctx, saved) == VerdictAccepted { res.Key = saved res.Outcome = OutcomeSavedKeyValid res.EnvKeyIgnored = env != "" && env != saved return res, nil } - if env != "" && env != saved && validate(ctx, env) { + if env != "" && env != saved && validate(ctx, env) == VerdictAccepted { res.Key = env res.Outcome = OutcomeEnvKeySaved res.SaveErr = save(path, env) diff --git a/backend/internal/crowdsec/bouncerkey/bouncerkey_test.go b/backend/internal/crowdsec/bouncerkey/bouncerkey_test.go index ea9e919f6..36647bf07 100644 --- a/backend/internal/crowdsec/bouncerkey/bouncerkey_test.go +++ b/backend/internal/crowdsec/bouncerkey/bouncerkey_test.go @@ -167,6 +167,7 @@ const ( // fakeLAPI records every validate, register and save call Ensure makes. type fakeLAPI struct { valid map[string]bool + unknown map[string]bool validated []string registers int regErr error @@ -178,9 +179,32 @@ func newFakeLAPI() *fakeLAPI { return &fakeLAPI{valid: map[string]bool{testSavedValid: true, testEnvValid: true}} } -func (f *fakeLAPI) validate(_ context.Context, key string) bool { +func (f *fakeLAPI) validate(_ context.Context, key string) Verdict { f.validated = append(f.validated, key) - return f.valid[key] + switch { + case f.valid[key]: + return VerdictAccepted + case f.unknown[key]: + return VerdictUnknown + default: + return VerdictRejected + } +} + +// TestEnsureUnknownVerdictFollowsRejectedPath pins the interim rule of the +// three-valued refactor: until Ensure learns about Unknown, an unknown saved key +// takes the same path as a rejected one (register). The next change replaces this. +func TestEnsureUnknownVerdictFollowsRejectedPath(t *testing.T) { + path := seedKeyFile(t, testSavedInvalid) + setOnlyEnvKey(t, "") + fake := newFakeLAPI() + fake.unknown = map[string]bool{testSavedInvalid: true} + + res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.Equal(t, OutcomeRegistered, res.Outcome) + require.Equal(t, 1, fake.registers) } func (f *fakeLAPI) register(_ context.Context) (string, error) { diff --git a/backend/internal/crowdsec/bouncerkey/hold_test.go b/backend/internal/crowdsec/bouncerkey/hold_test.go new file mode 100644 index 000000000..b5ab23890 --- /dev/null +++ b/backend/internal/crowdsec/bouncerkey/hold_test.go @@ -0,0 +1,150 @@ +package bouncerkey + +import ( + "fmt" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSourceMemoryName(t *testing.T) { + t.Parallel() + + require.Equal(t, Source("memory"), SourceMemory) +} + +func TestHeldEmptyWhenNothingHeld(t *testing.T) { + t.Parallel() + + require.Empty(t, Held(filepath.Join(t.TempDir(), "bouncer_key"))) +} + +func TestHoldThenHeldThenRelease(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "bouncer_key") + + Hold(path, testRegistered) + require.Equal(t, testRegistered, Held(path)) + + Release(path) + require.Empty(t, Held(path)) +} + +func TestHoldReplacesPreviousKey(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "bouncer_key") + + Hold(path, testEnvValid) + Hold(path, testRegistered) + + require.Equal(t, testRegistered, Held(path)) +} + +// TestHeldIsKeyedByPath checks that a key held for one path is invisible to another. +func TestHeldIsKeyedByPath(t *testing.T) { + t.Parallel() + + first := filepath.Join(t.TempDir(), "bouncer_key") + second := filepath.Join(t.TempDir(), "bouncer_key") + + Hold(first, testRegistered) + t.Cleanup(func() { Release(first) }) + + require.Equal(t, testRegistered, Held(first)) + require.Empty(t, Held(second)) + + Release(second) + require.Equal(t, testRegistered, Held(first), "releasing another path must not drop this key") +} + +// TestHoldReleaseConcurrent runs Hold, Held and Release from many goroutines on +// the same and on different paths. It must pass under -race and never return a +// value that was not held. +func TestHoldReleaseConcurrent(t *testing.T) { + t.Parallel() + + const workers = 16 + const rounds = 200 + dir := t.TempDir() + shared := filepath.Join(dir, "shared_key") + + var wg sync.WaitGroup + for w := range workers { + own := filepath.Join(dir, fmt.Sprintf("own_%d", w)) + want := fmt.Sprintf("concurrent-key-%02d-0000000000000000", w) + wg.Add(1) + go func() { + defer wg.Done() + for range rounds { + Hold(shared, want) + // Other workers hold the shared path too, so the value is any worker's key or + // empty (released); it must never be a value nobody wrote. + if got := Held(shared); got != "" && !strings.HasPrefix(got, "concurrent-key-") { + t.Errorf("shared path returned a foreign value %q", got) + } + Hold(own, want) + if got := Held(own); got != want { + t.Errorf("own path returned %q, want %q", got, want) + } + Release(own) + if got := Held(own); got != "" { + t.Errorf("own path still holds %q after Release", got) + } + Release(shared) + } + }() + } + wg.Wait() + + require.Empty(t, Held(shared)) +} + +func TestPathForOverrideWins(t *testing.T) { + t.Setenv(PathEnvName, " /custom/dir/../bouncer_key ") + + require.Equal(t, "/custom/bouncer_key", PathFor("/app/data/crowdsec")) +} + +func TestPathForOverrideWithoutDataDir(t *testing.T) { + t.Setenv(PathEnvName, "/custom/bouncer_key") + + require.Equal(t, "/custom/bouncer_key", PathFor("")) +} + +func TestPathForBlankOverrideFallsBackToDataDir(t *testing.T) { + t.Setenv(PathEnvName, " ") + + require.Equal(t, "/app/data/crowdsec/bouncer_key", PathFor("/app/data/crowdsec")) +} + +func TestPathForDataDir(t *testing.T) { + t.Setenv(PathEnvName, "") + + tests := []struct { + name string + dataDir string + want string + }{ + {name: "absolute dir", dataDir: "/app/data/crowdsec", want: "/app/data/crowdsec/bouncer_key"}, + {name: "trailing slash", dataDir: "/srv/cs/", want: "/srv/cs/bouncer_key"}, + {name: "relative dir", dataDir: "data/crowdsec", want: "data/crowdsec/bouncer_key"}, + {name: "relative dir with trailing slash", dataDir: "data/crowdsec/", want: "data/crowdsec/bouncer_key"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, filepath.FromSlash(tt.want), PathFor(tt.dataDir)) + }) + } +} + +func TestPathForNoDataDirUsesDefault(t *testing.T) { + t.Setenv(PathEnvName, "") + + require.Equal(t, DefaultFile, PathFor("")) +} diff --git a/backend/internal/crowdsec/bouncerkey/verdict.go b/backend/internal/crowdsec/bouncerkey/verdict.go new file mode 100644 index 000000000..c8a2d177b --- /dev/null +++ b/backend/internal/crowdsec/bouncerkey/verdict.go @@ -0,0 +1,118 @@ +package bouncerkey + +import ( + "context" + "io" + "net/http" + "time" +) + +// Verdict is the three-valued result of asking CrowdSec LAPI whether a key is accepted. +// +// Only a definite answer may change saved state: Accepted keeps the key, Rejected +// allows a new bouncer to be registered. Unknown means the answer could not be +// obtained (LAPI down, slow, erroring or the context was cancelled) and must never +// be read as a rejection. +type Verdict int + +const ( + // VerdictUnknown is the zero value. An unfilled Verdict is never a rejection. + VerdictUnknown Verdict = iota + // VerdictAccepted means LAPI answered 200 for the key. + VerdictAccepted + // VerdictRejected means LAPI answered 403 for the key. + VerdictRejected +) + +// Doer sends an HTTP request. *http.Client satisfies it. +type Doer interface { + Do(*http.Request) (*http.Response, error) +} + +// CheckOptions bounds a key check. +type CheckOptions struct { + // BaseURL is the LAPI base URL without the endpoint path. + BaseURL string + // Wait is the total time budget, including retries. + Wait time.Duration + // PerAttempt caps a single request. + PerAttempt time.Duration + // FirstBackoff is the sleep after the first failed attempt; it doubles up to MaxBackoff. + FirstBackoff time.Duration + // MaxBackoff caps the sleep between attempts. + MaxBackoff time.Duration +} + +// streamPath is the LAPI endpoint used for key checks. It requires authentication. +const streamPath = "/v1/decisions/stream" + +// maxDrainBytes bounds how much of an unused response body is read before it is closed. +const maxDrainBytes = 64 << 10 + +// Check asks LAPI whether key is accepted. +// +// Only 403 is a definite rejection; 200 is an acceptance. Every other outcome +// (any other status including 401 and 3xx, transport errors and per-attempt +// timeouts) is retried with backoff until opts.Wait is used up, then reported as +// VerdictUnknown. A cancelled ctx is VerdictUnknown at once. An empty key is +// VerdictRejected without a request. Check never logs the key. +func Check(ctx context.Context, doer Doer, key string, opts CheckOptions) Verdict { + if key == "" { + return VerdictRejected + } + + deadline := time.Now().Add(opts.Wait) + backoff := opts.FirstBackoff + for { + verdict := checkAttempt(ctx, doer, key, opts) + if verdict != VerdictUnknown { + return verdict + } + if ctx.Err() != nil { + return VerdictUnknown + } + + remaining := time.Until(deadline) + if remaining <= 0 { + return VerdictUnknown + } + select { + case <-ctx.Done(): + return VerdictUnknown + case <-time.After(min(backoff, remaining)): + } + backoff = min(backoff*2, opts.MaxBackoff) + } +} + +// checkAttempt makes one request. It returns VerdictUnknown when the attempt +// gave no definite answer and should be retried. +func checkAttempt(ctx context.Context, doer Doer, key string, opts CheckOptions) Verdict { + attemptCtx, cancel := context.WithTimeout(ctx, opts.PerAttempt) + defer cancel() + + req, err := http.NewRequestWithContext(attemptCtx, http.MethodGet, opts.BaseURL+streamPath, http.NoBody) + if err != nil { + return VerdictUnknown + } + req.Header.Set("X-Api-Key", key) + + resp, err := doer.Do(req) //nolint:gosec // G704: BaseURL comes from trusted config, not user input + if err != nil { + return VerdictUnknown + } + defer func() { + // Drain a bounded amount so the connection can be reused, then close. + _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, maxDrainBytes)) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return VerdictAccepted + case http.StatusForbidden: + return VerdictRejected + default: + return VerdictUnknown + } +} diff --git a/backend/internal/crowdsec/bouncerkey/verdict_test.go b/backend/internal/crowdsec/bouncerkey/verdict_test.go new file mode 100644 index 000000000..3f3b64850 --- /dev/null +++ b/backend/internal/crowdsec/bouncerkey/verdict_test.go @@ -0,0 +1,256 @@ +package bouncerkey + +import ( + "context" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// testCheckKey is the key every Check test sends. It is a fixed, non-secret value. +const testCheckKey = "check-test-key-0000000000000001" + +// newCheckDoer returns an HTTP client that returns redirects to the caller +// instead of following them, the same way the shared internal client does. +func newCheckDoer() *http.Client { + return &http.Client{ + Timeout: 2 * time.Second, + CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} + +// fastCheckOptions keeps the retry window short so tests stay quick. +func fastCheckOptions(baseURL string) CheckOptions { + return CheckOptions{ + BaseURL: baseURL, + Wait: 300 * time.Millisecond, + PerAttempt: 100 * time.Millisecond, + FirstBackoff: 5 * time.Millisecond, + MaxBackoff: 20 * time.Millisecond, + } +} + +// statusServer answers every request with status and counts the requests it receives. +func statusServer(t *testing.T, status int, requests *atomic.Int32) *httptest.Server { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests.Add(1) + w.WriteHeader(status) + })) + t.Cleanup(srv.Close) + return srv +} + +func TestVerdictZeroValueIsUnknown(t *testing.T) { + t.Parallel() + + var v Verdict + require.Equal(t, VerdictUnknown, v) + require.NotEqual(t, VerdictRejected, v) + require.NotEqual(t, VerdictAccepted, v) +} + +func TestCheckAcceptsOn200AndSendsKeyHeader(t *testing.T) { + t.Parallel() + + var got atomic.Value + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got.Store(r.Method + " " + r.URL.Path + " " + r.Header.Get("X-Api-Key")) + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictAccepted, verdict) + require.Equal(t, "GET /v1/decisions/stream "+testCheckKey, got.Load()) +} + +func TestCheckRejectsOn403WithoutRetry(t *testing.T) { + t.Parallel() + + var requests atomic.Int32 + srv := statusServer(t, http.StatusForbidden, &requests) + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictRejected, verdict) + require.Equal(t, int32(1), requests.Load()) +} + +// TestCheckUnauthorizedIsNotRejected pins the 401 mapping: only 403 is a definite rejection. +func TestCheckUnauthorizedIsNotRejected(t *testing.T) { + t.Parallel() + + var requests atomic.Int32 + srv := statusServer(t, http.StatusUnauthorized, &requests) + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictUnknown, verdict) + require.Greater(t, requests.Load(), int32(1), "401 must be retried inside the window") +} + +func TestCheckRetriesServerErrorThenAccepts(t *testing.T) { + t.Parallel() + + var requests atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if requests.Add(1) == 1 { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictAccepted, verdict) + require.Equal(t, int32(2), requests.Load()) +} + +func TestCheckServerErrorUntilWindowEndsIsUnknown(t *testing.T) { + t.Parallel() + + var requests atomic.Int32 + srv := statusServer(t, http.StatusInternalServerError, &requests) + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictUnknown, verdict) + require.Greater(t, requests.Load(), int32(1)) +} + +func TestCheckRateLimitedIsUnknown(t *testing.T) { + t.Parallel() + + var requests atomic.Int32 + srv := statusServer(t, http.StatusTooManyRequests, &requests) + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictUnknown, verdict) + require.Greater(t, requests.Load(), int32(1)) +} + +// TestCheckRedirectIsUnknown pins that a 3xx is an "other status": the client does +// not follow it, so the check retries and ends Unknown instead of trusting the target. +func TestCheckRedirectIsUnknown(t *testing.T) { + t.Parallel() + + var requests atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests.Add(1) + w.Header().Set("Location", "/elsewhere") + w.WriteHeader(http.StatusFound) + })) + t.Cleanup(srv.Close) + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictUnknown, verdict) + require.Greater(t, requests.Load(), int32(1)) +} + +func TestCheckRefusedConnectionIsUnknown(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + baseURL := srv.URL + srv.Close() // nothing listens any more: connection refused + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(baseURL)) + + require.Equal(t, VerdictUnknown, verdict) +} + +func TestCheckPerAttemptTimeoutIsUnknown(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + case <-time.After(2 * time.Second): + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + + start := time.Now() + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictUnknown, verdict, "a slow LAPI must not be read as a 200 or a 403") + require.Less(t, time.Since(start), 2*time.Second, "the per-attempt timeout bounds each try") +} + +func TestCheckConnectionResetIsUnknown(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + conn, _, err := w.(http.Hijacker).Hijack() + if err == nil { + _ = conn.Close() + } + })) + t.Cleanup(srv.Close) + + verdict := Check(context.Background(), newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictUnknown, verdict) +} + +func TestCheckCancelledBeforeStartIsUnknownWithoutRequest(t *testing.T) { + t.Parallel() + + var requests atomic.Int32 + srv := statusServer(t, http.StatusForbidden, &requests) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + verdict := Check(ctx, newCheckDoer(), testCheckKey, fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictUnknown, verdict, "a cancelled context is never evidence about the key") + require.Zero(t, requests.Load()) +} + +func TestCheckCancelledDuringAttemptIsUnknown(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + case <-time.After(2 * time.Second): + } + w.WriteHeader(http.StatusForbidden) + })) + t.Cleanup(srv.Close) + + ctx, cancel := context.WithCancel(context.Background()) + time.AfterFunc(50*time.Millisecond, cancel) + + opts := fastCheckOptions(srv.URL) + opts.Wait = 5 * time.Second + opts.PerAttempt = 5 * time.Second + verdict := Check(ctx, newCheckDoer(), testCheckKey, opts) + + require.Equal(t, VerdictUnknown, verdict, "a cancelled check must not report Rejected") +} + +func TestCheckEmptyKeyIsRejectedWithoutRequest(t *testing.T) { + t.Parallel() + + var requests atomic.Int32 + srv := statusServer(t, http.StatusOK, &requests) + + verdict := Check(context.Background(), newCheckDoer(), "", fastCheckOptions(srv.URL)) + + require.Equal(t, VerdictRejected, verdict) + require.Zero(t, requests.Load()) +} diff --git a/backend/internal/services/crowdsec_startup.go b/backend/internal/services/crowdsec_startup.go index dc016f0bf..cfb704f63 100644 --- a/backend/internal/services/crowdsec_startup.go +++ b/backend/internal/services/crowdsec_startup.go @@ -362,9 +362,14 @@ func ensureBouncerRegistration(dataDir, bouncerKeyFile string, cmdExec CommandEx return err } -// startupKeyValid tests a key against LAPI for the shared decision. -func startupKeyValid(_ context.Context, key string) bool { - return testKeyAgainstLAPIStartup(key) +// startupKeyValid tests a key against LAPI for the shared decision. The probe is +// testKeyAgainstLAPIStartup; a false result is mapped to VerdictRejected until the +// three-valued Check replaces it. +func startupKeyValid(_ context.Context, key string) bouncerkey.Verdict { + if testKeyAgainstLAPIStartup(key) { + return bouncerkey.VerdictAccepted + } + return bouncerkey.VerdictRejected } // rejectedEnvKeyLogMessage is the Warn text for a rejected key variable whose replacement was From 0340e880b26e6caa42c18cb369f37fd1bd54f44d Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 18:49:52 +0000 Subject: [PATCH 017/291] fix(security): harden log output in the CrowdSec integration --- .../internal/api/handlers/crowdsec_handler.go | 25 ++-- .../api/handlers/crowdsec_handler_test.go | 2 +- .../api/handlers/crowdsec_key_log_test.go | 4 +- .../api/handlers/crowdsec_log_capture_test.go | 110 ++++++++++++++++++ backend/internal/services/crowdsec_startup.go | 15 +-- .../services/crowdsec_startup_bouncer_test.go | 4 +- .../crowdsec_startup_log_capture_test.go | 109 +++++++++++++++++ backend/internal/util/mask.go | 6 +- 8 files changed, 243 insertions(+), 32 deletions(-) create mode 100644 backend/internal/api/handlers/crowdsec_log_capture_test.go create mode 100644 backend/internal/services/crowdsec_startup_log_capture_test.go diff --git a/backend/internal/api/handlers/crowdsec_handler.go b/backend/internal/api/handlers/crowdsec_handler.go index 316097233..45ed91a5d 100644 --- a/backend/internal/api/handlers/crowdsec_handler.go +++ b/backend/internal/api/handlers/crowdsec_handler.go @@ -1801,18 +1801,18 @@ func (h *CrowdsecHandler) keyAcceptedByLAPI(ctx context.Context, apiKey string) // Check response status if statusCode == http.StatusOK { - logger.Log().WithField("attempts", attempt).WithField("elapsed", time.Since(startTime)).WithField("masked_key", util.MaskAPIKey(apiKey)).Debug("API key validated successfully against LAPI") + logger.Log().WithField("attempts", attempt).WithField("elapsed", time.Since(startTime)).Debug("API key validated successfully against LAPI") return true } // 403 Forbidden = bad key, fail fast (no retries) if statusCode == http.StatusForbidden { - logger.Log().WithField("status", statusCode).WithField("masked_key", util.MaskAPIKey(apiKey)).Debug("API key rejected by LAPI (403 Forbidden)") + logger.Log().WithField("status", statusCode).Debug("API key rejected by LAPI (403 Forbidden)") return false } // Other non-OK status codes - logger.Log().WithField("status", statusCode).WithField("masked_key", util.MaskAPIKey(apiKey)).Debug("API key validation returned unexpected status") + logger.Log().WithField("status", statusCode).Debug("API key validation returned unexpected status") return false } } @@ -1936,14 +1936,14 @@ func (h *CrowdsecHandler) handleBouncerRegistration(res bouncerkey.EnsureResult, // The rejection is logged only with the outcome it led to: a failed registration keeps no new key. if regErr != nil { if res.EnvKeyRejected { - logger.Log().WithField("masked_key", util.MaskAPIKey(res.EnvKey)).Warn(rejectedEnvRegisterFailedLogMessage(keyPath)) + logger.Log().Warn(rejectedEnvRegisterFailedLogMessage(keyPath)) } logger.Log().WithError(regErr).Warn("Failed to register bouncer, CrowdSec may not enforce decisions") return } if res.EnvKeyRejected { - logger.Log().WithField("masked_key", util.MaskAPIKey(res.EnvKey)).Warn(rejectedEnvLogMessage(keyPath, res.SaveErr)) + logger.Log().Warn(rejectedEnvLogMessage(keyPath, res.SaveErr)) } if res.SaveErr != nil { @@ -1952,7 +1952,7 @@ func (h *CrowdsecHandler) handleBouncerRegistration(res bouncerkey.EnsureResult, switch res.Outcome { case bouncerkey.OutcomeRegistered: - h.logBouncerKeyBanner(res.Key, res.SaveErr) + h.logBouncerKeyBanner(res.SaveErr) case bouncerkey.OutcomeEnvKeySaved: if res.SaveErr == nil { logger.Log().WithField("file", keyPath).Info("CrowdSec bouncer: env var key validated and saved to key file") @@ -2068,10 +2068,10 @@ func validateAPIKeyFormat(key string) bool { return true } -// logBouncerKeyBanner logs the bouncer key with a formatted banner. When saveErr is set the key -// was not written to the key file, so the banner says CrowdSec blocking depends on the file becoming writable. -// Security: API key is masked to prevent exposure in logs (CWE-312). -func (h *CrowdsecHandler) logBouncerKeyBanner(apiKey string, saveErr error) { +// logBouncerKeyBanner logs the registration banner. It takes no key: the key is never logged. +// When saveErr is set the key was not written to the key file, so the banner says CrowdSec +// blocking depends on the file becoming writable. +func (h *CrowdsecHandler) logBouncerKeyBanner(saveErr error) { keyPath := h.bouncerKeyPath() storageLine := "Saved To: " + keyPath @@ -2090,16 +2090,13 @@ func (h *CrowdsecHandler) logBouncerKeyBanner(apiKey string, saveErr error) { 🔐 CrowdSec Bouncer Registered Successfully ──────────────────────────────────────────────────────────────────── Bouncer Name: %s -API Key: %s %s ──────────────────────────────────────────────────────────────────── %s %s 🔄 ROTATE: Change API keys regularly and never commit to version control ════════════════════════════════════════════════════════════════════` - // Security: Mask API key to prevent cleartext exposure in logs - maskedKey := util.MaskAPIKey(apiKey) - logger.Log().Infof(banner, bouncerName, maskedKey, storageLine, securityLine, tipLine) + logger.Log().Infof(banner, bouncerName, storageLine, securityLine, tipLine) } // GetBouncerInfo returns information about the current bouncer key. diff --git a/backend/internal/api/handlers/crowdsec_handler_test.go b/backend/internal/api/handlers/crowdsec_handler_test.go index 837af2caf..34457269f 100644 --- a/backend/internal/api/handlers/crowdsec_handler_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_test.go @@ -3849,7 +3849,7 @@ func TestLogBouncerKeyBanner_NoSecretExposure(t *testing.T) { require.Contains(t, maskedKey, "...wxyz") // Call the banner function (it will log, but we've verified masking works) - h.logBouncerKeyBanner(testAPIKey, nil) + h.logBouncerKeyBanner(nil) // If we reach here without panic, the function executed successfully // The actual log output would need to be captured in integration tests diff --git a/backend/internal/api/handlers/crowdsec_key_log_test.go b/backend/internal/api/handlers/crowdsec_key_log_test.go index 79326f2f6..f0d236530 100644 --- a/backend/internal/api/handlers/crowdsec_key_log_test.go +++ b/backend/internal/api/handlers/crowdsec_key_log_test.go @@ -107,7 +107,7 @@ func TestLogBouncerKeyBanner_SaveSucceededNamesSavedPath(t *testing.T) { buf := captureLogs(t) h := newPrecedenceHandler(t, "http://127.0.0.1:1", t.TempDir(), noCSCLIMock()) - h.logBouncerKeyBanner(precNewKey, nil) + h.logBouncerKeyBanner(nil) out := buf.String() assert.Contains(t, out, "Saved To: "+h.bouncerKeyPath()) @@ -120,7 +120,7 @@ func TestLogBouncerKeyBanner_SaveFailedSaysSessionOnly(t *testing.T) { buf := captureLogs(t) h := newPrecedenceHandler(t, "http://127.0.0.1:1", t.TempDir(), noCSCLIMock()) - h.logBouncerKeyBanner(precNewKey, errors.New("disk full")) + h.logBouncerKeyBanner(errors.New("disk full")) out := buf.String() assert.Contains(t, out, "could not save to "+h.bouncerKeyPath()) diff --git a/backend/internal/api/handlers/crowdsec_log_capture_test.go b/backend/internal/api/handlers/crowdsec_log_capture_test.go new file mode 100644 index 000000000..31bbb808e --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_log_capture_test.go @@ -0,0 +1,110 @@ +package handlers + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" + "github.com/Wikid82/charon/backend/internal/logger" +) + +// logCaptureFullKey is a 32-character key made of distinct characters, so every +// 4-character window of it is unique and a leaked fragment is detectable. +const logCaptureFullKey = "Qz7kP2mV9xR4tN6bL1wH8sJ3cF5gD0aY" + +// captureDebugLogs routes the global logger to a buffer at Debug level and +// subscribes a listener on the broadcast hook. It returns the buffer and the +// listener channel so a test can prove Debug entries were delivered. +func captureDebugLogs(t *testing.T) (buf *bytes.Buffer, entries <-chan *logrus.Entry) { + t.Helper() + buf = &bytes.Buffer{} + logger.Init(true, buf) + hook := logger.GetBroadcastHook() + id := t.Name() + entries = hook.Subscribe(id) + t.Cleanup(func() { + hook.Unsubscribe(id) + logger.Init(false, io.Discard) + }) + return buf, entries +} + +// lapiStatusServer answers every request with the given status code. +func lapiStatusServer(t *testing.T, status int) *httptest.Server { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(status) + })) + t.Cleanup(srv.Close) + return srv +} + +// TestCrowdSecLogsCarryNoKeyText runs every key-bearing log path at Debug level +// and fails if any 4-character window of the key appears in the captured output. +func TestCrowdSecLogsCarryNoKeyText(t *testing.T) { + buf, entries := captureDebugLogs(t) + + t.Run("LAPI key check", func(t *testing.T) { + for _, status := range []int{http.StatusOK, http.StatusForbidden, http.StatusInternalServerError} { + srv := lapiStatusServer(t, status) + h := newPrecedenceHandler(t, srv.URL, t.TempDir(), noCSCLIMock()) + h.testKeyAgainstLAPI(context.Background(), logCaptureFullKey) + } + }) + + t.Run("bouncer banner", func(t *testing.T) { + h := newPrecedenceHandler(t, "http://127.0.0.1:1", t.TempDir(), noCSCLIMock()) + h.logBouncerKeyBanner(nil) + h.logBouncerKeyBanner(errors.New("disk full")) + }) + + t.Run("registration outcomes", func(t *testing.T) { + h := newPrecedenceHandler(t, "http://127.0.0.1:1", t.TempDir(), noCSCLIMock()) + h.handleBouncerRegistration(bouncerkey.EnsureResult{ + Key: logCaptureFullKey, + Outcome: bouncerkey.OutcomeRegistered, + EnvKey: logCaptureFullKey, + EnvKeyRejected: true, + }, nil) + h.handleBouncerRegistration(bouncerkey.EnsureResult{ + Key: logCaptureFullKey, + Outcome: bouncerkey.OutcomeRegistered, + EnvKey: logCaptureFullKey, + EnvKeyRejected: true, + SaveErr: errors.New("disk full"), + }, nil) + h.handleBouncerRegistration(bouncerkey.EnsureResult{ + EnvKey: logCaptureFullKey, + EnvKeyRejected: true, + }, errors.New("bouncer registration failed")) + }) + + t.Run("hook received a debug entry", func(t *testing.T) { + var sawDebug bool + for drained := false; !drained; { + select { + case e := <-entries: + if e.Level == logrus.DebugLevel { + sawDebug = true + } + default: + drained = true + } + } + require.True(t, sawDebug, "the capture hook must observe Debug entries") + }) + + out := buf.String() + for i := 0; i+4 <= len(logCaptureFullKey); i++ { + assert.NotContains(t, out, logCaptureFullKey[i:i+4], "log output carries key text at offset %d", i) + } +} diff --git a/backend/internal/services/crowdsec_startup.go b/backend/internal/services/crowdsec_startup.go index cfb704f63..e13c77d20 100644 --- a/backend/internal/services/crowdsec_startup.go +++ b/backend/internal/services/crowdsec_startup.go @@ -399,9 +399,8 @@ func logStartupKeyDecision(res bouncerkey.EnsureResult, keyFile string, regErr e } if res.EnvKeyRejected { fields := map[string]any{ - "bouncer": startupBouncerName, - "key_file": keyFile, - "masked_key": util.MaskAPIKey(res.EnvKey), + "bouncer": startupBouncerName, + "key_file": keyFile, } if regErr != nil { logger.Log().WithFields(fields).Warn(rejectedEnvRegisterFailedLogMessage(keyFile)) @@ -422,17 +421,15 @@ func logStartupKeyDecision(res bouncerkey.EnsureResult, keyFile string, regErr e case bouncerkey.OutcomeEnvKeySaved: if res.SaveErr == nil { logger.Log().WithFields(map[string]any{ - "source": "environment_variable", - "key_file": keyFile, - "masked_key": util.MaskAPIKey(res.Key), + "source": "environment_variable", + "key_file": keyFile, }).Info("CrowdSec bouncer: env var key validated and saved to key file") } case bouncerkey.OutcomeRegistered: if res.SaveErr == nil { logger.Log().WithFields(map[string]any{ - "bouncer": startupBouncerName, - "key_file": keyFile, - "masked_key": util.MaskAPIKey(res.Key), + "bouncer": startupBouncerName, + "key_file": keyFile, }).Info("CrowdSec bouncer: successfully registered and saved key") } } diff --git a/backend/internal/services/crowdsec_startup_bouncer_test.go b/backend/internal/services/crowdsec_startup_bouncer_test.go index 48fa73faf..32d49cfa0 100644 --- a/backend/internal/services/crowdsec_startup_bouncer_test.go +++ b/backend/internal/services/crowdsec_startup_bouncer_test.go @@ -20,7 +20,6 @@ import ( "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" "github.com/Wikid82/charon/backend/internal/logger" - "github.com/Wikid82/charon/backend/internal/util" ) const ( @@ -258,7 +257,6 @@ func TestEnsureBouncerRegistration_EnvKeyRejected_DiagnosticsRedacted(t *testing joined := strings.Join(channels, "\n") assert.Contains(t, joined, h.keyFile) assert.Contains(t, joined, "0600") - assert.Contains(t, joined, util.MaskAPIKey(testRejectedEnvKey)) }) } } @@ -274,7 +272,7 @@ func TestEnsureBouncerRegistration_EnvKeyRejected_ShortKeyRedacted(t *testing.T) channels := h.channels(h.drain()) assertNoSubstring(t, channels, shortKey) assertNoSubstring(t, channels, testGeneratedKey) - assert.Contains(t, strings.Join(channels, "\n"), "[REDACTED]") + assert.Contains(t, strings.Join(channels, "\n"), h.keyFile, "the rejected-key warning still names the key file") } func TestEnsureBouncerRegistration_RegistrationError_DiagnosticsRedacted(t *testing.T) { diff --git a/backend/internal/services/crowdsec_startup_log_capture_test.go b/backend/internal/services/crowdsec_startup_log_capture_test.go new file mode 100644 index 000000000..6e2302f4b --- /dev/null +++ b/backend/internal/services/crowdsec_startup_log_capture_test.go @@ -0,0 +1,109 @@ +package services + +import ( + "bytes" + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" + "github.com/Wikid82/charon/backend/internal/logger" +) + +// Keys made of distinct characters: every 4-character window is unique, so a +// leaked fragment is detectable in the captured output. +const ( + startupCaptureEnvKey = "Qz7kP2mV9xR4tN6bL1wH8sJ3cF5gD0aY" + startupCaptureGenKey = "Mn3pQ8rS1tU6vW0xY4zA9bC2dE7fG5hJ" +) + +// TestCrowdSecLogsCarryNoKeyText runs every startup key-bearing log path at Debug +// level and fails if any 4-character window of either key appears in the output. +func TestCrowdSecLogsCarryNoKeyText(t *testing.T) { + for _, name := range bouncerkey.EnvNames() { + t.Setenv(name, "") + } + lapi := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("X-Api-Key") == startupCaptureEnvKey { + w.WriteHeader(http.StatusForbidden) + return + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(lapi.Close) + t.Setenv("CHARON_SECURITY_CROWDSEC_API_URL", lapi.URL) + + buf := &bytes.Buffer{} + logger.Init(true, buf) + hook := logger.GetBroadcastHook() + hookID := t.Name() + entries := hook.Subscribe(hookID) + t.Cleanup(func() { + hook.Unsubscribe(hookID) + logger.Init(false, io.Discard) + }) + + t.Run("rejected env key and registration", func(t *testing.T) { + dir := t.TempDir() + keyFile := filepath.Join(dir, "crowdsec", "bouncer_key") + t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", startupCaptureEnvKey) + exec := &scriptedBouncerExecutor{addOutput: []byte(startupCaptureGenKey + "\n")} + require.NoError(t, ensureBouncerRegistration(dir, keyFile, exec)) + }) + + t.Run("saved file key", func(t *testing.T) { + dir := t.TempDir() + keyFile := filepath.Join(dir, "crowdsec", "bouncer_key") + require.NoError(t, os.MkdirAll(filepath.Dir(keyFile), 0o700)) + require.NoError(t, os.WriteFile(keyFile, []byte(startupCaptureGenKey), 0o600)) + exec := &scriptedBouncerExecutor{} + require.NoError(t, ensureBouncerRegistration(dir, keyFile, exec)) + }) + + t.Run("decision logging for every outcome", func(t *testing.T) { + for _, res := range []bouncerkey.EnsureResult{ + {Key: startupCaptureGenKey, Outcome: bouncerkey.OutcomeRegistered, EnvKey: startupCaptureEnvKey, EnvKeyRejected: true}, + {Key: startupCaptureGenKey, Outcome: bouncerkey.OutcomeRegistered, EnvKey: startupCaptureEnvKey, EnvKeyRejected: true, SaveErr: errors.New("disk full")}, + {Key: startupCaptureGenKey, Outcome: bouncerkey.OutcomeRegistered}, + {Key: startupCaptureEnvKey, Outcome: bouncerkey.OutcomeEnvKeySaved}, + {Key: startupCaptureEnvKey, Outcome: bouncerkey.OutcomeEnvKeySaved, SaveErr: errors.New("disk full")}, + {EnvKey: startupCaptureEnvKey, EnvKeyRejected: true}, + } { + logStartupKeyDecision(res, "/data/crowdsec/bouncer_key", nil) + } + logStartupKeyDecision(bouncerkey.EnsureResult{EnvKey: startupCaptureEnvKey, EnvKeyRejected: true}, "/data/crowdsec/bouncer_key", errors.New("register failed")) + }) + + t.Run("debug probe reaches the capture", func(t *testing.T) { + logger.Log().Debug("capture probe") + var sawProbe bool + for drained := false; !drained; { + select { + case e := <-entries: + if e.Level == logrus.DebugLevel && e.Message == "capture probe" { + sawProbe = true + } + default: + drained = true + } + } + require.True(t, sawProbe, "the capture hook must observe Debug entries") + }) + + out := buf.String() + require.True(t, strings.Contains(out, "CrowdSec bouncer"), "startup logs must be captured") + for _, key := range []string{startupCaptureEnvKey, startupCaptureGenKey} { + for i := 0; i+4 <= len(key); i++ { + assert.NotContains(t, out, key[i:i+4], "log output carries key text at offset %d", i) + } + } +} diff --git a/backend/internal/util/mask.go b/backend/internal/util/mask.go index 80972ea78..86224f1ad 100644 --- a/backend/internal/util/mask.go +++ b/backend/internal/util/mask.go @@ -5,9 +5,9 @@ import "fmt" // maskMinLength is the shortest value that may show its first and last characters. const maskMinLength = 24 -// MaskAPIKey returns a display-safe form of an API key: "[empty]" for an empty -// value, "[REDACTED]" below the minimum length, otherwise the first and last -// four characters. +// MaskAPIKey returns a display-safe form of an API key for UI previews only: +// "[empty]" for an empty value, "[REDACTED]" below the minimum length, otherwise +// the first and last four characters. Never pass its output to a log. func MaskAPIKey(key string) string { if key == "" { return "[empty]" From 0645198851c321b245ebbde0fc06e33e6ecd4a05 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 19:36:59 +0000 Subject: [PATCH 018/291] fix: keep the saved CrowdSec key when CrowdSec cannot be reached --- .../internal/api/handlers/crowdsec_handler.go | 233 ++++++------- .../api/handlers/crowdsec_handler_test.go | 6 +- .../crowdsec_key_check_unknown_test.go | 321 ++++++++++++++++++ .../api/handlers/crowdsec_lifecycle.go | 1 + .../crowdsec/bouncerkey/bouncerkey.go | 71 +++- .../crowdsec/bouncerkey/bouncerkey_test.go | 16 - .../bouncerkey/ensure_unverified_test.go | 129 +++++++ backend/internal/services/crowdsec_startup.go | 67 ++-- .../crowdsec_startup_unverified_test.go | 173 ++++++++++ 9 files changed, 811 insertions(+), 206 deletions(-) create mode 100644 backend/internal/api/handlers/crowdsec_key_check_unknown_test.go create mode 100644 backend/internal/crowdsec/bouncerkey/ensure_unverified_test.go create mode 100644 backend/internal/services/crowdsec_startup_unverified_test.go diff --git a/backend/internal/api/handlers/crowdsec_handler.go b/backend/internal/api/handlers/crowdsec_handler.go index 45ed91a5d..f0e217b73 100644 --- a/backend/internal/api/handlers/crowdsec_handler.go +++ b/backend/internal/api/handlers/crowdsec_handler.go @@ -160,9 +160,17 @@ type CrowdsecHandler struct { applyJobsCfgMu sync.Mutex applyJobsHub *crowdsec.HubService - // registrationMutex protects concurrent bouncer registration attempts + // decisionMu serializes the bouncer key decision (Ensure, including its LAPI check and any register), so two + // decisions never both register. GetKeyStatus never takes it. + decisionMu sync.Mutex + + // registrationMutex guards the rejection flags below. It is held only for short reads and writes, never across + // a network call, so GetKeyStatus can snapshot the flags while a decision is waiting on LAPI. registrationMutex sync.Mutex + // keyCheckWait bounds the LAPI key check retries; 0 means defaultLAPIKeyCheckWait. Tests shorten it. + keyCheckWait time.Duration + // envKeyRejected tracks whether the env var key was rejected by LAPI // This is set during ensureBouncerRegistration() and used by GetKeyStatus() envKeyRejected bool @@ -1681,6 +1689,10 @@ type KeyStatusResponse struct { // Message provides user-friendly guidance Message string `json:"message,omitempty"` + // KeyCheck is the LAPI check of the current key: "accepted", "rejected" (403 only), "unknown" (LAPI could not + // answer) or "none" (no key). Valid is true only for "accepted". + KeyCheck string `json:"key_check"` + // Valid indicates whether the current key is valid (authenticated successfully with LAPI) Valid bool `json:"valid"` @@ -1691,27 +1703,27 @@ type KeyStatusResponse struct { KeyFilePath string `json:"key_file_path"` } -// testKeyAgainstLAPI reports the LAPI verdict for an API key. The probe itself is -// keyAcceptedByLAPI; a false result is mapped to VerdictRejected until the three-valued -// Check replaces it. -func (h *CrowdsecHandler) testKeyAgainstLAPI(ctx context.Context, apiKey string) bouncerkey.Verdict { - if h.keyAcceptedByLAPI(ctx, apiKey) { - return bouncerkey.VerdictAccepted - } - return bouncerkey.VerdictRejected -} +// defaultLAPIKeyCheckWait is the retry window for one LAPI key check, the same window Start always used. +const defaultLAPIKeyCheckWait = 30 * time.Second -// keyAcceptedByLAPI validates an API key by making an authenticated request to LAPI. -// Uses /v1/decisions/stream endpoint which requires authentication. -// Returns true if the key is accepted (200 OK), false otherwise. -// Implements retry logic with exponential backoff for LAPI startup (connection refused). -// Fails fast on 403 Forbidden (invalid key - no retries). -func (h *CrowdsecHandler) keyAcceptedByLAPI(ctx context.Context, apiKey string) bool { - if apiKey == "" { - return false +// lapiKeyCheckOptions returns the retry policy for a handler key check against baseURL. +func (h *CrowdsecHandler) lapiKeyCheckOptions(baseURL string) bouncerkey.CheckOptions { + wait := h.keyCheckWait + if wait == 0 { + wait = defaultLAPIKeyCheckWait + } + return bouncerkey.CheckOptions{ + BaseURL: strings.TrimRight(baseURL, "/"), + Wait: wait, + PerAttempt: 5 * time.Second, + FirstBackoff: 500 * time.Millisecond, + MaxBackoff: 5 * time.Second, } +} - // Get LAPI URL from security config or use default +// testKeyAgainstLAPI reports the three-valued LAPI verdict for an API key, using the stored LAPI URL and the +// internal-service client. Only 403 is a rejection; an unreachable, slow or erroring LAPI is Unknown. +func (h *CrowdsecHandler) testKeyAgainstLAPI(ctx context.Context, apiKey string) bouncerkey.Verdict { lapiURL := "http://127.0.0.1:8085" if h.Security != nil { cfg, err := h.Security.Get() @@ -1720,154 +1732,88 @@ func (h *CrowdsecHandler) keyAcceptedByLAPI(ctx context.Context, apiKey string) } } - // Use /v1/decisions/stream endpoint (guaranteed to require authentication) - endpoint := fmt.Sprintf("%s/v1/decisions/stream", strings.TrimRight(lapiURL, "/")) - - // Retry logic for LAPI startup (30s max with exponential backoff) - const maxStartupWait = 30 * time.Second - const initialBackoff = 500 * time.Millisecond - const maxBackoff = 5 * time.Second - - backoff := initialBackoff - startTime := time.Now() - attempt := 0 - - for { - attempt++ - - // Check for context cancellation before each attempt - select { - case <-ctx.Done(): - logger.Log().WithField("attempts", attempt).Debug("Context cancelled during LAPI key validation") - return false - default: - // Continue - } - - // Create request with 5s timeout per attempt - testCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - req, err := http.NewRequestWithContext(testCtx, http.MethodGet, endpoint, http.NoBody) - if err != nil { - cancel() - logger.Log().WithError(err).Debug("Failed to create LAPI test request") - return false - } - - // Set API key header - req.Header.Set("X-Api-Key", apiKey) - - // Execute request - client := network.NewInternalServiceHTTPClient(5 * time.Second) - resp, err := client.Do(req) - cancel() - - if err != nil { - // Check if connection refused (LAPI not ready yet) - if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "connect: connection refused") { - // LAPI not ready - retry with backoff if within time limit - if time.Since(startTime) < maxStartupWait { - logger.Log().WithField("attempt", attempt).WithField("backoff", backoff).WithField("elapsed", time.Since(startTime)).Debug("LAPI not ready, retrying with backoff") - - // Check for context cancellation before sleeping - select { - case <-ctx.Done(): - logger.Log().WithField("attempts", attempt).Debug("Context cancelled during LAPI retry") - return false - case <-time.After(backoff): - // Continue with retry - } - - // Exponential backoff: 500ms → 750ms → 1125ms → ... (capped at 5s) - backoff = time.Duration(float64(backoff) * 1.5) - if backoff > maxBackoff { - backoff = maxBackoff - } - continue - } - - logger.Log().WithField("attempts", attempt).WithField("elapsed", time.Since(startTime)).WithField("max_wait", maxStartupWait).Warn("LAPI failed to start within timeout") - return false - } - - // Other errors (not connection refused) - logger.Log().WithError(err).Debug("Failed to connect to LAPI for key validation") - return false - } - // Only the status is needed; close the body now so it is released per attempt. - statusCode := resp.StatusCode - if closeErr := resp.Body.Close(); closeErr != nil { - logger.Log().WithError(closeErr).Debug("Failed to close HTTP response body") - } - - // Check response status - if statusCode == http.StatusOK { - logger.Log().WithField("attempts", attempt).WithField("elapsed", time.Since(startTime)).Debug("API key validated successfully against LAPI") - return true - } - - // 403 Forbidden = bad key, fail fast (no retries) - if statusCode == http.StatusForbidden { - logger.Log().WithField("status", statusCode).Debug("API key rejected by LAPI (403 Forbidden)") - return false - } - - // Other non-OK status codes - logger.Log().WithField("status", statusCode).Debug("API key validation returned unexpected status") - return false + client := network.NewInternalServiceHTTPClient(5 * time.Second) + verdict := bouncerkey.Check(ctx, client, apiKey, h.lapiKeyCheckOptions(lapiURL)) + logger.Log().WithField("verdict", keyCheckName(verdict)).Debug("CrowdSec bouncer key checked against LAPI") + return verdict +} + +// keyCheckName is the key_check value of the status response for a verdict. +func keyCheckName(v bouncerkey.Verdict) string { + switch v { + case bouncerkey.VerdictAccepted: + return "accepted" + case bouncerkey.VerdictRejected: + return "rejected" + default: + return "unknown" } } // GetKeyStatus returns the current CrowdSec bouncer key status and any rejection information. // This endpoint provides UX feedback when env var keys are rejected by LAPI. // @Summary Get CrowdSec API key status -// @Description Returns current key source, validity, and rejection status if env key was invalid +// @Description Returns current key source, validity, the LAPI key check and rejection status if env key was invalid // @Tags crowdsec // @Produce json // @Success 200 {object} KeyStatusResponse // @Router /admin/crowdsec/key-status [get] func (h *CrowdsecHandler) GetKeyStatus(c *gin.Context) { - h.registrationMutex.Lock() - defer h.registrationMutex.Unlock() keyPath := h.bouncerKeyPath() + // Snapshot the rejection state under the lock, then release it before any file read or LAPI check. A Start + // that is waiting on LAPI must not hold a status request, and every flag below comes from this one snapshot. + h.registrationMutex.Lock() + rej := rejectionSnapshot{ + envKeyRejected: h.envKeyRejected, + rejectedKey: h.rejectedEnvKey, + saveFailed: h.rejectedEnvSaveFailed, + registerFailed: h.rejectedEnvRegisterFailed, + } + h.registrationMutex.Unlock() + response := KeyStatusResponse{ BouncerName: bouncerName, KeyFilePath: keyPath, } // A rejected env key is reported whatever branch below applies. - if h.envKeyRejected && h.rejectedEnvKey != "" { + if rej.envKeyRejected && rej.rejectedKey != "" { response.EnvKeyRejected = true - response.RejectedKeyPreview = util.MaskAPIKey(h.rejectedEnvKey) - response.Message = rejectedEnvKeyMessage(keyPath, h.rejectedEnvSaveFailed, h.rejectedEnvRegisterFailed) + response.RejectedKeyPreview = util.MaskAPIKey(rej.rejectedKey) + response.Message = rejectedEnvKeyMessage(keyPath, rej.saveFailed, rej.registerFailed) } envKey, _ := bouncerkey.FromEnv() fileKey := bouncerkey.ReadFile(keyPath) + // The key check can run while a Start is rotating the key (after its delete, before its save). This status + // can then show the old key as rejected for one response. Nothing destructive reads this response, so that + // brief display glitch is accepted rather than locking the file and the LAPI check together. switch { - case h.envKeyRejected && fileKey != "" && !h.rejectedEnvSaveFailed && !h.rejectedEnvRegisterFailed: + case rej.envKeyRejected && fileKey != "" && !rej.saveFailed && !rej.registerFailed: // Charon saved a replacement key, so the file holds it; give the full key so the user can fix docker-compose.yml. // After a failed save or registration the file still holds the rejected key, so it is reported as a file key below. // Security: the caller is an authenticated admin who needs this to correct their config. response.KeySource = "auto-generated" response.FullKey = fileKey response.CurrentKeyPreview = util.MaskAPIKey(fileKey) - response.Valid = h.keyStillValid(c, fileKey) + applyKeyCheck(&response, h.keyStillValid(c, fileKey)) case fileKey != "": response.KeySource = "file" response.CurrentKeyPreview = util.MaskAPIKey(fileKey) - if envKey != "" && envKey != fileKey && !h.envKeyRejected { + if envKey != "" && envKey != fileKey && !rej.envKeyRejected { response.Message = ignoredEnvKeyMessage(keyPath) } - response.Valid = h.keyStillValid(c, fileKey) + applyKeyCheck(&response, h.keyStillValid(c, fileKey)) case envKey != "": // No saved file yet (for example a failed save): the env value is what the proxy reads. response.KeySource = "env" response.CurrentKeyPreview = util.MaskAPIKey(envKey) - response.Valid = h.keyStillValid(c, envKey) + applyKeyCheck(&response, h.keyStillValid(c, envKey)) default: response.KeySource = "none" + response.KeyCheck = "none" response.Valid = false if response.Message == "" { response.Message = "No CrowdSec API key configured. Start CrowdSec to auto-generate one." @@ -1877,6 +1823,28 @@ func (h *CrowdsecHandler) GetKeyStatus(c *gin.Context) { c.JSON(http.StatusOK, response) } +// rejectionSnapshot is the rejection state GetKeyStatus reads under registrationMutex. +type rejectionSnapshot struct { + envKeyRejected bool + rejectedKey string + saveFailed bool + registerFailed bool +} + +// applyKeyCheck records a LAPI verdict on the status response. valid stays true only for an accepted key. +func applyKeyCheck(response *KeyStatusResponse, verdict bouncerkey.Verdict) { + response.KeyCheck = keyCheckName(verdict) + response.Valid = verdict == bouncerkey.VerdictAccepted +} + +// unverifiedKeyWarning is the Start warning when the saved key could not be checked, and empty otherwise. +func unverifiedKeyWarning(res bouncerkey.EnsureResult) string { + if res.Outcome == bouncerkey.OutcomeUnverified { + return bouncerkey.UnverifiedMessage + } + return "" +} + // rejectedEnvKeyMessage explains that an env key was rejected and what Charon did about it. The key file // is named as the key in use only when the replacement was saved. func rejectedEnvKeyMessage(keyPath string, saveFailed, registerFailed bool) string { @@ -1898,21 +1866,24 @@ func ignoredEnvKeyMessage(keyPath string) string { "; remove the variable to silence this note." } -// keyStillValid tests key against LAPI with a 5 second budget for status checks. -func (h *CrowdsecHandler) keyStillValid(c *gin.Context, key string) bool { +// keyStillValid returns the LAPI verdict for key with a 5 second budget for status checks. +func (h *CrowdsecHandler) keyStillValid(c *gin.Context, key string) bouncerkey.Verdict { ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() - return h.testKeyAgainstLAPI(ctx, key) == bouncerkey.VerdictAccepted + return h.testKeyAgainstLAPI(ctx, key) } // ensureBouncerRegistration decides which bouncer key to use and makes the key file match it. -// The decision and the rejection state are updated under registrationMutex, which GetKeyStatus also takes. +// decisionMu makes the decision (LAPI check and any register) one at a time; registrationMutex is taken only +// to write the rejection flags, so GetKeyStatus can answer while a Start waits on LAPI. func (h *CrowdsecHandler) ensureBouncerRegistration(ctx context.Context) (bouncerkey.EnsureResult, error) { - h.registrationMutex.Lock() - defer h.registrationMutex.Unlock() + h.decisionMu.Lock() + defer h.decisionMu.Unlock() res, err := bouncerkey.Ensure(ctx, h.bouncerKeyPath(), h.testKeyAgainstLAPI, h.registerBouncer, crowdsec.SaveBouncerKey) + h.registrationMutex.Lock() + defer h.registrationMutex.Unlock() if res.EnvKeyRejected { h.envKeyRejected = true h.rejectedEnvKey = res.EnvKey @@ -1961,6 +1932,8 @@ func (h *CrowdsecHandler) handleBouncerRegistration(res bouncerkey.EnsureResult, if res.EnvKeyIgnored { logger.Log().WithField("file", keyPath).Info("CrowdSec bouncer: saved key is in use; the key variable is ignored") } + case bouncerkey.OutcomeUnverified: + logger.Log().WithField("file", keyPath).Warn(bouncerkey.UnverifiedMessage) } } diff --git a/backend/internal/api/handlers/crowdsec_handler_test.go b/backend/internal/api/handlers/crowdsec_handler_test.go index 34457269f..e9a952d74 100644 --- a/backend/internal/api/handlers/crowdsec_handler_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_test.go @@ -3945,6 +3945,7 @@ func TestTestKeyAgainstLAPI_Timeout(t *testing.T) { db := setupCrowdDB(t) handler := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + handler.keyCheckWait = 300 * time.Millisecond // Create security config with test server URL cfg := models.SecurityConfig{ @@ -3957,7 +3958,7 @@ func TestTestKeyAgainstLAPI_Timeout(t *testing.T) { ctx := context.Background() result := handler.testKeyAgainstLAPI(ctx, "test-key") - require.Equal(t, bouncerkey.VerdictRejected, result, "Should be rejected after timeout") + require.Equal(t, bouncerkey.VerdictUnknown, result, "a timed-out check gives no verdict, so it is never a rejection") } // TestTestKeyAgainstLAPI_NonOKStatus verifies that non-200/403 status codes are handled. @@ -3975,6 +3976,7 @@ func TestTestKeyAgainstLAPI_NonOKStatus(t *testing.T) { db := setupCrowdDB(t) handler := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir()) + handler.keyCheckWait = 300 * time.Millisecond // Create security config with test server URL cfg := models.SecurityConfig{ @@ -3987,7 +3989,7 @@ func TestTestKeyAgainstLAPI_NonOKStatus(t *testing.T) { ctx := context.Background() result := handler.testKeyAgainstLAPI(ctx, "test-key") - require.Equal(t, bouncerkey.VerdictRejected, result, "Should be rejected for non-OK status") + require.Equal(t, bouncerkey.VerdictUnknown, result, "a 500 is not a rejection") } // TestEnsureBouncerRegistration_ValidEnvKey verifies that valid environment keys are used. diff --git a/backend/internal/api/handlers/crowdsec_key_check_unknown_test.go b/backend/internal/api/handlers/crowdsec_key_check_unknown_test.go new file mode 100644 index 000000000..599894b4b --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_key_check_unknown_test.go @@ -0,0 +1,321 @@ +package handlers + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" +) + +// unreachableLAPIURL returns a base URL with nothing listening, so every connection is refused. +func unreachableLAPIURL(t *testing.T) string { + t.Helper() + srv := httptest.NewServer(http.NotFoundHandler()) + url := srv.URL + srv.Close() + return url +} + +// statusLAPI answers every key check with status. +func statusLAPI(t *testing.T, status int) string { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(status) + })) + t.Cleanup(srv.Close) + return srv.URL +} + +// keyCheckCmdRecorder records every command and answers nil, so a test can assert that no bouncer command ran. +type keyCheckCmdRecorder struct { + mu sync.Mutex + calls []string +} + +func (r *keyCheckCmdRecorder) Execute(_ context.Context, name string, args ...string) ([]byte, error) { + r.mu.Lock() + defer r.mu.Unlock() + r.calls = append(r.calls, name+" "+strings.Join(args, " ")) + return nil, nil +} + +func (r *keyCheckCmdRecorder) bouncerCalls() []string { + r.mu.Lock() + defer r.mu.Unlock() + var out []string + for _, c := range r.calls { + if strings.Contains(c, "bouncers") { + out = append(out, c) + } + } + return out +} + +// keyStatusBody runs GetKeyStatus and returns the status code and decoded body. It makes no assertions, +// so it is safe to call from a goroutine. +func keyStatusBody(h *CrowdsecHandler) (code int, body map[string]any) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/admin/crowdsec/key-status", http.NoBody) + h.GetKeyStatus(c) + _ = json.Unmarshal(w.Body.Bytes(), &body) + return w.Code, body +} + +// keyStatusFor runs GetKeyStatus and decodes the response. +func keyStatusFor(t *testing.T, h *CrowdsecHandler) map[string]any { + t.Helper() + code, body := keyStatusBody(h) + require.Equal(t, http.StatusOK, code) + return body +} + +// newKeyCheckHandler builds a handler whose LAPI key check gives up after a short window, so an unreachable +// or erroring LAPI does not hold a test for the production 30 s window. +func newKeyCheckHandler(t *testing.T, lapiURL, dataDir string, cmd CommandExecutor) *CrowdsecHandler { + t.Helper() + h := newPrecedenceHandler(t, lapiURL, dataDir, cmd) + h.keyCheckWait = 300 * time.Millisecond + return h +} + +func TestKeyCheckUnreachableStoredURLIsUnknown(t *testing.T) { + h := newKeyCheckHandler(t, unreachableLAPIURL(t), t.TempDir(), noCSCLIMock()) + + require.Equal(t, bouncerkey.VerdictUnknown, h.testKeyAgainstLAPI(context.Background(), precSavedKey), + "a refused connection is no evidence about the key") +} + +func TestKeyCheckUnauthorizedIsUnknown(t *testing.T) { + h := newKeyCheckHandler(t, statusLAPI(t, http.StatusUnauthorized), t.TempDir(), noCSCLIMock()) + + require.Equal(t, bouncerkey.VerdictUnknown, h.testKeyAgainstLAPI(context.Background(), precSavedKey), + "only 403 is a rejection; 401 stays unknown") +} + +func TestKeyCheckServerErrorIsUnknown(t *testing.T) { + h := newKeyCheckHandler(t, statusLAPI(t, http.StatusInternalServerError), t.TempDir(), noCSCLIMock()) + + require.Equal(t, bouncerkey.VerdictUnknown, h.testKeyAgainstLAPI(context.Background(), precSavedKey)) +} + +func TestKeyCheckForbiddenIsRejected(t *testing.T) { + h := newKeyCheckHandler(t, statusLAPI(t, http.StatusForbidden), t.TempDir(), noCSCLIMock()) + + require.Equal(t, bouncerkey.VerdictRejected, h.testKeyAgainstLAPI(context.Background(), precSavedKey)) +} + +func TestKeyCheckCancelledContextIsUnknownWithoutRequest(t *testing.T) { + var requests atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests.Add(1) + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + h := newKeyCheckHandler(t, srv.URL, t.TempDir(), noCSCLIMock()) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + require.Equal(t, bouncerkey.VerdictUnknown, h.testKeyAgainstLAPI(ctx, precSavedKey), + "a shut-down context is never evidence about the key") + require.Zero(t, requests.Load()) +} + +// TestEnsureBouncerRegistrationUnreachableKeepsSavedKey: an unreachable LAPI leaves the saved key, the bouncer +// and the key file alone, and the rejection facts untouched. +func TestEnsureBouncerRegistrationUnreachableKeepsSavedKey(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + keyPath := filepath.Join(dataDir, "bouncer_key") + writeSavedKey(t, keyPath, precSavedKey) + before, err := os.ReadFile(keyPath) + require.NoError(t, err) + h := newKeyCheckHandler(t, unreachableLAPIURL(t), dataDir, noCSCLIMock()) + h.envKeyRejected = true + h.rejectedEnvKey = precEnvKeyOther + h.rejectedEnvSaveFailed = true + + res, regErr := h.ensureBouncerRegistration(context.Background()) + + require.NoError(t, regErr) + require.Equal(t, bouncerkey.OutcomeUnverified, res.Outcome) + require.Equal(t, precSavedKey, res.Key) + after, err := os.ReadFile(keyPath) + require.NoError(t, err) + require.Equal(t, before, after, "the key file is byte-for-byte unchanged") + require.True(t, h.envKeyRejected, "an unverified check leaves the rejection state as it was") + require.Equal(t, precEnvKeyOther, h.rejectedEnvKey) + require.True(t, h.rejectedEnvSaveFailed) +} + +// TestEnsureBouncerRegistrationShutdownDoesNotRotate: a shutdown during the key check reaches neither the +// delete nor the add, even when LAPI would reject the key. +func TestEnsureBouncerRegistrationShutdownDoesNotRotate(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + keyPath := filepath.Join(dataDir, "bouncer_key") + writeSavedKey(t, keyPath, precSavedKey) + lapiURL, _ := newKeyLAPI(t) // accepts no key: every check is a definite 403 + cmd := &keyCheckCmdRecorder{} + h := newKeyCheckHandler(t, lapiURL, dataDir, cmd) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + res, err := h.ensureBouncerRegistration(ctx) + + require.NoError(t, err) + require.Equal(t, bouncerkey.OutcomeUnverified, res.Outcome) + require.Empty(t, cmd.bouncerCalls(), "a shutdown must not delete or add a bouncer") + require.Equal(t, precSavedKey, bouncerkey.ReadFile(keyPath)) +} + +// TestStartUnreachableStoredURLWarnsWithoutRotating: Start with a stored LAPI URL that cannot be reached keeps +// the saved key, runs no bouncer command and says so in the response. +func TestStartUnreachableStoredURLWarnsWithoutRotating(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + keyPath := filepath.Join(dataDir, "bouncer_key") + writeSavedKey(t, keyPath, precSavedKey) + cmd := &keyCheckCmdRecorder{} + applier := &recordingApplier{} + h := newStartApplyHandler(t, unreachableLAPIURL(t), dataDir, cmd, &fakeExec{}, applier) + h.keyCheckWait = 300 * time.Millisecond + + w := startRequest(t, h, context.Background()) + + require.Equal(t, http.StatusOK, w.Code, w.Body.String()) + var body map[string]any + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + require.Equal(t, true, body["lapi_ready"]) + require.Equal(t, bouncerkey.UnverifiedMessage, body["warning"]) + require.Empty(t, cmd.bouncerCalls(), "no bouncer delete or add while the key is unverified") + require.Equal(t, precSavedKey, bouncerkey.ReadFile(keyPath)) + require.Equal(t, 1, applier.count(), "the mode is set, so the proxy is applied once") +} + +// TestGetKeyStatusKeyCheckUnreachableIsUnknown: the status reports the check as unknown, not as valid or +// rejected, and leaves the rejection state alone. +func TestGetKeyStatusKeyCheckUnreachableIsUnknown(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + writeSavedKey(t, filepath.Join(dataDir, "bouncer_key"), precSavedKey) + h := newKeyCheckHandler(t, unreachableLAPIURL(t), dataDir, noCSCLIMock()) + + body := keyStatusFor(t, h) + + require.Equal(t, "unknown", body["key_check"]) + require.Equal(t, false, body["valid"]) + require.Equal(t, false, body["env_key_rejected"]) + require.Equal(t, "file", body["key_source"]) +} + +// TestGetKeyStatusKeyCheckStates: accepted, rejected and none, with valid equal to accepted. +func TestGetKeyStatusKeyCheckStates(t *testing.T) { + t.Run("accepted", func(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + writeSavedKey(t, filepath.Join(dataDir, "bouncer_key"), precSavedKey) + lapiURL, _ := newKeyLAPI(t, precSavedKey) + h := newKeyCheckHandler(t, lapiURL, dataDir, noCSCLIMock()) + + body := keyStatusFor(t, h) + + require.Equal(t, "accepted", body["key_check"]) + require.Equal(t, true, body["valid"]) + }) + t.Run("rejected", func(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + writeSavedKey(t, filepath.Join(dataDir, "bouncer_key"), precSavedKey) + lapiURL, _ := newKeyLAPI(t) + h := newKeyCheckHandler(t, lapiURL, dataDir, noCSCLIMock()) + + body := keyStatusFor(t, h) + + require.Equal(t, "rejected", body["key_check"]) + require.Equal(t, false, body["valid"]) + }) + t.Run("none", func(t *testing.T) { + clearKeyEnv(t, "") + h := newKeyCheckHandler(t, unreachableLAPIURL(t), t.TempDir(), noCSCLIMock()) + + body := keyStatusFor(t, h) + + require.Equal(t, "none", body["key_check"]) + require.Equal(t, false, body["valid"]) + require.Equal(t, "none", body["key_source"]) + }) +} + +// TestGetKeyStatusAnswersWhileStartIsChecking: a status request is not held behind a Start that is waiting on +// LAPI. The first LAPI request blocks until released; every later request answers at once. +func TestGetKeyStatusAnswersWhileStartIsChecking(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + writeSavedKey(t, filepath.Join(dataDir, "bouncer_key"), precSavedKey) + + entered := make(chan struct{}) + release := make(chan struct{}) + var first atomic.Bool + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if first.CompareAndSwap(false, true) { + close(entered) + <-release + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + h := newKeyCheckHandler(t, srv.URL, dataDir, noCSCLIMock()) + + startDone := make(chan struct{}) + go func() { + defer close(startDone) + _, _ = h.ensureBouncerRegistration(context.Background()) + }() + t.Cleanup(func() { + select { + case <-release: + default: + close(release) + } + <-startDone + }) + + select { + case <-entered: + case <-time.After(5 * time.Second): + t.Fatal("the Start key check never reached LAPI") + } + + type statusReply struct { + code int + body map[string]any + } + statusDone := make(chan statusReply, 1) + started := time.Now() + go func() { + code, body := keyStatusBody(h) + statusDone <- statusReply{code: code, body: body} + }() + + select { + case reply := <-statusDone: + require.Equal(t, http.StatusOK, reply.code) + require.Less(t, time.Since(started), 5*time.Second, "the status must answer within its own 5 s budget") + require.Equal(t, "accepted", reply.body["key_check"]) + case <-time.After(5 * time.Second): + t.Fatal("GetKeyStatus waited for the Start key check; it must snapshot under the lock and release it first") + } +} diff --git a/backend/internal/api/handlers/crowdsec_lifecycle.go b/backend/internal/api/handlers/crowdsec_lifecycle.go index 16c0d86ab..2808debb0 100644 --- a/backend/internal/api/handlers/crowdsec_lifecycle.go +++ b/backend/internal/api/handlers/crowdsec_lifecycle.go @@ -379,6 +379,7 @@ func (h *CrowdsecHandler) StartCrowdSec(reqCtx context.Context) (CrowdsecStartRe Status: "started", PID: pid, LAPIReady: true, + Warning: unverifiedKeyWarning(res), ProxyApplied: h.applyProxyConfig(ctx) == nil, }, nil } diff --git a/backend/internal/crowdsec/bouncerkey/bouncerkey.go b/backend/internal/crowdsec/bouncerkey/bouncerkey.go index 217b400af..e7fa8a204 100644 --- a/backend/internal/crowdsec/bouncerkey/bouncerkey.go +++ b/backend/internal/crowdsec/bouncerkey/bouncerkey.go @@ -140,6 +140,9 @@ const ( OutcomeEnvKeySaved // OutcomeRegistered means a new bouncer was registered with LAPI. OutcomeRegistered + // OutcomeUnverified means LAPI could not give a verdict on a present key. The key is kept as it + // was, nothing is registered, deleted or saved, and the caller reports it as unverified. + OutcomeUnverified ) // EnsureResult reports the key chosen by Ensure and the facts callers need to @@ -161,14 +164,22 @@ type EnsureResult struct { // Ensure decides which bouncer key to use and makes the key file match it. // -// Only a VerdictAccepted from validate keeps or adopts a key. VerdictRejected and -// VerdictUnknown are both treated as "not accepted" for now. +// validate returns a three-valued Verdict. Only a definite VerdictRejected may lead to a +// new bouncer; VerdictUnknown (LAPI unreachable, slow or erroring, or the context ended) +// is never evidence against a key. // // Rules, in order: // 1. A saved key that validates is kept. The env value is not tested. +// An unknown saved key stops the decision as OutcomeUnverified. // 2. Otherwise an env value that differs from the saved key and validates is saved. -// 3. Otherwise a new bouncer is registered and its key saved. An env value -// equal to the saved key is not validated again; it counts as rejected. +// An unknown env value stops the decision as OutcomeUnverified. +// 3. Otherwise a new bouncer is registered and its key saved. This runs only when every +// present candidate was definitively rejected. A cancelled ctx also stops here as +// OutcomeUnverified, so a shutdown never rotates a key. An env value equal to the saved +// key is not validated again; it counts as rejected. +// +// OutcomeUnverified changes nothing: no register, delete or save, and no rejection facts. +// Its Key is the saved key, or the unknown env value when no saved key exists. // // A save failure after rule 2 or 3 is reported in EnsureResult.SaveErr, not as // the error. A register failure is returned as the error, with the rejection @@ -184,18 +195,34 @@ func Ensure( saved := ReadFile(path) res := EnsureResult{EnvKey: env} - if saved != "" && validate(ctx, saved) == VerdictAccepted { - res.Key = saved - res.Outcome = OutcomeSavedKeyValid - res.EnvKeyIgnored = env != "" && env != saved - return res, nil + if saved != "" { + switch validate(ctx, saved) { + case VerdictAccepted: + res.Key = saved + res.Outcome = OutcomeSavedKeyValid + res.EnvKeyIgnored = env != "" && env != saved + return res, nil + case VerdictUnknown: + return unverified(res, saved, ""), nil + } + } + + if env != "" && env != saved { + switch validate(ctx, env) { + case VerdictAccepted: + res.Key = env + res.Outcome = OutcomeEnvKeySaved + res.SaveErr = save(path, env) + return res, nil + case VerdictUnknown: + return unverified(res, saved, env), nil + } } - if env != "" && env != saved && validate(ctx, env) == VerdictAccepted { - res.Key = env - res.Outcome = OutcomeEnvKeySaved - res.SaveErr = save(path, env) - return res, nil + // Every present candidate was definitively rejected here. A cancelled context is not + // evidence about the key, so it must not reach the destructive register step. + if ctx.Err() != nil { + return unverified(res, saved, ""), nil } res.EnvKeyRejected = env != "" @@ -209,6 +236,17 @@ func Ensure( return res, nil } +// unverified finishes an Ensure result whose key could not be checked. The saved key stays in +// use; with no saved key, the unknown env value is the key the proxy reads. +func unverified(res EnsureResult, saved, unknownEnv string) EnsureResult { + res.Outcome = OutcomeUnverified + res.Key = saved + if res.Key == "" { + res.Key = unknownEnv + } + return res +} + // SaveFailedNotice says the new key could not be written to path. The new key is held nowhere else, so // CrowdSec blocking depends on the key file becoming writable. func SaveFailedNotice(path string) string { @@ -222,3 +260,8 @@ func RegisterFailedNotice(path string) string { return "Charon could not register a new key with CrowdSec, so no new key was saved to " + path + ". CrowdSec blocking may not work until a key can be registered." } + +// UnverifiedMessage says a saved key could not be checked, so it was kept unchanged. It is the one +// wording for every caller (Start warning, handler and startup logs); it names no key material. +const UnverifiedMessage = "CrowdSec could not be reached to check the key, so Charon keeps the current key " + + "and did not register a new one. It checks again at the next start." diff --git a/backend/internal/crowdsec/bouncerkey/bouncerkey_test.go b/backend/internal/crowdsec/bouncerkey/bouncerkey_test.go index 36647bf07..9c5e6fb8a 100644 --- a/backend/internal/crowdsec/bouncerkey/bouncerkey_test.go +++ b/backend/internal/crowdsec/bouncerkey/bouncerkey_test.go @@ -191,22 +191,6 @@ func (f *fakeLAPI) validate(_ context.Context, key string) Verdict { } } -// TestEnsureUnknownVerdictFollowsRejectedPath pins the interim rule of the -// three-valued refactor: until Ensure learns about Unknown, an unknown saved key -// takes the same path as a rejected one (register). The next change replaces this. -func TestEnsureUnknownVerdictFollowsRejectedPath(t *testing.T) { - path := seedKeyFile(t, testSavedInvalid) - setOnlyEnvKey(t, "") - fake := newFakeLAPI() - fake.unknown = map[string]bool{testSavedInvalid: true} - - res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) - - require.NoError(t, err) - require.Equal(t, OutcomeRegistered, res.Outcome) - require.Equal(t, 1, fake.registers) -} - func (f *fakeLAPI) register(_ context.Context) (string, error) { f.registers++ if f.regErr != nil { diff --git a/backend/internal/crowdsec/bouncerkey/ensure_unverified_test.go b/backend/internal/crowdsec/bouncerkey/ensure_unverified_test.go new file mode 100644 index 000000000..c9153637c --- /dev/null +++ b/backend/internal/crowdsec/bouncerkey/ensure_unverified_test.go @@ -0,0 +1,129 @@ +package bouncerkey + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +// Keys for the rows where LAPI could not give a verdict. The fake answers Unknown for them. +const ( + testSavedUnknown = "saved-unknown-key-0001" + testEnvUnknown = "env-unknown-key-00001" +) + +// TestEnsureUnknownNeverRegistersOrSaves covers every reachable row in which a present +// candidate got VerdictUnknown. Such a row must keep the saved key (or the env key when +// no file exists), must not register, delete or save, and must not test candidates +// after the unknown one. +func TestEnsureUnknownNeverRegistersOrSaves(t *testing.T) { + rows := []struct { + name string + saved string + env string + wantKey string + wantValidated []string + }{ + { + name: "saved unknown, env none", saved: testSavedUnknown, env: "", + wantKey: testSavedUnknown, wantValidated: []string{testSavedUnknown}, + }, + { + name: "saved unknown, env valid different", saved: testSavedUnknown, env: testEnvValid, + wantKey: testSavedUnknown, wantValidated: []string{testSavedUnknown}, + }, + { + name: "saved unknown, env invalid different", saved: testSavedUnknown, env: testEnvInvalid, + wantKey: testSavedUnknown, wantValidated: []string{testSavedUnknown}, + }, + { + name: "saved unknown, env unknown different", saved: testSavedUnknown, env: testEnvUnknown, + wantKey: testSavedUnknown, wantValidated: []string{testSavedUnknown}, + }, + { + name: "saved unknown, env same", saved: testSavedUnknown, env: testSavedUnknown, + wantKey: testSavedUnknown, wantValidated: []string{testSavedUnknown}, + }, + { + name: "saved invalid, env unknown", saved: testSavedInvalid, env: testEnvUnknown, + wantKey: testSavedInvalid, wantValidated: []string{testSavedInvalid, testEnvUnknown}, + }, + { + name: "saved none, env unknown", saved: "", env: testEnvUnknown, + wantKey: testEnvUnknown, wantValidated: []string{testEnvUnknown}, + }, + } + + for _, row := range rows { + t.Run(row.name, func(t *testing.T) { + path := seedKeyFile(t, row.saved) + setOnlyEnvKey(t, row.env) + fake := newFakeLAPI() + fake.unknown = map[string]bool{testSavedUnknown: true, testEnvUnknown: true} + + res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.NoError(t, res.SaveErr) + require.Equal(t, OutcomeUnverified, res.Outcome, "outcome") + require.Equal(t, row.wantKey, res.Key, "returned key") + require.Equal(t, row.wantValidated, fake.validated, "validate calls") + require.Zero(t, fake.registers, "an unknown verdict must never register") + require.Empty(t, fake.saved, "an unknown verdict must never save") + require.False(t, res.EnvKeyRejected, "an unknown verdict is not a rejection") + require.Equal(t, row.env, res.EnvKey, "EnvKey") + require.Equal(t, row.saved, ReadFile(path), "key file is unchanged") + }) + } +} + +// TestEnsureDefinitelyRejectedCandidatesStillRegister pins the destructive rule from the +// other side: every present candidate rejected with a definite answer still registers. +func TestEnsureDefinitelyRejectedCandidatesStillRegister(t *testing.T) { + path := seedKeyFile(t, testSavedInvalid) + setOnlyEnvKey(t, testEnvInvalid) + fake := newFakeLAPI() + + res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.Equal(t, OutcomeRegistered, res.Outcome) + require.Equal(t, 1, fake.registers) + require.Equal(t, []string{testRegistered}, fake.saved) +} + +// TestEnsureCancelledContextNeverRegisters covers a shutdown during the decision. The +// validators here answer Rejected, so only the context guard can keep the destructive +// step from running. +func TestEnsureCancelledContextNeverRegisters(t *testing.T) { + rows := []struct { + name string + saved string + env string + wantKey string + }{ + {name: "saved rejected, env rejected", saved: testSavedInvalid, env: testEnvInvalid, wantKey: testSavedInvalid}, + {name: "saved none, env rejected", saved: "", env: testEnvInvalid, wantKey: ""}, + {name: "nothing present", saved: "", env: "", wantKey: ""}, + } + + for _, row := range rows { + t.Run(row.name, func(t *testing.T) { + path := seedKeyFile(t, row.saved) + setOnlyEnvKey(t, row.env) + fake := newFakeLAPI() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + res, err := Ensure(ctx, path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.Equal(t, OutcomeUnverified, res.Outcome) + require.Equal(t, row.wantKey, res.Key) + require.Zero(t, fake.registers, "a cancelled context must never register") + require.Empty(t, fake.saved, "a cancelled context must never save") + require.False(t, res.EnvKeyRejected) + }) + } +} diff --git a/backend/internal/services/crowdsec_startup.go b/backend/internal/services/crowdsec_startup.go index e13c77d20..e6b1427c1 100644 --- a/backend/internal/services/crowdsec_startup.go +++ b/backend/internal/services/crowdsec_startup.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "net/http" "os" "os/exec" "path/filepath" @@ -18,6 +17,7 @@ import ( "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" "github.com/Wikid82/charon/backend/internal/logger" "github.com/Wikid82/charon/backend/internal/models" + "github.com/Wikid82/charon/backend/internal/network" "github.com/Wikid82/charon/backend/internal/util" "gorm.io/gorm" ) @@ -288,6 +288,15 @@ type CommandExecutor interface { const defaultBouncerKeyFile = bouncerkey.DefaultFile +// startupKeyCheckTiming is the retry policy of the boot key check: one window of 10 s, shared with the handler rule. +// Only Wait, PerAttempt and the backoffs are read; the URL comes from the environment at check time. Tests shorten it. +var startupKeyCheckTiming = bouncerkey.CheckOptions{ + Wait: 10 * time.Second, + PerAttempt: 5 * time.Second, + FirstBackoff: 500 * time.Millisecond, + MaxBackoff: 5 * time.Second, +} + // startupBouncerName is the bouncer registered with LAPI at startup. const startupBouncerName = "caddy-bouncer" @@ -362,14 +371,18 @@ func ensureBouncerRegistration(dataDir, bouncerKeyFile string, cmdExec CommandEx return err } -// startupKeyValid tests a key against LAPI for the shared decision. The probe is -// testKeyAgainstLAPIStartup; a false result is mapped to VerdictRejected until the -// three-valued Check replaces it. -func startupKeyValid(_ context.Context, key string) bouncerkey.Verdict { - if testKeyAgainstLAPIStartup(key) { - return bouncerkey.VerdictAccepted +// startupKeyValid returns the LAPI verdict for a key for the shared boot decision. It uses the same +// three-valued check as the handler: only 403 is a rejection, and an unreachable or erroring LAPI is Unknown. +// The URL is CHARON_SECURITY_CROWDSEC_API_URL or the local default. +func startupKeyValid(ctx context.Context, key string) bouncerkey.Verdict { + lapiURL := os.Getenv("CHARON_SECURITY_CROWDSEC_API_URL") + if lapiURL == "" { + lapiURL = "http://127.0.0.1:8085" } - return bouncerkey.VerdictRejected + opts := startupKeyCheckTiming + opts.BaseURL = strings.TrimRight(lapiURL, "/") + client := network.NewInternalServiceHTTPClient(opts.PerAttempt) + return bouncerkey.Check(ctx, client, key, opts) } // rejectedEnvKeyLogMessage is the Warn text for a rejected key variable whose replacement was @@ -432,45 +445,11 @@ func logStartupKeyDecision(res bouncerkey.EnsureResult, keyFile string, regErr e "key_file": keyFile, }).Info("CrowdSec bouncer: successfully registered and saved key") } + case bouncerkey.OutcomeUnverified: + logger.Log().WithField("key_file", keyFile).Warn(bouncerkey.UnverifiedMessage) } } -// Helper functions for startup bouncer registration (minimal dependencies) - -func testKeyAgainstLAPIStartup(apiKey string) bool { - if apiKey == "" { - return false - } - - lapiURL := os.Getenv("CHARON_SECURITY_CROWDSEC_API_URL") - if lapiURL == "" { - lapiURL = "http://127.0.0.1:8085" - } - endpoint := strings.TrimRight(lapiURL, "/") + "/v1/decisions/stream" - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) //nolint:gosec // G704: endpoint from env config, not user input - if err != nil { - return false - } - req.Header.Set("X-Api-Key", apiKey) - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) //nolint:gosec // G704: request built from trusted config endpoint - if err != nil { - return false - } - defer func() { - if closeErr := resp.Body.Close(); closeErr != nil { - logger.Log().WithError(closeErr).Debug("Failed to close HTTP response body") - } - }() - - return resp.StatusCode == 200 -} - // simpleCommandExecutor provides minimal command execution for startup registration type simpleCommandExecutor struct{} diff --git a/backend/internal/services/crowdsec_startup_unverified_test.go b/backend/internal/services/crowdsec_startup_unverified_test.go new file mode 100644 index 000000000..5d2d51b69 --- /dev/null +++ b/backend/internal/services/crowdsec_startup_unverified_test.go @@ -0,0 +1,173 @@ +package services + +import ( + "net/http" + "net/http/httptest" + "os" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" +) + +// shortenStartupKeyCheck makes the startup key check give up quickly, so tests with an unreachable, +// slow or erroring LAPI do not wait for the production 10 s window. +func shortenStartupKeyCheck(t *testing.T) { + t.Helper() + saved := startupKeyCheckTiming + startupKeyCheckTiming.Wait = 300 * time.Millisecond + startupKeyCheckTiming.PerAttempt = 100 * time.Millisecond + startupKeyCheckTiming.FirstBackoff = 5 * time.Millisecond + startupKeyCheckTiming.MaxBackoff = 20 * time.Millisecond + t.Cleanup(func() { startupKeyCheckTiming = saved }) +} + +// lapiAnswering starts a LAPI stub that answers every key check with status. +func lapiAnswering(t *testing.T, status int) string { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(status) + })) + t.Cleanup(srv.Close) + return srv.URL +} + +// unreachableURL returns a base URL with nothing listening, so every connection is refused. +func unreachableURL(t *testing.T) string { + t.Helper() + srv := httptest.NewServer(http.NotFoundHandler()) + url := srv.URL + srv.Close() + return url +} + +// assertNoKeyFragments fails if any 4-character window of key appears in the captured output. +func assertNoKeyFragments(t *testing.T, out []string, key string) { + t.Helper() + for i := 0; i+4 <= len(key); i++ { + fragment := key[i : i+4] + for _, s := range out { + require.NotContains(t, s, fragment, "log output must carry no key characters") + } + } +} + +// TestStartupKeyCheckWindowIsTenSeconds pins the boot window (Q-7): one shared retry rule with a 10 s budget. +func TestStartupKeyCheckWindowIsTenSeconds(t *testing.T) { + require.Equal(t, 10*time.Second, startupKeyCheckTiming.Wait) +} + +// logProbeKey is written as the saved key in the log-scan tests. Its characters do not form words, so a +// 4-character window cannot match ordinary log text. +const logProbeKey = "Q7zX9kP2mW4nB8vC1hL5jR3tY6gF0dS" + +// TestEnsureBouncerRegistrationUnreachableLAPIKeepsSavedKey: a LAPI that cannot be reached at boot leaves the +// saved key, the bouncer and the key file byte-for-byte alone, logs one neutral Warn, and lets boot continue. +func TestEnsureBouncerRegistrationUnreachableLAPIKeepsSavedKey(t *testing.T) { + h := newBouncerHarness(t) + shortenStartupKeyCheck(t) + t.Setenv("CHARON_SECURITY_CROWDSEC_API_URL", unreachableURL(t)) + h.writeFileKey(t, logProbeKey) + before, err := os.ReadFile(h.keyFile) + require.NoError(t, err) + exec := &scriptedBouncerExecutor{} + + require.NoError(t, ensureBouncerRegistration(h.dataDir, h.keyFile, exec)) + + require.False(t, exec.called("bouncers"), "no bouncer delete or add while LAPI is unreachable") + after, err := os.ReadFile(h.keyFile) + require.NoError(t, err) + require.Equal(t, before, after, "the key file is byte-for-byte unchanged") + out := h.channels(h.drain()) + require.Contains(t, strings.Join(out, "\n"), bouncerkey.UnverifiedMessage) + assertNoKeyFragments(t, out, logProbeKey) +} + +// TestEnsureBouncerRegistrationUnverifiedOutcomesNeverRotate runs the other non-definite answers through the +// same boot decision: a slow LAPI, a server error, an unauthorized answer and a redirect. None may delete or add. +func TestEnsureBouncerRegistrationUnverifiedOutcomesNeverRotate(t *testing.T) { + slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + time.Sleep(time.Second) + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(slow.Close) + + cases := []struct { + name string + url func(t *testing.T) string + }{ + {name: "slow", url: func(*testing.T) string { return slow.URL }}, + {name: "server error", url: func(t *testing.T) string { return lapiAnswering(t, http.StatusInternalServerError) }}, + {name: "unauthorized", url: func(t *testing.T) string { return lapiAnswering(t, http.StatusUnauthorized) }}, + {name: "redirect", url: func(t *testing.T) string { return lapiAnswering(t, http.StatusFound) }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + h := newBouncerHarness(t) + shortenStartupKeyCheck(t) + t.Setenv("CHARON_SECURITY_CROWDSEC_API_URL", tc.url(t)) + h.writeFileKey(t, testFileKey) + exec := &scriptedBouncerExecutor{} + + require.NoError(t, ensureBouncerRegistration(h.dataDir, h.keyFile, exec)) + + require.False(t, exec.called("bouncers"), "%s is not a definite rejection", tc.name) + require.Equal(t, testFileKey, bouncerkey.ReadFile(h.keyFile)) + }) + } +} + +// TestEnsureBouncerRegistrationRecoversWithinWindow: a 500 followed by a 200 inside the window is accepted, so the +// saved key is kept without any bouncer command. +func TestEnsureBouncerRegistrationRecoversWithinWindow(t *testing.T) { + h := newBouncerHarness(t) + shortenStartupKeyCheck(t) + var requests atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if requests.Add(1) == 1 { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + t.Setenv("CHARON_SECURITY_CROWDSEC_API_URL", srv.URL) + h.writeFileKey(t, testFileKey) + exec := &scriptedBouncerExecutor{} + + require.NoError(t, ensureBouncerRegistration(h.dataDir, h.keyFile, exec)) + + require.GreaterOrEqual(t, requests.Load(), int32(2), "the check retries inside the window") + require.False(t, exec.called("bouncers")) + require.Equal(t, testFileKey, bouncerkey.ReadFile(h.keyFile)) +} + +// TestEnsureBouncerRegistrationDefiniteRejectionStillRotates: a 403 is still the one definite rejection, so the +// saved key is replaced by a registered one. +func TestEnsureBouncerRegistrationDefiniteRejectionStillRotates(t *testing.T) { + h := newBouncerHarness(t) + shortenStartupKeyCheck(t) + h.writeFileKey(t, testRejectedEnvKey) + exec := &scriptedBouncerExecutor{addOutput: []byte(testGeneratedKey + "\n")} + + require.NoError(t, ensureBouncerRegistration(h.dataDir, h.keyFile, exec)) + + require.True(t, exec.called("bouncers delete")) + require.True(t, exec.called("bouncers add")) + require.Equal(t, testGeneratedKey, bouncerkey.ReadFile(h.keyFile)) +} + +// TestLogStartupKeyDecisionUnverifiedIsNeutral: the unverified outcome has its own log line, with no key text. +func TestLogStartupKeyDecisionUnverifiedIsNeutral(t *testing.T) { + h := newBouncerHarness(t) + + logStartupKeyDecision(bouncerkey.EnsureResult{Outcome: bouncerkey.OutcomeUnverified, Key: logProbeKey}, h.keyFile, nil) + + out := h.channels(h.drain()) + require.Contains(t, strings.Join(out, "\n"), bouncerkey.UnverifiedMessage) + assertNoKeyFragments(t, out, logProbeKey) +} From f2990b57a3598a994e1c6fd6bfce9df5088270bd Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 20:05:54 +0000 Subject: [PATCH 019/291] fix: keep a new CrowdSec key in use when saving it fails --- .../internal/api/handlers/crowdsec_handler.go | 17 + .../api/handlers/crowdsec_key_held_test.go | 207 ++++++++++++ backend/internal/caddy/config.go | 2 + .../internal/caddy/config_held_key_test.go | 24 ++ .../crowdsec/bouncerkey/bouncerkey.go | 95 +++++- .../crowdsec/bouncerkey/held_ensure_test.go | 310 ++++++++++++++++++ backend/internal/services/crowdsec_startup.go | 2 + .../services/crowdsec_startup_held_test.go | 66 ++++ 8 files changed, 714 insertions(+), 9 deletions(-) create mode 100644 backend/internal/api/handlers/crowdsec_key_held_test.go create mode 100644 backend/internal/caddy/config_held_key_test.go create mode 100644 backend/internal/crowdsec/bouncerkey/held_ensure_test.go create mode 100644 backend/internal/services/crowdsec_startup_held_test.go diff --git a/backend/internal/api/handlers/crowdsec_handler.go b/backend/internal/api/handlers/crowdsec_handler.go index f0e217b73..afa047710 100644 --- a/backend/internal/api/handlers/crowdsec_handler.go +++ b/backend/internal/api/handlers/crowdsec_handler.go @@ -1790,7 +1790,18 @@ func (h *CrowdsecHandler) GetKeyStatus(c *gin.Context) { // The key check can run while a Start is rotating the key (after its delete, before its save). This status // can then show the old key as rejected for one response. Nothing destructive reads this response, so that // brief display glitch is accepted rather than locking the file and the LAPI check together. + // A key held in memory is the key in use while the file holds a different one (mirrors bouncerkey.Resolve). + // It is reported with a preview only, never in full. The status and the check come from the one held value. + heldKey := bouncerkey.Held(keyPath) + switch { + case heldKey != "" && heldKey != fileKey: + response.KeySource = "memory" + response.CurrentKeyPreview = util.MaskAPIKey(heldKey) + applyKeyCheck(&response, h.keyStillValid(c, heldKey)) + if response.Message == "" { + response.Message = bouncerkey.SaveFailedNotice(keyPath) + } case rej.envKeyRejected && fileKey != "" && !rej.saveFailed && !rej.registerFailed: // Charon saved a replacement key, so the file holds it; give the full key so the user can fix docker-compose.yml. // After a failed save or registration the file still holds the rejected key, so it is reported as a file key below. @@ -1934,6 +1945,8 @@ func (h *CrowdsecHandler) handleBouncerRegistration(res bouncerkey.EnsureResult, } case bouncerkey.OutcomeUnverified: logger.Log().WithField("file", keyPath).Warn(bouncerkey.UnverifiedMessage) + case bouncerkey.OutcomeHeldKeyValid: + logger.Log().WithField("file", keyPath).Info(bouncerkey.HeldKeyInUseMessage(res.HeldSaveErr == nil)) } } @@ -2090,6 +2103,8 @@ func (h *CrowdsecHandler) GetBouncerInfo(c *gin.Context) { info.KeySource = "env_var" case bouncerkey.SourceFile: info.KeySource = "file" + case bouncerkey.SourceMemory: + info.KeySource = "memory" default: info.KeySource = "none" } @@ -2116,6 +2131,8 @@ func (h *CrowdsecHandler) GetBouncerKey(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"key": key, "source": "env_var"}) case bouncerkey.SourceFile: c.JSON(http.StatusOK, gin.H{"key": key, "source": "file"}) + case bouncerkey.SourceMemory: + c.JSON(http.StatusOK, gin.H{"key": key, "source": "memory"}) default: c.JSON(http.StatusNotFound, gin.H{"error": "No bouncer key configured"}) } diff --git a/backend/internal/api/handlers/crowdsec_key_held_test.go b/backend/internal/api/handlers/crowdsec_key_held_test.go new file mode 100644 index 000000000..bd6d5b54c --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_key_held_test.go @@ -0,0 +1,207 @@ +package handlers + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "slices" + "strings" + "sync" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" + "github.com/Wikid82/charon/backend/internal/util" +) + +// heldKeyCmd answers every cscli call and gives bouncers add the key precNewKey. It records the bouncer calls +// so a test can see whether a Start deleted or added a bouncer. +type heldKeyCmd struct { + mu sync.Mutex + calls []string +} + +func (c *heldKeyCmd) Execute(_ context.Context, _ string, args ...string) ([]byte, error) { + c.mu.Lock() + defer c.mu.Unlock() + c.calls = append(c.calls, strings.Join(args, " ")) + switch { + case slices.Contains(args, "bouncers") && slices.Contains(args, "add"): + return []byte(precNewKey), nil + case slices.Contains(args, "lapi"): + return []byte("ok"), nil + default: + return nil, nil + } +} + +func (c *heldKeyCmd) bouncerCalls() []string { + c.mu.Lock() + defer c.mu.Unlock() + var out []string + for _, call := range c.calls { + if strings.Contains(call, "bouncers") { + out = append(out, call) + } + } + return out +} + +// unwritableKeyHandler builds a Start handler whose key directory and saved key file are read-only. The saved +// key is rejected by LAPI and the registered key (precNewKey) is accepted, so the first Start registers a key +// it cannot save and holds it in memory. +func unwritableKeyHandler(t *testing.T) (h *CrowdsecHandler, keyPath string, cmd *heldKeyCmd, applier *recordingApplier) { + t.Helper() + clearKeyEnv(t, "") + dataDir := t.TempDir() + keyPath = filepath.Join(dataDir, "bouncer_key") + writeSavedKey(t, keyPath, precSavedKey) + require.NoError(t, os.Chmod(keyPath, 0o400)) + require.NoError(t, os.Chmod(dataDir, 0o500)) + t.Cleanup(func() { + _ = os.Chmod(dataDir, 0o700) + _ = os.Chmod(keyPath, 0o600) + bouncerkey.Release(keyPath) + }) + + lapiURL, _ := newKeyLAPI(t, precNewKey) + cmd = &heldKeyCmd{} + applier = &recordingApplier{} + h = newStartApplyHandler(t, lapiURL, dataDir, cmd, &fakeExec{}, applier) + h.keyCheckWait = 300 * time.Millisecond + return h, keyPath, cmd, applier +} + +// TestStartHoldsUnsavedKeyAndSecondStartKeepsBouncer: a registered key that cannot be saved is held and used. +// A second Start with the file still read-only validates the held key and does not delete or add a bouncer. +func TestStartHoldsUnsavedKeyAndSecondStartKeepsBouncer(t *testing.T) { + h, keyPath, cmd, applier := unwritableKeyHandler(t) + + first := startRequest(t, h, context.Background()) + require.Equal(t, http.StatusOK, first.Code, first.Body.String()) + require.Equal(t, precNewKey, bouncerkey.Held(keyPath), "the unsaved key is held in memory") + afterFirst := cmd.bouncerCalls() + require.Len(t, afterFirst, 2, "the first Start deletes and adds the bouncer once") + + second := startRequest(t, h, context.Background()) + require.Equal(t, http.StatusOK, second.Code, second.Body.String()) + require.Equal(t, afterFirst, cmd.bouncerCalls(), "the second Start must not delete or add a bouncer") + require.Equal(t, precNewKey, bouncerkey.Held(keyPath), "a still-unwritable file keeps the hold") + require.Equal(t, 2, applier.count(), "each Start applies the proxy config once") + + var body map[string]any + require.NoError(t, json.Unmarshal(second.Body.Bytes(), &body)) + require.Empty(t, body["warning"], "a held key that validates is not an unverified key") +} + +// TestGetKeyStatusHeldKeyReportsMemoryWithoutFullKey: a held key is reported as memory, with a preview only. +func TestGetKeyStatusHeldKeyReportsMemoryWithoutFullKey(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + lapiURL, _ := newKeyLAPI(t, precNewKey) + h := newKeyCheckHandler(t, lapiURL, dataDir, noCSCLIMock()) + keyPath := h.bouncerKeyPath() + bouncerkey.Hold(keyPath, precNewKey) + t.Cleanup(func() { bouncerkey.Release(keyPath) }) + + body := keyStatusFor(t, h) + + require.Equal(t, "memory", body["key_source"]) + require.Equal(t, "accepted", body["key_check"]) + require.Equal(t, true, body["valid"]) + require.NotContains(t, body, "full_key", "a held key is never returned in full") + require.Equal(t, util.MaskAPIKey(precNewKey), body["current_key_preview"]) + require.Equal(t, bouncerkey.SaveFailedNotice(keyPath), body["message"]) + encoded, err := json.Marshal(body) + require.NoError(t, err) + require.NotContains(t, string(encoded), precNewKey) +} + +// TestBouncerInfoAndKeyReportMemorySource: the bouncer info and the key endpoint use the held key and say so. +func TestBouncerInfoAndKeyReportMemorySource(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + lapiURL, _ := newKeyLAPI(t, precNewKey) + h := newKeyCheckHandler(t, lapiURL, dataDir, &heldKeyCmd{}) + keyPath := h.bouncerKeyPath() + bouncerkey.Hold(keyPath, precNewKey) + t.Cleanup(func() { bouncerkey.Release(keyPath) }) + + infoW := httptest.NewRecorder() + infoCtx, _ := gin.CreateTestContext(infoW) + infoCtx.Request = httptest.NewRequest(http.MethodGet, "/admin/crowdsec/bouncer", http.NoBody) + h.GetBouncerInfo(infoCtx) + require.Equal(t, http.StatusOK, infoW.Code) + var info map[string]any + require.NoError(t, json.Unmarshal(infoW.Body.Bytes(), &info)) + require.Equal(t, "memory", info["key_source"]) + + keyW := httptest.NewRecorder() + keyCtx, _ := gin.CreateTestContext(keyW) + keyCtx.Request = httptest.NewRequest(http.MethodGet, "/admin/crowdsec/bouncer/key", http.NoBody) + h.GetBouncerKey(keyCtx) + require.Equal(t, http.StatusOK, keyW.Code) + var key map[string]any + require.NoError(t, json.Unmarshal(keyW.Body.Bytes(), &key)) + require.Equal(t, precNewKey, key["key"]) + require.Equal(t, "memory", key["source"]) +} + +// TestEnsureBouncerRegistrationHeldKeyKeepsRejectionFlags: a held key that validates leaves the rejection state +// as it was (the earlier save failure is still true). +func TestEnsureBouncerRegistrationHeldKeyKeepsRejectionFlags(t *testing.T) { + clearKeyEnv(t, "") + dataDir := t.TempDir() + lapiURL, _ := newKeyLAPI(t, precNewKey) + h := newKeyCheckHandler(t, lapiURL, dataDir, noCSCLIMock()) + keyPath := h.bouncerKeyPath() + bouncerkey.Hold(keyPath, precNewKey) + t.Cleanup(func() { bouncerkey.Release(keyPath) }) + h.envKeyRejected = true + h.rejectedEnvKey = precEnvKeyOther + h.rejectedEnvSaveFailed = true + + res, err := h.ensureBouncerRegistration(context.Background()) + + require.NoError(t, err) + require.Equal(t, bouncerkey.OutcomeHeldKeyValid, res.Outcome) + require.Equal(t, precNewKey, res.Key) + require.True(t, h.envKeyRejected) + require.Equal(t, precEnvKeyOther, h.rejectedEnvKey) + require.True(t, h.rejectedEnvSaveFailed) +} + +// TestHandleBouncerRegistrationHeldKeyLogsInfoWithoutKey: a held key that validates is one Info line, with no +// key text and no Error or Warn. +func TestHandleBouncerRegistrationHeldKeyLogsInfoWithoutKey(t *testing.T) { + cases := []struct { + name string + saveErr error + want string + }{ + {name: "saved", saveErr: nil, want: "was saved to the key file"}, + {name: "still unwritable", saveErr: os.ErrPermission, want: "could not be written"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + buf := captureLogs(t) + h := newPrecedenceHandler(t, "http://127.0.0.1:1", t.TempDir(), noCSCLIMock()) + res := bouncerkey.EnsureResult{Key: precNewKey, Outcome: bouncerkey.OutcomeHeldKeyValid, HeldSaveErr: tc.saveErr} + + h.handleBouncerRegistration(res, nil) + + out := buf.String() + require.Contains(t, out, tc.want) + require.Contains(t, out, `"level":"info"`) + require.NotContains(t, out, `"level":"warning"`) + require.NotContains(t, out, `"level":"error"`) + require.NotContains(t, out, precNewKey, "the held key must never be logged") + }) + } +} diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go index bcf8b258a..9a63d25c1 100644 --- a/backend/internal/caddy/config.go +++ b/backend/internal/caddy/config.go @@ -1133,6 +1133,8 @@ func getCrowdSecAPIKey() string { logger.Log().WithField("source", "file").WithField("file", crowdSecBouncerKeyFile).Debug("CrowdSec API key loaded from file") case bouncerkey.SourceEnv: logger.Log().WithField("source", "env_var").Debug("CrowdSec API key loaded from environment variable") + case bouncerkey.SourceMemory: + logger.Log().WithField("source", "memory").Debug("CrowdSec API key held in memory after a failed save") default: logger.Log().Debug("No CrowdSec API key found in file or environment variables") } diff --git a/backend/internal/caddy/config_held_key_test.go b/backend/internal/caddy/config_held_key_test.go new file mode 100644 index 000000000..bbb4b9da4 --- /dev/null +++ b/backend/internal/caddy/config_held_key_test.go @@ -0,0 +1,24 @@ +package caddy + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" +) + +// TestGetCrowdSecAPIKeyUsesHeldKey: a key that could not be saved is the key the proxy config carries, ahead of a +// different saved file and any environment variable. +func TestGetCrowdSecAPIKeyUsesHeldKey(t *testing.T) { + clearCrowdSecKeyEnv(t) + t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "env-key-held-caddy-0000000001") + path := filepath.Join(t.TempDir(), "bouncer_key") + useBouncerKeyFile(t, path) + const held = "held-caddy-key-00000000000001" + bouncerkey.Hold(path, held) + t.Cleanup(func() { bouncerkey.Release(path) }) + + require.Equal(t, held, getCrowdSecAPIKey()) +} diff --git a/backend/internal/crowdsec/bouncerkey/bouncerkey.go b/backend/internal/crowdsec/bouncerkey/bouncerkey.go index e7fa8a204..a053dfd30 100644 --- a/backend/internal/crowdsec/bouncerkey/bouncerkey.go +++ b/backend/internal/crowdsec/bouncerkey/bouncerkey.go @@ -44,6 +44,10 @@ var ( // Hold remembers key for path after a save to path failed, so readers in this // process keep using the key that CrowdSec accepted. +// +// The hold is in memory only. A restart drops it: the next boot checks the key file +// again and registers a new bouncer when the file key is rejected, and the Error log +// and the status notice say that the key is not saved. func Hold(path, key string) { heldMu.Lock() defer heldMu.Unlock() @@ -116,11 +120,22 @@ const ( SourceMemory Source = "memory" ) -// Resolve returns the key the proxy uses: the saved key file first, then the -// first non-empty key environment variable. +// Resolve returns the key the proxy uses: a key held in memory, then the saved key +// file, then the first non-empty key environment variable. +// +// A held key wins while the file does not hold the same key, including when the file is +// empty. Once the file holds it, the hold is dropped and the file is the source. Between +// Ensure runs this is the only check, since only Ensure can validate a key. func Resolve(path string) (key string, src Source) { - if key := ReadFile(path); key != "" { - return key, SourceFile + file := ReadFile(path) + if held := Held(path); held != "" { + if held != file { + return held, SourceMemory + } + Release(path) + } + if file != "" { + return file, SourceFile } if val, _ := FromEnv(); val != "" { return val, SourceEnv @@ -143,6 +158,9 @@ const ( // OutcomeUnverified means LAPI could not give a verdict on a present key. The key is kept as it // was, nothing is registered, deleted or saved, and the caller reports it as unverified. OutcomeUnverified + // OutcomeHeldKeyValid means the key held in memory after a failed save validated. Nothing is + // registered, deleted or saved except one best-effort save of the held key. + OutcomeHeldKeyValid ) // EnsureResult reports the key chosen by Ensure and the facts callers need to @@ -160,6 +178,9 @@ type EnsureResult struct { EnvKeyRejected bool // SaveErr is a failed save of Key. Nothing else holds the key, so readers of the key file do not see it. SaveErr error + // HeldSaveErr is the failed best-effort save of a held key that validated (OutcomeHeldKeyValid). It is + // reported at Info, not Error, because the key is already in use from memory. + HeldSaveErr error } // Ensure decides which bouncer key to use and makes the key file match it. @@ -169,6 +190,11 @@ type EnsureResult struct { // is never evidence against a key. // // Rules, in order: +// 0. A key held in memory after a failed save is tested first, unless the file already +// holds it (the hold is then dropped). Accepted: OutcomeHeldKeyValid, with one best-effort +// save that releases the hold on success and reports a failure in HeldSaveErr only. +// Unknown: OutcomeUnverified with the held key. Rejected: the hold is dropped and rules +// 1-3 run as if nothing were held. // 1. A saved key that validates is kept. The env value is not tested. // An unknown saved key stops the decision as OutcomeUnverified. // 2. Otherwise an env value that differs from the saved key and validates is saved. @@ -182,8 +208,9 @@ type EnsureResult struct { // Its Key is the saved key, or the unknown env value when no saved key exists. // // A save failure after rule 2 or 3 is reported in EnsureResult.SaveErr, not as -// the error. A register failure is returned as the error, with the rejection -// facts still filled in. Ensure does not log; callers do. +// the error, and the new key is held in memory so the proxy keeps using it. A +// register failure is returned as the error, with the rejection facts still +// filled in. Ensure does not log; callers do. func Ensure( ctx context.Context, path string, @@ -195,6 +222,23 @@ func Ensure( saved := ReadFile(path) res := EnsureResult{EnvKey: env} + if held := Held(path); held != "" { + if held == saved { + Release(path) + } else { + switch validate(ctx, held) { + case VerdictAccepted: + return heldKeyValid(res, path, held, save), nil + case VerdictUnknown: + res.Key = held + res.Outcome = OutcomeUnverified + return res, nil + default: + Release(path) + } + } + } + if saved != "" { switch validate(ctx, saved) { case VerdictAccepted: @@ -213,6 +257,7 @@ func Ensure( res.Key = env res.Outcome = OutcomeEnvKeySaved res.SaveErr = save(path, env) + settleNewKey(path, &res) return res, nil case VerdictUnknown: return unverified(res, saved, env), nil @@ -233,9 +278,32 @@ func Ensure( res.Key = newKey res.Outcome = OutcomeRegistered res.SaveErr = save(path, newKey) + settleNewKey(path, &res) return res, nil } +// settleNewKey records a newly chosen key: a failed save holds it in memory, a successful save releases any hold. +func settleNewKey(path string, res *EnsureResult) { + if res.SaveErr != nil { + Hold(path, res.Key) + return + } + Release(path) +} + +// heldKeyValid finishes an Ensure result whose held key validated. The key is in use from memory either way. +// One best-effort save writes it to the key file; a success releases the hold. Nothing is registered or +// deleted, and a failed save is reported in HeldSaveErr only. +func heldKeyValid(res EnsureResult, path, held string, save func(path, key string) error) EnsureResult { + res.Key = held + res.Outcome = OutcomeHeldKeyValid + res.HeldSaveErr = save(path, held) + if res.HeldSaveErr == nil { + Release(path) + } + return res +} + // unverified finishes an Ensure result whose key could not be checked. The saved key stays in // use; with no saved key, the unknown env value is the key the proxy reads. func unverified(res EnsureResult, saved, unknownEnv string) EnsureResult { @@ -247,14 +315,23 @@ func unverified(res EnsureResult, saved, unknownEnv string) EnsureResult { return res } -// SaveFailedNotice says the new key could not be written to path. The new key is held nowhere else, so -// CrowdSec blocking depends on the key file becoming writable. +// SaveFailedNotice says the new key could not be written to path. It is held in memory, so it is used until +// the process restarts; after a restart the key file must be writable again. func SaveFailedNotice(path string) string { return "Charon could not save the new key to " + path + - "; CrowdSec blocking may not work until the key file can be written. " + + ". It uses the new key until it restarts; after a restart CrowdSec blocking may not work until the key file can be written. " + "Fix write access to the data directory and restart CrowdSec." } +// HeldKeyInUseMessage says a key held in memory validated and is in use. saved reports whether this check +// wrote it to the key file. Callers log it at Info; it names no key material. +func HeldKeyInUseMessage(saved bool) string { + if saved { + return "CrowdSec bouncer: the key held in memory was saved to the key file and is in use" + } + return "CrowdSec bouncer: the key held in memory is still in use; the key file could not be written" +} + // RegisterFailedNotice says no replacement key could be registered, so no new key was saved to path. func RegisterFailedNotice(path string) string { return "Charon could not register a new key with CrowdSec, so no new key was saved to " + path + diff --git a/backend/internal/crowdsec/bouncerkey/held_ensure_test.go b/backend/internal/crowdsec/bouncerkey/held_ensure_test.go new file mode 100644 index 000000000..023dd0821 --- /dev/null +++ b/backend/internal/crowdsec/bouncerkey/held_ensure_test.go @@ -0,0 +1,310 @@ +package bouncerkey + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +// Keys for the held-key rows. The fake accepts testHeldValid, answers Unknown for testHeldUnknown and +// rejects every other key. +const ( + testHeldValid = "held-valid-key-0000001" + testHeldUnknown = "held-unknown-key-000001" + testHeldInvalid = "held-invalid-key-000001" +) + +// newHeldFake returns the shared fake with the held keys added to its verdict tables. +func newHeldFake() *fakeLAPI { + fake := newFakeLAPI() + fake.valid[testHeldValid] = true + fake.unknown = map[string]bool{testHeldUnknown: true} + return fake +} + +// TestEnsureHeldKeyMatrix covers a held key (candidate 0) against the saved and env keys. A held key that +// validates is kept and nothing is registered or deleted; one that is Unknown stops as OutcomeUnverified; one +// that is rejected is released and the saved and env candidates decide as they do without a hold. +func TestEnsureHeldKeyMatrix(t *testing.T) { + rows := []struct { + name string + held string + saved string + env string + wantOutcome Outcome + wantValidated []string + wantRegisters int + wantSaved []string + wantKey string + wantFile string + wantHeldAfter string + }{ + { + name: "held accepted, saved none, env none", held: testHeldValid, + wantOutcome: OutcomeHeldKeyValid, wantValidated: []string{testHeldValid}, + wantSaved: []string{testHeldValid}, wantKey: testHeldValid, wantFile: testHeldValid, + }, + { + name: "held accepted, saved invalid, env valid different", held: testHeldValid, + saved: testSavedInvalid, env: testEnvValid, + wantOutcome: OutcomeHeldKeyValid, wantValidated: []string{testHeldValid}, + wantSaved: []string{testHeldValid}, wantKey: testHeldValid, wantFile: testHeldValid, + }, + { + name: "held accepted, saved valid (restored backup), env invalid", held: testHeldValid, + saved: testSavedValid, env: testEnvInvalid, + wantOutcome: OutcomeHeldKeyValid, wantValidated: []string{testHeldValid}, + wantSaved: []string{testHeldValid}, wantKey: testHeldValid, wantFile: testHeldValid, + }, + { + name: "held equals saved, accepted", held: testHeldValid, saved: testHeldValid, + wantOutcome: OutcomeSavedKeyValid, wantValidated: []string{testHeldValid}, + wantKey: testHeldValid, wantFile: testHeldValid, + }, + { + name: "held unknown, saved valid, env valid", held: testHeldUnknown, + saved: testSavedValid, env: testEnvValid, + wantOutcome: OutcomeUnverified, wantValidated: []string{testHeldUnknown}, + wantKey: testHeldUnknown, wantFile: testSavedValid, wantHeldAfter: testHeldUnknown, + }, + { + name: "held unknown, saved none, env none", held: testHeldUnknown, + wantOutcome: OutcomeUnverified, wantValidated: []string{testHeldUnknown}, + wantKey: testHeldUnknown, wantFile: "", wantHeldAfter: testHeldUnknown, + }, + { + name: "held rejected, saved valid, env none", held: testHeldInvalid, saved: testSavedValid, + wantOutcome: OutcomeSavedKeyValid, wantValidated: []string{testHeldInvalid, testSavedValid}, + wantKey: testSavedValid, wantFile: testSavedValid, + }, + { + name: "held rejected, saved none, env valid", held: testHeldInvalid, env: testEnvValid, + wantOutcome: OutcomeEnvKeySaved, wantValidated: []string{testHeldInvalid, testEnvValid}, + wantSaved: []string{testEnvValid}, wantKey: testEnvValid, wantFile: testEnvValid, + }, + { + name: "held rejected, saved none, env none", held: testHeldInvalid, + wantOutcome: OutcomeRegistered, wantValidated: []string{testHeldInvalid}, + wantRegisters: 1, wantSaved: []string{testRegistered}, wantKey: testRegistered, wantFile: testRegistered, + }, + { + name: "held rejected, saved invalid, env invalid", held: testHeldInvalid, + saved: testSavedInvalid, env: testEnvInvalid, + wantOutcome: OutcomeRegistered, wantValidated: []string{testHeldInvalid, testSavedInvalid, testEnvInvalid}, + wantRegisters: 1, wantSaved: []string{testRegistered}, wantKey: testRegistered, wantFile: testRegistered, + }, + } + + for _, row := range rows { + t.Run(row.name, func(t *testing.T) { + path := seedKeyFile(t, row.saved) + setOnlyEnvKey(t, row.env) + Hold(path, row.held) + t.Cleanup(func() { Release(path) }) + fake := newHeldFake() + + res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.NoError(t, res.SaveErr, "the save of a held key is reported as HeldSaveErr, not SaveErr") + require.NoError(t, res.HeldSaveErr) + require.Equal(t, row.wantOutcome, res.Outcome, "outcome") + require.Equal(t, row.wantKey, res.Key, "returned key") + require.Equal(t, row.wantValidated, fake.validated, "validate calls") + require.Equal(t, row.wantRegisters, fake.registers, "register calls") + require.Equal(t, row.wantSaved, fake.saved, "save calls") + require.Equal(t, row.wantFile, ReadFile(path), "key file after Ensure") + require.Equal(t, row.wantHeldAfter, Held(path), "held key after Ensure") + }) + } +} + +// TestEnsureHeldAcceptedSaveFailureKeepsHold: a held key that validates but cannot be written again is still +// used, nothing is registered or deleted, the failure is reported only as HeldSaveErr, and the hold stays. +func TestEnsureHeldAcceptedSaveFailureKeepsHold(t *testing.T) { + path := seedKeyFile(t, testSavedInvalid) + setOnlyEnvKey(t, "") + Hold(path, testHeldValid) + t.Cleanup(func() { Release(path) }) + fake := newHeldFake() + fake.saveErr = errors.New("read-only file system") + + res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.Equal(t, OutcomeHeldKeyValid, res.Outcome) + require.Equal(t, testHeldValid, res.Key) + require.ErrorContains(t, res.HeldSaveErr, "read-only file system") + require.NoError(t, res.SaveErr) + require.Zero(t, fake.registers) + require.Equal(t, testHeldValid, Held(path), "a failed retry keeps the key in memory") + require.Equal(t, testSavedInvalid, ReadFile(path), "the key file is unchanged") +} + +// TestEnsureSaveFailureHoldsNewKey: a new key that cannot be saved (env replacement or registration) is held +// in memory, so the proxy keeps using it. A successful save leaves nothing held. +func TestEnsureSaveFailureHoldsNewKey(t *testing.T) { + rows := []struct { + name string + saved string + env string + wantHeld string + wantOut Outcome + }{ + {name: "env replacement", saved: "", env: testEnvValid, wantHeld: testEnvValid, wantOut: OutcomeEnvKeySaved}, + {name: "registration", saved: testSavedInvalid, env: "", wantHeld: testRegistered, wantOut: OutcomeRegistered}, + } + for _, row := range rows { + t.Run(row.name, func(t *testing.T) { + path := seedKeyFile(t, row.saved) + setOnlyEnvKey(t, row.env) + t.Cleanup(func() { Release(path) }) + fake := newHeldFake() + fake.saveErr = errors.New("read-only file system") + + res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.Equal(t, row.wantOut, res.Outcome) + require.ErrorContains(t, res.SaveErr, "read-only file system") + require.Equal(t, row.wantHeld, Held(path)) + require.Equal(t, row.wantHeld, res.Key) + }) + } +} + +// TestEnsureSuccessfulSaveLeavesNothingHeld: a new key that is saved releases any hold for the path. +func TestEnsureSuccessfulSaveLeavesNothingHeld(t *testing.T) { + path := seedKeyFile(t, "") + setOnlyEnvKey(t, testEnvValid) + t.Cleanup(func() { Release(path) }) + fake := newHeldFake() + + res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.NoError(t, res.SaveErr) + require.Equal(t, OutcomeEnvKeySaved, res.Outcome) + require.Empty(t, Held(path)) +} + +// TestEnsureHeldUnknownDoesNotTestEnv: an Unknown held key stays held, and no env candidate is tested after it. +func TestEnsureHeldUnknownDoesNotTestEnv(t *testing.T) { + path := seedKeyFile(t, "") + setOnlyEnvKey(t, testEnvValid) + Hold(path, testHeldUnknown) + t.Cleanup(func() { Release(path) }) + fake := newHeldFake() + + res, err := Ensure(context.Background(), path, fake.validate, fake.register, fake.save) + + require.NoError(t, err) + require.Equal(t, OutcomeUnverified, res.Outcome) + require.Equal(t, []string{testHeldUnknown}, fake.validated) + require.Equal(t, testHeldUnknown, Held(path)) +} + +// TestResolveHeldWinsOverFileAndEnv: while the file does not hold the same key, Resolve returns the held key +// from memory, ahead of both the file and the env value. +func TestResolveHeldWinsOverFileAndEnv(t *testing.T) { + cases := []struct { + name string + file string + }{ + {name: "file has another valid key", file: testSavedValid}, + {name: "file is empty", file: ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + path := seedKeyFile(t, tc.file) + setOnlyEnvKey(t, testEnvValid) + Hold(path, testHeldValid) + t.Cleanup(func() { Release(path) }) + + key, src := Resolve(path) + + require.Equal(t, testHeldValid, key) + require.Equal(t, SourceMemory, src) + require.Equal(t, testHeldValid, Held(path), "a different file does not drop the hold") + }) + } +} + +// TestResolveReleasesHeldOnceFileMatches: once the key file holds the held key, the file is the source and the +// in-memory entry is dropped. +func TestResolveReleasesHeldOnceFileMatches(t *testing.T) { + path := seedKeyFile(t, testHeldValid) + setOnlyEnvKey(t, testEnvValid) + Hold(path, testHeldValid) + + key, src := Resolve(path) + + require.Equal(t, testHeldValid, key) + require.Equal(t, SourceFile, src) + require.Empty(t, Held(path)) +} + +// TestSaveFailedNoticeWording pins the reworded notice shared by the handler, the startup log and the banner. +func TestSaveFailedNoticeWording(t *testing.T) { + const path = "/app/data/crowdsec/bouncer_key" + + require.Equal(t, + "Charon could not save the new key to "+path+ + ". It uses the new key until it restarts; after a restart CrowdSec blocking may not work until the key file can be written. "+ + "Fix write access to the data directory and restart CrowdSec.", + SaveFailedNotice(path)) +} + +// TestReleaseRacesHoldAndResolve runs Hold, Resolve and Release from many goroutines on one path while the +// file is rewritten. It must pass under -race, and every result must be a key some worker held, the file key, +// or nothing. +func TestReleaseRacesHoldAndResolve(t *testing.T) { + t.Parallel() + + const workers = 12 + const rounds = 150 + path := seedKeyFile(t, "") + const fileKey = "race-file-key-0000000000000001" + + var wg sync.WaitGroup + for w := range workers { + heldKey := fmt.Sprintf("race-held-key-%02d-0000000000000000", w) + wg.Add(1) + go func() { + defer wg.Done() + for range rounds { + Hold(path, heldKey) + key, src := Resolve(path) + switch src { + case SourceMemory: + if !strings.HasPrefix(key, "race-held-key-") { + t.Errorf("memory source returned %q", key) + } + case SourceFile: + if key != fileKey { + t.Errorf("file source returned %q", key) + } + } + Release(path) + } + }() + } + wg.Add(1) + go func() { + defer wg.Done() + for range rounds { + _ = os.WriteFile(path, []byte(fileKey+"\n"), 0o600) + _ = os.Remove(path) + } + }() + wg.Wait() + + Release(path) + require.Empty(t, Held(path)) +} diff --git a/backend/internal/services/crowdsec_startup.go b/backend/internal/services/crowdsec_startup.go index e6b1427c1..48cd06dd2 100644 --- a/backend/internal/services/crowdsec_startup.go +++ b/backend/internal/services/crowdsec_startup.go @@ -447,6 +447,8 @@ func logStartupKeyDecision(res bouncerkey.EnsureResult, keyFile string, regErr e } case bouncerkey.OutcomeUnverified: logger.Log().WithField("key_file", keyFile).Warn(bouncerkey.UnverifiedMessage) + case bouncerkey.OutcomeHeldKeyValid: + logger.Log().WithField("key_file", keyFile).Info(bouncerkey.HeldKeyInUseMessage(res.HeldSaveErr == nil)) } } diff --git a/backend/internal/services/crowdsec_startup_held_test.go b/backend/internal/services/crowdsec_startup_held_test.go new file mode 100644 index 000000000..5c6e1c8ef --- /dev/null +++ b/backend/internal/services/crowdsec_startup_held_test.go @@ -0,0 +1,66 @@ +package services + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" +) + +// bouncerCommandCount counts the bouncer cscli calls the scripted executor recorded. +func bouncerCommandCount(e *scriptedBouncerExecutor) int { + e.mu.Lock() + defer e.mu.Unlock() + n := 0 + for _, c := range e.calls { + if strings.Contains(strings.Join(c, " "), "bouncers") { + n++ + } + } + return n +} + +// TestEnsureBouncerRegistrationHeldKeySurvivesSecondBoot: a registered key that cannot be saved is held, and +// a second boot in the same process validates it and registers nothing new. +func TestEnsureBouncerRegistrationHeldKeySurvivesSecondBoot(t *testing.T) { + h := newBouncerHarness(t) + h.writeFileKey(t, "rejected-saved-key-000000000001") + keyDir := filepath.Dir(h.keyFile) + require.NoError(t, os.Chmod(h.keyFile, 0o400)) + require.NoError(t, os.Chmod(keyDir, 0o500)) + t.Cleanup(func() { + _ = os.Chmod(keyDir, 0o700) + _ = os.Chmod(h.keyFile, 0o600) + bouncerkey.Release(h.keyFile) + }) + exec := &scriptedBouncerExecutor{addOutput: []byte(testGeneratedKey)} + + require.NoError(t, ensureBouncerRegistration(h.dataDir, h.keyFile, exec)) + require.Equal(t, testGeneratedKey, bouncerkey.Held(h.keyFile), "the unsaved key is held") + afterFirst := bouncerCommandCount(exec) + require.Equal(t, 2, afterFirst, "the first boot deletes and adds the bouncer once") + + require.NoError(t, ensureBouncerRegistration(h.dataDir, h.keyFile, exec)) + require.Equal(t, afterFirst, bouncerCommandCount(exec), "the second boot must not delete or add a bouncer") + require.Equal(t, testGeneratedKey, bouncerkey.Held(h.keyFile)) +} + +// TestLogStartupKeyDecisionHeldKeyIsInfoWithoutKey: a held key that validates logs one Info line and no key text. +func TestLogStartupKeyDecisionHeldKeyIsInfoWithoutKey(t *testing.T) { + h := newBouncerHarness(t) + + logStartupKeyDecision(bouncerkey.EnsureResult{ + Outcome: bouncerkey.OutcomeHeldKeyValid, + Key: testGeneratedKey, + HeldSaveErr: errors.New("read-only file system"), + }, h.keyFile, nil) + + out := h.channels(h.drain()) + require.Contains(t, strings.Join(out, "\n"), "still in use") + assertNoKeyFragments(t, out, testGeneratedKey) +} From 98c880ab9d0831afde4b8acea9569c96e4c1fe72 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 20:40:38 +0000 Subject: [PATCH 020/291] fix: use one CrowdSec key file path everywhere --- backend/cmd/api/main.go | 8 +- .../internal/api/handlers/crowdsec_handler.go | 12 +- .../api/handlers/crowdsec_handler_test.go | 24 ++-- .../api/handlers/crowdsec_key_path_test.go | 40 +++++++ backend/internal/caddy/config.go | 14 +-- .../caddy/config_bouncer_key_path_test.go | 107 ++++++++++++++++++ .../caddy/config_crowdsec_key_test.go | 37 ++++-- .../internal/caddy/config_crowdsec_test.go | 4 +- backend/internal/caddy/config_extra_test.go | 4 +- .../caddy/config_generate_additional_test.go | 8 +- .../internal/caddy/config_held_key_test.go | 3 +- backend/internal/caddy/config_options.go | 16 ++- .../caddy/config_patch_coverage_test.go | 9 +- backend/internal/caddy/config_test.go | 36 +++--- backend/internal/caddy/manager.go | 2 + backend/internal/config/config.go | 38 ++++++- .../config/config_crowdsec_data_test.go | 84 ++++++++++++++ backend/internal/services/crowdsec_startup.go | 5 +- .../services/crowdsec_startup_datadir_test.go | 94 +++++++++++++++ 19 files changed, 448 insertions(+), 97 deletions(-) create mode 100644 backend/internal/api/handlers/crowdsec_key_path_test.go create mode 100644 backend/internal/caddy/config_bouncer_key_path_test.go create mode 100644 backend/internal/config/config_crowdsec_data_test.go create mode 100644 backend/internal/services/crowdsec_startup_datadir_test.go diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 1eb2df134..5bbdb3acc 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -270,13 +270,9 @@ func main() { if crowdsecBinPath == "" { crowdsecBinPath = "/usr/local/bin/crowdsec" } - crowdsecDataDir := os.Getenv("CHARON_CROWDSEC_DATA") - if crowdsecDataDir == "" { - crowdsecDataDir = "/app/data/crowdsec" - } - + // The same data directory the handlers and the proxy use (see config.loadCrowdSecConfigDir). crowdsecExec := handlers.NewDefaultCrowdsecExecutor() - services.ReconcileCrowdSecOnStartup(db, crowdsecExec, crowdsecBinPath, crowdsecDataDir, nil, + services.ReconcileCrowdSecOnStartup(db, crowdsecExec, crowdsecBinPath, cfg.Security.CrowdSecConfigDir, nil, services.WithCerberusCheck(services.ProxyCerberusCheck(db, cfg.Security))) // Initialize plugin loader and load external DNS provider plugins (Phase 5) diff --git a/backend/internal/api/handlers/crowdsec_handler.go b/backend/internal/api/handlers/crowdsec_handler.go index afa047710..59a57c043 100644 --- a/backend/internal/api/handlers/crowdsec_handler.go +++ b/backend/internal/api/handlers/crowdsec_handler.go @@ -189,7 +189,6 @@ type CrowdsecHandler struct { // Bouncer auto-registration constants. const ( - bouncerKeyFile = bouncerkey.DefaultFile bouncerName = "caddy-bouncer" defaultRegisterBouncerScript = "/usr/local/bin/register_bouncer.sh" ) @@ -202,14 +201,13 @@ func (h *CrowdsecHandler) resolveLAPIURLValidator(raw string) (*url.URL, error) return validateCrowdsecLAPIBaseURLDefault(raw) } +// bouncerKeyPath is the key file the handlers read and write: bouncerkey.PathFor over the data directory. func (h *CrowdsecHandler) bouncerKeyPath() string { - if h != nil && strings.TrimSpace(h.DataDir) != "" { - return filepath.Join(h.DataDir, "bouncer_key") + dataDir := "" + if h != nil { + dataDir = h.DataDir } - if path := strings.TrimSpace(os.Getenv("CHARON_CROWDSEC_BOUNCER_KEY_PATH")); path != "" { - return path - } - return bouncerKeyFile + return bouncerkey.PathFor(dataDir) } // lapiKey returns the key for direct local API calls: the saved bouncer key file first, diff --git a/backend/internal/api/handlers/crowdsec_handler_test.go b/backend/internal/api/handlers/crowdsec_handler_test.go index e9a952d74..0e8461863 100644 --- a/backend/internal/api/handlers/crowdsec_handler_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_test.go @@ -3994,14 +3994,14 @@ func TestTestKeyAgainstLAPI_NonOKStatus(t *testing.T) { // TestEnsureBouncerRegistration_ValidEnvKey verifies that valid environment keys are used. func TestEnsureBouncerRegistration_ValidEnvKey(t *testing.T) { - // Note: Not parallel - tests share bouncerKeyFile constant + // Note: Not parallel - tests share the default bouncer key file // Clean up bouncer key file to ensure test isolation - if err := os.Remove(bouncerKeyFile); err != nil && !os.IsNotExist(err) { + if err := os.Remove(bouncerkey.DefaultFile); err != nil && !os.IsNotExist(err) { t.Logf("Warning: failed to remove bouncer key file: %v", err) } t.Cleanup(func() { - if err := os.Remove(bouncerKeyFile); err != nil && !os.IsNotExist(err) { + if err := os.Remove(bouncerkey.DefaultFile); err != nil && !os.IsNotExist(err) { t.Logf("Warning: failed to remove bouncer key file: %v", err) } }) @@ -4052,14 +4052,14 @@ func TestEnsureBouncerRegistration_ValidEnvKey(t *testing.T) { // TestEnsureBouncerRegistration_InvalidEnvKeyFallback verifies fallback when env key is invalid. func TestEnsureBouncerRegistration_InvalidEnvKeyFallback(t *testing.T) { - // Note: Not parallel - tests share bouncerKeyFile constant + // Note: Not parallel - tests share the default bouncer key file // Clean up bouncer key file to ensure test isolation - if err := os.Remove(bouncerKeyFile); err != nil && !os.IsNotExist(err) { + if err := os.Remove(bouncerkey.DefaultFile); err != nil && !os.IsNotExist(err) { t.Logf("Warning: failed to remove bouncer key file: %v", err) } t.Cleanup(func() { - if err := os.Remove(bouncerKeyFile); err != nil && !os.IsNotExist(err) { + if err := os.Remove(bouncerkey.DefaultFile); err != nil && !os.IsNotExist(err) { t.Logf("Warning: failed to remove bouncer key file: %v", err) } }) @@ -4114,10 +4114,10 @@ func TestEnsureBouncerRegistration_InvalidEnvKeyFallback(t *testing.T) { // TestEnsureBouncerRegistration_ConcurrentCalls verifies that concurrent registration // attempts are protected by mutex and only ONE bouncer registration occurs. func TestEnsureBouncerRegistration_ConcurrentCalls(t *testing.T) { - // Note: Not parallel - tests share bouncerKeyFile constant + // Note: Not parallel - tests share the default bouncer key file // Clean up bouncer key file before and after test to ensure isolation - testKeyFile := bouncerKeyFile + testKeyFile := bouncerkey.DefaultFile if err := os.Remove(testKeyFile); err != nil && !os.IsNotExist(err) { t.Logf("Warning: failed to remove test key file: %v", err) } @@ -4226,14 +4226,6 @@ func TestEnsureBouncerRegistration_ConcurrentCalls(t *testing.T) { } require.NoError(t, db.Create(&cfg).Error) - // Override bouncerKeyFile for this test (normally a const) - // We'll verify by reading from the temp file after registration - originalBouncerKeyFile := bouncerKeyFile - t.Cleanup(func() { - // Restore original (though it's a const, this is to satisfy linters) - _ = originalBouncerKeyFile - }) - // Execute: Launch 10 concurrent ensureBouncerRegistration() calls const concurrency = 10 var wg sync.WaitGroup diff --git a/backend/internal/api/handlers/crowdsec_key_path_test.go b/backend/internal/api/handlers/crowdsec_key_path_test.go new file mode 100644 index 000000000..4f4178b98 --- /dev/null +++ b/backend/internal/api/handlers/crowdsec_key_path_test.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" +) + +// TestBouncerKeyPathOverrideWinsOverDataDir pins the precedence of the key file path (#1719): the +// CHARON_CROWDSEC_BOUNCER_KEY_PATH override wins even when a data directory is set, because boot and the +// proxy resolve the same override. +func TestBouncerKeyPathOverrideWinsOverDataDir(t *testing.T) { + override := t.TempDir() + "/override_key" + t.Setenv(bouncerkey.PathEnvName, override) + + h := &CrowdsecHandler{DataDir: t.TempDir()} + require.Equal(t, override, h.bouncerKeyPath()) +} + +// TestBouncerKeyPathUsesDataDirWithoutOverride checks the data directory is used when no override is set. +func TestBouncerKeyPathUsesDataDirWithoutOverride(t *testing.T) { + t.Setenv(bouncerkey.PathEnvName, "") + dataDir := t.TempDir() + + h := &CrowdsecHandler{DataDir: dataDir} + require.Equal(t, dataDir+"/bouncer_key", h.bouncerKeyPath()) +} + +// TestBouncerKeyPathDefaultWithoutDataDirOrOverride checks the image default applies to an unset handler +// and to a nil handler. +func TestBouncerKeyPathDefaultWithoutDataDirOrOverride(t *testing.T) { + t.Setenv(bouncerkey.PathEnvName, "") + + require.Equal(t, bouncerkey.DefaultFile, (&CrowdsecHandler{}).bouncerKeyPath()) + + var nilHandler *CrowdsecHandler + require.Equal(t, bouncerkey.DefaultFile, nilHandler.bouncerKeyPath()) +} diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go index 9a63d25c1..c1ecf03a9 100644 --- a/backend/internal/caddy/config.go +++ b/backend/internal/caddy/config.go @@ -85,7 +85,7 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir, acmeEmail, frontendDir // crowdsecEnabled, because the engine keeps reading the log. crowdsecInProxy := false if crowdsecEnabled { - apiKey := getCrowdSecAPIKey() + apiKey := getCrowdSecAPIKey(resolvedOpts.bouncerKeyPath) if apiKey == "" { logger.Log().Warn(crowdSecNoKeyWarning) } else { @@ -1125,12 +1125,12 @@ func buildCrowdSecHandler(_ *models.ProxyHost, _ *models.SecurityConfig, crowdse // getCrowdSecAPIKey retrieves the CrowdSec bouncer API key the proxy uses. // The saved key file wins over the environment variables (see bouncerkey.Resolve), // so a key that Charon validated or registered is used even if an invalid -// variable is still set in docker-compose.yml. -func getCrowdSecAPIKey() string { - key, src := bouncerkey.Resolve(crowdSecBouncerKeyFile) +// variable is still set in docker-compose.yml. keyPath is the saved key file (see WithBouncerKeyPath). +func getCrowdSecAPIKey(keyPath string) string { + key, src := bouncerkey.Resolve(keyPath) switch src { case bouncerkey.SourceFile: - logger.Log().WithField("source", "file").WithField("file", crowdSecBouncerKeyFile).Debug("CrowdSec API key loaded from file") + logger.Log().WithField("source", "file").WithField("file", keyPath).Debug("CrowdSec API key loaded from file") case bouncerkey.SourceEnv: logger.Log().WithField("source", "env_var").Debug("CrowdSec API key loaded from environment variable") case bouncerkey.SourceMemory: @@ -1141,10 +1141,6 @@ func getCrowdSecAPIKey() string { return key } -// crowdSecBouncerKeyFile is the saved bouncer key location. A variable (not a -// constant) so tests can point the file branch at a temporary file. -var crowdSecBouncerKeyFile = bouncerkey.DefaultFile - // getAccessLogPath determines the appropriate path for Caddy access logs. // When CrowdSec is enabled or running in Docker (detected via /.dockerenv), // we use /var/log/caddy/access.log which is the standard location that diff --git a/backend/internal/caddy/config_bouncer_key_path_test.go b/backend/internal/caddy/config_bouncer_key_path_test.go new file mode 100644 index 000000000..be446258c --- /dev/null +++ b/backend/internal/caddy/config_bouncer_key_path_test.go @@ -0,0 +1,107 @@ +package caddy + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/Wikid82/charon/backend/internal/config" + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" + "github.com/Wikid82/charon/backend/internal/models" +) + +const bouncerKeyPathTestKey = "file-key-bouncerkeypath-000000001" + +// TestGenerateConfigWithBouncerKeyPathReadsThatFile checks the proxy config carries the key from the file named by +// WithBouncerKeyPath, not from the image default. +func TestGenerateConfigWithBouncerKeyPathReadsThatFile(t *testing.T) { + clearCrowdSecKeyEnv(t) + keyPath := filepath.Join(t.TempDir(), "bouncer_key") + require.NoError(t, os.WriteFile(keyPath, []byte(bouncerKeyPathTestKey+"\n"), 0o600)) + + cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, + &models.SecurityConfig{CrowdSecMode: "local"}, nil, WithBouncerKeyPath(keyPath)) + require.NoError(t, err) + + require.NotNil(t, cfg.Apps.CrowdSec, "a key in the configured file must attach the crowdsec app") + require.Equal(t, bouncerKeyPathTestKey, cfg.Apps.CrowdSec.APIKey) +} + +// TestGenerateConfigWithoutBouncerKeyPathIsUnchanged checks an absent option keeps the image default path. +func TestGenerateConfigWithoutBouncerKeyPathIsUnchanged(t *testing.T) { + clearCrowdSecKeyEnv(t) + t.Setenv("CROWDSEC_API_KEY", "env-only-key") + + resolved := resolveGenerateConfigOptions(nil) + require.Equal(t, bouncerkey.DefaultFile, resolved.bouncerKeyPath) + + cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, + &models.SecurityConfig{CrowdSecMode: "local"}, nil) + require.NoError(t, err) + require.NotNil(t, cfg.Apps.CrowdSec) + require.Equal(t, "env-only-key", cfg.Apps.CrowdSec.APIKey) +} + +// TestGetCrowdSecAPIKeyHonorsGivenPath checks the key is read from the path it is given, file first. +func TestGetCrowdSecAPIKeyHonorsGivenPath(t *testing.T) { + clearCrowdSecKeyEnv(t) + t.Setenv("CROWDSEC_API_KEY", "env-key") + keyPath := filepath.Join(t.TempDir(), "bouncer_key") + require.Equal(t, "env-key", getCrowdSecAPIKey(keyPath)) + + require.NoError(t, os.WriteFile(keyPath, []byte("path-file-key\n"), 0o600)) + require.Equal(t, "path-file-key", getCrowdSecAPIKey(keyPath)) +} + +// TestManagerPassesKeyPathFromCrowdSecConfigDir checks Manager hands GenerateConfig the key file under its data +// directory, the same file the handlers and boot use, and the override when one is set. +func TestManagerPassesKeyPathFromCrowdSecConfigDir(t *testing.T) { + caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/load" && r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusNotFound) + })) + t.Cleanup(caddyServer.Close) + + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, db.AutoMigrate( + &models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, + &models.SSLCertificate{}, &models.SecurityConfig{}, &models.SecurityRuleSet{}, + &models.SecurityDecision{}, &models.DNSProvider{}, + )) + + origGen := generateConfigFunc + t.Cleanup(func() { generateConfigFunc = origGen }) + var captured string + generateConfigFunc = func(_ []models.ProxyHost, _, _, _, _ string, _, _, _, _, _ bool, _ string, + _ []models.SecurityRuleSet, _ map[string]string, _ []models.SecurityDecision, _ *models.SecurityConfig, + _ []DNSProviderConfig, opts ...GenerateConfigOption) (*Config, error) { + captured = resolveGenerateConfigOptions(opts).bouncerKeyPath + return &Config{}, nil + } + origVal := validateConfigFunc + t.Cleanup(func() { validateConfigFunc = origVal }) + validateConfigFunc = func(_ *Config) error { return nil } + + dataDir := filepath.Join(t.TempDir(), "alt-crowdsec") + t.Setenv(bouncerkey.PathEnvName, "") + manager := NewManager(newTestClient(t, caddyServer.URL), db, t.TempDir(), "", false, + config.SecurityConfig{CerberusEnabled: true, CrowdSecConfigDir: dataDir}) + require.NoError(t, manager.ApplyConfig(context.Background())) + require.Equal(t, filepath.Join(dataDir, "bouncer_key"), captured) + + overridePath := filepath.Join(t.TempDir(), "override_key") + t.Setenv(bouncerkey.PathEnvName, overridePath) + require.NoError(t, manager.ApplyConfig(context.Background())) + require.Equal(t, overridePath, captured, "the override wins over the data directory") +} diff --git a/backend/internal/caddy/config_crowdsec_key_test.go b/backend/internal/caddy/config_crowdsec_key_test.go index 3a0f6da38..20503b1bc 100644 --- a/backend/internal/caddy/config_crowdsec_key_test.go +++ b/backend/internal/caddy/config_crowdsec_key_test.go @@ -7,9 +7,11 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" "github.com/Wikid82/charon/backend/internal/logger" "github.com/Wikid82/charon/backend/internal/models" - "github.com/stretchr/testify/require" ) // crowdSecNoKeyLogLine is the text the no-key warning must contain exactly once per generated config. @@ -46,11 +48,11 @@ func routeHasCrowdSecHandler(t *testing.T, routes []*Route) bool { } func TestGenerateConfig_CrowdSecOnWithoutKey_LeavesOutAppAndHandlers(t *testing.T) { - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "missing_bouncer_key")) + keyPath := isolateBouncerKeyFile(t) clearCrowdSecKeyEnv(t) logs := captureCaddyLogs(t) - cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, &models.SecurityConfig{CrowdSecMode: "local"}, nil) + cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, &models.SecurityConfig{CrowdSecMode: "local"}, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) require.Nil(t, cfg.Apps.CrowdSec, "apps.crowdsec must be left out while no bouncer key exists") @@ -62,11 +64,11 @@ func TestGenerateConfig_CrowdSecOnWithoutKey_LeavesOutAppAndHandlers(t *testing. } func TestGenerateConfig_CrowdSecOnWithoutKey_KeepsAccessLogPath(t *testing.T) { - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "missing_bouncer_key")) + keyPath := isolateBouncerKeyFile(t) clearCrowdSecKeyEnv(t) captureCaddyLogs(t) - cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, nil, nil) + cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, nil, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) // The engine still reads the access log while CrowdSec is on, so the log path stays on the CrowdSec location. @@ -74,7 +76,7 @@ func TestGenerateConfig_CrowdSecOnWithoutKey_KeepsAccessLogPath(t *testing.T) { } func TestGenerateConfig_CrowdSecOff_NoWarningNoAppNoHandler(t *testing.T) { - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "missing_bouncer_key")) + isolateBouncerKeyFile(t) clearCrowdSecKeyEnv(t) logs := captureCaddyLogs(t) @@ -87,12 +89,12 @@ func TestGenerateConfig_CrowdSecOff_NoWarningNoAppNoHandler(t *testing.T) { } func TestGenerateConfig_CrowdSecOnWithEnvKey_IncludesAppAndHandlers(t *testing.T) { - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "missing_bouncer_key")) + keyPath := isolateBouncerKeyFile(t) clearCrowdSecKeyEnv(t) t.Setenv("CROWDSEC_API_KEY", "env-key-for-test") logs := captureCaddyLogs(t) - cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, &models.SecurityConfig{CrowdSecMode: "local"}, nil) + cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, &models.SecurityConfig{CrowdSecMode: "local"}, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) require.NotNil(t, cfg.Apps.CrowdSec) @@ -104,10 +106,9 @@ func TestGenerateConfig_CrowdSecOnWithEnvKey_IncludesAppAndHandlers(t *testing.T func TestGenerateConfig_CrowdSecOnWithSavedKey_IncludesAppAndHandlers(t *testing.T) { keyPath := filepath.Join(t.TempDir(), "bouncer_key") require.NoError(t, os.WriteFile(keyPath, []byte("saved-key-for-test\n"), 0o600)) - useBouncerKeyFile(t, keyPath) clearCrowdSecKeyEnv(t) - cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, nil, nil) + cfg, err := GenerateConfig(noKeyHosts(), "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, nil, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) // The saved key file is the source GenerateConfig reads, so it counts as a key. @@ -116,10 +117,22 @@ func TestGenerateConfig_CrowdSecOnWithSavedKey_IncludesAppAndHandlers(t *testing require.True(t, routeHasCrowdSecHandler(t, cfg.Apps.HTTP.Servers["charon_server"].Routes)) } +// isolateBouncerKeyFile points the key file at a path that does not exist and returns it. Pass the path to +// GenerateConfig with WithBouncerKeyPath; the environment override also covers Manager, which derives its path +// with bouncerkey.PathFor. +func isolateBouncerKeyFile(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "missing_bouncer_key") + t.Setenv(bouncerkey.PathEnvName, path) + return path +} + // withTestBouncerKey gives the test a bouncer key from the environment and isolates it from any saved key file. -func withTestBouncerKey(t *testing.T) { +// It returns the isolated key file path for GenerateConfig callers. +func withTestBouncerKey(t *testing.T) string { t.Helper() - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "missing_bouncer_key")) + keyPath := isolateBouncerKeyFile(t) clearCrowdSecKeyEnv(t) t.Setenv("CROWDSEC_API_KEY", "test-env-key") + return keyPath } diff --git a/backend/internal/caddy/config_crowdsec_test.go b/backend/internal/caddy/config_crowdsec_test.go index ea4567fa2..5538d80c3 100644 --- a/backend/internal/caddy/config_crowdsec_test.go +++ b/backend/internal/caddy/config_crowdsec_test.go @@ -93,7 +93,7 @@ func TestBuildCrowdSecHandler_WithHost(t *testing.T) { } func TestGenerateConfig_WithCrowdSec(t *testing.T) { - withTestBouncerKey(t) + keyPath := withTestBouncerKey(t) // Test that CrowdSec is configured at app-level when enabled hosts := []models.ProxyHost{ { @@ -111,7 +111,7 @@ func TestGenerateConfig_WithCrowdSec(t *testing.T) { } // crowdsecEnabled=true should configure app-level CrowdSec - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, true, false, false, false, "", nil, nil, nil, secCfg, nil) + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, true, false, false, false, "", nil, nil, nil, secCfg, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) require.NotNil(t, config.Apps.HTTP) diff --git a/backend/internal/caddy/config_extra_test.go b/backend/internal/caddy/config_extra_test.go index 0137d0ab7..3ae0f64f0 100644 --- a/backend/internal/caddy/config_extra_test.go +++ b/backend/internal/caddy/config_extra_test.go @@ -219,7 +219,7 @@ func TestBuildACLHandler_UnknownIPTypeReturnsNil(t *testing.T) { } func TestGenerateConfig_SecurityPipeline_Order(t *testing.T) { - withTestBouncerKey(t) + keyPath := withTestBouncerKey(t) // Create host with ACL and HSTS/BlockExploits ipRules := `[ { "cidr": "192.168.1.0/24" } ]` acl := models.AccessList{ID: 200, Name: "WL", Enabled: true, Type: "whitelist", IPRules: ipRules} @@ -230,7 +230,7 @@ func TestGenerateConfig_SecurityPipeline_Order(t *testing.T) { rulesetPaths := map[string]string{"owasp-crs": "/tmp/owasp.conf"} // Set rate limit values so rate_limit handler is included (uses caddy-ratelimit format) secCfg := &models.SecurityConfig{CrowdSecMode: "local", RateLimitRequests: 100, RateLimitWindowSec: 60} - cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", rulesets, rulesetPaths, nil, secCfg, nil) + cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", rulesets, rulesetPaths, nil, secCfg, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) route := cfg.Apps.HTTP.Servers["charon_server"].Routes[1] diff --git a/backend/internal/caddy/config_generate_additional_test.go b/backend/internal/caddy/config_generate_additional_test.go index 952e85a8b..cedc1f8c8 100644 --- a/backend/internal/caddy/config_generate_additional_test.go +++ b/backend/internal/caddy/config_generate_additional_test.go @@ -45,7 +45,7 @@ func TestGenerateConfig_ZerosslAndBothProviders(t *testing.T) { } func TestGenerateConfig_SecurityPipeline_Order_Locations(t *testing.T) { - withTestBouncerKey(t) + keyPath := withTestBouncerKey(t) // Create host with a location so location-level handlers are generated ipRules := `[ { "cidr": "192.168.1.0/24" } ]` acl := models.AccessList{ID: 201, Name: "WL2", Enabled: true, Type: "whitelist", IPRules: ipRules} @@ -56,7 +56,7 @@ func TestGenerateConfig_SecurityPipeline_Order_Locations(t *testing.T) { rulesetPaths := map[string]string{"owasp-crs": "/tmp/owasp.conf"} // Set rate limit values so rate_limit handler is included (uses caddy-ratelimit format) sec := &models.SecurityConfig{CrowdSecMode: "local", RateLimitRequests: 100, RateLimitWindowSec: 60} - cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", rulesets, rulesetPaths, nil, sec, nil) + cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", rulesets, rulesetPaths, nil, sec, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) server := cfg.Apps.HTTP.Servers["charon_server"] @@ -383,10 +383,10 @@ func TestGenerateConfig_RateLimitFromSecCfg(t *testing.T) { } func TestGenerateConfig_CrowdSecHandlerFromSecCfg(t *testing.T) { - withTestBouncerKey(t) + keyPath := withTestBouncerKey(t) host := models.ProxyHost{UUID: "cs-1", DomainNames: "cs.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080} sec := &models.SecurityConfig{CrowdSecMode: "local", CrowdSecAPIURL: "http://cs.local"} - cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, sec, nil) + cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, sec, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) // Check app-level CrowdSec configuration diff --git a/backend/internal/caddy/config_held_key_test.go b/backend/internal/caddy/config_held_key_test.go index bbb4b9da4..f165e7f61 100644 --- a/backend/internal/caddy/config_held_key_test.go +++ b/backend/internal/caddy/config_held_key_test.go @@ -15,10 +15,9 @@ func TestGetCrowdSecAPIKeyUsesHeldKey(t *testing.T) { clearCrowdSecKeyEnv(t) t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "env-key-held-caddy-0000000001") path := filepath.Join(t.TempDir(), "bouncer_key") - useBouncerKeyFile(t, path) const held = "held-caddy-key-00000000000001" bouncerkey.Hold(path, held) t.Cleanup(func() { bouncerkey.Release(path) }) - require.Equal(t, held, getCrowdSecAPIKey()) + require.Equal(t, held, getCrowdSecAPIKey(path)) } diff --git a/backend/internal/caddy/config_options.go b/backend/internal/caddy/config_options.go index d702ae4b5..510ee72ef 100644 --- a/backend/internal/caddy/config_options.go +++ b/backend/internal/caddy/config_options.go @@ -1,6 +1,7 @@ package caddy import ( + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" "github.com/Wikid82/charon/backend/internal/crypto" "github.com/Wikid82/charon/backend/internal/models" "github.com/Wikid82/charon/backend/internal/security/selfhop" @@ -28,12 +29,14 @@ type generateConfigOptions struct { selfHop *selfHopConfig remoteHosts map[string]struct{} enforcement func(EnforcementReport) + // bouncerKeyPath is the saved CrowdSec bouncer key file the proxy reads. + bouncerKeyPath string } // resolveGenerateConfigOptions applies opts in order and returns the // resolved options struct GenerateConfig reads from internally. func resolveGenerateConfigOptions(opts []GenerateConfigOption) *generateConfigOptions { - resolved := &generateConfigOptions{} + resolved := &generateConfigOptions{bouncerKeyPath: bouncerkey.DefaultFile} for _, opt := range opts { if opt != nil { opt(resolved) @@ -75,6 +78,17 @@ func (o *generateConfigOptions) isRemoteHost(uuid string) bool { return ok } +// WithBouncerKeyPath sets the saved CrowdSec bouncer key file the proxy reads. Manager passes +// bouncerkey.PathFor(the CrowdSec data directory), the file the handlers and boot use. An empty path keeps +// the image default, bouncerkey.DefaultFile. +func WithBouncerKeyPath(path string) GenerateConfigOption { + return func(o *generateConfigOptions) { + if path != "" { + o.bouncerKeyPath = path + } + } +} + // WithEnforcementSink receives the enforcement report of one GenerateConfig call. The sink is called on every return // path, so a caller never keeps a report from an earlier generation. func WithEnforcementSink(sink func(EnforcementReport)) GenerateConfigOption { diff --git a/backend/internal/caddy/config_patch_coverage_test.go b/backend/internal/caddy/config_patch_coverage_test.go index 55ceea638..daef93345 100644 --- a/backend/internal/caddy/config_patch_coverage_test.go +++ b/backend/internal/caddy/config_patch_coverage_test.go @@ -2,7 +2,6 @@ package caddy import ( "os" - "path/filepath" "testing" "time" @@ -325,20 +324,20 @@ func TestGenerateConfig_HTTPChallenge_ExcludesIPDomains(t *testing.T) { } func TestGetCrowdSecAPIKey_EnvPriority(t *testing.T) { - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "missing_bouncer_key")) + keyPath := isolateBouncerKeyFile(t) clearCrowdSecKeyEnv(t) t.Setenv("CROWDSEC_BOUNCER_API_KEY", "bouncer") t.Setenv("CROWDSEC_API_KEY", "primary") - require.Equal(t, "primary", getCrowdSecAPIKey()) + require.Equal(t, "primary", getCrowdSecAPIKey(keyPath)) // CHARON_SECURITY_CROWDSEC_API_KEY has highest priority among env vars t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "charon") - require.Equal(t, "charon", getCrowdSecAPIKey()) + require.Equal(t, "charon", getCrowdSecAPIKey(keyPath)) t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "") t.Setenv("CROWDSEC_API_KEY", "") - require.Equal(t, "bouncer", getCrowdSecAPIKey()) + require.Equal(t, "bouncer", getCrowdSecAPIKey(keyPath)) } func TestHasWildcard_TrueFalse(t *testing.T) { diff --git a/backend/internal/caddy/config_test.go b/backend/internal/caddy/config_test.go index 8123d6112..ce572509c 100644 --- a/backend/internal/caddy/config_test.go +++ b/backend/internal/caddy/config_test.go @@ -807,7 +807,7 @@ func TestGenerateConfig_DuplicateDomains(t *testing.T) { // TestGenerateConfig_WithCrowdSecApp verifies CrowdSec app configuration func TestGenerateConfig_WithCrowdSecApp(t *testing.T) { // Point the key file at a missing path so the env var is the key source (the file takes priority over env vars). - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "missing_bouncer_key")) + keyPath := isolateBouncerKeyFile(t) hosts := []models.ProxyHost{ { @@ -836,7 +836,7 @@ func TestGenerateConfig_WithCrowdSecApp(t *testing.T) { // Set test API key _ = os.Setenv("CROWDSEC_API_KEY", "test-api-key-12345") - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, true, false, false, false, "", nil, nil, nil, secCfg, nil) + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, true, false, false, false, "", nil, nil, nil, secCfg, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) // Verify CrowdSec app is configured @@ -850,7 +850,7 @@ func TestGenerateConfig_WithCrowdSecApp(t *testing.T) { // TestGenerateConfig_CrowdSecHandlerAdded verifies CrowdSec handler is added to routes func TestGenerateConfig_CrowdSecHandlerAdded(t *testing.T) { - withTestBouncerKey(t) + keyPath := withTestBouncerKey(t) hosts := []models.ProxyHost{ { UUID: "test-uuid", @@ -861,7 +861,7 @@ func TestGenerateConfig_CrowdSecHandlerAdded(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, true, false, false, false, "", nil, nil, nil, nil, nil) + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, true, false, false, false, "", nil, nil, nil, nil, nil, WithBouncerKeyPath(keyPath)) require.NoError(t, err) server := config.Apps.HTTP.Servers["charon_server"] @@ -1758,15 +1758,6 @@ func TestNormalizeAdvancedConfig_ArrayInput(t *testing.T) { require.Len(t, arr, 1) } -// useBouncerKeyFile points getCrowdSecAPIKey at path for the rest of the test. -// Tests that call it must not use t.Parallel (the package variable is shared). -func useBouncerKeyFile(t *testing.T, path string) { - t.Helper() - original := crowdSecBouncerKeyFile - crowdSecBouncerKeyFile = path - t.Cleanup(func() { crowdSecBouncerKeyFile = original }) -} - // clearCrowdSecKeyEnv blanks every bouncer key variable for the test (restored on cleanup). func clearCrowdSecKeyEnv(t *testing.T) { t.Helper() @@ -1777,20 +1768,20 @@ func clearCrowdSecKeyEnv(t *testing.T) { // TestGetCrowdSecAPIKey verifies API key retrieval from environment when no key file exists. func TestGetCrowdSecAPIKey(t *testing.T) { - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "missing_bouncer_key")) + keyPath := isolateBouncerKeyFile(t) clearCrowdSecKeyEnv(t) // No keys set - should return empty - require.Equal(t, "", getCrowdSecAPIKey()) + require.Equal(t, "", getCrowdSecAPIKey(keyPath)) // Set primary key t.Setenv("CROWDSEC_API_KEY", "primary-key") - require.Equal(t, "primary-key", getCrowdSecAPIKey()) + require.Equal(t, "primary-key", getCrowdSecAPIKey(keyPath)) // Test fallback priority t.Setenv("CROWDSEC_API_KEY", "") t.Setenv("CROWDSEC_BOUNCER_API_KEY", "bouncer-key") - require.Equal(t, "bouncer-key", getCrowdSecAPIKey()) + require.Equal(t, "bouncer-key", getCrowdSecAPIKey(keyPath)) } // TestGetCrowdSecAPIKeyPrefersKeyFile covers the key file branch: a non-empty @@ -1798,27 +1789,26 @@ func TestGetCrowdSecAPIKey(t *testing.T) { // whitespace-only file falls back to the environment. func TestGetCrowdSecAPIKeyPrefersKeyFile(t *testing.T) { keyPath := filepath.Join(t.TempDir(), "bouncer_key") - useBouncerKeyFile(t, keyPath) clearCrowdSecKeyEnv(t) t.Setenv("CROWDSEC_API_KEY", "env-key") require.NoError(t, os.WriteFile(keyPath, []byte("file-key\n"), 0o600)) - require.Equal(t, "file-key", getCrowdSecAPIKey()) + require.Equal(t, "file-key", getCrowdSecAPIKey(keyPath)) require.NoError(t, os.WriteFile(keyPath, []byte(" \n"), 0o600)) - require.Equal(t, "env-key", getCrowdSecAPIKey(), "whitespace-only file must fall back to env") + require.Equal(t, "env-key", getCrowdSecAPIKey(keyPath), "whitespace-only file must fall back to env") require.NoError(t, os.WriteFile(keyPath, nil, 0o600)) - require.Equal(t, "env-key", getCrowdSecAPIKey(), "empty file must fall back to env") + require.Equal(t, "env-key", getCrowdSecAPIKey(keyPath), "empty file must fall back to env") } // TestGetCrowdSecAPIKeyMissingFileUsesEnv covers a key path that does not exist. func TestGetCrowdSecAPIKeyMissingFileUsesEnv(t *testing.T) { - useBouncerKeyFile(t, filepath.Join(t.TempDir(), "absent")) + keyPath := filepath.Join(t.TempDir(), "absent") clearCrowdSecKeyEnv(t) t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "cerberus-key") - require.Equal(t, "cerberus-key", getCrowdSecAPIKey()) + require.Equal(t, "cerberus-key", getCrowdSecAPIKey(keyPath)) } type mockOrthrusResolver struct { diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go index 97345d29f..1ba42705d 100644 --- a/backend/internal/caddy/manager.go +++ b/backend/internal/caddy/manager.go @@ -17,6 +17,7 @@ import ( "gorm.io/gorm" "github.com/Wikid82/charon/backend/internal/config" + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" "github.com/Wikid82/charon/backend/internal/crypto" "github.com/Wikid82/charon/backend/internal/logger" "github.com/Wikid82/charon/backend/internal/models" @@ -481,6 +482,7 @@ func (m *Manager) ApplyConfig(ctx context.Context) error { WithEncryptionService(m.encSvc), WithRedirectionHosts(redirectHosts), WithRemoteHosts(remoteHosts), + WithBouncerKeyPath(bouncerkey.PathFor(m.securityCfg.CrowdSecConfigDir)), WithEnforcementSink(func(report EnforcementReport) { generatedReport = &report }), } if m.hopSecret != nil { diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index c8d643cc2..34605d5a8 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -129,11 +129,14 @@ func Load() (Config, error) { PluginsDir: getEnvAny("/app/plugins", "CHARON_PLUGINS_DIR"), CaddyLogDir: getEnvAny("/var/log/caddy", "CHARON_CADDY_LOG_DIR"), CrowdSecLogDir: getEnvAny("/var/log/crowdsec", "CHARON_CROWDSEC_LOG_DIR"), - Security: loadSecurityConfig(), Emergency: loadEmergencyConfig(), Debug: getEnvAny("false", "CHARON_DEBUG", "CPM_DEBUG") == "true", } + securityCfg, securityWarnings := loadSecurityConfig() + cfg.Security = securityCfg + cfg.StartupWarnings = append(cfg.StartupWarnings, securityWarnings...) + trustedProxies, proxyWarnings := ValidateTrustedProxies(cfg.Security.TrustedProxies) cfg.Security.TrustedProxies = trustedProxies cfg.StartupWarnings = append(cfg.StartupWarnings, proxyWarnings...) @@ -209,13 +212,38 @@ func loadDBCompactOnStart() (mode string, warnings []string) { } } -// loadSecurityConfig loads the security configuration with proper parsing of array fields -func loadSecurityConfig() SecurityConfig { +// crowdSecDataAliasEnv is the legacy name for the CrowdSec data directory. It is an alias of +// CHARON_CROWDSEC_CONFIG_DIR; the config-dir variable wins when both are set and differ. +const crowdSecDataAliasEnv = "CHARON_CROWDSEC_DATA" + +// loadCrowdSecConfigDir returns the CrowdSec data directory shared by the handlers, boot reconciliation and the +// proxy key file. CHARON_CROWDSEC_CONFIG_DIR (or CPM_CROWDSEC_CONFIG_DIR) wins; CHARON_CROWDSEC_DATA is used +// only when that is unset, and a warning names the conflict when the two differ. +func loadCrowdSecConfigDir() (dir string, warnings []string) { + configDir := getEnvAny("", "CHARON_CROWDSEC_CONFIG_DIR", "CPM_CROWDSEC_CONFIG_DIR") + alias := getEnvAny("", crowdSecDataAliasEnv) + switch { + case configDir == "" && alias != "": + return alias, nil + case configDir == "": + return filepath.Join("data", "crowdsec"), nil + case alias != "" && alias != configDir: + return configDir, []string{fmt.Sprintf( + "%s=%q is ignored because CHARON_CROWDSEC_CONFIG_DIR=%q is set; keep only CHARON_CROWDSEC_CONFIG_DIR", crowdSecDataAliasEnv, alias, configDir)} + default: + return configDir, nil + } +} + +// loadSecurityConfig loads the security configuration with proper parsing of array fields and returns the +// startup warnings found while reading it. +func loadSecurityConfig() (settings SecurityConfig, warnings []string) { + crowdSecDir, crowdSecWarnings := loadCrowdSecConfigDir() cfg := SecurityConfig{ CrowdSecMode: getEnvAny("disabled", "CERBERUS_SECURITY_CROWDSEC_MODE", "CHARON_SECURITY_CROWDSEC_MODE", "CPM_SECURITY_CROWDSEC_MODE"), CrowdSecAPIURL: getEnvAny("", "CERBERUS_SECURITY_CROWDSEC_API_URL", "CHARON_SECURITY_CROWDSEC_API_URL", "CPM_SECURITY_CROWDSEC_API_URL"), CrowdSecAPIKey: getEnvAny("", bouncerkey.EnvNames()...), - CrowdSecConfigDir: getEnvAny(filepath.Join("data", "crowdsec"), "CHARON_CROWDSEC_CONFIG_DIR", "CPM_CROWDSEC_CONFIG_DIR"), + CrowdSecConfigDir: crowdSecDir, WAFMode: getEnvAny("disabled", "CERBERUS_SECURITY_WAF_MODE", "CHARON_SECURITY_WAF_MODE", "CPM_SECURITY_WAF_MODE"), RateLimitMode: getEnvAny("disabled", "CERBERUS_SECURITY_RATELIMIT_MODE", "CHARON_SECURITY_RATELIMIT_MODE", "CPM_SECURITY_RATELIMIT_MODE"), RateLimitRequests: getEnvIntAny(100, "CERBERUS_SECURITY_RATELIMIT_REQUESTS", "CHARON_SECURITY_RATELIMIT_REQUESTS"), @@ -246,7 +274,7 @@ func loadSecurityConfig() SecurityConfig { } } - return cfg + return cfg, crowdSecWarnings } // loadEmergencyConfig loads the emergency server configuration diff --git a/backend/internal/config/config_crowdsec_data_test.go b/backend/internal/config/config_crowdsec_data_test.go new file mode 100644 index 000000000..a05b775b9 --- /dev/null +++ b/backend/internal/config/config_crowdsec_data_test.go @@ -0,0 +1,84 @@ +package config + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// loadCrowdSecDirEnv sets the database and caddy paths Load needs and clears the CrowdSec data-dir variables. +func loadCrowdSecDirEnv(t *testing.T) { + t.Helper() + tempDir := t.TempDir() + t.Setenv("CHARON_DB_PATH", filepath.Join(tempDir, "test.db")) + t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tempDir, "caddy")) + t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tempDir, "imports")) + t.Setenv("CHARON_CROWDSEC_CONFIG_DIR", "") + t.Setenv("CPM_CROWDSEC_CONFIG_DIR", "") + t.Setenv("CHARON_CROWDSEC_DATA", "") +} + +// crowdSecDirWarnings returns the startup warnings that name the CrowdSec data-dir variables. +func crowdSecDirWarnings(warnings []string) []string { + var out []string + for _, w := range warnings { + if strings.Contains(w, "CHARON_CROWDSEC_DATA") { + out = append(out, w) + } + } + return out +} + +// TestLoad_CrowdSecConfigDirDefault keeps the relative default when neither variable is set. +func TestLoad_CrowdSecConfigDirDefault(t *testing.T) { + loadCrowdSecDirEnv(t) + + cfg, err := Load() + require.NoError(t, err) + assert.Equal(t, filepath.Join("data", "crowdsec"), cfg.Security.CrowdSecConfigDir) + assert.Empty(t, crowdSecDirWarnings(cfg.StartupWarnings)) +} + +// TestLoad_CrowdSecDataAliasSetsConfigDir reads CHARON_CROWDSEC_DATA when the config-dir variable is unset. +func TestLoad_CrowdSecDataAliasSetsConfigDir(t *testing.T) { + loadCrowdSecDirEnv(t) + alias := filepath.Join("data", "alias-crowdsec") + t.Setenv("CHARON_CROWDSEC_DATA", alias) + + cfg, err := Load() + require.NoError(t, err) + assert.Equal(t, alias, cfg.Security.CrowdSecConfigDir) + assert.Empty(t, crowdSecDirWarnings(cfg.StartupWarnings), "an alias alone is not a conflict") +} + +// TestLoad_CrowdSecConfigDirWinsOverDataAlias keeps the config-dir value when both variables differ and warns once. +func TestLoad_CrowdSecConfigDirWinsOverDataAlias(t *testing.T) { + loadCrowdSecDirEnv(t) + configDir := filepath.Join("data", "crowdsec") + t.Setenv("CHARON_CROWDSEC_CONFIG_DIR", configDir) + t.Setenv("CHARON_CROWDSEC_DATA", filepath.Join("data", "other")) + + cfg, err := Load() + require.NoError(t, err) + assert.Equal(t, configDir, cfg.Security.CrowdSecConfigDir) + + warnings := crowdSecDirWarnings(cfg.StartupWarnings) + require.Len(t, warnings, 1, "warnings: %v", cfg.StartupWarnings) + assert.Contains(t, warnings[0], "CHARON_CROWDSEC_CONFIG_DIR") +} + +// TestLoad_CrowdSecDataAliasEqualToConfigDirIsSilent accepts the alias when it names the same directory. +func TestLoad_CrowdSecDataAliasEqualToConfigDirIsSilent(t *testing.T) { + loadCrowdSecDirEnv(t) + same := filepath.Join("data", "crowdsec") + t.Setenv("CHARON_CROWDSEC_CONFIG_DIR", same) + t.Setenv("CHARON_CROWDSEC_DATA", same) + + cfg, err := Load() + require.NoError(t, err) + assert.Equal(t, same, cfg.Security.CrowdSecConfigDir) + assert.Empty(t, crowdSecDirWarnings(cfg.StartupWarnings)) +} diff --git a/backend/internal/services/crowdsec_startup.go b/backend/internal/services/crowdsec_startup.go index 48cd06dd2..91ce1c7c2 100644 --- a/backend/internal/services/crowdsec_startup.go +++ b/backend/internal/services/crowdsec_startup.go @@ -286,8 +286,6 @@ type CommandExecutor interface { Execute(ctx context.Context, name string, args ...string) ([]byte, error) } -const defaultBouncerKeyFile = bouncerkey.DefaultFile - // startupKeyCheckTiming is the retry policy of the boot key check: one window of 10 s, shared with the handler rule. // Only Wait, PerAttempt and the backoffs are read; the URL comes from the environment at check time. Tests shorten it. var startupKeyCheckTiming = bouncerkey.CheckOptions{ @@ -303,8 +301,9 @@ const startupBouncerName = "caddy-bouncer" // ensureBouncerRegistrationOnStartup registers the caddy-bouncer with LAPI during container startup. // This is called after CrowdSec LAPI is confirmed running to ensure bouncer key is properly registered. // Priority: a valid saved key wins, then a valid env key is saved, then a new key is registered. +// The key file is bouncerkey.PathFor(dataDir), the file the handlers and the proxy read. func ensureBouncerRegistrationOnStartup(dataDir string, cmdExec CommandExecutor) error { - return ensureBouncerRegistration(dataDir, defaultBouncerKeyFile, cmdExec) + return ensureBouncerRegistration(dataDir, bouncerkey.PathFor(dataDir), cmdExec) } // ensureBouncerRegistration implements ensureBouncerRegistrationOnStartup with an explicit key-file path. diff --git a/backend/internal/services/crowdsec_startup_datadir_test.go b/backend/internal/services/crowdsec_startup_datadir_test.go new file mode 100644 index 000000000..9b171cf9b --- /dev/null +++ b/backend/internal/services/crowdsec_startup_datadir_test.go @@ -0,0 +1,94 @@ +package services + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Wikid82/charon/backend/internal/crowdsec/bouncerkey" + "github.com/Wikid82/charon/backend/internal/models" +) + +// dirRecordingExecutor is a CrowdsecProcessManager that records the data directory each call receives. +type dirRecordingExecutor struct { + startPid int + startDir string + statusDir string + started bool +} + +func (e *dirRecordingExecutor) Start(_ context.Context, _, configDir string) (int, error) { + e.started = true + e.startDir = configDir + return e.startPid, nil +} + +func (e *dirRecordingExecutor) Stop(_ context.Context, _ string) error { return nil } + +func (e *dirRecordingExecutor) Status(_ context.Context, configDir string) (running bool, pid int, err error) { + e.statusDir = configDir + if e.started { + return true, e.startPid, nil + } + return false, 0, nil +} + +// TestEnsureBouncerRegistrationOnStartupUsesDataDirKeyPath pins the boot key path (#1719): the key file is +// /bouncer_key, the same file the handlers and the proxy use, not the image default. +func TestEnsureBouncerRegistrationOnStartupUsesDataDirKeyPath(t *testing.T) { + h := newBouncerHarness(t) + t.Setenv(bouncerkey.PathEnvName, "") + dataDir := filepath.Join(h.dataDir, "crowdsec") + + exec := &scriptedBouncerExecutor{addOutput: []byte(testGeneratedKey)} + require.NoError(t, ensureBouncerRegistrationOnStartup(dataDir, exec)) + + require.Equal(t, testGeneratedKey, bouncerkey.ReadFile(filepath.Join(dataDir, "bouncer_key"))) +} + +// TestEnsureBouncerRegistrationOnStartupHonorsKeyPathOverride checks the override wins over the data directory at boot. +func TestEnsureBouncerRegistrationOnStartupHonorsKeyPathOverride(t *testing.T) { + h := newBouncerHarness(t) + override := filepath.Join(h.dataDir, "override", "alt_bouncer_key") + t.Setenv(bouncerkey.PathEnvName, override) + dataDir := filepath.Join(h.dataDir, "crowdsec") + + exec := &scriptedBouncerExecutor{addOutput: []byte(testGeneratedKey)} + require.NoError(t, ensureBouncerRegistrationOnStartup(dataDir, exec)) + + require.Equal(t, testGeneratedKey, bouncerkey.ReadFile(override)) + _, statErr := os.Stat(filepath.Join(dataDir, "bouncer_key")) + require.True(t, os.IsNotExist(statErr), "the data directory key file must not be written when the override is set") +} + +// TestReconcileCrowdSecOnStartupRelativeDataDirEndToEnd runs boot reconciliation with a relative data directory +// (the non-container default): the engine receives the same relative directory, the whitelist and the key file +// live under it, and nothing is written under the absolute image path. +func TestReconcileCrowdSecOnStartupRelativeDataDirEndToEnd(t *testing.T) { + h := newBouncerHarness(t) + t.Setenv(bouncerkey.PathEnvName, "") + shortenCrowdSecVerifyWait(t) + t.Chdir(h.dataDir) + + relDataDir := filepath.Join("data", "crowdsec") + require.NoError(t, os.MkdirAll(filepath.Join(relDataDir, "config"), 0o750)) + binPath := filepath.Join(h.dataDir, "crowdsec-bin") + require.NoError(t, os.WriteFile(binPath, []byte("#!/bin/sh\nexit 0\n"), 0o750)) // #nosec G306 -- executable test script + + db := setupCrowdsecTestDB(t) + require.NoError(t, db.Create(&models.SecurityConfig{Name: models.DefaultSecurityConfigName, CrowdSecMode: "local"}).Error) + + engine := &dirRecordingExecutor{startPid: 4242} + cmdExec := &scriptedBouncerExecutor{addOutput: []byte(testGeneratedKey)} + + ReconcileCrowdSecOnStartup(db, engine, binPath, relDataDir, cmdExec) + + require.True(t, engine.started, "reconciliation should start the engine") + require.Equal(t, relDataDir, engine.startDir, "the engine must receive the configured data directory") + require.Equal(t, relDataDir, engine.statusDir) + require.Equal(t, testGeneratedKey, bouncerkey.ReadFile(filepath.Join(relDataDir, "bouncer_key"))) + require.Equal(t, testGeneratedKey, bouncerkey.ReadFile(filepath.Join(h.dataDir, relDataDir, "bouncer_key"))) +} From a4fa566d04217eebbcec18e57f4f891be6d07d4a Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 20:47:00 +0000 Subject: [PATCH 021/291] fix: show where a held CrowdSec key comes from --- frontend/src/api/__tests__/crowdsec.test.ts | 16 +++++++ frontend/src/api/crowdsec.ts | 6 ++- .../components/CrowdSecBouncerKeyDisplay.tsx | 18 +++++--- .../CrowdSecBouncerKeyDisplay.test.tsx | 44 +++++++++++++++++++ .../CrowdSecKeyWarning.copy.test.tsx | 2 + .../__tests__/CrowdSecKeyWarning.test.tsx | 2 + frontend/src/locales/de/translation.json | 1 + frontend/src/locales/en/translation.json | 1 + frontend/src/locales/es/translation.json | 1 + frontend/src/locales/fr/translation.json | 1 + frontend/src/locales/zh/translation.json | 1 + .../__tests__/Security.crowdsec.test.tsx | 2 + .../src/pages/__tests__/Security.spec.tsx | 1 + 13 files changed, 89 insertions(+), 7 deletions(-) diff --git a/frontend/src/api/__tests__/crowdsec.test.ts b/frontend/src/api/__tests__/crowdsec.test.ts index 83b39a199..3626f1693 100644 --- a/frontend/src/api/__tests__/crowdsec.test.ts +++ b/frontend/src/api/__tests__/crowdsec.test.ts @@ -290,6 +290,22 @@ describe('crowdsec API', () => { expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/key-status') expect(result).toEqual(mockData) }) + + it('should pass through a memory-held key with an unknown LAPI check', async () => { + const mockData = { + key_source: 'memory' as const, + key_check: 'unknown' as const, + env_key_rejected: false, + current_key_preview: 'abc***xyz', + message: 'Held in memory', + } + vi.mocked(client.get).mockResolvedValue({ data: mockData }) + + const result = await crowdsec.getCrowdsecKeyStatus() + + expect(result.key_source).toBe('memory') + expect(result.key_check).toBe('unknown') + }) }) describe('listWhitelists', () => { diff --git a/frontend/src/api/crowdsec.ts b/frontend/src/api/crowdsec.ts index 6a0bfb81d..0811ab23d 100644 --- a/frontend/src/api/crowdsec.ts +++ b/frontend/src/api/crowdsec.ts @@ -345,8 +345,12 @@ export async function unbanIP(ip: string): Promise { } /** CrowdSec API key status information for key rejection notifications. */ +/** LAPI check of the current key: `none` when there is no key to check. */ +export type CrowdSecKeyCheck = 'accepted' | 'rejected' | 'unknown' | 'none' + export interface CrowdSecKeyStatus { - key_source: 'env' | 'file' | 'auto-generated' | 'none' + key_source: 'env' | 'file' | 'auto-generated' | 'memory' | 'none' + key_check: CrowdSecKeyCheck env_key_rejected: boolean full_key?: string // Only present when env_key_rejected is true current_key_preview: string diff --git a/frontend/src/components/CrowdSecBouncerKeyDisplay.tsx b/frontend/src/components/CrowdSecBouncerKeyDisplay.tsx index f5f06dc3a..28a1b1a91 100644 --- a/frontend/src/components/CrowdSecBouncerKeyDisplay.tsx +++ b/frontend/src/components/CrowdSecBouncerKeyDisplay.tsx @@ -13,7 +13,7 @@ import { toast } from '../utils/toast' interface BouncerInfo { name: string key_preview: string - key_source: 'env_var' | 'file' | 'none' + key_source: 'env_var' | 'file' | 'memory' | 'none' file_path: string registered: boolean } @@ -132,13 +132,19 @@ export function CrowdSecBouncerKeyDisplay() { ? t('security.crowdsec.registered') : t('security.crowdsec.notRegistered')} - - {info.key_source === 'env_var' - ? t('security.crowdsec.sourceEnvVar') - : t('security.crowdsec.sourceFile')} - + {info.key_source !== 'memory' && ( + + {info.key_source === 'env_var' + ? t('security.crowdsec.sourceEnvVar') + : t('security.crowdsec.sourceFile')} + + )} + {info.key_source === 'memory' && ( +

{t('security.crowdsec.keyHeldInMemory')}

+ )} +

{t('security.crowdsec.keyStoredAt')}: {info.file_path}

diff --git a/frontend/src/components/__tests__/CrowdSecBouncerKeyDisplay.test.tsx b/frontend/src/components/__tests__/CrowdSecBouncerKeyDisplay.test.tsx index e04c89b4a..d1769d47b 100644 --- a/frontend/src/components/__tests__/CrowdSecBouncerKeyDisplay.test.tsx +++ b/frontend/src/components/__tests__/CrowdSecBouncerKeyDisplay.test.tsx @@ -41,6 +41,7 @@ vi.mock('react-i18next', () => ({ 'security.crowdsec.sourceEnvVar': 'Environment Variable', 'security.crowdsec.sourceFile': 'File', 'security.crowdsec.keyStoredAt': 'Key stored at', + 'security.crowdsec.keyHeldInMemory': 'Held in memory until the key file can be written', 'common.copy': 'Copy', 'common.success': 'Success', } @@ -66,6 +67,14 @@ const mockBouncerInfoEnvVar = { registered: true, } +const mockBouncerInfoMemory = { + name: 'caddy-bouncer', + key_preview: 'mem***key', + key_source: 'memory' as const, + file_path: '/app/data/crowdsec/bouncer_key', + registered: true, +} + const mockBouncerInfoNotRegistered = { name: 'caddy-bouncer', key_preview: 'unreg***key', @@ -177,6 +186,41 @@ describe('CrowdSecBouncerKeyDisplay', () => { }) }) + describe('Registered Bouncer with Memory Key Source', () => { + it('shows the held-in-memory source line', async () => { + vi.mocked(client.get).mockResolvedValue({ data: mockBouncerInfoMemory }) + + renderComponent() + + await waitFor(() => { + expect(screen.getByText('Held in memory until the key file can be written')).toBeInTheDocument() + }) + }) + + it('does not label a memory key as coming from a file or environment variable', async () => { + vi.mocked(client.get).mockResolvedValue({ data: mockBouncerInfoMemory }) + + renderComponent() + + await waitFor(() => { + expect(screen.getByText('mem***key')).toBeInTheDocument() + }) + expect(screen.queryByText('File')).not.toBeInTheDocument() + expect(screen.queryByText('Environment Variable')).not.toBeInTheDocument() + }) + + it('does not show the memory line for a file key', async () => { + vi.mocked(client.get).mockResolvedValue({ data: mockBouncerInfo }) + + renderComponent() + + await waitFor(() => { + expect(screen.getByText('File')).toBeInTheDocument() + }) + expect(screen.queryByText('Held in memory until the key file can be written')).not.toBeInTheDocument() + }) + }) + describe('Unregistered Bouncer', () => { it('should show not registered badge', async () => { vi.mocked(client.get).mockResolvedValue({ data: mockBouncerInfoNotRegistered }) diff --git a/frontend/src/components/__tests__/CrowdSecKeyWarning.copy.test.tsx b/frontend/src/components/__tests__/CrowdSecKeyWarning.copy.test.tsx index ef4c0efba..5bda5efb5 100644 --- a/frontend/src/components/__tests__/CrowdSecKeyWarning.copy.test.tsx +++ b/frontend/src/components/__tests__/CrowdSecKeyWarning.copy.test.tsx @@ -39,6 +39,7 @@ describe('CrowdSecKeyWarning English copy', () => { it('describes the saved key Charon now uses and asks to remove the stale variable', async () => { vi.mocked(crowdsecApi.getCrowdsecKeyStatus).mockResolvedValue({ key_source: 'auto-generated', + key_check: 'accepted', env_key_rejected: true, full_key: 'generated-key-0123456789abcdef', current_key_preview: 'gene...', @@ -64,6 +65,7 @@ describe('CrowdSecKeyWarning English copy', () => { it('renders nothing when the key source is none and no env key was rejected', async () => { vi.mocked(crowdsecApi.getCrowdsecKeyStatus).mockResolvedValue({ key_source: 'none', + key_check: 'none', env_key_rejected: false, current_key_preview: '', message: 'No CrowdSec API key configured', diff --git a/frontend/src/components/__tests__/CrowdSecKeyWarning.test.tsx b/frontend/src/components/__tests__/CrowdSecKeyWarning.test.tsx index c7f6bd319..45497d6e2 100644 --- a/frontend/src/components/__tests__/CrowdSecKeyWarning.test.tsx +++ b/frontend/src/components/__tests__/CrowdSecKeyWarning.test.tsx @@ -38,6 +38,7 @@ const createWrapper = () => { describe('CrowdSecKeyWarning', () => { const defaultStatus = { key_source: 'env' as const, + key_check: 'accepted' as const, env_key_rejected: true, full_key: 'new-valid-key', current_key_preview: 'old...', @@ -67,6 +68,7 @@ describe('CrowdSecKeyWarning', () => { it('returns null when key is valid (present)', async () => { vi.mocked(crowdsecApi.getCrowdsecKeyStatus).mockResolvedValue({ key_source: 'env', + key_check: 'accepted', env_key_rejected: false, current_key_preview: 'valid...', message: 'OK', diff --git a/frontend/src/locales/de/translation.json b/frontend/src/locales/de/translation.json index f7dc7bf57..3377a2bd0 100644 --- a/frontend/src/locales/de/translation.json +++ b/frontend/src/locales/de/translation.json @@ -423,6 +423,7 @@ "notRegistered": "Nicht registriert", "sourceEnvVar": "Aus Umgebungsvariable", "sourceFile": "Aus Datei", + "keyHeldInMemory": "Im Speicher gehalten, bis die Schlüsseldatei geschrieben werden kann", "keyStoredAt": "Schlüssel gespeichert unter", "keyWarning": { "title": "CrowdSec API-Schlüssel aktualisiert", diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index db999beb6..765f2d06a 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -428,6 +428,7 @@ "notRegistered": "Not Registered", "sourceEnvVar": "From environment variable", "sourceFile": "From file", + "keyHeldInMemory": "Held in memory until the key file can be written", "keyStoredAt": "Key stored at", "keyWarning": { "title": "CrowdSec API Key Updated", diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 816f06c44..667b023ec 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -423,6 +423,7 @@ "notRegistered": "No registrado", "sourceEnvVar": "Desde variable de entorno", "sourceFile": "Desde archivo", + "keyHeldInMemory": "Se mantiene en memoria hasta que se pueda escribir el archivo de la clave", "keyStoredAt": "Clave almacenada en", "keyWarning": { "title": "Clave API de CrowdSec Actualizada", diff --git a/frontend/src/locales/fr/translation.json b/frontend/src/locales/fr/translation.json index f1fa46765..9ce964cd7 100644 --- a/frontend/src/locales/fr/translation.json +++ b/frontend/src/locales/fr/translation.json @@ -423,6 +423,7 @@ "notRegistered": "Non enregistré", "sourceEnvVar": "Depuis une variable d'environnement", "sourceFile": "Depuis un fichier", + "keyHeldInMemory": "Conservée en mémoire jusqu'à ce que le fichier de clé puisse être écrit", "keyStoredAt": "Clé stockée dans", "keyWarning": { "title": "Clé API CrowdSec Mise à Jour", diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index 17f576c2a..706b1711b 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -423,6 +423,7 @@ "notRegistered": "未注册", "sourceEnvVar": "来自环境变量", "sourceFile": "来自文件", + "keyHeldInMemory": "在内存中保留,直到可以写入密钥文件", "keyStoredAt": "密钥存储位置", "keyWarning": { "title": "CrowdSec API密钥已更新", diff --git a/frontend/src/pages/__tests__/Security.crowdsec.test.tsx b/frontend/src/pages/__tests__/Security.crowdsec.test.tsx index b0ed75038..52797d38b 100644 --- a/frontend/src/pages/__tests__/Security.crowdsec.test.tsx +++ b/frontend/src/pages/__tests__/Security.crowdsec.test.tsx @@ -135,6 +135,7 @@ describe('Security CrowdSec mutation UX', () => { vi.mocked(crowdsecApi.getCrowdsecKeyStatus).mockResolvedValue({ env_key_rejected: false, key_source: 'auto-generated', + key_check: 'accepted', current_key_preview: '...', message: 'OK', }) @@ -176,6 +177,7 @@ describe('Security CrowdSec mutation UX', () => { vi.mocked(crowdsecApi.getCrowdsecKeyStatus).mockResolvedValue({ env_key_rejected: true, key_source: 'env', + key_check: 'accepted', full_key: 'abc123', current_key_preview: 'abc...', rejected_key_preview: 'def...', diff --git a/frontend/src/pages/__tests__/Security.spec.tsx b/frontend/src/pages/__tests__/Security.spec.tsx index e67dc301c..acd430103 100644 --- a/frontend/src/pages/__tests__/Security.spec.tsx +++ b/frontend/src/pages/__tests__/Security.spec.tsx @@ -119,6 +119,7 @@ describe('Security page', () => { vi.mocked(crowdsecApi.getCrowdsecKeyStatus).mockResolvedValue({ env_key_rejected: false, key_source: 'auto-generated', + key_check: 'accepted', current_key_preview: '...', message: 'OK' }) From f4d2072883c79ad8c473d513de46ee2d85654dc4 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 20:51:19 +0000 Subject: [PATCH 022/291] docs: describe CrowdSec key checks, the held key and the key file path --- ARCHITECTURE.md | 5 +++- docs/features/crowdsec.md | 18 ++++++++++--- docs/guides/crowdsec-setup.md | 3 +-- docs/security.md | 2 +- .../crowdsec_auth_manual_verification.md | 27 ++++++++++--------- docs/troubleshooting/crowdsec.md | 2 ++ .../security/crowdsec-key-robustness.spec.ts | 8 +++--- 7 files changed, 40 insertions(+), 25 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 18e94bf99..550543935 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -888,12 +888,15 @@ Sign-in protection is a separate, always-on limiter described under "Management **CrowdSec bouncer key precedence:** the bouncer key is the shared secret the Caddy bouncer uses to talk to the CrowdSec Local API. One rule set, in `backend/internal/crowdsec/bouncerkey`, is used by the proxy config, the handlers and startup: -1. A saved key file (`bouncer_key` in the CrowdSec data folder, `/app/data/crowdsec/bouncer_key` by default) that the Local API accepts wins. Environment values are not tested in this case. +0. A key held in memory (only after a failed save of a new key) is tested first. It is dropped once the key file holds the same key or the Local API rejects it. +1. A saved key file (`bouncer_key` in the CrowdSec data folder, `/app/data/crowdsec/bouncer_key` by default; one function, `bouncerkey.PathFor`, derives the path for the handlers, startup and the proxy config, and `CHARON_CROWDSEC_BOUNCER_KEY_PATH` overrides it everywhere) that the Local API accepts wins. Environment values are not tested in this case. 2. Otherwise the first non-empty environment variable is tested and, if accepted, saved to the key file. Lookup order: `CHARON_SECURITY_CROWDSEC_API_KEY`, `CROWDSEC_API_KEY`, `CROWDSEC_BOUNCER_API_KEY`, `CERBERUS_SECURITY_CROWDSEC_API_KEY`, `CPM_SECURITY_CROWDSEC_API_KEY`. 3. Otherwise a new bouncer is registered and its key saved. Key writes go through `crowdsec.SaveBouncerKey`: a temporary file is written and renamed over the key (mode `0600`). A bind-mounted single file, where rename is not allowed, falls back to an in-place write. A symlinked key file is replaced by a regular file. A failed save is logged as an error. Nothing keeps the new key in memory: Caddy and the key check read the key file, so blocking may not work until the file can be written (fix write access to the data directory, then restart CrowdSec). +Only a definite rejection (HTTP 403) of every candidate key leads to step 3. An unreachable or unclear Local API answer (timeout, 401, 5xx, redirect) leaves the saved key and the bouncer untouched, and the check runs again at the next start. + **Preset apply and config edits:** Applying a hub preset, importing a configuration, and saving a file in the config editor never rename or empty the live CrowdSec directory. Each operation first takes a copy-based snapshot diff --git a/docs/features/crowdsec.md b/docs/features/crowdsec.md index d10ce52cd..23abcdae5 100644 --- a/docs/features/crowdsec.md +++ b/docs/features/crowdsec.md @@ -186,9 +186,12 @@ The bouncer key is the password Charon's built-in firewall layer uses to talk to **Which key Charon uses (in order):** -1. **The saved key file.** If it exists and CrowdSec still accepts it, Charon uses it. This always wins. -2. **A key you set in your compose file.** Charon uses it only when there is no working saved key. Charon then saves it to the file, so it is kept after restarts. -3. **Otherwise,** Charon registers a new key with CrowdSec and saves it. +1. **A key held in memory.** This happens only when Charon got a new key but could not write it to the key file (see "If Charon cannot save the key" below). Charon keeps using it until it restarts. +2. **The saved key file.** If it exists and CrowdSec accepts it, Charon uses it. +3. **A key you set in your compose file.** Charon uses it only when there is no working saved key. Charon then saves it to the file, so it is kept after restarts. +4. **Otherwise,** Charon registers a new key with CrowdSec and saves it. + +**When Charon checks the key:** Charon asks CrowdSec whether the saved key still works. Charon replaces a key only when CrowdSec clearly rejects it. If CrowdSec cannot be reached, or its answer is unclear (for example, it is slow, stopped or restarting), Charon keeps the saved key and does not register a new one. It checks again the next time CrowdSec starts. If you set a key in your compose file, use one of these variable names. Charon checks them in this order and uses the first one that is set: @@ -198,6 +201,13 @@ If you set a key in your compose file, use one of these variable names. Charon c 4. `CERBERUS_SECURITY_CROWDSEC_API_KEY` 5. `CPM_SECURITY_CROWDSEC_API_KEY` +**Advanced: folder and file locations (usually left unset):** + +- `CHARON_CROWDSEC_CONFIG_DIR` sets the folder where Charon keeps CrowdSec's data. In the container it is `/app/data/crowdsec`. +- `CHARON_CROWDSEC_DATA` is an older name for the same setting and still works. If both are set to different folders, Charon writes a warning to the log and uses `CHARON_CROWDSEC_CONFIG_DIR`. +- `CHARON_CROWDSEC_BOUNCER_KEY_PATH` sets the exact file for the key. It now takes priority over the folder setting. If your compose file already sets it, Charon uses a different key file after this update and registers a new key once. +- Outside the container, Charon keeps CrowdSec's files in `data/crowdsec` inside your Charon folder, the same place the CrowdSec settings pages already use. + **Changing the key through your compose file:** changing the variable alone does not replace a saved key that still works. To switch to the key in your variable, do both of these: 1. Change the variable in your compose file. @@ -207,7 +217,7 @@ If you set a key in your compose file, use one of these variable names. Charon c **Symlinks:** if `bouncer_key` is a shortcut (a symlink) to another file, the next save replaces the shortcut with a normal private file. The file the shortcut pointed to is not changed. -**If Charon cannot save the key:** Charon writes an error to the log, and it does not keep the key anywhere else. Blocking may not work until the key file can be written. To fix it, give Charon write access to the data folder, then restart CrowdSec. +**If Charon cannot save the key:** Charon writes an error to the log and keeps the new key in memory, so it keeps working for now. The CrowdSec page shows the key as "held in memory" until the key file can be written. The key is not kept in a file, so it is lost when Charon restarts. After a restart, Charon starts from the key file again and may register a new key, and blocking may not work until the key file can be written. To fix it, give Charon write access to the data folder, then restart CrowdSec. **See it in the UI:** Open the CrowdSec page and look at the **Bouncer API Key** card. It shows a shortened preview of the key, whether it is registered, and where the file is stored. diff --git a/docs/guides/crowdsec-setup.md b/docs/guides/crowdsec-setup.md index 64c772df7..44b0262dc 100644 --- a/docs/guides/crowdsec-setup.md +++ b/docs/guides/crowdsec-setup.md @@ -144,14 +144,13 @@ When Charon registers a new bouncer key, it writes a short notice to the contain docker logs charon ``` -Look for a section like this. The key is partly hidden on purpose: +Look for a section like this: ```text ════════════════════════════════════════════════════════════════════ 🔐 CrowdSec Bouncer Registered Successfully ──────────────────────────────────────────────────────────────────── Bouncer Name: caddy-bouncer -API Key: (partly hidden) Saved To: /app/data/crowdsec/bouncer_key ──────────────────────────────────────────────────────────────────── ⚠️ SECURITY: Full API key saved to file (permissions: 0600) diff --git a/docs/security.md b/docs/security.md index acab36e0c..bf0966e17 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1369,7 +1369,7 @@ services: - CHARON_CADDY_BINARY=caddy - CHARON_IMPORT_CADDYFILE=/import/Caddyfile - CHARON_IMPORT_DIR=/app/data/imports - - CHARON_CROWDSEC_CONFIG_DIR=/app/data/crowdsec + - CHARON_CROWDSEC_CONFIG_DIR=/app/data/crowdsec # Where CrowdSec keeps its data and the bouncer key. CHARON_CROWDSEC_DATA is an older name for the same setting; if both are set to different folders, Charon logs a warning and uses CHARON_CROWDSEC_CONFIG_DIR. extra_hosts: - "host.docker.internal:host-gateway" diff --git a/docs/testing/crowdsec_auth_manual_verification.md b/docs/testing/crowdsec_auth_manual_verification.md index c0abadf75..93f4ca9ed 100644 --- a/docs/testing/crowdsec_auth_manual_verification.md +++ b/docs/testing/crowdsec_auth_manual_verification.md @@ -49,13 +49,13 @@ This document provides step-by-step procedures for manually verifying the Bug #1 4. **Verify Logs Show Validation Failure** ```bash - docker logs charon --tail 100 | grep -i "invalid" + docker logs charon --tail 100 | grep -i "rejected by CrowdSec" ``` **Expected Output**: ```text - time="..." level=warning msg="Environment variable CHARON_SECURITY_CROWDSEC_API_KEY is set but invalid. Either remove it from docker-compose.yml or update it to match the auto-generated key. A new valid key will be generated and saved." masked_key=fake...345 + time="..." level=warning msg="The configured CrowdSec API key was rejected by CrowdSec and is ignored. Charon uses the key file (permissions 0600); no action is required." bouncer=caddy-bouncer key_file=/app/data/crowdsec/bouncer_key ``` 5. **Verify New Key Auto-Generated** @@ -79,13 +79,13 @@ This document provides step-by-step procedures for manually verifying the Bug #1 7. **Verify Logs Show Success** ```bash - docker logs charon --tail 50 | grep -i "authentication successful" + docker logs charon --tail 50 | grep -i "validated successfully" ``` **Expected Output**: ```text - time="..." level=info msg="CrowdSec bouncer authentication successful" masked_key="abcd...wxyz" source=file + time="..." level=info msg="CrowdSec bouncer: file-stored key validated successfully" file=/app/data/crowdsec/bouncer_key ``` **Success Criteria**: @@ -134,7 +134,7 @@ This document provides step-by-step procedures for manually verifying the Bug #1 Look for success message: ```text - time="..." level=info msg="CrowdSec bouncer authentication successful" masked_key="abcd...wxyz" source=file + time="..." level=info msg="CrowdSec bouncer: file-stored key validated successfully" file=/app/data/crowdsec/bouncer_key ``` 5. **Verify Bouncer Connection** @@ -173,8 +173,8 @@ This document provides step-by-step procedures for manually verifying the Bug #1 With new code, same invalid env var should produce: ```text - time="..." level=warning msg="Environment variable CHARON_SECURITY_CROWDSEC_API_KEY is set but invalid..." - time="..." level=info msg="CrowdSec bouncer authentication successful" masked_key="abcd...wxyz" source=file + time="..." level=warning msg="The configured CrowdSec API key was rejected by CrowdSec and is ignored. Charon uses the key file (permissions 0600); no action is required." + time="..." level=info msg="CrowdSec bouncer: file-stored key validated successfully" file=/app/data/crowdsec/bouncer_key ``` **Success Criteria**: @@ -201,9 +201,11 @@ docker restart charon **Expected Log**: ```text -time="..." level=info msg="CrowdSec bouncer authentication successful" masked_key="vali...test" source=environment_variable +time="..." level=info msg="CrowdSec bouncer: env var key validated and saved to key file" key_file=/app/data/crowdsec/bouncer_key source=environment_variable ``` +This line appears on the first start after you set the variable, when no saved key exists yet. Later starts show the file line in 4b instead. + #### 4b. File-Based Key ```bash @@ -215,7 +217,7 @@ docker restart charon **Expected Log**: ```text -time="..." level=info msg="CrowdSec bouncer authentication successful" masked_key="abcd...wxyz" source=file +time="..." level=info msg="CrowdSec bouncer: file-stored key validated successfully" file=/app/data/crowdsec/bouncer_key ``` #### 4c. Auto-Generated Key @@ -229,13 +231,12 @@ docker restart charon **Expected Log**: ```text -time="..." level=info msg="Registering new CrowdSec bouncer: caddy-bouncer" -time="..." level=info msg="CrowdSec bouncer registration successful" masked_key="new-...123" source=auto_generated +time="..." level=info msg="CrowdSec bouncer: successfully registered and saved key" bouncer=caddy-bouncer key_file=/app/data/crowdsec/bouncer_key ``` **Success Criteria**: -- ✅ Logs clearly show `source=environment_variable`, `source=file`, or `source=auto_generated` +- ✅ Logs clearly show which key is in use: the environment variable line, the file line, or the "successfully registered and saved key" line - ✅ User can determine which key is active without reading code --- @@ -325,7 +326,7 @@ docker exec charon cscli bouncers list ## Notes for QA and Code Review - **Backward Compatibility**: Old behavior (name-based validation) is preserved in `validateBouncerKey()` for backward compatibility. New authentication logic is in `testKeyAgainstLAPI()`. -- **Security**: API keys are masked in logs (first 4 + last 4 chars only) to prevent exposure via CWE-312. +- **Logging**: Log lines name the key file and the outcome. They do not include any part of the key. - **File Permissions**: Bouncer key file created with 0600 permissions (read/write owner only), directory with 0700. - **Atomic Writes**: `saveKeyToFile()` uses temp file + rename pattern to prevent corruption. - **Retry Logic**: Connection refused errors trigger exponential backoff (500ms → 750ms → 1125ms → ..., capped at 5s per attempt, 30s total). diff --git a/docs/troubleshooting/crowdsec.md b/docs/troubleshooting/crowdsec.md index 305eecc80..218969b09 100644 --- a/docs/troubleshooting/crowdsec.md +++ b/docs/troubleshooting/crowdsec.md @@ -212,6 +212,8 @@ charon-local-machine 127.0.0.1 password v1.x.x - Apply failed: review the apply response and restore from the backup that was taken automatically, then retry after fixing the underlying issue. If the automatic restore failed too, Charon keeps that backup for manual recovery and puts a small `.keep` file next to it. A kept backup is protected for 7 days. After that, only the newest 3 kept backups keep their `.keep` file, and an older one that loses it becomes eligible for normal cleanup, which removes the oldest backups once more than the usual number are kept. To release a backup for cleanup sooner, delete its `.keep` file. - Apply not supported (501): use curated/offline presets; Hub apply will be re-enabled when supported in your environment. - **I changed the CrowdSec key in my compose file, but Charon still uses the old one**: Charon keeps the key it saved first, as long as CrowdSec still accepts it. Delete the saved file `/app/data/crowdsec/bouncer_key` (inside your data folder) and restart Charon. Charon then uses the key from your compose file if CrowdSec accepts it; otherwise it creates a new key. Details are in the [CrowdSec feature guide](../features/crowdsec.md#managing-the-crowdsec-bouncer-key). +- **The log says "CrowdSec could not be reached to check the key"**: CrowdSec was slow, stopped or still starting, so Charon could not tell whether your saved key works. Charon keeps the saved key and did not register a new one. Nothing needs to be done; Charon checks again the next time CrowdSec starts. If it keeps happening, check that CrowdSec is running (see [Verifying LAPI Status](#verifying-lapi-status)). +- **The log says "Charon could not save the new key"**: Charon has the new key in memory and keeps using it, so blocking keeps working until Charon restarts. Give Charon write access to the data folder (`/app/data/crowdsec`), then restart CrowdSec. Until then, the CrowdSec page shows the key as "held in memory". Details are in the [CrowdSec feature guide](../features/crowdsec.md#managing-the-crowdsec-bouncer-key). - **Security Engine Offline**: If your dashboard says "Offline", it means CrowdSec LAPI is not running. - **Fix**: Ensure CrowdSec is **enabled via GUI toggle** in the Security dashboard. Do NOT use environment variables. - **Action**: Go to Security dashboard, toggle CrowdSec ON, wait 15 seconds, verify status shows "Active". diff --git a/tests/security/crowdsec-key-robustness.spec.ts b/tests/security/crowdsec-key-robustness.spec.ts index d4de23ad3..ee31f8a3f 100644 --- a/tests/security/crowdsec-key-robustness.spec.ts +++ b/tests/security/crowdsec-key-robustness.spec.ts @@ -7,8 +7,8 @@ * `valid` is true exactly when `key_check` is `accepted`. * - `GET /admin/crowdsec/bouncer` returns `key_source` (env_var | file | memory | none). * - * Written as `test.fixme` in commit 1 of the CrowdSec key robustness PR, before the - * `key_check` field and the `memory` key source exist. Commit 8 of that PR removes the fixme. + * The assertions accept every documented value, so they hold whether or not the CrowdSec engine + * is reachable (`key_check` is `none` or `unknown` when no engine answers). * * @see /projects/Charon/docs/plans/crowdsec_hardening_batch5_spec.md (section 3t.5, 3t.6, 3t.14) */ @@ -21,7 +21,7 @@ const KEY_CHECK_VALUES = ['accepted', 'rejected', 'unknown', 'none']; const KEY_SOURCE_VALUES = ['env_var', 'file', 'memory', 'none']; test.describe('CrowdSec key robustness: key check and key source (API)', () => { - test.fixme('key-status reports a key_check whose value agrees with valid', async ({ request }) => { + test('key-status reports a key_check whose value agrees with valid', async ({ request }) => { const response = await request.get(`${ADMIN}/key-status`); expect(response.status()).toBe(200); @@ -30,7 +30,7 @@ test.describe('CrowdSec key robustness: key check and key source (API)', () => { expect(body.valid).toBe(body.key_check === 'accepted'); }); - test.fixme('bouncer info reports a key_source from the allowed set', async ({ request }) => { + test('bouncer info reports a key_source from the allowed set', async ({ request }) => { const response = await request.get(`${ADMIN}/bouncer`); expect(response.status()).toBe(200); From d49e985ea925e16742cd70945300bfcef29be34b Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 21:20:35 +0000 Subject: [PATCH 023/291] fix: release a held CrowdSec key only when it is still the one that was checked --- .../internal/api/handlers/crowdsec_handler.go | 4 +- .../api/handlers/crowdsec_log_capture_test.go | 2 +- .../crowdsec/bouncerkey/bouncerkey.go | 24 +++- .../crowdsec/bouncerkey/release_if_test.go | 107 ++++++++++++++++++ .../services/crowdsec_startup_held_test.go | 2 +- .../crowdsec_startup_log_capture_test.go | 2 +- .../crowdsec_startup_unverified_test.go | 12 +- backend/internal/util/mask.go | 6 +- 8 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 backend/internal/crowdsec/bouncerkey/release_if_test.go diff --git a/backend/internal/api/handlers/crowdsec_handler.go b/backend/internal/api/handlers/crowdsec_handler.go index 59a57c043..456cba253 100644 --- a/backend/internal/api/handlers/crowdsec_handler.go +++ b/backend/internal/api/handlers/crowdsec_handler.go @@ -1659,7 +1659,7 @@ func (h *CrowdsecHandler) GetLAPIDecisions(c *gin.Context) { type BouncerInfo struct { Name string `json:"name"` KeyPreview string `json:"key_preview"` // First 4 + last 3 chars - KeySource string `json:"key_source"` // "env_var" | "file" | "none" + KeySource string `json:"key_source"` // "env_var" | "file" | "memory" | "none" FilePath string `json:"file_path"` Registered bool `json:"registered"` } @@ -1668,7 +1668,7 @@ type BouncerInfo struct { // This endpoint provides UX feedback when env var keys are rejected by LAPI. type KeyStatusResponse struct { // KeySource indicates where the current key came from - // Values: "env" (from environment variable), "file" (from bouncer_key file), "auto-generated" (newly generated) + // Values: "env" (from environment variable), "file" (from bouncer_key file), "memory" (held in memory after a failed save), "auto-generated" (newly generated) KeySource string `json:"key_source"` // EnvKeyRejected is true if an environment variable key was set but rejected by LAPI diff --git a/backend/internal/api/handlers/crowdsec_log_capture_test.go b/backend/internal/api/handlers/crowdsec_log_capture_test.go index 31bbb808e..7140864fb 100644 --- a/backend/internal/api/handlers/crowdsec_log_capture_test.go +++ b/backend/internal/api/handlers/crowdsec_log_capture_test.go @@ -18,7 +18,7 @@ import ( ) // logCaptureFullKey is a 32-character key made of distinct characters, so every -// 4-character window of it is unique and a leaked fragment is detectable. +// 4-character window of it is unique, so any key text in captured output is detectable. const logCaptureFullKey = "Qz7kP2mV9xR4tN6bL1wH8sJ3cF5gD0aY" // captureDebugLogs routes the global logger to a buffer at Debug level and diff --git a/backend/internal/crowdsec/bouncerkey/bouncerkey.go b/backend/internal/crowdsec/bouncerkey/bouncerkey.go index a053dfd30..cfe109d48 100644 --- a/backend/internal/crowdsec/bouncerkey/bouncerkey.go +++ b/backend/internal/crowdsec/bouncerkey/bouncerkey.go @@ -68,6 +68,17 @@ func Release(path string) { delete(heldKeys, path) } +// releaseIf drops the hold for path only while it still equals key. Callers read the held key, check it +// against the key file or a validator, and then release it; a concurrent Hold of a different key in that +// window must survive, so an unconditional Release is not safe here. +func releaseIf(path, key string) { + heldMu.Lock() + defer heldMu.Unlock() + if heldKeys[path] == key { + delete(heldKeys, path) + } +} + // envNames lists the environment variables that may carry the bouncer key, // in lookup order (the first non-empty value wins). var envNames = [...]string{ @@ -132,7 +143,7 @@ func Resolve(path string) (key string, src Source) { if held != file { return held, SourceMemory } - Release(path) + releaseIf(path, held) } if file != "" { return file, SourceFile @@ -224,7 +235,7 @@ func Ensure( if held := Held(path); held != "" { if held == saved { - Release(path) + releaseIf(path, held) } else { switch validate(ctx, held) { case VerdictAccepted: @@ -234,7 +245,7 @@ func Ensure( res.Outcome = OutcomeUnverified return res, nil default: - Release(path) + releaseIf(path, held) } } } @@ -282,13 +293,14 @@ func Ensure( return res, nil } -// settleNewKey records a newly chosen key: a failed save holds it in memory, a successful save releases any hold. +// settleNewKey records a newly chosen key: a failed save holds it in memory, a successful save releases a +// hold on that same key. func settleNewKey(path string, res *EnsureResult) { if res.SaveErr != nil { Hold(path, res.Key) return } - Release(path) + releaseIf(path, res.Key) } // heldKeyValid finishes an Ensure result whose held key validated. The key is in use from memory either way. @@ -299,7 +311,7 @@ func heldKeyValid(res EnsureResult, path, held string, save func(path, key strin res.Outcome = OutcomeHeldKeyValid res.HeldSaveErr = save(path, held) if res.HeldSaveErr == nil { - Release(path) + releaseIf(path, held) } return res } diff --git a/backend/internal/crowdsec/bouncerkey/release_if_test.go b/backend/internal/crowdsec/bouncerkey/release_if_test.go new file mode 100644 index 000000000..d43e9fd84 --- /dev/null +++ b/backend/internal/crowdsec/bouncerkey/release_if_test.go @@ -0,0 +1,107 @@ +package bouncerkey + +import ( + "context" + "fmt" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestReleaseIfDropsMatchingHold checks that a compare-and-release clears a hold that still holds the key. +func TestReleaseIfDropsMatchingHold(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "bouncer_key") + Hold(path, testRegistered) + + releaseIf(path, testRegistered) + + require.Empty(t, Held(path)) +} + +// TestReleaseIfKeepsHoldOfAnotherKey checks that releasing a key that was read earlier does not drop a +// different key held for the same path in the meantime. +func TestReleaseIfKeepsHoldOfAnotherKey(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "bouncer_key") + Hold(path, testHeldInvalid) + // Another goroutine holds a different key between the read of testHeldInvalid and the release. + Hold(path, testRegistered) + + releaseIf(path, testHeldInvalid) + + require.Equal(t, testRegistered, Held(path)) + t.Cleanup(func() { Release(path) }) +} + +// TestEnsureKeepsHoldSetDuringValidation drives Ensure so that a different key is held while Ensure +// validates the key it read. Ensure must not drop the new hold when it releases its own key. +func TestEnsureKeepsHoldSetDuringValidation(t *testing.T) { + clearEnv(t) + + cases := []struct { + name string + verdict Verdict + }{ + {name: "rejected held key", verdict: VerdictRejected}, + {name: "accepted held key", verdict: VerdictAccepted}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + path := seedKeyFile(t, testSavedValid) + Hold(path, testHeldInvalid) + t.Cleanup(func() { Release(path) }) + + fake := newFakeLAPI() + validate := func(ctx context.Context, key string) Verdict { + if key == testHeldInvalid { + // A concurrent failed save holds a different key while the held key is checked. + Hold(path, testRegistered) + return tc.verdict + } + return fake.validate(ctx, key) + } + + _, err := Ensure(context.Background(), path, validate, fake.register, fake.save) + require.NoError(t, err) + require.Equal(t, testRegistered, Held(path)) + }) + } +} + +// TestResolveKeepsHoldSetAfterFileRead runs Resolve against a file that matches the held key while another +// goroutine holds a different key for the same path. The race detector must stay quiet, and the last hold +// must survive once the workers finish. +func TestResolveKeepsHoldSetAfterFileRead(t *testing.T) { + t.Parallel() + + const rounds = 200 + path := seedKeyFile(t, testSavedValid) + t.Cleanup(func() { Release(path) }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + for range rounds { + Hold(path, testSavedValid) + _, _ = Resolve(path) + } + }() + go func() { + defer wg.Done() + for i := range rounds { + Hold(path, fmt.Sprintf("concurrent-held-key-%03d-0000000", i)) + } + }() + wg.Wait() + + Hold(path, testRegistered) + key, src := Resolve(path) + require.Equal(t, SourceMemory, src) + require.Equal(t, testRegistered, key) +} diff --git a/backend/internal/services/crowdsec_startup_held_test.go b/backend/internal/services/crowdsec_startup_held_test.go index 5c6e1c8ef..1c7c5e446 100644 --- a/backend/internal/services/crowdsec_startup_held_test.go +++ b/backend/internal/services/crowdsec_startup_held_test.go @@ -62,5 +62,5 @@ func TestLogStartupKeyDecisionHeldKeyIsInfoWithoutKey(t *testing.T) { out := h.channels(h.drain()) require.Contains(t, strings.Join(out, "\n"), "still in use") - assertNoKeyFragments(t, out, testGeneratedKey) + assertLogsCarryNoKeyText(t, out, testGeneratedKey) } diff --git a/backend/internal/services/crowdsec_startup_log_capture_test.go b/backend/internal/services/crowdsec_startup_log_capture_test.go index 6e2302f4b..1d49b7fcf 100644 --- a/backend/internal/services/crowdsec_startup_log_capture_test.go +++ b/backend/internal/services/crowdsec_startup_log_capture_test.go @@ -20,7 +20,7 @@ import ( ) // Keys made of distinct characters: every 4-character window is unique, so a -// leaked fragment is detectable in the captured output. +// key text in the captured output is detectable. const ( startupCaptureEnvKey = "Qz7kP2mV9xR4tN6bL1wH8sJ3cF5gD0aY" startupCaptureGenKey = "Mn3pQ8rS1tU6vW0xY4zA9bC2dE7fG5hJ" diff --git a/backend/internal/services/crowdsec_startup_unverified_test.go b/backend/internal/services/crowdsec_startup_unverified_test.go index 5d2d51b69..e5830ba53 100644 --- a/backend/internal/services/crowdsec_startup_unverified_test.go +++ b/backend/internal/services/crowdsec_startup_unverified_test.go @@ -45,13 +45,13 @@ func unreachableURL(t *testing.T) string { return url } -// assertNoKeyFragments fails if any 4-character window of key appears in the captured output. -func assertNoKeyFragments(t *testing.T, out []string, key string) { +// assertLogsCarryNoKeyText fails if any 4-character window of key appears in the captured output. +func assertLogsCarryNoKeyText(t *testing.T, out []string, key string) { t.Helper() for i := 0; i+4 <= len(key); i++ { - fragment := key[i : i+4] + window := key[i : i+4] for _, s := range out { - require.NotContains(t, s, fragment, "log output must carry no key characters") + require.NotContains(t, s, window, "log output must carry no key characters") } } } @@ -84,7 +84,7 @@ func TestEnsureBouncerRegistrationUnreachableLAPIKeepsSavedKey(t *testing.T) { require.Equal(t, before, after, "the key file is byte-for-byte unchanged") out := h.channels(h.drain()) require.Contains(t, strings.Join(out, "\n"), bouncerkey.UnverifiedMessage) - assertNoKeyFragments(t, out, logProbeKey) + assertLogsCarryNoKeyText(t, out, logProbeKey) } // TestEnsureBouncerRegistrationUnverifiedOutcomesNeverRotate runs the other non-definite answers through the @@ -169,5 +169,5 @@ func TestLogStartupKeyDecisionUnverifiedIsNeutral(t *testing.T) { out := h.channels(h.drain()) require.Contains(t, strings.Join(out, "\n"), bouncerkey.UnverifiedMessage) - assertNoKeyFragments(t, out, logProbeKey) + assertLogsCarryNoKeyText(t, out, logProbeKey) } diff --git a/backend/internal/util/mask.go b/backend/internal/util/mask.go index 86224f1ad..2d70c3286 100644 --- a/backend/internal/util/mask.go +++ b/backend/internal/util/mask.go @@ -5,9 +5,9 @@ import "fmt" // maskMinLength is the shortest value that may show its first and last characters. const maskMinLength = 24 -// MaskAPIKey returns a display-safe form of an API key for UI previews only: -// "[empty]" for an empty value, "[REDACTED]" below the minimum length, otherwise -// the first and last four characters. Never pass its output to a log. +// MaskAPIKey returns the masked form of an API key for UI previews and API display +// fields only: "[empty]" for an empty value, "[REDACTED]" below the minimum length, +// otherwise the first and last four characters. func MaskAPIKey(key string) string { if key == "" { return "[empty]" From 01981fbba9977bf5df7ab77db2f30eaa428f8149 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 21:21:04 +0000 Subject: [PATCH 024/291] docs: tidy CrowdSec key wording and comments after review --- ARCHITECTURE.md | 2 +- docs/features/crowdsec.md | 2 +- docs/testing/crowdsec_auth_manual_verification.md | 2 +- docs/troubleshooting/crowdsec.md | 2 +- frontend/src/api/crowdsec.ts | 6 ++++-- tests/security/crowdsec-key-robustness.spec.ts | 2 -- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 550543935..51ed9d4dc 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -895,7 +895,7 @@ Sign-in protection is a separate, always-on limiter described under "Management Key writes go through `crowdsec.SaveBouncerKey`: a temporary file is written and renamed over the key (mode `0600`). A bind-mounted single file, where rename is not allowed, falls back to an in-place write. A symlinked key file is replaced by a regular file. A failed save is logged as an error. Nothing keeps the new key in memory: Caddy and the key check read the key file, so blocking may not work until the file can be written (fix write access to the data directory, then restart CrowdSec). -Only a definite rejection (HTTP 403) of every candidate key leads to step 3. An unreachable or unclear Local API answer (timeout, 401, 5xx, redirect) leaves the saved key and the bouncer untouched, and the check runs again at the next start. +Only a definite rejection (HTTP 403) of every candidate key leads to step 3. An unreachable or unclear Local API answer (timeout, 401, 5xx, redirect) leaves the saved key and the bouncer untouched, and the check runs again at the next start. With no saved key yet, the key already in use (the environment value, not yet checked) stays in use, no new bouncer is registered, and the check runs again at the next start. **Preset apply and config edits:** Applying a hub preset, importing a configuration, and saving a file in the config editor never rename or empty the diff --git a/docs/features/crowdsec.md b/docs/features/crowdsec.md index 23abcdae5..bfae05207 100644 --- a/docs/features/crowdsec.md +++ b/docs/features/crowdsec.md @@ -191,7 +191,7 @@ The bouncer key is the password Charon's built-in firewall layer uses to talk to 3. **A key you set in your compose file.** Charon uses it only when there is no working saved key. Charon then saves it to the file, so it is kept after restarts. 4. **Otherwise,** Charon registers a new key with CrowdSec and saves it. -**When Charon checks the key:** Charon asks CrowdSec whether the saved key still works. Charon replaces a key only when CrowdSec clearly rejects it. If CrowdSec cannot be reached, or its answer is unclear (for example, it is slow, stopped or restarting), Charon keeps the saved key and does not register a new one. It checks again the next time CrowdSec starts. +**When Charon checks the key:** Charon asks CrowdSec whether the saved key still works. Charon replaces a key only when CrowdSec clearly rejects it. If CrowdSec cannot be reached, or its answer is unclear (for example, it is slow, stopped or restarting), Charon keeps the saved key and does not register a new one. It checks again the next time CrowdSec starts. If there is no saved key yet, Charon keeps using the key it already has (for example, the one from your compose file, which Charon has not been able to check yet) and does not register a new one. If you set a key in your compose file, use one of these variable names. Charon checks them in this order and uses the first one that is set: diff --git a/docs/testing/crowdsec_auth_manual_verification.md b/docs/testing/crowdsec_auth_manual_verification.md index 93f4ca9ed..f5c659700 100644 --- a/docs/testing/crowdsec_auth_manual_verification.md +++ b/docs/testing/crowdsec_auth_manual_verification.md @@ -326,7 +326,7 @@ docker exec charon cscli bouncers list ## Notes for QA and Code Review - **Backward Compatibility**: Old behavior (name-based validation) is preserved in `validateBouncerKey()` for backward compatibility. New authentication logic is in `testKeyAgainstLAPI()`. -- **Logging**: Log lines name the key file and the outcome. They do not include any part of the key. +- **Logging**: Log lines name the key file and the outcome. - **File Permissions**: Bouncer key file created with 0600 permissions (read/write owner only), directory with 0700. - **Atomic Writes**: `saveKeyToFile()` uses temp file + rename pattern to prevent corruption. - **Retry Logic**: Connection refused errors trigger exponential backoff (500ms → 750ms → 1125ms → ..., capped at 5s per attempt, 30s total). diff --git a/docs/troubleshooting/crowdsec.md b/docs/troubleshooting/crowdsec.md index 218969b09..07d4e4f53 100644 --- a/docs/troubleshooting/crowdsec.md +++ b/docs/troubleshooting/crowdsec.md @@ -212,7 +212,7 @@ charon-local-machine 127.0.0.1 password v1.x.x - Apply failed: review the apply response and restore from the backup that was taken automatically, then retry after fixing the underlying issue. If the automatic restore failed too, Charon keeps that backup for manual recovery and puts a small `.keep` file next to it. A kept backup is protected for 7 days. After that, only the newest 3 kept backups keep their `.keep` file, and an older one that loses it becomes eligible for normal cleanup, which removes the oldest backups once more than the usual number are kept. To release a backup for cleanup sooner, delete its `.keep` file. - Apply not supported (501): use curated/offline presets; Hub apply will be re-enabled when supported in your environment. - **I changed the CrowdSec key in my compose file, but Charon still uses the old one**: Charon keeps the key it saved first, as long as CrowdSec still accepts it. Delete the saved file `/app/data/crowdsec/bouncer_key` (inside your data folder) and restart Charon. Charon then uses the key from your compose file if CrowdSec accepts it; otherwise it creates a new key. Details are in the [CrowdSec feature guide](../features/crowdsec.md#managing-the-crowdsec-bouncer-key). -- **The log says "CrowdSec could not be reached to check the key"**: CrowdSec was slow, stopped or still starting, so Charon could not tell whether your saved key works. Charon keeps the saved key and did not register a new one. Nothing needs to be done; Charon checks again the next time CrowdSec starts. If it keeps happening, check that CrowdSec is running (see [Verifying LAPI Status](#verifying-lapi-status)). +- **The log says "CrowdSec could not be reached to check the key"**: CrowdSec was slow, stopped or still starting, so Charon could not tell whether your saved key works. Charon keeps the saved key and did not register a new one. If you have no saved key yet, Charon keeps using the key it already has (for example, the one from your compose file) and did not register a new one either. Nothing needs to be done; Charon checks again the next time CrowdSec starts. If it keeps happening, check that CrowdSec is running (see [Verifying LAPI Status](#verifying-lapi-status)). - **The log says "Charon could not save the new key"**: Charon has the new key in memory and keeps using it, so blocking keeps working until Charon restarts. Give Charon write access to the data folder (`/app/data/crowdsec`), then restart CrowdSec. Until then, the CrowdSec page shows the key as "held in memory". Details are in the [CrowdSec feature guide](../features/crowdsec.md#managing-the-crowdsec-bouncer-key). - **Security Engine Offline**: If your dashboard says "Offline", it means CrowdSec LAPI is not running. - **Fix**: Ensure CrowdSec is **enabled via GUI toggle** in the Security dashboard. Do NOT use environment variables. diff --git a/frontend/src/api/crowdsec.ts b/frontend/src/api/crowdsec.ts index 0811ab23d..36fc48354 100644 --- a/frontend/src/api/crowdsec.ts +++ b/frontend/src/api/crowdsec.ts @@ -344,10 +344,12 @@ export async function unbanIP(ip: string): Promise { await client.delete(`/admin/crowdsec/ban/${ip.split('/').map(encodeURIComponent).join('/')}`) } -/** CrowdSec API key status information for key rejection notifications. */ -/** LAPI check of the current key: `none` when there is no key to check. */ export type CrowdSecKeyCheck = 'accepted' | 'rejected' | 'unknown' | 'none' +/** + * CrowdSec API key status information for key rejection notifications. + * `key_check` is the LAPI check of the current key: `none` when there is no key to check. + */ export interface CrowdSecKeyStatus { key_source: 'env' | 'file' | 'auto-generated' | 'memory' | 'none' key_check: CrowdSecKeyCheck diff --git a/tests/security/crowdsec-key-robustness.spec.ts b/tests/security/crowdsec-key-robustness.spec.ts index ee31f8a3f..d57d964fc 100644 --- a/tests/security/crowdsec-key-robustness.spec.ts +++ b/tests/security/crowdsec-key-robustness.spec.ts @@ -9,8 +9,6 @@ * * The assertions accept every documented value, so they hold whether or not the CrowdSec engine * is reachable (`key_check` is `none` or `unknown` when no engine answers). - * - * @see /projects/Charon/docs/plans/crowdsec_hardening_batch5_spec.md (section 3t.5, 3t.6, 3t.14) */ import { test, expect } from '@playwright/test'; From f910e13706fb889f26cba8d5483ff765cf785715 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 23:33:26 +0000 Subject: [PATCH 025/291] chore: annotate gosec findings in CrowdSec key tests --- .../internal/api/handlers/crowdsec_key_check_unknown_test.go | 4 ++-- backend/internal/api/handlers/crowdsec_key_held_test.go | 4 ++-- backend/internal/services/crowdsec_startup_held_test.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/internal/api/handlers/crowdsec_key_check_unknown_test.go b/backend/internal/api/handlers/crowdsec_key_check_unknown_test.go index 599894b4b..78816f50a 100644 --- a/backend/internal/api/handlers/crowdsec_key_check_unknown_test.go +++ b/backend/internal/api/handlers/crowdsec_key_check_unknown_test.go @@ -140,7 +140,7 @@ func TestEnsureBouncerRegistrationUnreachableKeepsSavedKey(t *testing.T) { dataDir := t.TempDir() keyPath := filepath.Join(dataDir, "bouncer_key") writeSavedKey(t, keyPath, precSavedKey) - before, err := os.ReadFile(keyPath) + before, err := os.ReadFile(keyPath) //nolint:gosec // G304: test reads a key file under t.TempDir() require.NoError(t, err) h := newKeyCheckHandler(t, unreachableLAPIURL(t), dataDir, noCSCLIMock()) h.envKeyRejected = true @@ -152,7 +152,7 @@ func TestEnsureBouncerRegistrationUnreachableKeepsSavedKey(t *testing.T) { require.NoError(t, regErr) require.Equal(t, bouncerkey.OutcomeUnverified, res.Outcome) require.Equal(t, precSavedKey, res.Key) - after, err := os.ReadFile(keyPath) + after, err := os.ReadFile(keyPath) //nolint:gosec // G304: test reads a key file under t.TempDir() require.NoError(t, err) require.Equal(t, before, after, "the key file is byte-for-byte unchanged") require.True(t, h.envKeyRejected, "an unverified check leaves the rejection state as it was") diff --git a/backend/internal/api/handlers/crowdsec_key_held_test.go b/backend/internal/api/handlers/crowdsec_key_held_test.go index bd6d5b54c..f4a946e78 100644 --- a/backend/internal/api/handlers/crowdsec_key_held_test.go +++ b/backend/internal/api/handlers/crowdsec_key_held_test.go @@ -63,9 +63,9 @@ func unwritableKeyHandler(t *testing.T) (h *CrowdsecHandler, keyPath string, cmd keyPath = filepath.Join(dataDir, "bouncer_key") writeSavedKey(t, keyPath, precSavedKey) require.NoError(t, os.Chmod(keyPath, 0o400)) - require.NoError(t, os.Chmod(dataDir, 0o500)) + require.NoError(t, os.Chmod(dataDir, 0o500)) //nolint:gosec // G302: test makes its temp directory read-only on purpose t.Cleanup(func() { - _ = os.Chmod(dataDir, 0o700) + _ = os.Chmod(dataDir, 0o700) //nolint:gosec // G302: test restores its temp directory so cleanup can remove it _ = os.Chmod(keyPath, 0o600) bouncerkey.Release(keyPath) }) diff --git a/backend/internal/services/crowdsec_startup_held_test.go b/backend/internal/services/crowdsec_startup_held_test.go index 1c7c5e446..74c622212 100644 --- a/backend/internal/services/crowdsec_startup_held_test.go +++ b/backend/internal/services/crowdsec_startup_held_test.go @@ -32,9 +32,9 @@ func TestEnsureBouncerRegistrationHeldKeySurvivesSecondBoot(t *testing.T) { h.writeFileKey(t, "rejected-saved-key-000000000001") keyDir := filepath.Dir(h.keyFile) require.NoError(t, os.Chmod(h.keyFile, 0o400)) - require.NoError(t, os.Chmod(keyDir, 0o500)) + require.NoError(t, os.Chmod(keyDir, 0o500)) //nolint:gosec // G302: test makes its temp directory read-only on purpose t.Cleanup(func() { - _ = os.Chmod(keyDir, 0o700) + _ = os.Chmod(keyDir, 0o700) //nolint:gosec // G302: test restores its temp directory so cleanup can remove it _ = os.Chmod(h.keyFile, 0o600) bouncerkey.Release(h.keyFile) }) From 957c21a4ac9db75ccf5c1642db183cfeba8daa19 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Mon, 2 Nov 2026 23:33:57 +0000 Subject: [PATCH 026/291] docs: add QA report for the CrowdSec key robustness work --- docs/reports/qa_report.md | 238 ++++++++++++++++++++++---------------- 1 file changed, 140 insertions(+), 98 deletions(-) diff --git a/docs/reports/qa_report.md b/docs/reports/qa_report.md index b66a0f718..dfd71ea86 100644 --- a/docs/reports/qa_report.md +++ b/docs/reports/qa_report.md @@ -1,126 +1,168 @@ -# QA Report: CrowdSec Hub Outage Fallback +# QA Report: CrowdSec Bouncer Key Robustness (PR 12) -- Branch: `fix/crowdsec-hub-mirror-fallback`. Re-verified at HEAD `85334c334` (11 commits ahead of `origin/development`). The first audit ran at `e3310bfcc`; this pass re-checks the findings it raised. -- Date: 2026-10-31 -- Scope: behaviour when the CrowdSec hub CDN is slow, refused or unreachable. Covers the bounded time for the hub index fetch, the mirror retry for cscli hub operations (API and startup), the mirror-first memory, the `hub_offline` indicator on the presets list, and the matching UI note. -- Working tree: nothing committed, staged, stashed or pushed by QA. This report is the only file QA wrote. -- **Verdict: READY.** QA-1 and QA-4 are verified fixed at HEAD. QA-3 (the real-cscli contract fixture) was fixed afterwards by removing the unknown `hub_dir` key from the fixture's `cscli` section, a change that had already passed in the container as a scratch copy. QA-2 is amended to a tracked, out-of-scope behaviour (#1796). The coverage and build gates (rows 2-5) were last run at `e3310bfcc`; later changes touched only the shell helper, test files and log wording, and CI repeats the full gates on this PR. +- Branch: `fix/crowdsec-key-robustness`, HEAD `01981fbba` (10 commits ahead of `origin/development` `5381f3f52`). The audit ran first at `6e38d5984`; the branch was then rebased onto a newer `origin/development` during the run (see "Rebase during the run"). The 10 commit patches are identical before and after (`git range-diff`, all `=`). +- Date: 2026-11-02 +- Spec: `docs/plans/crowdsec_hardening_batch5_spec.md` section 3t (3t.14 to 3t.16 authoritative), with `SECURITY.md` and `CLAUDE.md`. +- Scope: the three-valued LAPI key check (403 is the only rejection), the held key after a failed save, the single key path (`PathFor`, `CHARON_CROWDSEC_BOUNCER_KEY_PATH`), and log hygiene for key material. +- Working tree: nothing committed, staged, stashed or pushed by QA. This report is the only file QA wrote. Throwaway containers and volumes were removed; `charon-e2e` and the live `charon` container were not touched by the gates. +- **Verdict: BLOCKED on one CI-blocking lint failure.** `make lint-backend` (full golangci-lint v2.15.0 with gosec) exits 2 on six gosec findings in test files this PR adds (finding B1). Every other DoD gate and every container gate (a) to (l) passes. Fix B1 in a `test:` or `fix:` commit inside the PR, then re-run `make lint-backend`. ## Gate Summary -Rows marked "re-run" were executed at HEAD `85334c334` in this pass. Rows marked "last run" were executed at `e3310bfcc` and have not been repeated, because the code changed since (`fa3b0aa8f`, `85334c334`). Coverage (rows 2-4) must be re-run before merge. - | # | Gate | Result | Numbers and notes | | --- | --- | --- | --- | -| 1 | Container image rebuilt from HEAD (`docker-rebuild-e2e`, no `--clean`) | PASS (re-run) | Built and healthy; `backend/internal/changelog/data/changelog.json` restored with `git checkout` afterwards | -| 2 | Backend coverage (`scripts/go-test-coverage.sh`, gate 87%) | last run | At `e3310bfcc`: line 92.5%, statements 93.5%. Re-run required at HEAD | -| 3 | Frontend coverage (`scripts/frontend-test-coverage.sh`, gate 85%) | last run | At `e3310bfcc`: lines 91.85%, statements 90.85%, branches 85.06%, functions 88.3%. Re-run required at HEAD | -| 4 | Local patch coverage (`scripts/local-patch-report.sh`) | last run | At `e3310bfcc`: overall 98.7%. Re-run required at HEAD | -| 5 | `npm run type-check`, `npm run build`, `go build ./...` | last run | Clean at `e3310bfcc`. Re-run required at HEAD | -| 6 | `make lint-fast` (staticcheck, govet, errcheck, ineffassign, unused) | PASS (re-run) | 0 issues (backend and agent) | -| 7 | shellcheck (`.docker/cscli-hub-mirror.sh`, `.docker/docker-entrypoint.sh`, `configs/crowdsec/install_hub_items.sh`, `scripts/cscli_hub_mirror_test.sh`) | PASS (re-run) | clean | -| 8 | `scripts/cscli_hub_mirror_test.sh` under bash, dash and busybox sh | PASS (re-run) | 118 passed, 0 failed under each shell (109 at `e3310bfcc`; the new case covers the `terminated signal received` exit shape, QA-1) | -| 9 | `go test -race -count=1 ./internal/crowdsec/` | PASS (re-run) | `ok` | -| 10 | `go test -race -count=1 -run 'Hub\|Preset\|Mirror' ./internal/api/handlers/` | PASS (re-run) | `ok` | -| 11 | Real-cscli contract test `TestCSCLIContractAcceptsMirrorConfig` (cscli 1.8.2, inside the container) | FIXED | Failed at `85334c334` because `cscli.hub_dir` is an unknown key for cscli 1.8.2; the key was removed from the fixture (see QA-3). Passed in the container as a scratch copy | -| 12 | `hadolint` (`Dockerfile`) | last run | 9 pre-existing findings, the same rule codes as on `origin/development` (O-2) | -| 13 | GORM security scan | NOT REQUIRED | no file under `backend/internal/models/` and no migration changed | -| 14 | Playwright `tests/security/crowdsec-hub-offline.spec.ts`, `crowdsec-ui-states`, `crowdsec-import`, `crowdsec-first-enable`, `crowdsec-diagnostics`, `crowdsec-export-redaction` (`--project=security-tests`) | PASS (re-run) | 107 passed, against the image rebuilt at HEAD | -| 15 | Playwright `tests/crowdsec-whitelist.spec.ts` (`--project=firefox`) | PASS (re-run) | 12 of 12 | -| 16 | CodeQL and Trivy | DEFERRED TO CI | `fix:`, `test:` and `docs:` commits only; CI runs both on the PR | - -## Container Gate - -Method: throwaway containers from the locally built `charon:local` image, rebuilt from HEAD for this pass. A netshoot holder container owns the network namespace, and the Charon container joins it. The holder carries the packet rules, so the rules survive Charon restarts. The Docker daemon was the rootless one. No host port was published, no Docker socket was mounted, and the live `charon` container was not touched. Rules were added to the hub CDN and mirror addresses resolved at run time, and removed by the holder's own state file. All QA containers and volumes were deleted by exact name at the end. The standard `charon-e2e` container from the rebuild skill was left running for the Playwright suites. - -Hub hosts: the CDN (`cdn-hub.crowdsec.net`) and the GitHub mirror (`raw.githubusercontent.com`). "Refused" means `REJECT` with TCP reset; "hung" means `DROP`. - -| Case | What was run | Observed | Result | +| 1 | `cd backend && go build ./...`, `go vet` on touched packages | PASS | clean | +| 2 | `go test -race -count=1` `./internal/crowdsec/...`, `./internal/config/...`, `./internal/caddy/...`, `./internal/util/...`, `./cmd/api/...` | PASS | crowdsec ok (127 s), bouncerkey ok, config ok, caddy ok, util ok, cmd/api ok | +| 3 | `go test -race -count=1 ./internal/api/handlers/` | PASS | ok, 491 s | +| 4 | `go test -race -count=1 ./internal/services/` | PASS | ok, 607 s | +| 5 | `scripts/go-test-coverage.sh` (gate 87%) | PASS | line coverage 92.0%, statements 93.1% | +| 6 | `bash scripts/local-patch-report.sh` | PASS | overall and backend patch coverage 96.2% (235 changed lines, 226 covered); both artifacts written. 7 uncovered changed lines, see L1 | +| 7 | `npm run type-check` (frontend) | PASS | clean, run on the first tree and again on `01981fbba` | +| 8 | `scripts/frontend-test-coverage.sh` (gate 85%) | PASS | at `01981fbba`: 319 files, 4009 passed, 4 skipped, 2 todo; lines 91.83%, branches 85.05%, functions 88.3%. Branches sit at the floor (3t.8 said to watch it) | +| 9 | `make lint-backend` (full golangci-lint, gosec) | **FAIL** | exit 2, 6 issues, all gosec, all in new test files (B1) | +| 10 | `lefthook run pre-commit` | PASS, but a no-op on a clean tree | with no staged files every hook was skipped. Re-run as `lefthook run pre-commit --all-files`: exit 0 on `01981fbba`, including `golangci-lint-fast`, `go-vet`, `frontend-lint`, `frontend-type-check`, `shellcheck`, `markdownlint`, `actionlint` and `semgrep` (0 findings, 367 rules, 2313 files) | +| 11 | `./scripts/scan-gorm-security.sh --check` | NOT REQUIRED | `git diff --name-only origin/development..HEAD` has zero `backend/internal/models/` paths | +| 12 | Targeted Playwright: `tests/security/crowdsec-key-robustness.spec.ts`, `crowdsec-first-enable.spec.ts`, `crowdsec-import.spec.ts` | PASS, 64 of 64 | ran on `--project=security-tests`, see note N2 | +| 13 | Container gates (a) to (l) | PASS (a to l), see table below | the development control reproduces the bug for (b) and (d) | +| 14 | CodeQL and Trivy | DEFERRED TO CI | `fix:`, `test:`, `docs:` and `refactor:` commits, no new feature surface (CLAUDE.md scope rule) | + +### Container gates (a) to (l) + +Images: `charon:local` rebuilt from the branch with `.github/skills/scripts/skill-runner.sh docker-rebuild-e2e` (exit 0, healthy). Control: `charon:qa-control` built with `docker build` from `git archive 3c24d1b21` (the development base before the PR). All throwaway containers ran on the rootless daemon with unique names, named volumes, and host port 80/443 unpublished. The live `charon` container was not used. + +| Gate | Branch result | Control (development) result | Evidence | | --- | --- | --- | --- | -| (e) entrypoint startup, CDN hung, mirror reachable | 3 fresh starts with a new volume | Healthy after 41.2 s, 40.8 s and 41.5 s. The first `cscli hub update` ran its 30 s budget, failed with `terminated signal received`, and the log then shows `cscli hub update did not complete on the CDN; retrying through the hub mirror.` The retry downloaded the index and startup continued. No `.charon-hub-mirror-*` file remained. | PASS (QA-1 verified fixed) | -| (e) entrypoint startup, CDN refused | 1 fresh start | Healthy after 10.7 s. The connect was refused at once, the mirror retry ran, and the install step followed. No derived file remained. | PASS | -| (e) entrypoint startup, both hosts refused | 1 fresh start | Healthy after 5.3 s. Both attempts failed at once, then `Hub index update failed (network issue?)`; startup continued. No derived file remained. | PASS (bounded) | -| (e) entrypoint startup, both hosts hung | 1 fresh start (no cached index, new volume) | Healthy after 66.6 s: two 30 s hub attempts, then the item installs failed fast on the missing index, as designed. No derived file remained. | PASS (bounded) | -| (e) missing item installed under a hung CDN | One scenario removed from a running container (`http-sqli-probing`), container stopped, CDN hung, container started | The hub update fell back to the mirror after 30 s. The install of the missing scenario ran in about 0.3 s (17:35:57.3 to 17:35:57.6 in the container log) with no CDN connect delay, so it used the mirror-first window. The item was installed afterwards. No derived file remained. The 60 s install bound was not reached. | PASS (mirror-first install observed) | -| (a) CDN refused, API list, pull and apply | 3 repetitions | List 0.09-0.37 s with `hub_offline` false. Pulls 0.17-0.21 s. Apply completed after the mirror answered the hub update. | PASS (last run at `e3310bfcc`) | -| (b) CDN hung, API list, pull | 3 repetitions | List 5.07-5.09 s; pulls 5.18-5.27 s, inside the 25 s pull budget. | PASS (last run at `e3310bfcc`) | -| (c) cscli through the derived config | Apply with CDN refused; apply with CDN hung in mirror-first; restore with both hung | Derived config used and removed; mirror-first apply 0.98-1.1 s; derived file excluded from the file list and export (0 entries). | PASS (last run at `e3310bfcc`) | -| (d) CDN and mirror both down, API | Presets list, uncached pull, restore with a second restore, whitelist write and preset apply during the restore | List answered in 10.16 s with `hub_offline` true; pull returned 503 `hub_unreachable` in 10.16 s. Second restore returned 409 in 1.8 ms (`a restore is already running`, no `error_code`). Whitelist write 201. Preset apply during the restore waited and returned 200 after 76.7 s. | PASS-with-note (QA-2, tracked as #1796) | -| (f) operator-set `__hub_url_template__` | Key in `config.yaml` and in `config.yaml.local`; restart with the CDN refused; API apply | Startup logged `Hub mirror retry unavailable (the cscli config could not be prepared)`, no retry, no derived file. The `.local` variant logged the local-override decline, made no retry and left no derived file. The `config.yaml` variant was checked at startup only. | PASS (last run at `e3310bfcc`) | -| (QA-4) retry log wording, API | Live-first: CDN hung, first preset pull and apply. Mirror-first: CDN restored, mirror hung, second apply. | Live-first: `cscli hub request failed on the live hub address; retrying on the hub mirror`, followed by `cscli hub request answered by the hub mirror`. Mirror-first: `cscli hub request failed on the hub mirror; retrying on the live hub address`, followed by `answered by the live hub address; mirror-first window cleared`. Both wordings match the cause. | PASS (QA-4 verified fixed) | -| Real cscli contract (QA-3) | `cscli_mirror_contract_test.go` built as a static test binary and run in the container with cscli 1.8.2 | `exec: cscli -c version` fails: `field hub_dir not found in type csconfig.CscliCfg`. The `config_paths` fix from the earlier run is in place; the new failure is the `hub_dir` key in the `cscli` section of the fixture. | FAIL (QA-3) | - -Other observations from the container runs: - -- A refused CDN never makes the presets list or a pull slow (about 0.1-0.4 s). A hung CDN costs about 5 s per call. -- The export logs one warning per symlinked config entry (O-1). -- The restore returned 502 `msgRestoreHubDown` at 60.0 s in every both-down run at `e3310bfcc`. +| (a) real-LAPI verdict | PASS | n/a | Real engine CrowdSec v1.8.2 inside the container. Bogus key `GET /v1/decisions` returned **403**. No key returned **403** too. A key from the file returned **200**. `kill -STOP` on the engine: `key-status` returned `key_check: unknown` after 5.02 s. `kill -CONT`: `accepted` in 0.02 s. `kill -9` on the engine: `unknown` after 5.03 s, key file hash unchanged, `caddy-bouncer` still listed. | +| (b) unreachable `CHARON_SECURITY_CROWDSEC_API_URL=http://127.0.0.1:9` at boot | PASS | **FAIL** (bug reproduced) | Branch: key hash `0b371f6112570859` before and after; `created_at` unchanged; boot log line 110 "CrowdSec bouncer registration: LAPI is ready" precedes the single Warn at line 111; `cscli lapi status` exit 0 at that time (gate l). Control: hash `0f4ca0249c954c31` became `ef128badbf7eb933`, `created_at` moved, and the log says "successfully registered and saved key" with a `masked_key` field | +| (c) garbage saved key | PASS | not run | Key file replaced by `printf` garbage (`chown charon`, mode 600). Restart with healthy LAPI: hash `4711337f164a535e`, new `created_at`, `key-status` `accepted`, Caddy-carried key returns **200** from LAPI | +| (d) read-only bind-mounted key file holding a rejected key | PASS | **FAIL** (stale key kept) | Branch: `-v :/app/data/crowdsec/bouncer_key:ro`. `key_source: memory`, `key_check: accepted`, message says the key is used until restart. Caddy admin config carries the registered key: LAPI **200**. The error log shows `read-only file system`. Control: `key_source: file`, `valid: false`, Caddy carries the garbage key: LAPI **403** | +| (e) `CHARON_CROWDSEC_BOUNCER_KEY_PATH` | PASS | not run | `alt_bouncer_key` created, the default `bouncer_key` not created, `key_file_path` reports the alt path, Caddy-carried key LAPI **200** | +| (f) log scan for key material | PASS on the branch | control positive | Scanned 6 branch and control log files for `masked_key`, `API Key:`, and the first and last four characters of every key collected from the containers (7 distinct values). Branch: zero matches. Control: `masked_key` x1 and a 4+4 character match in the control boot log (expected for the pre-fix build) | +| (g) existing CrowdSec key specs | PASS | not run | 64 of 64 (see the Playwright row above) | +| (h) Start with a stored unreachable `CrowdSecAPIURL` | PASS | not run | `POST /security/config` set `crowdsec_api_url` to `http://127.0.0.1:9`. Then stop and Start. Response carries `warning`; key hash and bouncer `created_at` unchanged; **0** `bouncers delete`/`add` log lines | +| (i) double Start with the read-only file | PASS | not run | Two stop/start cycles. Bouncer `created_at` unchanged after each; `key_source` stays `memory`; one read-only save error logged | +| (j) restart after a hold | PASS | not run | `docker restart` with the read-only file still in place. The bouncer rotated (`created_at` changed) and the `read-only file system` error was logged. The held key is lost on restart, as the spec's "lost on restart" claim (3t.14 m5 j) states | +| (k) 401 vs 403 probe | PASS (accepted as "403 seen, no 401") | not run | 403 for a bogus key and for a missing key. No 401 seen | +| (l) LAPI wait passed before the unverified warning | PASS | not run | Proof in (b): "LAPI is ready" (line 110) before the Warn (line 111); `cscli lapi status` exit 0 | ## Findings -Severity follows the public scale (critical, high, medium, low). "Blocking" means the finding must be fixed in this PR under the Definition of Done. +Severity order. Each finding has evidence and the remediation. -| ID | Severity | Status | Title | -| --- | --- | --- | --- | -| QA-1 | Medium | Verified fixed | Startup mirror retry did not run when the hub CDN hung | -| QA-2 | Medium | Amended: PASS-with-note, tracked as #1796 | A conflicting preset apply waits for an in-flight restore instead of refusing at once | -| QA-3 | Low (test defect) | Fixed in this PR | The real-cscli contract test fails when cscli is installed | -| QA-4 | Low | Verified fixed | Retry warnings named the wrong hub address and printed before a declined retry | -| QA-5 | Info | Open (documentation) | Observed budgets differ from the change's design notes | -| QA-6 | Info | Accepted, may improve | The presets list spends one CDN connect attempt per call while the outage lasts | +### B1 (High, CI blocker): six gosec findings fail `make lint-backend` -### QA-1: Startup mirror retry did not run when the hub CDN hung (Medium, verified fixed) +- Evidence: `make lint-backend` on `01981fbba` ends with `6 issues: * gosec: 6` and `make: *** [Makefile:199: lint-backend] Error 1` (exit 2). The findings: + - `backend/internal/api/handlers/crowdsec_key_check_unknown_test.go:143` G304 (`os.ReadFile(keyPath)`), `:155` G304. + - `backend/internal/api/handlers/crowdsec_key_held_test.go:66` G302 (`os.Chmod(dataDir, 0o500)`), `:68` G302 (`os.Chmod(dataDir, 0o700)`). + - `backend/internal/services/crowdsec_startup_held_test.go:35` G302 (`os.Chmod(keyDir, 0o500)`), `:37` G302. +- Why it slipped: `lefthook` `golangci-lint-fast` does not enable gosec, so the local pre-commit hook passes. CI runs `make lint-backend` and will fail. +- Remediation: these are deliberate test fixtures (read-only directories and a fixed test path). Add a per-line `//nolint:gosec // : ` with a reason, the same form as `backend/internal/orthrus/ca.go:84`. Put the change in a `test:` commit in this PR. Re-run `make lint-backend` and expect `0 issues`. -- **Where.** `.docker/cscli-hub-mirror.sh` (`_hub_retry_wanted`, `_hub_network_failure`), called from `cscli_hub` for `hub update` in `.docker/docker-entrypoint.sh`. -- **Original cause.** The retry rule treated an attempt as timed out only on exit status 124 or 143, or on a network-error marker. cscli exits with status 1 and the text `terminated signal received` when its budget runs out, which matched neither rule. -- **Verification at HEAD.** Three fresh hung-CDN starts each logged the retry line and became healthy at about 41 s. The derived file was absent afterwards. The shell test has the new case, and it passes under bash, dash and busybox sh. +### L1 (Low): uncovered changed lines in the patch -### QA-2: A conflicting preset apply waits for an in-flight restore instead of refusing at once (Medium, amended, tracked as #1796) +- `backend/internal/services/crowdsec_startup.go:379-380` (the default `http://127.0.0.1:8085` branch when `CHARON_SECURITY_CROWDSEC_API_URL` is unset). +- `backend/internal/crowdsec/bouncerkey/verdict.go:81` (`ctx.Done()` inside the retry wait) and `:96-97` (the `NewRequest` error branch, practically unreachable). +- `backend/internal/crowdsec/bouncerkey/bouncerkey.go:341-344` (`HeldKeyInUseMessage(saved=true)`, the "saved to the key file" text). +- Patch coverage is 96.2%, above the gate. Remediation (optional): one table case each for the default URL, the `ctx.Done` return during a backoff wait, and the `saved=true` message. -- **Where.** `backend/internal/api/handlers/crowdsec_handler.go` `holdUnlessBusy` and `backend/internal/crowdsec/apply_jobs.go` `Hold`; the restore handler in `crowdsec_hub_items.go`. -- **Evidence.** With both hosts hung, a restore was in flight. A preset apply returned 200 after 76.7 s, so it waited for the restore. A second restore returned 409 at once, but the response has no `error_code`. -- **Decision.** The waiting behaviour is existing behaviour and is out of scope for this change. It is tracked as #1796, which also covers the missing `error_code` on the restore-versus-restore 409. The acceptance criterion for this change is amended to match. Both behaviours stay within their time bounds. -- **Gate row (d)** is therefore PASS-with-note. +### R3 (Low, carried from 3t.16 r3): delete-then-add is not atomic -### QA-3: The real-cscli contract test fails when cscli is installed (Low, test defect, fixed) +- Evidence: `backend/internal/api/handlers/crowdsec_handler.go:2021-2024` and `backend/internal/services/crowdsec_startup.go:348-354` run `bouncers delete` and then `bouncers add`. The ctx guard runs before both, so a cancel between the two calls leaves no `caddy-bouncer` until the next Start. The next Start then sees the saved key rejected and registers again. No runtime test covers the window. +- Remediation: low. Record as a low issue (not a regression; the base had the same delete-then-add). Optional later fix: add the new bouncer under a temporary name and swap, or validate after add. -- **Where.** `backend/internal/crowdsec/cscli_mirror_contract_test.go`, `contractLiveConfig` (the fixture written by `writeCSCLIConfig`, whose `cscli` section is `{hub_branch: master, hub_dir: }`). -- **Evidence.** Inside the container with cscli 1.8.2, the derived configuration is rejected: `line 11: field hub_dir not found in type csconfig.CscliCfg`. The earlier `no configuration paths provided` error is fixed; the `config_paths` block is now present. A hand-written configuration with `config_paths` and `cscli: {hub_branch: master}` is accepted (exit 0). -- **Validation.** In a scratch copy outside the repository, the `"hub_dir"` entry was removed from the `cscli` map in the fixture. The test then passed in the container: `--- PASS: TestCSCLIContractAcceptsMirrorConfig`. -- **Impact.** The test skips on hosts without cscli, so it passes in those environments and hides the defect. Where cscli is installed it fails. -- **Remediation (done).** Removed `hub_dir` from the `cscli` section of the fixture (kept in `config_paths`). The fixture still parses on every host; the cscli run itself skips on hosts without cscli. +### O1 (Low, code-read, not run): a reachable non-local LAPI can drive a local re-register on each Start -### QA-4: Retry warnings named the wrong hub address and printed before a declined retry (Low, verified fixed) +- Evidence: the handler key check uses the stored `CrowdSecAPIURL` (`backend/internal/api/handlers/crowdsec_handler.go:1728-1731`), while registration runs `cscli` against the local engine. If the stored URL is a reachable LAPI that does not know the local `caddy-bouncer` key, its 403 is a definite Rejected, and the local bouncer is deleted and re-added on every Start. +- Status: pre-existing. The development base uses the same stored-URL check (`git show 3c24d1b21:backend/internal/api/handlers/crowdsec_handler.go`, around `:1708`). This PR does not change the behaviour, and I did not run it. Remediation: file as a low issue, scoped to the stored URL and the local registration mismatch. -- **Where.** `backend/internal/crowdsec/cscli_run.go` `logHubRetry`, `logMirrorUnavailable`. -- **Verification at HEAD.** The wording is built from the address that failed (live-first and mirror-first both observed in the container logs). The retry line is logged only once the retry is certain to run; a declined retry logs `hub mirror retry unavailable` alone. The shell helper's live-first line names the failed command and the mirror. +### N1 (Informational): key exposure through the API -### QA-5: Observed budgets differ from the change's design notes (Info, open) +- `GET /admin/crowdsec/key-status` sets `full_key` only in the `auto-generated` branch, as before (`crowdsec_handler.go:1803-1809`). The `memory` branch sets `CurrentKeyPreview` (masked) and no `FullKey` (`:1796-1801`). Good. +- `GET /admin/crowdsec/bouncer/key` (`crowdsec_handler.go:2124-2136`) returns the full key for `source: memory`, the same as for the file and env sources. It sits in the `managementAdmin` route group (`routes.go:1063`, `RegisterRoutes` at `crowdsec_handler.go:2836`), so only admins can call it. Accepted as the existing admin reveal; it now also reveals a held in-memory key. No change requested. -- The restore's hub update budget is 60 s (`hubRestoreUpdateTimeout`, `crowdsec_hub_items.go:39`). The design notes state 20 s. -- A hung CDN connect fails after about 5 s (the dial timeout), not after the roughly 11 s per-attempt share the notes describe. The bounds still hold with a wide margin. -- Remediation: correct the design notes, or the constant, so both agree. +### N2 (Informational): Playwright project -### QA-6: The presets list spends one CDN connect attempt per call while the outage lasts (Info, accepted) +- The firefox project excludes `**/tests/security/**` (`playwright.config.js`, the firefox `testIgnore`). The three specs run only under `security-tests`, which uses a Desktop Chrome device. I ran that single project for them. It is one project, not a second browser. Result: 64 passed, exit 0. -- While the cscli mirror-first window is open, the HTTP index walk still tries the CDN first on every list call, costing about 5 s per call under a hang (about 10 s when both hosts are down). This is inside every bound measured. Optionally, reorder the walk to try the mirror first while the window is open, or record the trade-off in the commit body. +### N3 (Informational, pre-existing, out of scope): Caddy admin on the container interface -## Out-of-scope findings (for filing) +- Boot log: `admin endpoint on open interface; host checking disabled`, `address: 0.0.0.0:2019`, in the branch and control logs. The Caddy config carries the bouncer API key. The E2E compose publishes 2019 on loopback only. Not exploitable from the host in the tested setup, and not part of this PR. Security-sensitive: keep any tracking in a private advisory per `SECURITY.md`, not a public issue. -| ID | Severity | Summary | Evidence and suggested approach | -| --- | --- | --- | --- | -| #1796 | Tracked (existing issue) | A preset apply during an in-flight hub restore waits for the restore; restore-versus-restore returns 409 without an `error_code`. | Already tracked. Not blocking under the amended criterion (QA-2). | -| O-1 | Low | The export logs one warning per symlinked config entry, so one export writes dozens of warning lines. | Seen on every export in the container (`skipping non-regular entry config/... during export`). The export code is not in this branch's diff. Suggested: log once per export with a count, or downgrade to debug. | -| O-2 | Low | The Dockerfile has 9 hadolint findings (DL3003, DL3025, DL3066, DL3067, DL4006, SC2012). | Present on `origin/development` too; not introduced here. Suggested: a hygiene pass on the Dockerfile. | -| O-3 | Info | The frontend branch coverage (85.06% at `e3310bfcc`) is close to the 85% floor, although the gate passes on lines and statements. | Watch on future frontend changes; re-measure at HEAD before merge. | +### C1 (Low, commit text): `(security)` subject wording + +- Commit `0340e880b` subject: `fix(security): harden log output in the CrowdSec integration`. It meets the CLAUDE.md rule: general category, no vulnerability class, no vector, no code path. It is the most specific of the approved options, and "log output" names the surface. If the orchestrator wants the vaguer reading, use `fix(security): harden the CrowdSec integration` (own commit, not yet pushed). +- The 10 commit bodies are empty. No session ID or session link in commits, code comments, test names or docs. The diff additions contain no `leak`, `exposure`, `fragment`, `secret`, `credential`, `vulnerab`, `CVE`, `GHSA` or `attacker` wording in code or test names. `TestCrowdSecLogsCarryNoKeyText` is neutral. +- Informational: historical records still show `masked_key` lines (`docs/reviews/crowdsec_auth_fix_code_review.md:45,158,161,163`, `docs/plans/bouncer_key_logging_spec.md:63`). They are review and plan records, not user docs, and they describe the old output as an example, not as a flaw. Leave them. + +### Review minors from 3t.16: status + +| Item | Status | Evidence | +| --- | --- | --- | +| r1 wording (`util/mask.go`, helper names, docs bullet) | RESOLVED | grep for "Never pass its output", "leaked fragment", "do not include any part", `assertNoKeyFragments` returns no hits in backend, frontend, docs or tests. `MaskAPIKey` says "for UI previews and API display fields only" (`backend/internal/util/mask.go:8-10`) | +| r2 TOCTOU in read-side release | RESOLVED | `releaseIf` is used in `Resolve` (`bouncerkey.go:146`) and `Ensure` (`:238`, `:248`), and in `settleNewKey` and `heldKeyValid` (`:303`, `:314`). `release_if_test.go` covers it and runs under `-race` | +| r3 delete-then-add not atomic | OPEN, low | see R3 | +| r4 env-only wording in docs | RESOLVED | `docs/features/crowdsec.md:194` now says "If there is no saved key yet, Charon keeps using the key it already has" | +| n6 stacked doc comments in `frontend/src/api/crowdsec.ts` | RESOLVED | the `CrowdSecKeyCheck` type and the `CrowdSecKeyStatus` doc now sit together | +| n7 `key_source` values comment | RESOLVED | `crowdsec_handler.go:1671` lists `memory` | +| n8 stale plan citations in the E2E spec header | RESOLVED | header lines 1-8 carry no planning-doc references | +| n9 QA report gates | RESOLVED by this report | | + +## Security Review + +### Destructive paths (can Unknown, an error or a cancel delete or re-register the bouncer?) + +- Only two destructive sites exist: `bouncers delete` followed by `bouncers add` in `registerBouncer` (`crowdsec_handler.go:2021-2024`) and in the startup `register` closure (`crowdsec_startup.go:348-354`). Both are reached only through `bouncerkey.Ensure`. +- `Ensure` reaches `register` only after every present candidate (held, saved, env) returned `VerdictRejected` (`bouncerkey.go:285`). `Unknown` returns `OutcomeUnverified` with no register, delete or save (`:243-263`). A cancelled context returns `OutcomeUnverified` before register (`:280-282`). +- `Check` maps only 403 to Rejected. 401, 3xx, 404, 429, 5xx, transport errors, per-attempt timeouts and cancellation all map to Unknown (`verdict.go`). Runtime evidence: gates (a), (b), (h) and (i) show no delete or add on Unknown (LAPI killed, stopped, stalled, or an unreachable stored URL). +- Residual, not destructive to the key: R3 (a cancel between delete and add) and O1 (a stored URL on another LAPI). +- Result: no path found where Unknown, an error or a cancel deletes or re-registers the bouncer, other than the R3 window, which recovers at the next Start. + +### Key exposure + +- API: see N1. The masked preview is used everywhere except the admin reveal route. +- Logs: gate (f) found no key material in the branch logs. No new log call carries the key or a held key. The debug verdict line carries only a verdict word. + +### Commit and text rule + +- See C1. Subject is at the category level. No cause text in commit bodies, code comments, test names, docs or the PR text (none was opened by QA). + +## Commands run (exact) + +- `cd /projects/Charon/backend && go build ./...` and `go vet ./internal/crowdsec/... ./internal/config/... ./internal/caddy/... ./internal/util/... ./internal/services/ ./internal/api/handlers/ ./cmd/api/`: exit 0. +- `go test -race -count=1 -timeout 25m ./internal/crowdsec/... ./internal/config/... ./internal/caddy/... ./internal/util/... ./cmd/api/...`: all ok. +- `go test -race -count=1 -timeout 25m ./internal/api/handlers/`: `ok ... 491.256s`. +- `go test -race -count=1 -timeout 25m ./internal/services/`: `ok ... 606.896s`. +- `bash scripts/go-test-coverage.sh`: `total: 93.1% statements`, `Line coverage: 92.0%`, `Coverage requirement met`, exit 0. +- `cd frontend && npm run type-check`: exit 0. +- `bash scripts/local-patch-report.sh`: `Overall 96.2 pass`, artifacts `test-results/local-patch-report.{md,json}` written. +- `bash scripts/frontend-test-coverage.sh`: `Coverage gate: PASS (lines 91.83% vs minimum 87%)`, exit 0. The script prints a deprecation notice pointing at the skill runner; it still runs the gate. +- `make lint-backend`: `6 issues`, exit 2 (B1). +- `lefthook run pre-commit`: exit 0, all hooks skipped (no staged files). +- `lefthook run pre-commit --all-files`: exit 0, all hooks passed. +- `.github/skills/scripts/skill-runner.sh docker-rebuild-e2e`: exit 0, `charon-e2e` healthy on `charon:local` (rootless daemon, the default context). +- `docker build -t charon:qa-control .` from `git archive 3c24d1b21`: exit 0. +- `npx playwright test tests/security/crowdsec-key-robustness.spec.ts tests/security/crowdsec-first-enable.spec.ts tests/security/crowdsec-import.spec.ts --project=security-tests`: `64 passed`, exit 0. +- Container gates: `docker run` with `--env-file /projects/Charon/.env`, `CHARON_ENV=e2e`, named volumes, `-p 127.0.0.1::8080`. In-container probes with `wget` (the image has no `curl`) against `http://127.0.0.1:8085/v1/decisions`. Management API calls with a per-container cookie jar. Key values were never printed; only SHA-256 prefixes were compared. + +## Gaps and deviations (not run or not complete) + +- `go test -race -count=3` for the four touched package groups (3t.8) was not run; the brief asked for `-count=1`. Estimated extra time about 1 hour. CI runs the full suite on the PR. +- CodeQL (Go and JS) and Trivy were not run locally (see gate 14). CI runs them on every PR. +- The Playwright run used `--project=security-tests` rather than `--project=firefox`, because firefox excludes `tests/security/**` (N2). +- The gate (f) scan covered the keys present in the containers at scan time. Keys that were replaced during the runs were not retained, so they are not in the scan set. +- Gate (d): my first hash comparison line in the shell was broken. The host file hash (`a68cc43f20ea4832`) matches the content written before the run, and the read-only mount rejected every write, so the file was not changed. The check was re-done by hash, not by a broken comparison. + +## Rebase during the run -## Verdict +- The reflog shows `rebase (finish): returning to refs/heads/fix/crowdsec-key-robustness` at 22:06:19 UTC. QA did not run the rebase. The branch moved from `3c24d1b21`-based history to `origin/development` `5381f3f52`. +- `git range-diff 37fd1ddb2^..6e38d5984 origin/development..HEAD`: all 10 commit pairs show `=` (identical patches). +- Base changes since `3c24d1b21` (`git diff --stat 3c24d1b21 origin/development`): frontend dependencies (`package-lock.json`, `package.json`, tailwindcss 4.3.4, react-query 5.105.3), CI workflows and `scripts/`. No Go or model changes. +- Therefore the Go race, coverage and container results are for the same PR content. The frontend coverage and type-check were re-run on `01981fbba`, and the lint and lefthook runs were on `01981fbba`. -**READY**. +## Action list (in order) -- **Fixed in this PR:** QA-3 (contract fixture), QA-1, QA-4. -- **Left to CI:** the coverage, patch coverage, build, type-check and hadolint gates re-run on the PR (last measured at `e3310bfcc`: backend 92.5% lines, frontend 91.85% lines, patch 98.7%). -- **Verified fixed:** QA-1 (startup mirror retry on a hung CDN, including the derived-file cleanup), QA-4 (retry wording for live-first and mirror-first). -- **Amended:** QA-2 (PASS-with-note, tracked as #1796). -- **Open (documentation):** QA-5 (design notes versus the 60 s restore budget). -- **Verified and working:** CDN refusal and hang for the presets list and pulls, startup under refused, hung and both-down hub hosts (all bounded), the mirror-first install under a hung CDN, the three shell interpreters, the race-enabled crowdsec and handler suites, and the security and firefox Playwright suites. +1. B1: add the `//nolint:gosec` reasons to the six test lines (or restructure the fixtures), commit as `test:`, re-run `make lint-backend` (must print `0 issues`). +2. Optional before merge: L1 table cases for the three uncovered branches. +3. File R3 and O1 as low GitHub issues (non-security, neutral wording). N3 goes to a private advisory only if the orchestrator judges it reportable. +4. Decide C1 (subject wording) before the PR is opened; only the orchestrator commits. +5. Run `go test -race -count=3` on the four package groups, or leave it to CI, and say which in the PR. From f87f7a533a933e2a2ad236f5f4f6ed9f09bb1b3e Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Mon, 2 Nov 2026 19:32:34 -0500 Subject: [PATCH 027/291] docs: remove Docker socket mounts for improved security and add Orthrus agent configuration --- .docker/compose/docker-compose.dev.yml | 1 - .docker/compose/docker-compose.local.yml | 17 ++++++++++++++++- .docker/compose/docker-compose.yml | 16 ++++++++++++++++ ARCHITECTURE.md | 1 - README.md | 1 - SECURITY.md | 19 +++++++++++++++++-- 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/.docker/compose/docker-compose.dev.yml b/.docker/compose/docker-compose.dev.yml index 7489cd6e2..496746496 100644 --- a/.docker/compose/docker-compose.dev.yml +++ b/.docker/compose/docker-compose.dev.yml @@ -37,7 +37,6 @@ services: # to docker-compose.override.yml and set your host's docker GID. volumes: # Dev flow uses direct local discovery. For real deployments Orthrus (docs/features/orthrus.md) is recommended. - - /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery - crowdsec_data:/app/data/crowdsec # Mount your existing Caddyfile for automatic import (optional) # - ./my-existing-Caddyfile:/import/Caddyfile:ro diff --git a/.docker/compose/docker-compose.local.yml b/.docker/compose/docker-compose.local.yml index d85dd4d85..60dd8a05c 100644 --- a/.docker/compose/docker-compose.local.yml +++ b/.docker/compose/docker-compose.local.yml @@ -46,7 +46,6 @@ services: - crowdsec_data:/app/data/crowdsec - plugins_data:/app/plugins # Read-write for development/hot-loading # Optional: direct local discovery. Orthrus (docs/features/orthrus.md) is the recommended, safer option. - - /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery - ./backend:/app/backend:ro # Mount source for debugging # Mount your existing Caddyfile for automatic import (optional) # - :/import/Caddyfile:ro @@ -58,6 +57,22 @@ services: retries: 3 start_period: 40s + orthrus-agent: + image: wikid82/orthrus:latest + container_name: orthrus + restart: always + environment: + # The websocket URL of your Charon instance, e.g. wss://charon.example.com/api/v1/ws/orthrus/connect + - ORTHRUS_SERVER_URL= + # The agent UUID shown when you registered this agent in Charon (Remote Agents -> Add Agent) + - ORTHRUS_AGENT_ID= + # The one-time auth key shown when you registered this agent + - ORTHRUS_AUTH_KEY= + volumes: + # Give the agent read-only access to the host Docker socket. The agent is read-only by default; + # write access is an explicit per-agent opt-in in Charon (see docs/features/orthrus.md). + - /var/run/docker.sock:/var/run/docker.sock:ro + volumes: charon_data: driver: local diff --git a/.docker/compose/docker-compose.yml b/.docker/compose/docker-compose.yml index 618ad9671..d2e236bb7 100644 --- a/.docker/compose/docker-compose.yml +++ b/.docker/compose/docker-compose.yml @@ -63,6 +63,22 @@ services: retries: 3 start_period: 40s + orthrus-agent: + image: wikid82/orthrus:latest + container_name: orthrus + restart: always + environment: + # The websocket URL of your Charon instance, e.g. wss://charon.example.com/api/v1/ws/orthrus/connect + - ORTHRUS_SERVER_URL= + # The agent UUID shown when you registered this agent in Charon (Remote Agents -> Add Agent) + - ORTHRUS_AGENT_ID= + # The one-time auth key shown when you registered this agent + - ORTHRUS_AUTH_KEY= + volumes: + # Give the agent read-only access to the host Docker socket. The agent is read-only by default; + # write access is an explicit per-agent opt-in in Charon (see docs/features/orthrus.md). + - /var/run/docker.sock:/var/run/docker.sock:ro + volumes: cpm_data: driver: local diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 51ed9d4dc..9e674d7a5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1458,7 +1458,6 @@ services: volumes: - ./data:/app/data # Optional: direct local discovery. Orthrus is the recommended, more secure alternative (no socket mount in Charon). - - /var/run/docker.sock:/var/run/docker.sock:ro environment: - CHARON_ENV=production - CHARON_ENCRYPTION_KEY=${CHARON_ENCRYPTION_KEY} diff --git a/README.md b/README.md index 882210711..e8bcee772 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,6 @@ services: - "8080:8080" volumes: - ./charon-data:/app/data - - /var/run/docker.sock:/var/run/docker.sock:ro environment: - TZ=America/New_York # Generate with: openssl rand -base64 32 diff --git a/SECURITY.md b/SECURITY.md index 9dcb5d356..931773c73 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1374,7 +1374,7 @@ fails the build on any blocking finding. ```yaml services: charon: - image: ghcr.io/wikid82/charon:latest + image: wikid82/charon:latest restart: unless-stopped environment: - CHARON_ENV=production @@ -1382,7 +1382,6 @@ services: volumes: - ./charon-data:/app/data:rw # Optional. Orthrus is the recommended, more secure way to connect Docker (no socket mount in Charon). - - /var/run/docker.sock:/var/run/docker.sock:ro networks: - charon-internal cap_drop: @@ -1394,6 +1393,22 @@ services: read_only: true tmpfs: - /tmp:noexec,nosuid,nodev + + orthrus-agent: + image: wikid82/orthrus:latest + container_name: orthrus + restart: always + environment: + # The websocket URL of your Charon instance, e.g. wss://charon.example.com/api/v1/ws/orthrus/connect + - ORTHRUS_SERVER_URL= + # The agent UUID shown when you registered this agent in Charon (Remote Agents -> Add Agent) + - ORTHRUS_AGENT_ID= + # The one-time auth key shown when you registered this agent + - ORTHRUS_AUTH_KEY= + volumes: + # Give the agent read-only access to the host Docker socket. The agent is read-only by default; + # write access is an explicit per-agent opt-in in Charon (see docs/features/orthrus.md). + - /var/run/docker.sock:/var/run/docker.sock:ro ``` ### Gotify Token Hygiene From bba8d6d6f42d199fbb9cc3cfad68d1ef7a50e2c3 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Tue, 3 Nov 2026 01:03:26 +0000 Subject: [PATCH 028/291] fix: read the proxy configuration from inside the container in the test tooling --- .docker/compose/docker-compose.dev.yml | 2 +- .../compose/docker-compose.playwright-ci.yml | 3 +- .../docker-compose.playwright-local.yml | 3 +- .github/workflows/quality-checks.yml | 4 + .../caddy/test_tooling_admin_access_test.go | 89 +++++++++++++++++++ scripts/cerberus_integration.sh | 7 +- scripts/ci/run-playwright-container.sh | 2 +- scripts/coraza_integration.sh | 9 +- scripts/crowdsec_decision_integration.sh | 3 +- scripts/debug_rate_limit.sh | 6 +- scripts/integration-test.sh | 4 +- scripts/rate_limit_integration.sh | 12 +-- scripts/security_headers_integration.sh | 10 +-- scripts/waf_integration.sh | 11 ++- tests/global-setup.ts | 37 -------- tests/security/self-proxy-requests.spec.ts | 38 +++++--- 16 files changed, 150 insertions(+), 90 deletions(-) create mode 100644 backend/internal/caddy/test_tooling_admin_access_test.go diff --git a/.docker/compose/docker-compose.dev.yml b/.docker/compose/docker-compose.dev.yml index 496746496..a5cdd61dd 100644 --- a/.docker/compose/docker-compose.dev.yml +++ b/.docker/compose/docker-compose.dev.yml @@ -13,7 +13,7 @@ services: - "443:443" - "443:443/udp" - "8080:8080" - - "2019:2019" # Caddy admin API (dev only) + # Caddy admin API stays inside the container: docker compose exec app wget -qO- http://127.0.0.1:2019/config/ environment: - CHARON_ENV=development - CPM_ENV=development diff --git a/.docker/compose/docker-compose.playwright-ci.yml b/.docker/compose/docker-compose.playwright-ci.yml index 2463bfaf3..b41d2d17a 100644 --- a/.docker/compose/docker-compose.playwright-ci.yml +++ b/.docker/compose/docker-compose.playwright-ci.yml @@ -37,8 +37,7 @@ services: # No .env file is used in CI as it's gitignored and not available. ports: - "8080:8080" # Management UI (Charon) - - "127.0.0.1:2019:2019" # Caddy admin API (IPv4 loopback) - - "[::1]:2019:2019" # Caddy admin API (IPv6 loopback) + # Caddy admin API is not published: read it with docker exec charon-playwright wget -qO- http://127.0.0.1:2019/config/ - "2020:2020" # Emergency tier-2 API (all interfaces for E2E tests) - "80:80" # Caddy proxy (all interfaces for E2E tests) - "443:443" # Caddy proxy HTTPS (all interfaces for E2E tests) diff --git a/.docker/compose/docker-compose.playwright-local.yml b/.docker/compose/docker-compose.playwright-local.yml index 12cce781c..a50b81f00 100644 --- a/.docker/compose/docker-compose.playwright-local.yml +++ b/.docker/compose/docker-compose.playwright-local.yml @@ -27,8 +27,7 @@ services: # that runs `npx playwright test` (see tests/utils/api-helpers.ts). - "8180:80" - "8080:8080" # Management UI (Charon) - E2E tests verify UI/UX here - - "127.0.0.1:2019:2019" # Caddy admin API (read-only status; keep loopback only) - - "[::1]:2019:2019" # Caddy admin API (IPv6 loopback) + # Caddy admin API is not published: read it with docker exec charon-e2e wget -qO- http://127.0.0.1:2019/config/ - "2020:2020" # Emergency tier-2 API (all interfaces for E2E tests) environment: - CHARON_ENV=e2e # E2E ACME/TLS policies without an ACME email; any non-production value also enables CHARON_CHANGELOG_VERSION diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index bfa217255..a7b91fa82 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -125,6 +125,10 @@ jobs: - name: bats (Playwright container helpers) run: bats scripts/tests/check-playwright-image-version.bats scripts/tests/run-playwright-container.bats + - name: shellcheck (integration test tooling, severity=error) + run: | + shellcheck --severity=error scripts/waf_integration.sh scripts/cerberus_integration.sh scripts/crowdsec_decision_integration.sh scripts/rate_limit_integration.sh scripts/security_headers_integration.sh scripts/coraza_integration.sh scripts/debug_rate_limit.sh scripts/integration-test.sh + container-prune-tests: name: Container registry prune scripts (bats) runs-on: ubuntu-latest diff --git a/backend/internal/caddy/test_tooling_admin_access_test.go b/backend/internal/caddy/test_tooling_admin_access_test.go new file mode 100644 index 000000000..982289819 --- /dev/null +++ b/backend/internal/caddy/test_tooling_admin_access_test.go @@ -0,0 +1,89 @@ +package caddy + +import ( + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" +) + +// repoRootFromCaddyPkg is the repository root relative to this package directory. +const repoRootFromCaddyPkg = "../../.." + +// adminAccessExemptScripts lists shell scripts that may publish the admin port. +// caddy-compat-matrix.sh starts its own stock Caddy containers, one per matrix +// cell, and publishes each admin API on a unique host port. It never reads the +// Charon container's admin interface, so the container-local rule does not apply. +var adminAccessExemptScripts = map[string]bool{ + "caddy-compat-matrix.sh": true, +} + +// hostPublishedAdminPortPattern matches a `docker run -p :2019` argument. +var hostPublishedAdminPortPattern = regexp.MustCompile(`-p\s+\S+:2019\b`) + +// composeAdminPortEntryPattern matches a compose ports list item whose mapping +// ends in :2019. Environment items are excluded by the `=` guard, so +// CHARON_CADDY_ADMIN_API=http://localhost:2019 is not treated as a port. +var composeAdminPortEntryPattern = regexp.MustCompile(`^\s*-\s*"?[^"=]*:2019"?\s*(#.*)?$`) + +// TestTestEnvironmentsUseContainerLocalAdminAccess verifies that no test +// environment publishes the proxy admin port to the host. Test tooling reads +// the admin interface from inside the Charon container with docker exec. +func TestTestEnvironmentsUseContainerLocalAdminAccess(t *testing.T) { + root, err := filepath.Abs(repoRootFromCaddyPkg) + if err != nil { + t.Fatalf("resolve repository root: %v", err) + } + if _, statErr := os.Stat(filepath.Join(root, "scripts")); statErr != nil { + t.Fatalf("repository root %q does not contain scripts/: %v", root, statErr) + } + + var violations []string + + scripts, err := filepath.Glob(filepath.Join(root, "scripts", "*.sh")) + if err != nil { + t.Fatalf("glob scripts: %v", err) + } + for _, path := range scripts { + if adminAccessExemptScripts[filepath.Base(path)] { + continue + } + violations = append(violations, scanFileLines(t, path, root, hostPublishedAdminPortPattern)...) + } + + composes, err := filepath.Glob(filepath.Join(root, ".docker", "compose", "*.yml")) + if err != nil { + t.Fatalf("glob compose files: %v", err) + } + for _, path := range composes { + violations = append(violations, scanFileLines(t, path, root, composeAdminPortEntryPattern)...) + } + + if len(violations) > 0 { + t.Errorf("test environments publish the proxy admin port; read it with "+ + "`docker exec wget -qO- http://127.0.0.1:2019/config/` instead:\n %s", + strings.Join(violations, "\n ")) + } +} + +// scanFileLines returns "relative/path:line: text" for every line that matches pattern. +func scanFileLines(t *testing.T, path, root string, pattern *regexp.Regexp) []string { + t.Helper() + data, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + rel, err := filepath.Rel(root, path) + if err != nil { + rel = path + } + var hits []string + for i, line := range strings.Split(string(data), "\n") { + if pattern.MatchString(line) { + hits = append(hits, rel+":"+strconv.Itoa(i+1)+": "+strings.TrimSpace(line)) + } + } + return hits +} diff --git a/scripts/cerberus_integration.sh b/scripts/cerberus_integration.sh index d9bbd89c2..ae05eb153 100755 --- a/scripts/cerberus_integration.sh +++ b/scripts/cerberus_integration.sh @@ -29,7 +29,6 @@ TEST_DOMAIN="cerberus.test.local" API_PORT=8480 HTTP_PORT=8481 HTTPS_PORT=8444 -CADDY_ADMIN_PORT=2319 # Rate limit config for testing RATE_LIMIT_REQUESTS=5 @@ -97,7 +96,7 @@ on_failure() { echo "" echo "=== Caddy Admin API Config ===" - curl -sL "http://localhost:${CADDY_ADMIN_PORT}/config/" 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" + docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" echo "" echo "=== Security Config in API ===" @@ -185,7 +184,7 @@ log_info "Starting Charon container with ALL Cerberus features enabled..." docker run -d --name ${CONTAINER_NAME} \ --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ --network containers_default \ - -p ${HTTP_PORT}:80 -p ${HTTPS_PORT}:443 -p ${API_PORT}:8080 -p ${CADDY_ADMIN_PORT}:2019 \ + -p ${HTTP_PORT}:80 -p ${HTTPS_PORT}:443 -p ${API_PORT}:8080 \ -e CHARON_ENV=development \ -e CHARON_DEBUG=1 \ -e CHARON_HTTP_PORT=8080 \ @@ -395,7 +394,7 @@ fi # Fetch Caddy config with timeout and retry CADDY_CONFIG="" for attempt in 1 2 3; do - CADDY_CONFIG=$(curl --max-time 10 -sL "http://localhost:${CADDY_ADMIN_PORT}/config/" 2>/dev/null || echo "") + CADDY_CONFIG=$(docker exec "${CONTAINER_NAME}" wget -T 10 -qO- http://127.0.0.1:2019/config/ 2>/dev/null || echo "") if [ -n "$CADDY_CONFIG" ]; then break fi diff --git a/scripts/ci/run-playwright-container.sh b/scripts/ci/run-playwright-container.sh index fe414a678..b4c729ef9 100755 --- a/scripts/ci/run-playwright-container.sh +++ b/scripts/ci/run-playwright-container.sh @@ -62,7 +62,7 @@ while IFS= read -r -d '' entry; do name="${entry%%=*}" case "$name" in PLAYWRIGHT_BROWSERS_PATH) ;; - PLAYWRIGHT_* | CHARON_* | E2E_* | CADDY_ADMIN_HOST | CI | CI_LOG_LEVEL | DEBUG | TEST_WORKER_INDEX) + PLAYWRIGHT_* | CHARON_* | E2E_* | CI | CI_LOG_LEVEL | DEBUG | TEST_WORKER_INDEX) forward_args+=(--env "$name") ;; esac diff --git a/scripts/coraza_integration.sh b/scripts/coraza_integration.sh index 6168b1127..08c6e3739 100755 --- a/scripts/coraza_integration.sh +++ b/scripts/coraza_integration.sh @@ -28,7 +28,6 @@ cd "$PROJECT_ROOT" CORAZA_HTTP_PORT="${CORAZA_HTTP_PORT:-80}" CORAZA_HTTPS_PORT="${CORAZA_HTTPS_PORT:-443}" CORAZA_API_PORT="${CORAZA_API_PORT:-8080}" -CORAZA_ADMIN_PORT="${CORAZA_ADMIN_PORT:-2019}" CORAZA_DEBUG_PORT="${CORAZA_DEBUG_PORT:-2345}" # ============================================================================ @@ -46,7 +45,7 @@ verify_waf_config() { for i in $(seq 1 $retries); do # Fetch Caddy config via admin API local caddy_config - caddy_config=$(curl -s "http://localhost:${CORAZA_ADMIN_PORT}/config/" 2>/dev/null || echo "") + caddy_config=$(docker exec charon-debug wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null || echo "") if [ -z "$caddy_config" ]; then echo " Attempt $i/$retries: Caddy admin API not responding, retrying..." @@ -90,7 +89,7 @@ on_failure() { echo "" echo "=== Caddy Admin API Config ===" - curl -s "http://localhost:${CORAZA_ADMIN_PORT}/config/" 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" + docker exec charon-debug wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" echo "" echo "=== Ruleset Files in Container ===" @@ -140,7 +139,7 @@ fi # In CI, frontend/dist does not exist (it's built inside the Docker image). # Mounting a non-existent directory would override the built frontend with an empty dir. # For local development with hot-reload, use .docker/compose/docker-compose.local.yml instead. -docker run -d --name charon-debug --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --network containers_default -p "${CORAZA_HTTP_PORT}":80 -p "${CORAZA_HTTPS_PORT}":443 -p "${CORAZA_API_PORT}":8080 -p "${CORAZA_ADMIN_PORT}":2019 -p "${CORAZA_DEBUG_PORT}":2345 \ +docker run -d --name charon-debug --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --network containers_default -p "${CORAZA_HTTP_PORT}":80 -p "${CORAZA_HTTPS_PORT}":443 -p "${CORAZA_API_PORT}":8080 -p "${CORAZA_DEBUG_PORT}":2345 \ -e CHARON_ENV=development -e CHARON_DEBUG=1 -e CHARON_HTTP_PORT=8080 -e CHARON_DB_PATH=/app/data/charon.db -e CHARON_FRONTEND_DIR=/app/frontend/dist \ -e CHARON_CADDY_ADMIN_API=http://localhost:2019 -e CHARON_CADDY_CONFIG_DIR=/app/data/caddy -e CHARON_CADDY_BINARY=caddy -e CHARON_IMPORT_CADDYFILE=/import/Caddyfile \ -e CHARON_IMPORT_DIR=/app/data/imports -e CHARON_ACME_STAGING=false -e CHARON_SECURITY_WAF_MODE=block \ @@ -247,7 +246,7 @@ echo "Apply rules and test payload..." # create minimal proxy host if needed; omitted here for brevity; test will target local Caddy root echo "Verifying Caddy config has WAF handler..." -curl -s "http://localhost:${CORAZA_ADMIN_PORT}/config/" | grep -E '"handler":"waf"' || echo "WARNING: WAF handler not found in initial config check" +docker exec charon-debug wget -qO- http://127.0.0.1:2019/config/ | grep -E '"handler":"waf"' || echo "WARNING: WAF handler not found in initial config check" echo "Inspecting ruleset file inside container..." docker exec charon-debug sh -c 'cat /app/data/caddy/coraza/rulesets/integration-xss-*.conf' || echo "WARNING: Could not read ruleset file" diff --git a/scripts/crowdsec_decision_integration.sh b/scripts/crowdsec_decision_integration.sh index 70f13221b..c58defd6a 100755 --- a/scripts/crowdsec_decision_integration.sh +++ b/scripts/crowdsec_decision_integration.sh @@ -43,7 +43,6 @@ TEST_REASON="Integration test ban" API_PORT=8280 HTTP_PORT=8180 HTTPS_PORT=8143 -CADDY_ADMIN_PORT=2119 # ============================================================================ # Colors for output @@ -165,7 +164,7 @@ log_info "Starting Charon container with CrowdSec features enabled..." docker run -d --name ${CONTAINER_NAME} \ --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ --network containers_default \ - -p ${HTTP_PORT}:80 -p ${HTTPS_PORT}:443 -p ${API_PORT}:8080 -p ${CADDY_ADMIN_PORT}:2019 \ + -p ${HTTP_PORT}:80 -p ${HTTPS_PORT}:443 -p ${API_PORT}:8080 \ -e CHARON_ENV=development \ -e CHARON_DEBUG=1 \ -e CHARON_HTTP_PORT=8080 \ diff --git a/scripts/debug_rate_limit.sh b/scripts/debug_rate_limit.sh index f5eb49e0d..27a92bd2b 100755 --- a/scripts/debug_rate_limit.sh +++ b/scripts/debug_rate_limit.sh @@ -7,7 +7,7 @@ echo "=== Starting debug container ===" docker rm -f charon-debug 2>/dev/null || true docker run -d --name charon-debug \ --network containers_default \ - -p 8180:80 -p 8280:8080 -p 2119:2019 \ + -p 8180:80 -p 8280:8080 \ -e CHARON_ENV=development \ charon:local @@ -58,11 +58,11 @@ curl -s -b /tmp/debug-cookie http://localhost:8280/api/v1/security/config | jq . echo "" echo "=== Caddy config (checking for rate_limit handler) ===" -curl -s http://localhost:2119/config/ | jq '.apps.http.servers.charon_server.routes[0].handle // []' | grep -i rate_limit || echo "No rate_limit handler found" +docker exec charon-debug wget -qO- http://127.0.0.1:2019/config/ | jq '.apps.http.servers.charon_server.routes[0].handle // []' | grep -i rate_limit || echo "No rate_limit handler found" echo "" echo "=== Full Caddy route handlers ===" -curl -s http://localhost:2119/config/ | jq '.apps.http.servers.charon_server.routes[0].handle // []' +docker exec charon-debug wget -qO- http://127.0.0.1:2019/config/ | jq '.apps.http.servers.charon_server.routes[0].handle // []' echo "" echo "=== Container logs (last 50 lines) ===" diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh index ff93b7436..dbe4eedf6 100755 --- a/scripts/integration-test.sh +++ b/scripts/integration-test.sh @@ -23,6 +23,8 @@ fi # Configuration API_URL="http://localhost:8080/api/v1" +# Container whose bundled Caddy admin API (loopback inside the container) is polled. +CHARON_CONTAINER_NAME="${CHARON_CONTAINER_NAME:-charon-debug}" ADMIN_EMAIL="admin@example.com" ADMIN_PASSWORD="changeme" @@ -183,7 +185,7 @@ for attempt in 1 2 3 4 5; do echo "Warning: failed to apply config on attempt $attempt, retrying..." # Wait for Caddy admin API on host to respond to /config to reduce collisions for i in $(seq 1 10); do - if curl -s -o /dev/null -w "%{http_code}" http://localhost:${CADDY_ADMIN_PORT:-20194}/config/ >/dev/null 2>&1; then + if docker exec "${CHARON_CONTAINER_NAME}" wget -qO /dev/null http://127.0.0.1:2019/config/ >/dev/null 2>&1; then break fi sleep 1 diff --git a/scripts/rate_limit_integration.sh b/scripts/rate_limit_integration.sh index d607563b5..c90f289e8 100755 --- a/scripts/rate_limit_integration.sh +++ b/scripts/rate_limit_integration.sh @@ -42,7 +42,7 @@ verify_rate_limit_config() { for i in $(seq 1 $retries); do # Fetch Caddy config via admin API local caddy_config - caddy_config=$(curl -s http://localhost:2119/config/ 2>/dev/null || echo "") + caddy_config=$(docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null || echo "") if [ -z "$caddy_config" ]; then echo " Attempt $i/$retries: Caddy admin API not responding, retrying..." @@ -79,7 +79,7 @@ on_failure() { echo "" echo "=== Caddy Admin API Config ===" - curl -s http://localhost:2119/config/ 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" + docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" echo "" echo "=== Security Config in API ===" @@ -150,7 +150,7 @@ echo "Starting Charon container..." docker run -d --name ${CONTAINER_NAME} \ --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ --network containers_default \ - -p 8180:80 -p 8143:443 -p 8280:8080 -p 2119:2019 \ + -p 8180:80 -p 8143:443 -p 8280:8080 \ -e CHARON_ENV=development \ -e CHARON_DEBUG=1 \ -e CHARON_HTTP_PORT=8080 \ @@ -274,7 +274,7 @@ EOF echo "Waiting for Caddy admin API to be ready..." for i in {1..20}; do - if curl -s -f http://localhost:2119/config/ >/dev/null 2>&1; then + if docker exec "${CONTAINER_NAME}" wget -qO /dev/null http://127.0.0.1:2019/config/ >/dev/null 2>&1; then echo "✓ Caddy admin API is ready" break fi @@ -310,7 +310,7 @@ if ! verify_rate_limit_config; then echo " The handler must be present in Caddy config before enforcement can be tested." echo "" echo "=== Caddy admin API full config ===" - curl -s http://localhost:2119/config/ 2>/dev/null | head -200 || echo "Admin API not responding" + docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null | head -200 || echo "Admin API not responding" echo "" echo "=== Security config from API ===" curl -s -b "${TMP_COOKIE}" http://localhost:8280/api/v1/security/config 2>/dev/null || echo "API not responding" @@ -385,7 +385,7 @@ else curl -s -b "${TMP_COOKIE}" http://localhost:8280/api/v1/security/status | jq . echo "" echo "=== DEBUG: Caddy config (first proxy route handlers) ===" - curl -s http://localhost:2119/config/ | jq '.apps.http.servers.charon_server.routes[0].handle // []' + docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ | jq '.apps.http.servers.charon_server.routes[0].handle // []' echo "" echo "=== DEBUG: Container logs (last 100 lines) ===" docker logs ${CONTAINER_NAME} 2>&1 | tail -100 diff --git a/scripts/security_headers_integration.sh b/scripts/security_headers_integration.sh index 5c1534b2b..4fc63d6e0 100755 --- a/scripts/security_headers_integration.sh +++ b/scripts/security_headers_integration.sh @@ -30,7 +30,7 @@ set -euo pipefail # Overridable environment (all optional): # CHARON_IMAGE image to test (default charon:local) # SH_PREFIX name prefix for containers/network/volumes (default secheaders) -# SH_HTTP_PORT / SH_HTTPS_PORT / SH_API_PORT / SH_ADMIN_PORT host ports +# SH_HTTP_PORT / SH_HTTPS_PORT / SH_API_PORT host ports # Ensure we operate from repo root PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -44,7 +44,6 @@ SH_PREFIX="${SH_PREFIX:-secheaders}" HTTP_PORT="${SH_HTTP_PORT:-8182}" HTTPS_PORT="${SH_HTTPS_PORT:-8144}" API_PORT="${SH_API_PORT:-8282}" -ADMIN_PORT="${SH_ADMIN_PORT:-2121}" CONTAINER_NAME="${SH_PREFIX}-charon" UPSTREAM_CONTAINER="${SH_PREFIX}-upstream" @@ -55,7 +54,6 @@ UPSTREAM_IMAGE="busybox:1.37" PROFILE_DOMAIN="secheaders-profile.local" PLAIN_DOMAIN="secheaders-plain.local" API_URL="http://localhost:${API_PORT}/api/v1" -ADMIN_URL="http://localhost:${ADMIN_PORT}" PROXY_URL="http://localhost:${HTTP_PORT}" PROFILE_CORP="same-origin" @@ -112,7 +110,7 @@ on_failure() { echo "=== Charon logs (last 100 lines) ===" docker logs "${CONTAINER_NAME}" 2>&1 | tail -100 || true echo "=== Caddy admin config (first 200 lines) ===" - curl -s "${ADMIN_URL}/config/" 2>/dev/null | head -200 || true + docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null | head -200 || true echo "=== Proxy hosts ===" curl -s -b "${TMP_COOKIE}" "${API_URL}/proxy-hosts" 2>/dev/null | head -50 || true echo "==============================================" @@ -209,7 +207,7 @@ echo "Starting Charon container..." docker run -d --name "${CONTAINER_NAME}" \ --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ --network "${NETWORK_NAME}" \ - -p "${HTTP_PORT}:80" -p "${HTTPS_PORT}:443" -p "${API_PORT}:8080" -p "${ADMIN_PORT}:2019" \ + -p "${HTTP_PORT}:80" -p "${HTTPS_PORT}:443" -p "${API_PORT}:8080" \ -e CHARON_ENV=development \ -e CHARON_DEBUG=1 \ -e CHARON_HTTP_PORT=8080 \ @@ -396,7 +394,7 @@ echo "" echo "==============================================" echo "=== Caddy config: headers handlers are non-deferred + deferred pairs ===" echo "==============================================" -CADDY_CONFIG=$(curl -s "${ADMIN_URL}/config/") +CADDY_CONFIG=$(docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/) # The non-deferred set covers Caddy-generated responses (e.g. 502), the deferred # set runs after reverse_proxy copied the upstream headers so it replaces them. # Count response headers handlers by kind, and count handlers with no partner: diff --git a/scripts/waf_integration.sh b/scripts/waf_integration.sh index b7975a3b8..b97e6232f 100755 --- a/scripts/waf_integration.sh +++ b/scripts/waf_integration.sh @@ -30,7 +30,6 @@ TEST_DOMAIN="waf.test.local" API_PORT=8380 HTTP_PORT=8180 HTTPS_PORT=8143 -CADDY_ADMIN_PORT=2119 # ============================================================================ # Colors for output @@ -94,7 +93,7 @@ on_failure() { echo "" echo "=== Caddy Admin API Config ===" - curl -sL "http://localhost:${CADDY_ADMIN_PORT}/config/" 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" + docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" echo "" echo "=== Security Config in API ===" @@ -128,7 +127,7 @@ verify_waf_config() { for i in $(seq 1 $retries); do local caddy_config - caddy_config=$(curl -sL "http://localhost:${CADDY_ADMIN_PORT}/config/" 2>/dev/null || echo "") + caddy_config=$(docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null || echo "") if [ -z "$caddy_config" ]; then echo " Attempt $i/$retries: Caddy admin API not responding, retrying..." @@ -201,7 +200,7 @@ log_info "Starting Charon container with Cerberus enabled..." docker run -d --name ${CONTAINER_NAME} \ --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ --network containers_default \ - -p ${HTTP_PORT}:80 -p ${HTTPS_PORT}:443 -p ${API_PORT}:8080 -p ${CADDY_ADMIN_PORT}:2019 \ + -p ${HTTP_PORT}:80 -p ${HTTPS_PORT}:443 -p ${API_PORT}:8080 \ -e CHARON_ENV=development \ -e CHARON_DEBUG=1 \ -e CHARON_HTTP_PORT=8080 \ @@ -567,8 +566,8 @@ assert_http "200" "$RESP" "Combined - Legitimate request passes" # ============================================================================ log_test "TC-9: Verify Caddy Config has WAF Handler" -# Note: Caddy admin API requires trailing slash, and -L follows redirects -CADDY_CONFIG=$(curl -sL "http://localhost:${CADDY_ADMIN_PORT}/config/" 2>/dev/null || echo "") +# Note: the Caddy admin API requires a trailing slash +CADDY_CONFIG=$(docker exec "${CONTAINER_NAME}" wget -qO- http://127.0.0.1:2019/config/ 2>/dev/null || echo "") if echo "$CADDY_CONFIG" | grep -q '"handler":"waf"'; then log_info " ✓ WAF handler found in Caddy config" diff --git a/tests/global-setup.ts b/tests/global-setup.ts index 2df4fc032..35fc8e783 100644 --- a/tests/global-setup.ts +++ b/tests/global-setup.ts @@ -17,35 +17,6 @@ function getBaseURL(): string { return process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:8080'; } -/** - * Check if Caddy admin API is enabled and healthy (port 2019 - read-only config inspection) - */ -async function checkCaddyAdminHealth(): Promise { - const caddyAdminHost = process.env.CADDY_ADMIN_HOST || 'http://localhost:2019'; - const startTime = Date.now(); - console.log(`🔍 Checking Caddy admin API health at ${caddyAdminHost}...`); - - const caddyContext = await request.newContext({ baseURL: caddyAdminHost }); - try { - const response = await caddyContext.get('/config', { timeout: 3000 }); - const elapsed = Date.now() - startTime; - - if (response.ok()) { - console.log(` ✅ Caddy admin API (port 2019) is healthy [${elapsed}ms]`); - return true; - } else { - console.log(` ⚠️ Caddy admin API returned: ${response.status()} [${elapsed}ms]`); - return false; - } - } catch (e) { - const elapsed = Date.now() - startTime; - console.log(` ⏭️ Caddy admin API unavailable (non-blocking) [${elapsed}ms]`); - return false; - } finally { - await caddyContext.dispose(); - } -} - /** * Wait for container to be ready before running global setup. * This prevents 401 errors when global-setup runs before containers finish starting. @@ -100,14 +71,6 @@ async function globalSetup(): Promise { console.log(' ⚠️ Could not parse base URL\n'); } - // Health-check Caddy admin and emergency tier-2 servers (non-blocking) - console.log('📊 Port Connectivity Checks:'); - const caddyHealthy = await checkCaddyAdminHealth(); - - console.log( - `\n✅ Connectivity Summary: Caddy=${caddyHealthy ? '✓' : '✗'}\n` - ); - // Create a request context const requestContext = await request.newContext({ baseURL, diff --git a/tests/security/self-proxy-requests.spec.ts b/tests/security/self-proxy-requests.spec.ts index 566295d38..17c262f99 100644 --- a/tests/security/self-proxy-requests.spec.ts +++ b/tests/security/self-proxy-requests.spec.ts @@ -11,8 +11,8 @@ * - the per-process value the proxy attaches to the hop never shows up in API * responses, the proxy access log, the container log or the app log file. * - * The live value is read from the proxy's admin API (published on loopback in - * the E2E compose files) so the log checks compare against the real value. It + * The live value is read from the proxy's admin API inside the E2E container + * (docker exec) so the log checks compare against the real value. It * is only ever used inside `includes()` checks: never logged, never passed to * an assertion message, never attached to the report. * @@ -31,7 +31,6 @@ import { caddyProxyOrigin, getAuthTokenFromPage } from '../utils/api-helpers'; import { waitForLoadingComplete } from '../utils/wait-helpers'; const CHARON_PORT = 8080; -const ADMIN_API = process.env.PLAYWRIGHT_CADDY_ADMIN_URL || 'http://127.0.0.1:2019'; const CONTAINER_CANDIDATES = [ process.env.PLAYWRIGHT_CONTAINER_NAME, 'charon-e2e', @@ -78,13 +77,24 @@ function containerFile(container: string, path: string): string { } } -/** Extract the live hop value from the proxy's loaded configuration. */ -async function readLiveHopValue( - request: import('@playwright/test').APIRequestContext -): Promise { - const response = await request.get(`${ADMIN_API}/config/`); - expect(response.ok()).toBeTruthy(); - const config = await response.text(); +/** + * Extract the live hop value from the proxy's loaded configuration. + * + * The admin interface listens on loopback inside the container, so the read + * runs there with docker exec. Returns '' when the read fails; the caller + * asserts on the result, so a failed read cannot silently weaken the checks. + */ +function readLiveHopValue(container: string): string { + let config: string; + try { + config = execFileSync( + 'docker', + ['exec', container, 'wget', '-qO-', 'http://127.0.0.1:2019/config/'], + { stdio: 'pipe', encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 } + ) as string; + } catch { + return ''; + } const match = new RegExp(`"${HOP_SECRET_HEADER}":\\["([^"]+)"\\]`).exec(config); return match ? match[1] : ''; } @@ -255,13 +265,13 @@ test.describe('Requests through a proxy host that targets Charon @security', () }); }); - test('keeps the per-process hop value out of responses and logs', async ({ page, request }) => { + test('keeps the per-process hop value out of responses and logs', async ({ page }) => { const container = resolveContainer(); - // The value is read from the proxy's admin API (loopback). If that read - // ever stops working this assertion fails rather than silently weakening + // The value is read from the proxy's admin API inside the container. If that + // read ever stops working this assertion fails rather than silently weakening // the log checks below. - const live = await readLiveHopValue(request); + const live = readLiveHopValue(container); expect(live, 'live hop value should be readable from the proxy configuration').toMatch( /^[0-9a-f]{32,}$/i ); From fa5db1e8a2586a4b9bae84de063bf6ff9722c530 Mon Sep 17 00:00:00 2001 From: Jeremy Hatfield Date: Tue, 3 Nov 2026 01:13:56 +0000 Subject: [PATCH 029/291] fix: pass the CrowdSec config file to every cscli call at container start --- .docker/cscli-hub-mirror.sh | 27 ++- .docker/docker-entrypoint.sh | 13 +- .github/workflows/quality-checks.yml | 3 +- .../crowdsec/cscli_shell_config_flag_test.go | 57 ++++++ .../testutil/cscliguard/cscliguard_test.go | 61 ++++++ backend/internal/testutil/cscliguard/shell.go | 124 ++++++++++++ .../cscliguard/testdata/shell/bare.sh.txt | 11 ++ .../testdata/shell/comment_and_echo.sh.txt | 5 + .../cscliguard/testdata/shell/marker.sh.txt | 2 + .../cscliguard/testdata/shell/wrapped.sh.txt | 10 + configs/crowdsec/install_hub_items.sh | 17 +- configs/crowdsec/register_bouncer.sh | 8 +- scripts/cscli_hub_mirror_test.sh | 187 +++++++++++++++--- 13 files changed, 483 insertions(+), 42 deletions(-) create mode 100644 backend/internal/crowdsec/cscli_shell_config_flag_test.go create mode 100644 backend/internal/testutil/cscliguard/shell.go create mode 100644 backend/internal/testutil/cscliguard/testdata/shell/bare.sh.txt create mode 100644 backend/internal/testutil/cscliguard/testdata/shell/comment_and_echo.sh.txt create mode 100644 backend/internal/testutil/cscliguard/testdata/shell/marker.sh.txt create mode 100644 backend/internal/testutil/cscliguard/testdata/shell/wrapped.sh.txt diff --git a/.docker/cscli-hub-mirror.sh b/.docker/cscli-hub-mirror.sh index 039177aec..475714857 100644 --- a/.docker/cscli-hub-mirror.sh +++ b/.docker/cscli-hub-mirror.sh @@ -11,6 +11,9 @@ # exit status, signal text, budget expiry) exists only here: the image runs cscli under busybox timeout, which reports # a trapped TERM as the command's own exit 1, and the Go side does not classify exit statuses. # +# Every cscli call names its config file with -c. The live file is CHARON_CSCLI_CONFIG, which the entrypoint sets +# (never an operator setting); without it the file is cscli's default, /etc/crowdsec/config.yaml. +# # Internal state, set by this helper and not meant for operators: # CHARON_CSCLI_MIRROR_CONFIG path of the derived config while it exists # CHARON_CSCLI_MIRROR_FIRST "1" once a mirror retry answered: later cscli_hub calls go mirror-first @@ -27,6 +30,19 @@ if ! command -v run_interruptible >/dev/null 2>&1; then } fi +# _hub_live_config +# Prints the live cscli config file. The entrypoint exports CHARON_CSCLI_CONFIG once it has initialised the config +# directory; without it (a script run by hand in the image) cscli's own default is named, so the flag is always there. +_hub_live_config() { + printf '%s' "${CHARON_CSCLI_CONFIG:-/etc/crowdsec/config.yaml}" +} + +# cscli_cfg +# Runs one cscli command on the live config, with no timeout and no mirror retry (quick reads and registration). +cscli_cfg() { + cscli -c "$(_hub_live_config)" "$@" +} + # hub_mirror_template_valid