mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-08 17:48:44 +00:00
Compare commits
1 Commits
fix/pin-ve
...
Xe/documen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
462f692355 |
2
.github/actions/spelling/allow.txt
vendored
2
.github/actions/spelling/allow.txt
vendored
@@ -10,5 +10,3 @@ ABee
|
||||
tencent
|
||||
maintnotifications
|
||||
azurediamond
|
||||
cooldown
|
||||
verifyfcrdns
|
||||
|
||||
792
.github/actions/spelling/expect.txt
vendored
792
.github/actions/spelling/expect.txt
vendored
@@ -1,400 +1,392 @@
|
||||
|
||||
acs
|
||||
Actorified
|
||||
actorifiedstore
|
||||
actorify
|
||||
Aibrew
|
||||
alibaba
|
||||
alrest
|
||||
amazonbot
|
||||
anthro
|
||||
anubis
|
||||
anubistest
|
||||
apnic
|
||||
APNICRANDNETAU
|
||||
Applebot
|
||||
archlinux
|
||||
arpa
|
||||
asnc
|
||||
asnchecker
|
||||
asns
|
||||
aspirational
|
||||
atuin
|
||||
azuretools
|
||||
badregexes
|
||||
bbolt
|
||||
bdba
|
||||
berr
|
||||
bezier
|
||||
bingbot
|
||||
Bitcoin
|
||||
bitrate
|
||||
Bluesky
|
||||
blueskybot
|
||||
boi
|
||||
Bokm
|
||||
botnet
|
||||
botstopper
|
||||
BPort
|
||||
Brightbot
|
||||
broked
|
||||
buildah
|
||||
byteslice
|
||||
Bytespider
|
||||
cachebuster
|
||||
cachediptoasn
|
||||
Caddyfile
|
||||
caninetools
|
||||
Cardyb
|
||||
celchecker
|
||||
celphase
|
||||
cerr
|
||||
certresolver
|
||||
cespare
|
||||
CGNAT
|
||||
cgr
|
||||
chainguard
|
||||
chall
|
||||
challengemozilla
|
||||
challengetest
|
||||
checkpath
|
||||
checkresult
|
||||
chibi
|
||||
cidranger
|
||||
ckie
|
||||
cloudflare
|
||||
Codespaces
|
||||
confd
|
||||
connnection
|
||||
containerbuild
|
||||
containerregistry
|
||||
coreutils
|
||||
Cotoyogi
|
||||
Cromite
|
||||
crt
|
||||
Cscript
|
||||
daemonizing
|
||||
dayjob
|
||||
DDOS
|
||||
Debian
|
||||
debrpm
|
||||
decaymap
|
||||
devcontainers
|
||||
Diffbot
|
||||
discordapp
|
||||
discordbot
|
||||
distros
|
||||
dnf
|
||||
dnsbl
|
||||
dnserr
|
||||
DNSTTL
|
||||
domainhere
|
||||
dracula
|
||||
dronebl
|
||||
droneblresponse
|
||||
dropin
|
||||
dsilence
|
||||
duckduckbot
|
||||
eerror
|
||||
ellenjoe
|
||||
emacs
|
||||
enbyware
|
||||
etld
|
||||
everyones
|
||||
evilbot
|
||||
evilsite
|
||||
expressionorlist
|
||||
externalagent
|
||||
externalfetcher
|
||||
extldflags
|
||||
facebookgo
|
||||
Factset
|
||||
fahedouch
|
||||
fastcgi
|
||||
FCr
|
||||
fcrdns
|
||||
fediverse
|
||||
ffprobe
|
||||
financials
|
||||
finfos
|
||||
Firecrawl
|
||||
flagenv
|
||||
Fordola
|
||||
forgejo
|
||||
forwardauth
|
||||
fsys
|
||||
fullchain
|
||||
gaissmai
|
||||
Galvus
|
||||
geoip
|
||||
geoipchecker
|
||||
gha
|
||||
GHSA
|
||||
Ghz
|
||||
gipc
|
||||
gitea
|
||||
godotenv
|
||||
goland
|
||||
gomod
|
||||
goodbot
|
||||
googlebot
|
||||
gopsutil
|
||||
govulncheck
|
||||
goyaml
|
||||
GPG
|
||||
GPT
|
||||
gptbot
|
||||
Graphene
|
||||
grpcprom
|
||||
grw
|
||||
gzw
|
||||
Hashcash
|
||||
hashrate
|
||||
headermap
|
||||
healthcheck
|
||||
healthz
|
||||
hec
|
||||
helpdesk
|
||||
Hetzner
|
||||
hmc
|
||||
homelab
|
||||
hostable
|
||||
htmlc
|
||||
htmx
|
||||
httpdebug
|
||||
huawei
|
||||
hypertext
|
||||
iaskspider
|
||||
iaso
|
||||
iat
|
||||
ifm
|
||||
Imagesift
|
||||
imgproxy
|
||||
impressum
|
||||
inbox
|
||||
ingressed
|
||||
inp
|
||||
internets
|
||||
IPTo
|
||||
iptoasn
|
||||
isp
|
||||
iss
|
||||
isset
|
||||
ivh
|
||||
Jenomis
|
||||
JGit
|
||||
jhjj
|
||||
joho
|
||||
journalctl
|
||||
jshelter
|
||||
JWTs
|
||||
kagi
|
||||
kagibot
|
||||
Keyfunc
|
||||
keypair
|
||||
KHTML
|
||||
kinda
|
||||
KUBECONFIG
|
||||
lcj
|
||||
ldflags
|
||||
letsencrypt
|
||||
Lexentale
|
||||
lfc
|
||||
lgbt
|
||||
licend
|
||||
licstart
|
||||
lightpanda
|
||||
limsa
|
||||
Linting
|
||||
listor
|
||||
LLU
|
||||
loadbalancer
|
||||
lol
|
||||
lominsa
|
||||
maintainership
|
||||
malware
|
||||
mcr
|
||||
memes
|
||||
metarefresh
|
||||
metrix
|
||||
mimi
|
||||
Minfilia
|
||||
mistralai
|
||||
mnt
|
||||
Mojeek
|
||||
mojeekbot
|
||||
mozilla
|
||||
myclient
|
||||
mymaster
|
||||
mypass
|
||||
myuser
|
||||
nbf
|
||||
nepeat
|
||||
netsurf
|
||||
nginx
|
||||
nicksnyder
|
||||
nobots
|
||||
NONINFRINGEMENT
|
||||
nosleep
|
||||
nullglob
|
||||
oci
|
||||
OCOB
|
||||
ogtag
|
||||
oklch
|
||||
omgili
|
||||
omgilibot
|
||||
openai
|
||||
opendns
|
||||
opengraph
|
||||
openrc
|
||||
oswald
|
||||
pag
|
||||
palemoon
|
||||
Pangu
|
||||
parseable
|
||||
passthrough
|
||||
Patreon
|
||||
pgrep
|
||||
phrik
|
||||
pidfile
|
||||
pids
|
||||
pipefail
|
||||
pki
|
||||
podkova
|
||||
podman
|
||||
Postgre
|
||||
poststart
|
||||
prebaked
|
||||
privkey
|
||||
promauto
|
||||
promhttp
|
||||
proofofwork
|
||||
publicsuffix
|
||||
purejs
|
||||
pwcmd
|
||||
pwuser
|
||||
qualys
|
||||
qwant
|
||||
qwantbot
|
||||
rac
|
||||
rawler
|
||||
rcvar
|
||||
redhat
|
||||
redir
|
||||
redirectscheme
|
||||
refactors
|
||||
remoteip
|
||||
reputational
|
||||
risc
|
||||
ruleset
|
||||
runlevels
|
||||
RUnlock
|
||||
runtimedir
|
||||
runtimedirectory
|
||||
Ryzen
|
||||
sas
|
||||
sasl
|
||||
screenshots
|
||||
searchbot
|
||||
searx
|
||||
sebest
|
||||
secretplans
|
||||
Semrush
|
||||
Seo
|
||||
setsebool
|
||||
shellcheck
|
||||
shirou
|
||||
shopt
|
||||
Sidetrade
|
||||
simprint
|
||||
sitemap
|
||||
sls
|
||||
sni
|
||||
snipster
|
||||
Spambot
|
||||
sparkline
|
||||
spyderbot
|
||||
srv
|
||||
stackoverflow
|
||||
startprecmd
|
||||
stoppostcmd
|
||||
storetest
|
||||
subgrid
|
||||
subr
|
||||
subrequest
|
||||
SVCNAME
|
||||
tagline
|
||||
tarballs
|
||||
tarrif
|
||||
taviso
|
||||
tbn
|
||||
tbr
|
||||
techaro
|
||||
techarohq
|
||||
telegrambot
|
||||
templ
|
||||
templruntime
|
||||
testarea
|
||||
Thancred
|
||||
thoth
|
||||
thothmock
|
||||
Tik
|
||||
Timpibot
|
||||
TLog
|
||||
traefik
|
||||
trunc
|
||||
uberspace
|
||||
Unbreak
|
||||
unbreakdocker
|
||||
unifiedjs
|
||||
unmarshal
|
||||
unparseable
|
||||
uvx
|
||||
UXP
|
||||
valkey
|
||||
Varis
|
||||
Velen
|
||||
vendored
|
||||
verify
|
||||
vhosts
|
||||
vkbot
|
||||
VKE
|
||||
vnd
|
||||
VPS
|
||||
Vultr
|
||||
weblate
|
||||
webmaster
|
||||
webpage
|
||||
websecure
|
||||
websites
|
||||
Webzio
|
||||
whois
|
||||
wildbase
|
||||
withthothmock
|
||||
wolfbeast
|
||||
wordpress
|
||||
workaround
|
||||
workdir
|
||||
wpbot
|
||||
XCircle
|
||||
xeiaso
|
||||
xeserv
|
||||
xesite
|
||||
xess
|
||||
xff
|
||||
XForwarded
|
||||
XNG
|
||||
XOB
|
||||
XOriginal
|
||||
XReal
|
||||
yae
|
||||
YAMLTo
|
||||
Yda
|
||||
yeet
|
||||
yeetfile
|
||||
yourdomain
|
||||
yyz
|
||||
Zenos
|
||||
zizmor
|
||||
zombocom
|
||||
zos
|
||||
acs
|
||||
Actorified
|
||||
actorifiedstore
|
||||
actorify
|
||||
Aibrew
|
||||
alibaba
|
||||
alrest
|
||||
amazonbot
|
||||
anthro
|
||||
anubis
|
||||
anubistest
|
||||
apnic
|
||||
APNICRANDNETAU
|
||||
Applebot
|
||||
archlinux
|
||||
asnc
|
||||
asnchecker
|
||||
asns
|
||||
aspirational
|
||||
atuin
|
||||
azuretools
|
||||
badregexes
|
||||
bbolt
|
||||
bdba
|
||||
berr
|
||||
bezier
|
||||
bingbot
|
||||
Bitcoin
|
||||
bitrate
|
||||
Bluesky
|
||||
blueskybot
|
||||
boi
|
||||
Bokm
|
||||
botnet
|
||||
botstopper
|
||||
BPort
|
||||
Brightbot
|
||||
broked
|
||||
buildah
|
||||
byteslice
|
||||
Bytespider
|
||||
cachebuster
|
||||
cachediptoasn
|
||||
Caddyfile
|
||||
caninetools
|
||||
Cardyb
|
||||
celchecker
|
||||
celphase
|
||||
cerr
|
||||
certresolver
|
||||
cespare
|
||||
CGNAT
|
||||
cgr
|
||||
chainguard
|
||||
chall
|
||||
challengemozilla
|
||||
challengetest
|
||||
checkpath
|
||||
checkresult
|
||||
chibi
|
||||
cidranger
|
||||
ckie
|
||||
cloudflare
|
||||
Codespaces
|
||||
confd
|
||||
connnection
|
||||
containerbuild
|
||||
containerregistry
|
||||
coreutils
|
||||
Cotoyogi
|
||||
Cromite
|
||||
crt
|
||||
Cscript
|
||||
daemonizing
|
||||
dayjob
|
||||
DDOS
|
||||
Debian
|
||||
debrpm
|
||||
decaymap
|
||||
devcontainers
|
||||
Diffbot
|
||||
discordapp
|
||||
discordbot
|
||||
distros
|
||||
dnf
|
||||
dnsbl
|
||||
dnserr
|
||||
domainhere
|
||||
dracula
|
||||
dronebl
|
||||
droneblresponse
|
||||
dropin
|
||||
dsilence
|
||||
duckduckbot
|
||||
eerror
|
||||
ellenjoe
|
||||
emacs
|
||||
enbyware
|
||||
etld
|
||||
everyones
|
||||
evilbot
|
||||
evilsite
|
||||
expressionorlist
|
||||
externalagent
|
||||
externalfetcher
|
||||
extldflags
|
||||
facebookgo
|
||||
Factset
|
||||
fahedouch
|
||||
fastcgi
|
||||
fediverse
|
||||
ffprobe
|
||||
financials
|
||||
finfos
|
||||
Firecrawl
|
||||
flagenv
|
||||
Fordola
|
||||
forgejo
|
||||
forwardauth
|
||||
fsys
|
||||
fullchain
|
||||
gaissmai
|
||||
Galvus
|
||||
geoip
|
||||
geoipchecker
|
||||
gha
|
||||
GHSA
|
||||
Ghz
|
||||
gipc
|
||||
gitea
|
||||
godotenv
|
||||
goland
|
||||
gomod
|
||||
goodbot
|
||||
googlebot
|
||||
gopsutil
|
||||
govulncheck
|
||||
goyaml
|
||||
GPG
|
||||
GPT
|
||||
gptbot
|
||||
Graphene
|
||||
grpcprom
|
||||
grw
|
||||
gzw
|
||||
Hashcash
|
||||
hashrate
|
||||
headermap
|
||||
healthcheck
|
||||
healthz
|
||||
hec
|
||||
helpdesk
|
||||
Hetzner
|
||||
hmc
|
||||
homelab
|
||||
hostable
|
||||
htmlc
|
||||
htmx
|
||||
httpdebug
|
||||
Huawei
|
||||
huawei
|
||||
hypertext
|
||||
iaskspider
|
||||
iaso
|
||||
iat
|
||||
ifm
|
||||
Imagesift
|
||||
imgproxy
|
||||
impressum
|
||||
inbox
|
||||
ingressed
|
||||
inp
|
||||
internets
|
||||
IPTo
|
||||
iptoasn
|
||||
isp
|
||||
iss
|
||||
isset
|
||||
ivh
|
||||
Jenomis
|
||||
JGit
|
||||
jhjj
|
||||
joho
|
||||
journalctl
|
||||
jshelter
|
||||
JWTs
|
||||
kagi
|
||||
kagibot
|
||||
Keyfunc
|
||||
keypair
|
||||
KHTML
|
||||
kinda
|
||||
KUBECONFIG
|
||||
lcj
|
||||
ldflags
|
||||
letsencrypt
|
||||
Lexentale
|
||||
lfc
|
||||
lgbt
|
||||
licend
|
||||
licstart
|
||||
lightpanda
|
||||
limsa
|
||||
Linting
|
||||
listor
|
||||
LLU
|
||||
loadbalancer
|
||||
lol
|
||||
lominsa
|
||||
maintainership
|
||||
malware
|
||||
mcr
|
||||
memes
|
||||
metarefresh
|
||||
metrix
|
||||
mimi
|
||||
Minfilia
|
||||
mistralai
|
||||
mnt
|
||||
Mojeek
|
||||
mojeekbot
|
||||
mozilla
|
||||
myclient
|
||||
mymaster
|
||||
mypass
|
||||
myuser
|
||||
nbf
|
||||
nepeat
|
||||
netsurf
|
||||
nginx
|
||||
nicksnyder
|
||||
nobots
|
||||
NONINFRINGEMENT
|
||||
nosleep
|
||||
nullglob
|
||||
oci
|
||||
OCOB
|
||||
ogtag
|
||||
oklch
|
||||
omgili
|
||||
omgilibot
|
||||
openai
|
||||
opengraph
|
||||
openrc
|
||||
oswald
|
||||
pag
|
||||
palemoon
|
||||
Pangu
|
||||
parseable
|
||||
passthrough
|
||||
Patreon
|
||||
pgrep
|
||||
phrik
|
||||
pidfile
|
||||
pids
|
||||
pipefail
|
||||
pki
|
||||
podkova
|
||||
podman
|
||||
Postgre
|
||||
poststart
|
||||
prebaked
|
||||
privkey
|
||||
promauto
|
||||
promhttp
|
||||
proofofwork
|
||||
publicsuffix
|
||||
purejs
|
||||
pwcmd
|
||||
pwuser
|
||||
qualys
|
||||
qwant
|
||||
qwantbot
|
||||
rac
|
||||
rawler
|
||||
rcvar
|
||||
redhat
|
||||
redir
|
||||
redirectscheme
|
||||
refactors
|
||||
remoteip
|
||||
reputational
|
||||
risc
|
||||
ruleset
|
||||
runlevels
|
||||
RUnlock
|
||||
runtimedir
|
||||
runtimedirectory
|
||||
Ryzen
|
||||
sas
|
||||
sasl
|
||||
screenshots
|
||||
searchbot
|
||||
searx
|
||||
sebest
|
||||
secretplans
|
||||
Semrush
|
||||
Seo
|
||||
setsebool
|
||||
shellcheck
|
||||
shirou
|
||||
shopt
|
||||
Sidetrade
|
||||
simprint
|
||||
sitemap
|
||||
sls
|
||||
sni
|
||||
Spambot
|
||||
sparkline
|
||||
spyderbot
|
||||
srv
|
||||
stackoverflow
|
||||
startprecmd
|
||||
stoppostcmd
|
||||
storetest
|
||||
subgrid
|
||||
subr
|
||||
subrequest
|
||||
SVCNAME
|
||||
tagline
|
||||
tarballs
|
||||
tarrif
|
||||
taviso
|
||||
tbn
|
||||
tbr
|
||||
techaro
|
||||
techarohq
|
||||
templ
|
||||
templruntime
|
||||
testarea
|
||||
Thancred
|
||||
thoth
|
||||
thothmock
|
||||
Tik
|
||||
Timpibot
|
||||
TLog
|
||||
traefik
|
||||
trunc
|
||||
uberspace
|
||||
Unbreak
|
||||
unbreakdocker
|
||||
unifiedjs
|
||||
unmarshal
|
||||
unparseable
|
||||
uvx
|
||||
UXP
|
||||
valkey
|
||||
Varis
|
||||
Velen
|
||||
vendored
|
||||
vhosts
|
||||
VKE
|
||||
vnd
|
||||
VPS
|
||||
Vultr
|
||||
weblate
|
||||
webmaster
|
||||
webpage
|
||||
websecure
|
||||
websites
|
||||
Webzio
|
||||
whois
|
||||
wildbase
|
||||
withthothmock
|
||||
wolfbeast
|
||||
wordpress
|
||||
Workaround
|
||||
workaround
|
||||
workdir
|
||||
wpbot
|
||||
XCircle
|
||||
xeiaso
|
||||
xeserv
|
||||
xesite
|
||||
xess
|
||||
xff
|
||||
XForwarded
|
||||
XNG
|
||||
XOB
|
||||
XOriginal
|
||||
XReal
|
||||
yae
|
||||
YAMLTo
|
||||
Yda
|
||||
yeet
|
||||
yeetfile
|
||||
yourdomain
|
||||
yyz
|
||||
Zenos
|
||||
zizmor
|
||||
zombocom
|
||||
zos
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -8,8 +8,6 @@ updates:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
@@ -19,8 +17,6 @@ updates:
|
||||
gomod:
|
||||
patterns:
|
||||
- "*"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
@@ -30,5 +26,3 @@ updates:
|
||||
npm:
|
||||
patterns:
|
||||
- "*"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
5
.github/workflows/asset-verification.yml
vendored
5
.github/workflows/asset-verification.yml
vendored
@@ -24,10 +24,11 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24.11.0'
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- name: install node deps
|
||||
run: |
|
||||
|
||||
5
.github/workflows/docker-pr.yml
vendored
5
.github/workflows/docker-pr.yml
vendored
@@ -28,10 +28,11 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24.11.0'
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
||||
|
||||
|
||||
5
.github/workflows/docker.yml
vendored
5
.github/workflows/docker.yml
vendored
@@ -38,10 +38,11 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24.11.0'
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
||||
|
||||
|
||||
4
.github/workflows/docs-deploy.yml
vendored
4
.github/workflows/docs-deploy.yml
vendored
@@ -53,14 +53,14 @@ jobs:
|
||||
push: true
|
||||
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@1d2c1e96fe0ae23b0c95ee8240ae151b1e638c23 # v1.34.2
|
||||
uses: actions-hub/kubectl@f14933a23bc8c582b5aa7d108defd8e2cb9fa86d # v1.34.1
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
args: apply -k docs/manifest
|
||||
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@1d2c1e96fe0ae23b0c95ee8240ae151b1e638c23 # v1.34.2
|
||||
uses: actions-hub/kubectl@f14933a23bc8c582b5aa7d108defd8e2cb9fa86d # v1.34.1
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
|
||||
4
.github/workflows/go-mod-tidy-check.yml
vendored
4
.github/workflows/go-mod-tidy-check.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- name: Check go.mod and go.sum in main directory
|
||||
run: |
|
||||
@@ -73,4 +73,4 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SUCCESS: go.mod and go.sum in test directory are tidy"
|
||||
echo "SUCCESS: go.mod and go.sum in test directory are tidy"
|
||||
5
.github/workflows/go.yml
vendored
5
.github/workflows/go.yml
vendored
@@ -26,10 +26,11 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24.11.0'
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
|
||||
5
.github/workflows/package-builds-stable.yml
vendored
5
.github/workflows/package-builds-stable.yml
vendored
@@ -27,10 +27,11 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24.11.0'
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- name: install node deps
|
||||
run: |
|
||||
|
||||
@@ -28,10 +28,11 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24.11.0'
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- name: install node deps
|
||||
run: |
|
||||
|
||||
5
.github/workflows/smoke-tests.yml
vendored
5
.github/workflows/smoke-tests.yml
vendored
@@ -35,10 +35,11 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24.11.0'
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
||||
|
||||
|
||||
2
.github/workflows/ssh-ci.yml
vendored
2
.github/workflows/ssh-ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
go-version: stable
|
||||
|
||||
- name: Run CI
|
||||
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}
|
||||
|
||||
@@ -50,7 +50,8 @@ bots:
|
||||
# user_agent_regex: (?i:bot|crawler)
|
||||
# action: CHALLENGE
|
||||
# challenge:
|
||||
# difficulty: 16 # impossible
|
||||
# difficulty: 16 # impossible
|
||||
# report_as: 4 # lie to the operator
|
||||
# algorithm: slow # intentionally waste CPU cycles and time
|
||||
|
||||
# Requires a subscription to Thoth to use, see
|
||||
@@ -248,6 +249,7 @@ thresholds:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
|
||||
algorithm: metarefresh
|
||||
difficulty: 1
|
||||
report_as: 1
|
||||
# For clients that are browser-like but have either gained points from custom rules or
|
||||
# report as a standard browser.
|
||||
- name: moderate-suspicion
|
||||
@@ -260,6 +262,7 @@ thresholds:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||
algorithm: fast
|
||||
difficulty: 2 # two leading zeros, very fast for most clients
|
||||
report_as: 2
|
||||
- name: mild-proof-of-work
|
||||
expression:
|
||||
all:
|
||||
@@ -270,6 +273,7 @@ thresholds:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||
algorithm: fast
|
||||
difficulty: 4
|
||||
report_as: 4
|
||||
# For clients that are browser like and have gained many points from custom rules
|
||||
- name: extreme-suspicion
|
||||
expression: weight >= 30
|
||||
@@ -278,3 +282,4 @@ thresholds:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||
algorithm: fast
|
||||
difficulty: 6
|
||||
report_as: 6
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
- name: telegrambot
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- userAgent.matches("TelegramBot")
|
||||
- verifyFCrDNS(remoteAddress, "ptr\\.telegram\\.org$")
|
||||
@@ -1,6 +0,0 @@
|
||||
- name: vkbot
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- userAgent.matches("vkShare[^+]+\\+http\\://vk\\.com/dev/Share")
|
||||
- verifyFCrDNS(remoteAddress, "^snipster\\d+\\.go\\.mail\\.ru$")
|
||||
55
data/common/acts-like-browser.yaml
Normal file
55
data/common/acts-like-browser.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
# Assert behaviour that only genuine browsers display. This ensures that modern Chrome
|
||||
# or Firefox versions will get through without a challenge.
|
||||
#
|
||||
# These rules have been known to be bypassed by some of the worst automated scrapers.
|
||||
# Use at your own risk.
|
||||
|
||||
- name: realistic-browser-catchall
|
||||
expression:
|
||||
all:
|
||||
- '"User-Agent" in headers'
|
||||
- '( userAgent.contains("Firefox") ) || ( userAgent.contains("Chrome") ) || ( userAgent.contains("Safari") )'
|
||||
- '"Accept" in headers'
|
||||
- '"Sec-Fetch-Dest" in headers'
|
||||
- '"Sec-Fetch-Mode" in headers'
|
||||
- '"Sec-Fetch-Site" in headers'
|
||||
- '"Accept-Encoding" in headers'
|
||||
- '( headers["Accept-Encoding"].contains("zstd") || headers["Accept-Encoding"].contains("br") )'
|
||||
- '"Accept-Language" in headers'
|
||||
action: WEIGH
|
||||
weight:
|
||||
adjust: -10
|
||||
|
||||
# The Upgrade-Insecure-Requests header is typically sent by browsers, but not always
|
||||
- name: upgrade-insecure-requests
|
||||
expression: '"Upgrade-Insecure-Requests" in headers'
|
||||
action: WEIGH
|
||||
weight:
|
||||
adjust: -2
|
||||
|
||||
# Chrome should behave like Chrome
|
||||
- name: chrome-is-proper
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("Chrome")
|
||||
- '"Sec-Ch-Ua" in headers'
|
||||
- 'headers["Sec-Ch-Ua"].contains("Chromium")'
|
||||
- '"Sec-Ch-Ua-Mobile" in headers'
|
||||
- '"Sec-Ch-Ua-Platform" in headers'
|
||||
action: WEIGH
|
||||
weight:
|
||||
adjust: -5
|
||||
|
||||
- name: should-have-accept
|
||||
expression: '!("Accept" in headers)'
|
||||
action: WEIGH
|
||||
weight:
|
||||
adjust: 5
|
||||
|
||||
# Generic catchall rule
|
||||
- name: generic-browser
|
||||
user_agent_regex: >-
|
||||
Mozilla|Opera
|
||||
action: WEIGH
|
||||
weight:
|
||||
adjust: 10
|
||||
@@ -8,4 +8,3 @@
|
||||
- import: (data)/crawlers/marginalia.yaml
|
||||
- import: (data)/crawlers/mojeekbot.yaml
|
||||
- import: (data)/crawlers/commoncrawl.yaml
|
||||
- import: (data)/crawlers/yandexbot.yaml
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
- name: yandexbot
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- userAgent.matches("\\+http\\://yandex\\.com/bots")
|
||||
- verifyFCrDNS(remoteAddress, "^.*\\.yandex\\.(ru|com|net)$")
|
||||
@@ -35,6 +35,7 @@
|
||||
# action: CHALLENGE
|
||||
# challenge:
|
||||
# difficulty: 16 # impossible
|
||||
# report_as: 4 # lie to the operator
|
||||
# algorithm: slow # intentionally waste CPU cycles and time
|
||||
|
||||
# Requires a subscription to Thoth to use, see
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
- import: (data)/clients/telegram-preview.yaml
|
||||
- import: (data)/clients/vk-preview.yaml
|
||||
@@ -24,42 +24,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Open Graph passthrough now reuses the configured target Host/SNI/TLS settings, so metadata fetches succeed when the upstream certificate differs from the public domain. ([1283](https://github.com/TecharoHQ/anubis/pull/1283))
|
||||
- Stabilize the CVE-2025-24369 regression test by always submitting an invalid proof instead of relying on random POW failures.
|
||||
|
||||
### Deprecate `report_as` in challenge configuration
|
||||
|
||||
Previously Anubis let you lie to users about the difficulty of a challenge to interfere with operators of malicious scrapers as a psychological attack:
|
||||
|
||||
```yaml
|
||||
bots:
|
||||
# Punish any bot with "bot" in the user-agent string
|
||||
# This is known to have a high false-positive rate, use at your own risk
|
||||
- name: generic-bot-catchall
|
||||
user_agent_regex: (?i:bot|crawler)
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
difficulty: 16 # impossible
|
||||
report_as: 4 # lie to the operator
|
||||
algorithm: slow # intentionally waste CPU cycles and time
|
||||
```
|
||||
|
||||
This has turned out to be a bad idea because it has caused massive user experience problems and has been removed. If you are using this setting, you will get a warning in your logs like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"time": "2025-11-25T23:10:31.092201549-05:00",
|
||||
"level": "WARN",
|
||||
"source": {
|
||||
"function": "github.com/TecharoHQ/anubis/lib/policy.ParseConfig",
|
||||
"file": "/home/xe/code/TecharoHQ/anubis/lib/policy/policy.go",
|
||||
"line": 201
|
||||
},
|
||||
"msg": "use of deprecated report_as setting detected, please remove this from your policy file when possible",
|
||||
"at": "config-validate",
|
||||
"name": "mild-suspicion"
|
||||
}
|
||||
```
|
||||
|
||||
To remove this warning, remove this setting from your policy file.
|
||||
|
||||
### Logging customization
|
||||
|
||||
Anubis now supports the ability to log to multiple backends ("sinks"). This allows you to have Anubis [log to a file](./admin/policies.mdx#file-sink) instead of just logging to standard out. You can also customize the [logging level](./admin/policies.mdx#log-levels) in the policy file:
|
||||
@@ -80,31 +44,6 @@ logging:
|
||||
|
||||
Additionally, information about [how Anubis uses each logging level](./admin/policies.mdx#log-levels) has been added to the documentation.
|
||||
|
||||
### DNS Features
|
||||
|
||||
- CEL expressions for:
|
||||
- FCrDNS checks
|
||||
- Forward DNS queries
|
||||
- Reverse DNS queries
|
||||
- `arpaReverseIP` to transform IPv4/6 addresses into ARPA reverse IP notation.
|
||||
- `regexSafe` to escape regex special characters (useful for including `remoteAddress` or headers in regular expressions).
|
||||
- DNS cache and other optimizations to minimize unnecessary DNS queries.
|
||||
|
||||
The DNS cache TTL can be changed in the bots config like this:
|
||||
```yaml
|
||||
dns_ttl:
|
||||
forward: 600
|
||||
reverse: 600
|
||||
```
|
||||
The default value for both forward and reverse queries is 300 seconds.
|
||||
|
||||
The `verifyFCrDNS` CEL function has two overloads:
|
||||
- `(addr)`
|
||||
Simply verifies that the remote side has PTR records pointing to the target address.
|
||||
- `(addr, ptrPattern)`
|
||||
Verifies that the remote side refers to a specific domain and that this domain points to the target IP.
|
||||
|
||||
|
||||
## v1.23.1: Lyse Hext - Echo 1
|
||||
|
||||
- Fix `SERVE_ROBOTS_TXT` setting after the double slash fix broke it.
|
||||
|
||||
@@ -12,6 +12,7 @@ To use it in your Anubis configuration:
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
difficulty: 1 # Number of seconds to wait before refreshing the page
|
||||
report_as: 4 # Unused by this challenge method
|
||||
algorithm: metarefresh # Specify a non-JS challenge method
|
||||
```
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ To use it in your Anubis configuration:
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
difficulty: 1 # Number of seconds to wait before refreshing the page
|
||||
report_as: 4 # Unused by this challenge method
|
||||
algorithm: preact
|
||||
```
|
||||
|
||||
|
||||
@@ -233,27 +233,6 @@ This is best applied when doing explicit block rules, eg:
|
||||
|
||||
It seems counter-intuitive to allow known bad clients through sometimes, but this allows you to confuse attackers by making Anubis' behavior random. Adjust the thresholds and numbers as facts and circumstances demand.
|
||||
|
||||
### `regexSafe`
|
||||
|
||||
Available in `bot` expressions.
|
||||
|
||||
```ts
|
||||
function regexSafe(input: string): string;
|
||||
```
|
||||
|
||||
`regexSafe` takes a string and escapes it for safe use inside of a regular expression. This is useful when you are creating regular expressions from headers or variables such as `remoteAddress`.
|
||||
|
||||
| Input | Output |
|
||||
| :------------------------ | :------------------------------ |
|
||||
| `regexSafe("1.2.3.4")` | `1\\.2\\.3\\.4` |
|
||||
| `regexSafe("techaro.lol")` | `techaro\\.lol` |
|
||||
| `regexSafe("star*")` | `star\\*` |
|
||||
| `regexSafe("plus+")` | `plus\\+` |
|
||||
| `regexSafe("{braces}")` | `\\{braces\\}` |
|
||||
| `regexSafe("start^")` | `start\\^` |
|
||||
| `regexSafe("back\\slash")` | `back\\\\slash` |
|
||||
| `regexSafe("dash-dash")` | `dash\\-dash` |
|
||||
|
||||
### `segments`
|
||||
|
||||
Available in `bot` expressions.
|
||||
@@ -287,99 +266,6 @@ This is useful if you want to write rules that allow requests that have no query
|
||||
- size(segments(path)) < 2
|
||||
```
|
||||
|
||||
### DNS Functions
|
||||
|
||||
Anubis can also perform DNS lookups as a part of its expression evaluation. This can be useful for doing things like checking for a valid [Forward-confirmed reverse DNS (FCrDNS)](https://en.wikipedia.org/wiki/Forward-confirmed_reverse_DNS) record.
|
||||
|
||||
#### `arpaReverseIP`
|
||||
|
||||
Available in `bot` expressions.
|
||||
|
||||
```ts
|
||||
function arpaReverseIP(ip: string): string;
|
||||
```
|
||||
|
||||
`arpaReverseIP` takes an IP address and returns its value in [ARPA notation](https://www.ietf.org/rfc/rfc2317.html). This can be useful when matching PTR record patterns.
|
||||
|
||||
| Input | Output |
|
||||
| :----------------------------- | :------------------------------------------------------------------- |
|
||||
| `arpaReverseIP("1.2.3.4")` | `4.3.2.1` |
|
||||
| `arpaReverseIP("2001:db8::1")` | `1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2` |
|
||||
|
||||
#### `lookupHost`
|
||||
|
||||
Available in `bot` expressions.
|
||||
|
||||
```ts
|
||||
function lookupHost(host: string): string[];
|
||||
```
|
||||
|
||||
`lookupHost` performs a DNS lookup for the given hostname and returns a list of IP addresses.
|
||||
|
||||
```yaml
|
||||
- name: cloudflare-ip-in-host-header
|
||||
action: DENY
|
||||
expression: '"104.16.0.0" in lookupHost(headers["Host"])'
|
||||
```
|
||||
|
||||
#### `reverseDNS`
|
||||
|
||||
Available in `bot` expressions.
|
||||
|
||||
```ts
|
||||
function reverseDNS(ip: string): string[];
|
||||
```
|
||||
|
||||
`reverseDNS` takes an IP address and returns the DNS names associated with it. This is useful when you want to check PTR records of an IP address.
|
||||
|
||||
```yaml
|
||||
- name: allow-googlebot
|
||||
action: ALLOW
|
||||
expression: 'reverseDNS(remoteAddress).endsWith(".googlebot.com")'
|
||||
```
|
||||
|
||||
::: warning
|
||||
|
||||
Do not use this for validating the legitimacy of an IP address. It is possible for DNS records to be out of date or otherwise manipulated. Use [`verifyFCrDNS`](#verifyfcrdns) instead for a more reliable result.
|
||||
|
||||
:::
|
||||
|
||||
#### `verifyFCrDNS`
|
||||
|
||||
Available in `bot` expressions.
|
||||
|
||||
```ts
|
||||
function verifyFCrDNS(ip: string): bool;
|
||||
function verifyFCrDNS(ip: string, pattern: string): bool;
|
||||
```
|
||||
|
||||
`verifyFCrDNS` checks if the reverse DNS of an IP address matches its forward DNS. This is a common technique to filter out spam and bot traffic. `verifyFCrDNS` comes in two forms:
|
||||
|
||||
- `verifyFCrDNS(remoteAddress)` will check that the reverse DNS of the remote address resolves back to the remote address. If no PTR records, returns true.
|
||||
- `verifyFCrDNS(remoteAddress, pattern)` will check that the reverse DNS of the remote address is matching with pattern and that name resolves back to the remote address.
|
||||
|
||||
This is best used in rules like this:
|
||||
|
||||
```yaml
|
||||
- name: require-fcrdns-for-post
|
||||
action: DENY
|
||||
expression:
|
||||
all:
|
||||
- method == "POST"
|
||||
- "!verifyFCrDNS(remoteAddress)"
|
||||
```
|
||||
|
||||
Here is an another example that allows requests from telegram:
|
||||
|
||||
```yaml
|
||||
- name: telegrambot
|
||||
action: ALLOW
|
||||
expression:
|
||||
all:
|
||||
- userAgent.matches("TelegramBot")
|
||||
- verifyFCrDNS(remoteAddress, "ptr\\.telegram\\.org$")
|
||||
```
|
||||
|
||||
## Life advice
|
||||
|
||||
Expressions are very powerful. This is a benefit and a burden. If you are not careful with your expression targeting, you will be liable to get yourself into trouble. If you are at all in doubt, throw a `CHALLENGE` over a `DENY`. Legitimate users can easily work around a `CHALLENGE` result with a [proof of work challenge](../../design/why-proof-of-work.mdx). Bots are less likely to be able to do this.
|
||||
|
||||
@@ -13,6 +13,8 @@ bots:
|
||||
- # This correlates to data/bots/ai-catchall.yaml in the source tree
|
||||
import: (data)/bots/ai-catchall.yaml
|
||||
- import: (data)/bots/cloudflare-workers.yaml
|
||||
# Import all the rules in the default configuration
|
||||
- import: (data)/meta/default-config.yaml
|
||||
```
|
||||
|
||||
Of note, a bot rule can either have inline bot configuration or import a bot config snippet. You cannot do both in a single bot rule.
|
||||
@@ -35,6 +37,33 @@ config.BotOrImport: rule definition is invalid, you must set either bot rules or
|
||||
|
||||
Paths can either be prefixed with `(data)` to import from the [the data folder in the Anubis source tree](https://github.com/TecharoHQ/anubis/tree/main/data) or anywhere on the filesystem. If you don't have access to the Anubis source tree, check /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
|
||||
|
||||
## Importing the default configuration
|
||||
|
||||
If you want to base your configuration off of the default configuration, import `(data)/meta/default-config.yaml`:
|
||||
|
||||
```yaml
|
||||
bots:
|
||||
- import: (data)/meta/default-config.yaml
|
||||
# Write your rules here
|
||||
```
|
||||
|
||||
This will keep your configuration up to date as Anubis adapts to emerging threats.
|
||||
|
||||
## How do I exempt most modern browsers from Anubis challenges?
|
||||
|
||||
If you want to exempt most modern browsers from Anubis challenges, import `(data)/common/acts-like-browser.yaml`:
|
||||
|
||||
```yaml
|
||||
bots:
|
||||
- import: (data)/meta/default-config.yaml
|
||||
- import: (data)/common/acts-like-browser.yaml
|
||||
# Write your rules here
|
||||
```
|
||||
|
||||
These rules will allow traffic that "looks like" it's from a modern copy of Edge, Safari, Chrome, or Firefox. These rules used to be enabled by default, however user reports have suggested that AI scraper bots have adapted to conform to these rules to scrape without regard for the infrastructure they are attacking.
|
||||
|
||||
Use these rules at your own risk.
|
||||
|
||||
## Importing from imports
|
||||
|
||||
You can also import from an imported file in case you want to import an entire folder of rules at once.
|
||||
|
||||
@@ -156,68 +156,3 @@ server {
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Caddy
|
||||
|
||||
Anubis can be used with the [`forward_auth`](https://caddyserver.com/docs/caddyfile/directives/forward_auth) directive in Caddy.
|
||||
|
||||
First, the `TARGET` environment variable in Anubis must be set to a space, eg:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="env-file" label="Environment file" default>
|
||||
|
||||
```shell
|
||||
# anubis.env
|
||||
|
||||
TARGET=" "
|
||||
# ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
```yaml
|
||||
services:
|
||||
anubis-caddy:
|
||||
image: ghcr.io/techarohq/anubis:latest
|
||||
environment:
|
||||
TARGET: " "
|
||||
# ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="k8s" label="Kubernetes">
|
||||
|
||||
Inside your Deployment, StatefulSet, or Pod:
|
||||
|
||||
```yaml
|
||||
- name: anubis
|
||||
image: ghcr.io/techarohq/anubis:latest
|
||||
env:
|
||||
- name: TARGET
|
||||
value: " "
|
||||
# ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Then configure the necessary directives in your site block:
|
||||
|
||||
```caddy
|
||||
route {
|
||||
# Assumption: Anubis is running in the same network namespace as
|
||||
# caddy on localhost TCP port 8923
|
||||
reverse_proxy /.within.website/* 127.0.0.1:8923
|
||||
forward_auth 127.0.0.1:8923 {
|
||||
uri /.within.website/x/cmd/anubis/api/check
|
||||
trusted_proxies private_ranges
|
||||
@unauthorized status 401
|
||||
handle_response @unauthorized {
|
||||
redir * /.within.website/?redir={uri} 307
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use this for multiple sites, you can create a [snippet](https://caddyserver.com/docs/caddyfile/concepts#snippets) and import it in multiple site blocks.
|
||||
|
||||
@@ -41,6 +41,7 @@ thresholds:
|
||||
challenge:
|
||||
algorithm: metarefresh
|
||||
difficulty: 1
|
||||
report_as: 1
|
||||
|
||||
- name: moderate-suspicion
|
||||
expression:
|
||||
@@ -51,6 +52,7 @@ thresholds:
|
||||
challenge:
|
||||
algorithm: fast
|
||||
difficulty: 2
|
||||
report_as: 2
|
||||
|
||||
- name: extreme-suspicion
|
||||
expression: weight >= 20
|
||||
@@ -58,6 +60,7 @@ thresholds:
|
||||
challenge:
|
||||
algorithm: fast
|
||||
difficulty: 4
|
||||
report_as: 4
|
||||
```
|
||||
|
||||
This defines a suite of 4 thresholds:
|
||||
@@ -127,6 +130,7 @@ action: CHALLENGE
|
||||
challenge:
|
||||
algorithm: metarefresh
|
||||
difficulty: 1
|
||||
report_as: 1
|
||||
```
|
||||
|
||||
</td>
|
||||
|
||||
@@ -84,6 +84,7 @@ This rule has been known to have a high false positive rate in testing. Please u
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
difficulty: 16 # impossible
|
||||
report_as: 4 # lie to the operator
|
||||
algorithm: slow # intentionally waste CPU cycles and time
|
||||
```
|
||||
|
||||
@@ -92,6 +93,7 @@ Challenges can be configured with these settings:
|
||||
| Key | Example | Description |
|
||||
| :----------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `difficulty` | `4` | The challenge difficulty (number of leading zeros) for proof-of-work. See [Why does Anubis use Proof-of-Work?](/docs/design/why-proof-of-work) for more details. |
|
||||
| `report_as` | `4` | What difficulty the UI should report to the user. Useful for messing with industrial-scale scraping efforts. |
|
||||
| `algorithm` | `"fast"` | The challenge method to use. See [the list of challenge methods](./configuration/challenges/) for more information. |
|
||||
|
||||
### Remote IP based filtering
|
||||
|
||||
@@ -49,6 +49,7 @@ bots:
|
||||
# action: CHALLENGE
|
||||
# challenge:
|
||||
# difficulty: 16 # impossible
|
||||
# report_as: 4 # lie to the operator
|
||||
# algorithm: slow # intentionally waste CPU cycles and time
|
||||
|
||||
- name: rss-feed-blog
|
||||
@@ -104,6 +105,7 @@ thresholds:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
|
||||
algorithm: metarefresh
|
||||
difficulty: 1
|
||||
report_as: 1
|
||||
# For clients that are browser-like but have either gained points from custom rules or
|
||||
# report as a standard browser.
|
||||
- name: moderate-suspicion
|
||||
@@ -120,6 +122,7 @@ thresholds:
|
||||
# challenge data, and forwards that to the client.
|
||||
algorithm: preact
|
||||
difficulty: 1
|
||||
report_as: 1
|
||||
- name: mild-proof-of-work
|
||||
expression:
|
||||
all:
|
||||
@@ -130,6 +133,7 @@ thresholds:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||
algorithm: fast
|
||||
difficulty: 2 # two leading zeros, very fast for most clients
|
||||
report_as: 2
|
||||
# For clients that are browser like and have gained many points from custom rules
|
||||
- name: extreme-suspicion
|
||||
expression: weight >= 30
|
||||
@@ -138,6 +142,7 @@ thresholds:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||
algorithm: fast
|
||||
difficulty: 4
|
||||
report_as: 4
|
||||
|
||||
dnsbl: false
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/all"
|
||||
)
|
||||
|
||||
type DnsCache struct {
|
||||
forward store.JSON[[]string]
|
||||
reverse store.JSON[[]string]
|
||||
forwardTTL time.Duration
|
||||
reverseTTL time.Duration
|
||||
}
|
||||
|
||||
func NewDNSCache(forwardTTL int, reverseTTL int, backend store.Interface) *DnsCache {
|
||||
return &DnsCache{
|
||||
forward: store.JSON[[]string]{
|
||||
Underlying: backend,
|
||||
Prefix: "forwardDNS",
|
||||
},
|
||||
reverse: store.JSON[[]string]{
|
||||
Underlying: backend,
|
||||
Prefix: "reverseDNS",
|
||||
},
|
||||
forwardTTL: time.Duration(forwardTTL) * time.Second,
|
||||
reverseTTL: time.Duration(reverseTTL) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dns) getCachedForward(host string) ([]string, bool) {
|
||||
if d.cache == nil {
|
||||
return nil, false
|
||||
}
|
||||
if cached, err := d.cache.forward.Get(d.ctx, host); err == nil {
|
||||
slog.Debug("DNS: forward cache hit", "name", host, "ips", cached)
|
||||
return cached, true
|
||||
}
|
||||
slog.Debug("DNS: forward cache miss", "name", host)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (d *Dns) getCachedReverse(addr string) ([]string, bool) {
|
||||
if d.cache == nil {
|
||||
return nil, false
|
||||
}
|
||||
if cached, err := d.cache.reverse.Get(d.ctx, addr); err == nil {
|
||||
slog.Debug("DNS: reverse cache hit", "addr", addr, "names", cached)
|
||||
return cached, true
|
||||
}
|
||||
slog.Debug("DNS: reverse cache miss", "addr", addr)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (d *Dns) forwardCachePut(host string, entries []string) {
|
||||
if d.cache == nil {
|
||||
return
|
||||
}
|
||||
d.cache.forward.Set(d.ctx, host, entries, d.cache.forwardTTL)
|
||||
}
|
||||
|
||||
func (d *Dns) reverseCachePut(addr string, entries []string) {
|
||||
if d.cache == nil {
|
||||
return
|
||||
}
|
||||
d.cache.reverse.Set(d.ctx, addr, entries, d.cache.reverseTTL)
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
DNSLookupAddr = net.LookupAddr
|
||||
DNSLookupHost = net.LookupHost
|
||||
)
|
||||
|
||||
type Dns struct {
|
||||
cache *DnsCache
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cache *DnsCache) *Dns {
|
||||
return &Dns{
|
||||
cache: cache,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// ReverseDNS performs a reverse DNS lookup for the given IP address and trims the trailing dot from the results.
|
||||
func (d *Dns) ReverseDNS(addr string) ([]string, error) {
|
||||
slog.Debug("DNS: performing reverse lookup", "addr", addr)
|
||||
|
||||
if cached, ok := d.getCachedReverse(addr); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
names, err := DNSLookupAddr(addr)
|
||||
if err != nil {
|
||||
if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
|
||||
slog.Debug("DNS: no PTR record found", "addr", addr)
|
||||
return []string{}, nil
|
||||
}
|
||||
slog.Error("DNS: reverse lookup failed", "addr", addr, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.Debug("DNS: reverse lookup successful", "addr", addr, "names", names)
|
||||
|
||||
trimmedNames := make([]string, len(names))
|
||||
for i, name := range names {
|
||||
trimmedNames[i] = strings.TrimSuffix(name, ".")
|
||||
}
|
||||
d.reverseCachePut(addr, trimmedNames)
|
||||
|
||||
return trimmedNames, nil
|
||||
}
|
||||
|
||||
// LookupHost performs a forward DNS lookup for the given hostname.
|
||||
func (d *Dns) LookupHost(host string) ([]string, error) {
|
||||
slog.Debug("DNS: performing forward lookup", "host", host)
|
||||
|
||||
if cached, ok := d.getCachedForward(host); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
addrs, err := DNSLookupHost(host)
|
||||
if err != nil {
|
||||
if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
|
||||
slog.Debug("DNS: no A/AAAA record found", "host", host)
|
||||
return []string{}, nil
|
||||
}
|
||||
slog.Error("DNS: forward lookup failed", "host", host, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.Debug("DNS: forward lookup successful", "host", host, "addrs", addrs)
|
||||
d.forwardCachePut(host, addrs)
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// verifyFCrDNSInternal performs the second half of the FCrDNS check, using a
|
||||
// pre-fetched list of names to perform the forward lookups.
|
||||
func (d *Dns) verifyFCrDNSInternal(addr string, names []string) bool {
|
||||
for _, name := range names {
|
||||
if cached, err := d.LookupHost(name); err == nil {
|
||||
if slices.Contains(cached, addr) {
|
||||
slog.Info("DNS: forward lookup confirmed original IP", "name", name, "addr", addr)
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("DNS: could not confirm original IP in forward lookups", "addr", addr)
|
||||
return false
|
||||
}
|
||||
|
||||
// VerifyFCrDNS performs a forward-confirmed reverse DNS (FCrDNS) lookup for the given IP address,
|
||||
// optionally matching against a provided pattern.
|
||||
func (d *Dns) VerifyFCrDNS(addr string, pattern *string) bool {
|
||||
var patternVal string
|
||||
if pattern != nil {
|
||||
patternVal = *pattern
|
||||
}
|
||||
slog.Debug("DNS: performing FCrDNS lookup", "addr", addr, "pattern", patternVal)
|
||||
|
||||
names, err := d.ReverseDNS(addr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return pattern == nil // If no pattern specified, check is passed
|
||||
}
|
||||
|
||||
// If a pattern is provided, check for a match.
|
||||
if pattern != nil {
|
||||
anyNameMatched := false
|
||||
for _, name := range names {
|
||||
matched, err := regexp.MatchString(*pattern, name)
|
||||
if err != nil {
|
||||
slog.Error("DNS: verifyFCrDNS invalid regex pattern", "err", err)
|
||||
return false // Invalid pattern is a failure.
|
||||
}
|
||||
if matched {
|
||||
anyNameMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !anyNameMatched {
|
||||
slog.Debug("DNS: FCrDNS no PTR matches the pattern", "addr", addr, "pattern", *pattern)
|
||||
return false
|
||||
}
|
||||
slog.Debug("DNS: FCrDNS PTR matched pattern, proceeding with forward check", "addr", addr, "pattern", *pattern)
|
||||
}
|
||||
|
||||
// If we're here, either there was no pattern, or the pattern matched.
|
||||
// Proceed with the forward lookup confirmation.
|
||||
return d.verifyFCrDNSInternal(addr, names)
|
||||
}
|
||||
|
||||
// ArpaReverseIP performs translation from ip v4/v6 to arpa reverse notation
|
||||
func (d *Dns) ArpaReverseIP(addr string) (string, error) {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil {
|
||||
return addr, errors.New("invalid IP address")
|
||||
}
|
||||
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", ipv4[3], ipv4[2], ipv4[1], ipv4[0]), nil
|
||||
}
|
||||
|
||||
ipv6 := ip.To16()
|
||||
if ipv6 == nil {
|
||||
return addr, errors.New("invalid IPv6 address")
|
||||
}
|
||||
|
||||
hexBytes := make([]byte, hex.EncodedLen(len(ipv6)))
|
||||
hex.Encode(hexBytes, ipv6)
|
||||
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(hexBytes)*2 - 1)
|
||||
|
||||
for i := len(hexBytes) - 1; i >= 0; i-- {
|
||||
sb.WriteByte(hexBytes[i])
|
||||
if i > 0 {
|
||||
sb.WriteByte('.')
|
||||
}
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store/memory"
|
||||
)
|
||||
|
||||
// newTestDNS is a helper function to create a new Dns object with an in-memory cache for testing.
|
||||
func newTestDNS(forwardTTL int, reverseTTL int) *Dns {
|
||||
ctx := context.Background()
|
||||
memStore := memory.New(ctx)
|
||||
cache := NewDNSCache(forwardTTL, reverseTTL, memStore)
|
||||
return New(ctx, cache)
|
||||
}
|
||||
|
||||
// mockLookupAddr is a mock implementation of the net.LookupAddr function.
|
||||
func mockLookupAddr(addr string) ([]string, error) {
|
||||
switch addr {
|
||||
case "8.8.8.8":
|
||||
return []string{"dns.google."}, nil
|
||||
case "1.1.1.1":
|
||||
return []string{"one.one.one.one."}, nil
|
||||
case "208.67.222.222":
|
||||
return []string{"resolver1.opendns.com."}, nil
|
||||
case "9.9.9.9":
|
||||
return nil, &net.DNSError{Err: "no such host", Name: "9.9.9.9", IsNotFound: true}
|
||||
case "1.2.3.4":
|
||||
return nil, errors.New("unknown error")
|
||||
default:
|
||||
return nil, &net.DNSError{Err: "no such host", Name: addr, IsNotFound: true}
|
||||
}
|
||||
}
|
||||
|
||||
// mockLookupHost is a mock implementation of the net.LookupHost function.
|
||||
func mockLookupHost(host string) ([]string, error) {
|
||||
switch host {
|
||||
case "dns.google":
|
||||
return []string{"8.8.8.8", "8.8.4.4"}, nil
|
||||
case "one.one.one.one":
|
||||
return []string{"1.1.1.1", "1.0.0.1"}, nil
|
||||
case "resolver1.opendns.com":
|
||||
return []string{"208.67.222.222"}, nil
|
||||
case "example.com":
|
||||
return nil, &net.DNSError{Err: "no such host", Name: "example.com", IsNotFound: true}
|
||||
default:
|
||||
return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Before all tests
|
||||
originalLookupAddr := DNSLookupAddr
|
||||
originalLookupHost := DNSLookupHost
|
||||
|
||||
DNSLookupAddr = mockLookupAddr
|
||||
DNSLookupHost = mockLookupHost
|
||||
|
||||
// Run tests
|
||||
exitCode := m.Run()
|
||||
|
||||
// After all tests
|
||||
DNSLookupAddr = originalLookupAddr
|
||||
DNSLookupHost = originalLookupHost
|
||||
|
||||
// Exit
|
||||
if exitCode != 0 {
|
||||
panic(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDns_ArpaReverseIP(t *testing.T) {
|
||||
d := newTestDNS(0, 0)
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ipv4", "192.0.2.1", "1.2.0.192", false},
|
||||
{"ipv6", "2001:db8::1", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2", false},
|
||||
{"invalid ip", "invalid", "invalid", true},
|
||||
{"ipv4-mapped ipv6", "::ffff:192.0.2.1", "1.2.0.192", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := d.ArpaReverseIP(tt.ip)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ArpaReverseIP() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ArpaReverseIP() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDns_ReverseDNS(t *testing.T) {
|
||||
d := newTestDNS(1, 1) // short TTL for testing cache
|
||||
|
||||
// First call - cache miss
|
||||
t.Run("cache miss", func(t *testing.T) {
|
||||
got, err := d.ReverseDNS("8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatalf("ReverseDNS() error = %v", err)
|
||||
}
|
||||
want := []string{"dns.google"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ReverseDNS() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
// Second call - cache hit
|
||||
t.Run("cache hit", func(t *testing.T) {
|
||||
// Temporarily replace lookup function to ensure cache is used
|
||||
originalLookupAddr := DNSLookupAddr
|
||||
DNSLookupAddr = func(addr string) ([]string, error) {
|
||||
return nil, errors.New("should not be called")
|
||||
}
|
||||
defer func() { DNSLookupAddr = originalLookupAddr }()
|
||||
|
||||
got, err := d.ReverseDNS("8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatalf("ReverseDNS() error = %v", err)
|
||||
}
|
||||
want := []string{"dns.google"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ReverseDNS() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
// Test cache expiration
|
||||
t.Run("cache expiration", func(t *testing.T) {
|
||||
time.Sleep(2 * time.Second)
|
||||
// Now the cache should be expired
|
||||
// We expect the mock to be called again
|
||||
// To test this we will change the mock to return something different
|
||||
originalLookupAddr := DNSLookupAddr
|
||||
DNSLookupAddr = func(addr string) ([]string, error) {
|
||||
if addr == "8.8.8.8" {
|
||||
return []string{"expired.google."}, nil
|
||||
}
|
||||
return mockLookupAddr(addr)
|
||||
}
|
||||
defer func() { DNSLookupAddr = originalLookupAddr }()
|
||||
|
||||
got, err := d.ReverseDNS("8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatalf("ReverseDNS() error = %v", err)
|
||||
}
|
||||
want := []string{"expired.google"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ReverseDNS() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
// Test not found
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
got, err := d.ReverseDNS("9.9.9.9")
|
||||
if err != nil {
|
||||
t.Fatalf("ReverseDNS() error = %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("ReverseDNS() = %v, want empty slice", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDns_LookupHost(t *testing.T) {
|
||||
d := newTestDNS(1, 1)
|
||||
|
||||
t.Run("cache miss", func(t *testing.T) {
|
||||
got, err := d.LookupHost("dns.google")
|
||||
if err != nil {
|
||||
t.Fatalf("LookupHost() error = %v", err)
|
||||
}
|
||||
want := []string{"8.8.8.8", "8.8.4.4"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("LookupHost() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cache hit", func(t *testing.T) {
|
||||
originalLookupHost := DNSLookupHost
|
||||
DNSLookupHost = func(host string) ([]string, error) {
|
||||
return nil, errors.New("should not be called")
|
||||
}
|
||||
defer func() { DNSLookupHost = originalLookupHost }()
|
||||
|
||||
got, err := d.LookupHost("dns.google")
|
||||
if err != nil {
|
||||
t.Fatalf("LookupHost() error = %v", err)
|
||||
}
|
||||
want := []string{"8.8.8.8", "8.8.4.4"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("LookupHost() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cache expiration", func(t *testing.T) {
|
||||
time.Sleep(2 * time.Second)
|
||||
originalLookupHost := DNSLookupHost
|
||||
DNSLookupHost = func(host string) ([]string, error) {
|
||||
if host == "dns.google" {
|
||||
return []string{"9.9.9.9"}, nil
|
||||
}
|
||||
return mockLookupHost(host)
|
||||
}
|
||||
defer func() { DNSLookupHost = originalLookupHost }()
|
||||
|
||||
got, err := d.LookupHost("dns.google")
|
||||
if err != nil {
|
||||
t.Fatalf("LookupHost() error = %v", err)
|
||||
}
|
||||
want := []string{"9.9.9.9"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("LookupHost() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
got, err := d.LookupHost("example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("LookupHost() error = %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("LookupHost() = %v, want empty slice", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDns_VerifyFCrDNS(t *testing.T) {
|
||||
d := newTestDNS(1, 1)
|
||||
|
||||
// Helper to convert string to *string
|
||||
p := func(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
pattern *string
|
||||
want bool
|
||||
}{
|
||||
// Cases without pattern
|
||||
{"valid no pattern", "8.8.8.8", nil, true},
|
||||
{"valid partial no pattern", "1.1.1.1", nil, true},
|
||||
{"not found no pattern", "9.9.9.9", nil, true},
|
||||
{"unknown error no pattern", "1.2.3.4", nil, false},
|
||||
|
||||
// Cases with pattern
|
||||
{"valid match", "8.8.8.8", p(`.*\.google$`), true},
|
||||
{"valid no match", "8.8.8.8", p(`\.com$`), false},
|
||||
{"not found with pattern", "9.9.9.9", p(".*"), false},
|
||||
{"unknown error with pattern", "1.2.3.4", p(".*"), false},
|
||||
{"invalid pattern", "8.8.8.8", p(`[`), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := d.VerifyFCrDNS(tt.ip, tt.pattern); got != tt.want {
|
||||
t.Errorf("VerifyFCrDNS() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("reverse cache hit", func(t *testing.T) {
|
||||
// Prime the cache
|
||||
if got := d.VerifyFCrDNS("8.8.8.8", nil); got != true {
|
||||
t.Fatalf("VerifyFCrDNS() priming failed, got %v, want true", got)
|
||||
}
|
||||
|
||||
// Now test with a failing lookup to ensure cache is used
|
||||
originalLookupAddr := DNSLookupAddr
|
||||
DNSLookupAddr = func(addr string) ([]string, error) {
|
||||
return nil, errors.New("should not be called")
|
||||
}
|
||||
defer func() { DNSLookupAddr = originalLookupAddr }()
|
||||
|
||||
if got := d.VerifyFCrDNS("8.8.8.8", nil); got != true {
|
||||
t.Errorf("VerifyFCrDNS() = %v, want true", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("forward cache hit", func(t *testing.T) {
|
||||
// Prime the cache
|
||||
if got := d.VerifyFCrDNS("8.8.8.8", nil); got != true {
|
||||
t.Fatalf("VerifyFCrDNS() priming failed, got %v, want true", got)
|
||||
}
|
||||
|
||||
// Now test with a failing lookup to ensure cache is used
|
||||
originalLookupHost := DNSLookupHost
|
||||
DNSLookupHost = func(host string) ([]string, error) {
|
||||
return nil, errors.New("should not be called")
|
||||
}
|
||||
defer func() { DNSLookupHost = originalLookupHost }()
|
||||
|
||||
if got := d.VerifyFCrDNS("8.8.8.8", nil); got != true {
|
||||
t.Errorf("VerifyFCrDNS() = %v, want true", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -167,8 +167,8 @@ func (s *Server) hydrateChallengeRule(rule *policy.Bot, chall *challenge.Challen
|
||||
if rule.Challenge.Difficulty == 0 {
|
||||
rule.Challenge.Difficulty = chall.Difficulty
|
||||
}
|
||||
if rule.Challenge.ReportAs != 0 {
|
||||
s.logger.Warn("[DEPRECATION] the report_as field in this bot rule is deprecated, see https://github.com/TecharoHQ/anubis/issues/1310 for more information", "bot_name", rule.Name, "difficulty", rule.Challenge.Difficulty, "report_as", rule.Challenge.ReportAs)
|
||||
if rule.Challenge.ReportAs == 0 {
|
||||
rule.Challenge.ReportAs = chall.Difficulty
|
||||
}
|
||||
if rule.Challenge.Algorithm == "" {
|
||||
rule.Challenge.Algorithm = chall.Method
|
||||
@@ -648,6 +648,7 @@ func (s *Server) check(r *http.Request, lg *slog.Logger) (policy.CheckResult, *p
|
||||
return cr("default/allow", config.RuleAllow, weight), &policy.Bot{
|
||||
Challenge: &config.ChallengeRules{
|
||||
Difficulty: s.policy.DefaultDifficulty,
|
||||
ReportAs: s.policy.DefaultDifficulty,
|
||||
Algorithm: config.DefaultAlgorithm,
|
||||
},
|
||||
Rules: &checker.List{},
|
||||
|
||||
@@ -464,6 +464,10 @@ func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
||||
if bot.Challenge.Difficulty != i {
|
||||
t.Errorf("Challenge.Difficulty is wrong, wanted %d, got: %d", i, bot.Challenge.Difficulty)
|
||||
}
|
||||
|
||||
if bot.Challenge.ReportAs != i {
|
||||
t.Errorf("Challenge.ReportAs is wrong, wanted %d, got: %d", i, bot.Challenge.ReportAs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ func TestBasic(t *testing.T) {
|
||||
Challenge: &config.ChallengeRules{
|
||||
Algorithm: "fast",
|
||||
Difficulty: 0,
|
||||
ReportAs: 0,
|
||||
},
|
||||
}
|
||||
const challengeStr = "hunter"
|
||||
|
||||
@@ -332,7 +332,6 @@ type fileConfig struct {
|
||||
Thresholds []Threshold `json:"thresholds"`
|
||||
StatusCodes StatusCodes `json:"status_codes"`
|
||||
DNSBL bool `json:"dnsbl"`
|
||||
DNSTTL DnsTTL `json:"dns_ttl"`
|
||||
Logging *Logging `json:"logging"`
|
||||
}
|
||||
|
||||
@@ -388,10 +387,6 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
Challenge: http.StatusOK,
|
||||
Deny: http.StatusOK,
|
||||
},
|
||||
DNSTTL: DnsTTL{
|
||||
Forward: 300,
|
||||
Reverse: 300,
|
||||
},
|
||||
Store: &Store{
|
||||
Backend: "memory",
|
||||
},
|
||||
@@ -407,8 +402,7 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
}
|
||||
|
||||
result := &Config{
|
||||
DNSBL: c.DNSBL,
|
||||
DNSTTL: c.DNSTTL,
|
||||
DNSBL: c.DNSBL,
|
||||
OpenGraph: OpenGraph{
|
||||
Enabled: c.OpenGraph.Enabled,
|
||||
ConsiderHost: c.OpenGraph.ConsiderHost,
|
||||
@@ -475,29 +469,6 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type DnsTTL struct {
|
||||
Forward int `json:"forward"`
|
||||
Reverse int `json:"reverse"`
|
||||
}
|
||||
|
||||
func (sc DnsTTL) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if sc.Forward < 0 {
|
||||
errs = append(errs, fmt.Errorf("%w: forward TTL is %d", ErrStatusCodeNotValid, sc.Forward))
|
||||
}
|
||||
|
||||
if sc.Reverse < 0 {
|
||||
errs = append(errs, fmt.Errorf("%w: reverse TTL is %d", ErrStatusCodeNotValid, sc.Reverse))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("dns TTL values not valid:\n%w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Impressum *Impressum
|
||||
Store *Store
|
||||
@@ -507,7 +478,6 @@ type Config struct {
|
||||
StatusCodes StatusCodes
|
||||
Logging *Logging
|
||||
DNSBL bool
|
||||
DNSTTL DnsTTL
|
||||
}
|
||||
|
||||
func (c Config) Valid() error {
|
||||
|
||||
@@ -110,6 +110,7 @@ func TestBotValid(t *testing.T) {
|
||||
PathRegex: p("Mozilla"),
|
||||
Challenge: &ChallengeRules{
|
||||
Difficulty: -1,
|
||||
ReportAs: 4,
|
||||
Algorithm: "fast",
|
||||
},
|
||||
},
|
||||
@@ -123,6 +124,7 @@ func TestBotValid(t *testing.T) {
|
||||
PathRegex: p("Mozilla"),
|
||||
Challenge: &ChallengeRules{
|
||||
Difficulty: 420,
|
||||
ReportAs: 4,
|
||||
Algorithm: "fast",
|
||||
},
|
||||
},
|
||||
@@ -359,6 +361,7 @@ func TestBotConfigZero(t *testing.T) {
|
||||
|
||||
b.Challenge = &ChallengeRules{
|
||||
Difficulty: 4,
|
||||
ReportAs: 4,
|
||||
Algorithm: DefaultAlgorithm,
|
||||
}
|
||||
if b.Zero() {
|
||||
|
||||
8
lib/config/testdata/bad/dns-ttl-custom.yaml
vendored
8
lib/config/testdata/bad/dns-ttl-custom.yaml
vendored
@@ -1,8 +0,0 @@
|
||||
dns_ttl:
|
||||
forward: 60.0
|
||||
reverse: "600"
|
||||
|
||||
bots:
|
||||
- name: "test"
|
||||
user_agent_regex: ".*"
|
||||
action: "DENY"
|
||||
8
lib/config/testdata/good/dns-ttl-custom.yaml
vendored
8
lib/config/testdata/good/dns-ttl-custom.yaml
vendored
@@ -1,8 +0,0 @@
|
||||
dns_ttl:
|
||||
forward: 600
|
||||
reverse: 600
|
||||
|
||||
bots:
|
||||
- name: "test"
|
||||
user_agent_regex: ".*"
|
||||
action: "DENY"
|
||||
3
lib/config/testdata/good/thresholds.yaml
vendored
3
lib/config/testdata/good/thresholds.yaml
vendored
@@ -18,6 +18,7 @@ thresholds:
|
||||
challenge:
|
||||
algorithm: metarefresh
|
||||
difficulty: 1
|
||||
report_as: 1
|
||||
- name: moderate-suspicion
|
||||
expression:
|
||||
all:
|
||||
@@ -27,9 +28,11 @@ thresholds:
|
||||
challenge:
|
||||
algorithm: fast
|
||||
difficulty: 2
|
||||
report_as: 2
|
||||
- name: extreme-suspicion
|
||||
expression: weight >= 20
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
algorithm: fast
|
||||
difficulty: 4
|
||||
report_as: 4
|
||||
|
||||
@@ -24,6 +24,7 @@ var (
|
||||
Challenge: &ChallengeRules{
|
||||
Algorithm: "fast",
|
||||
Difficulty: anubis.DefaultDifficulty,
|
||||
ReportAs: anubis.DefaultDifficulty,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ func TestThresholdValid(t *testing.T) {
|
||||
Challenge: &ChallengeRules{
|
||||
Algorithm: "fast",
|
||||
Difficulty: 1,
|
||||
ReportAs: 1,
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/dns"
|
||||
"github.com/TecharoHQ/anubis/lib/config"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/expressions"
|
||||
"github.com/google/cel-go/cel"
|
||||
@@ -17,8 +16,8 @@ type CELChecker struct {
|
||||
src string
|
||||
}
|
||||
|
||||
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker, error) {
|
||||
env, err := expressions.BotEnvironment(dnsObj)
|
||||
func NewCELChecker(cfg *config.ExpressionOrList) (*CELChecker, error) {
|
||||
env, err := expressions.BotEnvironment()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"math/rand/v2"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal/dns"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
@@ -16,7 +15,7 @@ import (
|
||||
// variables and functions that are passed into the CEL scope so that
|
||||
// Anubis can fail loudly and early when something is invalid instead
|
||||
// of blowing up at runtime.
|
||||
func BotEnvironment(dnsObj *dns.Dns) (*cel.Env, error) {
|
||||
func BotEnvironment() (*cel.Env, error) {
|
||||
return New(
|
||||
// Variables exposed to CEL programs:
|
||||
cel.Variable("remoteAddress", cel.StringType),
|
||||
@@ -58,118 +57,6 @@ func BotEnvironment(dnsObj *dns.Dns) (*cel.Env, error) {
|
||||
),
|
||||
),
|
||||
|
||||
cel.Function("reverseDNS",
|
||||
cel.Overload("reverseDNS_string_list_string",
|
||||
[]*cel.Type{cel.StringType},
|
||||
cel.ListType(cel.StringType),
|
||||
cel.UnaryBinding(func(addr ref.Val) ref.Val {
|
||||
addrStr, ok := addr.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(addr, "addr is not a string, but is %T", addr)
|
||||
}
|
||||
|
||||
names, err := dnsObj.ReverseDNS(string(addrStr))
|
||||
if err != nil {
|
||||
return types.NewStringList(types.DefaultTypeAdapter, []string{})
|
||||
}
|
||||
return types.NewStringList(types.DefaultTypeAdapter, names)
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
cel.Function("lookupHost",
|
||||
cel.Overload("lookupHost_string_list_string",
|
||||
[]*cel.Type{cel.StringType},
|
||||
cel.ListType(cel.StringType),
|
||||
cel.UnaryBinding(func(host ref.Val) ref.Val {
|
||||
hostStr, ok := host.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(host, "host is not a string, but is %T", host)
|
||||
}
|
||||
|
||||
addrs, err := dnsObj.LookupHost(string(hostStr))
|
||||
if err != nil {
|
||||
return types.NewStringList(types.DefaultTypeAdapter, []string{})
|
||||
}
|
||||
return types.NewStringList(types.DefaultTypeAdapter, addrs)
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
cel.Function("verifyFCrDNS",
|
||||
cel.Overload("verifyFCrDNS_string_bool",
|
||||
[]*cel.Type{cel.StringType},
|
||||
cel.BoolType,
|
||||
cel.UnaryBinding(func(addr ref.Val) ref.Val {
|
||||
addrStr, ok := addr.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(addr, "addr is not a string")
|
||||
}
|
||||
return types.Bool(dnsObj.VerifyFCrDNS(string(addrStr), nil))
|
||||
}),
|
||||
),
|
||||
cel.Overload("verifyFCrDNS_string_string_bool",
|
||||
[]*cel.Type{cel.StringType, cel.StringType},
|
||||
cel.BoolType,
|
||||
cel.BinaryBinding(func(addr, pattern ref.Val) ref.Val {
|
||||
addrStr, ok := addr.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(addr, "addr is not a string")
|
||||
}
|
||||
patternStr, ok := pattern.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(pattern, "pattern is not a string")
|
||||
}
|
||||
p := string(patternStr)
|
||||
return types.Bool(dnsObj.VerifyFCrDNS(string(addrStr), &p))
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
// arpaReverseIP transforms ip into arpa reverse notation like this
|
||||
// 1.2.3.4 -> 4.3.2.1
|
||||
// 2001:db8::1 -> 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2
|
||||
cel.Function("arpaReverseIP",
|
||||
cel.Overload("arpaReverseIP_string_string",
|
||||
[]*cel.Type{cel.StringType},
|
||||
cel.StringType,
|
||||
cel.UnaryBinding(func(addr ref.Val) ref.Val {
|
||||
s, ok := addr.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(addr, "addr is not a string")
|
||||
}
|
||||
|
||||
reversedIp, err := dnsObj.ArpaReverseIP(string(s))
|
||||
if err != nil {
|
||||
return types.ValOrErr(addr, "%s", err.Error())
|
||||
}
|
||||
return types.String(reversedIp)
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
// regexSafe escapes a string for insertion into a regular expression
|
||||
cel.Function("regexSafe",
|
||||
cel.Overload("regexSafe_string_string",
|
||||
[]*cel.Type{cel.StringType},
|
||||
cel.StringType,
|
||||
cel.UnaryBinding(func(str ref.Val) ref.Val {
|
||||
s, ok := str.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(str, "addr is not a string")
|
||||
}
|
||||
|
||||
escapes := []string{"\\", ".", ":", "*", "?", "-", "[", "]", "(", ")", "+", "{", "}", "|", "^", "$"}
|
||||
r := string(s)
|
||||
|
||||
for _, escape := range escapes {
|
||||
r = strings.ReplaceAll(r, escape, "\\"+escape)
|
||||
}
|
||||
return types.String(r)
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
cel.Function("segments",
|
||||
cel.Overload("segments_string_list_string",
|
||||
[]*cel.Type{cel.StringType},
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal/dns"
|
||||
"github.com/TecharoHQ/anubis/lib/store/memory"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
)
|
||||
|
||||
// newTestDNS is a helper function to create a new Dns object with an in-memory cache for testing.
|
||||
func newTestDNS(forwardTTL int, reverseTTL int) *dns.Dns {
|
||||
ctx := context.Background()
|
||||
memStore := memory.New(ctx)
|
||||
cache := dns.NewDNSCache(forwardTTL, reverseTTL, memStore)
|
||||
return dns.New(ctx, cache)
|
||||
}
|
||||
|
||||
func TestBotEnvironment(t *testing.T) {
|
||||
dnsObj := newTestDNS(300, 300)
|
||||
env, err := BotEnvironment(dnsObj)
|
||||
env, err := BotEnvironment()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create bot environment: %v", err)
|
||||
}
|
||||
@@ -251,344 +235,6 @@ func TestBotEnvironment(t *testing.T) {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("regexSafe", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
expected types.String
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "complex-test",
|
||||
expression: `regexSafe("^(test1|test2|)[a-z]+$")`,
|
||||
expected: types.String("\\^\\(test1\\|test2\\|\\)\\[a\\-z\\]\\+\\$"),
|
||||
description: "should escape all reserved regex characters",
|
||||
},
|
||||
{
|
||||
name: "backslash-test",
|
||||
expression: `regexSafe("use \\\\ for special characters escaping\t, one/\"\\\"/for/cel and one/for/regex")`,
|
||||
expected: types.String("use \\\\\\\\ for special characters escaping\t, one/\"\\\\\"/for/cel and one/for/regex"),
|
||||
description: "should escape double-backslashes as double-double-backslashes and ignore cel escaping and forward slashes",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prog, err := Compile(env, tt.expression)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
result, _, err := prog.Eval(map[string]interface{}{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("function-compilation", func(t *testing.T) {
|
||||
src := `regexSafe(".*")`
|
||||
_, err := Compile(env, src)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile regexSafe expression: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("dnsFunctions", func(t *testing.T) {
|
||||
originalDNSLookupAddr := dns.DNSLookupAddr
|
||||
originalDNSLookupHost := dns.DNSLookupHost
|
||||
defer func() {
|
||||
dns.DNSLookupAddr = originalDNSLookupAddr
|
||||
dns.DNSLookupHost = originalDNSLookupHost
|
||||
}()
|
||||
|
||||
t.Run("reverseDNS", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
addr string
|
||||
mockReturn []string
|
||||
mockError error
|
||||
expression string
|
||||
expected ref.Val
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
addr: "8.8.8.8",
|
||||
mockReturn: []string{"dns.google."},
|
||||
expression: `reverseDNS("8.8.8.8")`,
|
||||
expected: types.NewStringList(types.DefaultTypeAdapter, []string{"dns.google"}),
|
||||
description: "should return domain names for an IP",
|
||||
},
|
||||
{
|
||||
name: "not-found",
|
||||
addr: "127.0.0.1",
|
||||
mockReturn: []string{},
|
||||
mockError: &net.DNSError{IsNotFound: true},
|
||||
expression: `reverseDNS("127.0.0.1")`,
|
||||
expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
|
||||
description: "should return an empty list when not found",
|
||||
},
|
||||
{
|
||||
name: "error",
|
||||
addr: "error-addr",
|
||||
mockError: errors.New("some dns error"),
|
||||
expression: `reverseDNS("error-addr")`,
|
||||
expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
|
||||
description: "should return empty list on error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dns.DNSLookupAddr = func(addr string) ([]string, error) {
|
||||
if addr == tt.addr {
|
||||
return tt.mockReturn, tt.mockError
|
||||
}
|
||||
return nil, errors.New("unexpected address for reverse lookup")
|
||||
}
|
||||
|
||||
prog, err := Compile(env, tt.expression)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
result, _, err := prog.Eval(map[string]interface{}{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||
}
|
||||
if result.Equal(tt.expected) != types.True {
|
||||
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("lookupHost", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
mockReturn []string
|
||||
mockError error
|
||||
expression string
|
||||
expected ref.Val
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
host: "dns.google",
|
||||
mockReturn: []string{"8.8.8.8", "8.8.4.4"},
|
||||
expression: `lookupHost("dns.google")`,
|
||||
expected: types.NewStringList(types.DefaultTypeAdapter, []string{"8.8.8.8", "8.8.4.4"}),
|
||||
description: "should return IPs for a domain name",
|
||||
},
|
||||
{
|
||||
name: "not-found",
|
||||
host: "nonexistent.domain.example.com",
|
||||
mockReturn: []string{},
|
||||
mockError: &net.DNSError{IsNotFound: true},
|
||||
expression: `lookupHost("nonexistent.domain.example.com")`,
|
||||
expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
|
||||
description: "should return an empty list when not found",
|
||||
},
|
||||
{
|
||||
name: "error",
|
||||
host: "error-host",
|
||||
mockError: errors.New("some dns error"),
|
||||
expression: `lookupHost("error-host")`,
|
||||
expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
|
||||
description: "should return empty list on error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dns.DNSLookupHost = func(host string) ([]string, error) {
|
||||
if host == tt.host {
|
||||
return tt.mockReturn, tt.mockError
|
||||
}
|
||||
return nil, errors.New("unexpected host for forward lookup")
|
||||
}
|
||||
|
||||
prog, err := Compile(env, tt.expression)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
result, _, err := prog.Eval(map[string]interface{}{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||
}
|
||||
if result.Equal(tt.expected) != types.True {
|
||||
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verifyFCrDNS", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
addr string
|
||||
reverseMockReturn []string
|
||||
reverseMockError error
|
||||
forwardMockReturn map[string][]string // name -> ips
|
||||
forwardMockError map[string]error
|
||||
expression string
|
||||
expected types.Bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
addr: "8.8.8.8",
|
||||
reverseMockReturn: []string{"dns.google."},
|
||||
forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8", "8.8.4.4"}},
|
||||
expression: `verifyFCrDNS("8.8.8.8")`,
|
||||
expected: types.Bool(true),
|
||||
description: "should return true for valid FCrDNS",
|
||||
},
|
||||
{
|
||||
name: "failure",
|
||||
addr: "1.2.3.4",
|
||||
reverseMockReturn: []string{"spoofed.example.com."},
|
||||
forwardMockReturn: map[string][]string{"spoofed.example.com": {"5.6.7.8"}},
|
||||
expression: `verifyFCrDNS("1.2.3.4")`,
|
||||
expected: types.Bool(false),
|
||||
description: "should return false for invalid FCrDNS",
|
||||
},
|
||||
{
|
||||
name: "reverse-lookup-fails",
|
||||
addr: "1.1.1.1",
|
||||
reverseMockError: errors.New("reverse lookup failed"),
|
||||
expression: `verifyFCrDNS("1.1.1.1")`,
|
||||
expected: types.Bool(false),
|
||||
description: "should return false if reverse lookup fails",
|
||||
},
|
||||
{
|
||||
name: "success-with-pattern",
|
||||
addr: "8.8.8.8",
|
||||
reverseMockReturn: []string{"dns.google."},
|
||||
forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8"}},
|
||||
expression: `verifyFCrDNS("8.8.8.8", "dns.google")`,
|
||||
expected: types.Bool(true),
|
||||
description: "should return true for valid FCrDNS with matching pattern",
|
||||
},
|
||||
{
|
||||
name: "failure-with-pattern",
|
||||
addr: "8.8.8.8",
|
||||
reverseMockReturn: []string{"dns.google."},
|
||||
forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8"}},
|
||||
expression: `verifyFCrDNS("8.8.8.8", "wrong.pattern")`,
|
||||
expected: types.Bool(false),
|
||||
description: "should return false for FCrDNS with non-matching pattern",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dns.DNSLookupAddr = func(addr string) ([]string, error) {
|
||||
if addr == tt.addr {
|
||||
return tt.reverseMockReturn, tt.reverseMockError
|
||||
}
|
||||
return nil, errors.New("unexpected address for reverse lookup")
|
||||
}
|
||||
dns.DNSLookupHost = func(host string) ([]string, error) {
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
if ips, ok := tt.forwardMockReturn[host]; ok {
|
||||
return ips, nil
|
||||
}
|
||||
if err, ok := tt.forwardMockError[host]; ok {
|
||||
return nil, err
|
||||
}
|
||||
return nil, &net.DNSError{IsNotFound: true}
|
||||
}
|
||||
|
||||
prog, err := Compile(env, tt.expression)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
result, _, err := prog.Eval(map[string]interface{}{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||
}
|
||||
if result.Equal(tt.expected) != types.True {
|
||||
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("arpaReverseIP", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
expected types.String
|
||||
description string
|
||||
evalError bool
|
||||
}{
|
||||
{
|
||||
name: "ipv4",
|
||||
expression: `arpaReverseIP("1.2.3.4")`,
|
||||
expected: types.String("4.3.2.1"),
|
||||
description: "should correctly reverse an IPv4 address",
|
||||
},
|
||||
{
|
||||
name: "ipv6",
|
||||
expression: `arpaReverseIP("2001:db8::1")`,
|
||||
expected: types.String("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2"),
|
||||
description: "should correctly reverse an IPv6 address",
|
||||
},
|
||||
{
|
||||
name: "ipv6-full",
|
||||
expression: `arpaReverseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")`,
|
||||
expected: types.String("4.3.3.7.0.7.3.0.e.2.a.8.0.0.0.0.0.0.0.0.3.a.5.8.8.b.d.0.1.0.0.2"),
|
||||
description: "should correctly reverse a fully expanded IPv6 address",
|
||||
},
|
||||
{
|
||||
name: "ipv6-loopback",
|
||||
expression: `arpaReverseIP("::1")`,
|
||||
expected: types.String("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"),
|
||||
description: "should correctly reverse the IPv6 loopback address",
|
||||
},
|
||||
{
|
||||
name: "invalid-ip",
|
||||
expression: `arpaReverseIP("not-an-ip")`,
|
||||
evalError: true,
|
||||
description: "should error on an invalid IP",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prog, err := Compile(env, tt.expression)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||
}
|
||||
|
||||
result, _, err := prog.Eval(map[string]interface{}{})
|
||||
if tt.evalError {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected an evaluation error, but got none", tt.description)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||
}
|
||||
if result.Equal(tt.expected) != types.True {
|
||||
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestThresholdEnvironment(t *testing.T) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/dns"
|
||||
"github.com/TecharoHQ/anubis/lib/config"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
@@ -43,8 +42,6 @@ type ParsedConfig struct {
|
||||
StatusCodes config.StatusCodes
|
||||
DefaultDifficulty int
|
||||
DNSBL bool
|
||||
DnsCache *dns.DnsCache
|
||||
Dns *dns.Dns
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
@@ -69,45 +66,6 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
result := newParsedConfig(c)
|
||||
result.DefaultDifficulty = defaultDifficulty
|
||||
|
||||
if c.Logging.Level != nil {
|
||||
logLevel = c.Logging.Level.String()
|
||||
}
|
||||
|
||||
switch c.Logging.Sink {
|
||||
case config.LogSinkStdio:
|
||||
result.Logger = internal.InitSlog(logLevel, os.Stderr)
|
||||
case config.LogSinkFile:
|
||||
out := &logrotate.Logger{
|
||||
Filename: c.Logging.Parameters.Filename,
|
||||
FilenameTimeFormat: time.RFC3339,
|
||||
MaxBytes: c.Logging.Parameters.MaxBytes,
|
||||
MaxAge: c.Logging.Parameters.MaxAge,
|
||||
MaxBackups: c.Logging.Parameters.MaxBackups,
|
||||
LocalTime: c.Logging.Parameters.UseLocalTime,
|
||||
Compress: c.Logging.Parameters.Compress,
|
||||
}
|
||||
|
||||
result.Logger = internal.InitSlog(logLevel, out)
|
||||
}
|
||||
|
||||
lg := result.Logger.With("at", "config-validate")
|
||||
|
||||
stFac, ok := store.Get(c.Store.Backend)
|
||||
switch ok {
|
||||
case true:
|
||||
store, err := stFac.Build(ctx, c.Store.Parameters)
|
||||
if err != nil {
|
||||
validationErrs = append(validationErrs, err)
|
||||
} else {
|
||||
result.Store = store
|
||||
}
|
||||
case false:
|
||||
validationErrs = append(validationErrs, config.ErrUnknownStoreBackend)
|
||||
}
|
||||
|
||||
result.DnsCache = dns.NewDNSCache(result.orig.DNSTTL.Forward, result.orig.DNSTTL.Reverse, result.Store)
|
||||
result.Dns = dns.New(ctx, result.DnsCache)
|
||||
|
||||
for _, b := range c.Bots {
|
||||
if berr := b.Valid(); berr != nil {
|
||||
validationErrs = append(validationErrs, berr)
|
||||
@@ -158,7 +116,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
}
|
||||
|
||||
if b.Expression != nil {
|
||||
c, err := NewCELChecker(b.Expression, result.Dns)
|
||||
c, err := NewCELChecker(b.Expression)
|
||||
if err != nil {
|
||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
||||
} else {
|
||||
@@ -168,7 +126,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
|
||||
if b.ASNs != nil {
|
||||
if !hasThothClient {
|
||||
lg.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "asn", "settings", b.ASNs)
|
||||
slog.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "asn", "settings", b.ASNs)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -177,7 +135,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
|
||||
if b.GeoIP != nil {
|
||||
if !hasThothClient {
|
||||
lg.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "geoip", "settings", b.GeoIP)
|
||||
slog.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "geoip", "settings", b.GeoIP)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -187,6 +145,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
if b.Challenge == nil {
|
||||
parsedBot.Challenge = &config.ChallengeRules{
|
||||
Difficulty: defaultDifficulty,
|
||||
ReportAs: defaultDifficulty,
|
||||
Algorithm: "fast",
|
||||
}
|
||||
} else {
|
||||
@@ -196,7 +155,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
}
|
||||
|
||||
if parsedBot.Challenge.Algorithm == "slow" {
|
||||
lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name)
|
||||
slog.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,20 +172,17 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
|
||||
for _, t := range c.Thresholds {
|
||||
if t.Challenge != nil && t.Challenge.Algorithm == "slow" {
|
||||
lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", t.Name)
|
||||
}
|
||||
|
||||
if t.Challenge != nil && t.Challenge.ReportAs != 0 {
|
||||
lg.Warn("use of deprecated report_as setting detected, please remove this from your policy file when possible", "name", t.Name)
|
||||
slog.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", t.Name)
|
||||
}
|
||||
|
||||
if t.Name == "legacy-anubis-behaviour" && t.Expression.String() == "true" {
|
||||
if !warnedAboutThresholds.Load() {
|
||||
lg.Warn("configuration file does not contain thresholds, see docs for details on how to upgrade", "fname", fname, "docs_url", "https://anubis.techaro.lol/docs/admin/configuration/thresholds/")
|
||||
slog.Warn("configuration file does not contain thresholds, see docs for details on how to upgrade", "fname", fname, "docs_url", "https://anubis.techaro.lol/docs/admin/configuration/thresholds/")
|
||||
warnedAboutThresholds.Store(true)
|
||||
}
|
||||
|
||||
t.Challenge.Difficulty = defaultDifficulty
|
||||
t.Challenge.ReportAs = defaultDifficulty
|
||||
}
|
||||
|
||||
threshold, err := ParsedThresholdFromConfig(t)
|
||||
@@ -238,6 +194,40 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
result.Thresholds = append(result.Thresholds, threshold)
|
||||
}
|
||||
|
||||
stFac, ok := store.Get(c.Store.Backend)
|
||||
switch ok {
|
||||
case true:
|
||||
store, err := stFac.Build(ctx, c.Store.Parameters)
|
||||
if err != nil {
|
||||
validationErrs = append(validationErrs, err)
|
||||
} else {
|
||||
result.Store = store
|
||||
}
|
||||
case false:
|
||||
validationErrs = append(validationErrs, config.ErrUnknownStoreBackend)
|
||||
}
|
||||
|
||||
if c.Logging.Level != nil {
|
||||
logLevel = c.Logging.Level.String()
|
||||
}
|
||||
|
||||
switch c.Logging.Sink {
|
||||
case config.LogSinkStdio:
|
||||
result.Logger = internal.InitSlog(logLevel, os.Stderr)
|
||||
case config.LogSinkFile:
|
||||
out := &logrotate.Logger{
|
||||
Filename: c.Logging.Parameters.Filename,
|
||||
FilenameTimeFormat: time.RFC3339,
|
||||
MaxBytes: c.Logging.Parameters.MaxBytes,
|
||||
MaxAge: c.Logging.Parameters.MaxAge,
|
||||
MaxBackups: c.Logging.Parameters.MaxBackups,
|
||||
LocalTime: c.Logging.Parameters.UseLocalTime,
|
||||
Compress: c.Logging.Parameters.Compress,
|
||||
}
|
||||
|
||||
result.Logger = internal.InitSlog(logLevel, out)
|
||||
}
|
||||
|
||||
if len(validationErrs) > 0 {
|
||||
return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...))
|
||||
}
|
||||
|
||||
1
lib/testdata/invalid-challenge-method.yaml
vendored
1
lib/testdata/invalid-challenge-method.yaml
vendored
@@ -4,4 +4,5 @@ bots:
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
difficulty: 16
|
||||
report_as: 4
|
||||
algorithm: hunter2 # invalid algorithm
|
||||
|
||||
1
lib/testdata/test_config.yaml
vendored
1
lib/testdata/test_config.yaml
vendored
@@ -42,3 +42,4 @@ thresholds:
|
||||
challenge:
|
||||
algorithm: fast
|
||||
difficulty: 1
|
||||
report_as: 1
|
||||
|
||||
1
lib/testdata/zero_difficulty.yaml
vendored
1
lib/testdata/zero_difficulty.yaml
vendored
@@ -42,3 +42,4 @@ thresholds:
|
||||
challenge:
|
||||
algorithm: fast
|
||||
difficulty: 0
|
||||
report_as: 0
|
||||
|
||||
@@ -4,6 +4,7 @@ bots:
|
||||
action: CHALLENGE
|
||||
challenge:
|
||||
difficulty: 2
|
||||
report_as: 2
|
||||
algorithm: fast
|
||||
|
||||
status_codes:
|
||||
|
||||
@@ -155,7 +155,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
||||
return;
|
||||
}
|
||||
|
||||
status.innerHTML = `${t('calculating_difficulty')} ${rules.difficulty}, `;
|
||||
status.innerHTML = `${t('calculating_difficulty')} ${rules.report_as}, `;
|
||||
progress.style.display = "inline-block";
|
||||
|
||||
// the whole text, including "Speed:", as a single node, because some browsers
|
||||
@@ -166,7 +166,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
||||
|
||||
let lastSpeedUpdate = 0;
|
||||
let showingApology = false;
|
||||
const likelihood = Math.pow(16, -rules.difficulty);
|
||||
const likelihood = Math.pow(16, -rules.report_as);
|
||||
|
||||
try {
|
||||
const t0 = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user