84 Commits
v0.0.3 ... main

Author SHA1 Message Date
fab
cf45542c7e Update README.md 2025-12-29 10:16:34 +01:00
Fabrizio Salmi
571d095028 fix: restore full request body for large payloads (closes #76) 2025-12-08 07:30:22 +01:00
Fabrizio Salmi
d3f918c4c4 build: upgrade to Go 1.25 and Caddy v2.10.2 (security fix) 2025-12-06 23:15:40 +01:00
Fabrizio Salmi
5c5f32741c docs: release v0.1.4 preparation (changelog, security, readme) 2025-12-06 23:13:17 +01:00
Fabrizio Salmi
0a96f22563 style: fix imports ordering for gci linter 2025-12-06 23:08:54 +01:00
Fabrizio Salmi
12d70c0eec fix: use Go 1.24 and compatible quic-go v0.48.2 2025-12-06 23:03:23 +01:00
Fabrizio Salmi
83a4df7e65 fix: downgrade Caddy to v2.9.1 to resolve Go 1.25 requirement 2025-12-06 23:00:53 +01:00
Fabrizio Salmi
05152510f5 ci: fix release workflow (go 1.23 + gh cli) 2025-12-06 22:59:32 +01:00
Fabrizio Salmi
5928ff4210 ci: fix go version and bump to v0.1.3 2025-12-06 22:55:55 +01:00
Fabrizio Salmi
78f0066cb8 docs: update documentation for v0.1.2 (ASN, SOTA, Issues fixed) 2025-12-06 22:53:33 +01:00
Fabrizio Salmi
00c547e2a3 refactor: apply SOTA patterns (Atomic HitCount, Zero-Copy Body, Low-Lock RateLimit) 2025-12-06 22:52:01 +01:00
Fabrizio Salmi
c29a7ce9aa chore: bump version to v0.1.0 2025-12-06 22:47:03 +01:00
Fabrizio Salmi
eea39d253b Security: Implement hardening improvements (LimitReader, GeoIP Fail-Open, UI Decoupling, Go Version) 2025-12-06 22:46:11 +01:00
Fabrizio Salmi
5d57051169 Style: Fix Code Formatting (go fmt) 2025-12-06 22:39:53 +01:00
Fabrizio Salmi
47e05e907e Fix: Update CI to use test.caddyfile, block Nikto, and use stable GeoIP URL 2025-12-06 22:37:32 +01:00
Fabrizio Salmi
1c9b6a287d Refactor: Fix absolute path in test.caddyfile and resolve TODO in .golangci.yml 2025-12-06 22:28:18 +01:00
Fabrizio Salmi
b3d3d5692c Fix: Address security alerts and bump version to v0.0.9 2025-12-06 22:26:16 +01:00
fab
a179255b3f Merge pull request #72 from fabriziosalmi/dependabot/go_modules/go_modules-eb6ae95bef
Bump github.com/smallstep/certificates from 0.28.4 to 0.29.0 in the go_modules group across 1 directory
2025-12-06 22:19:13 +01:00
Fabrizio Salmi
1da1fea22b Feat: Implement ASN Blocking (#73) 2025-12-06 22:18:10 +01:00
Fabrizio Salmi
34d7a29119 Fix: restore request body after reading (#76) 2025-12-06 22:14:28 +01:00
fab
66685526e5 Merge pull request #75 from cyqlelabs/main
fix: config initialization
2025-12-06 22:11:33 +01:00
Nicolas Iglesias
971bc53f8a fix: config initialization 2025-12-06 15:48:28 -03:00
dependabot[bot]
937808048b Bump github.com/smallstep/certificates
Bumps the go_modules group with 1 update in the / directory: [github.com/smallstep/certificates](https://github.com/smallstep/certificates).


Updates `github.com/smallstep/certificates` from 0.28.4 to 0.29.0
- [Release notes](https://github.com/smallstep/certificates/releases)
- [Changelog](https://github.com/smallstep/certificates/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smallstep/certificates/compare/v0.28.4...v0.29.0)

---
updated-dependencies:
- dependency-name: github.com/smallstep/certificates
  dependency-version: 0.29.0
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 00:56:01 +00:00
fab
b9fe9ddbb3 Merge pull request #71 from fabriziosalmi/dependabot/go_modules/go_modules-dd7da38a6b
Bump golang.org/x/crypto from 0.43.0 to 0.45.0 in the go_modules group across 1 directory
2025-12-03 22:32:11 +01:00
dependabot[bot]
db95a9b2ed Bump golang.org/x/crypto in the go_modules group across 1 directory
Bumps the go_modules group with 1 update in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.43.0 to 0.45.0
- [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 02:43:20 +00:00
fab
e98fd16392 Merge pull request #70 from drev74/fix/lint
feat(ci): add golangci-lint to flow
2025-10-23 15:13:37 +02:00
drev74
65f8c8a62f refactor(ci): add linter to flow 2025-10-22 23:11:44 +03:00
drev74
c8c0fed9e2 fix: lint errors 2025-10-22 23:04:48 +03:00
fab
06a496e3d3 Update WAF version to v0.0.8 2025-10-20 17:48:43 +02:00
fab
a71b182158 Merge pull request #69 from drev74/fix/caddy-config
fix: solve integration issue with Caddy server
2025-10-20 17:43:59 +02:00
drev74
cf7c995137 fix: add trie instantiation on top
test(it): add blacklisting test with real data
2025-10-19 22:32:58 +03:00
fabriziosalmi
14e4de4b66 chore: update WAF version to v0.0.7 2025-10-16 01:03:55 +02:00
fab
8b702b4281 Update test workflow badge in README.md 2025-10-16 01:03:16 +02:00
fabriziosalmi
1c32e928f1 refactor: enhance IP blacklist handling and add panic recovery in middleware 2025-10-16 00:58:51 +02:00
fab
f45e8331cc Refactor IP extraction and handling in get_blacklisted_ip.py
The Caddy server was panicking on startup due to a data format mismatch.
The `caddy-waf` Go module expects IP addresses in CIDR notation (e.g., `1.2.3.4/32`) for its blacklist, but the `get_blacklisted_ip.py` script was generating a list of plain IPs.

This commit updates the Python script to:
- Append `/32` to all individual IPv4 addresses.
- Append `/128` to all individual IPv6 addresses.
- Preserve existing CIDR ranges from source blocklists.

This ensures the generated `ip_blacklist.txt` is in the correct format required by the Go module, resolving the `netip.ParsePrefix` panic.
2025-10-16 00:42:00 +02:00
fab
fa7f421773 Merge pull request #67 from drev74/refactor/blocking-priorities
refactor: blocking priorities
2025-10-16 00:35:24 +02:00
drev74
1a65ea7049 doc: add priorities to geoblocking 2025-10-14 22:20:02 +03:00
drev74
a77a2d2e36 refactor: update priorities for block/allow actions 2025-10-14 22:13:02 +03:00
fab
63ca645404 Update build-run-validate.yml 2025-10-12 23:59:05 +02:00
fab
1207bd7a6d Merge pull request #65 from drev74/test/blocking
feat: impl country whitelisting
2025-10-12 19:33:38 +02:00
drev74
5c8d13199b chore: renamed country block to country blacklisting for consistency 2025-10-12 16:04:26 +03:00
drev74
8be3863b48 feat: impl whitelisting and test 2025-10-12 15:57:33 +03:00
drev74
1e5d6d9e3d test: upd geoIP test 2025-10-12 14:20:43 +03:00
drev74
2e6aa32858 test: refactor custom responses 2025-10-12 13:27:29 +03:00
drev74
145feb4bf8 test: upd ip blacklist test 2025-10-12 13:12:13 +03:00
fab
485c86fdbc Merge pull request #64 from drev74/feat/lint
feat: add golangci linter rules
2025-10-11 23:50:47 +02:00
fab
08021ee7e0 Merge pull request #63 from drev74/refactor/ci
refactor(ci): upd flow
2025-10-11 23:50:29 +02:00
drev74
27abae69ea feat: add golangci linter rules 2025-10-11 22:36:20 +03:00
drev74
2fffae5d18 refactor(ci): upd flow 2025-10-11 22:16:36 +03:00
fab
8d5af6be5f Merge pull request #62 from drev74/refactor/trie
refactor: move to external trie
2025-10-11 09:54:19 +02:00
drev74
7938023ed1 refactor(trie): switched to an ext implementation 2025-10-10 23:50:13 +03:00
drev74
c905277058 feat!!: switch to go-trie 2025-10-10 23:21:41 +03:00
fab
6429e286fd Merge pull request #61 from drev74/fix/ipblock 2025-10-10 21:31:19 +02:00
drev74
95efcabc27 refactor(tor): add custom tor list url 2025-10-10 18:57:30 +03:00
drev74
c483f5baba test: remove legacy ioutil 2025-10-10 18:31:54 +03:00
drev74
feee09fcf7 fix(response): add custom response 2025-10-10 18:31:33 +03:00
drev74
6b5b686b55 refactor(handler): add missing custom responses 2025-10-10 18:20:48 +03:00
drev74
719dd2c007 test: fix blocking by ip test 2025-10-10 17:18:18 +03:00
drev74
be8baedaae chore: bump 2025-10-10 15:54:51 +03:00
fab
8685d03503 Merge pull request #58 from fabriziosalmi/copilot/fix-50 2025-09-14 14:12:24 +02:00
copilot-swe-agent[bot]
df5f0511ac Complete Caddy module registration preparation
Co-authored-by: fabriziosalmi <1569108+fabriziosalmi@users.noreply.github.com>
2025-09-13 10:20:08 +00:00
copilot-swe-agent[bot]
2bd1af566c Initial plan 2025-09-13 10:10:37 +00:00
fabriziosalmi
0ac97c5715 fix https://github.com/fabriziosalmi/caddy-waf/issues/56 2025-07-13 18:38:40 +02:00
fab
bae17679f1 Update tests.yml 2025-05-02 07:33:23 +02:00
fab
7f81733fd0 Update tests.yml 2025-04-30 20:36:10 +02:00
fab
5a87efcdf9 Update build-run-validate.yml 2025-04-30 18:05:32 +02:00
fab
da9b8dafc0 Update release.yml 2025-04-30 18:04:21 +02:00
fab
eba6e51887 Update tests.yml 2025-04-30 18:03:24 +02:00
fab
fe98e856fa Update build-run-validate.yml 2025-04-30 18:02:55 +02:00
fabriziosalmi
81f3ad5577 Update Go version to 1.23.0 and set toolchain to go1.24.2 in go.mod 2025-04-30 11:51:02 +02:00
fabriziosalmi
b2035a4acf Enhance middleware registration by ensuring proper interface implementation and updating Go version in go.mod 2025-04-30 11:50:33 +02:00
fabriziosalmi
13712e01d9 Bump WAF version to v0.0.5 and ensure proper module registration 2025-04-30 11:38:01 +02:00
fabriziosalmi
e4c88a3956 Add installation script for Caddy with WAF and module support 2025-04-30 11:25:56 +02:00
fabriziosalmi
fe84fbb5c5 Add debugging tools for WAF configuration and anomaly threshold testing
- Implemented debug_test_results.py to evaluate WAF test results with detailed request/response logging.
- Created debug_waf.go for logging request details and dumping WAF rules to a file.
- Developed debug_waf.py to extract WAF configuration from Caddy Admin API and test WAF rules with sample requests.
- Added sample_rules.json containing test rules for WAF evaluation.
- Configured test.caddyfile for local testing of WAF with defined rules and logging.
- Enhanced test_anomalythreshold.py to validate anomaly threshold behavior with comprehensive test cases and detailed output.
2025-04-30 11:19:17 +02:00
fabriziosalmi
533020d5e6 Update WAF configuration and add browser-friendly rules
- Increased anomaly threshold to reduce false positives.
- Added new rules for browser integrity checks and logging.
- Improved SQL injection and XSS rules to prevent false positives.
- Introduced a new rules file for browser-friendly traffic handling.
2025-04-30 08:57:22 +02:00
fab
bf367b5c53 Merge pull request #53 from fabriziosalmi/dependabot/go_modules/go_modules-c153b83258
Bump golang.org/x/net from 0.33.0 to 0.36.0 in the go_modules group across 1 directory
2025-03-13 11:09:29 +01:00
dependabot[bot]
1989037c29 Bump golang.org/x/net in the go_modules group across 1 directory
Bumps the go_modules group with 1 update in the / directory: [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/net` from 0.33.0 to 0.36.0
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 01:33:48 +00:00
fab
36464a222a Merge pull request #51 from fabriziosalmi/dependabot/go_modules/go_modules-e2b1dc0a79
Bump github.com/go-jose/go-jose/v3 from 3.0.3 to 3.0.4 in the go_modules group across 1 directory
2025-02-28 11:42:46 +01:00
dependabot[bot]
5c266d1665 Bump github.com/go-jose/go-jose/v3
Bumps the go_modules group with 1 update in the / directory: [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose).


Updates `github.com/go-jose/go-jose/v3` from 3.0.3 to 3.0.4
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v3.0.3...v3.0.4)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v3
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-26 22:21:08 +00:00
fab
cd959a2712 Update README.md
Rules generator powered by ChatGPT4 added: https://chatgpt.com/g/g-677d07dd07e48191b799b9e5d6da7828-caddy-waf-ruler
2025-02-25 21:39:07 +01:00
fab
a3fcc3a3f8 Update README.md 2025-02-23 22:31:35 +01:00
fabriziosalmi
1cbd739f7c minor imrpovements 2025-02-09 17:50:59 +01:00
fabriziosalmi
75d217f736 dockerfile glitch 2025-02-09 17:44:00 +01:00
fab
4f31673cb5 Update release.yml 2025-02-09 16:32:45 +01:00
61 changed files with 4262 additions and 1481 deletions

View File

@@ -27,18 +27,10 @@ jobs:
sudo apt update
sudo apt install -y wget git build-essential curl python3 python3-pip
- name: Install Go 1.23.4
- name: Install Go 1.25
uses: actions/setup-go@v4
with:
go-version: '1.23.4'
- name: Validate Go Installation
run: |
go version
if ! go version | grep -q "go1.23.4"; then
echo "Go installation failed or incorrect version"
exit 1
fi
go-version: '1.25'
- name: Clone caddy-waf Repository
run: |
@@ -61,7 +53,7 @@ jobs:
- name: Download GeoLite2 Country Database
run: |
cd caddy-waf
wget https://git.io/GeoLite2-Country.mmdb
wget https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/GeoLite2-Country.mmdb
- name: Validate GeoLite2 Download
run: |
@@ -139,7 +131,7 @@ jobs:
run: |
cd caddy-waf
chmod +x caddy
./caddy run > caddy_output.log 2>&1 &
./caddy run --config test.caddyfile > caddy_output.log 2>&1 &
sleep 5
if ! pgrep -f "caddy run"; then

View File

@@ -26,11 +26,11 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23.4' # Use your desired go version
go-version: '1.25' # Use your desired go version
- name: Extract Tag Name
id: extract_tag
if: "!startsWith(github.ref, 'refs/heads/')"
if: startsWith(github.ref, 'refs/tags/')
run: echo "TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_OUTPUT
- name: Build Binary
@@ -50,38 +50,31 @@ jobs:
create-release:
runs-on: ubuntu-latest
needs: build-and-release # Ensure all builds complete before creating release
permissions:
contents: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Extract Tag Name
id: extract_tag
if: "!startsWith(github.ref, 'refs/heads/')"
if: startsWith(github.ref, 'refs/tags/')
run: echo "TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_OUTPUT
- name: Create Release
id: create_release
if: "!startsWith(github.ref, 'refs/heads/')"
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.extract_tag.outputs.TAG_NAME }}
release_name: Release ${{ steps.extract_tag.outputs.TAG_NAME }}
body: |
This is a release of the Caddy WAF middleware version ${{ steps.extract_tag.outputs.TAG_NAME }}. Please download the appropriate binary for your OS/Architecture.
draft: false
prerelease: false
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Upload Release Assets
if: "!startsWith(github.ref, 'refs/heads/')"
run: |
for asset in $(ls *.tar.gz); do
echo "Uploading ${asset}"
gh release upload ${{ steps.create_release.outputs.upload_url }} ${asset} --clobber
done
- name: Release via GH CLI
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ steps.extract_tag.outputs.TAG_NAME }}
run: |
# Flatten artifacts structure (download-artifact puts them in subdirs matches artifact name)
# But we want all .tar.gz in current dir
find . -name "*.tar.gz" -exec mv {} . \;
echo "Creating or updating release for $TAG_NAME..."
# Try to create release with assets. If it fails (exists), upload assets.
gh release create "$TAG_NAME" *.tar.gz --title "$TAG_NAME" --generate-notes || \
gh release upload "$TAG_NAME" *.tar.gz --clobber

66
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Test
on:
push:
branches:
- main
- ci
pull_request:
branches:
- main
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
test:
strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: true
matrix:
os:
- ubuntu-latest
go:
- '1.25'
runs-on: ${{ matrix.os }}
permissions:
contents: read
pull-requests: read
actions: write # to allow uploading artifacts and cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ matrix.go }}
check-latest: true
- name: Get dependencies
run: |
go get -v
- name: Run linter
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
with:
version: latest
args: --timeout 5m
- name: Download MaxMindDB binary
run: |
wget https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/GeoLite2-Country.mmdb
# Commented bits below were useful to allow the job to continue
# even if the tests fail, so we can publish the report separately
# For info about set-output, see https://stackoverflow.com/questions/57850553/github-actions-check-steps-status
- name: Run tests
run: |
go test -v -failfast ./... -tags=it

View File

@@ -1,69 +0,0 @@
name: "Tests"
permissions:
contents: read
pull-requests: write
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.23.4"
- name: Get Dependencies
run: go get -v ./...
- name: Get Caddy Dependencies
run: |
go get github.com/caddyserver/caddy/v2/modules/caddyhttp/templates@v2.9.1
go get github.com/caddyserver/caddy/v2/modules/caddyhttp/proxyprotocol@v2.9.1
go get github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing@v2.9.1
go get github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver@v2.9.1
go get github.com/smallstep/certificates/acme@v0.26.1
- name: Build Caddy
run: |
go build -v -o caddy github.com/caddyserver/caddy/v2/cmd/caddy
- name: Tidy modules
run: go mod tidy
- name: Build
run: go build -v ./...
- name: Test
id: test
run: |
test_output=$(go test -v -count=1 ./... 2>&1)
echo "test_output<<EOF" >> $GITHUB_STEP_SUMMARY
echo "$test_output" >> $GITHUB_STEP_SUMMARY
echo "EOF" >> $GITHUB_STEP_SUMMARY
passed_count=$(echo "$test_output" | grep "PASS:" | wc -l)
failed_count=$(echo "$test_output" | grep "FAIL:" | wc -l)
echo "::set-output name=passed::$(echo $passed_count)"
echo "::set-output name=failed::$(echo $failed_count)"
- name: Test Summary
if: always()
run: |
echo "Test Results:"
echo "Passed Tests: ${{ steps.test.outputs.passed }}"
echo "Failed Tests: ${{ steps.test.outputs.failed }}"
- name: Fail if tests failed
if: steps.test.outputs.failed != '0'
run: exit 1

122
.golangci.yml Normal file
View File

@@ -0,0 +1,122 @@
version: "2"
run:
issues-exit-code: 1
tests: false
build-tags:
- nobadger
- nomysql
- nopgx
output:
formats:
text:
path: stdout
print-linter-name: true
print-issued-lines: true
linters:
default: none
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck
- errname
- exhaustive
- gosec
- govet
- importas
- ineffassign
- misspell
- prealloc
- promlinter
- sloglint
- sqlclosecheck
- staticcheck
- testableexamples
- testifylint
- tparallel
- unconvert
- unused
- wastedassign
- whitespace
- zerologlint
settings:
staticcheck:
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF1006", "-QF1008"] # default, and exclude 1 more undesired check
errcheck:
exclude-functions:
- fmt.*
- (go.uber.org/zap/zapcore.ObjectEncoder).AddObject
- (go.uber.org/zap/zapcore.ObjectEncoder).AddArray
exhaustive:
ignore-enum-types: reflect.Kind|svc.Cmd
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- gosec
text: G115 # Excluded due to potential noisy integer overflow warnings
- linters:
- gosec
text: G107 # we aren't calling unknown URL
- linters:
- gosec
text: G203 # as a web server that's expected to handle any template, this is totally in the hands of the user.
- linters:
- gosec
text: G204 # we're shelling out to known commands, not relying on user-defined input.
- linters:
- gosec
# the choice of weakrand is deliberate, hence the named import "weakrand"
path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: G404
- linters:
- gosec
path: modules/caddyhttp/reverseproxy/streaming.go
text: G404
- linters:
- dupl
path: modules/logging/filters.go
- linters:
- dupl
path: modules/caddyhttp/matchers.go
- linters:
- dupl
path: modules/caddyhttp/vars.go
- linters:
- errcheck
path: _test\.go
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
settings:
gci:
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
custom-order: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -0,0 +1,120 @@
# Caddy Module Registration Checklist
This document outlines the requirements and steps for successfully registering the caddy-waf module in the official Caddy modules directory.
## ✅ Completed Requirements
### 1. Module Structure Compliance
- [x] **Module Interface Implementation**: Properly implements `caddy.Module` interface
- [x] **Module ID**: Correctly uses `http.handlers.waf` as module ID
- [x] **Registration**: Module is registered in `init()` function using `caddy.RegisterModule()`
- [x] **Interface Guards**: Proper interface guards implemented for compile-time checking
- [x] **Caddyfile Support**: Implements `caddyfile.Unmarshaler` for Caddyfile parsing
### 2. Required Interfaces
- [x] **caddy.Module**: Implemented via `CaddyModule()` method
- [x] **caddy.Provisioner**: Implemented via `Provision()` method
- [x] **caddy.Validator**: Implemented via `Validate()` method
- [x] **caddyhttp.MiddlewareHandler**: Implemented via `ServeHTTP()` method
- [x] **caddyfile.Unmarshaler**: Implemented via `UnmarshalCaddyfile()` method
### 3. Documentation Requirements
- [x] **Package Documentation**: Added comprehensive package-level documentation
- [x] **Struct Documentation**: Added detailed documentation for main Middleware struct
- [x] **README.md**: Comprehensive README with examples and installation instructions
- [x] **Module Metadata**: Created `MODULE.md` with standardized module information
- [x] **Usage Examples**: Created `caddyfile.example` with practical configuration examples
- [x] **API Documentation**: Generated via `go doc` commands
### 4. Code Quality and Standards
- [x] **Go Module Structure**: Proper `go.mod` with correct module path
- [x] **Version Consistency**: Updated version constant to match latest release (v0.0.6)
- [x] **Build Verification**: Module builds successfully with `go build`
- [x] **Module Verification**: Passes `go mod verify`
- [x] **No Build Errors**: Clean compilation with no warnings or errors
### 5. Release Management
- [x] **Git Tags**: Proper semantic versioning tags (v0.0.3, v0.0.4, v0.0.5, v0.0.6)
- [x] **GitHub Releases**: Automated release workflow creating GitHub releases
- [x] **Release Notes**: Proper release descriptions and changelogs
- [x] **Binary Assets**: Cross-platform binaries generated for releases
### 6. Testing and Validation
- [x] **Test Suite**: Comprehensive test coverage across multiple files
- [x] **CI/CD Pipeline**: GitHub Actions workflows for testing and building
- [x] **Module Import**: Can be imported and used with `xcaddy build`
## 🔍 Potential Issues and Solutions
### Issue Analysis: Registration Error ID `2b782e50-057d-4dac-bbd5-4cd1c1188669`
Based on the error ID mentioned in the issue comments, this appears to be a server-side error during the registration process rather than a module compliance issue. Common causes and solutions:
### 1. **Server-Side Registration Issues**
- **Cause**: Temporary issues with the Caddy module registration service
- **Solution**: Retry registration after some time
- **Status**: May resolve automatically
### 2. **Module Path Validation**
- **Cause**: Registration service may have strict validation rules
- **Solution**: Ensure `github.com/fabriziosalmi/caddy-waf` is accessible and properly formatted
- **Status**: ✅ Module path is valid and accessible
### 3. **Go Module Accessibility**
- **Cause**: Registration service needs to fetch and validate the module
- **Solution**: Ensure module is publicly accessible and properly tagged
- **Status**: ✅ Repository is public with proper tags
### 4. **Caddy Version Compatibility**
- **Cause**: Module might require specific Caddy version
- **Solution**: Verify compatibility with latest Caddy version
- **Status**: ✅ Uses Caddy v2.9.1 (latest)
## 🚀 Next Steps for Registration
### 1. **Retry Registration**
- Visit https://caddyserver.com/account/register-package
- Use the exact module path: `github.com/fabriziosalmi/caddy-waf`
- Ensure using the latest tag: `v0.0.6`
### 2. **Contact Caddy Team**
- If registration continues to fail, contact Caddy maintainers
- Provide the error ID: `2b782e50-057d-4dac-bbd5-4cd1c1188669`
- Reference this module's compliance with all requirements
### 3. **Alternative Registration Paths**
- Consider submitting a PR to the Caddy Community repository
- Engage with the Caddy community on forums or Discord
- Document the module in community wikis or resources
## 📋 Final Verification Commands
Run these commands to verify module readiness:
```bash
# Verify module builds successfully
go build -v
# Verify module interfaces
go doc -short
# Test module import
go list -m github.com/fabriziosalmi/caddy-waf
# Verify with xcaddy (if available)
xcaddy build --with github.com/fabriziosalmi/caddy-waf
# Check latest version/tag
git describe --tags --abbrev=0
```
## 📞 Support Information
- **Repository**: https://github.com/fabriziosalmi/caddy-waf
- **Issues**: https://github.com/fabriziosalmi/caddy-waf/issues
- **License**: AGPLv3
- **Maintainer**: @fabriziosalmi
---
**Conclusion**: The caddy-waf module meets all technical requirements for Caddy module registration. The registration error appears to be a service-side issue that may resolve with retry attempts or by contacting the Caddy team directly.

34
CHANGELOG.md Normal file
View File

@@ -0,0 +1,34 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.1.5] - 2025-12-08
### Fixed
- Fixed critical bug where POST request bodies were lost or truncated by using `io.MultiReader` to restore the full body stream (fixes #76).
## [v0.1.4] - 2025-12-06
### Security
- Fixed Panic vulnerability in `quic-go` by upgrading to `v0.54.0` (requires Caddy v2.10.x and Go 1.25).
- Addressed Dependabot Alert #7.
### Changed
- Upgraded Caddy dependency to `v2.10.2`.
- Upgraded Go requirement to `1.25`.
- Improved CI workflows to use Go 1.25 for build and release.
## [v0.1.3] - 2025-12-06
### Fixed
- Downgraded `quic-go` to `v0.48.2` and Caddy to `v2.9.1` to temporarily resolve Go version conflicts (superseded by v0.1.4).
- Fixed import grouping for `gci` linter compliance.
- Fixed GitHub Actions release workflow.
## [v0.1.2] - 2025-12-06
### Added
- SOTA Engineering patterns (Zero-Copy headers, Wait-Free Ring Buffer, Circuit Breaker).
- ASN Blocking support.
- Configurable Request Body size limit.
- GeoIP Fail Open configuration.

View File

@@ -4,7 +4,6 @@
}
:8080 {
log {
output stdout
format console
@@ -19,8 +18,10 @@
# WAF Plugin runs on all requests first
waf {
metrics_endpoint /waf_metrics
anomaly_threshold 10
block_countries GeoLite2-Country.mmdb RU CN KP
anomaly_threshold 20
# Using modified rules file that prevents false positives with Chrome browser requests
rule_file rules.json
# block_countries GeoLite2-Country.mmdb RU CN KP
# whitelist_countries GeoLite2-Country.mmdb US
# custom_response 403 application/json error.json
@@ -30,7 +31,7 @@
requests 100
window 10s
cleanup_interval 5m
paths /ratelimited # List of individual regex patterns (example: paths ^/api/.*)
paths /ratelimited # List of individual regex patterns (example: paths ^/api/.*)
match_all_paths false
}
@@ -43,8 +44,6 @@
retry_interval 1h
}
rule_file rules.json
# rule_file rules/wordpress.json
ip_blacklist_file ip_blacklist.txt
dns_blacklist_file dns_blacklist.txt
log_severity info
@@ -56,7 +55,7 @@
# Match the waf metrics endpoint specifically and stop processing
@wafmetrics path /waf_metrics
handle @wafmetrics {
header Access-Control-Allow-Origin * # Allow requests from any origin (for development - see note below)
header Access-Control-Allow-Origin * # Allow requests from any origin (for development - see note below)
header Access-Control-Allow-Methods "GET, OPTIONS" # Allow GET and OPTIONS methods
header Access-Control-Allow-Headers "User-Agent, Content-Type, *" # Allow User-Agent and Content-Type headers
# Do not respond here so it goes to the WAF plugin

View File

@@ -1,5 +1,5 @@
# Use a Go base image to build the Caddy binary
FROM golang:1.22.3-alpine AS builder
FROM golang:1.24-alpine AS builder
# Install git and xcaddy (required for cloning the repository and building Caddy)
RUN apk add --no-cache git wget && \
@@ -14,10 +14,7 @@ RUN git clone https://github.com/fabriziosalmi/caddy-waf.git
# Navigate into the caddy-waf directory
WORKDIR /app/caddy-waf
# Fetch and install the required Go modules (including Caddy v2)
RUN go get -v github.com/caddyserver/caddy/v2 github.com/caddyserver/caddy/v2/caddyconfig/caddyfile github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile github.com/caddyserver/caddy/v2 github.com/caddyserver/caddy/v2/modules/caddyhttp github.com/oschwald/maxminddb-golang github.com/fsnotify/fsnotify github.com/fabriziosalmi/caddy-waf && \
# Clean up and update the go.mod file
# Clean up and update the go.mod file (dependencies are already defined in go.mod)
RUN go mod tidy
# Download the GeoLite2 Country database

29
GNUmakefile Normal file
View File

@@ -0,0 +1,29 @@
tidy:
@go mod tidy
@echo "Done!"
upd:
@go get -u ./...
@echo "Done!"
fmt:
@go fmt ./...
test:
@go test -v ./...
@echo "Done!"
it:
@go test -v ./... -tags=it
@echo "Done!"
lint:
@echo "==> Checking source code with golangci-lint..."
@golangci-lint run
lintfix:
@echo "==> Checking source code with golangci-lint..."
@golangci-lint run --fix
test-integration:
@docker run --rm -v $(PWD):/app -w /app python:3.9-slim python test.py

76
MODULE.md Normal file
View File

@@ -0,0 +1,76 @@
# Caddy WAF Module Information
**Module Name:** caddy-waf
**Module ID:** `http.handlers.waf`
**Go Module Path:** `github.com/fabriziosalmi/caddy-waf`
**License:** AGPLv3
**Latest Version:** v0.0.6
## Description
A robust, highly customizable, and feature-rich Web Application Firewall (WAF) middleware for the Caddy web server. This middleware provides advanced protection against a comprehensive range of web-based threats, seamlessly integrating with Caddy and offering flexible configuration options to secure your applications effectively.
## Module Type
HTTP Handler Middleware (`http.handlers.waf`)
## Features
- **Regex-Based Filtering:** Deep URL, data & header inspection using powerful regex rules
- **Blacklisting:** Blocks malicious IPs, domains & optionally TOR exit nodes
- **Geo-Blocking:** Restricts access by country using GeoIP
- **Rate Limiting:** Prevents abuse via customizable IP request limits
- **Anomaly Scoring:** Dynamically blocks requests based on cumulative rule matches
- **Multi-Phase Inspection:** Analyzes traffic throughout the request lifecycle
- **Sensitive Data Redaction:** Removes private info from logs
- **Custom Response Handling:** Tailored responses for blocked requests
- **Detailed Monitoring:** JSON endpoint for performance tracking & analysis
- **Dynamic Config Reloads:** Seamless updates without restarts
## Installation
```bash
xcaddy build --with github.com/fabriziosalmi/caddy-waf
```
## Basic Usage
```caddyfile
example.com {
waf {
rule_file rules.json
ip_blacklist_file ip_blacklist.txt
dns_blacklist_file dns_blacklist.txt
metrics_endpoint /waf_metrics
}
respond "Protected by Caddy WAF"
}
```
## Configuration Options
| Option | Type | Description |
|--------|------|-------------|
| `rule_file` | string | Path to WAF rules JSON file |
| `ip_blacklist_file` | string | Path to IP blacklist file |
| `dns_blacklist_file` | string | Path to DNS blacklist file |
| `metrics_endpoint` | string | Endpoint for WAF metrics |
| `anomaly_threshold` | int | Threshold for anomaly detection |
| `rate_limit` | block | Rate limiting configuration |
| `country_block` | block | Country blocking configuration |
| `custom_response` | block | Custom response configuration |
| `log_level` | string | Logging level (debug, info, warn, error) |
| `log_file` | string | Path to log file |
## Documentation
Complete documentation is available in the [docs directory](https://github.com/fabriziosalmi/caddy-waf/tree/main/docs).
## Repository
https://github.com/fabriziosalmi/caddy-waf
## Support
For issues and support, please visit the [GitHub Issues page](https://github.com/fabriziosalmi/caddy-waf/issues).

View File

@@ -2,12 +2,12 @@
A robust, highly customizable, and feature-rich **Web Application Firewall (WAF)** middleware for the Caddy web server. This middleware provides **advanced protection** against a comprehensive range of web-based threats, seamlessly integrating with Caddy and offering flexible configuration options to secure your applications effectively.
[![Tests](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/tests.yml/badge.svg)](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/go.yml) [![CodeQL](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/github-code-scanning/codeql) [![Build, Run and Validate](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/build-run-validate.yml/badge.svg)](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/build-run-validate.yml)
[![Tests](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/test.yml/badge.svg)](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/test.yml) [![CodeQL](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/github-code-scanning/codeql) [![Build, Run and Validate](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/build-run-validate.yml/badge.svg)](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/build-run-validate.yml)
## 🛡️ Core Protections
* **Regex-Based Filtering:** Deep URL, data & header inspection using powerful regex rules.
* **Blacklisting:** Blocks malicious IPs, domains & optionally TOR exit nodes.
* **Blacklisting:** Blocks malicious IPs, domains, ASNs & optionally TOR exit nodes.
* **Geo-Blocking:** Restricts access by country using GeoIP.
* **Rate Limiting:** Prevents abuse via customizable IP request limits.
* **Anomaly Scoring:** Dynamically blocks requests based on cumulative rule matches.
@@ -18,10 +18,18 @@ A robust, highly customizable, and feature-rich **Web Application Firewall (WAF)
* **Dynamic Config Reloads:** Seamless updates without restarts.
* **File Watchers:** Automatic reloads on rule/blacklist changes.
* **Observability:** Seamless integration with ELK stack and Prometheus.
* **Rules generator**: [available here](https://chatgpt.com/g/g-677d07dd07e48191b799b9e5d6da7828-caddy-waf-ruler)
_Simple at a glance UI :)_
![demo](https://github.com/fabriziosalmi/caddy-waf/blob/main/docs/caddy-waf-ui.png?raw=true)
## Security & Performance
* **Zero-Copy Networking**: Uses `unsafe.String` to eliminate memory allocations during request body inspection.
* **Wait-Free Concurrency**: Atomic counters ensure accurate metrics and rule hit counting without lock contention.
* **Circuit Breaker**: `geoip_fail_open` prevents database failures from causing service outages.
* **DoS Protection**: `io.LimitReader` enforces strict request body limits to prevent memory exhaustion.
* **ReDoS Safety**: Built on top of Go's `regexp` (RE2), guaranteeing linear time execution for all regex rules.
## 🚀 Quick Start
```bash
@@ -54,6 +62,12 @@ curl -fsSL -H "Pragma: no-cache" https://raw.githubusercontent.com/fabriziosalmi
## 🚀 Installation
### Prerequisites
- [Go](https://golang.org/dl/) **1.25** or higher
- [Caddy](https://caddyserver.com/docs/install) **v2.10.x** or higher (for building with this plugin)
- [xcaddy](https://github.com/caddyserver/xcaddy) (for building Caddy with plugins)
```bash
# Step 1: Clone the caddy-waf repository from GitHub
git clone https://github.com/fabriziosalmi/caddy-waf.git
@@ -166,6 +180,28 @@ This project is licensed under the **AGPLv3 License**.
---
## Others projects
If You like my projects, you may also like these ones:
- [patterns](https://github.com/fabriziosalmi/patterns) Automated OWASP CRS and Bad Bot Detection for Nginx, Apache, Traefik and HaProxy
- [blacklists](https://github.com/fabriziosalmi/blacklists) Hourly updated domains blacklist 🚫
- [proxmox-vm-autoscale](https://github.com/fabriziosalmi/proxmox-vm-autoscale) Automatically scale virtual machines resources on Proxmox hosts
- [UglyFeed](https://github.com/fabriziosalmi/UglyFeed) Retrieve, aggregate, filter, evaluate, rewrite and serve RSS feeds using Large Language Models for fun, research and learning purposes
- [proxmox-lxc-autoscale](https://github.com/fabriziosalmi/proxmox-lxc-autoscale) Automatically scale LXC containers resources on Proxmox hosts
- [DevAssistant](https://github.com/fabriziosalmi/DevGPT) Code together, right now! AI powered code assistant to build project in minutes
- [websites-monitor](https://github.com/fabriziosalmi/websites-monitor) Websites monitoring via GitHub Actions (expiration, security, performances, privacy, SEO)
- [caddy-mib](https://github.com/fabriziosalmi/caddy-mib) Track and ban client IPs generating repetitive errors on Caddy
- [zonecontrol](https://github.com/fabriziosalmi/zonecontrol) Cloudflare Zones Settings Automation using GitHub Actions
- [lws](https://github.com/fabriziosalmi/lws) linux (containers) web services
- [cf-box](https://github.com/fabriziosalmi/cf-box) cf-box is a set of Python tools to play with API and multiple Cloudflare accounts.
- [limits](https://github.com/fabriziosalmi/limits) Automated rate limits implementation for web servers
- [dnscontrol-actions](https://github.com/fabriziosalmi/dnscontrol-actions) Automate DNS updates and rollbacks across multiple providers using DNSControl and GitHub Actions
- [proxmox-lxc-autoscale-ml](https://github.com/fabriziosalmi/proxmox-lxc-autoscale-ml) Automatically scale the LXC containers resources on Proxmox hosts with AI
- [csv-anonymizer](https://github.com/fabriziosalmi/csv-anonymizer) CSV fuzzer/anonymizer
- [iamnotacoder](https://github.com/fabriziosalmi/iamnotacoder) AI code generation and improvement
## 🙏 Contributing
Contributions are highly welcome! Feel free to open an issue or submit a pull request.

View File

@@ -4,12 +4,35 @@
| Version | Supported |
| ------- | ------------------ |
| current | :white_check_mark: |
| v0.1.x | :white_check_mark: |
| < 0.1.0 | :x: |
## Reporting a Vulnerability
There is automated security code scanning in place provided by GitHub.
We take the security of `caddy-waf` seriously. If you find a vulnerability, please report it!
Please open an issue to report a vulnerability.
### How to Report
Please do **NOT** open a public issue on GitHub. Instead, report the vulnerability via:
1. **Email**: Send the details to the maintainer (fabrizio.salmi@gmail.com).
2. **GitHub Private Advisory**: Open a private advisory draft on this repository if you have permissions, or contact the maintainer to enable it.
### Required Information
When reporting a vulnerability, please include:
- A description of the vulnerability.
- Steps to reproduce the issue (PoC code is helpful).
- Impact of the vulnerability.
- Affected versions.
### Response Timeline
- We will acknowledge your report within 48 hours.
- We will provide an estimated timeline for the fix within 1 week.
- We will release a patch as soon as possible.
### Credit
We will credit you in the release notes and changelog for responsibly disclosing vulnerabilities, unless you prefer to remain anonymous.

8
assets.go Normal file
View File

@@ -0,0 +1,8 @@
//go:build with_ui
package caddywaf
import "embed"
//go:embed ui/*
var Assets embed.FS

7
assets_stub.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build !with_ui
package caddywaf
import "embed"
var Assets embed.FS

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"net"
"net/netip"
"os"
"strings"
@@ -63,17 +64,21 @@ func (bl *BlacklistLoader) LoadDNSBlacklistFromFile(path string, dnsBlacklist ma
return nil
}
func (m *Middleware) isIPBlacklisted(ip string) bool {
if m.ipBlacklist == nil { // Defensive check: ensure ipBlacklist is not nil
return false
func (m *Middleware) isIPBlacklisted(addr string) bool {
ip := extractIP(addr)
if m.ipBlacklist == nil {
m.logger.Error("blacklist", zap.String("IP blacklist", "is nil"))
}
if m.ipBlacklist.Contains(ip) {
if m.ipBlacklist.Contains(netip.MustParseAddr(ip)) {
m.muIPBlacklistMetrics.Lock() // Acquire lock before accessing shared counter
m.IPBlacklistBlockCount++ // Increment the counter
m.muIPBlacklistMetrics.Unlock() // Release lock after accessing counter
m.logger.Debug("IP blacklist hit", zap.String("ip", ip)) // Keep existing debug log
return true // Indicate that the IP is blacklisted
}
return false // Indicate that the IP is NOT blacklisted
}
@@ -111,23 +116,6 @@ func (m *Middleware) isDNSBlacklisted(host string) bool {
return false
}
// extractIP extracts the IP address from a remote address string.
func extractIP(remoteAddr string, logger *zap.Logger) string {
if logger == nil {
logger = zap.NewNop()
}
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
logger.Debug("Using full remote address as IP",
zap.String("remoteAddr", remoteAddr),
zap.Error(err),
)
return remoteAddr // Assume the input is already an IP address
}
return host
}
// LoadIPBlacklistFromFile loads IP addresses from a file into the provided map.
// LoadIPBlacklistFromFile loads IP addresses from a file into the provided map.
func (bl *BlacklistLoader) LoadIPBlacklistFromFile(path string, ipBlacklist map[string]struct{}) error {
bl.logger.Debug("Loading IP blacklist", zap.String("path", path))

View File

@@ -1,7 +1,6 @@
package caddywaf
import (
"io/ioutil"
"os"
"testing"
@@ -15,7 +14,7 @@ func TestLoadDNSBlacklistFromFile(t *testing.T) {
malicious.com
spaces.com
`
tmpfile, err := ioutil.TempFile("", "dnsblacklist")
tmpfile, err := os.CreateTemp("", "dnsblacklist")
if err != nil {
t.Fatal(err)
}
@@ -60,7 +59,7 @@ func TestLoadIPBlacklistFromFile(t *testing.T) {
172.16.1.1
invalid-ip
`
tmpfile, err := ioutil.TempFile("", "ipblacklist")
tmpfile, err := os.CreateTemp("", "ipblacklist")
if err != nil {
t.Fatal(err)
}
@@ -97,30 +96,6 @@ invalid-ip
}
}
func TestExtractIP(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"IP with port", "192.168.1.1:8080", "192.168.1.1"},
{"IP only", "192.168.1.1", "192.168.1.1"},
{"IPv6 with port", "[2001:db8::1]:8080", "2001:db8::1"},
{"IPv6 only", "2001:db8::1", "2001:db8::1"},
}
logger := zap.NewNop()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractIP(tt.input, logger)
if result != tt.expected {
t.Errorf("extractIP(%s) = %s; want %s", tt.input, result, tt.expected)
}
})
}
}
func TestAddIPEntry(t *testing.T) {
logger := zap.NewNop()
bl := NewBlacklistLoader(logger)

75
caddyfile.example Normal file
View File

@@ -0,0 +1,75 @@
# Example Caddyfile showing caddy-waf module usage
# This is a basic configuration example for the Caddy WAF middleware
{
auto_https off
admin localhost:2019
}
# Example 1: Basic WAF setup
example.com {
# Enable WAF protection with basic configuration
waf {
# Rule file for WAF rules
rule_file rules.json
# IP blacklist file
ip_blacklist_file ip_blacklist.txt
# DNS blacklist file
dns_blacklist_file dns_blacklist.txt
# Metrics endpoint
metrics_endpoint /waf_metrics
# Anomaly threshold
anomaly_threshold 10
# Log settings
log_level info
log_file waf.log
}
# Your web application
respond "Hello, World! Protected by Caddy WAF"
}
# Example 2: Advanced WAF configuration with rate limiting
api.example.com {
waf {
rule_file rules.json
ip_blacklist_file ip_blacklist.txt
dns_blacklist_file dns_blacklist.txt
metrics_endpoint /waf_metrics
# Rate limiting configuration
rate_limit {
requests 100
window 10s
paths "/api/*" "/admin/*"
}
# Country blocking
country_block {
enabled true
countries CN RU
geoip_db_path GeoLite2-Country.mmdb
}
# Custom response for blocked requests
custom_response {
status_code 403
body "Access Denied by WAF"
}
# Anomaly threshold
anomaly_threshold 15
# Logging
log_level debug
log_file api_waf.log
log_json true
}
reverse_proxy localhost:8080
}

View File

@@ -1,3 +1,18 @@
// Package caddywaf implements a Web Application Firewall (WAF) middleware for Caddy.
//
// This package provides comprehensive security features including:
// - Regex-based filtering for URLs, data, and headers
// - IP and DNS blacklisting capabilities
// - Geographic access control
// - Rate limiting
// - Anomaly detection and scoring
// - Multi-phase request inspection
// - Real-time metrics and monitoring
//
// The WAF integrates seamlessly with Caddy as an HTTP handler middleware
// and can be configured via Caddyfile or JSON configuration.
//
// Module ID: http.handlers.waf
package caddywaf
import (
@@ -5,37 +20,40 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/netip"
"os"
"strings"
"sync"
"sync/atomic"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/fsnotify/fsnotify"
"github.com/oschwald/maxminddb-golang"
"github.com/phemmer/go-iptrie"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/fsnotify/fsnotify"
)
// ==================== Constants and Globals ====================
var (
_ caddy.Module = (*Middleware)(nil) // <-- AGGIUNGI QUESTA RIGA!
_ caddy.Provisioner = (*Middleware)(nil)
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
_ caddyfile.Unmarshaler = (*Middleware)(nil)
_ caddy.Validator = (*Middleware)(nil)
_ caddy.Validator = (*Middleware)(nil) // Assicurati che anche questa sia presente se hai un metodo Validate()
)
// Add or update the version constant as needed
const wafVersion = "v0.0.1" // update this value to the new release version when tagging
const wafVersion = "v0.1.3" // update this value to the new release version when tagging
// ==================== Initialization and Setup ====================
func init() {
caddy.RegisterModule(&Middleware{}) // Changed from Middleware{} to &Middleware{}
caddy.RegisterModule(&Middleware{}) // Register the module with Caddy
httpcaddyfile.RegisterHandlerDirective("waf", parseCaddyfile)
}
@@ -64,7 +82,9 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
func (m *Middleware) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger(m)
m.ruleCache = NewRuleCache() // Initialize RuleCache
m.ruleCache = NewRuleCache() // Initialize RuleCache
m.Rules = make(map[int][]Rule) // Initialize Rules map to prevent nil pointer panic
m.ipBlacklist = iptrie.NewTrie()
// Set default log severity if not provided
if m.LogSeverity == "" {
@@ -102,7 +122,7 @@ func (m *Middleware) Provision(ctx caddy.Context) error {
fileCfg.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
fileEncoder := zapcore.NewJSONEncoder(fileCfg.EncoderConfig)
fileSync, err := os.OpenFile(m.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
fileSync, err := os.OpenFile(m.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
m.logger.Warn("Failed to open log file, logging only to console", zap.String("path", m.LogFilePath), zap.Error(err))
m.logger = zap.New(zapcore.NewCore(consoleEncoder, consoleSync, logLevel))
@@ -123,6 +143,14 @@ func (m *Middleware) Provision(ctx caddy.Context) error {
zap.Int("anomaly_threshold", m.AnomalyThreshold),
)
// ADDED: Set default anomaly threshold if not provided or invalid
if m.AnomalyThreshold <= 0 {
m.AnomalyThreshold = 20 // Use a reasonable default value
m.logger.Info("Using default anomaly threshold", zap.Int("anomaly_threshold", m.AnomalyThreshold))
} else {
m.logger.Info("Using configured anomaly threshold", zap.Int("anomaly_threshold", m.AnomalyThreshold))
}
// Start the asynchronous logging worker
m.StartLogWorker()
@@ -167,23 +195,23 @@ func (m *Middleware) Provision(ctx caddy.Context) error {
// Initialize GeoIP stats
m.geoIPStats = make(map[string]int64)
// Configure GeoIP-based country blocking/whitelisting
if m.CountryBlock.Enabled || m.CountryWhitelist.Enabled {
geoIPPath := m.CountryBlock.GeoIPDBPath
// Configure GeoIP-based country blacklisting/whitelisting
if m.CountryBlacklist.Enabled || m.CountryWhitelist.Enabled {
geoIPPath := m.CountryBlacklist.GeoIPDBPath
if m.CountryWhitelist.Enabled && m.CountryWhitelist.GeoIPDBPath != "" {
geoIPPath = m.CountryWhitelist.GeoIPDBPath
}
if !fileExists(geoIPPath) {
m.logger.Warn("GeoIP database not found. Country blocking/whitelisting will be disabled", zap.String("path", geoIPPath))
m.logger.Warn("GeoIP database not found. Country blacklisting/whitelisting will be disabled", zap.String("path", geoIPPath))
} else {
reader, err := maxminddb.Open(geoIPPath)
if err != nil {
m.logger.Error("Failed to load GeoIP database", zap.String("path", geoIPPath), zap.Error(err))
} else {
m.logger.Info("GeoIP database loaded successfully", zap.String("path", geoIPPath))
if m.CountryBlock.Enabled {
m.CountryBlock.geoIP = reader
if m.CountryBlacklist.Enabled {
m.CountryBlacklist.geoIP = reader
}
if m.CountryWhitelist.Enabled {
m.CountryWhitelist.geoIP = reader
@@ -192,27 +220,46 @@ func (m *Middleware) Provision(ctx caddy.Context) error {
}
}
// Configure ASN blocking
if m.BlockASNs.Enabled {
if !fileExists(m.BlockASNs.GeoIPDBPath) {
m.logger.Warn("ASN GeoIP database not found. ASN blocking will be disabled", zap.String("path", m.BlockASNs.GeoIPDBPath))
} else {
reader, err := maxminddb.Open(m.BlockASNs.GeoIPDBPath)
if err != nil {
m.logger.Error("Failed to load ASN GeoIP database", zap.String("path", m.BlockASNs.GeoIPDBPath), zap.Error(err))
} else {
m.logger.Info("ASN GeoIP database loaded successfully", zap.String("path", m.BlockASNs.GeoIPDBPath))
m.BlockASNs.geoIP = reader
}
}
}
// Initialize config and blacklist loaders
m.configLoader = NewConfigLoader(m.logger)
m.blacklistLoader = NewBlacklistLoader(m.logger)
m.geoIPHandler = NewGeoIPHandler(m.logger)
m.requestValueExtractor = NewRequestValueExtractor(m.logger, m.RedactSensitiveData)
m.requestValueExtractor = NewRequestValueExtractor(m.logger, m.RedactSensitiveData, m.MaxRequestBodySize)
// Configure GeoIP handler
m.geoIPHandler.WithGeoIPCache(m.geoIPCacheTTL)
m.geoIPHandler.WithGeoIPLookupFallbackBehavior(m.geoIPLookupFallbackBehavior)
// Load configuration from Caddyfile
dispenser := caddyfile.NewDispenser([]caddyfile.Token{})
err = m.configLoader.UnmarshalCaddyfile(dispenser, m)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
// Initialize TorConfig with default values if not set
if m.Tor.TORIPBlacklistFile == "" {
m.Tor.TORIPBlacklistFile = "tor_blacklist.txt"
}
if m.Tor.UpdateInterval == "" {
m.Tor.UpdateInterval = "24h"
}
if m.Tor.RetryInterval == "" {
m.Tor.RetryInterval = "5m"
}
// Load IP blacklist
if m.IPBlacklistFile != "" {
m.ipBlacklist = NewCIDRTrie()
err = m.loadIPBlacklist(m.IPBlacklistFile, m.ipBlacklist)
m.ipBlacklist = iptrie.NewTrie()
err = m.loadIPBlacklist(m.IPBlacklistFile, *m.ipBlacklist)
if err != nil {
return fmt.Errorf("failed to load IP blacklist: %w", err)
}
@@ -259,23 +306,19 @@ func (m *Middleware) Shutdown(ctx context.Context) error {
m.logger.Debug("Logging worker stopped.")
var firstError error
var errorOccurred bool
// Close GeoIP databases
if m.CountryBlock.geoIP != nil {
m.logger.Debug("Closing country block GeoIP database...")
if err := m.CountryBlock.geoIP.Close(); err != nil {
m.logger.Error("Error encountered while closing country block GeoIP database", zap.Error(err))
if !errorOccurred {
firstError = fmt.Errorf("error closing country block GeoIP: %w", err)
errorOccurred = true
}
if m.CountryBlacklist.geoIP != nil {
m.logger.Debug("Closing country blacklist GeoIP database...")
if err := m.CountryBlacklist.geoIP.Close(); err != nil {
m.logger.Error("Error encountered while closing country blacklist GeoIP database", zap.Error(err))
firstError = fmt.Errorf("error closing country blacklist GeoIP: %w", err)
} else {
m.logger.Debug("Country block GeoIP database closed successfully.")
m.logger.Debug("Country blacklist GeoIP database closed successfully.")
}
m.CountryBlock.geoIP = nil
m.CountryBlacklist.geoIP = nil
} else {
m.logger.Debug("Country block GeoIP database was not open, skipping close.")
m.logger.Debug("Country blacklist GeoIP database was not open, skipping close.")
}
if m.CountryWhitelist.geoIP != nil {
@@ -293,6 +336,19 @@ func (m *Middleware) Shutdown(ctx context.Context) error {
m.logger.Debug("Country whitelist GeoIP database was not open, skipping close.")
}
if m.BlockASNs.geoIP != nil {
m.logger.Debug("Closing ASN GeoIP database...")
if err := m.BlockASNs.geoIP.Close(); err != nil {
m.logger.Error("Error encountered while closing ASN GeoIP database", zap.Error(err))
if firstError == nil {
firstError = fmt.Errorf("error closing ASN GeoIP: %w", err)
}
} else {
m.logger.Debug("ASN GeoIP database closed successfully.")
}
m.BlockASNs.geoIP = nil
}
// Log rule hit statistics
m.logger.Info("Rule Hit Statistics:")
m.ruleHits.Range(func(key, value interface{}) bool {
@@ -302,15 +358,16 @@ func (m *Middleware) Shutdown(ctx context.Context) error {
return true
}
hitCount, ok := value.(HitCount)
atomicCounter, ok := value.(*atomic.Int64)
if !ok {
m.logger.Error("Invalid type for hit count in ruleHits map", zap.Any("value", value))
return true
}
hitCount := atomicCounter.Load()
m.logger.Info("Rule Hit",
zap.String("rule_id", string(ruleID)),
zap.Int("hits", int(hitCount)),
zap.Int64("hits", hitCount),
)
return true
})
@@ -400,8 +457,8 @@ func (m *Middleware) ReloadConfig() error {
m.logger.Info("Reloading WAF configuration")
if m.IPBlacklistFile != "" {
newIPBlacklist := NewCIDRTrie()
if err := m.loadIPBlacklist(m.IPBlacklistFile, newIPBlacklist); err != nil {
newIPBlacklist := iptrie.NewTrie()
if err := m.loadIPBlacklist(m.IPBlacklistFile, *newIPBlacklist); err != nil {
m.logger.Error("Failed to reload IP blacklist", zap.String("file", m.IPBlacklistFile), zap.Error(err))
return fmt.Errorf("failed to reload IP blacklist: %v", err)
}
@@ -426,7 +483,7 @@ func (m *Middleware) ReloadConfig() error {
return nil
}
func (m *Middleware) loadIPBlacklist(path string, blacklistMap *CIDRTrie) error {
func (m *Middleware) loadIPBlacklist(path string, blacklistMap iptrie.Trie) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
m.logger.Warn("Skipping IP blacklist load, file does not exist", zap.String("file", path))
return nil
@@ -440,7 +497,12 @@ func (m *Middleware) loadIPBlacklist(path string, blacklistMap *CIDRTrie) error
// Convert the map to CIDRTrie
for ip := range blacklist {
blacklistMap.Insert(ip)
prefix, err := netip.ParsePrefix(appendCIDR(ip))
if err != nil {
m.logger.Warn("Skipping invalid IP in blacklist", zap.String("ip", ip), zap.Error(err))
continue
}
blacklistMap.Insert(prefix, nil)
}
return nil
}
@@ -468,12 +530,13 @@ func (m *Middleware) getRuleHitStats() map[string]int {
m.logger.Error("Invalid type for rule ID in ruleHits map", zap.Any("key", key))
return true // Continue iteration
}
hitCount, ok := value.(HitCount)
// SOTA Pattern: Wait-Free stats collection
atomicCounter, ok := value.(*atomic.Int64)
if !ok {
m.logger.Error("Invalid type for hit count in ruleHits map", zap.Any("value", value))
return true // Continue iteration
}
stats[string(ruleID)] = int(hitCount)
stats[string(ruleID)] = int(atomicCounter.Load())
return true
})
return stats

View File

@@ -2,15 +2,14 @@ package caddywaf
import (
"context"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/stretchr/testify/assert"
"github.com/caddyserver/caddy/v2"
)
func TestMiddleware_Provision(t *testing.T) {
@@ -33,7 +32,7 @@ func TestMiddleware_Provision(t *testing.T) {
IPBlacklistFile: "testdata/ip_blacklist.txt",
DNSBlacklistFile: "testdata/dns_blacklist.txt",
AnomalyThreshold: 10,
CountryBlock: CountryAccessFilter{
CountryBlacklist: CountryAccessFilter{
Enabled: true,
CountryList: []string{"US"},
GeoIPDBPath: "testdata/GeoIP2-Country-Test.mmdb",

269
check_waf_config.py Normal file
View File

@@ -0,0 +1,269 @@
#!/usr/bin/env python3
import requests
import json
import sys
import re
import argparse
from termcolor import colored
def setup_args():
parser = argparse.ArgumentParser(description='Check WAF configuration for testing')
parser.add_argument('--url', default='http://localhost:8080', help='URL to test (default: http://localhost:8080)')
parser.add_argument('--config-endpoint', default='', help='Endpoint for accessing WAF configuration (if available)')
parser.add_argument('--rules-file', default='sample_rules.json', help='Path to rules file (default: sample_rules.json)')
return parser.parse_args()
def load_rules_from_file(file_path):
"""Load rules from a JSON file, handling comments if present."""
try:
# Read the file content
with open(file_path, 'r') as f:
content = f.read()
# Remove JavaScript-style comments if present
content = re.sub(r'//.*?\n', '\n', content) # Remove single-line comments
content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) # Remove multi-line comments
# Parse JSON
rules = json.loads(content)
print(colored(f"Loaded {len(rules)} rules from {file_path}", "green"))
return rules
except json.JSONDecodeError as e:
print(colored(f"Error parsing JSON from {file_path}: {str(e)}", "red"))
print(colored("Make sure the file is valid JSON. JavaScript-style comments are stripped automatically.", "yellow"))
return []
except Exception as e:
print(colored(f"Error loading rules from {file_path}: {str(e)}", "red"))
return []
def check_rule_coverage(rules, threshold=5):
"""Check if rules cover all test cases needed for anomaly threshold test."""
required_tests = {
"low_score_test": False,
"param1_score2": False,
"param2_score2": False,
"param1_score3": False,
"param2_score3": False,
"block_true": False,
"increment_score1": False,
"increment_score2": False,
"increment_score3": False
}
# Store rule scores for tests
rule_scores = {
"low_score_test": 0,
"param1_score2": 0,
"param2_score2": 0,
"param1_score3": 0,
"param2_score3": 0,
"increment_score1": 0,
"increment_score2": 0,
"increment_score3": 0
}
block_rule_mode = None
for rule in rules:
# Check for low score test rule
if 'targets' in rule and 'URL_PARAM:test' in rule['targets'] and 'pattern' in rule and 'low_score_test' in rule['pattern']:
required_tests["low_score_test"] = True
print(colored(f"✓ Found rule for test=low_score_test (ID: {rule.get('id', 'unknown')})", "green"))
if 'score' in rule:
rule_scores["low_score_test"] = rule.get('score', 0)
print(colored(f" Score: {rule['score']}", "yellow"))
# Check for param1 score2
if 'targets' in rule and 'URL_PARAM:param1' in rule['targets'] and 'pattern' in rule and 'score2' in rule['pattern']:
required_tests["param1_score2"] = True
print(colored(f"✓ Found rule for param1=score2 (ID: {rule.get('id', 'unknown')})", "green"))
if 'score' in rule:
rule_scores["param1_score2"] = rule.get('score', 0)
print(colored(f" Score: {rule['score']}", "yellow"))
# Check for param2 score2
if 'targets' in rule and 'URL_PARAM:param2' in rule['targets'] and 'pattern' in rule and 'score2' in rule['pattern']:
required_tests["param2_score2"] = True
print(colored(f"✓ Found rule for param2=score2 (ID: {rule.get('id', 'unknown')})", "green"))
if 'score' in rule:
rule_scores["param2_score2"] = rule.get('score', 0)
print(colored(f" Score: {rule['score']}", "yellow"))
# Check for param1 score3
if 'targets' in rule and 'URL_PARAM:param1' in rule['targets'] and 'pattern' in rule and 'score3' in rule['pattern']:
required_tests["param1_score3"] = True
print(colored(f"✓ Found rule for param1=score3 (ID: {rule.get('id', 'unknown')})", "green"))
if 'score' in rule:
rule_scores["param1_score3"] = rule.get('score', 0)
print(colored(f" Score: {rule['score']}", "yellow"))
# Check for param2 score3
if 'targets' in rule and 'URL_PARAM:param2' in rule['targets'] and 'pattern' in rule and 'score3' in rule['pattern']:
required_tests["param2_score3"] = True
print(colored(f"✓ Found rule for param2=score3 (ID: {rule.get('id', 'unknown')})", "green"))
if 'score' in rule:
rule_scores["param2_score3"] = rule.get('score', 0)
print(colored(f" Score: {rule['score']}", "yellow"))
# Check for block action
if 'targets' in rule and 'URL_PARAM:block' in rule['targets'] and 'pattern' in rule and 'true' in rule['pattern']:
required_tests["block_true"] = True
block_rule_mode = rule.get('mode', 'unknown')
print(colored(f"✓ Found rule for block=true (ID: {rule.get('id', 'unknown')})", "green"))
print(colored(f" Action: {block_rule_mode}", "yellow"))
if block_rule_mode != 'block':
print(colored(" WARNING: This rule should have mode='block'", "red"))
# Check for increment score rules
if 'targets' in rule and 'URL_PARAM:increment' in rule['targets']:
if 'pattern' in rule and 'score1' in rule['pattern']:
required_tests["increment_score1"] = True
rule_scores["increment_score1"] = rule.get('score', 0)
print(colored(f"✓ Found rule for increment=score1 (ID: {rule.get('id', 'unknown')})", "green"))
if 'score' in rule:
print(colored(f" Score: {rule['score']}", "yellow"))
if 'pattern' in rule and 'score2' in rule['pattern']:
required_tests["increment_score2"] = True
rule_scores["increment_score2"] = rule.get('score', 0)
print(colored(f"✓ Found rule for increment=score2 (ID: {rule.get('id', 'unknown')})", "green"))
if 'score' in rule:
print(colored(f" Score: {rule['score']}", "yellow"))
if 'pattern' in rule and 'score3' in rule['pattern']:
required_tests["increment_score3"] = True
rule_scores["increment_score3"] = rule.get('score', 0)
print(colored(f"✓ Found rule for increment=score3 (ID: {rule.get('id', 'unknown')})", "green"))
if 'score' in rule:
print(colored(f" Score: {rule['score']}", "yellow"))
# Check test coverage
missing_tests = [test.replace('_', '=') for test, found in required_tests.items() if not found]
if missing_tests:
print(colored(f"\n⚠ Missing rules for: {', '.join(missing_tests)}", "red"))
else:
print(colored("\n✓ All required test rules are present!", "green"))
# Validate expected scores for key test combinations
print(colored("\nCalculated Scores for Key Test Combinations:", "cyan"))
# Test 2: Below threshold
test2_score = rule_scores["param1_score2"] + rule_scores["param2_score2"]
test2_should_block = test2_score >= threshold
if required_tests["param1_score2"] and required_tests["param2_score2"]:
print(colored(f"Test 2 - param1=score2&param2=score2: Score = {test2_score}", "yellow"))
print(colored(f" Threshold: {threshold}, Should Block: {'Yes' if test2_should_block else 'No'}",
"red" if test2_should_block else "green"))
if test2_should_block:
print(colored(" WARNING: This test should pass (not block) but the score may trigger blocking", "red"))
else:
print(colored("Test 2 - param1=score2&param2=score2: Cannot calculate - missing rules", "red"))
# Test 3: Exceeds threshold
test3_score = rule_scores["param1_score3"] + rule_scores["param2_score3"]
test3_should_block = test3_score >= threshold
if required_tests["param1_score3"] and required_tests["param2_score3"]:
print(colored(f"Test 3 - param1=score3&param2=score3: Score = {test3_score}", "yellow"))
print(colored(f" Threshold: {threshold}, Should Block: {'Yes' if test3_should_block else 'No'}",
"green" if test3_should_block else "red"))
if not test3_should_block:
print(colored(" WARNING: This test should be blocked but the score is below threshold", "red"))
else:
print(colored("Test 3 - param1=score3&param2=score3: Cannot calculate - missing rules", "red"))
# Test 4: Block action
if required_tests["block_true"]:
block_should_work = block_rule_mode == 'block'
print(colored(f"Test 4 - block=true: Mode = {block_rule_mode}", "yellow"))
print(colored(f" Should Block: {'Yes' if block_should_work else 'No'}",
"green" if block_should_work else "red"))
if not block_should_work:
print(colored(" WARNING: This rule should have mode='block' to properly test blocking", "red"))
else:
print(colored("Test 4 - block=true: Cannot evaluate - missing rule", "red"))
return required_tests, missing_tests, {
"test2_score": test2_score if required_tests["param1_score2"] and required_tests["param2_score2"] else None,
"test3_score": test3_score if required_tests["param1_score3"] and required_tests["param2_score3"] else None,
"test2_should_block": test2_should_block if required_tests["param1_score2"] and required_tests["param2_score2"] else None,
"test3_should_block": test3_should_block if required_tests["param1_score3"] and required_tests["param2_score3"] else None,
"block_should_work": block_rule_mode == 'block' if required_tests["block_true"] else None
}
def check_waf_active(url):
"""Check if the WAF is active by attempting to trigger a basic rule."""
block_payload = {'block': 'true'}
try:
print(colored(f"\nSending test request to {url} with block=true", "blue"))
response = requests.get(url, params=block_payload, timeout=5)
if response.status_code == 403:
print(colored("✓ WAF appears to be active (blocked request as expected)", "green"))
return True
else:
print(colored(f"⚠ WAF might not be active - received status {response.status_code} instead of 403", "red"))
print(colored("Check your WAF configuration and make sure blocking is enabled", "yellow"))
return False
except requests.exceptions.RequestException as e:
print(colored(f"Error checking WAF: {str(e)}", "red"))
return False
def main():
args = setup_args()
base_url = args.url
rules_file = args.rules_file
print(colored("WAF Configuration Checker", "cyan"))
print(colored(f"Target URL: {base_url}", "yellow"))
print(colored(f"Rules file: {rules_file}", "yellow"))
# Check server connectivity
try:
response = requests.get(base_url, timeout=2)
print(colored(f"✓ Server is reachable at {base_url}", "green"))
except requests.exceptions.RequestException:
print(colored(f"⚠ Cannot reach server at {base_url}", "red"))
print(colored("Make sure Caddy is running with your WAF configuration.", "yellow"))
sys.exit(1)
# Load and check rules
rules = load_rules_from_file(rules_file)
if rules:
required_tests, missing_tests, test_scores = check_rule_coverage(rules)
print(colored("\nExpected Test Results Based on Rules:", "cyan"))
if test_scores["test2_should_block"] is not None:
status = "FAIL (should block)" if test_scores["test2_should_block"] else "PASS (should allow)"
color = "red" if test_scores["test2_should_block"] else "green"
print(colored(f"Test 2 (Below threshold): {status}", color))
if test_scores["test3_should_block"] is not None:
status = "PASS (should block)" if test_scores["test3_should_block"] else "FAIL (should allow)"
color = "green" if test_scores["test3_should_block"] else "red"
print(colored(f"Test 3 (Exceed threshold): {status}", color))
if test_scores["block_should_work"] is not None:
status = "PASS (should block)" if test_scores["block_should_work"] else "FAIL (won't block)"
color = "green" if test_scores["block_should_work"] else "red"
print(colored(f"Test 4 (Block action): {status}", color))
# Only check WAF if we have the necessary rules
if required_tests["block_true"]:
print(colored("\nVerifying WAF is active...", "cyan"))
check_waf_active(base_url)
# Provide recommendations
if missing_tests:
print(colored("\nRecommendations:", "cyan"))
print(colored("Add the missing rules to your configuration to run all tests successfully.", "yellow"))
print(colored("\nConfiguration check complete.", "cyan"))
else:
print(colored("\nCould not load rules for verification.", "red"))
if __name__ == "__main__":
main()

21
common_test.go Normal file
View File

@@ -0,0 +1,21 @@
package caddywaf
import "net/http"
const (
geoIPdata = "GeoLite2-Country.mmdb"
localIP = "127.0.0.1:32555"
aliCNIP = "47.88.198.38"
googleUSIP = "74.125.131.105"
googleBRIP = "128.201.228.12"
googleRUIP = "74.125.131.94"
testURL = "http://example.com"
torListURL = "https://cdn.nws.neurodyne.pro/nws-cdn-ut8hw561/waf/torbulkexitlist" // custom TOR list URL for testing
)
var customResponse = map[int]CustomBlockResponse{
403: {
StatusCode: http.StatusForbidden,
Body: "Access Denied",
},
}

View File

@@ -7,8 +7,9 @@ import (
"strings"
"time"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
// ConfigLoader structure to encapsulate loading and parsing logic
@@ -139,11 +140,12 @@ func (cl *ConfigLoader) UnmarshalCaddyfile(d *caddyfile.Dispenser, m *Middleware
m.LogSeverity = "info"
m.LogJSON = false
m.AnomalyThreshold = 5
m.CountryBlock.Enabled = false
m.CountryBlacklist.Enabled = false
m.CountryWhitelist.Enabled = false
m.LogFilePath = "debug.json"
m.RedactSensitiveData = false
m.LogBuffer = 1000
m.BlockASNs.Enabled = false // Default to false
directiveHandlers := map[string]func(d *caddyfile.Dispenser, m *Middleware) error{
"metrics_endpoint": cl.parseMetricsEndpoint,
@@ -151,6 +153,7 @@ func (cl *ConfigLoader) UnmarshalCaddyfile(d *caddyfile.Dispenser, m *Middleware
"rate_limit": cl.parseRateLimit,
"block_countries": cl.parseCountryBlockDirective(true), // Use directive-specific helper
"whitelist_countries": cl.parseCountryBlockDirective(false), // Use directive-specific helper
"block_asns": cl.parseBlockASNsDirective, // Add ASN block directive
"log_severity": cl.parseLogSeverity,
"log_json": cl.parseLogJSON,
"rule_file": cl.parseRuleFile,
@@ -268,7 +271,7 @@ func (cl *ConfigLoader) parseCustomResponse(d *caddyfile.Dispenser, m *Middlewar
// parseCountryBlockDirective returns a closure to handle block_countries and whitelist_countries directives.
func (cl *ConfigLoader) parseCountryBlockDirective(isBlock bool) func(d *caddyfile.Dispenser, m *Middleware) error {
return func(d *caddyfile.Dispenser, m *Middleware) error {
target := &m.CountryBlock
target := &m.CountryBlacklist
directiveName := "block_countries"
if !isBlock {
target = &m.CountryWhitelist
@@ -299,6 +302,31 @@ func (cl *ConfigLoader) parseCountryBlockDirective(isBlock bool) func(d *caddyfi
}
}
// parseBlockASNsDirective handles the block_asns directive
func (cl *ConfigLoader) parseBlockASNsDirective(d *caddyfile.Dispenser, m *Middleware) error {
target := &m.BlockASNs
target.Enabled = true
if !d.NextArg() {
return d.ArgErr()
}
target.GeoIPDBPath = d.Val()
target.BlockedASNs = []string{}
for d.NextArg() {
asn := d.Val()
target.BlockedASNs = append(target.BlockedASNs, asn)
}
cl.logger.Debug("ASN block list configured",
zap.Strings("asns", target.BlockedASNs),
zap.String("geoip_db_path", target.GeoIPDBPath),
zap.String("file", d.File()),
zap.Int("line", d.Line()),
)
return nil
}
func (cl *ConfigLoader) parseLogSeverity(d *caddyfile.Dispenser, m *Middleware) error {
if !d.NextArg() {
return d.ArgErr()

View File

@@ -2,15 +2,14 @@
package caddywaf
import (
"path/filepath"
"os"
"path/filepath"
"testing"
"time"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestNewConfigLoader(t *testing.T) {
@@ -202,14 +201,14 @@ func TestParseCountryBlock(t *testing.T) {
t.Fatalf("parseCountryBlockDirective failed: %v", err)
}
if !m.CountryBlock.Enabled {
t.Errorf("Expected country block to be enabled, got %v", m.CountryBlock.Enabled)
if !m.CountryBlacklist.Enabled {
t.Errorf("Expected country blacklist to be enabled, got %v", m.CountryBlacklist.Enabled)
}
if m.CountryBlock.GeoIPDBPath != "/etc/geoip/GeoIP.dat" {
t.Errorf("Expected GeoIP DB path to be '/etc/geoip/GeoIP.dat', got '%s'", m.CountryBlock.GeoIPDBPath)
if m.CountryBlacklist.GeoIPDBPath != "/etc/geoip/GeoIP.dat" {
t.Errorf("Expected GeoIP DB path to be '/etc/geoip/GeoIP.dat', got '%s'", m.CountryBlacklist.GeoIPDBPath)
}
if len(m.CountryBlock.CountryList) != 2 || m.CountryBlock.CountryList[0] != "US" || m.CountryBlock.CountryList[1] != "CA" {
t.Errorf("Expected country list to be ['US', 'CA'], got %v", m.CountryBlock.CountryList)
if len(m.CountryBlacklist.CountryList) != 2 || m.CountryBlacklist.CountryList[0] != "US" || m.CountryBlacklist.CountryList[1] != "CA" {
t.Errorf("Expected country list to be ['US', 'CA'], got %v", m.CountryBlacklist.CountryList)
}
}

143
debug_test_results.py Normal file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
import requests
import json
import sys
import argparse
from termcolor import colored
def setup_args():
parser = argparse.ArgumentParser(description='Debug WAF test result evaluation')
parser.add_argument('--url', default='http://localhost:8080', help='URL to test (default: http://localhost:8080)')
parser.add_argument('--detailed', action='store_true', help='Show detailed request/response information')
return parser.parse_args()
def debug_response_evaluation(url, test_name, payload, expected_status):
"""Send a request and debug the response evaluation logic."""
print(colored(f"\n=== Debugging {test_name} ===", "cyan"))
print(colored(f"URL: {url}", "yellow"))
print(colored(f"Payload: {payload}", "yellow"))
print(colored(f"Expected status: {expected_status}", "yellow"))
try:
# Send the request
print(colored("\nSending request...", "blue"))
response = requests.get(
url,
params=payload,
headers={'User-Agent': 'WAF-Threshold-Test-Debug/1.0'},
timeout=5
)
# Get the status code
status = response.status_code
print(colored(f"Received status code: {status}", "green"))
# Check if it matches expected
match = status == expected_status
match_str = "✓ MATCH" if match else "✗ MISMATCH"
match_color = "green" if match else "red"
print(colored(f"Status evaluation: {match_str}", match_color))
# Show response details
print(colored("\nResponse details:", "cyan"))
print(colored(f"Status code: {status}", "yellow"))
print(colored(f"Response body: {response.text[:100]}...", "yellow") if len(response.text) > 100 else colored(f"Response body: {response.text}", "yellow"))
# Show evaluation details
print(colored("\nEvaluation details:", "cyan"))
print(colored(f"Python expression: response.status_code == {expected_status}", "yellow"))
print(colored(f"Evaluation result: {response.status_code} == {expected_status} = {response.status_code == expected_status}", "yellow"))
# Boolean check
bool_result = bool(response and response.status_code == expected_status)
print(colored(f"Boolean check: bool(response and response.status_code == {expected_status}) = {bool_result}", "yellow"))
# Return result for summary
return {
"test_name": test_name,
"expected": expected_status,
"actual": status,
"match": match,
"bool_check": bool_result
}
except requests.exceptions.RequestException as e:
print(colored(f"Error sending request: {str(e)}", "red"))
return {
"test_name": test_name,
"error": str(e),
"match": False,
"bool_check": False
}
def run_all_tests(url):
"""Run all the tests from the anomaly threshold test script and debug the results."""
print(colored("Running all tests and debugging evaluation logic...", "cyan"))
# Define all test cases
test_cases = [
{"name": "Test 1 (Low score)", "payload": {"test": "low_score_test"}, "expected": 200},
{"name": "Test 2 (Below threshold)", "payload": {"param1": "score2", "param2": "score2"}, "expected": 200},
{"name": "Test 3 (Exceed threshold)", "payload": {"param1": "score3", "param2": "score3"}, "expected": 403},
{"name": "Test 4 (Block action)", "payload": {"block": "true"}, "expected": 403},
{"name": "Test 5a (Increment 1)", "payload": {"increment": "score1"}, "expected": 200},
{"name": "Test 5b (Increment 2)", "payload": {"increment": "score2"}, "expected": 200},
{"name": "Test 5c (Increment 3)", "payload": {"increment": "score3"}, "expected": 200},
]
# Run each test
results = []
for test in test_cases:
result = debug_response_evaluation(url, test["name"], test["payload"], test["expected"])
results.append(result)
# Show summary
print(colored("\n=== Test Evaluation Summary ===", "cyan"))
for result in results:
if "error" in result:
print(colored(f"{result['test_name']}: Error - {result['error']}", "red"))
else:
status = "PASS" if result["match"] else "FAIL"
color = "green" if result["match"] else "red"
print(colored(f"{result['test_name']}: {status} (Expected: {result['expected']}, Actual: {result['actual']})", color))
print(colored(f" Boolean evaluation: {result['bool_check']}", "yellow"))
# Check for any issues with Tests 3 and 4
test3 = next((r for r in results if r["test_name"] == "Test 3 (Exceed threshold)"), None)
test4 = next((r for r in results if r["test_name"] == "Test 4 (Block action)"), None)
if test3 and test4:
if test3["match"] and not test3["bool_check"]:
print(colored("\nISSUE DETECTED: Test 3 status matches but boolean evaluation fails!", "red"))
print(colored("This explains why the test incorrectly shows as failed.", "red"))
if test4["match"] and not test4["bool_check"]:
print(colored("\nISSUE DETECTED: Test 4 status matches but boolean evaluation fails!", "red"))
print(colored("This explains why the test incorrectly shows as failed.", "red"))
def main():
args = setup_args()
url = args.url
detailed = args.detailed
print(colored("WAF Test Result Debugging Tool", "cyan"))
print(colored(f"Target: {url}", "yellow"))
# Check server connectivity
try:
response = requests.get(url, timeout=2)
print(colored(f"Server is reachable at {url}", "green"))
# Run all tests
run_all_tests(url)
except requests.exceptions.RequestException:
print(colored(f"ERROR: Cannot reach server at {url}", "red"))
print(colored("Make sure Caddy is running with your WAF configuration.", "yellow"))
sys.exit(1)
print(colored("\nDebugging complete.", "cyan"))
if __name__ == "__main__":
main()

93
debug_waf.go Normal file
View File

@@ -0,0 +1,93 @@
package caddywaf
import (
"fmt"
"net/http"
"os"
"strings"
"sync/atomic"
"time"
"go.uber.org/zap"
)
// DebugRequest logs detailed information about a request for debugging
func (m *Middleware) DebugRequest(r *http.Request, state *WAFState, msg string) {
if m.LogSeverity != "debug" {
return
}
var ruleIDs []string
var scores []string
// Log all matched rules and their scores
m.ruleHits.Range(func(key, value interface{}) bool {
ruleID, ok := key.(RuleID)
if !ok {
return true
}
atomicCounter, ok := value.(*atomic.Int64)
if !ok {
return true
}
hitCount := atomicCounter.Load()
ruleIDs = append(ruleIDs, string(ruleID))
scores = append(scores, fmt.Sprintf("%s:%d", string(ruleID), hitCount))
return true
})
// Create a detailed debug log
m.logger.Debug(fmt.Sprintf("WAF DEBUG: %s", msg),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
zap.String("remote_addr", r.RemoteAddr),
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("query", r.URL.RawQuery),
zap.Int("total_score", state.TotalScore),
zap.Int("anomaly_threshold", m.AnomalyThreshold),
zap.Bool("blocked", state.Blocked),
zap.Int("status_code", state.StatusCode),
zap.Bool("response_written", state.ResponseWritten),
zap.String("matched_rules", strings.Join(ruleIDs, ",")),
zap.String("rule_scores", strings.Join(scores, ",")),
)
}
// DumpRulesToFile dumps the loaded rules to a file for inspection
func (m *Middleware) DumpRulesToFile(path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString("=== WAF Rules Dump ===\n\n"); err != nil {
return err
}
for phase := 1; phase <= 4; phase++ {
fmt.Fprintf(f, "== Phase %d Rules ==\n", phase)
rules, ok := m.Rules[phase]
if !ok || len(rules) == 0 {
if _, err := f.WriteString(" No rules for this phase\n\n"); err != nil {
return err
}
continue
}
for i, rule := range rules {
fmt.Fprintf(f, " Rule %d:\n", i+1)
fmt.Fprintf(f, " ID: %s\n", rule.ID)
fmt.Fprintf(f, " Pattern: %s\n", rule.Pattern)
fmt.Fprintf(f, " Targets: %v\n", rule.Targets)
fmt.Fprintf(f, " Score: %d\n", rule.Score)
fmt.Fprintf(f, " Action: %s\n", rule.Action)
fmt.Fprintf(f, " Description: %s\n", rule.Description)
if _, err := f.WriteString("\n"); err != nil {
return err
}
}
}
return nil
}

208
debug_waf.py Normal file
View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python3
import requests
import json
import sys
import argparse
from termcolor import colored
def setup_args():
parser = argparse.ArgumentParser(description='Debug WAF configuration via Caddy Admin API')
parser.add_argument('--admin-api', default='http://localhost:2019', help='Caddy Admin API URL (default: http://localhost:2019)')
parser.add_argument('--config-path', default='/config/', help='Config path in the API (default: /config/)')
parser.add_argument('--output', default='waf_config.json', help='Output file for configuration (default: waf_config.json)')
parser.add_argument('--pretty', action='store_true', help='Pretty-print JSON output')
parser.add_argument('--test-rules', action='store_true', help='Test WAF rules with sample requests')
parser.add_argument('--target-url', default='http://localhost:8080', help='Target URL for rule testing (default: http://localhost:8080)')
return parser.parse_args()
def get_caddy_config(admin_url, config_path):
"""Get the current Caddy configuration from the Admin API."""
try:
response = requests.get(f"{admin_url}{config_path}", timeout=5)
if response.status_code == 200:
return response.json()
else:
print(colored(f"Error fetching config: Status {response.status_code}", "red"))
return None
except requests.exceptions.RequestException as e:
print(colored(f"Error connecting to Caddy Admin API: {str(e)}", "red"))
return None
def extract_waf_config(config):
"""Extract WAF-related configuration from the Caddy config."""
if not config:
return None
waf_config = {"routes": [], "handlers": [], "thresholds": []}
# Try to find WAF configuration in apps.http.servers
if 'apps' in config and 'http' in config['apps'] and 'servers' in config['apps']['http']:
for server_name, server in config['apps']['http']['servers'].items():
print(colored(f"Examining server: {server_name}", "cyan"))
if 'routes' in server:
for route in server['routes']:
# Check for WAF in route handlers
if 'handle' in route:
for handler in route['handle']:
if handler.get('handler') == 'waf':
print(colored("Found WAF handler in route", "green"))
waf_config['routes'].append(route)
waf_config['handlers'].append(handler)
# Check for threshold
if 'anomaly_threshold' in handler:
print(colored(f"Found anomaly threshold: {handler['anomaly_threshold']}", "green"))
waf_config['thresholds'].append(handler['anomaly_threshold'])
if not waf_config['handlers']:
print(colored("No WAF handlers found in the configuration", "yellow"))
return waf_config
def save_config(config, file_path, pretty=False):
"""Save the configuration to a file."""
try:
with open(file_path, 'w') as f:
if pretty:
json.dump(config, f, indent=2)
else:
json.dump(config, f)
print(colored(f"Configuration saved to {file_path}", "green"))
except Exception as e:
print(colored(f"Error saving configuration: {str(e)}", "red"))
def test_waf_rules(target_url, waf_config):
"""Test WAF rules with sample requests to verify behavior."""
print(colored("\nTesting WAF rules with sample requests...", "cyan"))
# Check if we have any anomaly thresholds
thresholds = waf_config.get('thresholds', [])
threshold = thresholds[0] if thresholds else 5
print(colored(f"Using anomaly threshold: {threshold}", "yellow"))
# Test cases
test_cases = [
{"name": "Low Score Test", "payload": {"test": "low_score_test"}, "expected_status": 200},
{"name": "Below Threshold Test", "payload": {"param1": "score2", "param2": "score2"}, "expected_status": 200},
{"name": "Exceed Threshold Test", "payload": {"param1": "score3", "param2": "score3"}, "expected_status": 403},
{"name": "Block Action Test", "payload": {"block": "true"}, "expected_status": 403},
]
results = []
for test_case in test_cases:
print(colored(f"\nRunning test: {test_case['name']}", "cyan"))
print(colored(f"Payload: {test_case['payload']}", "yellow"))
print(colored(f"Expected status: {test_case['expected_status']}", "yellow"))
try:
response = requests.get(
target_url,
params=test_case['payload'],
headers={'User-Agent': 'WAF-Debug-Tool/1.0'},
timeout=5
)
status = response.status_code
matched = status == test_case['expected_status']
color = "green" if matched else "red"
print(colored(f"Actual status: {status} - {'✓ MATCH' if matched else '✗ MISMATCH'}", color))
print(colored(f"Response: {response.text[:100]}...", "yellow") if len(response.text) > 100 else colored(f"Response: {response.text}", "yellow"))
# Store result
results.append({
"name": test_case['name'],
"expected": test_case['expected_status'],
"actual": status,
"matched": matched
})
except requests.exceptions.RequestException as e:
print(colored(f"Error sending request: {str(e)}", "red"))
results.append({
"name": test_case['name'],
"error": str(e),
"matched": False
})
# Summary
print(colored("\nTest Results Summary:", "cyan"))
passes = sum(1 for r in results if r.get('matched', False))
failures = len(results) - passes
print(colored(f"Total Tests: {len(results)}", "yellow"))
print(colored(f"Passes: {passes}", "green"))
print(colored(f"Failures: {failures}", "red" if failures > 0 else "green"))
# Detailed results
print(colored("\nDetailed Results:", "cyan"))
for result in results:
status = "PASS" if result.get('matched', False) else "FAIL"
color = "green" if result.get('matched', False) else "red"
if 'error' in result:
print(colored(f"{result['name']}: {status} - Error: {result['error']}", color))
else:
print(colored(f"{result['name']}: {status} - Expected: {result['expected']}, Actual: {result['actual']}", color))
return results
def main():
args = setup_args()
admin_url = args.admin_api
config_path = args.config_path
output_file = args.output
pretty = args.pretty
test_rules = args.test_rules
target_url = args.target_url
print(colored("WAF Debug Tool", "cyan"))
print(colored(f"Caddy Admin API: {admin_url}", "yellow"))
# Get the current configuration
print(colored("\nFetching Caddy configuration...", "cyan"))
config = get_caddy_config(admin_url, config_path)
if config:
print(colored("Configuration retrieved successfully", "green"))
# Extract WAF configuration
print(colored("\nExtracting WAF configuration...", "cyan"))
waf_config = extract_waf_config(config)
if waf_config and waf_config['handlers']:
# Summary of WAF configuration
print(colored("\nWAF Configuration Summary:", "cyan"))
print(colored(f"WAF Handlers: {len(waf_config['handlers'])}", "yellow"))
for i, handler in enumerate(waf_config['handlers']):
print(colored(f"\nHandler {i+1}:", "yellow"))
if 'anomaly_threshold' in handler:
print(colored(f" Anomaly Threshold: {handler['anomaly_threshold']}", "green"))
else:
print(colored(" No anomaly threshold specified", "red"))
if 'rules' in handler:
print(colored(f" Rules: {len(handler['rules']) if isinstance(handler['rules'], list) else 'From file'}", "green"))
else:
print(colored(" No rules specified", "red"))
if 'rules_file' in handler:
print(colored(f" Rules File: {handler['rules_file']}", "green"))
# Test rules if requested
if test_rules:
test_waf_rules(target_url, waf_config)
# Save the WAF configuration
print(colored(f"\nSaving WAF configuration to {output_file}...", "cyan"))
save_config(waf_config, output_file, pretty)
else:
print(colored("No WAF configuration found", "red"))
print(colored("\nDebug complete.", "cyan"))
if __name__ == "__main__":
main()

30
doc.go Normal file
View File

@@ -0,0 +1,30 @@
// Package caddywaf provides Web Application Firewall (WAF) functionality as a Caddy module.
//
// Module ID: http.handlers.waf
// Module type: HTTP handler middleware
//
// This module implements comprehensive web security features including:
// - Regex-based request filtering
// - IP and DNS blacklisting
// - Geographic access control
// - Rate limiting with configurable windows
// - Anomaly detection and scoring
// - Multi-phase request inspection
// - Real-time metrics and monitoring
// - Custom response handling
// - Dynamic configuration reloading
//
// Installation:
//
// xcaddy build --with github.com/fabriziosalmi/caddy-waf
//
// Basic usage in Caddyfile:
//
// waf {
// rule_file rules.json
// ip_blacklist_file blacklist.txt
// metrics_endpoint /waf_metrics
// }
//
// For complete documentation, see: https://github.com/fabriziosalmi/caddy-waf
package caddywaf

19
docker-compose.yml Normal file
View File

@@ -0,0 +1,19 @@
services:
caddy-waf:
build: .
ports:
- "8080:8080"
volumes:
- ./Caddyfile:/app/Caddyfile:ro
- ./rules.json:/app/rules.json:ro
- ./ip_blacklist.txt:/app/ip_blacklist.txt:ro
- ./dns_blacklist.txt:/app/dns_blacklist.txt:ro
restart: unless-stopped
environment:
- CADDY_ADMIN=0.0.0.0:2019
networks:
- caddy-waf-net
networks:
caddy-waf-net:
driver: bridge

View File

@@ -140,6 +140,9 @@ The WAF provides a variety of configuration options to control its behavior. The
| **`log_path`** | Specifies the path for the WAF log file. | `log_path /var/log/waf/access.log` |
| **`redact_sensitive_data`** | Redacts sensitive data from the request query string in logs. | `redact_sensitive_data` |
| **`custom_response`** | Defines custom HTTP responses for blocked requests. Requires status code, content type, and response content or file path. | `custom_response 403 application/json error.json` |
| **`max_request_body_size`**| Configures request body size limit (default 10MB). Uses `io.LimitReader` for protection. | `max_request_body_size 20MB` |
| **`block_asns`** | Blocks requests from specified Autonomous Systems (ASNs) using the MaxMind GeoIP2 ASN database. | `block_asns GeoLite2-ASN.mmdb 12345 67890` |
| **`geoip_fail_open`** | Configures the WAF to allow requests if GeoIP/ASN lookup fails (Circuit Breaker pattern). Default is false (Fail Closed). | `geoip_fail_open` |
---

View File

@@ -4,6 +4,26 @@
* Download the `GeoLite2-Country.mmdb` file (see [Installation](#-installation)).
* Use `block_countries` or `whitelist_countries` with ISO country codes:
## Priorities
`Whitelisting` has a **higher** priority than `Blacklisting`.
### Config Example
Whitelist: BR <br>
Blacklist: US, UK <br>
Q: Which is THE priority ? <br>
A: BR IPs are allowed, all others are **blocked**
## Global blocking priorities
- IP blacklist
- DNS blacklist
- Rate limit
- Whitelist
- Blacklist
## Config example
```caddyfile
# Block requests from Russia, China, and North Korea
block_countries /path/to/GeoLite2-Country.mmdb RU CN KP

View File

@@ -3,6 +3,7 @@ package caddywaf
import (
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
@@ -63,11 +64,8 @@ func (gh *GeoIPHandler) IsCountryInList(remoteAddr string, countryList []string,
return false, fmt.Errorf("geoip database not loaded")
}
ip, err := gh.extractIPFromRemoteAddr(remoteAddr)
if err != nil {
gh.logger.Debug("Failed to extract IP from remote address", zap.String("remote_addr", remoteAddr), zap.Error(err))
return false, err
}
// Extract IP address without port
ip := extractIP(remoteAddr)
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
@@ -85,10 +83,10 @@ func (gh *GeoIPHandler) GetCountryCode(remoteAddr string, geoIP *maxminddb.Reade
return "N/A"
}
ip, err := gh.extractIPFromRemoteAddr(remoteAddr)
ip, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
gh.logger.Debug("Failed to extract IP from remote address for GetCountryCode", zap.String("remote_addr", remoteAddr), zap.Error(err))
return "N/A"
// fallback to input IP
ip = remoteAddr
}
parsedIP := net.ParseIP(ip)
@@ -101,7 +99,6 @@ func (gh *GeoIPHandler) GetCountryCode(remoteAddr string, geoIP *maxminddb.Reade
}
func (gh *GeoIPHandler) isCountryInListWithCache(ip string, parsedIP net.IP, countryList []string, geoIP *maxminddb.Reader) (bool, error) {
// Check cache first
if gh.geoIPCache != nil {
gh.geoIPCacheMutex.RLock()
@@ -127,7 +124,6 @@ func (gh *GeoIPHandler) isCountryInListWithCache(ip string, parsedIP net.IP, cou
}
func (gh *GeoIPHandler) getCountryCodeWithCache(ip string, parsedIP net.IP, geoIP *maxminddb.Reader) string {
// Check cache first for GetCountryCode as well for consistency and potential perf gain
if gh.geoIPCache != nil {
gh.geoIPCacheMutex.RLock()
@@ -153,20 +149,6 @@ func (gh *GeoIPHandler) getCountryCodeWithCache(ip string, parsedIP net.IP, geoI
return record.Country.ISOCode
}
// extractIPFromRemoteAddr extracts the ip from remote address
func (gh *GeoIPHandler) extractIPFromRemoteAddr(remoteAddr string) (string, error) {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
// If it's not in host:port format, assume it's just the IP
ip := net.ParseIP(remoteAddr)
if ip == nil {
return "", fmt.Errorf("invalid IP format: %s", remoteAddr)
}
return remoteAddr, nil
}
return host, nil
}
// Helper function to check if the country in the record is in the country list
func (gh *GeoIPHandler) isCountryInRecord(record GeoIPRecord, countryList []string) bool {
for _, country := range countryList {
@@ -216,3 +198,56 @@ func (gh *GeoIPHandler) cacheGeoIPRecord(ip string, record GeoIPRecord) {
})
}
}
// IsASNInList checks if an IP belongs to a list of blocked ASNs
func (gh *GeoIPHandler) IsASNInList(remoteAddr string, blockedASNs []string, geoIP *maxminddb.Reader) (bool, error) {
if geoIP == nil {
return false, fmt.Errorf("geoip database not loaded")
}
// Extract IP address without port
ip := extractIP(remoteAddr)
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
gh.logger.Debug("Invalid IP address for ASN lookup", zap.String("ip", ip))
return false, fmt.Errorf("invalid IP address: %s", ip)
}
var record ASNRecord
err := geoIP.Lookup(parsedIP, &record)
if err != nil {
gh.logger.Error("GeoIP ASN lookup failed", zap.String("ip", ip), zap.Error(err))
return false, fmt.Errorf("geoip lookup failed: %w", err)
}
asnStr := strconv.FormatUint(uint64(record.AutonomousSystemNumber), 10)
for _, blockedASN := range blockedASNs {
if asnStr == blockedASN {
return true, nil
}
}
return false, nil
}
// GetASN extracts the ASN for logging purposes
func (gh *GeoIPHandler) GetASN(remoteAddr string, geoIP *maxminddb.Reader) string {
if geoIP == nil {
return "N/A"
}
ipConf, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
ipConf = remoteAddr
}
parsedIP := net.ParseIP(ipConf)
if parsedIP == nil {
return "N/A"
}
var record ASNRecord
err = geoIP.Lookup(parsedIP, &record)
if err != nil {
return "N/A"
}
return fmt.Sprintf("AS%d %s", record.AutonomousSystemNumber, record.AutonomousSystemOrganization)
}

View File

@@ -66,49 +66,6 @@ func TestLoadGeoIPDatabase(t *testing.T) {
}
}
func TestExtractIPFromRemoteAddr(t *testing.T) {
handler := NewGeoIPHandler(nil)
tests := []struct {
name string
remoteAddr string
want string
wantErr bool
}{
{
name: "Valid IP and port",
remoteAddr: "192.168.1.1:8080",
want: "192.168.1.1",
wantErr: false,
},
{
name: "Valid IP only",
remoteAddr: "192.168.1.1",
want: "192.168.1.1",
wantErr: false,
},
{
name: "Invalid IP",
remoteAddr: "invalid-ip",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := handler.extractIPFromRemoteAddr(tt.remoteAddr)
if (err != nil) != tt.wantErr {
t.Errorf("extractIPFromRemoteAddr() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("extractIPFromRemoteAddr() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsCountryInList(t *testing.T) {
handler := NewGeoIPHandler(nil)

View File

@@ -13,107 +13,49 @@ blocklist_sources = {
"Greensnow": "https://blocklist.greensnow.co/greensnow.txt",
}
# --- Tor Exit Node Source (Testing) ---
tor_exit_nodes_url = "https://check.torproject.org/exit-addresses" # Testing
# Tor Exit Node Source
tor_exit_nodes_url = "https://check.torproject.org/exit-addresses"
def extract_ips(source_name, url):
"""Fetches data from the given URL and extracts IP addresses."""
"""Fetches data from the given URL and extracts IP addresses in CIDR format."""
ips = set()
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
response.raise_for_status()
content = response.text
except requests.exceptions.RequestException as e:
print(f"Error fetching {source_name} from {url}: {e}")
return ips
if source_name == "Talos Intelligence":
print(f"Skipping {source_name} due to webpage format, needs manual parsing.")
return ips
elif source_name == "TOR Exit Nodes":
for line in content.splitlines():
if line.startswith("ExitAddress"):
parts = line.split(" ")
if len(parts) > 1:
try:
ipaddress.ip_address(parts[1].strip())
ips.add(parts[1].strip())
except ValueError:
continue
return ips
elif source_name == "Spamhaus DROP" or source_name == "Spamhaus EDROP":
for line in content.splitlines():
line = line.strip()
if not line or line.startswith(";"):
for line in content.splitlines():
line = line.strip()
if not line or line.startswith("#") or line.startswith(";"):
continue
# MODIFIED: Preserve CIDR notation if it already exists
if "/" in line:
try:
# Validate it's a real network and add it
net = ipaddress.ip_network(line, strict=False)
ips.add(net.with_prefixlen)
except ValueError:
continue
if "/" in line:
try:
for ip in ipaddress.ip_network(line, strict=False):
ips.add(str(ip))
except ValueError:
continue
else:
try:
ipaddress.ip_address(line)
ips.add(line)
except ValueError:
continue
return ips
elif source_name == "MaxMind GeoIP2 Anonymous IP Database":
# Requires a license key, skipping for now.
print(f"Skipping {source_name} because it requires a license key.")
return ips
else:
# Default parsing for normal text file blocklists
for line in content.splitlines():
line = line.strip()
if not line or line.startswith("#"):
else:
# MODIFIED: Convert single IPs to CIDR notation
try:
ip_obj = ipaddress.ip_address(line)
if ip_obj.version == 4:
ips.add(f"{line}/32")
else:
ips.add(f"{line}/128")
except ValueError:
continue
# Normalize ranges to single IPs
if "/" in line:
try:
for ip in ipaddress.ip_network(line, strict=False):
ips.add(str(ip))
except ValueError:
continue
elif "-" in line:
try:
start, end = line.split('-')
start_ip = ipaddress.ip_address(start.strip())
end_ip = ipaddress.ip_address(end.strip())
if start_ip.version == end_ip.version:
for ip_int in range(int(start_ip), int(end_ip) + 1):
ips.add(str(ipaddress.ip_address(ip_int)))
except ValueError:
continue
else:
try:
ipaddress.ip_address(line)
ips.add(line)
except ValueError:
continue
return ips
return ips
def is_valid_ip(ip_str):
"""Helper function to check if an IP address is valid."""
try:
ipaddress.ip_address(ip_str)
return True
except ValueError:
return False
def ip_to_int(ip_str):
"""Converts an IP address string to its integer representation."""
try:
return int(ipaddress.ip_address(ip_str))
except ValueError:
return None
def extract_tor_exit_nodes(url):
"""Fetches data from the given URL and extracts Tor exit node IPs."""
"""Fetches Tor exit node IPs and returns them in CIDR format."""
ips = set()
try:
response = requests.get(url, timeout=10)
@@ -127,9 +69,14 @@ def extract_tor_exit_nodes(url):
if line.startswith("ExitAddress"):
parts = line.split(" ")
if len(parts) > 1:
ip_str = parts[1].strip()
# MODIFIED: Convert single IPs to CIDR notation
try:
ipaddress.ip_address(parts[1].strip())
ips.add(parts[1].strip())
ip_obj = ipaddress.ip_address(ip_str)
if ip_obj.version == 4:
ips.add(f"{ip_str}/32")
else:
ips.add(f"{ip_str}/128")
except ValueError:
continue
return ips
@@ -140,31 +87,21 @@ def main():
for source_name, url in tqdm(blocklist_sources.items(), desc="Processing Blocklists"):
print(f"Processing {source_name} from {url}")
ips = extract_ips(source_name, url)
print(f" Found {len(ips)} IPs in {source_name}")
print(f" Found {len(ips)} IPs/CIDRs in {source_name}")
combined_ips.update(ips)
# --- Tor Exit Node Processing (Testing) ---
# Tor Exit Node Processing
tor_exit_ips = extract_tor_exit_nodes(tor_exit_nodes_url)
print(f"Total Tor exit node IPs: {len(tor_exit_ips)}")
valid_tor_ips = [ip for ip in tor_exit_ips if is_valid_ip(ip)]
print(f"Total Valid Tor IPs after filtering: {len(valid_tor_ips)}")
print(f"Total Tor exit node IPs/CIDRs: {len(tor_exit_ips)}")
combined_ips.update(tor_exit_ips)
# Add Tor exit IPs to the combined IPs
combined_ips.update(valid_tor_ips)
print(f"Total IPs before filtering and deduplication: {len(combined_ips)}")
# Filter out invalid IPs before sorting.
valid_ips = [ip for ip in combined_ips if is_valid_ip(ip)]
print(f"Total Valid IPs after filtering: {len(valid_ips)}")
# Remove duplicates by converting to a set before sorting
unique_ips = set(valid_ips)
print(f"Total Unique IPs/CIDRs after deduplication: {len(combined_ips)}")
# MODIFIED: The final write loop is simpler. The sorting key is removed as sorting CIDRs as integers is incorrect.
# A simple lexicographical sort is sufficient here.
with open("ip_blacklist.txt", "w") as f:
# Sort using the integer representation and write each IP to the file
for ip in sorted(unique_ips, key=ip_to_int):
f.write(f"{ip}\n")
for ip_cidr in sorted(list(combined_ips)):
f.write(f"{ip_cidr}\n")
print("IP blacklist saved to ip_blacklist.txt")

190
go.mod
View File

@@ -1,124 +1,164 @@
module github.com/fabriziosalmi/caddy-waf
go 1.22.3
toolchain go1.23.4
go 1.25
require (
github.com/caddyserver/caddy/v2 v2.9.1
github.com/fsnotify/fsnotify v1.8.0
github.com/caddyserver/caddy/v2 v2.10.2
github.com/fsnotify/fsnotify v1.9.0
github.com/google/uuid v1.6.0
github.com/oschwald/maxminddb-golang v1.13.1
github.com/stretchr/testify v1.9.0
github.com/phemmer/go-iptrie v0.0.0-20240326174613-ba542f5282c9
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0
)
require (
dario.cat/mergo v1.0.1 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/KimMachineGun/automemlimit v0.7.4 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/certmagic v0.21.6 // indirect
github.com/caddyserver/certmagic v0.25.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/glog v1.2.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.21.0 // indirect
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/cel-go v0.26.1 // indirect
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
github.com/google/go-tpm v0.9.7 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v3 v3.0.0 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mholt/acmez/v3 v3.1.4 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.1 // indirect
github.com/prometheus/procfs v0.18.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/slackhq/nebula v1.6.1 // indirect
github.com/smallstep/certificates v0.26.1 // indirect
github.com/smallstep/nosql v0.6.1 // indirect
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slackhq/nebula v1.9.7 // indirect
github.com/smallstep/certificates v0.29.0 // indirect
github.com/smallstep/cli-utils v0.12.2 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect
github.com/smallstep/linkedca v0.25.0 // indirect
github.com/smallstep/nosql v0.7.0 // indirect
github.com/smallstep/pkcs7 v0.2.1 // indirect
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/urfave/cli v1.22.17 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.3.9 // indirect
go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.37.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.step.sm/crypto v0.74.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20251009181029-0b7aa0cfb07b // indirect
golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
howett.net/plist v1.0.1 // indirect
)

667
go.sum
View File

@@ -1,22 +1,25 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k=
cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
@@ -27,66 +30,80 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A=
github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE=
github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0=
github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w=
github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc=
github.com/aws/aws-sdk-go-v2/config v1.31.16/go.mod h1:2S9hBElpCyGMifv14WxQ7EfPumgoeCPZUpuPX8VtW34=
github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q=
github.com/aws/aws-sdk-go-v2/credentials v1.18.20/go.mod h1:9mCi28a+fmBHSQ0UM79omkz6JtN+PEsvLrnG36uoUv0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 h1:VO3FIM2TDbm0kqp6sFNR0PbioXJb/HzCDW6NtIZpIWE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12/go.mod h1:6C39gB8kg82tx3r72muZSrNhHia9rjGkX7ORaS2GKNE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 h1:p/9flfXdoAnwJnuW9xHEAFY22R3A6skYkW19JFF9F+8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12/go.mod h1:ZTLHakoVCTtW8AaLGSwJ3LXqHD9uQKnOcv1TrpO6u2k=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 h1:2lTWFvRcnWFFLzHWmtddu5MTchc5Oj2OOey++99tPZ0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12/go.mod h1:hI92pK+ho8HVcWMHKHrK3Uml4pfG7wvL86FzO0LVtQQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 h1:MM8imH7NZ0ovIVX7D2RxfMDv7Jt9OiUXkcQ+GqywA7M=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12/go.mod h1:gf4OGwdNkbEsb7elw2Sy76odfhwNktWII3WgvQgQQ6w=
github.com/aws/aws-sdk-go-v2/service/kms v1.47.0 h1:A97YCVyGz19rRs3+dWf3GpMPflCswgETA9r6/Q0JNSY=
github.com/aws/aws-sdk-go-v2/service/kms v1.47.0/go.mod h1:ZJ1ghBt9gQM8JoNscUua1siIgao8w74o3kvdWUU6N/Q=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0/go.mod h1:/e8m+AO6HNPPqMyfKRtzZ9+mBF5/x1Wk8QiDva4m07I=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0/go.mod h1:4EjU+4mIx6+JqKQkruye+CaigV7alL3thVPfDd9VlMs=
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
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/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/caddyserver/caddy/v2 v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
github.com/caddyserver/caddy/v2 v2.9.1/go.mod h1:ImUELya2el1FDVp3ahnSO2iH1or1aHxlQEQxd/spP68=
github.com/caddyserver/certmagic v0.21.6 h1:1th6GfprVfsAtFNOu4StNMF5IxK5XiaI0yZhAHlZFPE=
github.com/caddyserver/certmagic v0.21.6/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw=
github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=
github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0=
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -99,19 +116,18 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -121,11 +137,15 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@@ -137,41 +157,28 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -180,151 +187,102 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.6 h1:hwIwPG7w4z5eQEBq11gYw8YYr9xXLfBQ/0JsKyq5AJM=
github.com/google/go-tpm-tools v0.4.6/go.mod h1:MsVQbJnRhKDfWwf5zgr3cDGpj13P1uLAFF0wMEP/n5w=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU=
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/acmez/v3 v3.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E=
github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -335,18 +293,22 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
github.com/phemmer/go-iptrie v0.0.0-20240326174613-ba542f5282c9 h1:C8IqpV7kfAyZDRCnAVNi//l1mWlpyPmq1N6DjVvYEnY=
github.com/phemmer/go-iptrie v0.0.0-20240326174613-ba542f5282c9/go.mod h1:dDLiSjNqdp8VjphLdGTx19OeAUsHOzhtc1FFJqpzWMU=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -355,38 +317,31 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/procfs v0.18.0 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao=
github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
@@ -413,25 +368,27 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM=
github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI=
github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE=
github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o=
github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis=
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA=
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y=
github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y=
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg=
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y=
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw=
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU=
github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs=
github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo=
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
@@ -441,128 +398,136 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY=
go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 h1:1+EHlhAe/tukctfePZRrDruB9vn7MdwyC+rf36nUSPM=
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0/go.mod h1:skzESZBY3IYcqJgImc+fwXQWflvVe+jZxoA/uw60NaI=
go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss=
go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0=
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA=
go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg=
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM=
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM=
go.opentelemetry.io/contrib/propagators/ot v1.37.0 h1:tVjnBF6EiTDMXoq2Xuc2vK0I7MTbEs05II/0j9mMK+E=
go.opentelemetry.io/contrib/propagators/ot v1.37.0/go.mod h1:MQjyNXtxAC8PGN9gzPtO4GY5zuP+RI3XX53uWbCTvEQ=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.step.sm/crypto v0.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA=
go.step.sm/crypto v0.74.0/go.mod h1:UoXqCAJjjRgzPte0Llaqen7O9P7XjPmgjgTHQGkKCDk=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 h1:4cEcP5+OjGppY79LCQ5Go2B1Boix2x0v6pvA01P3FoA=
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251009181029-0b7aa0cfb07b h1:YjNArlzCQB2fDkuKSxMwY1ZUQeRXFIFa23Ov9Wa7TUE=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251009181029-0b7aa0cfb07b/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -570,23 +535,23 @@ golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -595,89 +560,83 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -687,27 +646,26 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
@@ -722,8 +680,7 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View File

@@ -5,20 +5,44 @@ import (
"net/http"
"strings"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/google/uuid"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
type ContextKeyLogId string
type ContextKeyRule string
type (
ContextKeyLogId string
ContextKeyRule string
)
// ServeHTTP implements caddyhttp.Handler.
// handler.go
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
logID := uuid.New().String()
// Add panic recovery to catch and log panics
defer func() {
if rec := recover(); rec != nil {
m.logger.Error("PANIC in ServeHTTP",
zap.String("log_id", logID),
zap.Any("panic", rec),
zap.Stack("stack"),
)
// Return 500 error to client
w.WriteHeader(http.StatusInternalServerError)
if _, err := w.Write([]byte("Internal Server Error")); err != nil {
m.logger.Error(err.Error(),
zap.String("log_id", logID),
zap.Any("panic", rec),
zap.Stack("stack"),
)
return
}
}
}()
m.logRequestStart(r, logID)
// Propagate log ID within the request context for logging
@@ -78,11 +102,27 @@ func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next cadd
// isPhaseBlocked encapsulates the phase handling and blocking check logic.
func (m *Middleware) isPhaseBlocked(w http.ResponseWriter, r *http.Request, phase int, state *WAFState) bool {
m.handlePhase(w, r, phase, state)
if state.Blocked {
m.incrementBlockedRequestsMetric()
w.WriteHeader(state.StatusCode)
// IMPORTANT: Log the block event with details
m.logger.Warn("Request blocked in phase evaluation",
zap.Int("phase", phase),
zap.Int("status_code", state.StatusCode),
zap.Int("total_score", state.TotalScore),
zap.Int("anomaly_threshold", m.AnomalyThreshold),
)
// Only write the status if not already written
if !state.ResponseWritten {
w.WriteHeader(state.StatusCode)
state.ResponseWritten = true
}
return true
}
return false
}
@@ -133,12 +173,18 @@ func (m *Middleware) handleResponseBodyPhase(recorder *responseRecorder, r *http
}
m.logger.Debug("Response body captured for Phase 4 analysis", zap.String("log_id", logID))
for _, rule := range m.Rules[4] {
// Check if rules exist for Phase 4 before iterating
rules, ok := m.Rules[4]
if !ok || len(rules) == 0 {
m.logger.Debug("No rules found for Phase 4")
return
}
for _, rule := range rules {
if rule.regex.MatchString(body) {
if m.processRuleMatch(recorder, r, &rule, body, state) {
if m.processRuleMatch(recorder, r, &rule, "RESPONSE_BODY", body, state) { // Pass RESPONSE_BODY as target
return
}
}
}
}
@@ -212,47 +258,9 @@ func (m *Middleware) handlePhase(w http.ResponseWriter, r *http.Request, phase i
zap.String("user_agent", r.UserAgent()),
)
if phase == 1 && m.CountryBlock.Enabled {
m.logger.Debug("Starting country blocking phase")
blocked, err := m.isCountryInList(r.RemoteAddr, m.CountryBlock.CountryList, m.CountryBlock.geoIP)
if err != nil {
m.logRequest(zapcore.ErrorLevel, "Failed to check country block",
r,
zap.Error(err),
)
m.blockRequest(w, r, state, http.StatusForbidden, "internal_error", "country_block_rule", r.RemoteAddr,
zap.String("message", "Request blocked due to internal error"),
)
m.logger.Debug("Country blocking phase completed - blocked due to error")
m.incrementGeoIPRequestsMetric(false) // Increment with false for error
return
} else if blocked {
m.blockRequest(w, r, state, http.StatusForbidden, "country_block", "country_block_rule", r.RemoteAddr,
zap.String("message", "Request blocked by country"))
m.incrementGeoIPRequestsMetric(true) // Increment with true for blocked
return
}
m.logger.Debug("Country blocking phase completed - not blocked")
m.incrementGeoIPRequestsMetric(false) // Increment with false for no block
}
if phase == 1 && m.rateLimiter != nil {
m.logger.Debug("Starting rate limiting phase")
ip := extractIP(r.RemoteAddr, m.logger) // Pass the logger here
path := r.URL.Path // Get the request path
if m.rateLimiter.isRateLimited(ip, path) {
m.incrementRateLimiterBlockedRequestsMetric() // Increment the counter in the Middleware
m.blockRequest(w, r, state, http.StatusTooManyRequests, "rate_limit", "rate_limit_rule", r.RemoteAddr,
zap.String("message", "Request blocked by rate limit"),
)
return
}
m.logger.Debug("Rate limiting phase completed - not blocked")
}
if phase == 1 {
m.logger.Debug("Checking for IP blacklisting", zap.String("remote_addr", r.RemoteAddr)) //Added log for checking before to isIPBlacklisted call
// IP blacklisting - the highest priority
m.logger.Debug("Checking for IP blacklisting", zap.String("remote_addr", r.RemoteAddr)) // Added log for checking before to isIPBlacklisted call
xForwardedFor := r.Header.Get("X-Forwarded-For")
if xForwardedFor != "" {
ips := strings.Split(xForwardedFor, ",")
@@ -261,54 +269,179 @@ func (m *Middleware) handlePhase(w http.ResponseWriter, r *http.Request, phase i
m.logger.Debug("Checking IP blacklist with X-Forwarded-For", zap.String("remote_addr_xff", firstIP), zap.String("r.RemoteAddr", r.RemoteAddr))
if m.isIPBlacklisted(firstIP) {
m.logger.Debug("Starting IP blacklist phase")
m.blockRequest(w, r, state, http.StatusForbidden, "ip_blacklist", "ip_blacklist_rule", firstIP,
m.blockRequest(w, r, state, http.StatusForbidden, "ip_blacklist", "ip_blacklist_rule",
zap.String("message", "Request blocked by IP blacklist"),
)
if m.CustomResponses != nil {
m.writeCustomResponse(w, state.StatusCode)
}
return
}
} else {
m.logger.Debug("X-Forwarded-For header present but empty or invalid")
}
} else {
m.logger.Debug("X-Forwarded-For header not present using r.RemoteAddr")
if m.isIPBlacklisted(r.RemoteAddr) {
m.logger.Debug("Starting IP blacklist phase")
m.blockRequest(w, r, state, http.StatusForbidden, "ip_blacklist", "ip_blacklist_rule", r.RemoteAddr,
m.blockRequest(w, r, state, http.StatusForbidden, "ip_blacklist", "ip_blacklist_rule",
zap.String("message", "Request blocked by IP blacklist"),
)
if m.CustomResponses != nil {
m.writeCustomResponse(w, state.StatusCode)
}
return
}
}
}
if phase == 1 && m.isDNSBlacklisted(r.Host) {
m.logger.Debug("Starting DNS blacklist phase")
m.blockRequest(w, r, state, http.StatusForbidden, "dns_blacklist", "dns_blacklist_rule", r.Host,
zap.String("message", "Request blocked by DNS blacklist"),
zap.String("host", r.Host),
)
return
// DNS blacklisting
if m.isDNSBlacklisted(r.Host) {
m.logger.Debug("Starting DNS blacklist phase")
m.blockRequest(w, r, state, http.StatusForbidden, "dns_blacklist", "dns_blacklist_rule",
zap.String("message", "Request blocked by DNS blacklist"),
zap.String("host", r.Host),
)
if m.CustomResponses != nil {
m.writeCustomResponse(w, state.StatusCode)
}
return
}
// Rate limiting
if m.rateLimiter != nil {
m.logger.Debug("Starting rate limiting phase")
ip := extractIP(r.RemoteAddr) // Pass the logger here
path := r.URL.Path // Get the request path
if m.rateLimiter.isRateLimited(ip, path) {
m.incrementRateLimiterBlockedRequestsMetric() // Increment the counter in the Middleware
m.blockRequest(w, r, state, http.StatusTooManyRequests, "rate_limit", "rate_limit_rule",
zap.String("message", "Request blocked by rate limit"),
)
if m.CustomResponses != nil {
m.writeCustomResponse(w, state.StatusCode)
}
return
}
m.logger.Debug("Rate limiting phase completed - not blocked")
}
// Whitelisting
if m.CountryWhitelist.Enabled {
m.logger.Debug("Starting country whitelisting phase")
allowed, err := m.isCountryInList(r.RemoteAddr, m.CountryWhitelist.CountryList, m.CountryWhitelist.geoIP)
if err != nil {
m.logRequest(zapcore.ErrorLevel, "Failed to check country whitelist",
r,
zap.Error(err),
)
if m.GeoIPFailOpen {
m.logger.Warn("GeoIP lookup failed (Whitelist); Failing OPEN")
} else {
m.blockRequest(w, r, state, http.StatusForbidden, "internal_error", "country_block_rule",
zap.String("message", "Request blocked due to internal error"),
)
m.logger.Debug("Country whitelisting phase completed - blocked due to error")
m.incrementGeoIPRequestsMetric(false) // Increment with false for error
return
}
} else if !allowed {
m.blockRequest(w, r, state, http.StatusForbidden, "country_block", "country_block_rule",
zap.String("message", "Request blocked by country"))
m.incrementGeoIPRequestsMetric(true) // Increment with true for blocked
if m.CustomResponses != nil {
m.writeCustomResponse(w, state.StatusCode)
}
return
}
m.logger.Debug("Country whitelisting phase completed - not blocked")
m.incrementGeoIPRequestsMetric(false) // Increment with false for no block
}
// ASN Blocking
if m.BlockASNs.Enabled {
m.logger.Debug("Starting ASN blocking phase")
blocked, err := m.geoIPHandler.IsASNInList(r.RemoteAddr, m.BlockASNs.BlockedASNs, m.BlockASNs.geoIP)
if err != nil {
m.logRequest(zapcore.ErrorLevel, "Failed to check ASN blocking",
r,
zap.Error(err),
)
if m.GeoIPFailOpen {
m.logger.Warn("ASN lookup failed; Failing OPEN")
} else {
m.blockRequest(w, r, state, http.StatusForbidden, "internal_error", "asn_block_rule",
zap.String("message", "Request blocked due to internal error"),
)
m.logger.Debug("ASN blocking phase completed - blocked due to error")
m.incrementGeoIPRequestsMetric(false) // Increment with false for error
return
}
} else if blocked {
asnInfo := m.geoIPHandler.GetASN(r.RemoteAddr, m.BlockASNs.geoIP)
m.blockRequest(w, r, state, http.StatusForbidden, "asn_block", "asn_block_rule",
zap.String("message", "Request blocked by ASN"),
zap.String("asn", asnInfo),
)
m.incrementGeoIPRequestsMetric(true) // Increment with true for blocked
if m.CustomResponses != nil {
m.writeCustomResponse(w, state.StatusCode)
}
return
}
m.logger.Debug("ASN blocking phase completed - not blocked")
}
// Blacklisting
if m.CountryBlacklist.Enabled {
m.logger.Debug("Starting country blacklisting phase")
blocked, err := m.isCountryInList(r.RemoteAddr, m.CountryBlacklist.CountryList, m.CountryBlacklist.geoIP)
if err != nil {
m.logRequest(zapcore.ErrorLevel, "Failed to check country blacklisting",
r,
zap.Error(err),
)
if m.GeoIPFailOpen {
m.logger.Warn("GeoIP lookup failed (Blacklist); Failing OPEN")
} else {
m.blockRequest(w, r, state, http.StatusForbidden, "internal_error", "country_block_rule",
zap.String("message", "Request blocked due to internal error"),
)
m.logger.Debug("Country blacklisting phase completed - blocked due to error")
m.incrementGeoIPRequestsMetric(false) // Increment with false for error
return
}
} else if blocked {
m.blockRequest(w, r, state, http.StatusForbidden, "country_block", "country_block_rule",
zap.String("message", "Request blocked by country"))
m.incrementGeoIPRequestsMetric(true) // Increment with true for blocked
if m.CustomResponses != nil {
m.writeCustomResponse(w, state.StatusCode)
}
return
}
m.logger.Debug("Country blacklisting phase completed - not blocked")
m.incrementGeoIPRequestsMetric(false) // Increment with false for no block
}
}
rules, ok := m.Rules[phase]
if !ok {
m.logger.Debug("No rules found for phase", zap.Int("phase", phase))
return
// Don't block on empty rules. There may be no rules specified
// return
}
m.logger.Debug("Starting rule evaluation for phase", zap.Int("phase", phase), zap.Int("rule_count", len(rules)))
for _, rule := range rules {
m.logger.Debug("Processing rule", zap.String("rule_id", string(rule.ID)), zap.Int("target_count", len(rule.Targets)))
m.logger.Debug("Processing rule", zap.String("rule_id", rule.ID), zap.Int("target_count", len(rule.Targets)))
// Use the custom type as the key
ctx := context.WithValue(r.Context(), ContextKeyRule("rule_id"), rule.ID)
r = r.WithContext(ctx)
for _, target := range rule.Targets {
m.logger.Debug("Extracting value for target", zap.String("target", target), zap.String("rule_id", string(rule.ID)))
m.logger.Debug("Extracting value for target", zap.String("target", target), zap.String("rule_id", rule.ID))
var value string
var err error
@@ -326,52 +459,63 @@ func (m *Middleware) handlePhase(w http.ResponseWriter, r *http.Request, phase i
if err != nil {
m.logger.Debug("Failed to extract value for target, skipping rule for this target",
zap.String("target", target),
zap.String("rule_id", string(rule.ID)),
zap.String("rule_id", rule.ID),
zap.Error(err),
)
continue
}
redactedValue := m.requestValueExtractor.RedactValueIfSensitive(target, value)
m.logger.Debug("Extracted value",
zap.String("rule_id", string(rule.ID)),
zap.String("rule_id", rule.ID),
zap.String("target", target),
zap.String("value", value),
zap.String("value", redactedValue),
)
if rule.regex.MatchString(value) {
m.logger.Debug("Rule matched",
zap.String("rule_id", string(rule.ID)),
zap.String("rule_id", rule.ID),
zap.String("target", target),
zap.String("value", value),
zap.String("value", redactedValue),
)
// FIXED: Correctly interpret processRuleMatch return value
var shouldContinue bool
if phase == 3 || phase == 4 {
if recorder, ok := w.(*responseRecorder); ok {
if m.processRuleMatch(recorder, r, &rule, value, state) {
return // Stop processing if the rule match indicates blocking
}
shouldContinue = m.processRuleMatch(recorder, r, &rule, target, value, state)
} else {
if m.processRuleMatch(w, r, &rule, value, state) {
return // Stop processing if the rule match indicates blocking
}
shouldContinue = m.processRuleMatch(w, r, &rule, target, value, state)
}
} else {
if m.processRuleMatch(w, r, &rule, value, state) {
return // Stop processing if the rule match indicates blocking
}
shouldContinue = m.processRuleMatch(w, r, &rule, target, value, state)
}
if state.Blocked || state.ResponseWritten {
m.logger.Debug("Rule evaluation completed early due to blocking or response written", zap.Int("phase", phase), zap.String("rule_id", string(rule.ID)))
// If processRuleMatch returned false or state is now blocked, stop processing
if !shouldContinue || state.Blocked || state.ResponseWritten {
m.logger.Debug("Rule evaluation stopping due to blocking or rule directive",
zap.Int("phase", phase),
zap.String("rule_id", rule.ID),
zap.Bool("continue", shouldContinue),
zap.Bool("blocked", state.Blocked),
)
if m.CustomResponses != nil {
m.writeCustomResponse(w, state.StatusCode)
}
return
}
} else {
m.logger.Debug("Rule did not match",
zap.String("rule_id", string(rule.ID)),
zap.String("rule_id", rule.ID),
zap.String("target", target),
zap.String("value", value),
zap.String("value", redactedValue),
)
}
}
}
m.logger.Debug("Rule evaluation completed for phase", zap.Int("phase", phase))
if phase == 3 {
@@ -393,6 +537,8 @@ func (m *Middleware) handlePhase(w http.ResponseWriter, r *http.Request, phase i
zap.Int("total_score", state.TotalScore),
zap.Int("anomaly_threshold", m.AnomalyThreshold),
)
m.allowRequest(state)
}
// incrementRateLimiterBlockedRequestsMetric increments the blocked requests metric for the rate limiter.

View File

@@ -6,14 +6,18 @@ import (
"mime/multipart"
"net/http"
"net/http/httptest"
"net/netip"
"os"
"regexp"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/phemmer/go-iptrie"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestBlockedRequestPhase1_DNSBlacklist(t *testing.T) {
@@ -23,63 +27,227 @@ func TestBlockedRequestPhase1_DNSBlacklist(t *testing.T) {
dnsBlacklist: map[string]struct{}{
"malicious.domain": {},
},
ipBlacklist: NewCIDRTrie(), // Initialize ipBlacklist
CustomResponses: map[int]CustomBlockResponse{
403: {
StatusCode: http.StatusForbidden,
Body: "Access Denied",
},
},
ipBlacklist: iptrie.NewTrie(),
CustomResponses: customResponse,
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
// Simulate a request to a blacklisted domain
req := httptest.NewRequest("GET", "http://malicious.domain", nil)
w := httptest.NewRecorder()
state := &WAFState{}
// Process the request in Phase 1
middleware.handlePhase(w, req, 1, state)
t.Run("Allow unblocked domain", func(t *testing.T) {
// Simulate a request to a blacklisted domain
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
// Debug: Print the response body and status code
t.Logf("Response Body: %s", w.Body.String())
t.Logf("Response Status Code: %d", w.Code)
// Process the request in Phase 1
middleware.handlePhase(w, req, 1, state)
assert.False(t, state.Blocked, "Request should be allowed")
assert.Equal(t, http.StatusOK, w.Code, "Expected status code 200")
})
// Verify that the request was blocked
assert.True(t, state.Blocked, "Request should be blocked")
assert.Equal(t, http.StatusForbidden, w.Code, "Expected status code 403")
assert.Contains(t, w.Body.String(), "Access Denied", "Response body should contain 'Access Denied'")
t.Run("Block blacklisted domain", func(t *testing.T) {
// Simulate a request to a blacklisted domain
req := httptest.NewRequest("GET", "http://malicious.domain", nil)
req.RemoteAddr = localIP
// Process the request in Phase 1
middleware.handlePhase(w, req, 1, state)
// Verify that the request was blocked
assert.True(t, state.Blocked, "Request should be blocked")
assert.Equal(t, http.StatusForbidden, w.Code, "Expected status code 403")
assert.Contains(t, w.Body.String(), "Access Denied", "Response body should contain 'Access Denied'")
})
}
func TestBlockedRequestPhase1_GeoIPBlocking(t *testing.T) {
logger := zap.NewNop()
middleware := &Middleware{
logger: logger,
CountryBlock: CountryAccessFilter{
if _, err := os.Stat(geoIPdata); os.IsNotExist(err) {
t.Skip("GeoIP database not found, skipping test")
}
logger, err := zap.NewDevelopment()
assert.NoError(t, err)
geoIPHandler := NewGeoIPHandler(logger)
geoIPBlock, err := geoIPHandler.LoadGeoIPDatabase(geoIPdata)
assert.NoError(t, err)
blMiddleware := &Middleware{
logger: logger,
ipBlacklist: iptrie.NewTrie(),
geoIPHandler: geoIPHandler,
CountryBlacklist: CountryAccessFilter{
Enabled: true,
CountryList: []string{"US"},
GeoIPDBPath: "testdata/GeoIP2-Country-Test.mmdb", // Path to a test GeoIP database
},
CustomResponses: map[int]CustomBlockResponse{
403: {
StatusCode: http.StatusForbidden,
Body: "Access Denied",
},
CountryList: []string{"US", "RU"},
GeoIPDBPath: geoIPdata, // Path to a test GeoIP database
geoIP: geoIPBlock,
},
CustomResponses: customResponse,
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
// Simulate a request from a blocked country (US)
req := httptest.NewRequest("GET", "http://example.com", nil)
req.RemoteAddr = "192.168.1.1:12345" // IP from the US (mocked in the test GeoIP database)
w := httptest.NewRecorder()
wlMiddleware := &Middleware{
logger: logger,
ipBlacklist: iptrie.NewTrie(),
geoIPHandler: geoIPHandler,
CountryWhitelist: CountryAccessFilter{
Enabled: true,
CountryList: []string{"BR"},
GeoIPDBPath: geoIPdata, // Path to a test GeoIP database
geoIP: geoIPBlock,
},
CustomResponses: customResponse,
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
blackWhiteMw := &Middleware{
logger: logger,
ipBlacklist: iptrie.NewTrie(),
geoIPHandler: geoIPHandler,
CountryWhitelist: CountryAccessFilter{
Enabled: true,
CountryList: []string{"BR"},
GeoIPDBPath: geoIPdata, // Path to a test GeoIP database
geoIP: geoIPBlock,
},
CountryBlacklist: CountryAccessFilter{
Enabled: true,
CountryList: []string{"US", "RU"},
GeoIPDBPath: geoIPdata, // Path to a test GeoIP database
geoIP: geoIPBlock,
},
CustomResponses: customResponse,
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", testURL, nil)
state := &WAFState{}
// Process the request in Phase 1
middleware.handlePhase(w, req, 1, state)
t.Run("GeoIP Blacklist: Allow CN IP", func(t *testing.T) {
w := httptest.NewRecorder()
req.RemoteAddr = aliCNIP
// Verify that the request was blocked
assert.True(t, state.Blocked, "Request should be blocked")
assert.Equal(t, http.StatusForbidden, w.Code, "Expected status code 403")
assert.Contains(t, w.Body.String(), "Access Denied", "Response body should contain 'Access Denied'")
// Process the request in Phase 1
blMiddleware.handlePhase(w, req, 1, state)
assert.False(t, state.Blocked, "Request should be allowed")
assert.Equal(t, http.StatusOK, w.Code, "Expected status code 200")
})
t.Run("GeoIP Blacklist: Block US IP", func(t *testing.T) {
w := httptest.NewRecorder()
req.RemoteAddr = googleUSIP
// Process the request in Phase 1
blMiddleware.handlePhase(w, req, 1, state)
// Verify that the request was blocked
assert.True(t, state.Blocked, "Request should be blocked")
assert.Equal(t, http.StatusForbidden, w.Code, "Expected status code 403")
assert.Contains(t, w.Body.String(), "Access Denied", "Response body should contain 'Access Denied'")
})
t.Run("GeoIP Whitelist: Allow BR IP", func(t *testing.T) {
w := httptest.NewRecorder()
req.RemoteAddr = googleBRIP
// Process the request in Phase 1
wlMiddleware.handlePhase(w, req, 1, state)
assert.False(t, state.Blocked, "Request should be allowed")
assert.Equal(t, http.StatusOK, w.Code, "Expected status code 200")
})
t.Run("GeoIP Whitelist: Block RU IP", func(t *testing.T) {
w := httptest.NewRecorder()
req.RemoteAddr = googleRUIP
// Process the request in Phase 1
wlMiddleware.handlePhase(w, req, 1, state)
// Verify that the request was blocked
assert.True(t, state.Blocked, "Request should be blocked")
assert.Equal(t, http.StatusForbidden, w.Code, "Expected status code 403")
assert.Contains(t, w.Body.String(), "Access Denied", "Response body should contain 'Access Denied'")
})
t.Run("GeoIP whitelist and blacklist: whitelist has the priority", func(t *testing.T) {
w := httptest.NewRecorder()
// BR should be allowed
req0 := httptest.NewRequest("GET", testURL, nil)
req0.RemoteAddr = googleBRIP
blackWhiteMw.handlePhase(w, req0, 1, state)
assert.False(t, state.Blocked, "Request should be allowed")
assert.Equal(t, http.StatusOK, w.Code, "Expected status code 200")
// US must be blocked
req1 := httptest.NewRequest("GET", testURL, nil)
req1.RemoteAddr = googleUSIP
blackWhiteMw.handlePhase(w, req1, 1, state)
// Verify that the request was blocked
assert.True(t, state.Blocked, "Request should be blocked")
assert.Equal(t, http.StatusForbidden, w.Code, "Expected status code 403")
assert.Contains(t, w.Body.String(), "Access Denied", "Response body should contain 'Access Denied'")
})
}
func TestBlockedRequestPhase1_IPBlocking(t *testing.T) {
logger, err := zap.NewDevelopment()
assert.NoError(t, err)
blackList := iptrie.NewTrie()
loader := iptrie.NewTrieLoader(blackList)
for _, net := range []string{
"192.168.0.0/24",
"192.168.1.1/32",
} {
loader.Insert(netip.MustParsePrefix(net), "net="+net)
}
state := &WAFState{}
w := httptest.NewRecorder()
t.Run("Allow unblocked CIDR", func(t *testing.T) {
middleware := &Middleware{
logger: logger,
ipBlacklist: blackList,
CustomResponses: customResponse,
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
// Process the request in Phase 1
middleware.handlePhase(w, req, 1, state)
assert.False(t, state.Blocked, "Request should be allowed")
assert.Equal(t, http.StatusOK, w.Code, "Expected status code 200")
})
t.Run("Blocks blacklisted CIDR", func(t *testing.T) {
middleware := &Middleware{
logger: logger,
ipBlacklist: blackList,
CustomResponses: customResponse,
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = "192.168.1.1"
// Process the request in Phase 1
middleware.handlePhase(w, req, 1, state)
// Verify that the request was blocked
assert.True(t, state.Blocked, "Request should be blocked")
assert.Equal(t, http.StatusForbidden, w.Code, "Expected status code 403")
assert.Contains(t, w.Body.String(), "Access Denied", "Response body should contain 'Access Denied'")
})
}
func TestHandlePhase_Phase2_NiktoUserAgent(t *testing.T) {
@@ -100,18 +268,13 @@ func TestHandlePhase_Phase2_NiktoUserAgent(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
CustomResponses: map[int]CustomBlockResponse{
403: {
StatusCode: http.StatusForbidden,
Body: "Access Denied",
},
},
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
CustomResponses: customResponse,
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.Header.Set("User-Agent", "nikto")
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -135,7 +298,9 @@ func TestHandlePhase_Phase2_NiktoUserAgent(t *testing.T) {
}
func TestBlockedRequestPhase1_HeaderRegex(t *testing.T) {
logger := zap.NewNop()
logger, err := zap.NewDevelopment()
assert.NoError(t, err)
middleware := &Middleware{
logger: logger,
Rules: map[int][]Rule{
@@ -158,12 +323,13 @@ func TestBlockedRequestPhase1_HeaderRegex(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("X-Custom-Header", "this-is-a-bad-header") // Simulate a request with bad header
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -210,12 +376,13 @@ func TestBlockedRequestPhase1_HeaderRegex_SpecificValue(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("X-Specific-Header", "specific-value") // Simulate a request with the specific header
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -262,12 +429,13 @@ func TestBlockedRequestPhase1_HeaderRegex_CommaSeparatedTargets(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("X-Custom-Header1", "good-value")
req.Header.Set("X-Custom-Header2", "bad-value") // Simulate a request with bad value in one of the headers
@@ -315,12 +483,13 @@ func TestBlockedRequestPhase1_CombinedConditions(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://bad-host.com", nil)
req.RemoteAddr = localIP
req.Header.Set("User-Agent", "good-user")
// Create a context and add logID to it
@@ -367,12 +536,13 @@ func TestBlockedRequestPhase1_NoMatch(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("User-Agent", "good-user")
// Create a context and add logID to it
@@ -419,12 +589,13 @@ func TestBlockedRequestPhase1_HeaderRegex_EmptyHeader(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
// Create a context and add logID to it
ctx := context.Background()
@@ -445,6 +616,7 @@ func TestBlockedRequestPhase1_HeaderRegex_EmptyHeader(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code, "Expected status code 200")
assert.Empty(t, w.Body.String(), "Response body should be empty")
}
func TestBlockedRequestPhase1_HeaderRegex_MissingHeader(t *testing.T) {
logger := zap.NewNop()
middleware := &Middleware{
@@ -469,12 +641,13 @@ func TestBlockedRequestPhase1_HeaderRegex_MissingHeader(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil) // Header not set
req := httptest.NewRequest("GET", testURL, nil) // Header not set
req.RemoteAddr = localIP
// Create a context and add logID to it
ctx := context.Background()
@@ -494,7 +667,6 @@ func TestBlockedRequestPhase1_HeaderRegex_MissingHeader(t *testing.T) {
assert.False(t, state.Blocked, "Request should not be blocked because header is missing")
assert.Equal(t, http.StatusOK, w.Code, "Expected status code 200")
assert.Empty(t, w.Body.String(), "Response body should be empty")
}
func TestBlockedRequestPhase1_HeaderRegex_ComplexPattern(t *testing.T) {
@@ -521,12 +693,13 @@ func TestBlockedRequestPhase1_HeaderRegex_ComplexPattern(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("X-Email-Header", "test@example.com") // Simulate a request with a valid email
// Create a context and add logID to it
@@ -573,12 +746,13 @@ func TestBlockedRequestPhase1_MultiTargetMatch(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("X-Custom-Header", "good-header")
req.Header.Set("User-Agent", "bad-user-agent")
@@ -625,12 +799,13 @@ func TestBlockedRequestPhase1_MultiTargetNoMatch(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("X-Custom-Header", "good-header")
req.Header.Set("User-Agent", "good-user-agent")
@@ -678,12 +853,13 @@ func TestBlockedRequestPhase1_URLParameterRegex_NoMatch(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com?param1=good-param-value¶m2=good-value", nil)
req.RemoteAddr = localIP
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
ctx := context.Background()
@@ -737,12 +913,13 @@ func TestBlockedRequestPhase1_MultipleRules(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://bad-host.com", nil)
req.RemoteAddr = localIP
req.Header.Set("User-Agent", "bad-user") // Simulate a request with a bad user agent
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -765,6 +942,7 @@ func TestBlockedRequestPhase1_MultipleRules(t *testing.T) {
assert.Contains(t, w.Body.String(), "Blocked by Multiple Rules", "Response body should contain 'Blocked by Multiple Rules'")
req2 := httptest.NewRequest("GET", "http://good-host.com", nil)
req2.RemoteAddr = localIP
req2.Header.Set("User-Agent", "bad-user") // Simulate a request with a bad user agent
// Create a context and add logID to it - FIX: ADD CONTEXT HERE for req2 as well!
@@ -811,18 +989,19 @@ func TestBlockedRequestPhase2_BodyRegex(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("POST", "http://example.com",
req := httptest.NewRequest("POST", testURL,
func() *bytes.Buffer {
b := new(bytes.Buffer)
b.WriteString("this-is-a-bad-body")
return b
}(), // Simulate a request with bad body
)
req.RemoteAddr = localIP
req.Header.Set("Content-Type", "text/plain")
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -869,18 +1048,19 @@ func TestBlockedRequestPhase2_BodyRegex_JSON(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("POST", "http://example.com",
req := httptest.NewRequest("POST", testURL,
func() *bytes.Buffer {
b := new(bytes.Buffer)
b.WriteString(`{"data":{"malicious":true,"name":"test"}}`)
return b
}(), // Simulate a request with JSON body
)
req.RemoteAddr = localIP
req.Header.Set("Content-Type", "application/json")
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -927,14 +1107,15 @@ func TestBlockedRequestPhase2_BodyRegex_FormURLEncoded(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("POST", "http://example.com",
req := httptest.NewRequest("POST", testURL,
strings.NewReader("param1=value1&secret=badvalue¶m2=value2"),
)
req.RemoteAddr = localIP
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -981,18 +1162,19 @@ func TestBlockedRequestPhase2_BodyRegex_SpecificPattern(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("POST", "http://example.com",
req := httptest.NewRequest("POST", testURL,
func() *bytes.Buffer {
b := new(bytes.Buffer)
b.WriteString("User ID: 123-45-6789")
return b
}(),
)
req.RemoteAddr = localIP
req.Header.Set("Content-Type", "text/plain") // Setting content type
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -1039,18 +1221,19 @@ func TestBlockedRequestPhase2_BodyRegex_NoMatch(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("POST", "http://example.com",
req := httptest.NewRequest("POST", testURL,
func() *bytes.Buffer {
b := new(bytes.Buffer)
b.WriteString("this-is-a-good-body")
return b
}(),
)
req.RemoteAddr = localIP
req.Header.Set("Content-Type", "text/plain")
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -1097,9 +1280,9 @@ func TestBlockedRequestPhase2_BodyRegex_NoMatch_MultipartForm(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
body := &bytes.Buffer{}
@@ -1117,7 +1300,8 @@ func TestBlockedRequestPhase2_BodyRegex_NoMatch_MultipartForm(t *testing.T) {
t.Fatalf("Failed to close multipart writer: %v", err)
}
req := httptest.NewRequest("POST", "http://example.com", body)
req := httptest.NewRequest("POST", testURL, body)
req.RemoteAddr = localIP
req.Header.Set("Content-Type", writer.FormDataContentType())
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -1164,12 +1348,13 @@ func TestBlockedRequestPhase2_BodyRegex_NoBody(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("POST", "http://example.com", nil)
req := httptest.NewRequest("POST", testURL, nil)
req.RemoteAddr = localIP
w := httptest.NewRecorder()
state := &WAFState{}
@@ -1209,9 +1394,9 @@ func TestBlockedRequestPhase3_ResponseHeaderRegex_NoMatch(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
mockHandler := func() caddyhttp.Handler {
@@ -1222,7 +1407,8 @@ func TestBlockedRequestPhase3_ResponseHeaderRegex_NoMatch(t *testing.T) {
})
}()
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
w := httptest.NewRecorder()
state := &WAFState{}
@@ -1264,9 +1450,9 @@ func TestBlockedRequestPhase4_ResponseBodyRegex_EmptyBody(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
mockHandler := func() caddyhttp.Handler {
@@ -1276,7 +1462,8 @@ func TestBlockedRequestPhase4_ResponseBodyRegex_EmptyBody(t *testing.T) {
})
}()
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
w := httptest.NewRecorder()
state := &WAFState{}
err := middleware.ServeHTTP(w, req, mockHandler)
@@ -1319,9 +1506,9 @@ func TestBlockedRequestPhase4_ResponseBodyRegex_NoBody(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
mockHandler := func() caddyhttp.Handler {
@@ -1331,7 +1518,8 @@ func TestBlockedRequestPhase4_ResponseBodyRegex_NoBody(t *testing.T) {
})
}()
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
w := httptest.NewRecorder()
state := &WAFState{}
err := middleware.ServeHTTP(w, req, mockHandler)
@@ -1372,9 +1560,9 @@ func TestBlockedRequestPhase3_ResponseHeaderRegex_NoSetCookie(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
mockHandler := func() caddyhttp.Handler {
return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
@@ -1384,7 +1572,8 @@ func TestBlockedRequestPhase3_ResponseHeaderRegex_NoSetCookie(t *testing.T) {
})
}()
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
w := httptest.NewRecorder()
state := &WAFState{}
err := middleware.ServeHTTP(w, req, mockHandler)
@@ -1427,12 +1616,13 @@ func TestBlockedRequestPhase1_HeaderRegex_CaseInsensitive(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("X-Custom-Header", "bAd-VaLuE") // Test with mixed-case header value
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
@@ -1479,12 +1669,13 @@ func TestBlockedRequestPhase1_HeaderRegex_MultipleMatchingHeaders(t *testing.T)
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = localIP
req.Header.Set("X-Custom-Header1", "bad-value")
req.Header.Set("X-Custom-Header2", "bad-value") // Both headers have a "bad" value
@@ -1507,7 +1698,8 @@ func TestBlockedRequestPhase1_HeaderRegex_MultipleMatchingHeaders(t *testing.T)
assert.Equal(t, http.StatusForbidden, w.Code, "Expected status code 403")
assert.Contains(t, w.Body.String(), "Blocked by Multiple Matching Headers Regex", "Response body should contain 'Blocked by Multiple Matching Headers Regex'")
req2 := httptest.NewRequest("GET", "http://example.com", nil)
req2 := httptest.NewRequest("GET", testURL, nil)
req2.RemoteAddr = localIP
req2.Header.Set("X-Custom-Header1", "good-value")
req2.Header.Set("X-Custom-Header2", "bad-value") // One header has a "bad" value
@@ -1530,7 +1722,8 @@ func TestBlockedRequestPhase1_HeaderRegex_MultipleMatchingHeaders(t *testing.T)
assert.Equal(t, http.StatusForbidden, w2.Code, "Expected status code 403")
assert.Contains(t, w2.Body.String(), "Blocked by Multiple Matching Headers Regex", "Response body should contain 'Blocked by Multiple Matching Headers Regex'")
req3 := httptest.NewRequest("GET", "http://example.com", nil)
req3 := httptest.NewRequest("GET", testURL, nil)
req3.RemoteAddr = localIP
req3.Header.Set("X-Custom-Header1", "good-value")
req3.Header.Set("X-Custom-Header2", "good-value") // None headers have a "bad" value
@@ -1564,16 +1757,16 @@ func TestBlockedRequestPhase1_RateLimiting_MultiplePaths(t *testing.T) {
middleware := &Middleware{
logger: logger,
rateLimiter: func() *RateLimiter {
rl := &RateLimiter{
config: RateLimit{
Requests: 1,
Window: time.Minute,
CleanupInterval: time.Minute,
Paths: []string{"/api/v1/.*", "/admin/.*"},
MatchAllPaths: false,
},
requests: make(map[string]map[string]*requestCounter),
stopCleanup: make(chan struct{}),
config := RateLimit{
Requests: 1,
Window: time.Minute,
CleanupInterval: time.Minute,
Paths: []string{"/api/v1/.*", "/admin/.*"},
MatchAllPaths: false,
}
rl, err := NewRateLimiter(config)
if err != nil {
t.Fatalf("Failed to create rate limiter: %v", err)
}
rl.startCleanup()
return rl
@@ -1584,13 +1777,13 @@ func TestBlockedRequestPhase1_RateLimiting_MultiplePaths(t *testing.T) {
Body: "Rate limit exceeded",
},
},
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: make(map[string]struct{}),
}
// Test path 1
req1 := httptest.NewRequest("GET", "/api/v1/users", nil)
req1.RemoteAddr = "192.168.1.1:12345"
req1.RemoteAddr = localIP
w1 := httptest.NewRecorder()
state1 := &WAFState{}
@@ -1599,7 +1792,7 @@ func TestBlockedRequestPhase1_RateLimiting_MultiplePaths(t *testing.T) {
assert.Equal(t, http.StatusOK, w1.Code, "Expected status code 200")
req2 := httptest.NewRequest("GET", "/api/v1/users", nil)
req2.RemoteAddr = "192.168.1.1:12345"
req2.RemoteAddr = localIP
w2 := httptest.NewRecorder()
state2 := &WAFState{}
middleware.handlePhase(w2, req2, 1, state2)
@@ -1608,7 +1801,7 @@ func TestBlockedRequestPhase1_RateLimiting_MultiplePaths(t *testing.T) {
// Test path 2
req3 := httptest.NewRequest("GET", "/admin/dashboard", nil)
req3.RemoteAddr = "192.168.1.1:12345"
req3.RemoteAddr = localIP
w3 := httptest.NewRecorder()
state3 := &WAFState{}
middleware.handlePhase(w3, req3, 1, state3)
@@ -1616,7 +1809,7 @@ func TestBlockedRequestPhase1_RateLimiting_MultiplePaths(t *testing.T) {
assert.Equal(t, http.StatusOK, w3.Code, "Expected status code 200")
req4 := httptest.NewRequest("GET", "/admin/dashboard", nil)
req4.RemoteAddr = "192.168.1.1:12345"
req4.RemoteAddr = localIP
w4 := httptest.NewRecorder()
state4 := &WAFState{}
middleware.handlePhase(w4, req4, 1, state4)
@@ -1624,7 +1817,7 @@ func TestBlockedRequestPhase1_RateLimiting_MultiplePaths(t *testing.T) {
assert.Equal(t, http.StatusTooManyRequests, w4.Code, "Expected status code 429")
req5 := httptest.NewRequest("GET", "/not-rate-limited", nil)
req5.RemoteAddr = "192.168.1.1:12345"
req5.RemoteAddr = localIP
w5 := httptest.NewRecorder()
state5 := &WAFState{}
middleware.handlePhase(w5, req5, 1, state5)
@@ -1654,13 +1847,13 @@ func TestBlockedRequestPhase1_RateLimiting_DifferentIPs(t *testing.T) {
Body: "Rate limit exceeded",
},
},
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: make(map[string]struct{}),
}
// Test different IPs
req1 := httptest.NewRequest("GET", "/api/users", nil)
req1.RemoteAddr = "192.168.1.1:12345"
req1.RemoteAddr = localIP
w1 := httptest.NewRecorder()
state1 := &WAFState{}
@@ -1669,7 +1862,7 @@ func TestBlockedRequestPhase1_RateLimiting_DifferentIPs(t *testing.T) {
assert.Equal(t, http.StatusOK, w1.Code, "Expected status code 200")
req2 := httptest.NewRequest("GET", "/api/users", nil)
req2.RemoteAddr = "192.168.1.2:12345"
req2.RemoteAddr = "192.168.1.2"
w2 := httptest.NewRecorder()
state2 := &WAFState{}
middleware.handlePhase(w2, req2, 1, state2)
@@ -1677,7 +1870,7 @@ func TestBlockedRequestPhase1_RateLimiting_DifferentIPs(t *testing.T) {
assert.Equal(t, http.StatusOK, w2.Code, "Expected status code 200")
req3 := httptest.NewRequest("GET", "/api/users", nil)
req3.RemoteAddr = "192.168.1.1:12345"
req3.RemoteAddr = localIP
w3 := httptest.NewRecorder()
state3 := &WAFState{}
middleware.handlePhase(w3, req3, 1, state3)
@@ -1707,13 +1900,13 @@ func TestBlockedRequestPhase1_RateLimiting_MatchAllPaths(t *testing.T) {
Body: "Rate limit exceeded",
},
},
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: make(map[string]struct{}),
}
// Test with match all paths
req1 := httptest.NewRequest("GET", "/api/users", nil)
req1.RemoteAddr = "192.168.1.1:12345"
req1.RemoteAddr = localIP
w1 := httptest.NewRecorder()
state1 := &WAFState{}
middleware.handlePhase(w1, req1, 1, state1)
@@ -1721,7 +1914,7 @@ func TestBlockedRequestPhase1_RateLimiting_MatchAllPaths(t *testing.T) {
assert.Equal(t, http.StatusOK, w1.Code, "Expected status code 200")
req2 := httptest.NewRequest("GET", "/api/users", nil)
req2.RemoteAddr = "192.168.1.1:12345"
req2.RemoteAddr = localIP
w2 := httptest.NewRecorder()
state2 := &WAFState{}
@@ -1730,7 +1923,7 @@ func TestBlockedRequestPhase1_RateLimiting_MatchAllPaths(t *testing.T) {
assert.Equal(t, http.StatusTooManyRequests, w2.Code, "Expected status code 429")
req3 := httptest.NewRequest("GET", "/some-other-path", nil)
req3.RemoteAddr = "192.168.1.1:12345"
req3.RemoteAddr = localIP
w3 := httptest.NewRecorder()
state3 := &WAFState{}
middleware.handlePhase(w3, req3, 1, state3)

View File

@@ -1,7 +1,9 @@
package caddywaf
import (
"net"
"os"
"strings"
)
// fileExists checks if a file exists and is readable.
@@ -15,3 +17,31 @@ func fileExists(path string) bool {
}
return !info.IsDir()
}
// isIPv4 - checks if input IP is of type v4
//
//nolint:unused
func isIPv4(addr string) bool {
return strings.Count(addr, ":") < 2
}
// appendCIDR - appends CIDR for a single IP
func appendCIDR(ip string) string {
// IPv4
if strings.Count(ip, ":") < 2 {
ip += "/32"
// IPv6
} else {
ip += "/64"
}
return ip
}
// extractIP extracts the IP address from a remote address string.
func extractIP(remoteAddr string) string {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return remoteAddr // Assume the input is already an IP address
}
return host
}

217
install_with_modules.sh Normal file
View File

@@ -0,0 +1,217 @@
#!/bin/bash
set -e
# --- Configuration ---
GREEN="\033[1;32m"
RED="\033[1;31m"
YELLOW="\033[1;33m"
NC="\033[0m" # No Color
GO_VERSION_REQUIRED="1.22.3"
GO_VERSION_TARGET="1.23.4"
XCADDY_VERSION="latest"
GEOLITE2_DB_URL="https://git.io/GeoLite2-Country.mmdb"
GEOLITE2_DB_FILE="GeoLite2-Country.mmdb"
# Default modules - can be overridden with environment variables
WAF_MODULE=${WAF_MODULE:-"github.com/fabriziosalmi/caddy-waf@latest"}
# Add additional modules here, comma-separated in EXTRA_MODULES env var
# Example: EXTRA_MODULES="github.com/greenpau/caddy-security@latest,github.com/example/module@latest"
EXTRA_MODULES=${EXTRA_MODULES:-""}
# --- Helper Functions ---
print_success() {
echo -e "${GREEN}✅ Success: $1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ Warning: $1${NC}"
}
print_info() {
echo -e " Info: $1${NC}"
}
print_error() {
echo -e "${RED}❌ Error: $1${NC}"
echo -e "${RED} $1${NC}" >&2
exit 1
}
check_command_exists() {
if ! command -v "$1" &> /dev/null; then
return 1 # Command not found
else
return 0 # Command found
fi
}
ensure_go_installed() {
if ! check_command_exists go; then
print_info "Go not found. Installing Go $GO_VERSION_TARGET..."
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
install_go_linux
elif [[ "$OSTYPE" == "darwin"* ]]; then
install_go_macos
else
print_error "Unsupported OS type: $OSTYPE"
fi
else
check_go_version
fi
}
check_go_version() {
local version
version=$(go version 2>&1 | awk '{print $3}' | sed 's/go//')
if [[ "$version" == *"error"* ]]; then
print_warning "Error checking Go version. Attempting to proceed anyway."
return
fi
# Compare versions (simple string comparison, assumes semantic versioning)
if [[ "$version" < "$GO_VERSION_REQUIRED" ]]; then
print_warning "Go version $version is older than required version $GO_VERSION_REQUIRED."
print_info "Installing Go $GO_VERSION_TARGET..."
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
install_go_linux
elif [[ "$OSTYPE" == "darwin"* ]]; then
install_go_macos
else
print_error "Unsupported OS type: $OSTYPE"
fi
else
print_info "Go version $version is installed (minimum required: $GO_VERSION_REQUIRED)."
fi
}
ensure_xcaddy_installed() {
if ! check_command_exists xcaddy; then
print_info "xcaddy not found. Installing xcaddy..."
install_xcaddy
else
print_info "xcaddy is already installed."
fi
}
install_xcaddy() {
print_info "Installing xcaddy $XCADDY_VERSION..."
GOBIN="$(go env GOBIN)"
if [ -z "$GOBIN" ]; then
GOBIN="$HOME/go/bin" # Default GOBIN if not set
fi
go install "github.com/caddyserver/xcaddy/cmd/xcaddy@$XCADDY_VERSION" || print_error "Failed to install xcaddy."
export PATH="$PATH:$GOBIN" # Ensure PATH is updated in current shell
print_success "xcaddy $XCADDY_VERSION installed successfully."
}
download_geolite2_db() {
if [ ! -f "$GEOLITE2_DB_FILE" ]; then
print_info "Downloading GeoLite2 Country database..."
if check_command_exists wget; then
wget -q "$GEOLITE2_DB_URL" -O "$GEOLITE2_DB_FILE" || print_error "Failed to download GeoLite2 database."
elif check_command_exists curl; then
curl -s "$GEOLITE2_DB_URL" -o "$GEOLITE2_DB_FILE" || print_error "Failed to download GeoLite2 database."
else
print_error "Neither wget nor curl is installed. Cannot download GeoLite2 database."
fi
print_success "GeoLite2 database downloaded."
else
print_info "GeoLite2 database already exists."
fi
}
build_caddy_with_modules() {
print_info "Building Caddy with modules..."
# Start building the xcaddy command
CMD="xcaddy build --with $WAF_MODULE"
# Add any extra modules
if [ -n "$EXTRA_MODULES" ]; then
IFS=',' read -ra MODULES <<< "$EXTRA_MODULES"
for MODULE in "${MODULES[@]}"; do
CMD="$CMD --with $MODULE"
done
fi
print_info "Running command: $CMD"
eval $CMD || print_error "Failed to build Caddy with modules."
print_success "Caddy built successfully with the following modules:"
print_info "- $WAF_MODULE"
if [ -n "$EXTRA_MODULES" ]; then
IFS=',' read -ra MODULES <<< "$EXTRA_MODULES"
for MODULE in "${MODULES[@]}"; do
print_info "- $MODULE"
done
fi
}
format_caddyfile() {
if [ -f "Caddyfile" ]; then
print_info "Formatting Caddyfile..."
./caddy fmt --overwrite Caddyfile || print_warning "Failed to format Caddyfile."
print_success "Caddyfile formatted."
else
print_info "No Caddyfile found to format."
fi
}
check_modules() {
print_info "Checking loaded modules..."
./caddy list-modules | grep -E "(waf|security)" || print_warning "Modules may not be properly loaded."
}
# --- OS Specific Functions ---
install_go_linux() {
print_info "Installing Go $GO_VERSION_TARGET for Linux..."
# Download and install Go
wget -q https://golang.org/dl/go${GO_VERSION_TARGET}.linux-amd64.tar.gz -O go.tar.gz || print_error "Failed to download Go."
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go.tar.gz
rm go.tar.gz
export PATH="$PATH:/usr/local/go/bin"
print_success "Go $GO_VERSION_TARGET installed successfully on Linux."
}
install_go_macos() {
print_info "Installing Go $GO_VERSION_TARGET for macOS..."
# Download and install Go
curl -sL https://golang.org/dl/go${GO_VERSION_TARGET}.darwin-amd64.tar.gz -o go.tar.gz || print_error "Failed to download Go."
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go.tar.gz
rm go.tar.gz
export PATH="$PATH:/usr/local/go/bin"
print_success "Go $GO_VERSION_TARGET installed successfully on macOS."
}
# --- Main Script ---
print_info "Starting setup for Caddy with multiple modules..."
# Display selected modules
print_info "Will install the following modules:"
print_info "- WAF Module: $WAF_MODULE"
if [ -n "$EXTRA_MODULES" ]; then
print_info "- Extra Modules: $EXTRA_MODULES"
fi
# Prompt user to confirm
read -p "Continue with these modules? [Y/n] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]] && [[ ! -z $REPLY ]]; then
print_info "Installation cancelled by user."
exit 0
fi
ensure_go_installed
ensure_xcaddy_installed
download_geolite2_db
build_caddy_with_modules
format_caddyfile
check_modules
print_success "Setup completed! You now have Caddy built with WAF and your selected modules."
print_info "To run Caddy: ./caddy run"
print_info "For a list of all installed modules: ./caddy list-modules"

38
it_test.go Normal file
View File

@@ -0,0 +1,38 @@
//go:build it
package caddywaf_test
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
_ "github.com/fabriziosalmi/caddy-waf"
)
func TestWaf_IPBlacklisting(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
route {
waf {
anomaly_threshold 20
rule_file rules.json
ip_blacklist_file ip_blacklist.txt
dns_blacklist_file dns_blacklist.txt
log_severity info
}
}
respond "Hello, World!"
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}

View File

@@ -27,7 +27,7 @@ var sensitiveKeys = []string{
"creditcard", // Credit card number
"cvv", // Card verification value
"cvc", // Card verification code
"eamil", // Email address
"email", // Email address
"phone", // Phone number
"address", // Physical address
"account", // Bank account number
@@ -105,7 +105,6 @@ func (m *Middleware) logRequest(level zapcore.Level, msg string, r *http.Request
)
m.logger.Log(level, msg, allFields...)
}
}
// redactSensitiveFields redacts sensitive information in the log fields.
@@ -130,7 +129,7 @@ func (m *Middleware) redactSensitiveFields(fields []zap.Field) []zap.Field {
// prepareLogFields consolidates the logic for preparing log fields, including common fields and log_id.
func (m *Middleware) prepareLogFields(r *http.Request, fields []zap.Field) []zap.Field {
var logID string
var allFields []zap.Field
allFields := make([]zap.Field, 0)
// Initialize with common fields
var sourceIP, userAgent, requestMethod, requestPath, queryParams string
@@ -208,6 +207,8 @@ func (m *Middleware) redactQueryParams(queryParams string) string {
}
func (m *Middleware) isSensitiveQueryParamKey(key string) bool {
sensitiveKeysMutex.RLock()
defer sensitiveKeysMutex.RUnlock()
for _, sensitiveKey := range sensitiveKeys { // Use package level sensitiveKeys variable
if strings.Contains(key, sensitiveKey) {
return true

View File

@@ -25,7 +25,7 @@ func TestLogRequest(t *testing.T) {
// Create a test request
req := httptest.NewRequest("GET", "/test?foo=bar", nil)
req.RemoteAddr = "192.168.1.1:12345"
req.RemoteAddr = localIP
req.Header.Set("User-Agent", "test-agent")
// Log a test message

View File

@@ -58,33 +58,41 @@ func NewRateLimiter(config RateLimit) (*RateLimiter, error) {
// isRateLimited checks if a given IP is rate limited for a specific path.
func (rl *RateLimiter) isRateLimited(ip, path string) bool {
now := time.Now()
rl.Lock() // Use Lock for write operations or potential creation of nested maps.
defer rl.Unlock()
rl.incrementTotalRequestsMetric() // Increment the total requests received
// SOTA Pattern: Reduce Lock Contention (move expensive regex out of critical section)
matched := false
var key string
// 1. Determine if this path needs limiting (Read-only config access, safe without lock if config is immutable)
if rl.config.MatchAllPaths {
matched = true
key = ip
} else {
// Check if path is matching
if len(rl.config.PathRegexes) > 0 {
matched := false
for _, regex := range rl.config.PathRegexes {
if regex.MatchString(path) {
matched = true
break
}
}
if !matched {
return false // Path does not match any configured paths, no rate limiting
if matched {
key = ip + path
}
}
key = ip + path
}
if !matched && !rl.config.MatchAllPaths {
// Optimization: If no path matched, we don't need to track this request
rl.incrementTotalRequestsMetric()
return false
}
now := time.Now()
rl.Lock() // Critical Section Start
defer rl.Unlock()
rl.incrementTotalRequestsMetric() // Metric under lock to ensure consistency (or use atomic outside)
// Initialize the nested map if it doesn't exist
if _, exists := rl.requests[ip]; !exists {
rl.requests[ip] = make(map[string]*requestCounter)

View File

@@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/phemmer/go-iptrie"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
)
@@ -397,13 +398,13 @@ func TestBlockedRequestPhase1_RateLimiting(t *testing.T) {
Body: "Rate limit exceeded",
},
},
ipBlacklist: NewCIDRTrie(), // Initialize ipBlacklist
ipBlacklist: iptrie.NewTrie(), // Initialize ipBlacklist
dnsBlacklist: make(map[string]struct{}), // Initialize dnsBlacklist
}
// Simulate two requests from the same IP
req := httptest.NewRequest("GET", "http://example.com/api/test", nil)
req.RemoteAddr = "192.168.1.1:12345"
req.RemoteAddr = localIP
w1 := httptest.NewRecorder()
w2 := httptest.NewRecorder()
state1 := &WAFState{}

View File

@@ -8,6 +8,7 @@ import (
"net/url"
"strconv"
"strings"
"unsafe"
"go.uber.org/zap"
)
@@ -16,6 +17,7 @@ import (
type RequestValueExtractor struct {
logger *zap.Logger
redactSensitiveData bool // Add this field
maxBodySize int64
}
// Extraction Target Constants - Improved Readability and Maintainability
@@ -47,8 +49,11 @@ const (
var sensitiveTargets = []string{"password", "token", "apikey", "authorization", "secret"} // Define sensitive targets for redaction as package variable
// NewRequestValueExtractor creates a new RequestValueExtractor with a given logger
func NewRequestValueExtractor(logger *zap.Logger, redactSensitiveData bool) *RequestValueExtractor {
return &RequestValueExtractor{logger: logger, redactSensitiveData: redactSensitiveData}
func NewRequestValueExtractor(logger *zap.Logger, redactSensitiveData bool, maxBodySize int64) *RequestValueExtractor {
if maxBodySize <= 0 {
maxBodySize = 10 * 1024 * 1024 // Default 10MB
}
return &RequestValueExtractor{logger: logger, redactSensitiveData: redactSensitiveData, maxBodySize: maxBodySize}
}
// ExtractValue extracts values based on the target, handling comma separated targets
@@ -78,7 +83,8 @@ func (rve *RequestValueExtractor) ExtractValue(target string, r *http.Request, w
// extractSingleValue extracts a value based on a single target
func (rve *RequestValueExtractor) extractSingleValue(target string, r *http.Request, w http.ResponseWriter) (string, error) {
target = strings.ToUpper(strings.TrimSpace(target))
origTarget := target
targetUpper := strings.ToUpper(strings.TrimSpace(target))
var unredactedValue string
var err error
@@ -121,7 +127,7 @@ func (rve *RequestValueExtractor) extractSingleValue(target string, r *http.Requ
},
}
if extractor, exists := extractionLogic[target]; exists {
if extractor, exists := extractionLogic[targetUpper]; exists {
unredactedValue, err = extractor()
if err != nil {
return "", err // Return error from extractor
@@ -146,13 +152,16 @@ func (rve *RequestValueExtractor) extractSingleValue(target string, r *http.Requ
if err != nil {
return "", err
}
} else if strings.HasPrefix(target, TargetURLParamPrefix) {
unredactedValue, err = rve.extractURLParam(r.URL, strings.TrimPrefix(target, TargetURLParamPrefix), target)
} else if strings.HasPrefix(targetUpper, TargetURLParamPrefix) {
// CRITICAL FIX: Use the original parameter name (without uppercase conversion)
paramName := strings.TrimPrefix(origTarget, TargetURLParamPrefix)
unredactedValue, err = rve.extractURLParam(r.URL, paramName, target)
if err != nil {
return "", err
}
} else if strings.HasPrefix(target, TargetJSONPathPrefix) {
unredactedValue, err = rve.extractValueForJSONPath(r, strings.TrimPrefix(target, TargetJSONPathPrefix), target)
} else if strings.HasPrefix(targetUpper, TargetJSONPathPrefix) {
jsonPath := strings.TrimPrefix(origTarget, TargetJSONPathPrefix)
unredactedValue, err = rve.extractValueForJSONPath(r, jsonPath, target)
if err != nil {
return "", err
}
@@ -162,7 +171,7 @@ func (rve *RequestValueExtractor) extractSingleValue(target string, r *http.Requ
}
// Redact sensitive fields before returning the value (as before)
value := rve.redactValueIfSensitive(target, unredactedValue)
value := rve.RedactValueIfSensitive(target, unredactedValue)
// Log the extracted value (redacted if necessary)
rve.logger.Debug("Extracted value",
@@ -200,13 +209,29 @@ func (rve *RequestValueExtractor) extractBody(r *http.Request, target string) (s
rve.logger.Debug("Request body is empty", zap.String("target", target))
return "", fmt.Errorf("request body is empty for target: %s", target)
}
bodyBytes, err := io.ReadAll(r.Body)
reader := io.LimitReader(r.Body, rve.maxBodySize)
bodyBytes, err := io.ReadAll(reader)
if err != nil {
rve.logger.Error("Failed to read request body", zap.Error(err))
return "", fmt.Errorf("failed to read request body for target %s: %w", target, err)
}
r.Body = http.NoBody // Reset body for next read - using http.NoBody
return string(bodyBytes), nil
// Restore body for next read, verifying if we need to combine with remaining body
// We use io.MultiReader to concatenate the bytes we read with the *remaining* bytes in the original body.
// This ensures that even if we hit the limit, the downstream consumer can read the full body.
// We also ensure the original Closer is preserved.
r.Body = &struct {
io.Reader
io.Closer
}{
Reader: io.MultiReader(strings.NewReader(string(bodyBytes)), r.Body),
Closer: r.Body,
}
// SOTA Pattern: Zero-Copy (avoid allocation for string conversion)
if len(bodyBytes) == 0 {
return "", nil
}
return unsafe.String(&bodyBytes[0], len(bodyBytes)), nil
}
// Helper function to extract all headers
@@ -303,9 +328,17 @@ func (rve *RequestValueExtractor) extractDynamicCookie(r *http.Request, cookieNa
// Helper function to extract URL parameter value
func (rve *RequestValueExtractor) extractURLParam(url *url.URL, paramName string, target string) (string, error) {
paramValue := url.Query().Get(paramName)
// Clean up the paramName by removing any potential remaining prefix
// This is critical for handling cases where the origTarget trimming didn't fully work
cleanParamName := strings.TrimPrefix(paramName, "url_param:")
paramValue := url.Query().Get(cleanParamName)
if paramValue == "" {
rve.logger.Debug("URL parameter not found", zap.String("parameter", paramName), zap.String("target", target))
rve.logger.Debug("URL parameter not found",
zap.String("parameter", paramName),
zap.String("clean_parameter", cleanParamName),
zap.String("target", target),
zap.String("available_params", url.RawQuery)) // Log available params for debugging
return "", fmt.Errorf("url parameter '%s' not found for target: %s", paramName, target)
}
return paramValue, nil
@@ -322,12 +355,13 @@ func (rve *RequestValueExtractor) extractValueForJSONPath(r *http.Request, jsonP
return "", fmt.Errorf("request body is empty for target: %s", target)
}
bodyBytes, err := io.ReadAll(r.Body)
reader := io.LimitReader(r.Body, rve.maxBodySize)
bodyBytes, err := io.ReadAll(reader)
if err != nil {
rve.logger.Error("Failed to read request body", zap.Error(err))
return "", fmt.Errorf("failed to read request body for JSON_PATH target %s: %w", target, err)
}
r.Body = http.NoBody // Reset body for next read
r.Body = io.NopCloser(strings.NewReader(string(bodyBytes))) // Restore body for next read
// Use helper method to dynamically extract value based on JSON path (e.g., 'data.items.0.name').
unredactedValue, err := rve.extractJSONPath(string(bodyBytes), jsonPath)
@@ -339,7 +373,7 @@ func (rve *RequestValueExtractor) extractValueForJSONPath(r *http.Request, jsonP
}
// Helper function to redact value if target is sensitive
func (rve *RequestValueExtractor) redactValueIfSensitive(target string, value string) string {
func (rve *RequestValueExtractor) RedactValueIfSensitive(target string, value string) string {
if rve.redactSensitiveData {
for _, sensitive := range sensitiveTargets {
if strings.Contains(strings.ToLower(target), sensitive) {
@@ -363,13 +397,12 @@ func (rve *RequestValueExtractor) extractAllCookies(cookies []*http.Cookie, logM
return strings.Join(cookieStrings, "; "), nil
}
// Helper function for JSON path extraction.
// Helper function for JSON path extraction
func (rve *RequestValueExtractor) extractJSONPath(jsonStr string, jsonPath string) (string, error) {
// Validate input JSON string
if jsonStr == "" {
return "", fmt.Errorf("json string is empty")
}
// Validate JSON path
if jsonPath == "" {
return "", fmt.Errorf("json path is empty")
@@ -380,7 +413,6 @@ func (rve *RequestValueExtractor) extractJSONPath(jsonStr string, jsonPath strin
if err := json.Unmarshal([]byte(jsonStr), &jsonData); err != nil {
return "", fmt.Errorf("failed to unmarshal JSON: %w", err)
}
// Check if JSON data is valid
if jsonData == nil {
return "", fmt.Errorf("invalid json data")

76
request_body_test.go Normal file
View File

@@ -0,0 +1,76 @@
package caddywaf
import (
"bytes"
"io"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
)
func TestMiddleware_RequestBodyRestoration(t *testing.T) {
// Setup middleware
// Create extractor
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false, 1024*1024) // 1MB limit
t.Run("Body < MaxSize", func(t *testing.T) {
bodyContent := "small body"
req := httptest.NewRequest("POST", "/", strings.NewReader(bodyContent))
// Extract body (simulates WAF inspecting it)
extracted, err := rve.ExtractValue(TargetBody, req, nil)
assert.NoError(t, err)
assert.Equal(t, bodyContent, extracted)
// Verify body is restored and readable again
restoredBody, err := io.ReadAll(req.Body)
assert.NoError(t, err)
assert.Equal(t, bodyContent, string(restoredBody))
})
t.Run("Body > MaxSize", func(t *testing.T) {
// Max size is 1MB. Let's send 2MB.
size := 2 * 1024 * 1024
bodyContent := make([]byte, size)
// Fill with some data
for i := 0; i < size; i++ {
bodyContent[i] = 'a'
}
req := httptest.NewRequest("POST", "/", bytes.NewReader(bodyContent))
// Extract body
extracted, err := rve.ExtractValue(TargetBody, req, nil)
assert.NoError(t, err)
// Extracted should be truncated to 1MB
assert.Equal(t, 1024*1024, len(extracted))
// Verify body restoration
// If the implementation is naive, it might only restore the 1MB we read,
// and the rest is lost because LimitReader consumed the prefix.
// OR, if it restores using LimitReader's underlying reader, maybe it's fine?
// Wait, LimitReader wraps the original request body.
// We read from LimitReader.
// If we replace req.Body with a new reader containing key read bytes...
// The original req.Body (the socket/buffer) has been advanced by 1MB.
// If we set req.Body = NewReader(readBytes), subsequent consumers will read 1MB and then EOF.
// The remaining 1MB in the original req.Body is skipped/lost!
restored, err := io.ReadAll(req.Body)
assert.NoError(t, err)
// This assertion is expected to FAIL if the bug exists for large bodies.
// Use NotEqual or expect failure if we want to demonstrate the bug?
// User says "POST request's body gone". They didn't specify size.
// But let's see what happens.
if len(restored) != size {
t.Logf("Bug confirmed: Expected %d bytes, got %d", size, len(restored))
}
assert.Equal(t, size, len(restored))
})
}

View File

@@ -6,19 +6,22 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/netip"
"sync"
"testing"
"time"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/phemmer/go-iptrie"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestExtractValue(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, true)
rve := NewRequestValueExtractor(logger, true, 0)
tests := []struct {
name string
@@ -31,7 +34,7 @@ func TestExtractValue(t *testing.T) {
name: "Extract METHOD",
target: "METHOD",
setupRequest: func() (*http.Request, http.ResponseWriter) {
req := httptest.NewRequest("POST", "http://example.com", nil)
req := httptest.NewRequest("POST", testURL, nil)
return req, httptest.NewRecorder()
},
expectedValue: "POST",
@@ -51,7 +54,7 @@ func TestExtractValue(t *testing.T) {
name: "Extract USER_AGENT",
target: "USER_AGENT",
setupRequest: func() (*http.Request, http.ResponseWriter) {
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.Header.Set("User-Agent", "test-agent")
return req, httptest.NewRecorder()
},
@@ -62,7 +65,7 @@ func TestExtractValue(t *testing.T) {
name: "Extract HEADERS prefix",
target: "HEADERS:Content-Type",
setupRequest: func() (*http.Request, http.ResponseWriter) {
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
req.Header.Set("Content-Type", "application/json")
return req, httptest.NewRecorder()
},
@@ -83,7 +86,7 @@ func TestExtractValue(t *testing.T) {
name: "Empty target",
target: "",
setupRequest: func() (*http.Request, http.ResponseWriter) {
return httptest.NewRequest("GET", "http://example.com", nil), httptest.NewRecorder()
return httptest.NewRequest("GET", testURL, nil), httptest.NewRecorder()
},
expectedError: true,
},
@@ -141,8 +144,8 @@ func TestRedactValueIfSensitive(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rve := NewRequestValueExtractor(logger, tt.redactSensitive)
result := rve.redactValueIfSensitive(tt.target, tt.value)
rve := NewRequestValueExtractor(logger, tt.redactSensitive, 0)
result := rve.RedactValueIfSensitive(tt.target, tt.value)
if tt.expectedRedacted && result != "REDACTED" {
t.Errorf("Expected REDACTED but got %q", result)
@@ -156,7 +159,7 @@ func TestRedactValueIfSensitive(t *testing.T) {
func TestExtractValue_HeaderCaseInsensitive(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("x-test-header", "test-value")
@@ -169,7 +172,7 @@ func TestExtractValue_HeaderCaseInsensitive(t *testing.T) {
func TestExtractValue_EmptyTarget(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
@@ -181,7 +184,7 @@ func TestExtractValue_EmptyTarget(t *testing.T) {
func TestExtractValue_Method(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
@@ -193,20 +196,20 @@ func TestExtractValue_Method(t *testing.T) {
func TestExtractValue_RemoteIP(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
req.RemoteAddr = "192.168.1.1:12345"
req.RemoteAddr = localIP
w := httptest.NewRecorder()
value, err := rve.ExtractValue("REMOTE_IP", req, w)
assert.NoError(t, err)
assert.Equal(t, "192.168.1.1:12345", value)
assert.Equal(t, localIP, value)
}
func TestExtractValue_Protocol(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
req.Proto = "HTTP/1.1"
@@ -219,7 +222,7 @@ func TestExtractValue_Protocol(t *testing.T) {
func TestExtractValue_Host(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
req.Host = "example.com"
@@ -232,7 +235,7 @@ func TestExtractValue_Host(t *testing.T) {
func TestExtractValue_Args(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/?foo=bar&baz=qux", nil)
w := httptest.NewRecorder()
@@ -244,7 +247,7 @@ func TestExtractValue_Args(t *testing.T) {
func TestExtractValue_UserAgent(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("User-Agent", "test-agent")
@@ -257,7 +260,7 @@ func TestExtractValue_UserAgent(t *testing.T) {
func TestExtractValue_Path(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/test-path", nil)
w := httptest.NewRecorder()
@@ -269,7 +272,7 @@ func TestExtractValue_Path(t *testing.T) {
func TestExtractValue_URI(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/test-path?foo=bar", nil)
w := httptest.NewRecorder()
@@ -281,7 +284,7 @@ func TestExtractValue_URI(t *testing.T) {
func TestExtractValue_Body(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
body := bytes.NewBufferString("test body")
req := httptest.NewRequest("POST", "/", body)
@@ -294,7 +297,7 @@ func TestExtractValue_Body(t *testing.T) {
func TestExtractValue_Headers(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("X-Test-Header", "test-value")
@@ -307,7 +310,7 @@ func TestExtractValue_Headers(t *testing.T) {
func TestExtractValue_Cookies(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
req.AddCookie(&http.Cookie{Name: "test-cookie", Value: "test-value"})
@@ -320,7 +323,7 @@ func TestExtractValue_Cookies(t *testing.T) {
func TestExtractValue_UnknownTarget(t *testing.T) {
logger := zap.NewNop()
rve := NewRequestValueExtractor(logger, false)
rve := NewRequestValueExtractor(logger, false, 0)
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
@@ -361,10 +364,11 @@ func newMockLogger() *MockLogger {
func TestProcessRuleMatch_HighScore(t *testing.T) {
logger := newMockLogger()
middleware := &Middleware{
logger: logger.Logger,
AnomalyThreshold: 100, // High threshold
ruleHits: sync.Map{},
muMetrics: sync.RWMutex{},
logger: logger.Logger,
AnomalyThreshold: 100, // High threshold
ruleHits: sync.Map{},
muMetrics: sync.RWMutex{},
requestValueExtractor: NewRequestValueExtractor(logger.Logger, false, 0), // Initialize
}
rule := &Rule{
@@ -380,7 +384,7 @@ func TestProcessRuleMatch_HighScore(t *testing.T) {
ResponseWritten: false,
}
req := httptest.NewRequest("GET", "http://example.com", nil)
req := httptest.NewRequest("GET", testURL, nil)
// Create a context and add logID to it - FIX: ADD CONTEXT HERE
ctx := context.Background()
@@ -391,7 +395,7 @@ func TestProcessRuleMatch_HighScore(t *testing.T) {
w := httptest.NewRecorder()
// Test blocking rule with high score
shouldContinue := middleware.processRuleMatch(w, req, rule, "value", state)
shouldContinue := middleware.processRuleMatch(w, req, rule, "header", "value", state)
assert.False(t, shouldContinue)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.True(t, state.Blocked)
@@ -416,7 +420,7 @@ func TestValidateRule_EmptyTargets(t *testing.T) {
func TestNewRequestValueExtractor(t *testing.T) {
logger := zap.NewNop()
redactSensitiveData := true
rve := NewRequestValueExtractor(logger, redactSensitiveData)
rve := NewRequestValueExtractor(logger, redactSensitiveData, 0)
assert.NotNil(t, rve)
assert.Equal(t, logger, rve.logger)
@@ -442,9 +446,9 @@ func TestConcurrentRuleEvaluation(t *testing.T) {
},
},
ruleCache: NewRuleCache(),
ipBlacklist: NewCIDRTrie(),
ipBlacklist: iptrie.NewTrie(),
dnsBlacklist: map[string]struct{}{},
requestValueExtractor: NewRequestValueExtractor(logger, false),
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
rateLimiter: func() *RateLimiter {
rl, err := NewRateLimiter(RateLimit{
Requests: 10,
@@ -456,25 +460,20 @@ func TestConcurrentRuleEvaluation(t *testing.T) {
}
return rl
}(),
CustomResponses: map[int]CustomBlockResponse{
403: {
StatusCode: http.StatusForbidden,
Body: "Access Denied",
},
},
CustomResponses: customResponse,
}
// Add some IPs to the blacklist
middleware.ipBlacklist.Insert("192.168.1.0/24")
middleware.ipBlacklist.Insert(netip.MustParsePrefix("192.168.1.0/24"), nil)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
for i := range 100 {
wg.Add(1)
go func(i int) {
defer wg.Done()
req := httptest.NewRequest("GET", "http://example.com", nil)
req.RemoteAddr = fmt.Sprintf("192.168.1.%d:12345", i%256) // Simulate different IPs
req.Header.Set("User-Agent", "test-agent") // Add a header for rule evaluation
req := httptest.NewRequest("GET", testURL, nil)
req.RemoteAddr = fmt.Sprintf("192.168.1.%d", i%256) // Simulate different IPs
req.Header.Set("User-Agent", "test-agent") // Add a header for rule evaluation
w := httptest.NewRecorder()
state := &WAFState{}
middleware.handlePhase(w, req, 1, state)

View File

@@ -2,65 +2,51 @@ package caddywaf
import (
"bytes"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// blockRequest handles blocking a request and logging the details.
func (m *Middleware) blockRequest(recorder http.ResponseWriter, r *http.Request, state *WAFState, statusCode int, reason, ruleID, matchedValue string, fields ...zap.Field) {
// allowRequest - handles request allowing
func (m *Middleware) allowRequest(state *WAFState) {
state.Blocked = false
state.StatusCode = http.StatusOK
state.ResponseWritten = false
m.incrementAllowedRequestsMetric()
}
// blockRequest handles blocking a request and logging the details.
func (m *Middleware) blockRequest(recorder http.ResponseWriter, r *http.Request, state *WAFState, statusCode int, reason, ruleID string, fields ...zap.Field) {
// CRITICAL FIX: Set these flags before any other operations
state.Blocked = true
state.StatusCode = statusCode
state.ResponseWritten = true
// Custom response handling
if resp, ok := m.CustomResponses[statusCode]; ok {
m.logger.Debug("Custom response found for status code",
zap.Int("status_code", statusCode),
zap.String("body", resp.Body),
)
m.writeCustomResponse(recorder, statusCode)
return
}
// Default blocking behavior
logID := uuid.New().String()
if logIDCtx, ok := r.Context().Value(ContextKeyLogId("logID")).(string); ok {
logID = logIDCtx
}
// Prepare standard fields for logging
blockFields := []zap.Field{
zap.String("log_id", logID),
zap.String("source_ip", r.RemoteAddr),
zap.String("user_agent", r.UserAgent()),
zap.String("request_method", r.Method),
zap.String("request_path", r.URL.Path),
zap.String("query_params", r.URL.RawQuery),
// CRITICAL FIX: Log at WARN level for visibility
m.logger.Warn("REQUEST BLOCKED BY WAF", append(fields,
zap.String("rule_id", ruleID),
zap.String("reason", reason),
zap.Int("status_code", statusCode),
zap.Time("timestamp", time.Now()),
zap.String("reason", reason), // Include the reason for blocking
zap.String("rule_id", ruleID), // Include the rule ID
zap.String("matched_value", matchedValue), // Include the matched value
}
zap.String("remote_addr", r.RemoteAddr),
zap.Int("total_score", state.TotalScore))...)
// Debug: Print the blockFields to verify they are correct
m.logger.Debug("Block fields being passed to logRequest",
zap.Any("blockFields", blockFields),
)
// CRITICAL FIX: Increment blocked metrics immediately
m.incrementBlockedRequestsMetric()
// Append additional fields if any
blockFields = append(blockFields, fields...)
// Log the blocked request at WARN level
m.logRequest(zapcore.WarnLevel, "Request blocked", r, blockFields...)
// Write default response with status code using the recorder
// Write a simple text response for blocked requests
recorder.Header().Set("Content-Type", "text/plain")
recorder.WriteHeader(statusCode)
if m.CustomResponses != nil {
m.writeCustomResponse(recorder, state.StatusCode)
} else {
message := fmt.Sprintf("Request blocked by WAF. Reason: %s", reason)
if _, err := recorder.Write([]byte(message)); err != nil {
m.logger.Error("Failed to write blocked response", zap.Error(err))
}
}
}
// responseRecorder captures the response status code, headers, and body.
@@ -85,7 +71,6 @@ func NewResponseRecorder(w http.ResponseWriter) *responseRecorder {
func (r *responseRecorder) WriteHeader(statusCode int) {
r.statusCode = statusCode
r.ResponseWriter.WriteHeader(statusCode)
}
// Header returns the response headers.

View File

@@ -37,7 +37,7 @@ func TestBlockRequest(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/test", nil)
state := &WAFState{}
m.blockRequest(w, r, state, http.StatusForbidden, "test reason", "rule1", "match1")
m.blockRequest(w, r, state, http.StatusForbidden, "test reason", "rule1")
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Equal(t, "Blocked", w.Body.String())
@@ -56,7 +56,7 @@ func TestBlockRequest(t *testing.T) {
r = r.WithContext(ctx)
state := &WAFState{}
m.blockRequest(w, r, state, http.StatusForbidden, "test reason", "rule1", "match1")
m.blockRequest(w, r, state, http.StatusForbidden, "test reason", "rule1")
assert.Equal(t, http.StatusForbidden, w.Code)
assert.True(t, state.Blocked)
@@ -75,7 +75,7 @@ func TestBlockRequest(t *testing.T) {
}
recorder := NewResponseRecorder(w)
m.blockRequest(recorder, r, state, http.StatusForbidden, "test reason", "rule1", "match1")
m.blockRequest(recorder, r, state, http.StatusForbidden, "test reason", "rule1")
assert.Equal(t, http.StatusForbidden, recorder.StatusCode()) // Check the Recorder status code instead
assert.True(t, state.ResponseWritten) // Check that the ResponseWritten flag is set

418
rules-browser-friendly.json Normal file
View File

@@ -0,0 +1,418 @@
[
{
"id": "allow-legit-browsers",
"phase": 1,
"pattern": "(?i)(caddy-waf-ui|Mozilla|Chrome|Safari|Edge|Firefox|Opera|AppleWebKit|Gecko|Trident|MSIE|Googlebot|Bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|Sogou|Exabot|facebot|facebookexternalhit|Twitterbot|Slackbot|LinkedInBot|TelegramBot)",
"targets": [
"HEADERS:User-Agent"
],
"severity": "LOW",
"action": "log",
"score": 1,
"description": "Allow and log traffic from legitimate browsers, search engine crawlers, and social media bots."
},
{
"id": "auth-login-form-missing",
"phase": 2,
"pattern": "^$",
"targets": [
"BODY"
],
"severity": "LOW",
"action": "log",
"score": 3,
"description": "Log login requests that do not contain login form fields"
},
{
"id": "block-scanners",
"phase": 1,
"pattern": "(?i)(nikto|sqlmap|nmap|acunetix|nessus|openvas|wpscan|dirbuster|burpsuite|owasp zap|netsparker|appscan|arachni|skipfish|gobuster|wfuzz|hydra|metasploit|nessus|openvas|qualys|zap|w3af|openwebspider|netsparker|appspider|rapid7|nessus|qualys|nuclei|zgrab|vega|gospider|gxspider|whatweb|xspider|joomscan|uniscan|blindelephant)",
"targets": [
"HEADERS:User-Agent"
],
"severity": "CRITICAL",
"action": "block",
"score": 10,
"description": "Block traffic from known vulnerability scanners and penetration testing tools. Includes more scanners."
},
{
"id": "crlf-injection-headers",
"phase": 1,
"pattern": "(?i)(%0d|\\r)%0a|%0a(%0d|$)|\\n|%0d%0a|%0a%0d|\\r\\n",
"targets": [
"HEADERS"
],
"severity": "MEDIUM",
"action": "log",
"score": 5,
"description": "Log requests with potential CRLF injection characters in headers. Improved pattern matching."
},
{
"id": "csrf-missing-token-post",
"phase": 2,
"pattern": "^$",
"targets": [
"BODY"
],
"severity": "HIGH",
"action": "log",
"score": 6,
"description": "Log POST requests to write operation endpoints that are missing a CSRF token in the body (use this with a condition to ensure that a write operation was done)."
},
{
"id": "header-attacks-consolidated",
"phase": 1,
"pattern": "(?i)(?:1'\\s+OR\\s+'1'='1|<script[^>]*>|\\.\\.\\/\\.\\.\\/etc\\/passwd|1'\\s+UNION\\s+SELECT\\s+NULL--|\\b(?:select|insert|update|delete|drop|alter)\\b(?:\\s|\\/\\*.*?\\*\\/|--.*?)?(?:from|into|where|table)\\b|\\bunion\\b(?:\\s|\\/\\*.*?\\*\\/|--.*?)?\\bselect\\b|'\\s*(?:and|or)\\s*\\d+\\s*(?:=|[<>!]+\\s*)\\d+|\\)\\s*(?:and|or)\\s*\\(\\d+\\s*(?:=|[<>!]+\\s*)\\d+\\)|\\b(?:sleep|benchmark|waitfor\\s+delay)\\s*\\()",
"targets": [
"HEADERS"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Block SQL injection, XSS, and path traversal attempts in headers. Improved pattern matching."
},
{
"id": "http-request-smuggling",
"phase": 1,
"pattern": "(?i)(?:Transfer-Encoding.*?(?:chunked|identity)|Content-Length:\\s*0|(?:Content-Length:\\s*\\d+)(?:\\n.*){2,}|(?:Content-Length:\\s*\\d+)(?:\\n\\w+:\\s*.*?\\n+)|(?:TE:\\s*chunked)(?:\\n.*){2,}|(?:TE:\\s*identity)(?:\\n.*){2,})",
"targets": [
"HEADERS",
"BODY"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Detects HTTP request smuggling patterns. Targets Transfer-Encoding and Content-Length headers."
},
{
"id": "idor-attacks",
"phase": 2,
"pattern": "(?i)(?:(?:\\b(?:id|user|account|profile|order|item|product|comment|post|blog|thread|task|note|group|file|image|report|json|api|rest|download|admin|dashboard|email|video)\\b(?:\\s*)[=:]\\s*(?:[\\-\\/]?\\d+|[\\w\\-\\.]+|[a-f0-9\\-]+))|\\b(?:[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\\b|\\/\\d+(?:\\/|$)|\\/[a-f0-9]{32}|\\/[a-f0-9]{40})",
"targets": [
"URI",
"BODY",
"HEADERS",
"COOKIES"
],
"severity": "MEDIUM",
"action": "log",
"score": 7,
"description": "Detects Insecure Direct Object Reference (IDOR) attempts by identifying common ID patterns in URIs, body, headers and cookies."
},
{
"id": "insecure-deserialization-java",
"phase": 2,
"pattern": "(?:rO0AB|aced0005|\\xac\\xed\\x00\\x05)",
"targets": [
"BODY",
"HEADERS",
"COOKIES"
],
"severity": "CRITICAL",
"action": "block",
"score": 9,
"description": "Block requests containing potential Java serialized objects, including magic bytes for serialized objects."
},
{
"id": "jwt-tampering",
"phase": 1,
"pattern": "^(eyJ[A-Za-z0-9_-]{0,}\\.eyJ[A-Za-z0-9_-]{0,}\\.[A-Za-z0-9_-]{0,})",
"targets": [
"HEADERS:Authorization",
"COOKIES"
],
"severity": "HIGH",
"action": "block",
"score": 8,
"description": "Block potential JWT tampering attempts in Authorization headers or cookies."
},
{
"id": "nosql-injection-attacks",
"phase": 2,
"pattern": "(?i)(?:\\$(?:gt|gte|lt|lte|ne|eq|regex|where|or|and|in|nin|exists|type|jsonSchema|not|mod|elemMatch|all|size|nor|comment|slice|expr|meta|text|search|near|nearSphere|geoWithin|geoIntersects|geoNear)\\b|\\b(?:db|collection|aggregate|mapReduce|count|group|distinct|findOne|find|remove|update|insert)\\b)",
"targets": [
"BODY",
"HEADERS",
"COOKIES"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Block NoSQL injection attempts in request body, headers, and cookies. Targets MongoDB operators and keywords."
},
{
"id": "open-redirect-attempt",
"phase": 2,
"pattern": "(?i)(?:https?://(?:[^/]+@)?[^/]+\\.[^/]+/|\\b(?:redirect|url|next|return|r|u)\\b\\s*=\\s*(?:https?://|//))",
"targets": [
"HEADERS",
"BODY"
],
"severity": "MEDIUM",
"action": "block",
"score": 6,
"description": "Block potential open redirect attempts in request body and headers."
},
{
"id": "path-traversal",
"phase": 1,
"pattern": "(?:\\.\\.[/\\\\]|\\.\\./|\\.\\.\\\\/|%2e%2e[/\\\\]|%2e%2e/|%2e%2e%5c|%252e%252e|\\b(?:etc(?:\\/|%2F)(?:passwd|shadow|hosts)|(?:proc|sys)(?:\\/|%2F)(?:self(?:\\/|%2F)environ|cmdline)|boot(?:\\/|%2F)grub(?:\\/|%2F)grub\\.cfg|\\/\\.\\.(?:\\/|%2F)|(?:\\/|%5c)(\\.\\.){2,}(?:\\/|%5c)|(?:\\.\\.){2,}(?:\\/|%5c)|(?:\\.\\.){2,}|(?:%2e%2e){2,}(?:%2f|%5c)|(?:%2e%2e%2f|%2e%2e%5c){2,}|(?:\\.\\.%2f|\\.\\.%5c){2,}|(?:%252e%252e%2f|%252e%252e%5c){2,}|%252e%252e|%252f%2e%2e|%255c%2e%2e|\\/\\.(?:\\/|%2F)|\\%2e(?:%2f|%5c))\\b)",
"targets": [
"URI",
"HEADERS"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Block path traversal attempts and direct access to sensitive files (Target: URI and Headers). Improved and more aggressive pattern matching, including more obfuscation techniques."
},
{
"id": "rce-commands-expanded",
"phase": 2,
"pattern": "(?i)(?:\\b(?:cat|base64|whoami|echo|curl|wget|bash|sh|python|perl|ls|id|ping|nslookup|ipconfig|ifconfig|powershell)\\b)",
"targets": [
"ARGS",
"HEADERS"
],
"severity": "HIGH",
"action": "block",
"score": 5,
"description": "Expanded rule to block more RCE related commands and utilities."
},
{
"id": "rfi-http-url",
"phase": 2,
"pattern": "(?i)https?:\\/\\/[^\\s]+",
"targets": [
"URI",
"ARGS",
"HEADERS"
],
"severity": "HIGH",
"action": "block",
"score": 8,
"description": "Block direct use of HTTP or HTTPS URLs for inclusion."
},
{
"id": "sensitive-files",
"phase": 1,
"pattern": "(?i)(?:/\\.git/(?:HEAD|index|config|refs|objects)|/\\.env(?:\\.local|\\.dev|\\.prod)?$|/\\.htaccess$|/\\.htpasswd$|/\\.svn/|/\\.DS_Store$|\\/WEB-INF\\/|\\/WEB-INF\\/web\\.xml|\\/META-INF\\/|\\.git/\\s*(?:H\\.E\\.A\\.D|HEAD)|\\.dockerenv|server-status|\\b(?:config|database|credentials|secrets|private|local|development|staging|production|backup|default)\\b(?:[\\-_\\.]?)(?:[a-z0-9]+)?\\.(?:json|yaml|yml|ini|properties|txt|conf|toml|lock|log|bak|swp|orig|dist|sample|example|template|env|sql))",
"targets": [
"URI"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Block access to sensitive files and directories (Target: URI). Expanded rule to include more config and backup file names."
},
{
"id": "sensitive-files-expanded",
"phase": 1,
"pattern": "(?i)(?:/\\.git/(?:HEAD|index|config|refs|objects)|/\\.env(?:\\.local|\\.dev|\\.prod)?$|/\\.htaccess$|/\\.htpasswd$|/\\.svn/|/\\.DS_Store$|\\/WEB-INF\\/|\\/WEB-INF\\/web.xml|\\/META-INF\\/|\\.git/\\s*(?:H\\.E\\.A\\.D|HEAD)|\\.dockerenv|server-status)",
"targets": [
"URI"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Expanded rule to block access to more sensitive files and account for obfuscation."
},
{
"id": "sql-injection",
"phase": 2,
"pattern": "(?i)(?:\\b(?:select|insert|update|delete|drop|alter|truncate|create|grant|revoke)\\b(?:\\s|\\/\\*.*?\\*\\/|--.*?)?(?:from|into|where|table|index|user|procedure|function|database)\\b|\\bunion\\b(?:\\s|\\/\\*.*?\\*\\/|--.*?)?(?:all|distinct)?(?:\\s|\\/\\*.*?\\*\\/|--.*?)?\\bselect\\b|'\\s*(?:and|or)\\s*['\\d]+\\s*(?:=|[<>]=?|!=)\\s*['\\d]+|\\)\\s*(?:and|or)\\s*\\([\\d]+\\s*(?:=|[<>]=?|!=)\\s*[\\d]+\\)|\\b(?:sleep|benchmark|waitfor\\s+delay)\\s*\\(|(?:\\bexec\\b|xp_cmdshell))",
"targets": [
"ARGS",
"BODY"
],
"severity": "HIGH",
"action": "block",
"score": 7,
"description": "Block SQL injection attempts in request arguments and body. Removed HEADERS target to avoid false positives."
},
{
"id": "sql-injection-improved-basic",
"phase": 2,
"pattern": "(?i)(?:'\\s*(?:and|or)\\s*\\d+\\s*[=<>!]+\\s*\\d+|['\"]\\s*\\d+\\s*[=<>!]+\\s*['\"]|'\\s*\\+\\s*'|--\\s*-|-{2,})",
"targets": [
"ARGS",
"BODY"
],
"severity": "HIGH",
"action": "block",
"score": 8,
"description": "Improved rule to catch basic SQL injection including quotes and boolean logic. Removed HEADERS and COOKIES targets and removed double quote pattern to prevent false positives."
},
{
"id": "ssrf-attacks",
"phase": 2,
"pattern": "(?i)(?:(?:https?|ftp|gopher|dict|ldap|tftp|file)://(?:[^/]+@)?(?:(?:127\\.0\\.0\\.\\d{1,3}|10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|172\\.(?:1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|169\\.254\\.\\d{1,3}\\.\\d{1,3}|(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|localhost|0\\.0\\.0\\.0|::1|\\d{1,10})|[^/]+\\.(?:internal|local|intranet|test))(?:\\:\\d{1,5})?(?:/[^\\s]*)?|\\b(?:metadata|aws|digitalocean|google|azure)\\b|\\b(?:169\\.254\\.\\d{1,3}\\.\\d{1,3})\\b(?:/[^\\s]*)?)",
"targets": [
"BODY"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Block Server-Side Request Forgery (SSRF) attempts, including internal IP ranges and cloud metadata endpoints. Removed HEADERS and COOKIES targets to prevent false positives with browser cookies."
},
{
"id": "ssrf-internal-ip",
"phase": 2,
"pattern": "(?:127\\.0\\.0\\.1|10\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.|192\\.168\\.)",
"targets": [
"URI",
"ARGS"
],
"severity": "HIGH",
"action": "block",
"score": 7,
"description": "Block SSRF to Internal IPs."
},
{
"id": "ssrf-reserved-ip",
"phase": 2,
"pattern": "(?:0\\.|169\\.254\\.|224\\.|240\\.|255\\.)",
"targets": [
"URI",
"ARGS"
],
"severity": "MEDIUM",
"action": "block",
"score": 5,
"description": "Block SSRF to Reserved/Multicast IPs."
},
{
"id": "ssti-attacks",
"phase": 2,
"pattern": "(?i)(?:\\{\\{.*?\\}\\}|\\{\\%.*?\\%\\}|\\$\\{.*?\\}|\\#\\{.*?\\}|\\$\\(.*?\\)|\\{\\*.*?\\*\\}|\\#\\*.*?\\*\\#|<%[=]?.*?%>|@\\{.*?\\}|\\b(?:Runtime|Process|exec|System|getClass|ClassLoader|loadLibrary|forName|newInstance|getMethod|invoke|getConstructor|getDeclaredMethod|getDeclaredField|setAccessible|getDeclaredConstructor|getInputStream|getOutputStream|get|put|setAttribute|getProperty|setProperty|setSecurityManager|load|defineClass|new|clone|readObject|writeObject|call|apply|bind|super)\\b\\s*\\(|\\b(?:T|Math|Object|String|Boolean|Number|BigInteger|BigDecimal|Date|List|Map|Set|Queue|Array|Tuple|Pattern|Locale|Class|ClassLoader|Proxy|SecurityManager|Thread|ThreadGroup)\\b)",
"targets": [
"BODY"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Block Server-Side Template Injection (SSTI) attacks in request body. Removed HEADERS and COOKIES targets to prevent false positives."
},
{
"id": "unusual-paths",
"phase": 1,
"pattern": "(?i)(?:/wp-admin|/phpmyadmin|/admin|/login|/cgi-bin|/shell|/backdoor|/cmd|/exec|/bin/(?:sh|bash|zsh)|/console|/setup|/test|\\.php$|\\.asp$|\\.aspx$|\\.jsp$|\\.do$|\\.action$|\\.pl$|\\.py$|\\.cgi$|\\.cfm$|\\.rb$|\\.php[0-9]?$|\\.phtml$|\\.htaccess$|\\.htpasswd$|\\.ini$|\\.config$|\\.lock$|\\.log$|\\.bak$|\\.swp$|\\.orig$|\\.dist$|\\.sample$|\\.example$|\\.template$|\\.env$)",
"targets": [
"URI"
],
"severity": "MEDIUM",
"action": "block",
"score": 7,
"description": "Block requests to unusual or suspicious paths and common scripting extensions (Target: URI). Expanded rule for more file types and endpoints."
},
{
"id": "xss-attacks",
"phase": 2,
"pattern": "(?i)(?:<script[^>]*>|<img[^>]*\\s+onerror=|javascript:|data:|vbscript:|<svg[^>]*\\s+onload=|alert\\(|document\\.(?:cookie|location)|eval\\(|base64_(?:encode|decode)|expression\\(|\\b(?:on(?:mouse(?:over|out|down|up|move)|focus|blur|click|key(?:press|down|up)|load|error|submit|reset|change))\\s*=|\\bstyle\\s*=|(?:&#[xX]?[0-9a-fA-F]+;)+|%[0-9a-fA-F]{2,}|\\biframe[^>]*srcdoc\\s*=)",
"targets": [
"BODY",
"ARGS"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Block XSS attempts using HTML tags, event handlers, javascript: protocol, encoded characters, iframe srcdoc, etc. Removed HEADERS and COOKIES targets and simplified pattern to prevent false positives."
},
{
"id": "xss-improved-encoding",
"phase": 2,
"pattern": "(?i)(?:<script[^>]*>|<img[^>]*\\s+onerror=|javascript:|data:|vbscript:|<svg[^>]*\\s+onload=|alert\\(|document\\.(?:cookie|location)|eval\\(|base64_(?:encode|decode)|expression\\(|\\b(?:on(?:mouse(?:over|out|down|up|move)|focus|blur|click|key(?:press|down|up)|load|error|submit|reset|change))\\s*=|\\bstyle\\s*=|(?:&#[xX]?[0-9a-fA-F]+;)+|%[0-9a-fA-F]{2,}|\\biframe[^>]*srcdoc\\s*=)",
"targets": [
"ARGS",
"BODY"
],
"severity": "HIGH",
"action": "block",
"score": 8,
"description": "Improved XSS rule to catch encoded payloads and iframe srcdoc. Removed HEADERS target to prevent false positives."
},
{
"id": "browser-integrity-sec-fetch-dest-missing-block",
"phase": 1,
"pattern": "^$",
"targets": [
"HEADERS:Sec-Fetch-Dest-Presence-Check"
],
"severity": "CRITICAL",
"action": "log",
"score": 5,
"description": "Changed to LOG: Requests missing Sec-Fetch-Dest header. Very strong indicator of non-browser traffic but can cause issues with normal browsers."
},
{
"id": "browser-integrity-sec-fetch-mode-missing-log-score",
"phase": 1,
"pattern": "^$",
"targets": [
"HEADERS:Sec-Fetch-Mode-Presence-Check"
],
"severity": "MEDIUM",
"action": "log",
"score": 3,
"description": "LOG+SCORE: Requests missing Sec-Fetch-Mode header. Suspicious, might be non-browser. Moderate confidence, low to moderate false positive risk. Assigns score."
},
{
"id": "browser-integrity-sec-fetch-site-missing-log-score",
"phase": 1,
"pattern": "^$",
"targets": [
"HEADERS:Sec-Fetch-Site-Presence-Check"
],
"severity": "LOW",
"action": "log",
"score": 1,
"description": "LOG+SCORE: Requests missing Sec-Fetch-Site header. Less critical but still worth monitoring. Lower confidence, slightly higher false positive risk. Assigns score."
},
{
"id": "browser-integrity-sec-fetch-user-missing-log-score",
"phase": 1,
"pattern": "^$",
"targets": [
"HEADERS:Sec-Fetch-User-Presence-Check"
],
"severity": "LOW",
"action": "log",
"score": 1,
"description": "LOG+SCORE: Requests missing Sec-Fetch-User header. Might indicate non-user-initiated actions or bots. Lowest confidence, moderate to higher false positive risk. Assigns score, mainly for correlation."
},
{
"id": "browser-integrity-sec-fetch-dest-not-document-ua-suspicious-log-score",
"phase": 1,
"pattern": "(?i)^(?:script|style|image|font|fetch|xhr|audio|video|manifest|object|embed|report|worker|sharedworker|serviceworker|empty|unknown)$",
"targets": [
"HEADERS:Sec-Fetch-Dest-Not"
],
"severity": "MEDIUM",
"action": "log",
"score": 3,
"description": "LOG+SCORE: Sec-Fetch-Dest not 'document' AND Suspicious User-Agent. More suspicious combination. Moderate confidence, reduced false positive risk by combining checks. Assigns score."
},
{
"id": "browser-integrity-sec-fetch-mode-no-cors-document-log-score",
"phase": 1,
"pattern": "(?i)^(?:no-cors)$",
"targets": [
"HEADERS:Sec-Fetch-Mode"
],
"severity": "LOW",
"action": "log",
"score": 2,
"description": "LOG+SCORE: 'document' requests with Sec-Fetch-Mode: 'no-cors'. Less common for initial page loads. Lower confidence, low false positive risk. Assigns score."
},
{
"id": "browser-integrity-sec-fetch-site-cross-site-document-log-score",
"phase": 1,
"pattern": "(?i)^(?:cross-site)$",
"targets": [
"HEADERS:Sec-Fetch-Site"
],
"severity": "LOW",
"action": "log",
"score": 1,
"description": "LOG+SCORE: 'document' requests with Sec-Fetch-Site: 'cross-site'. Common for external links, primarily for monitoring cross-site traffic patterns. Lowest confidence, very low false positive risk. Assigns score for traffic analysis."
}
]

View File

@@ -8,19 +8,21 @@ import (
"os"
"regexp"
"sort"
"strings"
"sync/atomic"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, rule *Rule, value string, state *WAFState) bool {
func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, rule *Rule, target, value string, state *WAFState) bool {
logID := r.Context().Value(ContextKeyLogId("logID")).(string)
redactedValue := m.requestValueExtractor.RedactValueIfSensitive(target, value)
m.logRequest(zapcore.DebugLevel, "Rule Matched", r, // More concise log message
zap.String("rule_id", string(rule.ID)),
zap.String("target", strings.Join(rule.Targets, ",")),
zap.String("value", value),
zap.String("rule_id", rule.ID),
zap.String("target", target), // Log the specific target that matched
zap.String("value", redactedValue),
zap.String("description", rule.Description),
zap.Int("score", rule.Score),
zap.Int("anomaly_threshold_config", m.AnomalyThreshold), // ADDED: Log configured anomaly threshold
@@ -37,70 +39,89 @@ func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, ru
state.TotalScore += rule.Score
m.logRequest(zapcore.DebugLevel, "Anomaly score increased", r, // Corrected argument order - 'r' is now the third argument
zap.String("log_id", logID),
zap.String("rule_id", string(rule.ID)),
zap.String("rule_id", rule.ID),
zap.Int("score_increase", rule.Score),
zap.Int("old_score", oldScore),
zap.Int("new_score", state.TotalScore),
zap.Int("anomaly_threshold", m.AnomalyThreshold),
)
shouldBlock := !state.ResponseWritten && (state.TotalScore >= m.AnomalyThreshold || rule.Action == "block")
blockReason := ""
// CRITICAL FIX: Check if "mode" field in rule doesn't match the required "action" field
// There's a mismatch between Rule.Action and the "mode" field in the JSON
// Map "mode" to "action" for proper rule processing
actualAction := rule.Action
// Debug the actual action field value to verify what's being used
m.logger.Debug("Rule action/mode check",
zap.String("rule_id", rule.ID),
zap.String("action_field", rule.Action),
zap.Int("score", rule.Score),
zap.Int("threshold", m.AnomalyThreshold),
zap.Int("total_score", state.TotalScore))
// CRITICAL FIX: Check if the request should be blocked
exceedsThreshold := !state.ResponseWritten && (state.TotalScore >= m.AnomalyThreshold)
explicitBlock := !state.ResponseWritten && (actualAction == "block")
shouldBlock := exceedsThreshold || explicitBlock
// Set appropriate block reason based on what triggered the block
blockReason := ""
if shouldBlock {
blockReason = "Anomaly threshold exceeded"
if rule.Action == "block" {
if exceedsThreshold {
blockReason = "Anomaly threshold exceeded"
}
if explicitBlock {
blockReason = "Rule action is 'block'"
}
}
m.logRequest(zapcore.DebugLevel, "Determining Block Action", r, // More descriptive log message
zap.String("action", rule.Action),
zap.Bool("should_block", shouldBlock),
zap.String("block_reason", blockReason),
zap.Int("total_score", state.TotalScore), // ADDED: Log total score in block decision log
zap.Int("anomaly_threshold", m.AnomalyThreshold), // ADDED: Log anomaly threshold in block decision log
)
// Ensure we're setting the blocked state
state.Blocked = true
state.StatusCode = http.StatusForbidden
if shouldBlock {
m.blockRequest(w, r, state, http.StatusForbidden, blockReason, string(rule.ID), value,
// Block the request and write the response immediately
m.blockRequest(w, r, state, http.StatusForbidden, blockReason, rule.ID,
zap.Int("total_score", state.TotalScore),
zap.Int("anomaly_threshold", m.AnomalyThreshold),
zap.String("final_block_reason", blockReason), // ADDED: Clarify block reason in blockRequest log
zap.String("final_block_reason", blockReason),
zap.Bool("explicitly_blocked", explicitBlock),
zap.Bool("threshold_exceeded", exceedsThreshold),
)
// Return false to stop processing more rules
return false
}
if rule.Action == "log" {
m.logRequest(zapcore.InfoLevel, "Rule action: Log", r,
zap.String("log_id", logID),
zap.String("rule_id", string(rule.ID)),
zap.String("rule_id", rule.ID),
zap.Int("total_score", state.TotalScore), // ADDED: Log total score for log action
zap.Int("anomaly_threshold", m.AnomalyThreshold), // ADDED: Log anomaly threshold for log action
)
} else if !shouldBlock && !state.ResponseWritten {
m.logRequest(zapcore.DebugLevel, "Rule action: No Block", r,
zap.String("log_id", logID),
zap.String("rule_id", string(rule.ID)),
zap.String("rule_id", rule.ID),
zap.String("action", rule.Action),
zap.Int("total_score", state.TotalScore),
zap.Int("anomaly_threshold", m.AnomalyThreshold),
)
}
// Continue processing other rules
return true
}
// incrementRuleHitCount increments the hit counter for a given rule ID.
func (m *Middleware) incrementRuleHitCount(ruleID RuleID) {
hitCount := HitCount(1) // Default increment
if currentCount, loaded := m.ruleHits.Load(ruleID); loaded {
hitCount = currentCount.(HitCount) + 1
}
m.ruleHits.Store(ruleID, hitCount)
// SOTA Pattern: Wait-Free / Lock-Free Data Structures (using atomic)
counterInterface, _ := m.ruleHits.LoadOrStore(ruleID, &atomic.Int64{})
counter := counterInterface.(*atomic.Int64)
newVal := counter.Add(1)
m.logger.Debug("Rule hit count updated",
zap.String("rule_id", string(ruleID)),
zap.Int("hit_count", int(hitCount)), // More descriptive log field
zap.Int64("hit_count", newVal),
)
}
@@ -221,11 +242,11 @@ func (m *Middleware) loadRulesFromFile(path string, ruleIDs map[string]bool) (va
continue
}
if _, exists := ruleIDs[string(rule.ID)]; exists {
if _, exists := ruleIDs[rule.ID]; exists {
fileInvalidRules = append(fileInvalidRules, fmt.Sprintf("Duplicate rule ID '%s' at index %d", rule.ID, i))
continue
}
ruleIDs[string(rule.ID)] = true // Track rule IDs to prevent duplicates
ruleIDs[rule.ID] = true // Track rule IDs to prevent duplicates
// RuleCache handling (compile and cache regex)
if cachedRegex, exists := m.ruleCache.Get(rule.ID); exists {

View File

@@ -234,17 +234,15 @@
{
"id": "sql-injection-improved-basic",
"phase": 2,
"pattern": "(?i)(?:'\\s*(?:and|or)\\s*\\d+\\s*[=<>!]+\\s*\\d+|['\"]\\s*\\d+\\s*[=<>!]+\\s*['\"]|'\\s*\\+\\s*'|--\\s*-|-{2,}|\")",
"pattern": "(?i)(?:'\\s*(?:and|or)\\s*\\d+\\s*[=<>!]+\\s*\\d+|['\"]\\s*\\d+\\s*[=<>!]+\\s*['\"]|'\\s*\\+\\s*'|--\\s*-|-{2,})",
"targets": [
"ARGS",
"BODY",
"HEADERS",
"COOKIES"
"BODY"
],
"severity": "HIGH",
"action": "block",
"score": 8,
"description": "Improved rule to catch basic SQL injection including quotes and boolean logic."
"description": "Improved rule to catch basic SQL injection including quotes and boolean logic. Removed HEADERS and COOKIES targets and removed double quote pattern to prevent false positives."
},
{
"id": "ssrf-attacks",
@@ -315,16 +313,15 @@
{
"id": "xss-attacks",
"phase": 2,
"pattern": "(?i)(?:<script[^>]*>|<img[^>]*\\s+onerror=|javascript:|data:|vbscript:|<svg[^>]*\\s+onload=|alert\\(|document\\.(?:cookie|location)|eval\\(|base64_(?:encode|decode)|expression\\(|\\b(?:on(?:mouse(?:over|out|down|up|move)|focus|blur|click|key(?:press|down|up)|load|error|submit|reset|change))\\s*=|\\bstyle\\s*=|(?:&#[xX]?[0-9a-fA-F]+;)+|%[0-9a-fA-F]{2,}|\\biframe[^>]*srcdoc\\s*=|\\bevent\\b\\s*=\\s*['\"](?:javascript:).*?['\"]|url\\s*\\([\\s\\n]*?(?:javascript:).*?\\)|\\b(?:\\b(?:src|href|action|data|code)\\s*=\\s*['\"]?(?:javascript:|data:)|\\b(?:formaction|background|poster|xlink:href)\\s*=\\s*['\"]?(?:javascript:|data:))|\\b(?:svg|math|marquee|audio|video|embed|object|plaintext|isindex)\\b)",
"pattern": "(?i)(?:<script[^>]*>|<img[^>]*\\s+onerror=|javascript:|data:|vbscript:|<svg[^>]*\\s+onload=|alert\\(|document\\.(?:cookie|location)|eval\\(|base64_(?:encode|decode)|expression\\(|\\b(?:on(?:mouse(?:over|out|down|up|move)|focus|blur|click|key(?:press|down|up)|load|error|submit|reset|change))\\s*=|\\bstyle\\s*=|(?:&#[xX]?[0-9a-fA-F]+;)+|%[0-9a-fA-F]{2,}|\\biframe[^>]*srcdoc\\s*=)",
"targets": [
"BODY",
"HEADERS",
"COOKIES"
"ARGS"
],
"severity": "HIGH",
"action": "block",
"score": 9,
"description": "Block XSS attempts using HTML tags, event handlers, javascript: protocol, encoded characters, iframe srcdoc, event attributes, url functions, and other vectors in request body, headers and cookies. Improved pattern matching, including more attack vectors."
"description": "Block XSS attempts using HTML tags, event handlers, javascript: protocol, encoded characters, iframe srcdoc, etc. Removed HEADERS and COOKIES targets and simplified pattern to prevent false positives."
},
{
"id": "xss-improved-encoding",

View File

@@ -146,10 +146,11 @@ func TestProcessRuleMatch(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &Middleware{
logger: logger,
AnomalyThreshold: tt.anomalyThreshold,
ruleHits: sync.Map{},
muMetrics: sync.RWMutex{},
logger: logger,
AnomalyThreshold: tt.anomalyThreshold,
ruleHits: sync.Map{},
muMetrics: sync.RWMutex{},
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
}
w := httptest.NewRecorder()
@@ -162,7 +163,7 @@ func TestProcessRuleMatch(t *testing.T) {
ResponseWritten: tt.responseWritten,
}
result := m.processRuleMatch(w, r, &tt.rule, "test-value", state)
result := m.processRuleMatch(w, r, &tt.rule, "ARGS", "test-value", state)
if result == tt.wantBlock {
t.Errorf("processRuleMatch() returned %v, want %v", result, !tt.wantBlock)
}
@@ -187,7 +188,7 @@ func TestLoadRules(t *testing.T) {
"action": "block"
}
]`
os.WriteFile(validRuleFile, []byte(validRules), 0644)
os.WriteFile(validRuleFile, []byte(validRules), 0o644)
invalidRuleFile := filepath.Join(tmpDir, "invalid_rules.json")
invalidRules := `[
@@ -199,7 +200,7 @@ func TestLoadRules(t *testing.T) {
"score": -1
}
]`
os.WriteFile(invalidRuleFile, []byte(invalidRules), 0644)
os.WriteFile(invalidRuleFile, []byte(invalidRules), 0o644)
tests := []struct {
name string

145
sample_rules.json Normal file
View File

@@ -0,0 +1,145 @@
[
{
"id": "BLOCK-NIKTO",
"phase": 2,
"pattern": "nikto",
"targets": [
"USER_AGENT"
],
"severity": "critical",
"score": 5,
"mode": "block",
"description": "Block Nikto scanner",
"priority": 10
},
{
"id": "TEST-RULE-1",
"phase": 2,
"pattern": "low_score_test",
"targets": [
"URL_PARAM:test"
],
"severity": "low",
"score": 1,
"mode": "log",
"description": "Low score test rule",
"priority": 10
},
{
"id": "TEST-RULE-PARAM1",
"phase": 2,
"pattern": "score2",
"targets": [
"URL_PARAM:param1"
],
"severity": "medium",
"score": 2,
"mode": "log",
"description": "Medium score test rule for param1",
"priority": 10
},
{
"id": "TEST-RULE-PARAM2",
"phase": 2,
"pattern": "score2",
"targets": [
"URL_PARAM:param2"
],
"severity": "medium",
"score": 2,
"mode": "log",
"description": "Medium score test rule for param2",
"priority": 10
},
{
"id": "TEST-RULE-PARAM1-HIGH",
"phase": 2,
"pattern": "score3",
"targets": [
"URL_PARAM:param1"
],
"severity": "high",
"score": 3,
"mode": "log",
"description": "High score test rule for param1",
"priority": 10
},
{
"id": "TEST-RULE-PARAM2-HIGH",
"phase": 2,
"pattern": "score3",
"targets": [
"URL_PARAM:param2"
],
"severity": "high",
"score": 3,
"mode": "log",
"description": "High score test rule for param2",
"priority": 10
},
{
"id": "TEST-RULE-PARAM3-HIGH",
"phase": 2,
"pattern": "score3",
"targets": [
"URL_PARAM:param3"
],
"severity": "high",
"score": 3,
"mode": "log",
"description": "High score test rule for param3",
"priority": 10
},
{
"id": "TEST-RULE-BLOCK",
"phase": 2,
"pattern": "true",
"targets": [
"URL_PARAM:block"
],
"severity": "critical",
"score": 0,
"mode": "block",
"description": "Block action test rule",
"priority": 10
},
{
"id": "TEST-RULE-INCR-1",
"phase": 2,
"pattern": "score1",
"targets": [
"URL_PARAM:increment"
],
"severity": "low",
"score": 1,
"mode": "log",
"description": "Incremental test rule 1",
"priority": 10
},
{
"id": "TEST-RULE-INCR-2",
"phase": 2,
"pattern": "score2",
"targets": [
"URL_PARAM:increment"
],
"severity": "medium",
"score": 2,
"mode": "log",
"description": "Incremental test rule 2",
"priority": 10
},
{
"id": "TEST-RULE-INCR-3",
"phase": 2,
"pattern": "score3",
"targets": [
"URL_PARAM:increment"
],
"severity": "high",
"score": 3,
"mode": "log",
"description": "Incremental test rule 3",
"priority": 10
}
]

17
test.caddyfile Normal file
View File

@@ -0,0 +1,17 @@
{
debug
auto_https off
admin localhost:2019
}
:8080 {
route {
waf {
rule_file ./sample_rules.json
anomaly_threshold 5
log_severity debug
metrics_endpoint /metrics
}
respond "Hello world!"
}
}

335
test_anomalythreshold.py Normal file
View File

@@ -0,0 +1,335 @@
#!/usr/bin/env python3
import requests
import json
import time
import sys
import argparse
from termcolor import colored
# --- setup_args function remains the same ---
def setup_args():
parser = argparse.ArgumentParser(description='Test WAF anomaly threshold behavior')
parser.add_argument('--url', default='http://localhost:8080', help='URL to test (default: http://localhost:8080)')
parser.add_argument('--threshold', type=int, default=5, help='Configured anomaly threshold (default: 5)')
parser.add_argument('--debug', action='store_true', help='Enable debug output for response headers')
parser.add_argument('--verbose', action='store_true', help='Show verbose test details')
return parser.parse_args()
# --- send_request function remains the same ---
def send_request(url, payload, headers=None, expected_status=None, debug=False):
"""
Send a request with the given payload and validate the response.
Returns:
tuple: (response object or None, dict of found WAF headers, bool or None for passed status)
'passed' is True if status matches expected_status, False if it doesn't or error occurs,
None if expected_status was not provided.
"""
if headers is None:
headers = {'User-Agent': 'WAF-Threshold-Test/1.0'}
print(colored(f"\n>>> Sending request to {url}", "blue"))
print(colored(f">>> Payload: {payload}", "blue"))
passed = None # Default if no expectation set
try:
response = requests.get(
url,
params=payload,
headers=headers,
timeout=10 # Increased timeout slightly
)
status = response.status_code
# Determine pass/fail based on expected status
if expected_status is not None:
passed = (status == expected_status)
color = "green" if passed else "red"
result_text = "✓ PASS" if passed else "✗ FAIL"
print(colored(f"<<< Status: {status} (Expected: {expected_status}) - {result_text}", color))
else:
# No expected status, just report what we got
print(colored(f"<<< Status: {status}", "yellow"))
response_text = response.text
print(colored(f"<<< Response: {response_text[:100]}...", "yellow") if len(response_text) > 100 else colored(f"<<< Response: {response_text}", "yellow"))
# Check for WAF-specific headers
waf_headers = {}
if debug:
print(colored("\n--- Response Headers ---", "cyan"))
for header, value in response.headers.items():
print(colored(f" {header}: {value}", "yellow"))
# Check for common WAF score headers - these may vary based on your WAF implementation
lower_header = header.lower()
if lower_header in ('x-waf-score', 'x-waf-anomaly-score', 'x-waf-status', 'x-waf-rules', 'x-waf-action'):
waf_headers[lower_header] = value
print(colored(f" Found WAF header: {header}={value}", "green"))
print(colored("--- End Headers ---", "cyan"))
return response, waf_headers, passed
except requests.exceptions.Timeout:
print(colored(f"Error: Request timed out after 10 seconds.", "red"))
passed = False # Timeout is a failure if status was expected
if expected_status is not None:
print(colored(f"<<< Status: TIMEOUT (Expected: {expected_status}) - ✗ FAIL", "red"))
else:
print(colored(f"<<< Status: TIMEOUT", "red"))
return None, {}, passed
except requests.exceptions.RequestException as e:
print(colored(f"Error sending request: {str(e)}", "red"))
passed = False # Request error is a failure if status was expected
if expected_status is not None:
print(colored(f"<<< Status: ERROR (Expected: {expected_status}) - ✗ FAIL", "red"))
else:
print(colored(f"<<< Status: ERROR", "red"))
return None, {}, passed
# --- test_anomaly_threshold function is UPDATED ---
def test_anomaly_threshold(base_url, threshold, debug=False, verbose=False):
"""Test that anomaly threshold is properly enforced."""
print(colored(f"\n=== Testing Anomaly Threshold (threshold={threshold}) ===", "cyan"))
results_data = {} # Store results keyed by test name
# --- Original Tests ---
# Test 1: Low score (should pass, 200 OK)
print(colored("\nTest 1: Low-score rule (should pass with 200 OK)", "magenta"))
low_score_payload = {'test': 'low_score_test'} # RULE-1 (Score 1)
expected_score = 1
low_response, low_headers, test1_passed = send_request(base_url, low_score_payload, expected_status=200, debug=debug)
results_data["Test 1 (Low score)"] = (test1_passed, low_response.status_code if low_response else "ERROR", f"Expected 200 OK for low score ({expected_score}) < threshold ({threshold})")
print(colored(f"-> Expected anomaly score contribution: {expected_score}", "yellow"))
# Test 2: Score below threshold (should pass, 200 OK)
print(colored(f"\nTest 2: Score below threshold (should pass with 200 OK)", "magenta"))
below_threshold_payload = {'param1': 'score2', 'param2': 'score2'} # RULE-PARAM1 (2) + RULE-PARAM2 (2) = 4
expected_total_score = 4
below_response, below_headers, test2_passed = send_request(base_url, below_threshold_payload, expected_status=200, debug=debug)
results_data["Test 2 (Below threshold)"] = (test2_passed, below_response.status_code if below_response else "ERROR", f"Expected 200 OK for score ({expected_total_score}) < threshold ({threshold})")
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
# Test 3: Score exceeding threshold (should block, 403 Forbidden)
print(colored(f"\nTest 3: Score exceeding threshold (should block with 403 Forbidden)", "magenta"))
exceed_threshold_payload = {'param1': 'score3', 'param2': 'score3'} # RULE-PARAM1-HIGH (3) + RULE-PARAM2-HIGH (3) = 6
expected_total_score = 6
exceed_response, exceed_headers, test3_passed = send_request(base_url, exceed_threshold_payload, expected_status=403, debug=debug)
results_data["Test 3 (Exceed threshold)"] = (test3_passed, exceed_response.status_code if exceed_response else "ERROR", f"Expected 403 Forbidden for score ({expected_total_score}) >= threshold ({threshold})")
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
# Test 4: Explicit 'block' action rule (should block, 403 Forbidden)
print(colored("\nTest 4: Explicit 'block' action rule (should block with 403 Forbidden)", "magenta"))
block_action_payload = {'block': 'true'} # RULE-BLOCK (Block Action)
block_response, block_headers, test4_passed = send_request(base_url, block_action_payload, expected_status=403, debug=debug)
results_data["Test 4 (Block action)"] = (test4_passed, block_response.status_code if block_response else "ERROR", "Expected 403 Forbidden for explicit block action")
print(colored("-> Score doesn't matter for this test - blocking action should take precedence", "yellow"))
# Test 5: Incremental scoring in separate requests (should pass, 200 OK)
print(colored("\nTest 5: Incremental scoring in separate requests (should be isolated per request, pass with 200 OK)", "magenta"))
incremental_results_passed = []
incremental_status_codes = []
for i in range(1, 4): # Tests INCR-1 (1), INCR-2 (2), INCR-3 (3)
print(colored(f"--- Request {i} of incremental test ---", "cyan"))
incremental_payload = {'increment': f'score{i}'}
expected_score = i
incremental_response, inc_headers, single_inc_passed = send_request(base_url, incremental_payload, expected_status=200, debug=debug)
incremental_results_passed.append(single_inc_passed if single_inc_passed is not None else False)
incremental_status_codes.append(incremental_response.status_code if incremental_response else "ERROR")
print(colored(f"-> Expected anomaly score contribution for this request: {expected_score}", "yellow"))
if i < 3: time.sleep(0.2) # Shorter delay
test5_passed = all(incremental_results_passed)
status_summary = ', '.join(map(str, incremental_status_codes))
results_data["Test 5 (Incremental)"] = (test5_passed, status_summary, f"Expected 200 OK for all incremental tests (scores {', '.join(map(str,range(1,4)))}) < threshold ({threshold})")
# --- NEW TESTS ---
# Test 6: Score hitting exact threshold (should block, 403 Forbidden)
print(colored(f"\nTest 6: Score hitting exact threshold (should block with 403 Forbidden)", "magenta"))
exact_threshold_payload = {'param1': 'score2', 'param2': 'score3'} # RULE-PARAM1 (2) + RULE-PARAM2-HIGH (3) = 5
expected_total_score = 5
exact_response, exact_headers, test6_passed = send_request(base_url, exact_threshold_payload, expected_status=403, debug=debug)
results_data["Test 6 (Exact threshold)"] = (test6_passed, exact_response.status_code if exact_response else "ERROR", f"Expected 403 Forbidden for score ({expected_total_score}) == threshold ({threshold})")
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
# Test 7: Mix High/Low score below threshold (should pass, 200 OK)
print(colored(f"\nTest 7: Mix High/Low score below threshold (should pass with 200 OK)", "magenta"))
mix_below_payload = {'test': 'low_score_test', 'param1': 'score3'} # RULE-1 (1) + RULE-PARAM1-HIGH (3) = 4
expected_total_score = 4
mix_below_response, mix_below_headers, test7_passed = send_request(base_url, mix_below_payload, expected_status=200, debug=debug)
results_data["Test 7 (Mix Below Threshold)"] = (test7_passed, mix_below_response.status_code if mix_below_response else "ERROR", f"Expected 200 OK for mixed score ({expected_total_score}) < threshold ({threshold})")
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
# Test 8: Score greatly exceeding threshold (with Param3) (should block, 403 Forbidden)
print(colored(f"\nTest 8: Score greatly exceeding threshold (should block with 403 Forbidden)", "magenta"))
exceed_greatly_payload = {'param1': 'score3', 'param2': 'score3', 'param3': 'score3'} # RULE-PARAM1-HIGH (3) + RULE-PARAM2-HIGH (3) + RULE-PARAM3-HIGH (3) = 9
expected_total_score = 9
exceed_greatly_response, exceed_greatly_headers, test8_passed = send_request(base_url, exceed_greatly_payload, expected_status=403, debug=debug)
results_data["Test 8 (Exceed Greatly)"] = (test8_passed, exceed_greatly_response.status_code if exceed_greatly_response else "ERROR", f"Expected 403 Forbidden for score ({expected_total_score}) >= threshold ({threshold})")
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
# Test 9: Block action triggered with other scoring rules (should block, 403 Forbidden)
print(colored(f"\nTest 9: Block action priority (should block with 403 Forbidden)", "magenta"))
block_priority_payload = {'block': 'true', 'param1': 'score2'} # RULE-BLOCK (block) + RULE-PARAM1 (2)
expected_total_score = 2 # Score is calculated but block action takes precedence
block_priority_response, block_priority_headers, test9_passed = send_request(base_url, block_priority_payload, expected_status=403, debug=debug)
results_data["Test 9 (Block Priority)"] = (test9_passed, block_priority_response.status_code if block_priority_response else "ERROR", "Expected 403 Forbidden due to explicit block action, regardless of score")
print(colored(f"-> Calculated anomaly score: {expected_total_score}. Block action should override.", "yellow"))
# Test 10: No matching rules (should pass, 200 OK)
print(colored(f"\nTest 10: No matching rules (should pass with 200 OK)", "magenta"))
no_match_payload = {'vanilla': 'test', 'unknown': 'data'}
expected_total_score = 0
no_match_response, no_match_headers, test10_passed = send_request(base_url, no_match_payload, expected_status=200, debug=debug)
results_data["Test 10 (No Match)"] = (test10_passed, no_match_response.status_code if no_match_response else "ERROR", f"Expected 200 OK when no rules match (score {expected_total_score})")
print(colored(f"-> Expected total anomaly score: {expected_total_score}", "yellow"))
# Test 11: Parameter name match, value mismatch (should pass, 200 OK)
print(colored(f"\nTest 11: Parameter name match, value mismatch (should pass with 200 OK)", "magenta"))
value_mismatch_payload = {'param1': 'non_matching_value', 'test': 'another_value'} # Neither value matches RULE-PARAM1 or RULE-1 patterns
expected_total_score = 0
value_mismatch_response, value_mismatch_headers, test11_passed = send_request(base_url, value_mismatch_payload, expected_status=200, debug=debug)
results_data["Test 11 (Value Mismatch)"] = (test11_passed, value_mismatch_response.status_code if value_mismatch_response else "ERROR", f"Expected 200 OK when parameter values don't match rule patterns (score {expected_total_score})")
print(colored(f"-> Expected total anomaly score: {expected_total_score}", "yellow"))
# Summarize results
print(colored("\n=== Anomaly Threshold Test Summary ===", "cyan"))
print(colored(f"Target URL: {base_url}", "yellow"))
print(colored(f"Configured threshold: {threshold}", "yellow"))
all_passed_flag = True
# Define the order tests should appear in the summary
test_order = [
"Test 1 (Low score)",
"Test 2 (Below threshold)",
"Test 7 (Mix Below Threshold)", # New test inserted logically
"Test 5 (Incremental)", # Incremental scores are below threshold
"Test 10 (No Match)",
"Test 11 (Value Mismatch)",
"Test 6 (Exact threshold)", # Blocking test
"Test 3 (Exceed threshold)", # Blocking test
"Test 8 (Exceed Greatly)", # Blocking test
"Test 4 (Block action)", # Blocking test
"Test 9 (Block Priority)" # Blocking test
]
print(colored("\n--- Test Results ---", "cyan"))
for test_name in test_order:
if test_name not in results_data:
print(colored(f"{test_name}: SKIPPED (Data not found)", "yellow"))
all_passed_flag = False # Consider missing data a failure
continue
passed, status_code, description = results_data[test_name]
# Treat None passed status as False for summary
passed = passed if passed is not None else False
result_text = "PASS" if passed else "FAIL"
color = "green" if passed else "red"
print(colored(f"{test_name}: {result_text} (Status: {status_code})", color))
if not passed:
all_passed_flag = False
print(colored(f" Reason: {description}", "yellow"))
elif verbose:
print(colored(f" Details: {description} (Status: {status_code})", "yellow"))
# Final Pass/Fail Summary
print(colored("\n--- Overall Result ---", "cyan"))
if all_passed_flag:
print(colored("✓ All tests passed! Anomaly threshold and blocking logic appear to be working correctly based on expected status codes.", "green"))
else:
print(colored("✗ Some tests failed. Review the output above.", "red"))
failed_tests = [name for name in test_order if name in results_data and not results_data[name][0]]
print(colored(f"Failed tests: {', '.join(failed_tests)}", "red"))
# Provide troubleshooting tips based on failure patterns
test3_failed = "Test 3 (Exceed threshold)" in failed_tests
test4_failed = "Test 4 (Block action)" in failed_tests
test6_failed = "Test 6 (Exact threshold)" in failed_tests
test8_failed = "Test 8 (Exceed Greatly)" in failed_tests
test9_failed = "Test 9 (Block Priority)" in failed_tests
blocking_tests_failed = test3_failed or test4_failed or test6_failed or test8_failed or test9_failed
if blocking_tests_failed:
print(colored("\nSuggestion: One or more blocking tests failed (expected 403).", "yellow"))
if test6_failed : print(colored(" - Check if the WAF blocks exactly *at* the threshold score.", "yellow"))
if test3_failed or test8_failed: print(colored(f" - Verify rules correctly contribute scores and the threshold ({threshold}) is enforced.", "yellow"))
if test4_failed or test9_failed: print(colored(" - Ensure rules with 'block' action are correctly configured and take priority.", "yellow"))
if "Test 5 (Incremental)" in failed_tests:
print(colored("\nSuggestion: One or more incremental tests failed (expected 200). This might indicate score accumulation across requests (incorrect) or unrelated blocking rules triggered.", "yellow"))
if "Test 10 (No Match)" in failed_tests or "Test 11 (Value Mismatch)" in failed_tests :
print(colored("\nSuggestion: Tests expecting no match failed (expected 200). Check for overly broad rules or default blocking actions.", "yellow"))
# --- check_server function remains the same ---
def check_server(url):
"""Check if the server is reachable."""
print(f"\nChecking server reachability at {url}...")
try:
# Use HEAD request for efficiency, or GET if HEAD is disallowed/problematic
response = requests.head(url, timeout=3)
# Allow any success or redirect status code as "reachable"
if 200 <= response.status_code < 400:
print(colored(f"Server is reachable (Status: {response.status_code}).", "green"))
return True
else:
# Handle client/server errors differently
if 400 <= response.status_code < 500:
print(colored(f"Server responded with client error: {response.status_code}. Check URL path/config.", "yellow"))
elif 500 <= response.status_code < 600:
print(colored(f"Server responded with server error: {response.status_code}. Check server/WAF logs.", "red"))
else:
print(colored(f"Server responded with unexpected status: {response.status_code}.", "yellow"))
return False # Treat non-success/redirect as potentially problematic
except requests.exceptions.Timeout:
print(colored(f"ERROR: Connection to {url} timed out.", "red"))
print(colored("Check if the server/proxy is running and accessible.", "yellow"))
return False
except requests.exceptions.ConnectionError:
print(colored(f"ERROR: Cannot connect to server at {url}", "red"))
print(colored("Make sure the server/proxy (e.g., Caddy) is running and the URL is correct.", "yellow"))
return False
except requests.exceptions.RequestException as e:
print(colored(f"ERROR: An unexpected network error occurred: {str(e)}", "red"))
return False
# --- main function is UPDATED (info section) ---
def main():
args = setup_args()
base_url = args.url.rstrip('/') # Remove trailing slash if present
threshold = args.threshold
debug = args.debug
verbose = args.verbose
print(colored(f"WAF Anomaly Threshold Test Tool", "cyan", attrs=["bold"]))
print(colored("-" * 30, "cyan"))
print(f"Target URL: {base_url}")
print(f"Expected Threshold: {threshold}")
print(f"Debug Mode: {'ON' if debug else 'OFF'}")
print(f"Verbose Mode: {'ON' if verbose else 'OFF'}")
print(colored("-" * 30, "cyan"))
# UPDATED Test rule setup recommendations
print(colored("\nINFO: This script assumes specific WAF rules are configured:", "yellow"))
print(colored(" - Rule(s) matching 'test=low_score_test' contribute score=1.", "yellow"))
print(colored(" - Rule(s) matching 'param1=score2' contribute score=2.", "yellow"))
print(colored(" - Rule(s) matching 'param2=score2' contribute score=2.", "yellow"))
print(colored(" - Rule(s) matching 'param1=score3' contribute score=3.", "yellow"))
print(colored(" - Rule(s) matching 'param2=score3' contribute score=3.", "yellow"))
print(colored(" - Rule(s) matching 'param3=score3' contribute score=3. (Used in Test 8)", "yellow")) # Added param3 rule info
print(colored(" - Rule matching 'block=true' has an explicit 'block' action.", "yellow"))
print(colored(" - Rule(s) matching 'increment=scoreX' contribute score=X (e.g., 'increment=score1' adds 1).", "yellow"))
if not check_server(base_url):
sys.exit(1)
test_anomaly_threshold(base_url, threshold, debug, verbose)
if __name__ == "__main__":
main()

33
tor.go
View File

@@ -9,20 +9,22 @@ import (
"strings"
"time"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
)
var torExitNodeURL = "https://check.torproject.org/torbulkexitlist"
type TorConfig struct {
Enabled bool `json:"enabled,omitempty"`
TORIPBlacklistFile string `json:"tor_ip_blacklist_file,omitempty"`
UpdateInterval string `json:"update_interval,omitempty"`
RetryOnFailure bool `json:"retry_on_failure,omitempty"` // Enable/disable retries
RetryInterval string `json:"retry_interval,omitempty"` // Retry interval (e.g., "5m")
lastUpdated time.Time
logger *zap.Logger
Enabled bool `json:"enabled,omitempty"`
CustomTORExitNodeURL string `json:"custom_tor_exit_node_url"`
TORIPBlacklistFile string `json:"tor_ip_blacklist_file,omitempty"`
UpdateInterval string `json:"update_interval,omitempty"`
RetryOnFailure bool `json:"retry_on_failure,omitempty"` // Enable/disable retries
RetryInterval string `json:"retry_interval,omitempty"` // Retry interval (e.g., "5m")
lastUpdated time.Time
logger *zap.Logger
}
// Provision sets up the Tor blocking configuration.
@@ -41,19 +43,24 @@ func (t *TorConfig) Provision(ctx caddy.Context) error {
func (t *TorConfig) updateTorExitNodes() error {
t.logger.Debug("Updating Tor exit nodes...") // Debug log at start of update
resp, err := http.Get(torExitNodeURL)
url := torExitNodeURL
if t.CustomTORExitNodeURL != "" {
url = t.CustomTORExitNodeURL
}
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("http get failed for %s: %w", torExitNodeURL, err) // Improved error message with URL
return fmt.Errorf("http get failed for %s: %w", url, err) // Improved error message with URL
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http get returned status %s for %s", resp.Status, torExitNodeURL) // Check for non-200 status
return fmt.Errorf("http get returned status %s for %s", resp.Status, url) // Check for non-200 status
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body from %s: %w", torExitNodeURL, err) // Improved error message with URL
return fmt.Errorf("failed to read response body from %s: %w", url, err) // Improved error message with URL
}
torIPs := strings.Split(string(data), "\n")
@@ -128,7 +135,7 @@ func (t *TorConfig) readExistingBlacklist() ([]string, error) {
// writeBlacklist writes the updated IP blacklist to the file.
func (t *TorConfig) writeBlacklist(ips []string) error {
data := strings.Join(ips, "\n")
err := os.WriteFile(t.TORIPBlacklistFile, []byte(data), 0644)
err := os.WriteFile(t.TORIPBlacklistFile, []byte(data), 0o600)
if err != nil {
return fmt.Errorf("failed to write IP blacklist file %s: %w", t.TORIPBlacklistFile, err) // Improved error message with filename
}

View File

@@ -6,9 +6,10 @@ import (
"os"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
)
func TestTorConfig_Provision(t *testing.T) {
@@ -40,10 +41,11 @@ func TestTorConfig_Provision(t *testing.T) {
{
name: "enabled config",
config: TorConfig{
Enabled: true,
TORIPBlacklistFile: tmpFile.Name(),
UpdateInterval: "5m",
logger: logger,
Enabled: true,
CustomTORExitNodeURL: torListURL,
TORIPBlacklistFile: tmpFile.Name(),
UpdateInterval: "5m",
logger: logger,
},
wantErr: false,
},

197
types.go
View File

@@ -1,8 +1,6 @@
package caddywaf
import (
"fmt"
"net"
"regexp"
"sync"
"time"
@@ -11,6 +9,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/oschwald/maxminddb-golang"
"github.com/phemmer/go-iptrie"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
@@ -20,76 +19,18 @@ import (
// ==================== Constants and Globals ====================
var (
_ caddy.Module = (*Middleware)(nil)
_ caddy.Provisioner = (*Middleware)(nil)
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
_ caddyfile.Unmarshaler = (*Middleware)(nil)
_ caddy.Validator = (*Middleware)(nil)
)
// Define custom types for rule hits
type RuleID string
type HitCount int
// ==================== Struct Definitions ====================
type TrieNode struct {
children map[byte]*TrieNode
isLeaf bool
}
func NewTrieNode() *TrieNode {
return &TrieNode{
children: make(map[byte]*TrieNode), // Initialize the map
isLeaf: false,
}
}
type CIDRTrie struct {
ipv4Root *TrieNode
ipv6Root *TrieNode
mu sync.RWMutex
}
func NewCIDRTrie() *CIDRTrie {
return &CIDRTrie{
ipv4Root: NewTrieNode(), // Initialize with a new TrieNode
ipv6Root: NewTrieNode(), // Initialize with a new TrieNode
}
}
func (t *CIDRTrie) Insert(cidr string) error {
t.mu.Lock()
defer t.mu.Unlock()
ip, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
if ip.To4() != nil {
// IPv4
return t.insertIPv4(ipNet)
} else {
// IPv6
return t.insertIPv6(ipNet)
}
}
func (t *CIDRTrie) Contains(ipStr string) bool {
t.mu.RLock()
defer t.mu.RUnlock()
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
if ip.To4() != nil {
// IPv4
return t.containsIPv4(ip)
} else {
// IPv6
return t.containsIPv6(ip)
}
}
type (
RuleID string
HitCount int
)
// RuleCache caches compiled regex patterns for rules.
type RuleCache struct {
@@ -105,6 +46,14 @@ type CountryAccessFilter struct {
geoIP *maxminddb.Reader `json:"-"` // Explicitly mark as not serialized
}
// ASNAccessFilter struct
type ASNAccessFilter struct {
Enabled bool `json:"enabled"`
BlockedASNs []string `json:"blocked_asns"`
GeoIPDBPath string `json:"geoip_db_path"`
geoIP *maxminddb.Reader `json:"-"` // Explicitly mark as not serialized
}
// GeoIPRecord struct
type GeoIPRecord struct {
Country struct {
@@ -112,6 +61,12 @@ type GeoIPRecord struct {
} `maxminddb:"country"`
}
// ASNRecord struct
type ASNRecord struct {
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
}
// Rule struct
type Rule struct {
ID string `json:"id"`
@@ -120,7 +75,7 @@ type Rule struct {
Targets []string `json:"targets"`
Severity string `json:"severity"` // Used for logging only
Score int `json:"score"`
Action string `json:"mode"` // Determines the action (block/log)
Action string `json:"mode"` // CRITICAL FIX: This should map to the "mode" field in JSON
Description string `json:"description"`
regex *regexp.Regexp
Priority int // New field for rule priority
@@ -141,7 +96,20 @@ type WAFState struct {
ResponseWritten bool
}
// Middleware struct
// Middleware is the main WAF middleware struct that implements Caddy's
// Module, Provisioner, Validator, and MiddlewareHandler interfaces.
//
// It provides comprehensive web application firewall functionality including:
// - Rule-based request filtering
// - IP and DNS blacklisting
// - Geographic access control
// - Rate limiting
// - Anomaly detection
// - Custom response handling
// - Real-time metrics and monitoring
//
// The middleware can be configured via Caddyfile or JSON and integrates
// seamlessly into Caddy's request processing pipeline.
type Middleware struct {
mu sync.RWMutex
@@ -149,10 +117,11 @@ type Middleware struct {
IPBlacklistFile string `json:"ip_blacklist_file"`
DNSBlacklistFile string `json:"dns_blacklist_file"`
AnomalyThreshold int `json:"anomaly_threshold"`
CountryBlock CountryAccessFilter `json:"country_block"`
CountryBlacklist CountryAccessFilter `json:"country_blacklist"`
CountryWhitelist CountryAccessFilter `json:"country_whitelist"`
BlockASNs ASNAccessFilter `json:"block_asns"`
Rules map[int][]Rule `json:"-"`
ipBlacklist *CIDRTrie `json:"-"` // Changed to CIDRTrie
ipBlacklist *iptrie.Trie `json:"-"`
dnsBlacklist map[string]struct{} `json:"-"` // Changed to map[string]struct{}
logger *zap.Logger
LogSeverity string `json:"log_severity,omitempty"`
@@ -165,8 +134,10 @@ type Middleware struct {
CustomResponses map[int]CustomBlockResponse `json:"custom_responses,omitempty"`
LogFilePath string
LogBuffer int `json:"log_buffer,omitempty"` // Add the LogBuffer field
RedactSensitiveData bool `json:"redact_sensitive_data,omitempty"`
LogBuffer int `json:"log_buffer,omitempty"` // Add the LogBuffer field
RedactSensitiveData bool `json:"redact_sensitive_data,omitempty"`
MaxRequestBodySize int64 `json:"max_request_body_size,omitempty"`
GeoIPFailOpen bool `json:"geoip_fail_open,omitempty"`
ruleHits sync.Map `json:"-"`
MetricsEndpoint string `json:"metrics_endpoint,omitempty"`
@@ -229,85 +200,3 @@ func (rc *RuleCache) Set(ruleID string, regex *regexp.Regexp) {
defer rc.mu.Unlock()
rc.rules[ruleID] = regex
}
func (t *CIDRTrie) insertIPv4(ipNet *net.IPNet) error {
ip := ipNet.IP.To4()
if ip == nil {
return fmt.Errorf("invalid IPv4 address")
}
mask, _ := ipNet.Mask.Size()
node := t.ipv4Root
for i := 0; i < mask; i++ {
bit := (ip[i/8] >> (7 - uint(i%8))) & 1
if node.children[bit] == nil {
node.children[bit] = NewTrieNode() // Initialize the child node
}
node = node.children[bit]
}
node.isLeaf = true
return nil
}
func (t *CIDRTrie) insertIPv6(ipNet *net.IPNet) error {
ip := ipNet.IP.To16()
if ip == nil {
return fmt.Errorf("invalid IPv6 address")
}
mask, _ := ipNet.Mask.Size()
node := t.ipv6Root
for i := 0; i < mask; i++ {
bit := (ip[i/8] >> (7 - uint(i%8))) & 1
if node.children[bit] == nil {
node.children[bit] = NewTrieNode() // Initialize the child node
}
node = node.children[bit]
}
node.isLeaf = true
return nil
}
func (t *CIDRTrie) containsIPv4(ip net.IP) bool {
ip = ip.To4()
if ip == nil {
return false
}
node := t.ipv4Root
for i := 0; i < len(ip)*8; i++ {
bit := (ip[i/8] >> (7 - uint(i%8))) & 1
if node.children[bit] == nil {
return false
}
node = node.children[bit]
if node.isLeaf {
return true
}
}
return node.isLeaf
}
func (t *CIDRTrie) containsIPv6(ip net.IP) bool {
ip = ip.To16()
if ip == nil {
return false
}
node := t.ipv6Root
for i := 0; i < len(ip)*8; i++ {
bit := (ip[i/8] >> (7 - uint(i%8))) & 1
if node.children[bit] == nil {
return false
}
node = node.children[bit]
if node.isLeaf {
return true
}
}
return false
}

View File

@@ -5,75 +5,6 @@ import (
"testing"
)
func TestNewCIDRTrie(t *testing.T) {
trie := NewCIDRTrie()
if trie == nil {
t.Fatal("NewCIDRTrie() returned nil")
}
if trie.ipv4Root == nil {
t.Fatal("NewCIDRTrie() created a trie with nil ipv4Root")
}
if trie.ipv6Root == nil {
t.Fatal("NewCIDRTrie() created a trie with nil ipv6Root")
}
if trie.ipv4Root.children == nil {
t.Fatal("NewCIDRTrie() created ipv4Root with nil children map")
}
if trie.ipv6Root.children == nil {
t.Fatal("NewCIDRTrie() created ipv6Root with nil children map")
}
}
func TestCIDRTrie_Insert(t *testing.T) {
tests := []struct {
name string
cidr string
wantErr bool
}{
{"valid IPv4 CIDR", "192.168.1.0/24", false},
{"valid IPv6 CIDR", "2001:db8::/32", false}, // IPv6 is now supported
{"invalid CIDR", "invalid", true},
{"invalid IPv4 mask", "192.168.1.0/33", true},
{"invalid IPv6 mask", "2001:db8::/129", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
trie := NewCIDRTrie()
err := trie.Insert(tt.cidr)
if (err != nil) != tt.wantErr {
t.Errorf("CIDRTrie.Insert() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestCIDRTrie_Contains(t *testing.T) {
trie := NewCIDRTrie()
_ = trie.Insert("192.168.1.0/24")
_ = trie.Insert("2001:db8::/32") // Add an IPv6 CIDR
tests := []struct {
name string
ip string
want bool
}{
{"IPv4 in range", "192.168.1.1", true},
{"IPv4 out of range", "192.168.2.1", false},
{"Invalid IP", "invalid", false},
{"IPv6 in range", "2001:db8::1", true},
{"IPv6 out of range", "2001:db9::1", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := trie.Contains(tt.ip); got != tt.want {
t.Errorf("CIDRTrie.Contains() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewRuleCache(t *testing.T) {
cache := NewRuleCache()
if cache == nil {