mirror of
https://github.com/fabriziosalmi/caddy-waf.git
synced 2026-01-03 11:38:34 -05:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf45542c7e | ||
|
|
571d095028 | ||
|
|
d3f918c4c4 | ||
|
|
5c5f32741c | ||
|
|
0a96f22563 | ||
|
|
12d70c0eec | ||
|
|
83a4df7e65 | ||
|
|
05152510f5 | ||
|
|
5928ff4210 | ||
|
|
78f0066cb8 | ||
|
|
00c547e2a3 | ||
|
|
c29a7ce9aa | ||
|
|
eea39d253b | ||
|
|
5d57051169 | ||
|
|
47e05e907e | ||
|
|
1c9b6a287d | ||
|
|
b3d3d5692c | ||
|
|
a179255b3f | ||
|
|
1da1fea22b | ||
|
|
34d7a29119 | ||
|
|
66685526e5 | ||
|
|
971bc53f8a | ||
|
|
937808048b | ||
|
|
b9fe9ddbb3 | ||
|
|
db95a9b2ed | ||
|
|
e98fd16392 | ||
|
|
65f8c8a62f | ||
|
|
c8c0fed9e2 | ||
|
|
06a496e3d3 | ||
|
|
a71b182158 | ||
|
|
cf7c995137 | ||
|
|
14e4de4b66 | ||
|
|
8b702b4281 | ||
|
|
1c32e928f1 | ||
|
|
f45e8331cc | ||
|
|
fa7f421773 | ||
|
|
1a65ea7049 | ||
|
|
a77a2d2e36 | ||
|
|
63ca645404 | ||
|
|
1207bd7a6d | ||
|
|
5c8d13199b | ||
|
|
8be3863b48 | ||
|
|
1e5d6d9e3d | ||
|
|
2e6aa32858 | ||
|
|
145feb4bf8 | ||
|
|
485c86fdbc | ||
|
|
08021ee7e0 | ||
|
|
27abae69ea | ||
|
|
2fffae5d18 | ||
|
|
8d5af6be5f | ||
|
|
7938023ed1 | ||
|
|
c905277058 | ||
|
|
6429e286fd | ||
|
|
95efcabc27 | ||
|
|
c483f5baba | ||
|
|
feee09fcf7 | ||
|
|
6b5b686b55 | ||
|
|
719dd2c007 | ||
|
|
be8baedaae | ||
|
|
8685d03503 | ||
|
|
df5f0511ac | ||
|
|
2bd1af566c | ||
|
|
0ac97c5715 | ||
|
|
bae17679f1 | ||
|
|
7f81733fd0 | ||
|
|
5a87efcdf9 | ||
|
|
da9b8dafc0 | ||
|
|
eba6e51887 | ||
|
|
fe98e856fa | ||
|
|
81f3ad5577 | ||
|
|
b2035a4acf | ||
|
|
13712e01d9 | ||
|
|
e4c88a3956 | ||
|
|
fe84fbb5c5 | ||
|
|
533020d5e6 | ||
|
|
bf367b5c53 | ||
|
|
1989037c29 | ||
|
|
36464a222a | ||
|
|
5c266d1665 | ||
|
|
cd959a2712 | ||
|
|
a3fcc3a3f8 | ||
|
|
1cbd739f7c | ||
|
|
75d217f736 | ||
|
|
4f31673cb5 |
16
.github/workflows/build-run-validate.yml
vendored
16
.github/workflows/build-run-validate.yml
vendored
@@ -27,18 +27,10 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt install -y wget git build-essential curl python3 python3-pip
|
||||
|
||||
- name: Install Go 1.23.4
|
||||
- name: Install Go 1.25
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23.4'
|
||||
|
||||
- name: Validate Go Installation
|
||||
run: |
|
||||
go version
|
||||
if ! go version | grep -q "go1.23.4"; then
|
||||
echo "Go installation failed or incorrect version"
|
||||
exit 1
|
||||
fi
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Clone caddy-waf Repository
|
||||
run: |
|
||||
@@ -61,7 +53,7 @@ jobs:
|
||||
- name: Download GeoLite2 Country Database
|
||||
run: |
|
||||
cd caddy-waf
|
||||
wget https://git.io/GeoLite2-Country.mmdb
|
||||
wget https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/GeoLite2-Country.mmdb
|
||||
|
||||
- name: Validate GeoLite2 Download
|
||||
run: |
|
||||
@@ -139,7 +131,7 @@ jobs:
|
||||
run: |
|
||||
cd caddy-waf
|
||||
chmod +x caddy
|
||||
./caddy run > caddy_output.log 2>&1 &
|
||||
./caddy run --config test.caddyfile > caddy_output.log 2>&1 &
|
||||
sleep 5
|
||||
|
||||
if ! pgrep -f "caddy run"; then
|
||||
|
||||
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@@ -26,11 +26,11 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.4' # Use your desired go version
|
||||
go-version: '1.25' # Use your desired go version
|
||||
|
||||
- name: Extract Tag Name
|
||||
id: extract_tag
|
||||
if: "!startsWith(github.ref, 'refs/heads/')"
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: echo "TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build Binary
|
||||
@@ -50,38 +50,31 @@ jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-release # Ensure all builds complete before creating release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract Tag Name
|
||||
id: extract_tag
|
||||
if: "!startsWith(github.ref, 'refs/heads/')"
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: echo "TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
if: "!startsWith(github.ref, 'refs/heads/')"
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.extract_tag.outputs.TAG_NAME }}
|
||||
release_name: Release ${{ steps.extract_tag.outputs.TAG_NAME }}
|
||||
body: |
|
||||
This is a release of the Caddy WAF middleware version ${{ steps.extract_tag.outputs.TAG_NAME }}. Please download the appropriate binary for your OS/Architecture.
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Upload Release Assets
|
||||
if: "!startsWith(github.ref, 'refs/heads/')"
|
||||
run: |
|
||||
for asset in $(ls *.tar.gz); do
|
||||
echo "Uploading ${asset}"
|
||||
gh release upload ${{ steps.create_release.outputs.upload_url }} ${asset} --clobber
|
||||
done
|
||||
- name: Release via GH CLI
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ steps.extract_tag.outputs.TAG_NAME }}
|
||||
run: |
|
||||
# Flatten artifacts structure (download-artifact puts them in subdirs matches artifact name)
|
||||
# But we want all .tar.gz in current dir
|
||||
find . -name "*.tar.gz" -exec mv {} . \;
|
||||
|
||||
echo "Creating or updating release for $TAG_NAME..."
|
||||
# Try to create release with assets. If it fails (exists), upload assets.
|
||||
gh release create "$TAG_NAME" *.tar.gz --title "$TAG_NAME" --generate-notes || \
|
||||
gh release upload "$TAG_NAME" *.tar.gz --clobber
|
||||
|
||||
66
.github/workflows/test.yml
vendored
Normal file
66
.github/workflows/test.yml
vendored
Normal 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
|
||||
69
.github/workflows/tests.yml
vendored
69
.github/workflows/tests.yml
vendored
@@ -1,69 +0,0 @@
|
||||
name: "Tests"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.4"
|
||||
|
||||
- name: Get Dependencies
|
||||
run: go get -v ./...
|
||||
|
||||
- name: Get Caddy Dependencies
|
||||
run: |
|
||||
go get github.com/caddyserver/caddy/v2/modules/caddyhttp/templates@v2.9.1
|
||||
go get github.com/caddyserver/caddy/v2/modules/caddyhttp/proxyprotocol@v2.9.1
|
||||
go get github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing@v2.9.1
|
||||
go get github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver@v2.9.1
|
||||
go get github.com/smallstep/certificates/acme@v0.26.1
|
||||
|
||||
- name: Build Caddy
|
||||
run: |
|
||||
go build -v -o caddy github.com/caddyserver/caddy/v2/cmd/caddy
|
||||
|
||||
- name: Tidy modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
id: test
|
||||
run: |
|
||||
test_output=$(go test -v -count=1 ./... 2>&1)
|
||||
echo "test_output<<EOF" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$test_output" >> $GITHUB_STEP_SUMMARY
|
||||
echo "EOF" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
passed_count=$(echo "$test_output" | grep "PASS:" | wc -l)
|
||||
failed_count=$(echo "$test_output" | grep "FAIL:" | wc -l)
|
||||
|
||||
echo "::set-output name=passed::$(echo $passed_count)"
|
||||
echo "::set-output name=failed::$(echo $failed_count)"
|
||||
|
||||
- name: Test Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "Test Results:"
|
||||
echo "Passed Tests: ${{ steps.test.outputs.passed }}"
|
||||
echo "Failed Tests: ${{ steps.test.outputs.failed }}"
|
||||
|
||||
- name: Fail if tests failed
|
||||
if: steps.test.outputs.failed != '0'
|
||||
run: exit 1
|
||||
122
.golangci.yml
Normal file
122
.golangci.yml
Normal 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$
|
||||
120
CADDY_MODULE_REGISTRATION.md
Normal file
120
CADDY_MODULE_REGISTRATION.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Caddy Module Registration Checklist
|
||||
|
||||
This document outlines the requirements and steps for successfully registering the caddy-waf module in the official Caddy modules directory.
|
||||
|
||||
## ✅ Completed Requirements
|
||||
|
||||
### 1. Module Structure Compliance
|
||||
- [x] **Module Interface Implementation**: Properly implements `caddy.Module` interface
|
||||
- [x] **Module ID**: Correctly uses `http.handlers.waf` as module ID
|
||||
- [x] **Registration**: Module is registered in `init()` function using `caddy.RegisterModule()`
|
||||
- [x] **Interface Guards**: Proper interface guards implemented for compile-time checking
|
||||
- [x] **Caddyfile Support**: Implements `caddyfile.Unmarshaler` for Caddyfile parsing
|
||||
|
||||
### 2. Required Interfaces
|
||||
- [x] **caddy.Module**: Implemented via `CaddyModule()` method
|
||||
- [x] **caddy.Provisioner**: Implemented via `Provision()` method
|
||||
- [x] **caddy.Validator**: Implemented via `Validate()` method
|
||||
- [x] **caddyhttp.MiddlewareHandler**: Implemented via `ServeHTTP()` method
|
||||
- [x] **caddyfile.Unmarshaler**: Implemented via `UnmarshalCaddyfile()` method
|
||||
|
||||
### 3. Documentation Requirements
|
||||
- [x] **Package Documentation**: Added comprehensive package-level documentation
|
||||
- [x] **Struct Documentation**: Added detailed documentation for main Middleware struct
|
||||
- [x] **README.md**: Comprehensive README with examples and installation instructions
|
||||
- [x] **Module Metadata**: Created `MODULE.md` with standardized module information
|
||||
- [x] **Usage Examples**: Created `caddyfile.example` with practical configuration examples
|
||||
- [x] **API Documentation**: Generated via `go doc` commands
|
||||
|
||||
### 4. Code Quality and Standards
|
||||
- [x] **Go Module Structure**: Proper `go.mod` with correct module path
|
||||
- [x] **Version Consistency**: Updated version constant to match latest release (v0.0.6)
|
||||
- [x] **Build Verification**: Module builds successfully with `go build`
|
||||
- [x] **Module Verification**: Passes `go mod verify`
|
||||
- [x] **No Build Errors**: Clean compilation with no warnings or errors
|
||||
|
||||
### 5. Release Management
|
||||
- [x] **Git Tags**: Proper semantic versioning tags (v0.0.3, v0.0.4, v0.0.5, v0.0.6)
|
||||
- [x] **GitHub Releases**: Automated release workflow creating GitHub releases
|
||||
- [x] **Release Notes**: Proper release descriptions and changelogs
|
||||
- [x] **Binary Assets**: Cross-platform binaries generated for releases
|
||||
|
||||
### 6. Testing and Validation
|
||||
- [x] **Test Suite**: Comprehensive test coverage across multiple files
|
||||
- [x] **CI/CD Pipeline**: GitHub Actions workflows for testing and building
|
||||
- [x] **Module Import**: Can be imported and used with `xcaddy build`
|
||||
|
||||
## 🔍 Potential Issues and Solutions
|
||||
|
||||
### Issue Analysis: Registration Error ID `2b782e50-057d-4dac-bbd5-4cd1c1188669`
|
||||
|
||||
Based on the error ID mentioned in the issue comments, this appears to be a server-side error during the registration process rather than a module compliance issue. Common causes and solutions:
|
||||
|
||||
### 1. **Server-Side Registration Issues**
|
||||
- **Cause**: Temporary issues with the Caddy module registration service
|
||||
- **Solution**: Retry registration after some time
|
||||
- **Status**: May resolve automatically
|
||||
|
||||
### 2. **Module Path Validation**
|
||||
- **Cause**: Registration service may have strict validation rules
|
||||
- **Solution**: Ensure `github.com/fabriziosalmi/caddy-waf` is accessible and properly formatted
|
||||
- **Status**: ✅ Module path is valid and accessible
|
||||
|
||||
### 3. **Go Module Accessibility**
|
||||
- **Cause**: Registration service needs to fetch and validate the module
|
||||
- **Solution**: Ensure module is publicly accessible and properly tagged
|
||||
- **Status**: ✅ Repository is public with proper tags
|
||||
|
||||
### 4. **Caddy Version Compatibility**
|
||||
- **Cause**: Module might require specific Caddy version
|
||||
- **Solution**: Verify compatibility with latest Caddy version
|
||||
- **Status**: ✅ Uses Caddy v2.9.1 (latest)
|
||||
|
||||
## 🚀 Next Steps for Registration
|
||||
|
||||
### 1. **Retry Registration**
|
||||
- Visit https://caddyserver.com/account/register-package
|
||||
- Use the exact module path: `github.com/fabriziosalmi/caddy-waf`
|
||||
- Ensure using the latest tag: `v0.0.6`
|
||||
|
||||
### 2. **Contact Caddy Team**
|
||||
- If registration continues to fail, contact Caddy maintainers
|
||||
- Provide the error ID: `2b782e50-057d-4dac-bbd5-4cd1c1188669`
|
||||
- Reference this module's compliance with all requirements
|
||||
|
||||
### 3. **Alternative Registration Paths**
|
||||
- Consider submitting a PR to the Caddy Community repository
|
||||
- Engage with the Caddy community on forums or Discord
|
||||
- Document the module in community wikis or resources
|
||||
|
||||
## 📋 Final Verification Commands
|
||||
|
||||
Run these commands to verify module readiness:
|
||||
|
||||
```bash
|
||||
# Verify module builds successfully
|
||||
go build -v
|
||||
|
||||
# Verify module interfaces
|
||||
go doc -short
|
||||
|
||||
# Test module import
|
||||
go list -m github.com/fabriziosalmi/caddy-waf
|
||||
|
||||
# Verify with xcaddy (if available)
|
||||
xcaddy build --with github.com/fabriziosalmi/caddy-waf
|
||||
|
||||
# Check latest version/tag
|
||||
git describe --tags --abbrev=0
|
||||
```
|
||||
|
||||
## 📞 Support Information
|
||||
|
||||
- **Repository**: https://github.com/fabriziosalmi/caddy-waf
|
||||
- **Issues**: https://github.com/fabriziosalmi/caddy-waf/issues
|
||||
- **License**: AGPLv3
|
||||
- **Maintainer**: @fabriziosalmi
|
||||
|
||||
---
|
||||
|
||||
**Conclusion**: The caddy-waf module meets all technical requirements for Caddy module registration. The registration error appears to be a service-side issue that may resolve with retry attempts or by contacting the Caddy team directly.
|
||||
34
CHANGELOG.md
Normal file
34
CHANGELOG.md
Normal 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.
|
||||
13
Caddyfile
13
Caddyfile
@@ -4,7 +4,6 @@
|
||||
}
|
||||
|
||||
:8080 {
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
@@ -19,8 +18,10 @@
|
||||
# WAF Plugin runs on all requests first
|
||||
waf {
|
||||
metrics_endpoint /waf_metrics
|
||||
anomaly_threshold 10
|
||||
block_countries GeoLite2-Country.mmdb RU CN KP
|
||||
anomaly_threshold 20
|
||||
# Using modified rules file that prevents false positives with Chrome browser requests
|
||||
rule_file rules.json
|
||||
# block_countries GeoLite2-Country.mmdb RU CN KP
|
||||
# whitelist_countries GeoLite2-Country.mmdb US
|
||||
|
||||
# custom_response 403 application/json error.json
|
||||
@@ -30,7 +31,7 @@
|
||||
requests 100
|
||||
window 10s
|
||||
cleanup_interval 5m
|
||||
paths /ratelimited # List of individual regex patterns (example: paths ^/api/.*)
|
||||
paths /ratelimited # List of individual regex patterns (example: paths ^/api/.*)
|
||||
match_all_paths false
|
||||
}
|
||||
|
||||
@@ -43,8 +44,6 @@
|
||||
retry_interval 1h
|
||||
}
|
||||
|
||||
rule_file rules.json
|
||||
# rule_file rules/wordpress.json
|
||||
ip_blacklist_file ip_blacklist.txt
|
||||
dns_blacklist_file dns_blacklist.txt
|
||||
log_severity info
|
||||
@@ -56,7 +55,7 @@
|
||||
# Match the waf metrics endpoint specifically and stop processing
|
||||
@wafmetrics path /waf_metrics
|
||||
handle @wafmetrics {
|
||||
header Access-Control-Allow-Origin * # Allow requests from any origin (for development - see note below)
|
||||
header Access-Control-Allow-Origin * # Allow requests from any origin (for development - see note below)
|
||||
header Access-Control-Allow-Methods "GET, OPTIONS" # Allow GET and OPTIONS methods
|
||||
header Access-Control-Allow-Headers "User-Agent, Content-Type, *" # Allow User-Agent and Content-Type headers
|
||||
# Do not respond here so it goes to the WAF plugin
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Use a Go base image to build the Caddy binary
|
||||
FROM golang:1.22.3-alpine AS builder
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# Install git and xcaddy (required for cloning the repository and building Caddy)
|
||||
RUN apk add --no-cache git wget && \
|
||||
@@ -14,10 +14,7 @@ RUN git clone https://github.com/fabriziosalmi/caddy-waf.git
|
||||
# Navigate into the caddy-waf directory
|
||||
WORKDIR /app/caddy-waf
|
||||
|
||||
# Fetch and install the required Go modules (including Caddy v2)
|
||||
RUN go get -v github.com/caddyserver/caddy/v2 github.com/caddyserver/caddy/v2/caddyconfig/caddyfile github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile github.com/caddyserver/caddy/v2 github.com/caddyserver/caddy/v2/modules/caddyhttp github.com/oschwald/maxminddb-golang github.com/fsnotify/fsnotify github.com/fabriziosalmi/caddy-waf && \
|
||||
|
||||
# Clean up and update the go.mod file
|
||||
# Clean up and update the go.mod file (dependencies are already defined in go.mod)
|
||||
RUN go mod tidy
|
||||
|
||||
# Download the GeoLite2 Country database
|
||||
|
||||
29
GNUmakefile
Normal file
29
GNUmakefile
Normal file
@@ -0,0 +1,29 @@
|
||||
tidy:
|
||||
@go mod tidy
|
||||
@echo "Done!"
|
||||
|
||||
upd:
|
||||
@go get -u ./...
|
||||
@echo "Done!"
|
||||
|
||||
fmt:
|
||||
@go fmt ./...
|
||||
|
||||
test:
|
||||
@go test -v ./...
|
||||
@echo "Done!"
|
||||
|
||||
it:
|
||||
@go test -v ./... -tags=it
|
||||
@echo "Done!"
|
||||
|
||||
lint:
|
||||
@echo "==> Checking source code with golangci-lint..."
|
||||
@golangci-lint run
|
||||
|
||||
lintfix:
|
||||
@echo "==> Checking source code with golangci-lint..."
|
||||
@golangci-lint run --fix
|
||||
|
||||
test-integration:
|
||||
@docker run --rm -v $(PWD):/app -w /app python:3.9-slim python test.py
|
||||
76
MODULE.md
Normal file
76
MODULE.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Caddy WAF Module Information
|
||||
|
||||
**Module Name:** caddy-waf
|
||||
**Module ID:** `http.handlers.waf`
|
||||
**Go Module Path:** `github.com/fabriziosalmi/caddy-waf`
|
||||
**License:** AGPLv3
|
||||
**Latest Version:** v0.0.6
|
||||
|
||||
## Description
|
||||
|
||||
A robust, highly customizable, and feature-rich Web Application Firewall (WAF) middleware for the Caddy web server. This middleware provides advanced protection against a comprehensive range of web-based threats, seamlessly integrating with Caddy and offering flexible configuration options to secure your applications effectively.
|
||||
|
||||
## Module Type
|
||||
|
||||
HTTP Handler Middleware (`http.handlers.waf`)
|
||||
|
||||
## Features
|
||||
|
||||
- **Regex-Based Filtering:** Deep URL, data & header inspection using powerful regex rules
|
||||
- **Blacklisting:** Blocks malicious IPs, domains & optionally TOR exit nodes
|
||||
- **Geo-Blocking:** Restricts access by country using GeoIP
|
||||
- **Rate Limiting:** Prevents abuse via customizable IP request limits
|
||||
- **Anomaly Scoring:** Dynamically blocks requests based on cumulative rule matches
|
||||
- **Multi-Phase Inspection:** Analyzes traffic throughout the request lifecycle
|
||||
- **Sensitive Data Redaction:** Removes private info from logs
|
||||
- **Custom Response Handling:** Tailored responses for blocked requests
|
||||
- **Detailed Monitoring:** JSON endpoint for performance tracking & analysis
|
||||
- **Dynamic Config Reloads:** Seamless updates without restarts
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
xcaddy build --with github.com/fabriziosalmi/caddy-waf
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```caddyfile
|
||||
example.com {
|
||||
waf {
|
||||
rule_file rules.json
|
||||
ip_blacklist_file ip_blacklist.txt
|
||||
dns_blacklist_file dns_blacklist.txt
|
||||
metrics_endpoint /waf_metrics
|
||||
}
|
||||
|
||||
respond "Protected by Caddy WAF"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Option | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `rule_file` | string | Path to WAF rules JSON file |
|
||||
| `ip_blacklist_file` | string | Path to IP blacklist file |
|
||||
| `dns_blacklist_file` | string | Path to DNS blacklist file |
|
||||
| `metrics_endpoint` | string | Endpoint for WAF metrics |
|
||||
| `anomaly_threshold` | int | Threshold for anomaly detection |
|
||||
| `rate_limit` | block | Rate limiting configuration |
|
||||
| `country_block` | block | Country blocking configuration |
|
||||
| `custom_response` | block | Custom response configuration |
|
||||
| `log_level` | string | Logging level (debug, info, warn, error) |
|
||||
| `log_file` | string | Path to log file |
|
||||
|
||||
## Documentation
|
||||
|
||||
Complete documentation is available in the [docs directory](https://github.com/fabriziosalmi/caddy-waf/tree/main/docs).
|
||||
|
||||
## Repository
|
||||
|
||||
https://github.com/fabriziosalmi/caddy-waf
|
||||
|
||||
## Support
|
||||
|
||||
For issues and support, please visit the [GitHub Issues page](https://github.com/fabriziosalmi/caddy-waf/issues).
|
||||
40
README.md
40
README.md
@@ -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.
|
||||
|
||||
[](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/go.yml) [](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/github-code-scanning/codeql) [](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/build-run-validate.yml)
|
||||
[](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/test.yml) [](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/github-code-scanning/codeql) [](https://github.com/fabriziosalmi/caddy-waf/actions/workflows/build-run-validate.yml)
|
||||
|
||||
## 🛡️ Core Protections
|
||||
|
||||
* **Regex-Based Filtering:** Deep URL, data & header inspection using powerful regex rules.
|
||||
* **Blacklisting:** Blocks malicious IPs, domains & optionally TOR exit nodes.
|
||||
* **Blacklisting:** Blocks malicious IPs, domains, ASNs & optionally TOR exit nodes.
|
||||
* **Geo-Blocking:** Restricts access by country using GeoIP.
|
||||
* **Rate Limiting:** Prevents abuse via customizable IP request limits.
|
||||
* **Anomaly Scoring:** Dynamically blocks requests based on cumulative rule matches.
|
||||
@@ -18,10 +18,18 @@ A robust, highly customizable, and feature-rich **Web Application Firewall (WAF)
|
||||
* **Dynamic Config Reloads:** Seamless updates without restarts.
|
||||
* **File Watchers:** Automatic reloads on rule/blacklist changes.
|
||||
* **Observability:** Seamless integration with ELK stack and Prometheus.
|
||||
* **Rules generator**: [available here](https://chatgpt.com/g/g-677d07dd07e48191b799b9e5d6da7828-caddy-waf-ruler)
|
||||
|
||||
_Simple at a glance UI :)_
|
||||

|
||||
|
||||
## Security & Performance
|
||||
* **Zero-Copy Networking**: Uses `unsafe.String` to eliminate memory allocations during request body inspection.
|
||||
* **Wait-Free Concurrency**: Atomic counters ensure accurate metrics and rule hit counting without lock contention.
|
||||
* **Circuit Breaker**: `geoip_fail_open` prevents database failures from causing service outages.
|
||||
* **DoS Protection**: `io.LimitReader` enforces strict request body limits to prevent memory exhaustion.
|
||||
* **ReDoS Safety**: Built on top of Go's `regexp` (RE2), guaranteeing linear time execution for all regex rules.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
@@ -54,6 +62,12 @@ curl -fsSL -H "Pragma: no-cache" https://raw.githubusercontent.com/fabriziosalmi
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Go](https://golang.org/dl/) **1.25** or higher
|
||||
- [Caddy](https://caddyserver.com/docs/install) **v2.10.x** or higher (for building with this plugin)
|
||||
- [xcaddy](https://github.com/caddyserver/xcaddy) (for building Caddy with plugins)
|
||||
|
||||
```bash
|
||||
# Step 1: Clone the caddy-waf repository from GitHub
|
||||
git clone https://github.com/fabriziosalmi/caddy-waf.git
|
||||
@@ -166,6 +180,28 @@ This project is licensed under the **AGPLv3 License**.
|
||||
|
||||
---
|
||||
|
||||
## Others projects
|
||||
|
||||
If You like my projects, you may also like these ones:
|
||||
|
||||
- [patterns](https://github.com/fabriziosalmi/patterns) Automated OWASP CRS and Bad Bot Detection for Nginx, Apache, Traefik and HaProxy
|
||||
- [blacklists](https://github.com/fabriziosalmi/blacklists) Hourly updated domains blacklist 🚫
|
||||
- [proxmox-vm-autoscale](https://github.com/fabriziosalmi/proxmox-vm-autoscale) Automatically scale virtual machines resources on Proxmox hosts
|
||||
- [UglyFeed](https://github.com/fabriziosalmi/UglyFeed) Retrieve, aggregate, filter, evaluate, rewrite and serve RSS feeds using Large Language Models for fun, research and learning purposes
|
||||
- [proxmox-lxc-autoscale](https://github.com/fabriziosalmi/proxmox-lxc-autoscale) Automatically scale LXC containers resources on Proxmox hosts
|
||||
- [DevAssistant](https://github.com/fabriziosalmi/DevGPT) Code together, right now! AI powered code assistant to build project in minutes
|
||||
- [websites-monitor](https://github.com/fabriziosalmi/websites-monitor) Websites monitoring via GitHub Actions (expiration, security, performances, privacy, SEO)
|
||||
- [caddy-mib](https://github.com/fabriziosalmi/caddy-mib) Track and ban client IPs generating repetitive errors on Caddy
|
||||
- [zonecontrol](https://github.com/fabriziosalmi/zonecontrol) Cloudflare Zones Settings Automation using GitHub Actions
|
||||
- [lws](https://github.com/fabriziosalmi/lws) linux (containers) web services
|
||||
- [cf-box](https://github.com/fabriziosalmi/cf-box) cf-box is a set of Python tools to play with API and multiple Cloudflare accounts.
|
||||
- [limits](https://github.com/fabriziosalmi/limits) Automated rate limits implementation for web servers
|
||||
- [dnscontrol-actions](https://github.com/fabriziosalmi/dnscontrol-actions) Automate DNS updates and rollbacks across multiple providers using DNSControl and GitHub Actions
|
||||
- [proxmox-lxc-autoscale-ml](https://github.com/fabriziosalmi/proxmox-lxc-autoscale-ml) Automatically scale the LXC containers resources on Proxmox hosts with AI
|
||||
- [csv-anonymizer](https://github.com/fabriziosalmi/csv-anonymizer) CSV fuzzer/anonymizer
|
||||
- [iamnotacoder](https://github.com/fabriziosalmi/iamnotacoder) AI code generation and improvement
|
||||
|
||||
|
||||
## 🙏 Contributing
|
||||
|
||||
Contributions are highly welcome! Feel free to open an issue or submit a pull request.
|
||||
|
||||
31
SECURITY.md
31
SECURITY.md
@@ -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
8
assets.go
Normal 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
7
assets_stub.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !with_ui
|
||||
|
||||
package caddywaf
|
||||
|
||||
import "embed"
|
||||
|
||||
var Assets embed.FS
|
||||
30
blacklist.go
30
blacklist.go
@@ -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))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package caddywaf
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -15,7 +14,7 @@ func TestLoadDNSBlacklistFromFile(t *testing.T) {
|
||||
malicious.com
|
||||
spaces.com
|
||||
`
|
||||
tmpfile, err := ioutil.TempFile("", "dnsblacklist")
|
||||
tmpfile, err := os.CreateTemp("", "dnsblacklist")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -60,7 +59,7 @@ func TestLoadIPBlacklistFromFile(t *testing.T) {
|
||||
172.16.1.1
|
||||
invalid-ip
|
||||
`
|
||||
tmpfile, err := ioutil.TempFile("", "ipblacklist")
|
||||
tmpfile, err := os.CreateTemp("", "ipblacklist")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -97,30 +96,6 @@ invalid-ip
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"IP with port", "192.168.1.1:8080", "192.168.1.1"},
|
||||
{"IP only", "192.168.1.1", "192.168.1.1"},
|
||||
{"IPv6 with port", "[2001:db8::1]:8080", "2001:db8::1"},
|
||||
{"IPv6 only", "2001:db8::1", "2001:db8::1"},
|
||||
}
|
||||
|
||||
logger := zap.NewNop()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := extractIP(tt.input, logger)
|
||||
if result != tt.expected {
|
||||
t.Errorf("extractIP(%s) = %s; want %s", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddIPEntry(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
bl := NewBlacklistLoader(logger)
|
||||
|
||||
75
caddyfile.example
Normal file
75
caddyfile.example
Normal file
@@ -0,0 +1,75 @@
|
||||
# Example Caddyfile showing caddy-waf module usage
|
||||
# This is a basic configuration example for the Caddy WAF middleware
|
||||
|
||||
{
|
||||
auto_https off
|
||||
admin localhost:2019
|
||||
}
|
||||
|
||||
# Example 1: Basic WAF setup
|
||||
example.com {
|
||||
# Enable WAF protection with basic configuration
|
||||
waf {
|
||||
# Rule file for WAF rules
|
||||
rule_file rules.json
|
||||
|
||||
# IP blacklist file
|
||||
ip_blacklist_file ip_blacklist.txt
|
||||
|
||||
# DNS blacklist file
|
||||
dns_blacklist_file dns_blacklist.txt
|
||||
|
||||
# Metrics endpoint
|
||||
metrics_endpoint /waf_metrics
|
||||
|
||||
# Anomaly threshold
|
||||
anomaly_threshold 10
|
||||
|
||||
# Log settings
|
||||
log_level info
|
||||
log_file waf.log
|
||||
}
|
||||
|
||||
# Your web application
|
||||
respond "Hello, World! Protected by Caddy WAF"
|
||||
}
|
||||
|
||||
# Example 2: Advanced WAF configuration with rate limiting
|
||||
api.example.com {
|
||||
waf {
|
||||
rule_file rules.json
|
||||
ip_blacklist_file ip_blacklist.txt
|
||||
dns_blacklist_file dns_blacklist.txt
|
||||
metrics_endpoint /waf_metrics
|
||||
|
||||
# Rate limiting configuration
|
||||
rate_limit {
|
||||
requests 100
|
||||
window 10s
|
||||
paths "/api/*" "/admin/*"
|
||||
}
|
||||
|
||||
# Country blocking
|
||||
country_block {
|
||||
enabled true
|
||||
countries CN RU
|
||||
geoip_db_path GeoLite2-Country.mmdb
|
||||
}
|
||||
|
||||
# Custom response for blocked requests
|
||||
custom_response {
|
||||
status_code 403
|
||||
body "Access Denied by WAF"
|
||||
}
|
||||
|
||||
# Anomaly threshold
|
||||
anomaly_threshold 15
|
||||
|
||||
# Logging
|
||||
log_level debug
|
||||
log_file api_waf.log
|
||||
log_json true
|
||||
}
|
||||
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
145
caddywaf.go
145
caddywaf.go
@@ -1,3 +1,18 @@
|
||||
// Package caddywaf implements a Web Application Firewall (WAF) middleware for Caddy.
|
||||
//
|
||||
// This package provides comprehensive security features including:
|
||||
// - Regex-based filtering for URLs, data, and headers
|
||||
// - IP and DNS blacklisting capabilities
|
||||
// - Geographic access control
|
||||
// - Rate limiting
|
||||
// - Anomaly detection and scoring
|
||||
// - Multi-phase request inspection
|
||||
// - Real-time metrics and monitoring
|
||||
//
|
||||
// The WAF integrates seamlessly with Caddy as an HTTP handler middleware
|
||||
// and can be configured via Caddyfile or JSON configuration.
|
||||
//
|
||||
// Module ID: http.handlers.waf
|
||||
package caddywaf
|
||||
|
||||
import (
|
||||
@@ -5,37 +20,40 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
"github.com/phemmer/go-iptrie"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// ==================== Constants and Globals ====================
|
||||
|
||||
var (
|
||||
_ caddy.Module = (*Middleware)(nil) // <-- AGGIUNGI QUESTA RIGA!
|
||||
_ caddy.Provisioner = (*Middleware)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
|
||||
_ caddyfile.Unmarshaler = (*Middleware)(nil)
|
||||
_ caddy.Validator = (*Middleware)(nil)
|
||||
_ caddy.Validator = (*Middleware)(nil) // Assicurati che anche questa sia presente se hai un metodo Validate()
|
||||
)
|
||||
|
||||
// Add or update the version constant as needed
|
||||
const wafVersion = "v0.0.1" // update this value to the new release version when tagging
|
||||
const wafVersion = "v0.1.3" // update this value to the new release version when tagging
|
||||
|
||||
// ==================== Initialization and Setup ====================
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(&Middleware{}) // Changed from Middleware{} to &Middleware{}
|
||||
caddy.RegisterModule(&Middleware{}) // Register the module with Caddy
|
||||
httpcaddyfile.RegisterHandlerDirective("waf", parseCaddyfile)
|
||||
}
|
||||
|
||||
@@ -64,7 +82,9 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
|
||||
func (m *Middleware) Provision(ctx caddy.Context) error {
|
||||
m.logger = ctx.Logger(m)
|
||||
m.ruleCache = NewRuleCache() // Initialize RuleCache
|
||||
m.ruleCache = NewRuleCache() // Initialize RuleCache
|
||||
m.Rules = make(map[int][]Rule) // Initialize Rules map to prevent nil pointer panic
|
||||
m.ipBlacklist = iptrie.NewTrie()
|
||||
|
||||
// Set default log severity if not provided
|
||||
if m.LogSeverity == "" {
|
||||
@@ -102,7 +122,7 @@ func (m *Middleware) Provision(ctx caddy.Context) error {
|
||||
fileCfg.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
fileEncoder := zapcore.NewJSONEncoder(fileCfg.EncoderConfig)
|
||||
|
||||
fileSync, err := os.OpenFile(m.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
fileSync, err := os.OpenFile(m.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to open log file, logging only to console", zap.String("path", m.LogFilePath), zap.Error(err))
|
||||
m.logger = zap.New(zapcore.NewCore(consoleEncoder, consoleSync, logLevel))
|
||||
@@ -123,6 +143,14 @@ func (m *Middleware) Provision(ctx caddy.Context) error {
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold),
|
||||
)
|
||||
|
||||
// ADDED: Set default anomaly threshold if not provided or invalid
|
||||
if m.AnomalyThreshold <= 0 {
|
||||
m.AnomalyThreshold = 20 // Use a reasonable default value
|
||||
m.logger.Info("Using default anomaly threshold", zap.Int("anomaly_threshold", m.AnomalyThreshold))
|
||||
} else {
|
||||
m.logger.Info("Using configured anomaly threshold", zap.Int("anomaly_threshold", m.AnomalyThreshold))
|
||||
}
|
||||
|
||||
// Start the asynchronous logging worker
|
||||
m.StartLogWorker()
|
||||
|
||||
@@ -167,23 +195,23 @@ func (m *Middleware) Provision(ctx caddy.Context) error {
|
||||
// Initialize GeoIP stats
|
||||
m.geoIPStats = make(map[string]int64)
|
||||
|
||||
// Configure GeoIP-based country blocking/whitelisting
|
||||
if m.CountryBlock.Enabled || m.CountryWhitelist.Enabled {
|
||||
geoIPPath := m.CountryBlock.GeoIPDBPath
|
||||
// Configure GeoIP-based country blacklisting/whitelisting
|
||||
if m.CountryBlacklist.Enabled || m.CountryWhitelist.Enabled {
|
||||
geoIPPath := m.CountryBlacklist.GeoIPDBPath
|
||||
if m.CountryWhitelist.Enabled && m.CountryWhitelist.GeoIPDBPath != "" {
|
||||
geoIPPath = m.CountryWhitelist.GeoIPDBPath
|
||||
}
|
||||
|
||||
if !fileExists(geoIPPath) {
|
||||
m.logger.Warn("GeoIP database not found. Country blocking/whitelisting will be disabled", zap.String("path", geoIPPath))
|
||||
m.logger.Warn("GeoIP database not found. Country blacklisting/whitelisting will be disabled", zap.String("path", geoIPPath))
|
||||
} else {
|
||||
reader, err := maxminddb.Open(geoIPPath)
|
||||
if err != nil {
|
||||
m.logger.Error("Failed to load GeoIP database", zap.String("path", geoIPPath), zap.Error(err))
|
||||
} else {
|
||||
m.logger.Info("GeoIP database loaded successfully", zap.String("path", geoIPPath))
|
||||
if m.CountryBlock.Enabled {
|
||||
m.CountryBlock.geoIP = reader
|
||||
if m.CountryBlacklist.Enabled {
|
||||
m.CountryBlacklist.geoIP = reader
|
||||
}
|
||||
if m.CountryWhitelist.Enabled {
|
||||
m.CountryWhitelist.geoIP = reader
|
||||
@@ -192,27 +220,46 @@ func (m *Middleware) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Configure ASN blocking
|
||||
if m.BlockASNs.Enabled {
|
||||
if !fileExists(m.BlockASNs.GeoIPDBPath) {
|
||||
m.logger.Warn("ASN GeoIP database not found. ASN blocking will be disabled", zap.String("path", m.BlockASNs.GeoIPDBPath))
|
||||
} else {
|
||||
reader, err := maxminddb.Open(m.BlockASNs.GeoIPDBPath)
|
||||
if err != nil {
|
||||
m.logger.Error("Failed to load ASN GeoIP database", zap.String("path", m.BlockASNs.GeoIPDBPath), zap.Error(err))
|
||||
} else {
|
||||
m.logger.Info("ASN GeoIP database loaded successfully", zap.String("path", m.BlockASNs.GeoIPDBPath))
|
||||
m.BlockASNs.geoIP = reader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize config and blacklist loaders
|
||||
m.configLoader = NewConfigLoader(m.logger)
|
||||
m.blacklistLoader = NewBlacklistLoader(m.logger)
|
||||
m.geoIPHandler = NewGeoIPHandler(m.logger)
|
||||
m.requestValueExtractor = NewRequestValueExtractor(m.logger, m.RedactSensitiveData)
|
||||
m.requestValueExtractor = NewRequestValueExtractor(m.logger, m.RedactSensitiveData, m.MaxRequestBodySize)
|
||||
|
||||
// Configure GeoIP handler
|
||||
m.geoIPHandler.WithGeoIPCache(m.geoIPCacheTTL)
|
||||
m.geoIPHandler.WithGeoIPLookupFallbackBehavior(m.geoIPLookupFallbackBehavior)
|
||||
|
||||
// Load configuration from Caddyfile
|
||||
dispenser := caddyfile.NewDispenser([]caddyfile.Token{})
|
||||
err = m.configLoader.UnmarshalCaddyfile(dispenser, m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
// Initialize TorConfig with default values if not set
|
||||
if m.Tor.TORIPBlacklistFile == "" {
|
||||
m.Tor.TORIPBlacklistFile = "tor_blacklist.txt"
|
||||
}
|
||||
if m.Tor.UpdateInterval == "" {
|
||||
m.Tor.UpdateInterval = "24h"
|
||||
}
|
||||
if m.Tor.RetryInterval == "" {
|
||||
m.Tor.RetryInterval = "5m"
|
||||
}
|
||||
|
||||
// Load IP blacklist
|
||||
if m.IPBlacklistFile != "" {
|
||||
m.ipBlacklist = NewCIDRTrie()
|
||||
err = m.loadIPBlacklist(m.IPBlacklistFile, m.ipBlacklist)
|
||||
m.ipBlacklist = iptrie.NewTrie()
|
||||
err = m.loadIPBlacklist(m.IPBlacklistFile, *m.ipBlacklist)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load IP blacklist: %w", err)
|
||||
}
|
||||
@@ -259,23 +306,19 @@ func (m *Middleware) Shutdown(ctx context.Context) error {
|
||||
m.logger.Debug("Logging worker stopped.")
|
||||
|
||||
var firstError error
|
||||
var errorOccurred bool
|
||||
|
||||
// Close GeoIP databases
|
||||
if m.CountryBlock.geoIP != nil {
|
||||
m.logger.Debug("Closing country block GeoIP database...")
|
||||
if err := m.CountryBlock.geoIP.Close(); err != nil {
|
||||
m.logger.Error("Error encountered while closing country block GeoIP database", zap.Error(err))
|
||||
if !errorOccurred {
|
||||
firstError = fmt.Errorf("error closing country block GeoIP: %w", err)
|
||||
errorOccurred = true
|
||||
}
|
||||
if m.CountryBlacklist.geoIP != nil {
|
||||
m.logger.Debug("Closing country blacklist GeoIP database...")
|
||||
if err := m.CountryBlacklist.geoIP.Close(); err != nil {
|
||||
m.logger.Error("Error encountered while closing country blacklist GeoIP database", zap.Error(err))
|
||||
firstError = fmt.Errorf("error closing country blacklist GeoIP: %w", err)
|
||||
} else {
|
||||
m.logger.Debug("Country block GeoIP database closed successfully.")
|
||||
m.logger.Debug("Country blacklist GeoIP database closed successfully.")
|
||||
}
|
||||
m.CountryBlock.geoIP = nil
|
||||
m.CountryBlacklist.geoIP = nil
|
||||
} else {
|
||||
m.logger.Debug("Country block GeoIP database was not open, skipping close.")
|
||||
m.logger.Debug("Country blacklist GeoIP database was not open, skipping close.")
|
||||
}
|
||||
|
||||
if m.CountryWhitelist.geoIP != nil {
|
||||
@@ -293,6 +336,19 @@ func (m *Middleware) Shutdown(ctx context.Context) error {
|
||||
m.logger.Debug("Country whitelist GeoIP database was not open, skipping close.")
|
||||
}
|
||||
|
||||
if m.BlockASNs.geoIP != nil {
|
||||
m.logger.Debug("Closing ASN GeoIP database...")
|
||||
if err := m.BlockASNs.geoIP.Close(); err != nil {
|
||||
m.logger.Error("Error encountered while closing ASN GeoIP database", zap.Error(err))
|
||||
if firstError == nil {
|
||||
firstError = fmt.Errorf("error closing ASN GeoIP: %w", err)
|
||||
}
|
||||
} else {
|
||||
m.logger.Debug("ASN GeoIP database closed successfully.")
|
||||
}
|
||||
m.BlockASNs.geoIP = nil
|
||||
}
|
||||
|
||||
// Log rule hit statistics
|
||||
m.logger.Info("Rule Hit Statistics:")
|
||||
m.ruleHits.Range(func(key, value interface{}) bool {
|
||||
@@ -302,15 +358,16 @@ func (m *Middleware) Shutdown(ctx context.Context) error {
|
||||
return true
|
||||
}
|
||||
|
||||
hitCount, ok := value.(HitCount)
|
||||
atomicCounter, ok := value.(*atomic.Int64)
|
||||
if !ok {
|
||||
m.logger.Error("Invalid type for hit count in ruleHits map", zap.Any("value", value))
|
||||
return true
|
||||
}
|
||||
hitCount := atomicCounter.Load()
|
||||
|
||||
m.logger.Info("Rule Hit",
|
||||
zap.String("rule_id", string(ruleID)),
|
||||
zap.Int("hits", int(hitCount)),
|
||||
zap.Int64("hits", hitCount),
|
||||
)
|
||||
return true
|
||||
})
|
||||
@@ -400,8 +457,8 @@ func (m *Middleware) ReloadConfig() error {
|
||||
|
||||
m.logger.Info("Reloading WAF configuration")
|
||||
if m.IPBlacklistFile != "" {
|
||||
newIPBlacklist := NewCIDRTrie()
|
||||
if err := m.loadIPBlacklist(m.IPBlacklistFile, newIPBlacklist); err != nil {
|
||||
newIPBlacklist := iptrie.NewTrie()
|
||||
if err := m.loadIPBlacklist(m.IPBlacklistFile, *newIPBlacklist); err != nil {
|
||||
m.logger.Error("Failed to reload IP blacklist", zap.String("file", m.IPBlacklistFile), zap.Error(err))
|
||||
return fmt.Errorf("failed to reload IP blacklist: %v", err)
|
||||
}
|
||||
@@ -426,7 +483,7 @@ func (m *Middleware) ReloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Middleware) loadIPBlacklist(path string, blacklistMap *CIDRTrie) error {
|
||||
func (m *Middleware) loadIPBlacklist(path string, blacklistMap iptrie.Trie) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
m.logger.Warn("Skipping IP blacklist load, file does not exist", zap.String("file", path))
|
||||
return nil
|
||||
@@ -440,7 +497,12 @@ func (m *Middleware) loadIPBlacklist(path string, blacklistMap *CIDRTrie) error
|
||||
|
||||
// Convert the map to CIDRTrie
|
||||
for ip := range blacklist {
|
||||
blacklistMap.Insert(ip)
|
||||
prefix, err := netip.ParsePrefix(appendCIDR(ip))
|
||||
if err != nil {
|
||||
m.logger.Warn("Skipping invalid IP in blacklist", zap.String("ip", ip), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
blacklistMap.Insert(prefix, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -468,12 +530,13 @@ func (m *Middleware) getRuleHitStats() map[string]int {
|
||||
m.logger.Error("Invalid type for rule ID in ruleHits map", zap.Any("key", key))
|
||||
return true // Continue iteration
|
||||
}
|
||||
hitCount, ok := value.(HitCount)
|
||||
// SOTA Pattern: Wait-Free stats collection
|
||||
atomicCounter, ok := value.(*atomic.Int64)
|
||||
if !ok {
|
||||
m.logger.Error("Invalid type for hit count in ruleHits map", zap.Any("value", value))
|
||||
return true // Continue iteration
|
||||
}
|
||||
stats[string(ruleID)] = int(hitCount)
|
||||
stats[string(ruleID)] = int(atomicCounter.Load())
|
||||
return true
|
||||
})
|
||||
return stats
|
||||
|
||||
@@ -2,15 +2,14 @@ package caddywaf
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func TestMiddleware_Provision(t *testing.T) {
|
||||
@@ -33,7 +32,7 @@ func TestMiddleware_Provision(t *testing.T) {
|
||||
IPBlacklistFile: "testdata/ip_blacklist.txt",
|
||||
DNSBlacklistFile: "testdata/dns_blacklist.txt",
|
||||
AnomalyThreshold: 10,
|
||||
CountryBlock: CountryAccessFilter{
|
||||
CountryBlacklist: CountryAccessFilter{
|
||||
Enabled: true,
|
||||
CountryList: []string{"US"},
|
||||
GeoIPDBPath: "testdata/GeoIP2-Country-Test.mmdb",
|
||||
|
||||
269
check_waf_config.py
Normal file
269
check_waf_config.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
from termcolor import colored
|
||||
|
||||
def setup_args():
|
||||
parser = argparse.ArgumentParser(description='Check WAF configuration for testing')
|
||||
parser.add_argument('--url', default='http://localhost:8080', help='URL to test (default: http://localhost:8080)')
|
||||
parser.add_argument('--config-endpoint', default='', help='Endpoint for accessing WAF configuration (if available)')
|
||||
parser.add_argument('--rules-file', default='sample_rules.json', help='Path to rules file (default: sample_rules.json)')
|
||||
return parser.parse_args()
|
||||
|
||||
def load_rules_from_file(file_path):
|
||||
"""Load rules from a JSON file, handling comments if present."""
|
||||
try:
|
||||
# Read the file content
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Remove JavaScript-style comments if present
|
||||
content = re.sub(r'//.*?\n', '\n', content) # Remove single-line comments
|
||||
content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) # Remove multi-line comments
|
||||
|
||||
# Parse JSON
|
||||
rules = json.loads(content)
|
||||
print(colored(f"Loaded {len(rules)} rules from {file_path}", "green"))
|
||||
return rules
|
||||
except json.JSONDecodeError as e:
|
||||
print(colored(f"Error parsing JSON from {file_path}: {str(e)}", "red"))
|
||||
print(colored("Make sure the file is valid JSON. JavaScript-style comments are stripped automatically.", "yellow"))
|
||||
return []
|
||||
except Exception as e:
|
||||
print(colored(f"Error loading rules from {file_path}: {str(e)}", "red"))
|
||||
return []
|
||||
|
||||
def check_rule_coverage(rules, threshold=5):
|
||||
"""Check if rules cover all test cases needed for anomaly threshold test."""
|
||||
required_tests = {
|
||||
"low_score_test": False,
|
||||
"param1_score2": False,
|
||||
"param2_score2": False,
|
||||
"param1_score3": False,
|
||||
"param2_score3": False,
|
||||
"block_true": False,
|
||||
"increment_score1": False,
|
||||
"increment_score2": False,
|
||||
"increment_score3": False
|
||||
}
|
||||
|
||||
# Store rule scores for tests
|
||||
rule_scores = {
|
||||
"low_score_test": 0,
|
||||
"param1_score2": 0,
|
||||
"param2_score2": 0,
|
||||
"param1_score3": 0,
|
||||
"param2_score3": 0,
|
||||
"increment_score1": 0,
|
||||
"increment_score2": 0,
|
||||
"increment_score3": 0
|
||||
}
|
||||
|
||||
block_rule_mode = None
|
||||
|
||||
for rule in rules:
|
||||
# Check for low score test rule
|
||||
if 'targets' in rule and 'URL_PARAM:test' in rule['targets'] and 'pattern' in rule and 'low_score_test' in rule['pattern']:
|
||||
required_tests["low_score_test"] = True
|
||||
print(colored(f"✓ Found rule for test=low_score_test (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
if 'score' in rule:
|
||||
rule_scores["low_score_test"] = rule.get('score', 0)
|
||||
print(colored(f" Score: {rule['score']}", "yellow"))
|
||||
|
||||
# Check for param1 score2
|
||||
if 'targets' in rule and 'URL_PARAM:param1' in rule['targets'] and 'pattern' in rule and 'score2' in rule['pattern']:
|
||||
required_tests["param1_score2"] = True
|
||||
print(colored(f"✓ Found rule for param1=score2 (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
if 'score' in rule:
|
||||
rule_scores["param1_score2"] = rule.get('score', 0)
|
||||
print(colored(f" Score: {rule['score']}", "yellow"))
|
||||
|
||||
# Check for param2 score2
|
||||
if 'targets' in rule and 'URL_PARAM:param2' in rule['targets'] and 'pattern' in rule and 'score2' in rule['pattern']:
|
||||
required_tests["param2_score2"] = True
|
||||
print(colored(f"✓ Found rule for param2=score2 (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
if 'score' in rule:
|
||||
rule_scores["param2_score2"] = rule.get('score', 0)
|
||||
print(colored(f" Score: {rule['score']}", "yellow"))
|
||||
|
||||
# Check for param1 score3
|
||||
if 'targets' in rule and 'URL_PARAM:param1' in rule['targets'] and 'pattern' in rule and 'score3' in rule['pattern']:
|
||||
required_tests["param1_score3"] = True
|
||||
print(colored(f"✓ Found rule for param1=score3 (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
if 'score' in rule:
|
||||
rule_scores["param1_score3"] = rule.get('score', 0)
|
||||
print(colored(f" Score: {rule['score']}", "yellow"))
|
||||
|
||||
# Check for param2 score3
|
||||
if 'targets' in rule and 'URL_PARAM:param2' in rule['targets'] and 'pattern' in rule and 'score3' in rule['pattern']:
|
||||
required_tests["param2_score3"] = True
|
||||
print(colored(f"✓ Found rule for param2=score3 (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
if 'score' in rule:
|
||||
rule_scores["param2_score3"] = rule.get('score', 0)
|
||||
print(colored(f" Score: {rule['score']}", "yellow"))
|
||||
|
||||
# Check for block action
|
||||
if 'targets' in rule and 'URL_PARAM:block' in rule['targets'] and 'pattern' in rule and 'true' in rule['pattern']:
|
||||
required_tests["block_true"] = True
|
||||
block_rule_mode = rule.get('mode', 'unknown')
|
||||
print(colored(f"✓ Found rule for block=true (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
print(colored(f" Action: {block_rule_mode}", "yellow"))
|
||||
if block_rule_mode != 'block':
|
||||
print(colored(" WARNING: This rule should have mode='block'", "red"))
|
||||
|
||||
# Check for increment score rules
|
||||
if 'targets' in rule and 'URL_PARAM:increment' in rule['targets']:
|
||||
if 'pattern' in rule and 'score1' in rule['pattern']:
|
||||
required_tests["increment_score1"] = True
|
||||
rule_scores["increment_score1"] = rule.get('score', 0)
|
||||
print(colored(f"✓ Found rule for increment=score1 (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
if 'score' in rule:
|
||||
print(colored(f" Score: {rule['score']}", "yellow"))
|
||||
|
||||
if 'pattern' in rule and 'score2' in rule['pattern']:
|
||||
required_tests["increment_score2"] = True
|
||||
rule_scores["increment_score2"] = rule.get('score', 0)
|
||||
print(colored(f"✓ Found rule for increment=score2 (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
if 'score' in rule:
|
||||
print(colored(f" Score: {rule['score']}", "yellow"))
|
||||
|
||||
if 'pattern' in rule and 'score3' in rule['pattern']:
|
||||
required_tests["increment_score3"] = True
|
||||
rule_scores["increment_score3"] = rule.get('score', 0)
|
||||
print(colored(f"✓ Found rule for increment=score3 (ID: {rule.get('id', 'unknown')})", "green"))
|
||||
if 'score' in rule:
|
||||
print(colored(f" Score: {rule['score']}", "yellow"))
|
||||
|
||||
# Check test coverage
|
||||
missing_tests = [test.replace('_', '=') for test, found in required_tests.items() if not found]
|
||||
if missing_tests:
|
||||
print(colored(f"\n⚠ Missing rules for: {', '.join(missing_tests)}", "red"))
|
||||
else:
|
||||
print(colored("\n✓ All required test rules are present!", "green"))
|
||||
|
||||
# Validate expected scores for key test combinations
|
||||
print(colored("\nCalculated Scores for Key Test Combinations:", "cyan"))
|
||||
|
||||
# Test 2: Below threshold
|
||||
test2_score = rule_scores["param1_score2"] + rule_scores["param2_score2"]
|
||||
test2_should_block = test2_score >= threshold
|
||||
|
||||
if required_tests["param1_score2"] and required_tests["param2_score2"]:
|
||||
print(colored(f"Test 2 - param1=score2¶m2=score2: Score = {test2_score}", "yellow"))
|
||||
print(colored(f" Threshold: {threshold}, Should Block: {'Yes' if test2_should_block else 'No'}",
|
||||
"red" if test2_should_block else "green"))
|
||||
if test2_should_block:
|
||||
print(colored(" WARNING: This test should pass (not block) but the score may trigger blocking", "red"))
|
||||
else:
|
||||
print(colored("Test 2 - param1=score2¶m2=score2: Cannot calculate - missing rules", "red"))
|
||||
|
||||
# Test 3: Exceeds threshold
|
||||
test3_score = rule_scores["param1_score3"] + rule_scores["param2_score3"]
|
||||
test3_should_block = test3_score >= threshold
|
||||
|
||||
if required_tests["param1_score3"] and required_tests["param2_score3"]:
|
||||
print(colored(f"Test 3 - param1=score3¶m2=score3: Score = {test3_score}", "yellow"))
|
||||
print(colored(f" Threshold: {threshold}, Should Block: {'Yes' if test3_should_block else 'No'}",
|
||||
"green" if test3_should_block else "red"))
|
||||
if not test3_should_block:
|
||||
print(colored(" WARNING: This test should be blocked but the score is below threshold", "red"))
|
||||
else:
|
||||
print(colored("Test 3 - param1=score3¶m2=score3: Cannot calculate - missing rules", "red"))
|
||||
|
||||
# Test 4: Block action
|
||||
if required_tests["block_true"]:
|
||||
block_should_work = block_rule_mode == 'block'
|
||||
print(colored(f"Test 4 - block=true: Mode = {block_rule_mode}", "yellow"))
|
||||
print(colored(f" Should Block: {'Yes' if block_should_work else 'No'}",
|
||||
"green" if block_should_work else "red"))
|
||||
if not block_should_work:
|
||||
print(colored(" WARNING: This rule should have mode='block' to properly test blocking", "red"))
|
||||
else:
|
||||
print(colored("Test 4 - block=true: Cannot evaluate - missing rule", "red"))
|
||||
|
||||
return required_tests, missing_tests, {
|
||||
"test2_score": test2_score if required_tests["param1_score2"] and required_tests["param2_score2"] else None,
|
||||
"test3_score": test3_score if required_tests["param1_score3"] and required_tests["param2_score3"] else None,
|
||||
"test2_should_block": test2_should_block if required_tests["param1_score2"] and required_tests["param2_score2"] else None,
|
||||
"test3_should_block": test3_should_block if required_tests["param1_score3"] and required_tests["param2_score3"] else None,
|
||||
"block_should_work": block_rule_mode == 'block' if required_tests["block_true"] else None
|
||||
}
|
||||
|
||||
def check_waf_active(url):
|
||||
"""Check if the WAF is active by attempting to trigger a basic rule."""
|
||||
block_payload = {'block': 'true'}
|
||||
|
||||
try:
|
||||
print(colored(f"\nSending test request to {url} with block=true", "blue"))
|
||||
response = requests.get(url, params=block_payload, timeout=5)
|
||||
|
||||
if response.status_code == 403:
|
||||
print(colored("✓ WAF appears to be active (blocked request as expected)", "green"))
|
||||
return True
|
||||
else:
|
||||
print(colored(f"⚠ WAF might not be active - received status {response.status_code} instead of 403", "red"))
|
||||
print(colored("Check your WAF configuration and make sure blocking is enabled", "yellow"))
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(colored(f"Error checking WAF: {str(e)}", "red"))
|
||||
return False
|
||||
|
||||
def main():
|
||||
args = setup_args()
|
||||
base_url = args.url
|
||||
rules_file = args.rules_file
|
||||
|
||||
print(colored("WAF Configuration Checker", "cyan"))
|
||||
print(colored(f"Target URL: {base_url}", "yellow"))
|
||||
print(colored(f"Rules file: {rules_file}", "yellow"))
|
||||
|
||||
# Check server connectivity
|
||||
try:
|
||||
response = requests.get(base_url, timeout=2)
|
||||
print(colored(f"✓ Server is reachable at {base_url}", "green"))
|
||||
except requests.exceptions.RequestException:
|
||||
print(colored(f"⚠ Cannot reach server at {base_url}", "red"))
|
||||
print(colored("Make sure Caddy is running with your WAF configuration.", "yellow"))
|
||||
sys.exit(1)
|
||||
|
||||
# Load and check rules
|
||||
rules = load_rules_from_file(rules_file)
|
||||
if rules:
|
||||
required_tests, missing_tests, test_scores = check_rule_coverage(rules)
|
||||
|
||||
print(colored("\nExpected Test Results Based on Rules:", "cyan"))
|
||||
if test_scores["test2_should_block"] is not None:
|
||||
status = "FAIL (should block)" if test_scores["test2_should_block"] else "PASS (should allow)"
|
||||
color = "red" if test_scores["test2_should_block"] else "green"
|
||||
print(colored(f"Test 2 (Below threshold): {status}", color))
|
||||
|
||||
if test_scores["test3_should_block"] is not None:
|
||||
status = "PASS (should block)" if test_scores["test3_should_block"] else "FAIL (should allow)"
|
||||
color = "green" if test_scores["test3_should_block"] else "red"
|
||||
print(colored(f"Test 3 (Exceed threshold): {status}", color))
|
||||
|
||||
if test_scores["block_should_work"] is not None:
|
||||
status = "PASS (should block)" if test_scores["block_should_work"] else "FAIL (won't block)"
|
||||
color = "green" if test_scores["block_should_work"] else "red"
|
||||
print(colored(f"Test 4 (Block action): {status}", color))
|
||||
|
||||
# Only check WAF if we have the necessary rules
|
||||
if required_tests["block_true"]:
|
||||
print(colored("\nVerifying WAF is active...", "cyan"))
|
||||
check_waf_active(base_url)
|
||||
|
||||
# Provide recommendations
|
||||
if missing_tests:
|
||||
print(colored("\nRecommendations:", "cyan"))
|
||||
print(colored("Add the missing rules to your configuration to run all tests successfully.", "yellow"))
|
||||
|
||||
print(colored("\nConfiguration check complete.", "cyan"))
|
||||
else:
|
||||
print(colored("\nCould not load rules for verification.", "red"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
21
common_test.go
Normal file
21
common_test.go
Normal 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",
|
||||
},
|
||||
}
|
||||
34
config.go
34
config.go
@@ -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()
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
package caddywaf
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
func TestNewConfigLoader(t *testing.T) {
|
||||
@@ -202,14 +201,14 @@ func TestParseCountryBlock(t *testing.T) {
|
||||
t.Fatalf("parseCountryBlockDirective failed: %v", err)
|
||||
}
|
||||
|
||||
if !m.CountryBlock.Enabled {
|
||||
t.Errorf("Expected country block to be enabled, got %v", m.CountryBlock.Enabled)
|
||||
if !m.CountryBlacklist.Enabled {
|
||||
t.Errorf("Expected country blacklist to be enabled, got %v", m.CountryBlacklist.Enabled)
|
||||
}
|
||||
if m.CountryBlock.GeoIPDBPath != "/etc/geoip/GeoIP.dat" {
|
||||
t.Errorf("Expected GeoIP DB path to be '/etc/geoip/GeoIP.dat', got '%s'", m.CountryBlock.GeoIPDBPath)
|
||||
if m.CountryBlacklist.GeoIPDBPath != "/etc/geoip/GeoIP.dat" {
|
||||
t.Errorf("Expected GeoIP DB path to be '/etc/geoip/GeoIP.dat', got '%s'", m.CountryBlacklist.GeoIPDBPath)
|
||||
}
|
||||
if len(m.CountryBlock.CountryList) != 2 || m.CountryBlock.CountryList[0] != "US" || m.CountryBlock.CountryList[1] != "CA" {
|
||||
t.Errorf("Expected country list to be ['US', 'CA'], got %v", m.CountryBlock.CountryList)
|
||||
if len(m.CountryBlacklist.CountryList) != 2 || m.CountryBlacklist.CountryList[0] != "US" || m.CountryBlacklist.CountryList[1] != "CA" {
|
||||
t.Errorf("Expected country list to be ['US', 'CA'], got %v", m.CountryBlacklist.CountryList)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
143
debug_test_results.py
Normal file
143
debug_test_results.py
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from termcolor import colored
|
||||
|
||||
def setup_args():
|
||||
parser = argparse.ArgumentParser(description='Debug WAF test result evaluation')
|
||||
parser.add_argument('--url', default='http://localhost:8080', help='URL to test (default: http://localhost:8080)')
|
||||
parser.add_argument('--detailed', action='store_true', help='Show detailed request/response information')
|
||||
return parser.parse_args()
|
||||
|
||||
def debug_response_evaluation(url, test_name, payload, expected_status):
|
||||
"""Send a request and debug the response evaluation logic."""
|
||||
print(colored(f"\n=== Debugging {test_name} ===", "cyan"))
|
||||
print(colored(f"URL: {url}", "yellow"))
|
||||
print(colored(f"Payload: {payload}", "yellow"))
|
||||
print(colored(f"Expected status: {expected_status}", "yellow"))
|
||||
|
||||
try:
|
||||
# Send the request
|
||||
print(colored("\nSending request...", "blue"))
|
||||
response = requests.get(
|
||||
url,
|
||||
params=payload,
|
||||
headers={'User-Agent': 'WAF-Threshold-Test-Debug/1.0'},
|
||||
timeout=5
|
||||
)
|
||||
|
||||
# Get the status code
|
||||
status = response.status_code
|
||||
print(colored(f"Received status code: {status}", "green"))
|
||||
|
||||
# Check if it matches expected
|
||||
match = status == expected_status
|
||||
match_str = "✓ MATCH" if match else "✗ MISMATCH"
|
||||
match_color = "green" if match else "red"
|
||||
print(colored(f"Status evaluation: {match_str}", match_color))
|
||||
|
||||
# Show response details
|
||||
print(colored("\nResponse details:", "cyan"))
|
||||
print(colored(f"Status code: {status}", "yellow"))
|
||||
print(colored(f"Response body: {response.text[:100]}...", "yellow") if len(response.text) > 100 else colored(f"Response body: {response.text}", "yellow"))
|
||||
|
||||
# Show evaluation details
|
||||
print(colored("\nEvaluation details:", "cyan"))
|
||||
print(colored(f"Python expression: response.status_code == {expected_status}", "yellow"))
|
||||
print(colored(f"Evaluation result: {response.status_code} == {expected_status} = {response.status_code == expected_status}", "yellow"))
|
||||
|
||||
# Boolean check
|
||||
bool_result = bool(response and response.status_code == expected_status)
|
||||
print(colored(f"Boolean check: bool(response and response.status_code == {expected_status}) = {bool_result}", "yellow"))
|
||||
|
||||
# Return result for summary
|
||||
return {
|
||||
"test_name": test_name,
|
||||
"expected": expected_status,
|
||||
"actual": status,
|
||||
"match": match,
|
||||
"bool_check": bool_result
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(colored(f"Error sending request: {str(e)}", "red"))
|
||||
return {
|
||||
"test_name": test_name,
|
||||
"error": str(e),
|
||||
"match": False,
|
||||
"bool_check": False
|
||||
}
|
||||
|
||||
def run_all_tests(url):
|
||||
"""Run all the tests from the anomaly threshold test script and debug the results."""
|
||||
print(colored("Running all tests and debugging evaluation logic...", "cyan"))
|
||||
|
||||
# Define all test cases
|
||||
test_cases = [
|
||||
{"name": "Test 1 (Low score)", "payload": {"test": "low_score_test"}, "expected": 200},
|
||||
{"name": "Test 2 (Below threshold)", "payload": {"param1": "score2", "param2": "score2"}, "expected": 200},
|
||||
{"name": "Test 3 (Exceed threshold)", "payload": {"param1": "score3", "param2": "score3"}, "expected": 403},
|
||||
{"name": "Test 4 (Block action)", "payload": {"block": "true"}, "expected": 403},
|
||||
{"name": "Test 5a (Increment 1)", "payload": {"increment": "score1"}, "expected": 200},
|
||||
{"name": "Test 5b (Increment 2)", "payload": {"increment": "score2"}, "expected": 200},
|
||||
{"name": "Test 5c (Increment 3)", "payload": {"increment": "score3"}, "expected": 200},
|
||||
]
|
||||
|
||||
# Run each test
|
||||
results = []
|
||||
for test in test_cases:
|
||||
result = debug_response_evaluation(url, test["name"], test["payload"], test["expected"])
|
||||
results.append(result)
|
||||
|
||||
# Show summary
|
||||
print(colored("\n=== Test Evaluation Summary ===", "cyan"))
|
||||
for result in results:
|
||||
if "error" in result:
|
||||
print(colored(f"{result['test_name']}: Error - {result['error']}", "red"))
|
||||
else:
|
||||
status = "PASS" if result["match"] else "FAIL"
|
||||
color = "green" if result["match"] else "red"
|
||||
print(colored(f"{result['test_name']}: {status} (Expected: {result['expected']}, Actual: {result['actual']})", color))
|
||||
print(colored(f" Boolean evaluation: {result['bool_check']}", "yellow"))
|
||||
|
||||
# Check for any issues with Tests 3 and 4
|
||||
test3 = next((r for r in results if r["test_name"] == "Test 3 (Exceed threshold)"), None)
|
||||
test4 = next((r for r in results if r["test_name"] == "Test 4 (Block action)"), None)
|
||||
|
||||
if test3 and test4:
|
||||
if test3["match"] and not test3["bool_check"]:
|
||||
print(colored("\nISSUE DETECTED: Test 3 status matches but boolean evaluation fails!", "red"))
|
||||
print(colored("This explains why the test incorrectly shows as failed.", "red"))
|
||||
|
||||
if test4["match"] and not test4["bool_check"]:
|
||||
print(colored("\nISSUE DETECTED: Test 4 status matches but boolean evaluation fails!", "red"))
|
||||
print(colored("This explains why the test incorrectly shows as failed.", "red"))
|
||||
|
||||
def main():
|
||||
args = setup_args()
|
||||
url = args.url
|
||||
detailed = args.detailed
|
||||
|
||||
print(colored("WAF Test Result Debugging Tool", "cyan"))
|
||||
print(colored(f"Target: {url}", "yellow"))
|
||||
|
||||
# Check server connectivity
|
||||
try:
|
||||
response = requests.get(url, timeout=2)
|
||||
print(colored(f"Server is reachable at {url}", "green"))
|
||||
|
||||
# Run all tests
|
||||
run_all_tests(url)
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
print(colored(f"ERROR: Cannot reach server at {url}", "red"))
|
||||
print(colored("Make sure Caddy is running with your WAF configuration.", "yellow"))
|
||||
sys.exit(1)
|
||||
|
||||
print(colored("\nDebugging complete.", "cyan"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
93
debug_waf.go
Normal file
93
debug_waf.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package caddywaf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// DebugRequest logs detailed information about a request for debugging
|
||||
func (m *Middleware) DebugRequest(r *http.Request, state *WAFState, msg string) {
|
||||
if m.LogSeverity != "debug" {
|
||||
return
|
||||
}
|
||||
|
||||
var ruleIDs []string
|
||||
var scores []string
|
||||
|
||||
// Log all matched rules and their scores
|
||||
m.ruleHits.Range(func(key, value interface{}) bool {
|
||||
ruleID, ok := key.(RuleID)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
atomicCounter, ok := value.(*atomic.Int64)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
hitCount := atomicCounter.Load()
|
||||
ruleIDs = append(ruleIDs, string(ruleID))
|
||||
scores = append(scores, fmt.Sprintf("%s:%d", string(ruleID), hitCount))
|
||||
return true
|
||||
})
|
||||
|
||||
// Create a detailed debug log
|
||||
m.logger.Debug(fmt.Sprintf("WAF DEBUG: %s", msg),
|
||||
zap.String("timestamp", time.Now().Format(time.RFC3339)),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("query", r.URL.RawQuery),
|
||||
zap.Int("total_score", state.TotalScore),
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold),
|
||||
zap.Bool("blocked", state.Blocked),
|
||||
zap.Int("status_code", state.StatusCode),
|
||||
zap.Bool("response_written", state.ResponseWritten),
|
||||
zap.String("matched_rules", strings.Join(ruleIDs, ",")),
|
||||
zap.String("rule_scores", strings.Join(scores, ",")),
|
||||
)
|
||||
}
|
||||
|
||||
// DumpRulesToFile dumps the loaded rules to a file for inspection
|
||||
func (m *Middleware) DumpRulesToFile(path string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString("=== WAF Rules Dump ===\n\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for phase := 1; phase <= 4; phase++ {
|
||||
fmt.Fprintf(f, "== Phase %d Rules ==\n", phase)
|
||||
rules, ok := m.Rules[phase]
|
||||
if !ok || len(rules) == 0 {
|
||||
if _, err := f.WriteString(" No rules for this phase\n\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for i, rule := range rules {
|
||||
fmt.Fprintf(f, " Rule %d:\n", i+1)
|
||||
fmt.Fprintf(f, " ID: %s\n", rule.ID)
|
||||
fmt.Fprintf(f, " Pattern: %s\n", rule.Pattern)
|
||||
fmt.Fprintf(f, " Targets: %v\n", rule.Targets)
|
||||
fmt.Fprintf(f, " Score: %d\n", rule.Score)
|
||||
fmt.Fprintf(f, " Action: %s\n", rule.Action)
|
||||
fmt.Fprintf(f, " Description: %s\n", rule.Description)
|
||||
if _, err := f.WriteString("\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
208
debug_waf.py
Normal file
208
debug_waf.py
Normal file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from termcolor import colored
|
||||
|
||||
def setup_args():
|
||||
parser = argparse.ArgumentParser(description='Debug WAF configuration via Caddy Admin API')
|
||||
parser.add_argument('--admin-api', default='http://localhost:2019', help='Caddy Admin API URL (default: http://localhost:2019)')
|
||||
parser.add_argument('--config-path', default='/config/', help='Config path in the API (default: /config/)')
|
||||
parser.add_argument('--output', default='waf_config.json', help='Output file for configuration (default: waf_config.json)')
|
||||
parser.add_argument('--pretty', action='store_true', help='Pretty-print JSON output')
|
||||
parser.add_argument('--test-rules', action='store_true', help='Test WAF rules with sample requests')
|
||||
parser.add_argument('--target-url', default='http://localhost:8080', help='Target URL for rule testing (default: http://localhost:8080)')
|
||||
return parser.parse_args()
|
||||
|
||||
def get_caddy_config(admin_url, config_path):
|
||||
"""Get the current Caddy configuration from the Admin API."""
|
||||
try:
|
||||
response = requests.get(f"{admin_url}{config_path}", timeout=5)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(colored(f"Error fetching config: Status {response.status_code}", "red"))
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(colored(f"Error connecting to Caddy Admin API: {str(e)}", "red"))
|
||||
return None
|
||||
|
||||
def extract_waf_config(config):
|
||||
"""Extract WAF-related configuration from the Caddy config."""
|
||||
if not config:
|
||||
return None
|
||||
|
||||
waf_config = {"routes": [], "handlers": [], "thresholds": []}
|
||||
|
||||
# Try to find WAF configuration in apps.http.servers
|
||||
if 'apps' in config and 'http' in config['apps'] and 'servers' in config['apps']['http']:
|
||||
for server_name, server in config['apps']['http']['servers'].items():
|
||||
print(colored(f"Examining server: {server_name}", "cyan"))
|
||||
|
||||
if 'routes' in server:
|
||||
for route in server['routes']:
|
||||
# Check for WAF in route handlers
|
||||
if 'handle' in route:
|
||||
for handler in route['handle']:
|
||||
if handler.get('handler') == 'waf':
|
||||
print(colored("Found WAF handler in route", "green"))
|
||||
waf_config['routes'].append(route)
|
||||
waf_config['handlers'].append(handler)
|
||||
|
||||
# Check for threshold
|
||||
if 'anomaly_threshold' in handler:
|
||||
print(colored(f"Found anomaly threshold: {handler['anomaly_threshold']}", "green"))
|
||||
waf_config['thresholds'].append(handler['anomaly_threshold'])
|
||||
|
||||
if not waf_config['handlers']:
|
||||
print(colored("No WAF handlers found in the configuration", "yellow"))
|
||||
|
||||
return waf_config
|
||||
|
||||
def save_config(config, file_path, pretty=False):
|
||||
"""Save the configuration to a file."""
|
||||
try:
|
||||
with open(file_path, 'w') as f:
|
||||
if pretty:
|
||||
json.dump(config, f, indent=2)
|
||||
else:
|
||||
json.dump(config, f)
|
||||
print(colored(f"Configuration saved to {file_path}", "green"))
|
||||
except Exception as e:
|
||||
print(colored(f"Error saving configuration: {str(e)}", "red"))
|
||||
|
||||
def test_waf_rules(target_url, waf_config):
|
||||
"""Test WAF rules with sample requests to verify behavior."""
|
||||
print(colored("\nTesting WAF rules with sample requests...", "cyan"))
|
||||
|
||||
# Check if we have any anomaly thresholds
|
||||
thresholds = waf_config.get('thresholds', [])
|
||||
threshold = thresholds[0] if thresholds else 5
|
||||
print(colored(f"Using anomaly threshold: {threshold}", "yellow"))
|
||||
|
||||
# Test cases
|
||||
test_cases = [
|
||||
{"name": "Low Score Test", "payload": {"test": "low_score_test"}, "expected_status": 200},
|
||||
{"name": "Below Threshold Test", "payload": {"param1": "score2", "param2": "score2"}, "expected_status": 200},
|
||||
{"name": "Exceed Threshold Test", "payload": {"param1": "score3", "param2": "score3"}, "expected_status": 403},
|
||||
{"name": "Block Action Test", "payload": {"block": "true"}, "expected_status": 403},
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for test_case in test_cases:
|
||||
print(colored(f"\nRunning test: {test_case['name']}", "cyan"))
|
||||
print(colored(f"Payload: {test_case['payload']}", "yellow"))
|
||||
print(colored(f"Expected status: {test_case['expected_status']}", "yellow"))
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
target_url,
|
||||
params=test_case['payload'],
|
||||
headers={'User-Agent': 'WAF-Debug-Tool/1.0'},
|
||||
timeout=5
|
||||
)
|
||||
|
||||
status = response.status_code
|
||||
matched = status == test_case['expected_status']
|
||||
color = "green" if matched else "red"
|
||||
|
||||
print(colored(f"Actual status: {status} - {'✓ MATCH' if matched else '✗ MISMATCH'}", color))
|
||||
print(colored(f"Response: {response.text[:100]}...", "yellow") if len(response.text) > 100 else colored(f"Response: {response.text}", "yellow"))
|
||||
|
||||
# Store result
|
||||
results.append({
|
||||
"name": test_case['name'],
|
||||
"expected": test_case['expected_status'],
|
||||
"actual": status,
|
||||
"matched": matched
|
||||
})
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(colored(f"Error sending request: {str(e)}", "red"))
|
||||
results.append({
|
||||
"name": test_case['name'],
|
||||
"error": str(e),
|
||||
"matched": False
|
||||
})
|
||||
|
||||
# Summary
|
||||
print(colored("\nTest Results Summary:", "cyan"))
|
||||
passes = sum(1 for r in results if r.get('matched', False))
|
||||
failures = len(results) - passes
|
||||
|
||||
print(colored(f"Total Tests: {len(results)}", "yellow"))
|
||||
print(colored(f"Passes: {passes}", "green"))
|
||||
print(colored(f"Failures: {failures}", "red" if failures > 0 else "green"))
|
||||
|
||||
# Detailed results
|
||||
print(colored("\nDetailed Results:", "cyan"))
|
||||
for result in results:
|
||||
status = "PASS" if result.get('matched', False) else "FAIL"
|
||||
color = "green" if result.get('matched', False) else "red"
|
||||
if 'error' in result:
|
||||
print(colored(f"{result['name']}: {status} - Error: {result['error']}", color))
|
||||
else:
|
||||
print(colored(f"{result['name']}: {status} - Expected: {result['expected']}, Actual: {result['actual']}", color))
|
||||
|
||||
return results
|
||||
|
||||
def main():
|
||||
args = setup_args()
|
||||
admin_url = args.admin_api
|
||||
config_path = args.config_path
|
||||
output_file = args.output
|
||||
pretty = args.pretty
|
||||
test_rules = args.test_rules
|
||||
target_url = args.target_url
|
||||
|
||||
print(colored("WAF Debug Tool", "cyan"))
|
||||
print(colored(f"Caddy Admin API: {admin_url}", "yellow"))
|
||||
|
||||
# Get the current configuration
|
||||
print(colored("\nFetching Caddy configuration...", "cyan"))
|
||||
config = get_caddy_config(admin_url, config_path)
|
||||
|
||||
if config:
|
||||
print(colored("Configuration retrieved successfully", "green"))
|
||||
|
||||
# Extract WAF configuration
|
||||
print(colored("\nExtracting WAF configuration...", "cyan"))
|
||||
waf_config = extract_waf_config(config)
|
||||
|
||||
if waf_config and waf_config['handlers']:
|
||||
# Summary of WAF configuration
|
||||
print(colored("\nWAF Configuration Summary:", "cyan"))
|
||||
print(colored(f"WAF Handlers: {len(waf_config['handlers'])}", "yellow"))
|
||||
|
||||
for i, handler in enumerate(waf_config['handlers']):
|
||||
print(colored(f"\nHandler {i+1}:", "yellow"))
|
||||
if 'anomaly_threshold' in handler:
|
||||
print(colored(f" Anomaly Threshold: {handler['anomaly_threshold']}", "green"))
|
||||
else:
|
||||
print(colored(" No anomaly threshold specified", "red"))
|
||||
|
||||
if 'rules' in handler:
|
||||
print(colored(f" Rules: {len(handler['rules']) if isinstance(handler['rules'], list) else 'From file'}", "green"))
|
||||
else:
|
||||
print(colored(" No rules specified", "red"))
|
||||
|
||||
if 'rules_file' in handler:
|
||||
print(colored(f" Rules File: {handler['rules_file']}", "green"))
|
||||
|
||||
# Test rules if requested
|
||||
if test_rules:
|
||||
test_waf_rules(target_url, waf_config)
|
||||
|
||||
# Save the WAF configuration
|
||||
print(colored(f"\nSaving WAF configuration to {output_file}...", "cyan"))
|
||||
save_config(waf_config, output_file, pretty)
|
||||
else:
|
||||
print(colored("No WAF configuration found", "red"))
|
||||
|
||||
print(colored("\nDebug complete.", "cyan"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
30
doc.go
Normal file
30
doc.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Package caddywaf provides Web Application Firewall (WAF) functionality as a Caddy module.
|
||||
//
|
||||
// Module ID: http.handlers.waf
|
||||
// Module type: HTTP handler middleware
|
||||
//
|
||||
// This module implements comprehensive web security features including:
|
||||
// - Regex-based request filtering
|
||||
// - IP and DNS blacklisting
|
||||
// - Geographic access control
|
||||
// - Rate limiting with configurable windows
|
||||
// - Anomaly detection and scoring
|
||||
// - Multi-phase request inspection
|
||||
// - Real-time metrics and monitoring
|
||||
// - Custom response handling
|
||||
// - Dynamic configuration reloading
|
||||
//
|
||||
// Installation:
|
||||
//
|
||||
// xcaddy build --with github.com/fabriziosalmi/caddy-waf
|
||||
//
|
||||
// Basic usage in Caddyfile:
|
||||
//
|
||||
// waf {
|
||||
// rule_file rules.json
|
||||
// ip_blacklist_file blacklist.txt
|
||||
// metrics_endpoint /waf_metrics
|
||||
// }
|
||||
//
|
||||
// For complete documentation, see: https://github.com/fabriziosalmi/caddy-waf
|
||||
package caddywaf
|
||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
caddy-waf:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./Caddyfile:/app/Caddyfile:ro
|
||||
- ./rules.json:/app/rules.json:ro
|
||||
- ./ip_blacklist.txt:/app/ip_blacklist.txt:ro
|
||||
- ./dns_blacklist.txt:/app/dns_blacklist.txt:ro
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- CADDY_ADMIN=0.0.0.0:2019
|
||||
networks:
|
||||
- caddy-waf-net
|
||||
|
||||
networks:
|
||||
caddy-waf-net:
|
||||
driver: bridge
|
||||
@@ -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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
83
geoip.go
83
geoip.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -13,107 +13,49 @@ blocklist_sources = {
|
||||
"Greensnow": "https://blocklist.greensnow.co/greensnow.txt",
|
||||
}
|
||||
|
||||
# --- Tor Exit Node Source (Testing) ---
|
||||
tor_exit_nodes_url = "https://check.torproject.org/exit-addresses" # Testing
|
||||
# Tor Exit Node Source
|
||||
tor_exit_nodes_url = "https://check.torproject.org/exit-addresses"
|
||||
|
||||
|
||||
def extract_ips(source_name, url):
|
||||
"""Fetches data from the given URL and extracts IP addresses."""
|
||||
"""Fetches data from the given URL and extracts IP addresses in CIDR format."""
|
||||
ips = set()
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
|
||||
response.raise_for_status()
|
||||
content = response.text
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching {source_name} from {url}: {e}")
|
||||
return ips
|
||||
|
||||
if source_name == "Talos Intelligence":
|
||||
print(f"Skipping {source_name} due to webpage format, needs manual parsing.")
|
||||
return ips
|
||||
elif source_name == "TOR Exit Nodes":
|
||||
for line in content.splitlines():
|
||||
if line.startswith("ExitAddress"):
|
||||
parts = line.split(" ")
|
||||
if len(parts) > 1:
|
||||
try:
|
||||
ipaddress.ip_address(parts[1].strip())
|
||||
ips.add(parts[1].strip())
|
||||
except ValueError:
|
||||
continue
|
||||
return ips
|
||||
elif source_name == "Spamhaus DROP" or source_name == "Spamhaus EDROP":
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith(";"):
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or line.startswith(";"):
|
||||
continue
|
||||
|
||||
# MODIFIED: Preserve CIDR notation if it already exists
|
||||
if "/" in line:
|
||||
try:
|
||||
# Validate it's a real network and add it
|
||||
net = ipaddress.ip_network(line, strict=False)
|
||||
ips.add(net.with_prefixlen)
|
||||
except ValueError:
|
||||
continue
|
||||
if "/" in line:
|
||||
try:
|
||||
for ip in ipaddress.ip_network(line, strict=False):
|
||||
ips.add(str(ip))
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
ipaddress.ip_address(line)
|
||||
ips.add(line)
|
||||
except ValueError:
|
||||
continue
|
||||
return ips
|
||||
elif source_name == "MaxMind GeoIP2 Anonymous IP Database":
|
||||
# Requires a license key, skipping for now.
|
||||
print(f"Skipping {source_name} because it requires a license key.")
|
||||
return ips
|
||||
else:
|
||||
# Default parsing for normal text file blocklists
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
else:
|
||||
# MODIFIED: Convert single IPs to CIDR notation
|
||||
try:
|
||||
ip_obj = ipaddress.ip_address(line)
|
||||
if ip_obj.version == 4:
|
||||
ips.add(f"{line}/32")
|
||||
else:
|
||||
ips.add(f"{line}/128")
|
||||
except ValueError:
|
||||
continue
|
||||
# Normalize ranges to single IPs
|
||||
if "/" in line:
|
||||
try:
|
||||
for ip in ipaddress.ip_network(line, strict=False):
|
||||
ips.add(str(ip))
|
||||
except ValueError:
|
||||
continue
|
||||
elif "-" in line:
|
||||
try:
|
||||
start, end = line.split('-')
|
||||
start_ip = ipaddress.ip_address(start.strip())
|
||||
end_ip = ipaddress.ip_address(end.strip())
|
||||
if start_ip.version == end_ip.version:
|
||||
for ip_int in range(int(start_ip), int(end_ip) + 1):
|
||||
ips.add(str(ipaddress.ip_address(ip_int)))
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
ipaddress.ip_address(line)
|
||||
ips.add(line)
|
||||
except ValueError:
|
||||
continue
|
||||
return ips
|
||||
return ips
|
||||
|
||||
|
||||
def is_valid_ip(ip_str):
|
||||
"""Helper function to check if an IP address is valid."""
|
||||
try:
|
||||
ipaddress.ip_address(ip_str)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def ip_to_int(ip_str):
|
||||
"""Converts an IP address string to its integer representation."""
|
||||
try:
|
||||
return int(ipaddress.ip_address(ip_str))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def extract_tor_exit_nodes(url):
|
||||
"""Fetches data from the given URL and extracts Tor exit node IPs."""
|
||||
"""Fetches Tor exit node IPs and returns them in CIDR format."""
|
||||
ips = set()
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
@@ -127,9 +69,14 @@ def extract_tor_exit_nodes(url):
|
||||
if line.startswith("ExitAddress"):
|
||||
parts = line.split(" ")
|
||||
if len(parts) > 1:
|
||||
ip_str = parts[1].strip()
|
||||
# MODIFIED: Convert single IPs to CIDR notation
|
||||
try:
|
||||
ipaddress.ip_address(parts[1].strip())
|
||||
ips.add(parts[1].strip())
|
||||
ip_obj = ipaddress.ip_address(ip_str)
|
||||
if ip_obj.version == 4:
|
||||
ips.add(f"{ip_str}/32")
|
||||
else:
|
||||
ips.add(f"{ip_str}/128")
|
||||
except ValueError:
|
||||
continue
|
||||
return ips
|
||||
@@ -140,31 +87,21 @@ def main():
|
||||
for source_name, url in tqdm(blocklist_sources.items(), desc="Processing Blocklists"):
|
||||
print(f"Processing {source_name} from {url}")
|
||||
ips = extract_ips(source_name, url)
|
||||
print(f" Found {len(ips)} IPs in {source_name}")
|
||||
print(f" Found {len(ips)} IPs/CIDRs in {source_name}")
|
||||
combined_ips.update(ips)
|
||||
|
||||
# --- Tor Exit Node Processing (Testing) ---
|
||||
# Tor Exit Node Processing
|
||||
tor_exit_ips = extract_tor_exit_nodes(tor_exit_nodes_url)
|
||||
print(f"Total Tor exit node IPs: {len(tor_exit_ips)}")
|
||||
valid_tor_ips = [ip for ip in tor_exit_ips if is_valid_ip(ip)]
|
||||
print(f"Total Valid Tor IPs after filtering: {len(valid_tor_ips)}")
|
||||
print(f"Total Tor exit node IPs/CIDRs: {len(tor_exit_ips)}")
|
||||
combined_ips.update(tor_exit_ips)
|
||||
|
||||
# Add Tor exit IPs to the combined IPs
|
||||
combined_ips.update(valid_tor_ips)
|
||||
|
||||
print(f"Total IPs before filtering and deduplication: {len(combined_ips)}")
|
||||
|
||||
# Filter out invalid IPs before sorting.
|
||||
valid_ips = [ip for ip in combined_ips if is_valid_ip(ip)]
|
||||
print(f"Total Valid IPs after filtering: {len(valid_ips)}")
|
||||
|
||||
# Remove duplicates by converting to a set before sorting
|
||||
unique_ips = set(valid_ips)
|
||||
print(f"Total Unique IPs/CIDRs after deduplication: {len(combined_ips)}")
|
||||
|
||||
# MODIFIED: The final write loop is simpler. The sorting key is removed as sorting CIDRs as integers is incorrect.
|
||||
# A simple lexicographical sort is sufficient here.
|
||||
with open("ip_blacklist.txt", "w") as f:
|
||||
# Sort using the integer representation and write each IP to the file
|
||||
for ip in sorted(unique_ips, key=ip_to_int):
|
||||
f.write(f"{ip}\n")
|
||||
for ip_cidr in sorted(list(combined_ips)):
|
||||
f.write(f"{ip_cidr}\n")
|
||||
|
||||
print("IP blacklist saved to ip_blacklist.txt")
|
||||
|
||||
|
||||
190
go.mod
190
go.mod
@@ -1,124 +1,164 @@
|
||||
module github.com/fabriziosalmi/caddy-waf
|
||||
|
||||
go 1.22.3
|
||||
|
||||
toolchain go1.23.4
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/caddyserver/caddy/v2 v2.9.1
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/caddyserver/caddy/v2 v2.10.2
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/phemmer/go-iptrie v0.0.0-20240326174613-ba542f5282c9
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/zap v1.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/KimMachineGun/automemlimit v0.7.4 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/certmagic v0.21.6 // indirect
|
||||
github.com/caddyserver/certmagic v0.25.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-kit/kit v0.13.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/glog v1.2.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/cel-go v0.21.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/cel-go v0.26.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
||||
github.com/google/go-tpm v0.9.7 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.14.3 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/libdns/libdns v0.2.2 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v3 v3.0.0 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.4 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.18.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.48.2 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/slackhq/nebula v1.6.1 // indirect
|
||||
github.com/smallstep/certificates v0.26.1 // indirect
|
||||
github.com/smallstep/nosql v0.6.1 // indirect
|
||||
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slackhq/nebula v1.9.7 // indirect
|
||||
github.com/smallstep/certificates v0.29.0 // indirect
|
||||
github.com/smallstep/cli-utils v0.12.2 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect
|
||||
github.com/smallstep/linkedca v0.25.0 // indirect
|
||||
github.com/smallstep/nosql v0.7.0 // indirect
|
||||
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
|
||||
github.com/smallstep/truststore v0.13.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/cobra v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/cobra v1.10.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
|
||||
github.com/urfave/cli v1.22.14 // indirect
|
||||
github.com/urfave/cli v1.22.17 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.9 // indirect
|
||||
go.step.sm/cli-utils v0.9.0 // indirect
|
||||
go.step.sm/crypto v0.45.0 // indirect
|
||||
go.step.sm/linkedca v0.20.1 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
go.step.sm/crypto v0.74.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251009181029-0b7aa0cfb07b // indirect
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
)
|
||||
|
||||
667
go.sum
667
go.sum
@@ -1,22 +1,25 @@
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||
cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
|
||||
cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
|
||||
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
|
||||
cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
|
||||
cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
|
||||
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
|
||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
|
||||
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k=
|
||||
cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
@@ -27,66 +30,80 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
|
||||
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
|
||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.16/go.mod h1:2S9hBElpCyGMifv14WxQ7EfPumgoeCPZUpuPX8VtW34=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.20/go.mod h1:9mCi28a+fmBHSQ0UM79omkz6JtN+PEsvLrnG36uoUv0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 h1:VO3FIM2TDbm0kqp6sFNR0PbioXJb/HzCDW6NtIZpIWE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12/go.mod h1:6C39gB8kg82tx3r72muZSrNhHia9rjGkX7ORaS2GKNE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 h1:p/9flfXdoAnwJnuW9xHEAFY22R3A6skYkW19JFF9F+8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12/go.mod h1:ZTLHakoVCTtW8AaLGSwJ3LXqHD9uQKnOcv1TrpO6u2k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 h1:2lTWFvRcnWFFLzHWmtddu5MTchc5Oj2OOey++99tPZ0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12/go.mod h1:hI92pK+ho8HVcWMHKHrK3Uml4pfG7wvL86FzO0LVtQQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 h1:MM8imH7NZ0ovIVX7D2RxfMDv7Jt9OiUXkcQ+GqywA7M=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12/go.mod h1:gf4OGwdNkbEsb7elw2Sy76odfhwNktWII3WgvQgQQ6w=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.47.0 h1:A97YCVyGz19rRs3+dWf3GpMPflCswgETA9r6/Q0JNSY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.47.0/go.mod h1:ZJ1ghBt9gQM8JoNscUua1siIgao8w74o3kvdWUU6N/Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0/go.mod h1:/e8m+AO6HNPPqMyfKRtzZ9+mBF5/x1Wk8QiDva4m07I=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0/go.mod h1:4EjU+4mIx6+JqKQkruye+CaigV7alL3thVPfDd9VlMs=
|
||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/caddyserver/caddy/v2 v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
|
||||
github.com/caddyserver/caddy/v2 v2.9.1/go.mod h1:ImUELya2el1FDVp3ahnSO2iH1or1aHxlQEQxd/spP68=
|
||||
github.com/caddyserver/certmagic v0.21.6 h1:1th6GfprVfsAtFNOu4StNMF5IxK5XiaI0yZhAHlZFPE=
|
||||
github.com/caddyserver/certmagic v0.21.6/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw=
|
||||
github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=
|
||||
github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0=
|
||||
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
|
||||
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
|
||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@@ -99,19 +116,18 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -121,11 +137,15 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
|
||||
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
|
||||
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
@@ -137,41 +157,28 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
|
||||
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
|
||||
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@@ -180,151 +187,102 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
|
||||
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
|
||||
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
|
||||
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
|
||||
github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
|
||||
github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
|
||||
github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.4.6 h1:hwIwPG7w4z5eQEBq11gYw8YYr9xXLfBQ/0JsKyq5AJM=
|
||||
github.com/google/go-tpm-tools v0.4.6/go.mod h1:MsVQbJnRhKDfWwf5zgr3cDGpj13P1uLAFF0wMEP/n5w=
|
||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU=
|
||||
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
|
||||
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
|
||||
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
|
||||
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mholt/acmez/v3 v3.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E=
|
||||
github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
|
||||
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -335,18 +293,22 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
|
||||
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/phemmer/go-iptrie v0.0.0-20240326174613-ba542f5282c9 h1:C8IqpV7kfAyZDRCnAVNi//l1mWlpyPmq1N6DjVvYEnY=
|
||||
github.com/phemmer/go-iptrie v0.0.0-20240326174613-ba542f5282c9/go.mod h1:dDLiSjNqdp8VjphLdGTx19OeAUsHOzhtc1FFJqpzWMU=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -355,38 +317,31 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
|
||||
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/procfs v0.18.0 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao=
|
||||
github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
||||
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
|
||||
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
@@ -413,25 +368,27 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM=
|
||||
github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI=
|
||||
github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE=
|
||||
github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||
github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o=
|
||||
github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
||||
github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y=
|
||||
github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y=
|
||||
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg=
|
||||
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y=
|
||||
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw=
|
||||
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU=
|
||||
github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs=
|
||||
github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo=
|
||||
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
||||
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
||||
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
|
||||
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
||||
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
|
||||
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
|
||||
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
||||
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
|
||||
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
|
||||
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
@@ -441,128 +398,136 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
|
||||
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
|
||||
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
|
||||
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
|
||||
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
|
||||
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
|
||||
go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY=
|
||||
go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
|
||||
go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 h1:1+EHlhAe/tukctfePZRrDruB9vn7MdwyC+rf36nUSPM=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0/go.mod h1:skzESZBY3IYcqJgImc+fwXQWflvVe+jZxoA/uw60NaI=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.37.0 h1:tVjnBF6EiTDMXoq2Xuc2vK0I7MTbEs05II/0j9mMK+E=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.37.0/go.mod h1:MQjyNXtxAC8PGN9gzPtO4GY5zuP+RI3XX53uWbCTvEQ=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.step.sm/crypto v0.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA=
|
||||
go.step.sm/crypto v0.74.0/go.mod h1:UoXqCAJjjRgzPte0Llaqen7O9P7XjPmgjgTHQGkKCDk=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 h1:4cEcP5+OjGppY79LCQ5Go2B1Boix2x0v6pvA01P3FoA=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251009181029-0b7aa0cfb07b h1:YjNArlzCQB2fDkuKSxMwY1ZUQeRXFIFa23Ov9Wa7TUE=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251009181029-0b7aa0cfb07b/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -570,23 +535,23 @@ golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -595,89 +560,83 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
|
||||
google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -687,27 +646,26 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
|
||||
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
@@ -722,8 +680,7 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
|
||||
306
handler.go
306
handler.go
@@ -5,20 +5,44 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
type ContextKeyLogId string
|
||||
type ContextKeyRule string
|
||||
type (
|
||||
ContextKeyLogId string
|
||||
ContextKeyRule string
|
||||
)
|
||||
|
||||
// ServeHTTP implements caddyhttp.Handler.
|
||||
// handler.go
|
||||
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
logID := uuid.New().String()
|
||||
|
||||
// Add panic recovery to catch and log panics
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
m.logger.Error("PANIC in ServeHTTP",
|
||||
zap.String("log_id", logID),
|
||||
zap.Any("panic", rec),
|
||||
zap.Stack("stack"),
|
||||
)
|
||||
// Return 500 error to client
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
if _, err := w.Write([]byte("Internal Server Error")); err != nil {
|
||||
m.logger.Error(err.Error(),
|
||||
zap.String("log_id", logID),
|
||||
zap.Any("panic", rec),
|
||||
zap.Stack("stack"),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
m.logRequestStart(r, logID)
|
||||
|
||||
// Propagate log ID within the request context for logging
|
||||
@@ -78,11 +102,27 @@ func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next cadd
|
||||
// isPhaseBlocked encapsulates the phase handling and blocking check logic.
|
||||
func (m *Middleware) isPhaseBlocked(w http.ResponseWriter, r *http.Request, phase int, state *WAFState) bool {
|
||||
m.handlePhase(w, r, phase, state)
|
||||
|
||||
if state.Blocked {
|
||||
m.incrementBlockedRequestsMetric()
|
||||
w.WriteHeader(state.StatusCode)
|
||||
|
||||
// IMPORTANT: Log the block event with details
|
||||
m.logger.Warn("Request blocked in phase evaluation",
|
||||
zap.Int("phase", phase),
|
||||
zap.Int("status_code", state.StatusCode),
|
||||
zap.Int("total_score", state.TotalScore),
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold),
|
||||
)
|
||||
|
||||
// Only write the status if not already written
|
||||
if !state.ResponseWritten {
|
||||
w.WriteHeader(state.StatusCode)
|
||||
state.ResponseWritten = true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -133,12 +173,18 @@ func (m *Middleware) handleResponseBodyPhase(recorder *responseRecorder, r *http
|
||||
}
|
||||
m.logger.Debug("Response body captured for Phase 4 analysis", zap.String("log_id", logID))
|
||||
|
||||
for _, rule := range m.Rules[4] {
|
||||
// Check if rules exist for Phase 4 before iterating
|
||||
rules, ok := m.Rules[4]
|
||||
if !ok || len(rules) == 0 {
|
||||
m.logger.Debug("No rules found for Phase 4")
|
||||
return
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.regex.MatchString(body) {
|
||||
if m.processRuleMatch(recorder, r, &rule, body, state) {
|
||||
if m.processRuleMatch(recorder, r, &rule, "RESPONSE_BODY", body, state) { // Pass RESPONSE_BODY as target
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,47 +258,9 @@ func (m *Middleware) handlePhase(w http.ResponseWriter, r *http.Request, phase i
|
||||
zap.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
|
||||
if phase == 1 && m.CountryBlock.Enabled {
|
||||
m.logger.Debug("Starting country blocking phase")
|
||||
blocked, err := m.isCountryInList(r.RemoteAddr, m.CountryBlock.CountryList, m.CountryBlock.geoIP)
|
||||
if err != nil {
|
||||
m.logRequest(zapcore.ErrorLevel, "Failed to check country block",
|
||||
r,
|
||||
zap.Error(err),
|
||||
)
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "internal_error", "country_block_rule", r.RemoteAddr,
|
||||
zap.String("message", "Request blocked due to internal error"),
|
||||
)
|
||||
m.logger.Debug("Country blocking phase completed - blocked due to error")
|
||||
m.incrementGeoIPRequestsMetric(false) // Increment with false for error
|
||||
return
|
||||
} else if blocked {
|
||||
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "country_block", "country_block_rule", r.RemoteAddr,
|
||||
zap.String("message", "Request blocked by country"))
|
||||
m.incrementGeoIPRequestsMetric(true) // Increment with true for blocked
|
||||
return
|
||||
}
|
||||
m.logger.Debug("Country blocking phase completed - not blocked")
|
||||
m.incrementGeoIPRequestsMetric(false) // Increment with false for no block
|
||||
}
|
||||
|
||||
if phase == 1 && m.rateLimiter != nil {
|
||||
m.logger.Debug("Starting rate limiting phase")
|
||||
ip := extractIP(r.RemoteAddr, m.logger) // Pass the logger here
|
||||
path := r.URL.Path // Get the request path
|
||||
if m.rateLimiter.isRateLimited(ip, path) {
|
||||
m.incrementRateLimiterBlockedRequestsMetric() // Increment the counter in the Middleware
|
||||
m.blockRequest(w, r, state, http.StatusTooManyRequests, "rate_limit", "rate_limit_rule", r.RemoteAddr,
|
||||
zap.String("message", "Request blocked by rate limit"),
|
||||
)
|
||||
return
|
||||
}
|
||||
m.logger.Debug("Rate limiting phase completed - not blocked")
|
||||
}
|
||||
|
||||
if phase == 1 {
|
||||
m.logger.Debug("Checking for IP blacklisting", zap.String("remote_addr", r.RemoteAddr)) //Added log for checking before to isIPBlacklisted call
|
||||
// IP blacklisting - the highest priority
|
||||
m.logger.Debug("Checking for IP blacklisting", zap.String("remote_addr", r.RemoteAddr)) // Added log for checking before to isIPBlacklisted call
|
||||
xForwardedFor := r.Header.Get("X-Forwarded-For")
|
||||
if xForwardedFor != "" {
|
||||
ips := strings.Split(xForwardedFor, ",")
|
||||
@@ -261,54 +269,179 @@ func (m *Middleware) handlePhase(w http.ResponseWriter, r *http.Request, phase i
|
||||
m.logger.Debug("Checking IP blacklist with X-Forwarded-For", zap.String("remote_addr_xff", firstIP), zap.String("r.RemoteAddr", r.RemoteAddr))
|
||||
if m.isIPBlacklisted(firstIP) {
|
||||
m.logger.Debug("Starting IP blacklist phase")
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "ip_blacklist", "ip_blacklist_rule", firstIP,
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "ip_blacklist", "ip_blacklist_rule",
|
||||
zap.String("message", "Request blocked by IP blacklist"),
|
||||
)
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(w, state.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
m.logger.Debug("X-Forwarded-For header present but empty or invalid")
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
m.logger.Debug("X-Forwarded-For header not present using r.RemoteAddr")
|
||||
if m.isIPBlacklisted(r.RemoteAddr) {
|
||||
m.logger.Debug("Starting IP blacklist phase")
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "ip_blacklist", "ip_blacklist_rule", r.RemoteAddr,
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "ip_blacklist", "ip_blacklist_rule",
|
||||
zap.String("message", "Request blocked by IP blacklist"),
|
||||
)
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(w, state.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if phase == 1 && m.isDNSBlacklisted(r.Host) {
|
||||
m.logger.Debug("Starting DNS blacklist phase")
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "dns_blacklist", "dns_blacklist_rule", r.Host,
|
||||
zap.String("message", "Request blocked by DNS blacklist"),
|
||||
zap.String("host", r.Host),
|
||||
)
|
||||
return
|
||||
// DNS blacklisting
|
||||
if m.isDNSBlacklisted(r.Host) {
|
||||
m.logger.Debug("Starting DNS blacklist phase")
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "dns_blacklist", "dns_blacklist_rule",
|
||||
zap.String("message", "Request blocked by DNS blacklist"),
|
||||
zap.String("host", r.Host),
|
||||
)
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(w, state.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if m.rateLimiter != nil {
|
||||
m.logger.Debug("Starting rate limiting phase")
|
||||
ip := extractIP(r.RemoteAddr) // Pass the logger here
|
||||
path := r.URL.Path // Get the request path
|
||||
if m.rateLimiter.isRateLimited(ip, path) {
|
||||
m.incrementRateLimiterBlockedRequestsMetric() // Increment the counter in the Middleware
|
||||
m.blockRequest(w, r, state, http.StatusTooManyRequests, "rate_limit", "rate_limit_rule",
|
||||
zap.String("message", "Request blocked by rate limit"),
|
||||
)
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(w, state.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
||||
m.logger.Debug("Rate limiting phase completed - not blocked")
|
||||
}
|
||||
|
||||
// Whitelisting
|
||||
if m.CountryWhitelist.Enabled {
|
||||
m.logger.Debug("Starting country whitelisting phase")
|
||||
allowed, err := m.isCountryInList(r.RemoteAddr, m.CountryWhitelist.CountryList, m.CountryWhitelist.geoIP)
|
||||
if err != nil {
|
||||
m.logRequest(zapcore.ErrorLevel, "Failed to check country whitelist",
|
||||
r,
|
||||
zap.Error(err),
|
||||
)
|
||||
if m.GeoIPFailOpen {
|
||||
m.logger.Warn("GeoIP lookup failed (Whitelist); Failing OPEN")
|
||||
} else {
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "internal_error", "country_block_rule",
|
||||
zap.String("message", "Request blocked due to internal error"),
|
||||
)
|
||||
m.logger.Debug("Country whitelisting phase completed - blocked due to error")
|
||||
m.incrementGeoIPRequestsMetric(false) // Increment with false for error
|
||||
return
|
||||
}
|
||||
} else if !allowed {
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "country_block", "country_block_rule",
|
||||
zap.String("message", "Request blocked by country"))
|
||||
m.incrementGeoIPRequestsMetric(true) // Increment with true for blocked
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(w, state.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
||||
m.logger.Debug("Country whitelisting phase completed - not blocked")
|
||||
m.incrementGeoIPRequestsMetric(false) // Increment with false for no block
|
||||
}
|
||||
|
||||
// ASN Blocking
|
||||
if m.BlockASNs.Enabled {
|
||||
m.logger.Debug("Starting ASN blocking phase")
|
||||
blocked, err := m.geoIPHandler.IsASNInList(r.RemoteAddr, m.BlockASNs.BlockedASNs, m.BlockASNs.geoIP)
|
||||
if err != nil {
|
||||
m.logRequest(zapcore.ErrorLevel, "Failed to check ASN blocking",
|
||||
r,
|
||||
zap.Error(err),
|
||||
)
|
||||
if m.GeoIPFailOpen {
|
||||
m.logger.Warn("ASN lookup failed; Failing OPEN")
|
||||
} else {
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "internal_error", "asn_block_rule",
|
||||
zap.String("message", "Request blocked due to internal error"),
|
||||
)
|
||||
m.logger.Debug("ASN blocking phase completed - blocked due to error")
|
||||
m.incrementGeoIPRequestsMetric(false) // Increment with false for error
|
||||
return
|
||||
}
|
||||
} else if blocked {
|
||||
asnInfo := m.geoIPHandler.GetASN(r.RemoteAddr, m.BlockASNs.geoIP)
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "asn_block", "asn_block_rule",
|
||||
zap.String("message", "Request blocked by ASN"),
|
||||
zap.String("asn", asnInfo),
|
||||
)
|
||||
m.incrementGeoIPRequestsMetric(true) // Increment with true for blocked
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(w, state.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
||||
m.logger.Debug("ASN blocking phase completed - not blocked")
|
||||
}
|
||||
|
||||
// Blacklisting
|
||||
if m.CountryBlacklist.Enabled {
|
||||
m.logger.Debug("Starting country blacklisting phase")
|
||||
blocked, err := m.isCountryInList(r.RemoteAddr, m.CountryBlacklist.CountryList, m.CountryBlacklist.geoIP)
|
||||
if err != nil {
|
||||
m.logRequest(zapcore.ErrorLevel, "Failed to check country blacklisting",
|
||||
r,
|
||||
zap.Error(err),
|
||||
)
|
||||
if m.GeoIPFailOpen {
|
||||
m.logger.Warn("GeoIP lookup failed (Blacklist); Failing OPEN")
|
||||
} else {
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "internal_error", "country_block_rule",
|
||||
zap.String("message", "Request blocked due to internal error"),
|
||||
)
|
||||
m.logger.Debug("Country blacklisting phase completed - blocked due to error")
|
||||
m.incrementGeoIPRequestsMetric(false) // Increment with false for error
|
||||
return
|
||||
}
|
||||
} else if blocked {
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "country_block", "country_block_rule",
|
||||
zap.String("message", "Request blocked by country"))
|
||||
m.incrementGeoIPRequestsMetric(true) // Increment with true for blocked
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(w, state.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
||||
m.logger.Debug("Country blacklisting phase completed - not blocked")
|
||||
m.incrementGeoIPRequestsMetric(false) // Increment with false for no block
|
||||
}
|
||||
}
|
||||
|
||||
rules, ok := m.Rules[phase]
|
||||
if !ok {
|
||||
m.logger.Debug("No rules found for phase", zap.Int("phase", phase))
|
||||
return
|
||||
// Don't block on empty rules. There may be no rules specified
|
||||
// return
|
||||
}
|
||||
|
||||
m.logger.Debug("Starting rule evaluation for phase", zap.Int("phase", phase), zap.Int("rule_count", len(rules)))
|
||||
|
||||
for _, rule := range rules {
|
||||
m.logger.Debug("Processing rule", zap.String("rule_id", string(rule.ID)), zap.Int("target_count", len(rule.Targets)))
|
||||
m.logger.Debug("Processing rule", zap.String("rule_id", rule.ID), zap.Int("target_count", len(rule.Targets)))
|
||||
|
||||
// Use the custom type as the key
|
||||
ctx := context.WithValue(r.Context(), ContextKeyRule("rule_id"), rule.ID)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
for _, target := range rule.Targets {
|
||||
m.logger.Debug("Extracting value for target", zap.String("target", target), zap.String("rule_id", string(rule.ID)))
|
||||
m.logger.Debug("Extracting value for target", zap.String("target", target), zap.String("rule_id", rule.ID))
|
||||
var value string
|
||||
var err error
|
||||
|
||||
@@ -326,52 +459,63 @@ func (m *Middleware) handlePhase(w http.ResponseWriter, r *http.Request, phase i
|
||||
if err != nil {
|
||||
m.logger.Debug("Failed to extract value for target, skipping rule for this target",
|
||||
zap.String("target", target),
|
||||
zap.String("rule_id", string(rule.ID)),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
redactedValue := m.requestValueExtractor.RedactValueIfSensitive(target, value)
|
||||
|
||||
m.logger.Debug("Extracted value",
|
||||
zap.String("rule_id", string(rule.ID)),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.String("target", target),
|
||||
zap.String("value", value),
|
||||
zap.String("value", redactedValue),
|
||||
)
|
||||
|
||||
if rule.regex.MatchString(value) {
|
||||
m.logger.Debug("Rule matched",
|
||||
zap.String("rule_id", string(rule.ID)),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.String("target", target),
|
||||
zap.String("value", value),
|
||||
zap.String("value", redactedValue),
|
||||
)
|
||||
|
||||
// FIXED: Correctly interpret processRuleMatch return value
|
||||
var shouldContinue bool
|
||||
if phase == 3 || phase == 4 {
|
||||
if recorder, ok := w.(*responseRecorder); ok {
|
||||
if m.processRuleMatch(recorder, r, &rule, value, state) {
|
||||
return // Stop processing if the rule match indicates blocking
|
||||
}
|
||||
shouldContinue = m.processRuleMatch(recorder, r, &rule, target, value, state)
|
||||
} else {
|
||||
if m.processRuleMatch(w, r, &rule, value, state) {
|
||||
return // Stop processing if the rule match indicates blocking
|
||||
}
|
||||
shouldContinue = m.processRuleMatch(w, r, &rule, target, value, state)
|
||||
}
|
||||
} else {
|
||||
if m.processRuleMatch(w, r, &rule, value, state) {
|
||||
return // Stop processing if the rule match indicates blocking
|
||||
}
|
||||
shouldContinue = m.processRuleMatch(w, r, &rule, target, value, state)
|
||||
}
|
||||
if state.Blocked || state.ResponseWritten {
|
||||
m.logger.Debug("Rule evaluation completed early due to blocking or response written", zap.Int("phase", phase), zap.String("rule_id", string(rule.ID)))
|
||||
|
||||
// If processRuleMatch returned false or state is now blocked, stop processing
|
||||
if !shouldContinue || state.Blocked || state.ResponseWritten {
|
||||
m.logger.Debug("Rule evaluation stopping due to blocking or rule directive",
|
||||
zap.Int("phase", phase),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.Bool("continue", shouldContinue),
|
||||
zap.Bool("blocked", state.Blocked),
|
||||
)
|
||||
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(w, state.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
m.logger.Debug("Rule did not match",
|
||||
zap.String("rule_id", string(rule.ID)),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.String("target", target),
|
||||
zap.String("value", value),
|
||||
zap.String("value", redactedValue),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Debug("Rule evaluation completed for phase", zap.Int("phase", phase))
|
||||
|
||||
if phase == 3 {
|
||||
@@ -393,6 +537,8 @@ func (m *Middleware) handlePhase(w http.ResponseWriter, r *http.Request, phase i
|
||||
zap.Int("total_score", state.TotalScore),
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold),
|
||||
)
|
||||
|
||||
m.allowRequest(state)
|
||||
}
|
||||
|
||||
// incrementRateLimiterBlockedRequestsMetric increments the blocked requests metric for the rate limiter.
|
||||
|
||||
493
handler_test.go
493
handler_test.go
@@ -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)
|
||||
|
||||
30
helpers.go
30
helpers.go
@@ -1,7 +1,9 @@
|
||||
package caddywaf
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fileExists checks if a file exists and is readable.
|
||||
@@ -15,3 +17,31 @@ func fileExists(path string) bool {
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
// isIPv4 - checks if input IP is of type v4
|
||||
//
|
||||
//nolint:unused
|
||||
func isIPv4(addr string) bool {
|
||||
return strings.Count(addr, ":") < 2
|
||||
}
|
||||
|
||||
// appendCIDR - appends CIDR for a single IP
|
||||
func appendCIDR(ip string) string {
|
||||
// IPv4
|
||||
if strings.Count(ip, ":") < 2 {
|
||||
ip += "/32"
|
||||
// IPv6
|
||||
} else {
|
||||
ip += "/64"
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
// extractIP extracts the IP address from a remote address string.
|
||||
func extractIP(remoteAddr string) string {
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
return remoteAddr // Assume the input is already an IP address
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
217
install_with_modules.sh
Normal file
217
install_with_modules.sh
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# --- Configuration ---
|
||||
GREEN="\033[1;32m"
|
||||
RED="\033[1;31m"
|
||||
YELLOW="\033[1;33m"
|
||||
NC="\033[0m" # No Color
|
||||
GO_VERSION_REQUIRED="1.22.3"
|
||||
GO_VERSION_TARGET="1.23.4"
|
||||
XCADDY_VERSION="latest"
|
||||
GEOLITE2_DB_URL="https://git.io/GeoLite2-Country.mmdb"
|
||||
GEOLITE2_DB_FILE="GeoLite2-Country.mmdb"
|
||||
|
||||
# Default modules - can be overridden with environment variables
|
||||
WAF_MODULE=${WAF_MODULE:-"github.com/fabriziosalmi/caddy-waf@latest"}
|
||||
# Add additional modules here, comma-separated in EXTRA_MODULES env var
|
||||
# Example: EXTRA_MODULES="github.com/greenpau/caddy-security@latest,github.com/example/module@latest"
|
||||
EXTRA_MODULES=${EXTRA_MODULES:-""}
|
||||
|
||||
# --- Helper Functions ---
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ Success: $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ Warning: $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "ℹ️ Info: $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ Error: $1${NC}"
|
||||
echo -e "${RED} $1${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_command_exists() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
return 1 # Command not found
|
||||
else
|
||||
return 0 # Command found
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_go_installed() {
|
||||
if ! check_command_exists go; then
|
||||
print_info "Go not found. Installing Go $GO_VERSION_TARGET..."
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
install_go_linux
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
install_go_macos
|
||||
else
|
||||
print_error "Unsupported OS type: $OSTYPE"
|
||||
fi
|
||||
else
|
||||
check_go_version
|
||||
fi
|
||||
}
|
||||
|
||||
check_go_version() {
|
||||
local version
|
||||
version=$(go version 2>&1 | awk '{print $3}' | sed 's/go//')
|
||||
if [[ "$version" == *"error"* ]]; then
|
||||
print_warning "Error checking Go version. Attempting to proceed anyway."
|
||||
return
|
||||
fi
|
||||
|
||||
# Compare versions (simple string comparison, assumes semantic versioning)
|
||||
if [[ "$version" < "$GO_VERSION_REQUIRED" ]]; then
|
||||
print_warning "Go version $version is older than required version $GO_VERSION_REQUIRED."
|
||||
print_info "Installing Go $GO_VERSION_TARGET..."
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
install_go_linux
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
install_go_macos
|
||||
else
|
||||
print_error "Unsupported OS type: $OSTYPE"
|
||||
fi
|
||||
else
|
||||
print_info "Go version $version is installed (minimum required: $GO_VERSION_REQUIRED)."
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_xcaddy_installed() {
|
||||
if ! check_command_exists xcaddy; then
|
||||
print_info "xcaddy not found. Installing xcaddy..."
|
||||
install_xcaddy
|
||||
else
|
||||
print_info "xcaddy is already installed."
|
||||
fi
|
||||
}
|
||||
|
||||
install_xcaddy() {
|
||||
print_info "Installing xcaddy $XCADDY_VERSION..."
|
||||
GOBIN="$(go env GOBIN)"
|
||||
if [ -z "$GOBIN" ]; then
|
||||
GOBIN="$HOME/go/bin" # Default GOBIN if not set
|
||||
fi
|
||||
go install "github.com/caddyserver/xcaddy/cmd/xcaddy@$XCADDY_VERSION" || print_error "Failed to install xcaddy."
|
||||
export PATH="$PATH:$GOBIN" # Ensure PATH is updated in current shell
|
||||
print_success "xcaddy $XCADDY_VERSION installed successfully."
|
||||
}
|
||||
|
||||
download_geolite2_db() {
|
||||
if [ ! -f "$GEOLITE2_DB_FILE" ]; then
|
||||
print_info "Downloading GeoLite2 Country database..."
|
||||
if check_command_exists wget; then
|
||||
wget -q "$GEOLITE2_DB_URL" -O "$GEOLITE2_DB_FILE" || print_error "Failed to download GeoLite2 database."
|
||||
elif check_command_exists curl; then
|
||||
curl -s "$GEOLITE2_DB_URL" -o "$GEOLITE2_DB_FILE" || print_error "Failed to download GeoLite2 database."
|
||||
else
|
||||
print_error "Neither wget nor curl is installed. Cannot download GeoLite2 database."
|
||||
fi
|
||||
print_success "GeoLite2 database downloaded."
|
||||
else
|
||||
print_info "GeoLite2 database already exists."
|
||||
fi
|
||||
}
|
||||
|
||||
build_caddy_with_modules() {
|
||||
print_info "Building Caddy with modules..."
|
||||
|
||||
# Start building the xcaddy command
|
||||
CMD="xcaddy build --with $WAF_MODULE"
|
||||
|
||||
# Add any extra modules
|
||||
if [ -n "$EXTRA_MODULES" ]; then
|
||||
IFS=',' read -ra MODULES <<< "$EXTRA_MODULES"
|
||||
for MODULE in "${MODULES[@]}"; do
|
||||
CMD="$CMD --with $MODULE"
|
||||
done
|
||||
fi
|
||||
|
||||
print_info "Running command: $CMD"
|
||||
eval $CMD || print_error "Failed to build Caddy with modules."
|
||||
|
||||
print_success "Caddy built successfully with the following modules:"
|
||||
print_info "- $WAF_MODULE"
|
||||
if [ -n "$EXTRA_MODULES" ]; then
|
||||
IFS=',' read -ra MODULES <<< "$EXTRA_MODULES"
|
||||
for MODULE in "${MODULES[@]}"; do
|
||||
print_info "- $MODULE"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
format_caddyfile() {
|
||||
if [ -f "Caddyfile" ]; then
|
||||
print_info "Formatting Caddyfile..."
|
||||
./caddy fmt --overwrite Caddyfile || print_warning "Failed to format Caddyfile."
|
||||
print_success "Caddyfile formatted."
|
||||
else
|
||||
print_info "No Caddyfile found to format."
|
||||
fi
|
||||
}
|
||||
|
||||
check_modules() {
|
||||
print_info "Checking loaded modules..."
|
||||
./caddy list-modules | grep -E "(waf|security)" || print_warning "Modules may not be properly loaded."
|
||||
}
|
||||
|
||||
# --- OS Specific Functions ---
|
||||
|
||||
install_go_linux() {
|
||||
print_info "Installing Go $GO_VERSION_TARGET for Linux..."
|
||||
# Download and install Go
|
||||
wget -q https://golang.org/dl/go${GO_VERSION_TARGET}.linux-amd64.tar.gz -O go.tar.gz || print_error "Failed to download Go."
|
||||
sudo rm -rf /usr/local/go
|
||||
sudo tar -C /usr/local -xzf go.tar.gz
|
||||
rm go.tar.gz
|
||||
export PATH="$PATH:/usr/local/go/bin"
|
||||
print_success "Go $GO_VERSION_TARGET installed successfully on Linux."
|
||||
}
|
||||
|
||||
install_go_macos() {
|
||||
print_info "Installing Go $GO_VERSION_TARGET for macOS..."
|
||||
# Download and install Go
|
||||
curl -sL https://golang.org/dl/go${GO_VERSION_TARGET}.darwin-amd64.tar.gz -o go.tar.gz || print_error "Failed to download Go."
|
||||
sudo rm -rf /usr/local/go
|
||||
sudo tar -C /usr/local -xzf go.tar.gz
|
||||
rm go.tar.gz
|
||||
export PATH="$PATH:/usr/local/go/bin"
|
||||
print_success "Go $GO_VERSION_TARGET installed successfully on macOS."
|
||||
}
|
||||
|
||||
# --- Main Script ---
|
||||
|
||||
print_info "Starting setup for Caddy with multiple modules..."
|
||||
|
||||
# Display selected modules
|
||||
print_info "Will install the following modules:"
|
||||
print_info "- WAF Module: $WAF_MODULE"
|
||||
if [ -n "$EXTRA_MODULES" ]; then
|
||||
print_info "- Extra Modules: $EXTRA_MODULES"
|
||||
fi
|
||||
|
||||
# Prompt user to confirm
|
||||
read -p "Continue with these modules? [Y/n] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]] && [[ ! -z $REPLY ]]; then
|
||||
print_info "Installation cancelled by user."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ensure_go_installed
|
||||
ensure_xcaddy_installed
|
||||
download_geolite2_db
|
||||
build_caddy_with_modules
|
||||
format_caddyfile
|
||||
check_modules
|
||||
|
||||
print_success "Setup completed! You now have Caddy built with WAF and your selected modules."
|
||||
print_info "To run Caddy: ./caddy run"
|
||||
print_info "For a list of all installed modules: ./caddy list-modules"
|
||||
38
it_test.go
Normal file
38
it_test.go
Normal 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!")
|
||||
}
|
||||
@@ -27,7 +27,7 @@ var sensitiveKeys = []string{
|
||||
"creditcard", // Credit card number
|
||||
"cvv", // Card verification value
|
||||
"cvc", // Card verification code
|
||||
"eamil", // Email address
|
||||
"email", // Email address
|
||||
"phone", // Phone number
|
||||
"address", // Physical address
|
||||
"account", // Bank account number
|
||||
@@ -105,7 +105,6 @@ func (m *Middleware) logRequest(level zapcore.Level, msg string, r *http.Request
|
||||
)
|
||||
m.logger.Log(level, msg, allFields...)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// redactSensitiveFields redacts sensitive information in the log fields.
|
||||
@@ -130,7 +129,7 @@ func (m *Middleware) redactSensitiveFields(fields []zap.Field) []zap.Field {
|
||||
// prepareLogFields consolidates the logic for preparing log fields, including common fields and log_id.
|
||||
func (m *Middleware) prepareLogFields(r *http.Request, fields []zap.Field) []zap.Field {
|
||||
var logID string
|
||||
var allFields []zap.Field
|
||||
allFields := make([]zap.Field, 0)
|
||||
|
||||
// Initialize with common fields
|
||||
var sourceIP, userAgent, requestMethod, requestPath, queryParams string
|
||||
@@ -208,6 +207,8 @@ func (m *Middleware) redactQueryParams(queryParams string) string {
|
||||
}
|
||||
|
||||
func (m *Middleware) isSensitiveQueryParamKey(key string) bool {
|
||||
sensitiveKeysMutex.RLock()
|
||||
defer sensitiveKeysMutex.RUnlock()
|
||||
for _, sensitiveKey := range sensitiveKeys { // Use package level sensitiveKeys variable
|
||||
if strings.Contains(key, sensitiveKey) {
|
||||
return true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{}
|
||||
|
||||
72
request.go
72
request.go
@@ -8,6 +8,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
type RequestValueExtractor struct {
|
||||
logger *zap.Logger
|
||||
redactSensitiveData bool // Add this field
|
||||
maxBodySize int64
|
||||
}
|
||||
|
||||
// Extraction Target Constants - Improved Readability and Maintainability
|
||||
@@ -47,8 +49,11 @@ const (
|
||||
var sensitiveTargets = []string{"password", "token", "apikey", "authorization", "secret"} // Define sensitive targets for redaction as package variable
|
||||
|
||||
// NewRequestValueExtractor creates a new RequestValueExtractor with a given logger
|
||||
func NewRequestValueExtractor(logger *zap.Logger, redactSensitiveData bool) *RequestValueExtractor {
|
||||
return &RequestValueExtractor{logger: logger, redactSensitiveData: redactSensitiveData}
|
||||
func NewRequestValueExtractor(logger *zap.Logger, redactSensitiveData bool, maxBodySize int64) *RequestValueExtractor {
|
||||
if maxBodySize <= 0 {
|
||||
maxBodySize = 10 * 1024 * 1024 // Default 10MB
|
||||
}
|
||||
return &RequestValueExtractor{logger: logger, redactSensitiveData: redactSensitiveData, maxBodySize: maxBodySize}
|
||||
}
|
||||
|
||||
// ExtractValue extracts values based on the target, handling comma separated targets
|
||||
@@ -78,7 +83,8 @@ func (rve *RequestValueExtractor) ExtractValue(target string, r *http.Request, w
|
||||
|
||||
// extractSingleValue extracts a value based on a single target
|
||||
func (rve *RequestValueExtractor) extractSingleValue(target string, r *http.Request, w http.ResponseWriter) (string, error) {
|
||||
target = strings.ToUpper(strings.TrimSpace(target))
|
||||
origTarget := target
|
||||
targetUpper := strings.ToUpper(strings.TrimSpace(target))
|
||||
var unredactedValue string
|
||||
var err error
|
||||
|
||||
@@ -121,7 +127,7 @@ func (rve *RequestValueExtractor) extractSingleValue(target string, r *http.Requ
|
||||
},
|
||||
}
|
||||
|
||||
if extractor, exists := extractionLogic[target]; exists {
|
||||
if extractor, exists := extractionLogic[targetUpper]; exists {
|
||||
unredactedValue, err = extractor()
|
||||
if err != nil {
|
||||
return "", err // Return error from extractor
|
||||
@@ -146,13 +152,16 @@ func (rve *RequestValueExtractor) extractSingleValue(target string, r *http.Requ
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else if strings.HasPrefix(target, TargetURLParamPrefix) {
|
||||
unredactedValue, err = rve.extractURLParam(r.URL, strings.TrimPrefix(target, TargetURLParamPrefix), target)
|
||||
} else if strings.HasPrefix(targetUpper, TargetURLParamPrefix) {
|
||||
// CRITICAL FIX: Use the original parameter name (without uppercase conversion)
|
||||
paramName := strings.TrimPrefix(origTarget, TargetURLParamPrefix)
|
||||
unredactedValue, err = rve.extractURLParam(r.URL, paramName, target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else if strings.HasPrefix(target, TargetJSONPathPrefix) {
|
||||
unredactedValue, err = rve.extractValueForJSONPath(r, strings.TrimPrefix(target, TargetJSONPathPrefix), target)
|
||||
} else if strings.HasPrefix(targetUpper, TargetJSONPathPrefix) {
|
||||
jsonPath := strings.TrimPrefix(origTarget, TargetJSONPathPrefix)
|
||||
unredactedValue, err = rve.extractValueForJSONPath(r, jsonPath, target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -162,7 +171,7 @@ func (rve *RequestValueExtractor) extractSingleValue(target string, r *http.Requ
|
||||
}
|
||||
|
||||
// Redact sensitive fields before returning the value (as before)
|
||||
value := rve.redactValueIfSensitive(target, unredactedValue)
|
||||
value := rve.RedactValueIfSensitive(target, unredactedValue)
|
||||
|
||||
// Log the extracted value (redacted if necessary)
|
||||
rve.logger.Debug("Extracted value",
|
||||
@@ -200,13 +209,29 @@ func (rve *RequestValueExtractor) extractBody(r *http.Request, target string) (s
|
||||
rve.logger.Debug("Request body is empty", zap.String("target", target))
|
||||
return "", fmt.Errorf("request body is empty for target: %s", target)
|
||||
}
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
reader := io.LimitReader(r.Body, rve.maxBodySize)
|
||||
bodyBytes, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
rve.logger.Error("Failed to read request body", zap.Error(err))
|
||||
return "", fmt.Errorf("failed to read request body for target %s: %w", target, err)
|
||||
}
|
||||
r.Body = http.NoBody // Reset body for next read - using http.NoBody
|
||||
return string(bodyBytes), nil
|
||||
// Restore body for next read, verifying if we need to combine with remaining body
|
||||
// We use io.MultiReader to concatenate the bytes we read with the *remaining* bytes in the original body.
|
||||
// This ensures that even if we hit the limit, the downstream consumer can read the full body.
|
||||
// We also ensure the original Closer is preserved.
|
||||
r.Body = &struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
Reader: io.MultiReader(strings.NewReader(string(bodyBytes)), r.Body),
|
||||
Closer: r.Body,
|
||||
}
|
||||
|
||||
// SOTA Pattern: Zero-Copy (avoid allocation for string conversion)
|
||||
if len(bodyBytes) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return unsafe.String(&bodyBytes[0], len(bodyBytes)), nil
|
||||
}
|
||||
|
||||
// Helper function to extract all headers
|
||||
@@ -303,9 +328,17 @@ func (rve *RequestValueExtractor) extractDynamicCookie(r *http.Request, cookieNa
|
||||
|
||||
// Helper function to extract URL parameter value
|
||||
func (rve *RequestValueExtractor) extractURLParam(url *url.URL, paramName string, target string) (string, error) {
|
||||
paramValue := url.Query().Get(paramName)
|
||||
// Clean up the paramName by removing any potential remaining prefix
|
||||
// This is critical for handling cases where the origTarget trimming didn't fully work
|
||||
cleanParamName := strings.TrimPrefix(paramName, "url_param:")
|
||||
|
||||
paramValue := url.Query().Get(cleanParamName)
|
||||
if paramValue == "" {
|
||||
rve.logger.Debug("URL parameter not found", zap.String("parameter", paramName), zap.String("target", target))
|
||||
rve.logger.Debug("URL parameter not found",
|
||||
zap.String("parameter", paramName),
|
||||
zap.String("clean_parameter", cleanParamName),
|
||||
zap.String("target", target),
|
||||
zap.String("available_params", url.RawQuery)) // Log available params for debugging
|
||||
return "", fmt.Errorf("url parameter '%s' not found for target: %s", paramName, target)
|
||||
}
|
||||
return paramValue, nil
|
||||
@@ -322,12 +355,13 @@ func (rve *RequestValueExtractor) extractValueForJSONPath(r *http.Request, jsonP
|
||||
return "", fmt.Errorf("request body is empty for target: %s", target)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
reader := io.LimitReader(r.Body, rve.maxBodySize)
|
||||
bodyBytes, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
rve.logger.Error("Failed to read request body", zap.Error(err))
|
||||
return "", fmt.Errorf("failed to read request body for JSON_PATH target %s: %w", target, err)
|
||||
}
|
||||
r.Body = http.NoBody // Reset body for next read
|
||||
r.Body = io.NopCloser(strings.NewReader(string(bodyBytes))) // Restore body for next read
|
||||
|
||||
// Use helper method to dynamically extract value based on JSON path (e.g., 'data.items.0.name').
|
||||
unredactedValue, err := rve.extractJSONPath(string(bodyBytes), jsonPath)
|
||||
@@ -339,7 +373,7 @@ func (rve *RequestValueExtractor) extractValueForJSONPath(r *http.Request, jsonP
|
||||
}
|
||||
|
||||
// Helper function to redact value if target is sensitive
|
||||
func (rve *RequestValueExtractor) redactValueIfSensitive(target string, value string) string {
|
||||
func (rve *RequestValueExtractor) RedactValueIfSensitive(target string, value string) string {
|
||||
if rve.redactSensitiveData {
|
||||
for _, sensitive := range sensitiveTargets {
|
||||
if strings.Contains(strings.ToLower(target), sensitive) {
|
||||
@@ -363,13 +397,12 @@ func (rve *RequestValueExtractor) extractAllCookies(cookies []*http.Cookie, logM
|
||||
return strings.Join(cookieStrings, "; "), nil
|
||||
}
|
||||
|
||||
// Helper function for JSON path extraction.
|
||||
// Helper function for JSON path extraction
|
||||
func (rve *RequestValueExtractor) extractJSONPath(jsonStr string, jsonPath string) (string, error) {
|
||||
// Validate input JSON string
|
||||
if jsonStr == "" {
|
||||
return "", fmt.Errorf("json string is empty")
|
||||
}
|
||||
|
||||
// Validate JSON path
|
||||
if jsonPath == "" {
|
||||
return "", fmt.Errorf("json path is empty")
|
||||
@@ -380,7 +413,6 @@ func (rve *RequestValueExtractor) extractJSONPath(jsonStr string, jsonPath strin
|
||||
if err := json.Unmarshal([]byte(jsonStr), &jsonData); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
// Check if JSON data is valid
|
||||
if jsonData == nil {
|
||||
return "", fmt.Errorf("invalid json data")
|
||||
|
||||
76
request_body_test.go
Normal file
76
request_body_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
77
response.go
77
response.go
@@ -2,65 +2,51 @@ package caddywaf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// blockRequest handles blocking a request and logging the details.
|
||||
func (m *Middleware) blockRequest(recorder http.ResponseWriter, r *http.Request, state *WAFState, statusCode int, reason, ruleID, matchedValue string, fields ...zap.Field) {
|
||||
// allowRequest - handles request allowing
|
||||
func (m *Middleware) allowRequest(state *WAFState) {
|
||||
state.Blocked = false
|
||||
state.StatusCode = http.StatusOK
|
||||
state.ResponseWritten = false
|
||||
|
||||
m.incrementAllowedRequestsMetric()
|
||||
}
|
||||
|
||||
// blockRequest handles blocking a request and logging the details.
|
||||
func (m *Middleware) blockRequest(recorder http.ResponseWriter, r *http.Request, state *WAFState, statusCode int, reason, ruleID string, fields ...zap.Field) {
|
||||
// CRITICAL FIX: Set these flags before any other operations
|
||||
state.Blocked = true
|
||||
state.StatusCode = statusCode
|
||||
state.ResponseWritten = true
|
||||
|
||||
// Custom response handling
|
||||
if resp, ok := m.CustomResponses[statusCode]; ok {
|
||||
m.logger.Debug("Custom response found for status code",
|
||||
zap.Int("status_code", statusCode),
|
||||
zap.String("body", resp.Body),
|
||||
)
|
||||
m.writeCustomResponse(recorder, statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Default blocking behavior
|
||||
logID := uuid.New().String()
|
||||
if logIDCtx, ok := r.Context().Value(ContextKeyLogId("logID")).(string); ok {
|
||||
logID = logIDCtx
|
||||
}
|
||||
|
||||
// Prepare standard fields for logging
|
||||
blockFields := []zap.Field{
|
||||
zap.String("log_id", logID),
|
||||
zap.String("source_ip", r.RemoteAddr),
|
||||
zap.String("user_agent", r.UserAgent()),
|
||||
zap.String("request_method", r.Method),
|
||||
zap.String("request_path", r.URL.Path),
|
||||
zap.String("query_params", r.URL.RawQuery),
|
||||
// CRITICAL FIX: Log at WARN level for visibility
|
||||
m.logger.Warn("REQUEST BLOCKED BY WAF", append(fields,
|
||||
zap.String("rule_id", ruleID),
|
||||
zap.String("reason", reason),
|
||||
zap.Int("status_code", statusCode),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
zap.String("reason", reason), // Include the reason for blocking
|
||||
zap.String("rule_id", ruleID), // Include the rule ID
|
||||
zap.String("matched_value", matchedValue), // Include the matched value
|
||||
}
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.Int("total_score", state.TotalScore))...)
|
||||
|
||||
// Debug: Print the blockFields to verify they are correct
|
||||
m.logger.Debug("Block fields being passed to logRequest",
|
||||
zap.Any("blockFields", blockFields),
|
||||
)
|
||||
// CRITICAL FIX: Increment blocked metrics immediately
|
||||
m.incrementBlockedRequestsMetric()
|
||||
|
||||
// Append additional fields if any
|
||||
blockFields = append(blockFields, fields...)
|
||||
|
||||
// Log the blocked request at WARN level
|
||||
m.logRequest(zapcore.WarnLevel, "Request blocked", r, blockFields...)
|
||||
|
||||
// Write default response with status code using the recorder
|
||||
// Write a simple text response for blocked requests
|
||||
recorder.Header().Set("Content-Type", "text/plain")
|
||||
recorder.WriteHeader(statusCode)
|
||||
|
||||
if m.CustomResponses != nil {
|
||||
m.writeCustomResponse(recorder, state.StatusCode)
|
||||
} else {
|
||||
message := fmt.Sprintf("Request blocked by WAF. Reason: %s", reason)
|
||||
if _, err := recorder.Write([]byte(message)); err != nil {
|
||||
m.logger.Error("Failed to write blocked response", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// responseRecorder captures the response status code, headers, and body.
|
||||
@@ -85,7 +71,6 @@ func NewResponseRecorder(w http.ResponseWriter) *responseRecorder {
|
||||
func (r *responseRecorder) WriteHeader(statusCode int) {
|
||||
r.statusCode = statusCode
|
||||
r.ResponseWriter.WriteHeader(statusCode)
|
||||
|
||||
}
|
||||
|
||||
// Header returns the response headers.
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestBlockRequest(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
state := &WAFState{}
|
||||
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "test reason", "rule1", "match1")
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "test reason", "rule1")
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
assert.Equal(t, "Blocked", w.Body.String())
|
||||
@@ -56,7 +56,7 @@ func TestBlockRequest(t *testing.T) {
|
||||
r = r.WithContext(ctx)
|
||||
state := &WAFState{}
|
||||
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "test reason", "rule1", "match1")
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, "test reason", "rule1")
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
assert.True(t, state.Blocked)
|
||||
@@ -75,7 +75,7 @@ func TestBlockRequest(t *testing.T) {
|
||||
}
|
||||
recorder := NewResponseRecorder(w)
|
||||
|
||||
m.blockRequest(recorder, r, state, http.StatusForbidden, "test reason", "rule1", "match1")
|
||||
m.blockRequest(recorder, r, state, http.StatusForbidden, "test reason", "rule1")
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, recorder.StatusCode()) // Check the Recorder status code instead
|
||||
assert.True(t, state.ResponseWritten) // Check that the ResponseWritten flag is set
|
||||
|
||||
418
rules-browser-friendly.json
Normal file
418
rules-browser-friendly.json
Normal file
@@ -0,0 +1,418 @@
|
||||
[
|
||||
{
|
||||
"id": "allow-legit-browsers",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)(caddy-waf-ui|Mozilla|Chrome|Safari|Edge|Firefox|Opera|AppleWebKit|Gecko|Trident|MSIE|Googlebot|Bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|Sogou|Exabot|facebot|facebookexternalhit|Twitterbot|Slackbot|LinkedInBot|TelegramBot)",
|
||||
"targets": [
|
||||
"HEADERS:User-Agent"
|
||||
],
|
||||
"severity": "LOW",
|
||||
"action": "log",
|
||||
"score": 1,
|
||||
"description": "Allow and log traffic from legitimate browsers, search engine crawlers, and social media bots."
|
||||
},
|
||||
{
|
||||
"id": "auth-login-form-missing",
|
||||
"phase": 2,
|
||||
"pattern": "^$",
|
||||
"targets": [
|
||||
"BODY"
|
||||
],
|
||||
"severity": "LOW",
|
||||
"action": "log",
|
||||
"score": 3,
|
||||
"description": "Log login requests that do not contain login form fields"
|
||||
},
|
||||
{
|
||||
"id": "block-scanners",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)(nikto|sqlmap|nmap|acunetix|nessus|openvas|wpscan|dirbuster|burpsuite|owasp zap|netsparker|appscan|arachni|skipfish|gobuster|wfuzz|hydra|metasploit|nessus|openvas|qualys|zap|w3af|openwebspider|netsparker|appspider|rapid7|nessus|qualys|nuclei|zgrab|vega|gospider|gxspider|whatweb|xspider|joomscan|uniscan|blindelephant)",
|
||||
"targets": [
|
||||
"HEADERS:User-Agent"
|
||||
],
|
||||
"severity": "CRITICAL",
|
||||
"action": "block",
|
||||
"score": 10,
|
||||
"description": "Block traffic from known vulnerability scanners and penetration testing tools. Includes more scanners."
|
||||
},
|
||||
{
|
||||
"id": "crlf-injection-headers",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)(%0d|\\r)%0a|%0a(%0d|$)|\\n|%0d%0a|%0a%0d|\\r\\n",
|
||||
"targets": [
|
||||
"HEADERS"
|
||||
],
|
||||
"severity": "MEDIUM",
|
||||
"action": "log",
|
||||
"score": 5,
|
||||
"description": "Log requests with potential CRLF injection characters in headers. Improved pattern matching."
|
||||
},
|
||||
{
|
||||
"id": "csrf-missing-token-post",
|
||||
"phase": 2,
|
||||
"pattern": "^$",
|
||||
"targets": [
|
||||
"BODY"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "log",
|
||||
"score": 6,
|
||||
"description": "Log POST requests to write operation endpoints that are missing a CSRF token in the body (use this with a condition to ensure that a write operation was done)."
|
||||
},
|
||||
{
|
||||
"id": "header-attacks-consolidated",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)(?:1'\\s+OR\\s+'1'='1|<script[^>]*>|\\.\\.\\/\\.\\.\\/etc\\/passwd|1'\\s+UNION\\s+SELECT\\s+NULL--|\\b(?:select|insert|update|delete|drop|alter)\\b(?:\\s|\\/\\*.*?\\*\\/|--.*?)?(?:from|into|where|table)\\b|\\bunion\\b(?:\\s|\\/\\*.*?\\*\\/|--.*?)?\\bselect\\b|'\\s*(?:and|or)\\s*\\d+\\s*(?:=|[<>!]+\\s*)\\d+|\\)\\s*(?:and|or)\\s*\\(\\d+\\s*(?:=|[<>!]+\\s*)\\d+\\)|\\b(?:sleep|benchmark|waitfor\\s+delay)\\s*\\()",
|
||||
"targets": [
|
||||
"HEADERS"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block SQL injection, XSS, and path traversal attempts in headers. Improved pattern matching."
|
||||
},
|
||||
{
|
||||
"id": "http-request-smuggling",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)(?:Transfer-Encoding.*?(?:chunked|identity)|Content-Length:\\s*0|(?:Content-Length:\\s*\\d+)(?:\\n.*){2,}|(?:Content-Length:\\s*\\d+)(?:\\n\\w+:\\s*.*?\\n+)|(?:TE:\\s*chunked)(?:\\n.*){2,}|(?:TE:\\s*identity)(?:\\n.*){2,})",
|
||||
"targets": [
|
||||
"HEADERS",
|
||||
"BODY"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Detects HTTP request smuggling patterns. Targets Transfer-Encoding and Content-Length headers."
|
||||
},
|
||||
{
|
||||
"id": "idor-attacks",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:(?:\\b(?:id|user|account|profile|order|item|product|comment|post|blog|thread|task|note|group|file|image|report|json|api|rest|download|admin|dashboard|email|video)\\b(?:\\s*)[=:]\\s*(?:[\\-\\/]?\\d+|[\\w\\-\\.]+|[a-f0-9\\-]+))|\\b(?:[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\\b|\\/\\d+(?:\\/|$)|\\/[a-f0-9]{32}|\\/[a-f0-9]{40})",
|
||||
"targets": [
|
||||
"URI",
|
||||
"BODY",
|
||||
"HEADERS",
|
||||
"COOKIES"
|
||||
],
|
||||
"severity": "MEDIUM",
|
||||
"action": "log",
|
||||
"score": 7,
|
||||
"description": "Detects Insecure Direct Object Reference (IDOR) attempts by identifying common ID patterns in URIs, body, headers and cookies."
|
||||
},
|
||||
{
|
||||
"id": "insecure-deserialization-java",
|
||||
"phase": 2,
|
||||
"pattern": "(?:rO0AB|aced0005|\\xac\\xed\\x00\\x05)",
|
||||
"targets": [
|
||||
"BODY",
|
||||
"HEADERS",
|
||||
"COOKIES"
|
||||
],
|
||||
"severity": "CRITICAL",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block requests containing potential Java serialized objects, including magic bytes for serialized objects."
|
||||
},
|
||||
{
|
||||
"id": "jwt-tampering",
|
||||
"phase": 1,
|
||||
"pattern": "^(eyJ[A-Za-z0-9_-]{0,}\\.eyJ[A-Za-z0-9_-]{0,}\\.[A-Za-z0-9_-]{0,})",
|
||||
"targets": [
|
||||
"HEADERS:Authorization",
|
||||
"COOKIES"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 8,
|
||||
"description": "Block potential JWT tampering attempts in Authorization headers or cookies."
|
||||
},
|
||||
{
|
||||
"id": "nosql-injection-attacks",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:\\$(?:gt|gte|lt|lte|ne|eq|regex|where|or|and|in|nin|exists|type|jsonSchema|not|mod|elemMatch|all|size|nor|comment|slice|expr|meta|text|search|near|nearSphere|geoWithin|geoIntersects|geoNear)\\b|\\b(?:db|collection|aggregate|mapReduce|count|group|distinct|findOne|find|remove|update|insert)\\b)",
|
||||
"targets": [
|
||||
"BODY",
|
||||
"HEADERS",
|
||||
"COOKIES"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block NoSQL injection attempts in request body, headers, and cookies. Targets MongoDB operators and keywords."
|
||||
},
|
||||
{
|
||||
"id": "open-redirect-attempt",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:https?://(?:[^/]+@)?[^/]+\\.[^/]+/|\\b(?:redirect|url|next|return|r|u)\\b\\s*=\\s*(?:https?://|//))",
|
||||
"targets": [
|
||||
"HEADERS",
|
||||
"BODY"
|
||||
],
|
||||
"severity": "MEDIUM",
|
||||
"action": "block",
|
||||
"score": 6,
|
||||
"description": "Block potential open redirect attempts in request body and headers."
|
||||
},
|
||||
{
|
||||
"id": "path-traversal",
|
||||
"phase": 1,
|
||||
"pattern": "(?:\\.\\.[/\\\\]|\\.\\./|\\.\\.\\\\/|%2e%2e[/\\\\]|%2e%2e/|%2e%2e%5c|%252e%252e|\\b(?:etc(?:\\/|%2F)(?:passwd|shadow|hosts)|(?:proc|sys)(?:\\/|%2F)(?:self(?:\\/|%2F)environ|cmdline)|boot(?:\\/|%2F)grub(?:\\/|%2F)grub\\.cfg|\\/\\.\\.(?:\\/|%2F)|(?:\\/|%5c)(\\.\\.){2,}(?:\\/|%5c)|(?:\\.\\.){2,}(?:\\/|%5c)|(?:\\.\\.){2,}|(?:%2e%2e){2,}(?:%2f|%5c)|(?:%2e%2e%2f|%2e%2e%5c){2,}|(?:\\.\\.%2f|\\.\\.%5c){2,}|(?:%252e%252e%2f|%252e%252e%5c){2,}|%252e%252e|%252f%2e%2e|%255c%2e%2e|\\/\\.(?:\\/|%2F)|\\%2e(?:%2f|%5c))\\b)",
|
||||
"targets": [
|
||||
"URI",
|
||||
"HEADERS"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block path traversal attempts and direct access to sensitive files (Target: URI and Headers). Improved and more aggressive pattern matching, including more obfuscation techniques."
|
||||
},
|
||||
{
|
||||
"id": "rce-commands-expanded",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:\\b(?:cat|base64|whoami|echo|curl|wget|bash|sh|python|perl|ls|id|ping|nslookup|ipconfig|ifconfig|powershell)\\b)",
|
||||
"targets": [
|
||||
"ARGS",
|
||||
"HEADERS"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 5,
|
||||
"description": "Expanded rule to block more RCE related commands and utilities."
|
||||
},
|
||||
{
|
||||
"id": "rfi-http-url",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)https?:\\/\\/[^\\s]+",
|
||||
"targets": [
|
||||
"URI",
|
||||
"ARGS",
|
||||
"HEADERS"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 8,
|
||||
"description": "Block direct use of HTTP or HTTPS URLs for inclusion."
|
||||
},
|
||||
{
|
||||
"id": "sensitive-files",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)(?:/\\.git/(?:HEAD|index|config|refs|objects)|/\\.env(?:\\.local|\\.dev|\\.prod)?$|/\\.htaccess$|/\\.htpasswd$|/\\.svn/|/\\.DS_Store$|\\/WEB-INF\\/|\\/WEB-INF\\/web\\.xml|\\/META-INF\\/|\\.git/\\s*(?:H\\.E\\.A\\.D|HEAD)|\\.dockerenv|server-status|\\b(?:config|database|credentials|secrets|private|local|development|staging|production|backup|default)\\b(?:[\\-_\\.]?)(?:[a-z0-9]+)?\\.(?:json|yaml|yml|ini|properties|txt|conf|toml|lock|log|bak|swp|orig|dist|sample|example|template|env|sql))",
|
||||
"targets": [
|
||||
"URI"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block access to sensitive files and directories (Target: URI). Expanded rule to include more config and backup file names."
|
||||
},
|
||||
{
|
||||
"id": "sensitive-files-expanded",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)(?:/\\.git/(?:HEAD|index|config|refs|objects)|/\\.env(?:\\.local|\\.dev|\\.prod)?$|/\\.htaccess$|/\\.htpasswd$|/\\.svn/|/\\.DS_Store$|\\/WEB-INF\\/|\\/WEB-INF\\/web.xml|\\/META-INF\\/|\\.git/\\s*(?:H\\.E\\.A\\.D|HEAD)|\\.dockerenv|server-status)",
|
||||
"targets": [
|
||||
"URI"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Expanded rule to block access to more sensitive files and account for obfuscation."
|
||||
},
|
||||
{
|
||||
"id": "sql-injection",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:\\b(?:select|insert|update|delete|drop|alter|truncate|create|grant|revoke)\\b(?:\\s|\\/\\*.*?\\*\\/|--.*?)?(?:from|into|where|table|index|user|procedure|function|database)\\b|\\bunion\\b(?:\\s|\\/\\*.*?\\*\\/|--.*?)?(?:all|distinct)?(?:\\s|\\/\\*.*?\\*\\/|--.*?)?\\bselect\\b|'\\s*(?:and|or)\\s*['\\d]+\\s*(?:=|[<>]=?|!=)\\s*['\\d]+|\\)\\s*(?:and|or)\\s*\\([\\d]+\\s*(?:=|[<>]=?|!=)\\s*[\\d]+\\)|\\b(?:sleep|benchmark|waitfor\\s+delay)\\s*\\(|(?:\\bexec\\b|xp_cmdshell))",
|
||||
"targets": [
|
||||
"ARGS",
|
||||
"BODY"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 7,
|
||||
"description": "Block SQL injection attempts in request arguments and body. Removed HEADERS target to avoid false positives."
|
||||
},
|
||||
{
|
||||
"id": "sql-injection-improved-basic",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:'\\s*(?:and|or)\\s*\\d+\\s*[=<>!]+\\s*\\d+|['\"]\\s*\\d+\\s*[=<>!]+\\s*['\"]|'\\s*\\+\\s*'|--\\s*-|-{2,})",
|
||||
"targets": [
|
||||
"ARGS",
|
||||
"BODY"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 8,
|
||||
"description": "Improved rule to catch basic SQL injection including quotes and boolean logic. Removed HEADERS and COOKIES targets and removed double quote pattern to prevent false positives."
|
||||
},
|
||||
{
|
||||
"id": "ssrf-attacks",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:(?:https?|ftp|gopher|dict|ldap|tftp|file)://(?:[^/]+@)?(?:(?:127\\.0\\.0\\.\\d{1,3}|10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|172\\.(?:1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|169\\.254\\.\\d{1,3}\\.\\d{1,3}|(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|localhost|0\\.0\\.0\\.0|::1|\\d{1,10})|[^/]+\\.(?:internal|local|intranet|test))(?:\\:\\d{1,5})?(?:/[^\\s]*)?|\\b(?:metadata|aws|digitalocean|google|azure)\\b|\\b(?:169\\.254\\.\\d{1,3}\\.\\d{1,3})\\b(?:/[^\\s]*)?)",
|
||||
"targets": [
|
||||
"BODY"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block Server-Side Request Forgery (SSRF) attempts, including internal IP ranges and cloud metadata endpoints. Removed HEADERS and COOKIES targets to prevent false positives with browser cookies."
|
||||
},
|
||||
{
|
||||
"id": "ssrf-internal-ip",
|
||||
"phase": 2,
|
||||
"pattern": "(?:127\\.0\\.0\\.1|10\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.|192\\.168\\.)",
|
||||
"targets": [
|
||||
"URI",
|
||||
"ARGS"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 7,
|
||||
"description": "Block SSRF to Internal IPs."
|
||||
},
|
||||
{
|
||||
"id": "ssrf-reserved-ip",
|
||||
"phase": 2,
|
||||
"pattern": "(?:0\\.|169\\.254\\.|224\\.|240\\.|255\\.)",
|
||||
"targets": [
|
||||
"URI",
|
||||
"ARGS"
|
||||
],
|
||||
"severity": "MEDIUM",
|
||||
"action": "block",
|
||||
"score": 5,
|
||||
"description": "Block SSRF to Reserved/Multicast IPs."
|
||||
},
|
||||
{
|
||||
"id": "ssti-attacks",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:\\{\\{.*?\\}\\}|\\{\\%.*?\\%\\}|\\$\\{.*?\\}|\\#\\{.*?\\}|\\$\\(.*?\\)|\\{\\*.*?\\*\\}|\\#\\*.*?\\*\\#|<%[=]?.*?%>|@\\{.*?\\}|\\b(?:Runtime|Process|exec|System|getClass|ClassLoader|loadLibrary|forName|newInstance|getMethod|invoke|getConstructor|getDeclaredMethod|getDeclaredField|setAccessible|getDeclaredConstructor|getInputStream|getOutputStream|get|put|setAttribute|getProperty|setProperty|setSecurityManager|load|defineClass|new|clone|readObject|writeObject|call|apply|bind|super)\\b\\s*\\(|\\b(?:T|Math|Object|String|Boolean|Number|BigInteger|BigDecimal|Date|List|Map|Set|Queue|Array|Tuple|Pattern|Locale|Class|ClassLoader|Proxy|SecurityManager|Thread|ThreadGroup)\\b)",
|
||||
"targets": [
|
||||
"BODY"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block Server-Side Template Injection (SSTI) attacks in request body. Removed HEADERS and COOKIES targets to prevent false positives."
|
||||
},
|
||||
{
|
||||
"id": "unusual-paths",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)(?:/wp-admin|/phpmyadmin|/admin|/login|/cgi-bin|/shell|/backdoor|/cmd|/exec|/bin/(?:sh|bash|zsh)|/console|/setup|/test|\\.php$|\\.asp$|\\.aspx$|\\.jsp$|\\.do$|\\.action$|\\.pl$|\\.py$|\\.cgi$|\\.cfm$|\\.rb$|\\.php[0-9]?$|\\.phtml$|\\.htaccess$|\\.htpasswd$|\\.ini$|\\.config$|\\.lock$|\\.log$|\\.bak$|\\.swp$|\\.orig$|\\.dist$|\\.sample$|\\.example$|\\.template$|\\.env$)",
|
||||
"targets": [
|
||||
"URI"
|
||||
],
|
||||
"severity": "MEDIUM",
|
||||
"action": "block",
|
||||
"score": 7,
|
||||
"description": "Block requests to unusual or suspicious paths and common scripting extensions (Target: URI). Expanded rule for more file types and endpoints."
|
||||
},
|
||||
{
|
||||
"id": "xss-attacks",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:<script[^>]*>|<img[^>]*\\s+onerror=|javascript:|data:|vbscript:|<svg[^>]*\\s+onload=|alert\\(|document\\.(?:cookie|location)|eval\\(|base64_(?:encode|decode)|expression\\(|\\b(?:on(?:mouse(?:over|out|down|up|move)|focus|blur|click|key(?:press|down|up)|load|error|submit|reset|change))\\s*=|\\bstyle\\s*=|(?:&#[xX]?[0-9a-fA-F]+;)+|%[0-9a-fA-F]{2,}|\\biframe[^>]*srcdoc\\s*=)",
|
||||
"targets": [
|
||||
"BODY",
|
||||
"ARGS"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block XSS attempts using HTML tags, event handlers, javascript: protocol, encoded characters, iframe srcdoc, etc. Removed HEADERS and COOKIES targets and simplified pattern to prevent false positives."
|
||||
},
|
||||
{
|
||||
"id": "xss-improved-encoding",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:<script[^>]*>|<img[^>]*\\s+onerror=|javascript:|data:|vbscript:|<svg[^>]*\\s+onload=|alert\\(|document\\.(?:cookie|location)|eval\\(|base64_(?:encode|decode)|expression\\(|\\b(?:on(?:mouse(?:over|out|down|up|move)|focus|blur|click|key(?:press|down|up)|load|error|submit|reset|change))\\s*=|\\bstyle\\s*=|(?:&#[xX]?[0-9a-fA-F]+;)+|%[0-9a-fA-F]{2,}|\\biframe[^>]*srcdoc\\s*=)",
|
||||
"targets": [
|
||||
"ARGS",
|
||||
"BODY"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 8,
|
||||
"description": "Improved XSS rule to catch encoded payloads and iframe srcdoc. Removed HEADERS target to prevent false positives."
|
||||
},
|
||||
{
|
||||
"id": "browser-integrity-sec-fetch-dest-missing-block",
|
||||
"phase": 1,
|
||||
"pattern": "^$",
|
||||
"targets": [
|
||||
"HEADERS:Sec-Fetch-Dest-Presence-Check"
|
||||
],
|
||||
"severity": "CRITICAL",
|
||||
"action": "log",
|
||||
"score": 5,
|
||||
"description": "Changed to LOG: Requests missing Sec-Fetch-Dest header. Very strong indicator of non-browser traffic but can cause issues with normal browsers."
|
||||
},
|
||||
{
|
||||
"id": "browser-integrity-sec-fetch-mode-missing-log-score",
|
||||
"phase": 1,
|
||||
"pattern": "^$",
|
||||
"targets": [
|
||||
"HEADERS:Sec-Fetch-Mode-Presence-Check"
|
||||
],
|
||||
"severity": "MEDIUM",
|
||||
"action": "log",
|
||||
"score": 3,
|
||||
"description": "LOG+SCORE: Requests missing Sec-Fetch-Mode header. Suspicious, might be non-browser. Moderate confidence, low to moderate false positive risk. Assigns score."
|
||||
},
|
||||
{
|
||||
"id": "browser-integrity-sec-fetch-site-missing-log-score",
|
||||
"phase": 1,
|
||||
"pattern": "^$",
|
||||
"targets": [
|
||||
"HEADERS:Sec-Fetch-Site-Presence-Check"
|
||||
],
|
||||
"severity": "LOW",
|
||||
"action": "log",
|
||||
"score": 1,
|
||||
"description": "LOG+SCORE: Requests missing Sec-Fetch-Site header. Less critical but still worth monitoring. Lower confidence, slightly higher false positive risk. Assigns score."
|
||||
},
|
||||
{
|
||||
"id": "browser-integrity-sec-fetch-user-missing-log-score",
|
||||
"phase": 1,
|
||||
"pattern": "^$",
|
||||
"targets": [
|
||||
"HEADERS:Sec-Fetch-User-Presence-Check"
|
||||
],
|
||||
"severity": "LOW",
|
||||
"action": "log",
|
||||
"score": 1,
|
||||
"description": "LOG+SCORE: Requests missing Sec-Fetch-User header. Might indicate non-user-initiated actions or bots. Lowest confidence, moderate to higher false positive risk. Assigns score, mainly for correlation."
|
||||
},
|
||||
{
|
||||
"id": "browser-integrity-sec-fetch-dest-not-document-ua-suspicious-log-score",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)^(?:script|style|image|font|fetch|xhr|audio|video|manifest|object|embed|report|worker|sharedworker|serviceworker|empty|unknown)$",
|
||||
"targets": [
|
||||
"HEADERS:Sec-Fetch-Dest-Not"
|
||||
],
|
||||
"severity": "MEDIUM",
|
||||
"action": "log",
|
||||
"score": 3,
|
||||
"description": "LOG+SCORE: Sec-Fetch-Dest not 'document' AND Suspicious User-Agent. More suspicious combination. Moderate confidence, reduced false positive risk by combining checks. Assigns score."
|
||||
},
|
||||
{
|
||||
"id": "browser-integrity-sec-fetch-mode-no-cors-document-log-score",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)^(?:no-cors)$",
|
||||
"targets": [
|
||||
"HEADERS:Sec-Fetch-Mode"
|
||||
],
|
||||
"severity": "LOW",
|
||||
"action": "log",
|
||||
"score": 2,
|
||||
"description": "LOG+SCORE: 'document' requests with Sec-Fetch-Mode: 'no-cors'. Less common for initial page loads. Lower confidence, low false positive risk. Assigns score."
|
||||
},
|
||||
{
|
||||
"id": "browser-integrity-sec-fetch-site-cross-site-document-log-score",
|
||||
"phase": 1,
|
||||
"pattern": "(?i)^(?:cross-site)$",
|
||||
"targets": [
|
||||
"HEADERS:Sec-Fetch-Site"
|
||||
],
|
||||
"severity": "LOW",
|
||||
"action": "log",
|
||||
"score": 1,
|
||||
"description": "LOG+SCORE: 'document' requests with Sec-Fetch-Site: 'cross-site'. Common for external links, primarily for monitoring cross-site traffic patterns. Lowest confidence, very low false positive risk. Assigns score for traffic analysis."
|
||||
}
|
||||
]
|
||||
83
rules.go
83
rules.go
@@ -8,19 +8,21 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, rule *Rule, value string, state *WAFState) bool {
|
||||
func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, rule *Rule, target, value string, state *WAFState) bool {
|
||||
logID := r.Context().Value(ContextKeyLogId("logID")).(string)
|
||||
|
||||
redactedValue := m.requestValueExtractor.RedactValueIfSensitive(target, value)
|
||||
|
||||
m.logRequest(zapcore.DebugLevel, "Rule Matched", r, // More concise log message
|
||||
zap.String("rule_id", string(rule.ID)),
|
||||
zap.String("target", strings.Join(rule.Targets, ",")),
|
||||
zap.String("value", value),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.String("target", target), // Log the specific target that matched
|
||||
zap.String("value", redactedValue),
|
||||
zap.String("description", rule.Description),
|
||||
zap.Int("score", rule.Score),
|
||||
zap.Int("anomaly_threshold_config", m.AnomalyThreshold), // ADDED: Log configured anomaly threshold
|
||||
@@ -37,70 +39,89 @@ func (m *Middleware) processRuleMatch(w http.ResponseWriter, r *http.Request, ru
|
||||
state.TotalScore += rule.Score
|
||||
m.logRequest(zapcore.DebugLevel, "Anomaly score increased", r, // Corrected argument order - 'r' is now the third argument
|
||||
zap.String("log_id", logID),
|
||||
zap.String("rule_id", string(rule.ID)),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.Int("score_increase", rule.Score),
|
||||
zap.Int("old_score", oldScore),
|
||||
zap.Int("new_score", state.TotalScore),
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold),
|
||||
)
|
||||
|
||||
shouldBlock := !state.ResponseWritten && (state.TotalScore >= m.AnomalyThreshold || rule.Action == "block")
|
||||
blockReason := ""
|
||||
// CRITICAL FIX: Check if "mode" field in rule doesn't match the required "action" field
|
||||
// There's a mismatch between Rule.Action and the "mode" field in the JSON
|
||||
// Map "mode" to "action" for proper rule processing
|
||||
actualAction := rule.Action
|
||||
|
||||
// Debug the actual action field value to verify what's being used
|
||||
m.logger.Debug("Rule action/mode check",
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.String("action_field", rule.Action),
|
||||
zap.Int("score", rule.Score),
|
||||
zap.Int("threshold", m.AnomalyThreshold),
|
||||
zap.Int("total_score", state.TotalScore))
|
||||
|
||||
// CRITICAL FIX: Check if the request should be blocked
|
||||
exceedsThreshold := !state.ResponseWritten && (state.TotalScore >= m.AnomalyThreshold)
|
||||
explicitBlock := !state.ResponseWritten && (actualAction == "block")
|
||||
shouldBlock := exceedsThreshold || explicitBlock
|
||||
|
||||
// Set appropriate block reason based on what triggered the block
|
||||
blockReason := ""
|
||||
if shouldBlock {
|
||||
blockReason = "Anomaly threshold exceeded"
|
||||
if rule.Action == "block" {
|
||||
if exceedsThreshold {
|
||||
blockReason = "Anomaly threshold exceeded"
|
||||
}
|
||||
if explicitBlock {
|
||||
blockReason = "Rule action is 'block'"
|
||||
}
|
||||
}
|
||||
|
||||
m.logRequest(zapcore.DebugLevel, "Determining Block Action", r, // More descriptive log message
|
||||
zap.String("action", rule.Action),
|
||||
zap.Bool("should_block", shouldBlock),
|
||||
zap.String("block_reason", blockReason),
|
||||
zap.Int("total_score", state.TotalScore), // ADDED: Log total score in block decision log
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold), // ADDED: Log anomaly threshold in block decision log
|
||||
)
|
||||
// Ensure we're setting the blocked state
|
||||
state.Blocked = true
|
||||
state.StatusCode = http.StatusForbidden
|
||||
|
||||
if shouldBlock {
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, blockReason, string(rule.ID), value,
|
||||
// Block the request and write the response immediately
|
||||
m.blockRequest(w, r, state, http.StatusForbidden, blockReason, rule.ID,
|
||||
zap.Int("total_score", state.TotalScore),
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold),
|
||||
zap.String("final_block_reason", blockReason), // ADDED: Clarify block reason in blockRequest log
|
||||
zap.String("final_block_reason", blockReason),
|
||||
zap.Bool("explicitly_blocked", explicitBlock),
|
||||
zap.Bool("threshold_exceeded", exceedsThreshold),
|
||||
)
|
||||
|
||||
// Return false to stop processing more rules
|
||||
return false
|
||||
}
|
||||
|
||||
if rule.Action == "log" {
|
||||
m.logRequest(zapcore.InfoLevel, "Rule action: Log", r,
|
||||
zap.String("log_id", logID),
|
||||
zap.String("rule_id", string(rule.ID)),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.Int("total_score", state.TotalScore), // ADDED: Log total score for log action
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold), // ADDED: Log anomaly threshold for log action
|
||||
)
|
||||
} else if !shouldBlock && !state.ResponseWritten {
|
||||
m.logRequest(zapcore.DebugLevel, "Rule action: No Block", r,
|
||||
zap.String("log_id", logID),
|
||||
zap.String("rule_id", string(rule.ID)),
|
||||
zap.String("rule_id", rule.ID),
|
||||
zap.String("action", rule.Action),
|
||||
zap.Int("total_score", state.TotalScore),
|
||||
zap.Int("anomaly_threshold", m.AnomalyThreshold),
|
||||
)
|
||||
}
|
||||
|
||||
// Continue processing other rules
|
||||
return true
|
||||
}
|
||||
|
||||
// incrementRuleHitCount increments the hit counter for a given rule ID.
|
||||
func (m *Middleware) incrementRuleHitCount(ruleID RuleID) {
|
||||
hitCount := HitCount(1) // Default increment
|
||||
if currentCount, loaded := m.ruleHits.Load(ruleID); loaded {
|
||||
hitCount = currentCount.(HitCount) + 1
|
||||
}
|
||||
m.ruleHits.Store(ruleID, hitCount)
|
||||
// SOTA Pattern: Wait-Free / Lock-Free Data Structures (using atomic)
|
||||
counterInterface, _ := m.ruleHits.LoadOrStore(ruleID, &atomic.Int64{})
|
||||
counter := counterInterface.(*atomic.Int64)
|
||||
newVal := counter.Add(1)
|
||||
|
||||
m.logger.Debug("Rule hit count updated",
|
||||
zap.String("rule_id", string(ruleID)),
|
||||
zap.Int("hit_count", int(hitCount)), // More descriptive log field
|
||||
zap.Int64("hit_count", newVal),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -221,11 +242,11 @@ func (m *Middleware) loadRulesFromFile(path string, ruleIDs map[string]bool) (va
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := ruleIDs[string(rule.ID)]; exists {
|
||||
if _, exists := ruleIDs[rule.ID]; exists {
|
||||
fileInvalidRules = append(fileInvalidRules, fmt.Sprintf("Duplicate rule ID '%s' at index %d", rule.ID, i))
|
||||
continue
|
||||
}
|
||||
ruleIDs[string(rule.ID)] = true // Track rule IDs to prevent duplicates
|
||||
ruleIDs[rule.ID] = true // Track rule IDs to prevent duplicates
|
||||
|
||||
// RuleCache handling (compile and cache regex)
|
||||
if cachedRegex, exists := m.ruleCache.Get(rule.ID); exists {
|
||||
|
||||
15
rules.json
15
rules.json
@@ -234,17 +234,15 @@
|
||||
{
|
||||
"id": "sql-injection-improved-basic",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:'\\s*(?:and|or)\\s*\\d+\\s*[=<>!]+\\s*\\d+|['\"]\\s*\\d+\\s*[=<>!]+\\s*['\"]|'\\s*\\+\\s*'|--\\s*-|-{2,}|\")",
|
||||
"pattern": "(?i)(?:'\\s*(?:and|or)\\s*\\d+\\s*[=<>!]+\\s*\\d+|['\"]\\s*\\d+\\s*[=<>!]+\\s*['\"]|'\\s*\\+\\s*'|--\\s*-|-{2,})",
|
||||
"targets": [
|
||||
"ARGS",
|
||||
"BODY",
|
||||
"HEADERS",
|
||||
"COOKIES"
|
||||
"BODY"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 8,
|
||||
"description": "Improved rule to catch basic SQL injection including quotes and boolean logic."
|
||||
"description": "Improved rule to catch basic SQL injection including quotes and boolean logic. Removed HEADERS and COOKIES targets and removed double quote pattern to prevent false positives."
|
||||
},
|
||||
{
|
||||
"id": "ssrf-attacks",
|
||||
@@ -315,16 +313,15 @@
|
||||
{
|
||||
"id": "xss-attacks",
|
||||
"phase": 2,
|
||||
"pattern": "(?i)(?:<script[^>]*>|<img[^>]*\\s+onerror=|javascript:|data:|vbscript:|<svg[^>]*\\s+onload=|alert\\(|document\\.(?:cookie|location)|eval\\(|base64_(?:encode|decode)|expression\\(|\\b(?:on(?:mouse(?:over|out|down|up|move)|focus|blur|click|key(?:press|down|up)|load|error|submit|reset|change))\\s*=|\\bstyle\\s*=|(?:&#[xX]?[0-9a-fA-F]+;)+|%[0-9a-fA-F]{2,}|\\biframe[^>]*srcdoc\\s*=|\\bevent\\b\\s*=\\s*['\"](?:javascript:).*?['\"]|url\\s*\\([\\s\\n]*?(?:javascript:).*?\\)|\\b(?:\\b(?:src|href|action|data|code)\\s*=\\s*['\"]?(?:javascript:|data:)|\\b(?:formaction|background|poster|xlink:href)\\s*=\\s*['\"]?(?:javascript:|data:))|\\b(?:svg|math|marquee|audio|video|embed|object|plaintext|isindex)\\b)",
|
||||
"pattern": "(?i)(?:<script[^>]*>|<img[^>]*\\s+onerror=|javascript:|data:|vbscript:|<svg[^>]*\\s+onload=|alert\\(|document\\.(?:cookie|location)|eval\\(|base64_(?:encode|decode)|expression\\(|\\b(?:on(?:mouse(?:over|out|down|up|move)|focus|blur|click|key(?:press|down|up)|load|error|submit|reset|change))\\s*=|\\bstyle\\s*=|(?:&#[xX]?[0-9a-fA-F]+;)+|%[0-9a-fA-F]{2,}|\\biframe[^>]*srcdoc\\s*=)",
|
||||
"targets": [
|
||||
"BODY",
|
||||
"HEADERS",
|
||||
"COOKIES"
|
||||
"ARGS"
|
||||
],
|
||||
"severity": "HIGH",
|
||||
"action": "block",
|
||||
"score": 9,
|
||||
"description": "Block XSS attempts using HTML tags, event handlers, javascript: protocol, encoded characters, iframe srcdoc, event attributes, url functions, and other vectors in request body, headers and cookies. Improved pattern matching, including more attack vectors."
|
||||
"description": "Block XSS attempts using HTML tags, event handlers, javascript: protocol, encoded characters, iframe srcdoc, etc. Removed HEADERS and COOKIES targets and simplified pattern to prevent false positives."
|
||||
},
|
||||
{
|
||||
"id": "xss-improved-encoding",
|
||||
|
||||
@@ -146,10 +146,11 @@ func TestProcessRuleMatch(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &Middleware{
|
||||
logger: logger,
|
||||
AnomalyThreshold: tt.anomalyThreshold,
|
||||
ruleHits: sync.Map{},
|
||||
muMetrics: sync.RWMutex{},
|
||||
logger: logger,
|
||||
AnomalyThreshold: tt.anomalyThreshold,
|
||||
ruleHits: sync.Map{},
|
||||
muMetrics: sync.RWMutex{},
|
||||
requestValueExtractor: NewRequestValueExtractor(logger, false, 0),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -162,7 +163,7 @@ func TestProcessRuleMatch(t *testing.T) {
|
||||
ResponseWritten: tt.responseWritten,
|
||||
}
|
||||
|
||||
result := m.processRuleMatch(w, r, &tt.rule, "test-value", state)
|
||||
result := m.processRuleMatch(w, r, &tt.rule, "ARGS", "test-value", state)
|
||||
if result == tt.wantBlock {
|
||||
t.Errorf("processRuleMatch() returned %v, want %v", result, !tt.wantBlock)
|
||||
}
|
||||
@@ -187,7 +188,7 @@ func TestLoadRules(t *testing.T) {
|
||||
"action": "block"
|
||||
}
|
||||
]`
|
||||
os.WriteFile(validRuleFile, []byte(validRules), 0644)
|
||||
os.WriteFile(validRuleFile, []byte(validRules), 0o644)
|
||||
|
||||
invalidRuleFile := filepath.Join(tmpDir, "invalid_rules.json")
|
||||
invalidRules := `[
|
||||
@@ -199,7 +200,7 @@ func TestLoadRules(t *testing.T) {
|
||||
"score": -1
|
||||
}
|
||||
]`
|
||||
os.WriteFile(invalidRuleFile, []byte(invalidRules), 0644)
|
||||
os.WriteFile(invalidRuleFile, []byte(invalidRules), 0o644)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
145
sample_rules.json
Normal file
145
sample_rules.json
Normal file
@@ -0,0 +1,145 @@
|
||||
[
|
||||
{
|
||||
"id": "BLOCK-NIKTO",
|
||||
"phase": 2,
|
||||
"pattern": "nikto",
|
||||
"targets": [
|
||||
"USER_AGENT"
|
||||
],
|
||||
"severity": "critical",
|
||||
"score": 5,
|
||||
"mode": "block",
|
||||
"description": "Block Nikto scanner",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-1",
|
||||
"phase": 2,
|
||||
"pattern": "low_score_test",
|
||||
"targets": [
|
||||
"URL_PARAM:test"
|
||||
],
|
||||
"severity": "low",
|
||||
"score": 1,
|
||||
"mode": "log",
|
||||
"description": "Low score test rule",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-PARAM1",
|
||||
"phase": 2,
|
||||
"pattern": "score2",
|
||||
"targets": [
|
||||
"URL_PARAM:param1"
|
||||
],
|
||||
"severity": "medium",
|
||||
"score": 2,
|
||||
"mode": "log",
|
||||
"description": "Medium score test rule for param1",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-PARAM2",
|
||||
"phase": 2,
|
||||
"pattern": "score2",
|
||||
"targets": [
|
||||
"URL_PARAM:param2"
|
||||
],
|
||||
"severity": "medium",
|
||||
"score": 2,
|
||||
"mode": "log",
|
||||
"description": "Medium score test rule for param2",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-PARAM1-HIGH",
|
||||
"phase": 2,
|
||||
"pattern": "score3",
|
||||
"targets": [
|
||||
"URL_PARAM:param1"
|
||||
],
|
||||
"severity": "high",
|
||||
"score": 3,
|
||||
"mode": "log",
|
||||
"description": "High score test rule for param1",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-PARAM2-HIGH",
|
||||
"phase": 2,
|
||||
"pattern": "score3",
|
||||
"targets": [
|
||||
"URL_PARAM:param2"
|
||||
],
|
||||
"severity": "high",
|
||||
"score": 3,
|
||||
"mode": "log",
|
||||
"description": "High score test rule for param2",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-PARAM3-HIGH",
|
||||
"phase": 2,
|
||||
"pattern": "score3",
|
||||
"targets": [
|
||||
"URL_PARAM:param3"
|
||||
],
|
||||
"severity": "high",
|
||||
"score": 3,
|
||||
"mode": "log",
|
||||
"description": "High score test rule for param3",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-BLOCK",
|
||||
"phase": 2,
|
||||
"pattern": "true",
|
||||
"targets": [
|
||||
"URL_PARAM:block"
|
||||
],
|
||||
"severity": "critical",
|
||||
"score": 0,
|
||||
"mode": "block",
|
||||
"description": "Block action test rule",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-INCR-1",
|
||||
"phase": 2,
|
||||
"pattern": "score1",
|
||||
"targets": [
|
||||
"URL_PARAM:increment"
|
||||
],
|
||||
"severity": "low",
|
||||
"score": 1,
|
||||
"mode": "log",
|
||||
"description": "Incremental test rule 1",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-INCR-2",
|
||||
"phase": 2,
|
||||
"pattern": "score2",
|
||||
"targets": [
|
||||
"URL_PARAM:increment"
|
||||
],
|
||||
"severity": "medium",
|
||||
"score": 2,
|
||||
"mode": "log",
|
||||
"description": "Incremental test rule 2",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"id": "TEST-RULE-INCR-3",
|
||||
"phase": 2,
|
||||
"pattern": "score3",
|
||||
"targets": [
|
||||
"URL_PARAM:increment"
|
||||
],
|
||||
"severity": "high",
|
||||
"score": 3,
|
||||
"mode": "log",
|
||||
"description": "Incremental test rule 3",
|
||||
"priority": 10
|
||||
}
|
||||
]
|
||||
17
test.caddyfile
Normal file
17
test.caddyfile
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
debug
|
||||
auto_https off
|
||||
admin localhost:2019
|
||||
}
|
||||
|
||||
:8080 {
|
||||
route {
|
||||
waf {
|
||||
rule_file ./sample_rules.json
|
||||
anomaly_threshold 5
|
||||
log_severity debug
|
||||
metrics_endpoint /metrics
|
||||
}
|
||||
respond "Hello world!"
|
||||
}
|
||||
}
|
||||
335
test_anomalythreshold.py
Normal file
335
test_anomalythreshold.py
Normal file
@@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import argparse
|
||||
from termcolor import colored
|
||||
|
||||
# --- setup_args function remains the same ---
|
||||
def setup_args():
|
||||
parser = argparse.ArgumentParser(description='Test WAF anomaly threshold behavior')
|
||||
parser.add_argument('--url', default='http://localhost:8080', help='URL to test (default: http://localhost:8080)')
|
||||
parser.add_argument('--threshold', type=int, default=5, help='Configured anomaly threshold (default: 5)')
|
||||
parser.add_argument('--debug', action='store_true', help='Enable debug output for response headers')
|
||||
parser.add_argument('--verbose', action='store_true', help='Show verbose test details')
|
||||
return parser.parse_args()
|
||||
|
||||
# --- send_request function remains the same ---
|
||||
def send_request(url, payload, headers=None, expected_status=None, debug=False):
|
||||
"""
|
||||
Send a request with the given payload and validate the response.
|
||||
|
||||
Returns:
|
||||
tuple: (response object or None, dict of found WAF headers, bool or None for passed status)
|
||||
'passed' is True if status matches expected_status, False if it doesn't or error occurs,
|
||||
None if expected_status was not provided.
|
||||
"""
|
||||
if headers is None:
|
||||
headers = {'User-Agent': 'WAF-Threshold-Test/1.0'}
|
||||
|
||||
print(colored(f"\n>>> Sending request to {url}", "blue"))
|
||||
print(colored(f">>> Payload: {payload}", "blue"))
|
||||
|
||||
passed = None # Default if no expectation set
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
url,
|
||||
params=payload,
|
||||
headers=headers,
|
||||
timeout=10 # Increased timeout slightly
|
||||
)
|
||||
|
||||
status = response.status_code
|
||||
|
||||
# Determine pass/fail based on expected status
|
||||
if expected_status is not None:
|
||||
passed = (status == expected_status)
|
||||
color = "green" if passed else "red"
|
||||
result_text = "✓ PASS" if passed else "✗ FAIL"
|
||||
print(colored(f"<<< Status: {status} (Expected: {expected_status}) - {result_text}", color))
|
||||
else:
|
||||
# No expected status, just report what we got
|
||||
print(colored(f"<<< Status: {status}", "yellow"))
|
||||
|
||||
response_text = response.text
|
||||
print(colored(f"<<< Response: {response_text[:100]}...", "yellow") if len(response_text) > 100 else colored(f"<<< Response: {response_text}", "yellow"))
|
||||
|
||||
# Check for WAF-specific headers
|
||||
waf_headers = {}
|
||||
if debug:
|
||||
print(colored("\n--- Response Headers ---", "cyan"))
|
||||
for header, value in response.headers.items():
|
||||
print(colored(f" {header}: {value}", "yellow"))
|
||||
# Check for common WAF score headers - these may vary based on your WAF implementation
|
||||
lower_header = header.lower()
|
||||
if lower_header in ('x-waf-score', 'x-waf-anomaly-score', 'x-waf-status', 'x-waf-rules', 'x-waf-action'):
|
||||
waf_headers[lower_header] = value
|
||||
print(colored(f" Found WAF header: {header}={value}", "green"))
|
||||
print(colored("--- End Headers ---", "cyan"))
|
||||
|
||||
return response, waf_headers, passed
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
print(colored(f"Error: Request timed out after 10 seconds.", "red"))
|
||||
passed = False # Timeout is a failure if status was expected
|
||||
if expected_status is not None:
|
||||
print(colored(f"<<< Status: TIMEOUT (Expected: {expected_status}) - ✗ FAIL", "red"))
|
||||
else:
|
||||
print(colored(f"<<< Status: TIMEOUT", "red"))
|
||||
return None, {}, passed
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(colored(f"Error sending request: {str(e)}", "red"))
|
||||
passed = False # Request error is a failure if status was expected
|
||||
if expected_status is not None:
|
||||
print(colored(f"<<< Status: ERROR (Expected: {expected_status}) - ✗ FAIL", "red"))
|
||||
else:
|
||||
print(colored(f"<<< Status: ERROR", "red"))
|
||||
return None, {}, passed
|
||||
|
||||
# --- test_anomaly_threshold function is UPDATED ---
|
||||
def test_anomaly_threshold(base_url, threshold, debug=False, verbose=False):
|
||||
"""Test that anomaly threshold is properly enforced."""
|
||||
print(colored(f"\n=== Testing Anomaly Threshold (threshold={threshold}) ===", "cyan"))
|
||||
|
||||
results_data = {} # Store results keyed by test name
|
||||
|
||||
# --- Original Tests ---
|
||||
# Test 1: Low score (should pass, 200 OK)
|
||||
print(colored("\nTest 1: Low-score rule (should pass with 200 OK)", "magenta"))
|
||||
low_score_payload = {'test': 'low_score_test'} # RULE-1 (Score 1)
|
||||
expected_score = 1
|
||||
low_response, low_headers, test1_passed = send_request(base_url, low_score_payload, expected_status=200, debug=debug)
|
||||
results_data["Test 1 (Low score)"] = (test1_passed, low_response.status_code if low_response else "ERROR", f"Expected 200 OK for low score ({expected_score}) < threshold ({threshold})")
|
||||
print(colored(f"-> Expected anomaly score contribution: {expected_score}", "yellow"))
|
||||
|
||||
# Test 2: Score below threshold (should pass, 200 OK)
|
||||
print(colored(f"\nTest 2: Score below threshold (should pass with 200 OK)", "magenta"))
|
||||
below_threshold_payload = {'param1': 'score2', 'param2': 'score2'} # RULE-PARAM1 (2) + RULE-PARAM2 (2) = 4
|
||||
expected_total_score = 4
|
||||
below_response, below_headers, test2_passed = send_request(base_url, below_threshold_payload, expected_status=200, debug=debug)
|
||||
results_data["Test 2 (Below threshold)"] = (test2_passed, below_response.status_code if below_response else "ERROR", f"Expected 200 OK for score ({expected_total_score}) < threshold ({threshold})")
|
||||
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
|
||||
|
||||
# Test 3: Score exceeding threshold (should block, 403 Forbidden)
|
||||
print(colored(f"\nTest 3: Score exceeding threshold (should block with 403 Forbidden)", "magenta"))
|
||||
exceed_threshold_payload = {'param1': 'score3', 'param2': 'score3'} # RULE-PARAM1-HIGH (3) + RULE-PARAM2-HIGH (3) = 6
|
||||
expected_total_score = 6
|
||||
exceed_response, exceed_headers, test3_passed = send_request(base_url, exceed_threshold_payload, expected_status=403, debug=debug)
|
||||
results_data["Test 3 (Exceed threshold)"] = (test3_passed, exceed_response.status_code if exceed_response else "ERROR", f"Expected 403 Forbidden for score ({expected_total_score}) >= threshold ({threshold})")
|
||||
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
|
||||
|
||||
# Test 4: Explicit 'block' action rule (should block, 403 Forbidden)
|
||||
print(colored("\nTest 4: Explicit 'block' action rule (should block with 403 Forbidden)", "magenta"))
|
||||
block_action_payload = {'block': 'true'} # RULE-BLOCK (Block Action)
|
||||
block_response, block_headers, test4_passed = send_request(base_url, block_action_payload, expected_status=403, debug=debug)
|
||||
results_data["Test 4 (Block action)"] = (test4_passed, block_response.status_code if block_response else "ERROR", "Expected 403 Forbidden for explicit block action")
|
||||
print(colored("-> Score doesn't matter for this test - blocking action should take precedence", "yellow"))
|
||||
|
||||
# Test 5: Incremental scoring in separate requests (should pass, 200 OK)
|
||||
print(colored("\nTest 5: Incremental scoring in separate requests (should be isolated per request, pass with 200 OK)", "magenta"))
|
||||
incremental_results_passed = []
|
||||
incremental_status_codes = []
|
||||
for i in range(1, 4): # Tests INCR-1 (1), INCR-2 (2), INCR-3 (3)
|
||||
print(colored(f"--- Request {i} of incremental test ---", "cyan"))
|
||||
incremental_payload = {'increment': f'score{i}'}
|
||||
expected_score = i
|
||||
incremental_response, inc_headers, single_inc_passed = send_request(base_url, incremental_payload, expected_status=200, debug=debug)
|
||||
incremental_results_passed.append(single_inc_passed if single_inc_passed is not None else False)
|
||||
incremental_status_codes.append(incremental_response.status_code if incremental_response else "ERROR")
|
||||
print(colored(f"-> Expected anomaly score contribution for this request: {expected_score}", "yellow"))
|
||||
if i < 3: time.sleep(0.2) # Shorter delay
|
||||
test5_passed = all(incremental_results_passed)
|
||||
status_summary = ', '.join(map(str, incremental_status_codes))
|
||||
results_data["Test 5 (Incremental)"] = (test5_passed, status_summary, f"Expected 200 OK for all incremental tests (scores {', '.join(map(str,range(1,4)))}) < threshold ({threshold})")
|
||||
|
||||
# --- NEW TESTS ---
|
||||
|
||||
# Test 6: Score hitting exact threshold (should block, 403 Forbidden)
|
||||
print(colored(f"\nTest 6: Score hitting exact threshold (should block with 403 Forbidden)", "magenta"))
|
||||
exact_threshold_payload = {'param1': 'score2', 'param2': 'score3'} # RULE-PARAM1 (2) + RULE-PARAM2-HIGH (3) = 5
|
||||
expected_total_score = 5
|
||||
exact_response, exact_headers, test6_passed = send_request(base_url, exact_threshold_payload, expected_status=403, debug=debug)
|
||||
results_data["Test 6 (Exact threshold)"] = (test6_passed, exact_response.status_code if exact_response else "ERROR", f"Expected 403 Forbidden for score ({expected_total_score}) == threshold ({threshold})")
|
||||
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
|
||||
|
||||
# Test 7: Mix High/Low score below threshold (should pass, 200 OK)
|
||||
print(colored(f"\nTest 7: Mix High/Low score below threshold (should pass with 200 OK)", "magenta"))
|
||||
mix_below_payload = {'test': 'low_score_test', 'param1': 'score3'} # RULE-1 (1) + RULE-PARAM1-HIGH (3) = 4
|
||||
expected_total_score = 4
|
||||
mix_below_response, mix_below_headers, test7_passed = send_request(base_url, mix_below_payload, expected_status=200, debug=debug)
|
||||
results_data["Test 7 (Mix Below Threshold)"] = (test7_passed, mix_below_response.status_code if mix_below_response else "ERROR", f"Expected 200 OK for mixed score ({expected_total_score}) < threshold ({threshold})")
|
||||
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
|
||||
|
||||
# Test 8: Score greatly exceeding threshold (with Param3) (should block, 403 Forbidden)
|
||||
print(colored(f"\nTest 8: Score greatly exceeding threshold (should block with 403 Forbidden)", "magenta"))
|
||||
exceed_greatly_payload = {'param1': 'score3', 'param2': 'score3', 'param3': 'score3'} # RULE-PARAM1-HIGH (3) + RULE-PARAM2-HIGH (3) + RULE-PARAM3-HIGH (3) = 9
|
||||
expected_total_score = 9
|
||||
exceed_greatly_response, exceed_greatly_headers, test8_passed = send_request(base_url, exceed_greatly_payload, expected_status=403, debug=debug)
|
||||
results_data["Test 8 (Exceed Greatly)"] = (test8_passed, exceed_greatly_response.status_code if exceed_greatly_response else "ERROR", f"Expected 403 Forbidden for score ({expected_total_score}) >= threshold ({threshold})")
|
||||
print(colored(f"-> Expected total anomaly score: {expected_total_score} (Threshold: {threshold})", "yellow"))
|
||||
|
||||
# Test 9: Block action triggered with other scoring rules (should block, 403 Forbidden)
|
||||
print(colored(f"\nTest 9: Block action priority (should block with 403 Forbidden)", "magenta"))
|
||||
block_priority_payload = {'block': 'true', 'param1': 'score2'} # RULE-BLOCK (block) + RULE-PARAM1 (2)
|
||||
expected_total_score = 2 # Score is calculated but block action takes precedence
|
||||
block_priority_response, block_priority_headers, test9_passed = send_request(base_url, block_priority_payload, expected_status=403, debug=debug)
|
||||
results_data["Test 9 (Block Priority)"] = (test9_passed, block_priority_response.status_code if block_priority_response else "ERROR", "Expected 403 Forbidden due to explicit block action, regardless of score")
|
||||
print(colored(f"-> Calculated anomaly score: {expected_total_score}. Block action should override.", "yellow"))
|
||||
|
||||
# Test 10: No matching rules (should pass, 200 OK)
|
||||
print(colored(f"\nTest 10: No matching rules (should pass with 200 OK)", "magenta"))
|
||||
no_match_payload = {'vanilla': 'test', 'unknown': 'data'}
|
||||
expected_total_score = 0
|
||||
no_match_response, no_match_headers, test10_passed = send_request(base_url, no_match_payload, expected_status=200, debug=debug)
|
||||
results_data["Test 10 (No Match)"] = (test10_passed, no_match_response.status_code if no_match_response else "ERROR", f"Expected 200 OK when no rules match (score {expected_total_score})")
|
||||
print(colored(f"-> Expected total anomaly score: {expected_total_score}", "yellow"))
|
||||
|
||||
# Test 11: Parameter name match, value mismatch (should pass, 200 OK)
|
||||
print(colored(f"\nTest 11: Parameter name match, value mismatch (should pass with 200 OK)", "magenta"))
|
||||
value_mismatch_payload = {'param1': 'non_matching_value', 'test': 'another_value'} # Neither value matches RULE-PARAM1 or RULE-1 patterns
|
||||
expected_total_score = 0
|
||||
value_mismatch_response, value_mismatch_headers, test11_passed = send_request(base_url, value_mismatch_payload, expected_status=200, debug=debug)
|
||||
results_data["Test 11 (Value Mismatch)"] = (test11_passed, value_mismatch_response.status_code if value_mismatch_response else "ERROR", f"Expected 200 OK when parameter values don't match rule patterns (score {expected_total_score})")
|
||||
print(colored(f"-> Expected total anomaly score: {expected_total_score}", "yellow"))
|
||||
|
||||
|
||||
# Summarize results
|
||||
print(colored("\n=== Anomaly Threshold Test Summary ===", "cyan"))
|
||||
print(colored(f"Target URL: {base_url}", "yellow"))
|
||||
print(colored(f"Configured threshold: {threshold}", "yellow"))
|
||||
|
||||
all_passed_flag = True
|
||||
# Define the order tests should appear in the summary
|
||||
test_order = [
|
||||
"Test 1 (Low score)",
|
||||
"Test 2 (Below threshold)",
|
||||
"Test 7 (Mix Below Threshold)", # New test inserted logically
|
||||
"Test 5 (Incremental)", # Incremental scores are below threshold
|
||||
"Test 10 (No Match)",
|
||||
"Test 11 (Value Mismatch)",
|
||||
"Test 6 (Exact threshold)", # Blocking test
|
||||
"Test 3 (Exceed threshold)", # Blocking test
|
||||
"Test 8 (Exceed Greatly)", # Blocking test
|
||||
"Test 4 (Block action)", # Blocking test
|
||||
"Test 9 (Block Priority)" # Blocking test
|
||||
]
|
||||
|
||||
print(colored("\n--- Test Results ---", "cyan"))
|
||||
for test_name in test_order:
|
||||
if test_name not in results_data:
|
||||
print(colored(f"{test_name}: SKIPPED (Data not found)", "yellow"))
|
||||
all_passed_flag = False # Consider missing data a failure
|
||||
continue
|
||||
|
||||
passed, status_code, description = results_data[test_name]
|
||||
# Treat None passed status as False for summary
|
||||
passed = passed if passed is not None else False
|
||||
result_text = "PASS" if passed else "FAIL"
|
||||
color = "green" if passed else "red"
|
||||
print(colored(f"{test_name}: {result_text} (Status: {status_code})", color))
|
||||
|
||||
if not passed:
|
||||
all_passed_flag = False
|
||||
print(colored(f" Reason: {description}", "yellow"))
|
||||
elif verbose:
|
||||
print(colored(f" Details: {description} (Status: {status_code})", "yellow"))
|
||||
|
||||
|
||||
# Final Pass/Fail Summary
|
||||
print(colored("\n--- Overall Result ---", "cyan"))
|
||||
if all_passed_flag:
|
||||
print(colored("✓ All tests passed! Anomaly threshold and blocking logic appear to be working correctly based on expected status codes.", "green"))
|
||||
else:
|
||||
print(colored("✗ Some tests failed. Review the output above.", "red"))
|
||||
failed_tests = [name for name in test_order if name in results_data and not results_data[name][0]]
|
||||
print(colored(f"Failed tests: {', '.join(failed_tests)}", "red"))
|
||||
|
||||
# Provide troubleshooting tips based on failure patterns
|
||||
test3_failed = "Test 3 (Exceed threshold)" in failed_tests
|
||||
test4_failed = "Test 4 (Block action)" in failed_tests
|
||||
test6_failed = "Test 6 (Exact threshold)" in failed_tests
|
||||
test8_failed = "Test 8 (Exceed Greatly)" in failed_tests
|
||||
test9_failed = "Test 9 (Block Priority)" in failed_tests
|
||||
blocking_tests_failed = test3_failed or test4_failed or test6_failed or test8_failed or test9_failed
|
||||
|
||||
if blocking_tests_failed:
|
||||
print(colored("\nSuggestion: One or more blocking tests failed (expected 403).", "yellow"))
|
||||
if test6_failed : print(colored(" - Check if the WAF blocks exactly *at* the threshold score.", "yellow"))
|
||||
if test3_failed or test8_failed: print(colored(f" - Verify rules correctly contribute scores and the threshold ({threshold}) is enforced.", "yellow"))
|
||||
if test4_failed or test9_failed: print(colored(" - Ensure rules with 'block' action are correctly configured and take priority.", "yellow"))
|
||||
|
||||
if "Test 5 (Incremental)" in failed_tests:
|
||||
print(colored("\nSuggestion: One or more incremental tests failed (expected 200). This might indicate score accumulation across requests (incorrect) or unrelated blocking rules triggered.", "yellow"))
|
||||
if "Test 10 (No Match)" in failed_tests or "Test 11 (Value Mismatch)" in failed_tests :
|
||||
print(colored("\nSuggestion: Tests expecting no match failed (expected 200). Check for overly broad rules or default blocking actions.", "yellow"))
|
||||
|
||||
|
||||
# --- check_server function remains the same ---
|
||||
def check_server(url):
|
||||
"""Check if the server is reachable."""
|
||||
print(f"\nChecking server reachability at {url}...")
|
||||
try:
|
||||
# Use HEAD request for efficiency, or GET if HEAD is disallowed/problematic
|
||||
response = requests.head(url, timeout=3)
|
||||
# Allow any success or redirect status code as "reachable"
|
||||
if 200 <= response.status_code < 400:
|
||||
print(colored(f"Server is reachable (Status: {response.status_code}).", "green"))
|
||||
return True
|
||||
else:
|
||||
# Handle client/server errors differently
|
||||
if 400 <= response.status_code < 500:
|
||||
print(colored(f"Server responded with client error: {response.status_code}. Check URL path/config.", "yellow"))
|
||||
elif 500 <= response.status_code < 600:
|
||||
print(colored(f"Server responded with server error: {response.status_code}. Check server/WAF logs.", "red"))
|
||||
else:
|
||||
print(colored(f"Server responded with unexpected status: {response.status_code}.", "yellow"))
|
||||
return False # Treat non-success/redirect as potentially problematic
|
||||
except requests.exceptions.Timeout:
|
||||
print(colored(f"ERROR: Connection to {url} timed out.", "red"))
|
||||
print(colored("Check if the server/proxy is running and accessible.", "yellow"))
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(colored(f"ERROR: Cannot connect to server at {url}", "red"))
|
||||
print(colored("Make sure the server/proxy (e.g., Caddy) is running and the URL is correct.", "yellow"))
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(colored(f"ERROR: An unexpected network error occurred: {str(e)}", "red"))
|
||||
return False
|
||||
|
||||
# --- main function is UPDATED (info section) ---
|
||||
def main():
|
||||
args = setup_args()
|
||||
base_url = args.url.rstrip('/') # Remove trailing slash if present
|
||||
threshold = args.threshold
|
||||
debug = args.debug
|
||||
verbose = args.verbose
|
||||
|
||||
print(colored(f"WAF Anomaly Threshold Test Tool", "cyan", attrs=["bold"]))
|
||||
print(colored("-" * 30, "cyan"))
|
||||
print(f"Target URL: {base_url}")
|
||||
print(f"Expected Threshold: {threshold}")
|
||||
print(f"Debug Mode: {'ON' if debug else 'OFF'}")
|
||||
print(f"Verbose Mode: {'ON' if verbose else 'OFF'}")
|
||||
print(colored("-" * 30, "cyan"))
|
||||
|
||||
# UPDATED Test rule setup recommendations
|
||||
print(colored("\nINFO: This script assumes specific WAF rules are configured:", "yellow"))
|
||||
print(colored(" - Rule(s) matching 'test=low_score_test' contribute score=1.", "yellow"))
|
||||
print(colored(" - Rule(s) matching 'param1=score2' contribute score=2.", "yellow"))
|
||||
print(colored(" - Rule(s) matching 'param2=score2' contribute score=2.", "yellow"))
|
||||
print(colored(" - Rule(s) matching 'param1=score3' contribute score=3.", "yellow"))
|
||||
print(colored(" - Rule(s) matching 'param2=score3' contribute score=3.", "yellow"))
|
||||
print(colored(" - Rule(s) matching 'param3=score3' contribute score=3. (Used in Test 8)", "yellow")) # Added param3 rule info
|
||||
print(colored(" - Rule matching 'block=true' has an explicit 'block' action.", "yellow"))
|
||||
print(colored(" - Rule(s) matching 'increment=scoreX' contribute score=X (e.g., 'increment=score1' adds 1).", "yellow"))
|
||||
|
||||
if not check_server(base_url):
|
||||
sys.exit(1)
|
||||
|
||||
test_anomaly_threshold(base_url, threshold, debug, verbose)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
33
tor.go
33
tor.go
@@ -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
|
||||
}
|
||||
|
||||
12
tor_test.go
12
tor_test.go
@@ -6,9 +6,10 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func TestTorConfig_Provision(t *testing.T) {
|
||||
@@ -40,10 +41,11 @@ func TestTorConfig_Provision(t *testing.T) {
|
||||
{
|
||||
name: "enabled config",
|
||||
config: TorConfig{
|
||||
Enabled: true,
|
||||
TORIPBlacklistFile: tmpFile.Name(),
|
||||
UpdateInterval: "5m",
|
||||
logger: logger,
|
||||
Enabled: true,
|
||||
CustomTORExitNodeURL: torListURL,
|
||||
TORIPBlacklistFile: tmpFile.Name(),
|
||||
UpdateInterval: "5m",
|
||||
logger: logger,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
197
types.go
197
types.go
@@ -1,8 +1,6 @@
|
||||
package caddywaf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -11,6 +9,7 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
"github.com/phemmer/go-iptrie"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
@@ -20,76 +19,18 @@ import (
|
||||
// ==================== Constants and Globals ====================
|
||||
|
||||
var (
|
||||
_ caddy.Module = (*Middleware)(nil)
|
||||
_ caddy.Provisioner = (*Middleware)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
|
||||
_ caddyfile.Unmarshaler = (*Middleware)(nil)
|
||||
_ caddy.Validator = (*Middleware)(nil)
|
||||
)
|
||||
|
||||
// Define custom types for rule hits
|
||||
type RuleID string
|
||||
type HitCount int
|
||||
|
||||
// ==================== Struct Definitions ====================
|
||||
type TrieNode struct {
|
||||
children map[byte]*TrieNode
|
||||
isLeaf bool
|
||||
}
|
||||
|
||||
func NewTrieNode() *TrieNode {
|
||||
return &TrieNode{
|
||||
children: make(map[byte]*TrieNode), // Initialize the map
|
||||
isLeaf: false,
|
||||
}
|
||||
}
|
||||
|
||||
type CIDRTrie struct {
|
||||
ipv4Root *TrieNode
|
||||
ipv6Root *TrieNode
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewCIDRTrie() *CIDRTrie {
|
||||
return &CIDRTrie{
|
||||
ipv4Root: NewTrieNode(), // Initialize with a new TrieNode
|
||||
ipv6Root: NewTrieNode(), // Initialize with a new TrieNode
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CIDRTrie) Insert(cidr string) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
ip, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ip.To4() != nil {
|
||||
// IPv4
|
||||
return t.insertIPv4(ipNet)
|
||||
} else {
|
||||
// IPv6
|
||||
return t.insertIPv6(ipNet)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CIDRTrie) Contains(ipStr string) bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ip.To4() != nil {
|
||||
// IPv4
|
||||
return t.containsIPv4(ip)
|
||||
} else {
|
||||
// IPv6
|
||||
return t.containsIPv6(ip)
|
||||
}
|
||||
}
|
||||
type (
|
||||
RuleID string
|
||||
HitCount int
|
||||
)
|
||||
|
||||
// RuleCache caches compiled regex patterns for rules.
|
||||
type RuleCache struct {
|
||||
@@ -105,6 +46,14 @@ type CountryAccessFilter struct {
|
||||
geoIP *maxminddb.Reader `json:"-"` // Explicitly mark as not serialized
|
||||
}
|
||||
|
||||
// ASNAccessFilter struct
|
||||
type ASNAccessFilter struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
BlockedASNs []string `json:"blocked_asns"`
|
||||
GeoIPDBPath string `json:"geoip_db_path"`
|
||||
geoIP *maxminddb.Reader `json:"-"` // Explicitly mark as not serialized
|
||||
}
|
||||
|
||||
// GeoIPRecord struct
|
||||
type GeoIPRecord struct {
|
||||
Country struct {
|
||||
@@ -112,6 +61,12 @@ type GeoIPRecord struct {
|
||||
} `maxminddb:"country"`
|
||||
}
|
||||
|
||||
// ASNRecord struct
|
||||
type ASNRecord struct {
|
||||
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
|
||||
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
|
||||
}
|
||||
|
||||
// Rule struct
|
||||
type Rule struct {
|
||||
ID string `json:"id"`
|
||||
@@ -120,7 +75,7 @@ type Rule struct {
|
||||
Targets []string `json:"targets"`
|
||||
Severity string `json:"severity"` // Used for logging only
|
||||
Score int `json:"score"`
|
||||
Action string `json:"mode"` // Determines the action (block/log)
|
||||
Action string `json:"mode"` // CRITICAL FIX: This should map to the "mode" field in JSON
|
||||
Description string `json:"description"`
|
||||
regex *regexp.Regexp
|
||||
Priority int // New field for rule priority
|
||||
@@ -141,7 +96,20 @@ type WAFState struct {
|
||||
ResponseWritten bool
|
||||
}
|
||||
|
||||
// Middleware struct
|
||||
// Middleware is the main WAF middleware struct that implements Caddy's
|
||||
// Module, Provisioner, Validator, and MiddlewareHandler interfaces.
|
||||
//
|
||||
// It provides comprehensive web application firewall functionality including:
|
||||
// - Rule-based request filtering
|
||||
// - IP and DNS blacklisting
|
||||
// - Geographic access control
|
||||
// - Rate limiting
|
||||
// - Anomaly detection
|
||||
// - Custom response handling
|
||||
// - Real-time metrics and monitoring
|
||||
//
|
||||
// The middleware can be configured via Caddyfile or JSON and integrates
|
||||
// seamlessly into Caddy's request processing pipeline.
|
||||
type Middleware struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
@@ -149,10 +117,11 @@ type Middleware struct {
|
||||
IPBlacklistFile string `json:"ip_blacklist_file"`
|
||||
DNSBlacklistFile string `json:"dns_blacklist_file"`
|
||||
AnomalyThreshold int `json:"anomaly_threshold"`
|
||||
CountryBlock CountryAccessFilter `json:"country_block"`
|
||||
CountryBlacklist CountryAccessFilter `json:"country_blacklist"`
|
||||
CountryWhitelist CountryAccessFilter `json:"country_whitelist"`
|
||||
BlockASNs ASNAccessFilter `json:"block_asns"`
|
||||
Rules map[int][]Rule `json:"-"`
|
||||
ipBlacklist *CIDRTrie `json:"-"` // Changed to CIDRTrie
|
||||
ipBlacklist *iptrie.Trie `json:"-"`
|
||||
dnsBlacklist map[string]struct{} `json:"-"` // Changed to map[string]struct{}
|
||||
logger *zap.Logger
|
||||
LogSeverity string `json:"log_severity,omitempty"`
|
||||
@@ -165,8 +134,10 @@ type Middleware struct {
|
||||
|
||||
CustomResponses map[int]CustomBlockResponse `json:"custom_responses,omitempty"`
|
||||
LogFilePath string
|
||||
LogBuffer int `json:"log_buffer,omitempty"` // Add the LogBuffer field
|
||||
RedactSensitiveData bool `json:"redact_sensitive_data,omitempty"`
|
||||
LogBuffer int `json:"log_buffer,omitempty"` // Add the LogBuffer field
|
||||
RedactSensitiveData bool `json:"redact_sensitive_data,omitempty"`
|
||||
MaxRequestBodySize int64 `json:"max_request_body_size,omitempty"`
|
||||
GeoIPFailOpen bool `json:"geoip_fail_open,omitempty"`
|
||||
|
||||
ruleHits sync.Map `json:"-"`
|
||||
MetricsEndpoint string `json:"metrics_endpoint,omitempty"`
|
||||
@@ -229,85 +200,3 @@ func (rc *RuleCache) Set(ruleID string, regex *regexp.Regexp) {
|
||||
defer rc.mu.Unlock()
|
||||
rc.rules[ruleID] = regex
|
||||
}
|
||||
|
||||
func (t *CIDRTrie) insertIPv4(ipNet *net.IPNet) error {
|
||||
ip := ipNet.IP.To4()
|
||||
if ip == nil {
|
||||
return fmt.Errorf("invalid IPv4 address")
|
||||
}
|
||||
|
||||
mask, _ := ipNet.Mask.Size()
|
||||
node := t.ipv4Root
|
||||
|
||||
for i := 0; i < mask; i++ {
|
||||
bit := (ip[i/8] >> (7 - uint(i%8))) & 1
|
||||
if node.children[bit] == nil {
|
||||
node.children[bit] = NewTrieNode() // Initialize the child node
|
||||
}
|
||||
node = node.children[bit]
|
||||
}
|
||||
|
||||
node.isLeaf = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CIDRTrie) insertIPv6(ipNet *net.IPNet) error {
|
||||
ip := ipNet.IP.To16()
|
||||
if ip == nil {
|
||||
return fmt.Errorf("invalid IPv6 address")
|
||||
}
|
||||
|
||||
mask, _ := ipNet.Mask.Size()
|
||||
node := t.ipv6Root
|
||||
|
||||
for i := 0; i < mask; i++ {
|
||||
bit := (ip[i/8] >> (7 - uint(i%8))) & 1
|
||||
if node.children[bit] == nil {
|
||||
node.children[bit] = NewTrieNode() // Initialize the child node
|
||||
}
|
||||
node = node.children[bit]
|
||||
}
|
||||
|
||||
node.isLeaf = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CIDRTrie) containsIPv4(ip net.IP) bool {
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
node := t.ipv4Root
|
||||
for i := 0; i < len(ip)*8; i++ {
|
||||
bit := (ip[i/8] >> (7 - uint(i%8))) & 1
|
||||
if node.children[bit] == nil {
|
||||
return false
|
||||
}
|
||||
node = node.children[bit]
|
||||
if node.isLeaf {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return node.isLeaf
|
||||
}
|
||||
|
||||
func (t *CIDRTrie) containsIPv6(ip net.IP) bool {
|
||||
ip = ip.To16()
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
node := t.ipv6Root
|
||||
for i := 0; i < len(ip)*8; i++ {
|
||||
bit := (ip[i/8] >> (7 - uint(i%8))) & 1
|
||||
if node.children[bit] == nil {
|
||||
return false
|
||||
}
|
||||
node = node.children[bit]
|
||||
if node.isLeaf {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user