60 Commits

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
48 changed files with 1987 additions and 1413 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.24.2
- name: Install Go 1.25
uses: actions/setup-go@v4
with:
go-version: '1.24.2'
- name: Validate Go Installation
run: |
go version
if ! go version | grep -q "go1.24.2"; 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.24.2' # 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: ${{ 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.24.2"
- 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 "passed=$passed_count" >> $GITHUB_ENV
echo "failed=$failed_count" >> $GITHUB_ENV
- 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$

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.

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

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,11 +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**: powered by custom GPT, [try it here](https://chatgpt.com/g/g-677d07dd07e48191b799b9e5d6da7828-caddy-waf-ruler)
* **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
@@ -55,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
@@ -176,7 +189,7 @@ If You like my projects, you may also like these ones:
- [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
- [DevGPT](https://github.com/fabriziosalmi/DevGPT) Code togheter, right now! GPT powered code assistant to build project in minutes
- [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

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)

View File

@@ -20,19 +20,21 @@ 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 ====================
@@ -46,7 +48,7 @@ var (
)
// Add or update the version constant as needed
const wafVersion = "v0.0.6" // 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 ====================
@@ -80,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 == "" {
@@ -118,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))
@@ -191,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
@@ -216,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)
}
@@ -283,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 {
@@ -317,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 {
@@ -326,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
})
@@ -424,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)
}
@@ -450,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
@@ -464,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
}
@@ -492,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",

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)
}
}

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"os"
"strings"
"sync/atomic"
"time"
"go.uber.org/zap"
@@ -25,10 +26,11 @@ func (m *Middleware) DebugRequest(r *http.Request, state *WAFState, msg string)
if !ok {
return true
}
hitCount, ok := value.(HitCount)
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
@@ -59,25 +61,31 @@ func (m *Middleware) DumpRulesToFile(path string) error {
}
defer f.Close()
f.WriteString("=== WAF Rules Dump ===\n\n")
if _, err := f.WriteString("=== WAF Rules Dump ===\n\n"); err != nil {
return err
}
for phase := 1; phase <= 4; phase++ {
f.WriteString(fmt.Sprintf("== Phase %d Rules ==\n", phase))
fmt.Fprintf(f, "== Phase %d Rules ==\n", phase)
rules, ok := m.Rules[phase]
if !ok || len(rules) == 0 {
f.WriteString(" No rules for this phase\n\n")
if _, err := f.WriteString(" No rules for this phase\n\n"); err != nil {
return err
}
continue
}
for i, rule := range rules {
f.WriteString(fmt.Sprintf(" Rule %d:\n", i+1))
f.WriteString(fmt.Sprintf(" ID: %s\n", rule.ID))
f.WriteString(fmt.Sprintf(" Pattern: %s\n", rule.Pattern))
f.WriteString(fmt.Sprintf(" Targets: %v\n", rule.Targets))
f.WriteString(fmt.Sprintf(" Score: %d\n", rule.Score))
f.WriteString(fmt.Sprintf(" Action: %s\n", rule.Action))
f.WriteString(fmt.Sprintf(" Description: %s\n", rule.Description))
f.WriteString("\n")
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
}
}
}

16
doc.go
View File

@@ -15,14 +15,16 @@
// - Dynamic configuration reloading
//
// Installation:
// xcaddy build --with github.com/fabriziosalmi/caddy-waf
//
// 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
// }
//
// 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
package caddywaf

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")

188
go.mod
View File

@@ -1,124 +1,164 @@
module github.com/fabriziosalmi/caddy-waf
go 1.23.0
toolchain go1.24.2
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/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-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/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.35.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.36.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.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
)

655
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.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-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/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.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
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.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
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/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
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
@@ -149,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
}
}
}
}
@@ -228,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, ",")
@@ -277,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
@@ -342,52 +459,58 @@ 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 {
shouldContinue = m.processRuleMatch(recorder, r, &rule, value, state)
shouldContinue = m.processRuleMatch(recorder, r, &rule, target, value, state)
} else {
shouldContinue = m.processRuleMatch(w, r, &rule, value, state)
shouldContinue = m.processRuleMatch(w, r, &rule, target, value, state)
}
} else {
shouldContinue = m.processRuleMatch(w, r, &rule, value, state)
shouldContinue = m.processRuleMatch(w, r, &rule, target, value, state)
}
// 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", string(rule.ID)),
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),
)
}
}
@@ -414,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
}

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

@@ -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

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
@@ -166,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",
@@ -204,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
@@ -334,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)
@@ -351,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) {

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

@@ -8,8 +8,17 @@ import (
"go.uber.org/zap"
)
// 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, matchedValue string, fields ...zap.Field) {
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
@@ -30,9 +39,13 @@ func (m *Middleware) blockRequest(recorder http.ResponseWriter, r *http.Request,
recorder.Header().Set("Content-Type", "text/plain")
recorder.WriteHeader(statusCode)
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))
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))
}
}
}
@@ -58,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

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,7 +39,7 @@ 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),
@@ -51,7 +53,7 @@ func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, ru
// Debug the actual action field value to verify what's being used
m.logger.Debug("Rule action/mode check",
zap.String("rule_id", string(rule.ID)),
zap.String("rule_id", rule.ID),
zap.String("action_field", rule.Action),
zap.Int("score", rule.Score),
zap.Int("threshold", m.AnomalyThreshold),
@@ -77,7 +79,7 @@ func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, ru
state.StatusCode = http.StatusForbidden
// Block the request and write the response immediately
m.blockRequest(w, r, state, http.StatusForbidden, blockReason, string(rule.ID), value,
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),
@@ -92,14 +94,14 @@ func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, ru
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),
@@ -112,14 +114,14 @@ func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, ru
// 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),
)
}
@@ -240,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

@@ -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

View File

@@ -1,9 +1,24 @@
[
{
"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"],
"targets": [
"URL_PARAM:test"
],
"severity": "low",
"score": 1,
"mode": "log",
@@ -14,7 +29,9 @@
"id": "TEST-RULE-PARAM1",
"phase": 2,
"pattern": "score2",
"targets": ["URL_PARAM:param1"],
"targets": [
"URL_PARAM:param1"
],
"severity": "medium",
"score": 2,
"mode": "log",
@@ -25,7 +42,9 @@
"id": "TEST-RULE-PARAM2",
"phase": 2,
"pattern": "score2",
"targets": ["URL_PARAM:param2"],
"targets": [
"URL_PARAM:param2"
],
"severity": "medium",
"score": 2,
"mode": "log",
@@ -36,7 +55,9 @@
"id": "TEST-RULE-PARAM1-HIGH",
"phase": 2,
"pattern": "score3",
"targets": ["URL_PARAM:param1"],
"targets": [
"URL_PARAM:param1"
],
"severity": "high",
"score": 3,
"mode": "log",
@@ -47,7 +68,9 @@
"id": "TEST-RULE-PARAM2-HIGH",
"phase": 2,
"pattern": "score3",
"targets": ["URL_PARAM:param2"],
"targets": [
"URL_PARAM:param2"
],
"severity": "high",
"score": 3,
"mode": "log",
@@ -58,7 +81,9 @@
"id": "TEST-RULE-PARAM3-HIGH",
"phase": 2,
"pattern": "score3",
"targets": ["URL_PARAM:param3"],
"targets": [
"URL_PARAM:param3"
],
"severity": "high",
"score": 3,
"mode": "log",
@@ -69,7 +94,9 @@
"id": "TEST-RULE-BLOCK",
"phase": 2,
"pattern": "true",
"targets": ["URL_PARAM:block"],
"targets": [
"URL_PARAM:block"
],
"severity": "critical",
"score": 0,
"mode": "block",
@@ -80,7 +107,9 @@
"id": "TEST-RULE-INCR-1",
"phase": 2,
"pattern": "score1",
"targets": ["URL_PARAM:increment"],
"targets": [
"URL_PARAM:increment"
],
"severity": "low",
"score": 1,
"mode": "log",
@@ -91,7 +120,9 @@
"id": "TEST-RULE-INCR-2",
"phase": 2,
"pattern": "score2",
"targets": ["URL_PARAM:increment"],
"targets": [
"URL_PARAM:increment"
],
"severity": "medium",
"score": 2,
"mode": "log",
@@ -102,11 +133,13 @@
"id": "TEST-RULE-INCR-3",
"phase": 2,
"pattern": "score3",
"targets": ["URL_PARAM:increment"],
"targets": [
"URL_PARAM:increment"
],
"severity": "high",
"score": 3,
"mode": "log",
"description": "Incremental test rule 3",
"priority": 10
}
]
]

View File

@@ -7,7 +7,7 @@
:8080 {
route {
waf {
rule_file /Users/fab/GitHub/caddy-waf/sample_rules.json
rule_file ./sample_rules.json
anomaly_threshold 5
log_severity debug
metrics_endpoint /metrics

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,
},

183
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"
)
@@ -28,70 +27,10 @@ var (
)
// 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 {
@@ -107,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 {
@@ -114,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"`
@@ -164,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"`
@@ -180,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"`
@@ -244,90 +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
}
// Add this check to ensure ip is not empty
if len(ip) == 0 {
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 {