mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-13 03:58:45 +00:00
Compare commits
12 Commits
fix/pin-ve
...
Xe/logrota
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad680d8a48 | ||
|
|
7a9590efd8 | ||
|
|
524efe3126 | ||
|
|
13adb0ca35 | ||
|
|
19215664d5 | ||
|
|
96232ec191 | ||
|
|
96d3c70f4c | ||
|
|
ad4a3ab202 | ||
|
|
0b0e10a284 | ||
|
|
b654d4d709 | ||
|
|
d55979250f | ||
|
|
8912050882 |
2
.github/actions/spelling/allow.txt
vendored
2
.github/actions/spelling/allow.txt
vendored
@@ -10,5 +10,3 @@ ABee
|
|||||||
tencent
|
tencent
|
||||||
maintnotifications
|
maintnotifications
|
||||||
azurediamond
|
azurediamond
|
||||||
cooldown
|
|
||||||
verifyfcrdns
|
|
||||||
|
|||||||
791
.github/actions/spelling/expect.txt
vendored
791
.github/actions/spelling/expect.txt
vendored
@@ -1,400 +1,391 @@
|
|||||||
|
acs
|
||||||
acs
|
Actorified
|
||||||
Actorified
|
actorifiedstore
|
||||||
actorifiedstore
|
actorify
|
||||||
actorify
|
Aibrew
|
||||||
Aibrew
|
alibaba
|
||||||
alibaba
|
alrest
|
||||||
alrest
|
amazonbot
|
||||||
amazonbot
|
anthro
|
||||||
anthro
|
anubis
|
||||||
anubis
|
anubistest
|
||||||
anubistest
|
apnic
|
||||||
apnic
|
APNICRANDNETAU
|
||||||
APNICRANDNETAU
|
Applebot
|
||||||
Applebot
|
archlinux
|
||||||
archlinux
|
asnc
|
||||||
arpa
|
asnchecker
|
||||||
asnc
|
asns
|
||||||
asnchecker
|
aspirational
|
||||||
asns
|
atuin
|
||||||
aspirational
|
azuretools
|
||||||
atuin
|
badregexes
|
||||||
azuretools
|
bbolt
|
||||||
badregexes
|
bdba
|
||||||
bbolt
|
berr
|
||||||
bdba
|
bezier
|
||||||
berr
|
bingbot
|
||||||
bezier
|
Bitcoin
|
||||||
bingbot
|
bitrate
|
||||||
Bitcoin
|
Bluesky
|
||||||
bitrate
|
blueskybot
|
||||||
Bluesky
|
boi
|
||||||
blueskybot
|
Bokm
|
||||||
boi
|
botnet
|
||||||
Bokm
|
botstopper
|
||||||
botnet
|
BPort
|
||||||
botstopper
|
Brightbot
|
||||||
BPort
|
broked
|
||||||
Brightbot
|
buildah
|
||||||
broked
|
byteslice
|
||||||
buildah
|
Bytespider
|
||||||
byteslice
|
cachebuster
|
||||||
Bytespider
|
cachediptoasn
|
||||||
cachebuster
|
Caddyfile
|
||||||
cachediptoasn
|
caninetools
|
||||||
Caddyfile
|
Cardyb
|
||||||
caninetools
|
celchecker
|
||||||
Cardyb
|
celphase
|
||||||
celchecker
|
cerr
|
||||||
celphase
|
certresolver
|
||||||
cerr
|
cespare
|
||||||
certresolver
|
CGNAT
|
||||||
cespare
|
cgr
|
||||||
CGNAT
|
chainguard
|
||||||
cgr
|
chall
|
||||||
chainguard
|
challengemozilla
|
||||||
chall
|
challengetest
|
||||||
challengemozilla
|
checkpath
|
||||||
challengetest
|
checkresult
|
||||||
checkpath
|
chibi
|
||||||
checkresult
|
cidranger
|
||||||
chibi
|
ckie
|
||||||
cidranger
|
cloudflare
|
||||||
ckie
|
Codespaces
|
||||||
cloudflare
|
confd
|
||||||
Codespaces
|
connnection
|
||||||
confd
|
containerbuild
|
||||||
connnection
|
containerregistry
|
||||||
containerbuild
|
coreutils
|
||||||
containerregistry
|
Cotoyogi
|
||||||
coreutils
|
Cromite
|
||||||
Cotoyogi
|
crt
|
||||||
Cromite
|
Cscript
|
||||||
crt
|
daemonizing
|
||||||
Cscript
|
dayjob
|
||||||
daemonizing
|
DDOS
|
||||||
dayjob
|
Debian
|
||||||
DDOS
|
debrpm
|
||||||
Debian
|
decaymap
|
||||||
debrpm
|
devcontainers
|
||||||
decaymap
|
Diffbot
|
||||||
devcontainers
|
discordapp
|
||||||
Diffbot
|
discordbot
|
||||||
discordapp
|
distros
|
||||||
discordbot
|
dnf
|
||||||
distros
|
dnsbl
|
||||||
dnf
|
dnserr
|
||||||
dnsbl
|
domainhere
|
||||||
dnserr
|
dracula
|
||||||
DNSTTL
|
dronebl
|
||||||
domainhere
|
droneblresponse
|
||||||
dracula
|
dropin
|
||||||
dronebl
|
dsilence
|
||||||
droneblresponse
|
duckduckbot
|
||||||
dropin
|
eerror
|
||||||
dsilence
|
ellenjoe
|
||||||
duckduckbot
|
emacs
|
||||||
eerror
|
enbyware
|
||||||
ellenjoe
|
etld
|
||||||
emacs
|
everyones
|
||||||
enbyware
|
evilbot
|
||||||
etld
|
evilsite
|
||||||
everyones
|
expressionorlist
|
||||||
evilbot
|
externalagent
|
||||||
evilsite
|
externalfetcher
|
||||||
expressionorlist
|
extldflags
|
||||||
externalagent
|
facebookgo
|
||||||
externalfetcher
|
Factset
|
||||||
extldflags
|
fahedouch
|
||||||
facebookgo
|
fastcgi
|
||||||
Factset
|
fediverse
|
||||||
fahedouch
|
ffprobe
|
||||||
fastcgi
|
financials
|
||||||
FCr
|
finfos
|
||||||
fcrdns
|
Firecrawl
|
||||||
fediverse
|
flagenv
|
||||||
ffprobe
|
Fordola
|
||||||
financials
|
forgejo
|
||||||
finfos
|
forwardauth
|
||||||
Firecrawl
|
fsys
|
||||||
flagenv
|
fullchain
|
||||||
Fordola
|
gaissmai
|
||||||
forgejo
|
Galvus
|
||||||
forwardauth
|
geoip
|
||||||
fsys
|
geoipchecker
|
||||||
fullchain
|
gha
|
||||||
gaissmai
|
GHSA
|
||||||
Galvus
|
Ghz
|
||||||
geoip
|
gipc
|
||||||
geoipchecker
|
gitea
|
||||||
gha
|
godotenv
|
||||||
GHSA
|
goland
|
||||||
Ghz
|
gomod
|
||||||
gipc
|
goodbot
|
||||||
gitea
|
googlebot
|
||||||
godotenv
|
gopsutil
|
||||||
goland
|
govulncheck
|
||||||
gomod
|
goyaml
|
||||||
goodbot
|
GPG
|
||||||
googlebot
|
GPT
|
||||||
gopsutil
|
gptbot
|
||||||
govulncheck
|
Graphene
|
||||||
goyaml
|
grpcprom
|
||||||
GPG
|
grw
|
||||||
GPT
|
gzw
|
||||||
gptbot
|
Hashcash
|
||||||
Graphene
|
hashrate
|
||||||
grpcprom
|
headermap
|
||||||
grw
|
healthcheck
|
||||||
gzw
|
healthz
|
||||||
Hashcash
|
hec
|
||||||
hashrate
|
helpdesk
|
||||||
headermap
|
Hetzner
|
||||||
healthcheck
|
hmc
|
||||||
healthz
|
homelab
|
||||||
hec
|
hostable
|
||||||
helpdesk
|
htmlc
|
||||||
Hetzner
|
htmx
|
||||||
hmc
|
httpdebug
|
||||||
homelab
|
Huawei
|
||||||
hostable
|
huawei
|
||||||
htmlc
|
hypertext
|
||||||
htmx
|
iaskspider
|
||||||
httpdebug
|
iaso
|
||||||
huawei
|
iat
|
||||||
hypertext
|
ifm
|
||||||
iaskspider
|
Imagesift
|
||||||
iaso
|
imgproxy
|
||||||
iat
|
impressum
|
||||||
ifm
|
inbox
|
||||||
Imagesift
|
ingressed
|
||||||
imgproxy
|
inp
|
||||||
impressum
|
internets
|
||||||
inbox
|
IPTo
|
||||||
ingressed
|
iptoasn
|
||||||
inp
|
isp
|
||||||
internets
|
iss
|
||||||
IPTo
|
isset
|
||||||
iptoasn
|
ivh
|
||||||
isp
|
Jenomis
|
||||||
iss
|
JGit
|
||||||
isset
|
jhjj
|
||||||
ivh
|
joho
|
||||||
Jenomis
|
journalctl
|
||||||
JGit
|
jshelter
|
||||||
jhjj
|
JWTs
|
||||||
joho
|
kagi
|
||||||
journalctl
|
kagibot
|
||||||
jshelter
|
Keyfunc
|
||||||
JWTs
|
keypair
|
||||||
kagi
|
KHTML
|
||||||
kagibot
|
kinda
|
||||||
Keyfunc
|
KUBECONFIG
|
||||||
keypair
|
lcj
|
||||||
KHTML
|
ldflags
|
||||||
kinda
|
letsencrypt
|
||||||
KUBECONFIG
|
Lexentale
|
||||||
lcj
|
lfc
|
||||||
ldflags
|
lgbt
|
||||||
letsencrypt
|
licend
|
||||||
Lexentale
|
licstart
|
||||||
lfc
|
lightpanda
|
||||||
lgbt
|
limsa
|
||||||
licend
|
Linting
|
||||||
licstart
|
listor
|
||||||
lightpanda
|
LLU
|
||||||
limsa
|
loadbalancer
|
||||||
Linting
|
lol
|
||||||
listor
|
lominsa
|
||||||
LLU
|
maintainership
|
||||||
loadbalancer
|
malware
|
||||||
lol
|
mcr
|
||||||
lominsa
|
memes
|
||||||
maintainership
|
metarefresh
|
||||||
malware
|
metrix
|
||||||
mcr
|
mimi
|
||||||
memes
|
Minfilia
|
||||||
metarefresh
|
mistralai
|
||||||
metrix
|
mnt
|
||||||
mimi
|
Mojeek
|
||||||
Minfilia
|
mojeekbot
|
||||||
mistralai
|
mozilla
|
||||||
mnt
|
myclient
|
||||||
Mojeek
|
mymaster
|
||||||
mojeekbot
|
mypass
|
||||||
mozilla
|
myuser
|
||||||
myclient
|
nbf
|
||||||
mymaster
|
nepeat
|
||||||
mypass
|
netsurf
|
||||||
myuser
|
nginx
|
||||||
nbf
|
nicksnyder
|
||||||
nepeat
|
nobots
|
||||||
netsurf
|
NONINFRINGEMENT
|
||||||
nginx
|
nosleep
|
||||||
nicksnyder
|
nullglob
|
||||||
nobots
|
oci
|
||||||
NONINFRINGEMENT
|
OCOB
|
||||||
nosleep
|
ogtag
|
||||||
nullglob
|
oklch
|
||||||
oci
|
omgili
|
||||||
OCOB
|
omgilibot
|
||||||
ogtag
|
openai
|
||||||
oklch
|
opengraph
|
||||||
omgili
|
openrc
|
||||||
omgilibot
|
oswald
|
||||||
openai
|
pag
|
||||||
opendns
|
palemoon
|
||||||
opengraph
|
Pangu
|
||||||
openrc
|
parseable
|
||||||
oswald
|
passthrough
|
||||||
pag
|
Patreon
|
||||||
palemoon
|
pgrep
|
||||||
Pangu
|
phrik
|
||||||
parseable
|
pidfile
|
||||||
passthrough
|
pids
|
||||||
Patreon
|
pipefail
|
||||||
pgrep
|
pki
|
||||||
phrik
|
podkova
|
||||||
pidfile
|
podman
|
||||||
pids
|
Postgre
|
||||||
pipefail
|
poststart
|
||||||
pki
|
prebaked
|
||||||
podkova
|
privkey
|
||||||
podman
|
promauto
|
||||||
Postgre
|
promhttp
|
||||||
poststart
|
proofofwork
|
||||||
prebaked
|
publicsuffix
|
||||||
privkey
|
purejs
|
||||||
promauto
|
pwcmd
|
||||||
promhttp
|
pwuser
|
||||||
proofofwork
|
qualys
|
||||||
publicsuffix
|
qwant
|
||||||
purejs
|
qwantbot
|
||||||
pwcmd
|
rac
|
||||||
pwuser
|
rawler
|
||||||
qualys
|
rcvar
|
||||||
qwant
|
redhat
|
||||||
qwantbot
|
redir
|
||||||
rac
|
redirectscheme
|
||||||
rawler
|
refactors
|
||||||
rcvar
|
reputational
|
||||||
redhat
|
risc
|
||||||
redir
|
ruleset
|
||||||
redirectscheme
|
runlevels
|
||||||
refactors
|
RUnlock
|
||||||
remoteip
|
runtimedir
|
||||||
reputational
|
runtimedirectory
|
||||||
risc
|
Ryzen
|
||||||
ruleset
|
sas
|
||||||
runlevels
|
sasl
|
||||||
RUnlock
|
screenshots
|
||||||
runtimedir
|
searchbot
|
||||||
runtimedirectory
|
searx
|
||||||
Ryzen
|
sebest
|
||||||
sas
|
secretplans
|
||||||
sasl
|
Semrush
|
||||||
screenshots
|
Seo
|
||||||
searchbot
|
setsebool
|
||||||
searx
|
shellcheck
|
||||||
sebest
|
shirou
|
||||||
secretplans
|
shopt
|
||||||
Semrush
|
Sidetrade
|
||||||
Seo
|
simprint
|
||||||
setsebool
|
sitemap
|
||||||
shellcheck
|
sls
|
||||||
shirou
|
sni
|
||||||
shopt
|
Spambot
|
||||||
Sidetrade
|
sparkline
|
||||||
simprint
|
spyderbot
|
||||||
sitemap
|
srv
|
||||||
sls
|
stackoverflow
|
||||||
sni
|
startprecmd
|
||||||
snipster
|
stoppostcmd
|
||||||
Spambot
|
storetest
|
||||||
sparkline
|
subgrid
|
||||||
spyderbot
|
subr
|
||||||
srv
|
subrequest
|
||||||
stackoverflow
|
SVCNAME
|
||||||
startprecmd
|
tagline
|
||||||
stoppostcmd
|
tarballs
|
||||||
storetest
|
tarrif
|
||||||
subgrid
|
taviso
|
||||||
subr
|
tbn
|
||||||
subrequest
|
tbr
|
||||||
SVCNAME
|
techaro
|
||||||
tagline
|
techarohq
|
||||||
tarballs
|
templ
|
||||||
tarrif
|
templruntime
|
||||||
taviso
|
testarea
|
||||||
tbn
|
Thancred
|
||||||
tbr
|
thoth
|
||||||
techaro
|
thothmock
|
||||||
techarohq
|
Tik
|
||||||
telegrambot
|
Timpibot
|
||||||
templ
|
TLog
|
||||||
templruntime
|
traefik
|
||||||
testarea
|
trunc
|
||||||
Thancred
|
uberspace
|
||||||
thoth
|
Unbreak
|
||||||
thothmock
|
unbreakdocker
|
||||||
Tik
|
unifiedjs
|
||||||
Timpibot
|
unmarshal
|
||||||
TLog
|
unparseable
|
||||||
traefik
|
uvx
|
||||||
trunc
|
UXP
|
||||||
uberspace
|
valkey
|
||||||
Unbreak
|
Varis
|
||||||
unbreakdocker
|
Velen
|
||||||
unifiedjs
|
vendored
|
||||||
unmarshal
|
vhosts
|
||||||
unparseable
|
VKE
|
||||||
uvx
|
vnd
|
||||||
UXP
|
VPS
|
||||||
valkey
|
Vultr
|
||||||
Varis
|
weblate
|
||||||
Velen
|
webmaster
|
||||||
vendored
|
webpage
|
||||||
verify
|
websecure
|
||||||
vhosts
|
websites
|
||||||
vkbot
|
Webzio
|
||||||
VKE
|
whois
|
||||||
vnd
|
wildbase
|
||||||
VPS
|
withthothmock
|
||||||
Vultr
|
wolfbeast
|
||||||
weblate
|
wordpress
|
||||||
webmaster
|
Workaround
|
||||||
webpage
|
workaround
|
||||||
websecure
|
workdir
|
||||||
websites
|
wpbot
|
||||||
Webzio
|
XCircle
|
||||||
whois
|
xeiaso
|
||||||
wildbase
|
xeserv
|
||||||
withthothmock
|
xesite
|
||||||
wolfbeast
|
xess
|
||||||
wordpress
|
xff
|
||||||
workaround
|
XForwarded
|
||||||
workdir
|
XNG
|
||||||
wpbot
|
XOB
|
||||||
XCircle
|
XOriginal
|
||||||
xeiaso
|
XReal
|
||||||
xeserv
|
yae
|
||||||
xesite
|
YAMLTo
|
||||||
xess
|
Yda
|
||||||
xff
|
yeet
|
||||||
XForwarded
|
yeetfile
|
||||||
XNG
|
yourdomain
|
||||||
XOB
|
yyz
|
||||||
XOriginal
|
Zenos
|
||||||
XReal
|
zizmor
|
||||||
yae
|
zombocom
|
||||||
YAMLTo
|
zos
|
||||||
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:
|
github-actions:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
- package-ecosystem: gomod
|
- package-ecosystem: gomod
|
||||||
directory: /
|
directory: /
|
||||||
@@ -19,8 +17,6 @@ updates:
|
|||||||
gomod:
|
gomod:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: /
|
directory: /
|
||||||
@@ -30,5 +26,3 @@ updates:
|
|||||||
npm:
|
npm:
|
||||||
patterns:
|
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
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.11.0'
|
node-version: latest
|
||||||
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.25.4'
|
go-version: stable
|
||||||
|
|
||||||
- name: install node deps
|
- name: install node deps
|
||||||
run: |
|
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
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.11.0'
|
node-version: latest
|
||||||
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.25.4'
|
go-version: stable
|
||||||
|
|
||||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
- 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
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.11.0'
|
node-version: latest
|
||||||
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.25.4'
|
go-version: stable
|
||||||
|
|
||||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
- 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
|
push: true
|
||||||
|
|
||||||
- name: Apply k8s manifests to limsa lominsa
|
- name: Apply k8s manifests to limsa lominsa
|
||||||
uses: actions-hub/kubectl@1d2c1e96fe0ae23b0c95ee8240ae151b1e638c23 # v1.34.2
|
uses: actions-hub/kubectl@f14933a23bc8c582b5aa7d108defd8e2cb9fa86d # v1.34.1
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||||
with:
|
with:
|
||||||
args: apply -k docs/manifest
|
args: apply -k docs/manifest
|
||||||
|
|
||||||
- name: Apply k8s manifests to limsa lominsa
|
- name: Apply k8s manifests to limsa lominsa
|
||||||
uses: actions-hub/kubectl@1d2c1e96fe0ae23b0c95ee8240ae151b1e638c23 # v1.34.2
|
uses: actions-hub/kubectl@f14933a23bc8c582b5aa7d108defd8e2cb9fa86d # v1.34.1
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
76
.github/workflows/go-mod-tidy-check.yml
vendored
76
.github/workflows/go-mod-tidy-check.yml
vendored
@@ -1,76 +0,0 @@
|
|||||||
name: Go Mod Tidy Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["main"]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
go_mod_tidy_check:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
|
||||||
with:
|
|
||||||
go-version: '1.25.4'
|
|
||||||
|
|
||||||
- name: Check go.mod and go.sum in main directory
|
|
||||||
run: |
|
|
||||||
# Store original file state
|
|
||||||
cp go.mod go.mod.orig
|
|
||||||
cp go.sum go.sum.orig
|
|
||||||
|
|
||||||
# Run go mod tidy
|
|
||||||
go mod tidy
|
|
||||||
|
|
||||||
# Check if files changed
|
|
||||||
if ! diff -q go.mod.orig go.mod > /dev/null 2>&1; then
|
|
||||||
echo "ERROR: go.mod in main directory has changed after running 'go mod tidy'"
|
|
||||||
echo "Please run 'go mod tidy' locally and commit the changes"
|
|
||||||
diff go.mod.orig go.mod
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! diff -q go.sum.orig go.sum > /dev/null 2>&1; then
|
|
||||||
echo "ERROR: go.sum in main directory has changed after running 'go mod tidy'"
|
|
||||||
echo "Please run 'go mod tidy' locally and commit the changes"
|
|
||||||
diff go.sum.orig go.sum
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "SUCCESS: go.mod and go.sum in main directory are tidy"
|
|
||||||
|
|
||||||
- name: Check go.mod and go.sum in test directory
|
|
||||||
run: |
|
|
||||||
cd test
|
|
||||||
|
|
||||||
# Store original file state
|
|
||||||
cp go.mod go.mod.orig
|
|
||||||
cp go.sum go.sum.orig
|
|
||||||
|
|
||||||
# Run go mod tidy
|
|
||||||
go mod tidy
|
|
||||||
|
|
||||||
# Check if files changed
|
|
||||||
if ! diff -q go.mod.orig go.mod > /dev/null 2>&1; then
|
|
||||||
echo "ERROR: go.mod in test directory has changed after running 'go mod tidy'"
|
|
||||||
echo "Please run 'go mod tidy' locally and commit the changes"
|
|
||||||
diff go.mod.orig go.mod
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! diff -q go.sum.orig go.sum > /dev/null 2>&1; then
|
|
||||||
echo "ERROR: go.sum in test directory has changed after running 'go mod tidy'"
|
|
||||||
echo "Please run 'go mod tidy' locally and commit the changes"
|
|
||||||
diff go.sum.orig go.sum
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
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
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.11.0'
|
node-version: latest
|
||||||
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.25.4'
|
go-version: stable
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
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
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.11.0'
|
node-version: latest
|
||||||
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.25.4'
|
go-version: stable
|
||||||
|
|
||||||
- name: install node deps
|
- name: install node deps
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.11.0'
|
node-version: latest
|
||||||
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.25.4'
|
go-version: stable
|
||||||
|
|
||||||
- name: install node deps
|
- name: install node deps
|
||||||
run: |
|
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
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.11.0'
|
node-version: latest
|
||||||
|
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.25.4'
|
go-version: stable
|
||||||
|
|
||||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
- 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
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.25.4'
|
go-version: stable
|
||||||
|
|
||||||
- name: Run CI
|
- name: Run CI
|
||||||
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}
|
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)
|
# user_agent_regex: (?i:bot|crawler)
|
||||||
# action: CHALLENGE
|
# action: CHALLENGE
|
||||||
# challenge:
|
# challenge:
|
||||||
# difficulty: 16 # impossible
|
# difficulty: 16 # impossible
|
||||||
|
# report_as: 4 # lie to the operator
|
||||||
# algorithm: slow # intentionally waste CPU cycles and time
|
# algorithm: slow # intentionally waste CPU cycles and time
|
||||||
|
|
||||||
# Requires a subscription to Thoth to use, see
|
# Requires a subscription to Thoth to use, see
|
||||||
@@ -248,6 +249,7 @@ thresholds:
|
|||||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
|
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
|
||||||
algorithm: metarefresh
|
algorithm: metarefresh
|
||||||
difficulty: 1
|
difficulty: 1
|
||||||
|
report_as: 1
|
||||||
# For clients that are browser-like but have either gained points from custom rules or
|
# For clients that are browser-like but have either gained points from custom rules or
|
||||||
# report as a standard browser.
|
# report as a standard browser.
|
||||||
- name: moderate-suspicion
|
- name: moderate-suspicion
|
||||||
@@ -260,6 +262,7 @@ thresholds:
|
|||||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 2 # two leading zeros, very fast for most clients
|
difficulty: 2 # two leading zeros, very fast for most clients
|
||||||
|
report_as: 2
|
||||||
- name: mild-proof-of-work
|
- name: mild-proof-of-work
|
||||||
expression:
|
expression:
|
||||||
all:
|
all:
|
||||||
@@ -270,6 +273,7 @@ thresholds:
|
|||||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 4
|
difficulty: 4
|
||||||
|
report_as: 4
|
||||||
# For clients that are browser like and have gained many points from custom rules
|
# For clients that are browser like and have gained many points from custom rules
|
||||||
- name: extreme-suspicion
|
- name: extreme-suspicion
|
||||||
expression: weight >= 30
|
expression: weight >= 30
|
||||||
@@ -278,3 +282,4 @@ thresholds:
|
|||||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 6
|
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$")
|
|
||||||
@@ -8,4 +8,3 @@
|
|||||||
- import: (data)/crawlers/marginalia.yaml
|
- import: (data)/crawlers/marginalia.yaml
|
||||||
- import: (data)/crawlers/mojeekbot.yaml
|
- import: (data)/crawlers/mojeekbot.yaml
|
||||||
- import: (data)/crawlers/commoncrawl.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
|
# action: CHALLENGE
|
||||||
# challenge:
|
# challenge:
|
||||||
# difficulty: 16 # impossible
|
# difficulty: 16 # impossible
|
||||||
|
# report_as: 4 # lie to the operator
|
||||||
# algorithm: slow # intentionally waste CPU cycles and time
|
# algorithm: slow # intentionally waste CPU cycles and time
|
||||||
|
|
||||||
# Requires a subscription to Thoth to use, see
|
# 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))
|
- 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.
|
- 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
|
### 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:
|
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.
|
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
|
## v1.23.1: Lyse Hext - Echo 1
|
||||||
|
|
||||||
- Fix `SERVE_ROBOTS_TXT` setting after the double slash fix broke it.
|
- 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
|
action: CHALLENGE
|
||||||
challenge:
|
challenge:
|
||||||
difficulty: 1 # Number of seconds to wait before refreshing the page
|
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
|
algorithm: metarefresh # Specify a non-JS challenge method
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ To use it in your Anubis configuration:
|
|||||||
action: CHALLENGE
|
action: CHALLENGE
|
||||||
challenge:
|
challenge:
|
||||||
difficulty: 1 # Number of seconds to wait before refreshing the page
|
difficulty: 1 # Number of seconds to wait before refreshing the page
|
||||||
|
report_as: 4 # Unused by this challenge method
|
||||||
algorithm: preact
|
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.
|
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`
|
### `segments`
|
||||||
|
|
||||||
Available in `bot` expressions.
|
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
|
- 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -156,68 +156,3 @@ server {
|
|||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</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:
|
challenge:
|
||||||
algorithm: metarefresh
|
algorithm: metarefresh
|
||||||
difficulty: 1
|
difficulty: 1
|
||||||
|
report_as: 1
|
||||||
|
|
||||||
- name: moderate-suspicion
|
- name: moderate-suspicion
|
||||||
expression:
|
expression:
|
||||||
@@ -51,6 +52,7 @@ thresholds:
|
|||||||
challenge:
|
challenge:
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 2
|
difficulty: 2
|
||||||
|
report_as: 2
|
||||||
|
|
||||||
- name: extreme-suspicion
|
- name: extreme-suspicion
|
||||||
expression: weight >= 20
|
expression: weight >= 20
|
||||||
@@ -58,6 +60,7 @@ thresholds:
|
|||||||
challenge:
|
challenge:
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 4
|
difficulty: 4
|
||||||
|
report_as: 4
|
||||||
```
|
```
|
||||||
|
|
||||||
This defines a suite of 4 thresholds:
|
This defines a suite of 4 thresholds:
|
||||||
@@ -127,6 +130,7 @@ action: CHALLENGE
|
|||||||
challenge:
|
challenge:
|
||||||
algorithm: metarefresh
|
algorithm: metarefresh
|
||||||
difficulty: 1
|
difficulty: 1
|
||||||
|
report_as: 1
|
||||||
```
|
```
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -92,11 +92,6 @@ Assuming you are protecting `anubistest.techaro.lol`, you need the following ser
|
|||||||
DocumentRoot /var/www/anubistest.techaro.lol
|
DocumentRoot /var/www/anubistest.techaro.lol
|
||||||
ErrorLog /var/log/httpd/anubistest.techaro.lol_error.log
|
ErrorLog /var/log/httpd/anubistest.techaro.lol_error.log
|
||||||
CustomLog /var/log/httpd/anubistest.techaro.lol_access.log combined
|
CustomLog /var/log/httpd/anubistest.techaro.lol_access.log combined
|
||||||
|
|
||||||
# Pass the remote IP to the proxied application instead of 127.0.0.1
|
|
||||||
# This requires mod_remoteip
|
|
||||||
RemoteIPHeader X-Real-IP
|
|
||||||
RemoteIPTrustedProxy 127.0.0.1/32
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ This rule has been known to have a high false positive rate in testing. Please u
|
|||||||
action: CHALLENGE
|
action: CHALLENGE
|
||||||
challenge:
|
challenge:
|
||||||
difficulty: 16 # impossible
|
difficulty: 16 # impossible
|
||||||
|
report_as: 4 # lie to the operator
|
||||||
algorithm: slow # intentionally waste CPU cycles and time
|
algorithm: slow # intentionally waste CPU cycles and time
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ Challenges can be configured with these settings:
|
|||||||
| Key | Example | Description |
|
| 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. |
|
| `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. |
|
| `algorithm` | `"fast"` | The challenge method to use. See [the list of challenge methods](./configuration/challenges/) for more information. |
|
||||||
|
|
||||||
### Remote IP based filtering
|
### Remote IP based filtering
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ bots:
|
|||||||
# action: CHALLENGE
|
# action: CHALLENGE
|
||||||
# challenge:
|
# challenge:
|
||||||
# difficulty: 16 # impossible
|
# difficulty: 16 # impossible
|
||||||
|
# report_as: 4 # lie to the operator
|
||||||
# algorithm: slow # intentionally waste CPU cycles and time
|
# algorithm: slow # intentionally waste CPU cycles and time
|
||||||
|
|
||||||
- name: rss-feed-blog
|
- name: rss-feed-blog
|
||||||
@@ -104,6 +105,7 @@ thresholds:
|
|||||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
|
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
|
||||||
algorithm: metarefresh
|
algorithm: metarefresh
|
||||||
difficulty: 1
|
difficulty: 1
|
||||||
|
report_as: 1
|
||||||
# For clients that are browser-like but have either gained points from custom rules or
|
# For clients that are browser-like but have either gained points from custom rules or
|
||||||
# report as a standard browser.
|
# report as a standard browser.
|
||||||
- name: moderate-suspicion
|
- name: moderate-suspicion
|
||||||
@@ -120,6 +122,7 @@ thresholds:
|
|||||||
# challenge data, and forwards that to the client.
|
# challenge data, and forwards that to the client.
|
||||||
algorithm: preact
|
algorithm: preact
|
||||||
difficulty: 1
|
difficulty: 1
|
||||||
|
report_as: 1
|
||||||
- name: mild-proof-of-work
|
- name: mild-proof-of-work
|
||||||
expression:
|
expression:
|
||||||
all:
|
all:
|
||||||
@@ -130,6 +133,7 @@ thresholds:
|
|||||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 2 # two leading zeros, very fast for most clients
|
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
|
# For clients that are browser like and have gained many points from custom rules
|
||||||
- name: extreme-suspicion
|
- name: extreme-suspicion
|
||||||
expression: weight >= 30
|
expression: weight >= 30
|
||||||
@@ -138,6 +142,7 @@ thresholds:
|
|||||||
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 4
|
difficulty: 4
|
||||||
|
report_as: 4
|
||||||
|
|
||||||
dnsbl: false
|
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 {
|
if rule.Challenge.Difficulty == 0 {
|
||||||
rule.Challenge.Difficulty = chall.Difficulty
|
rule.Challenge.Difficulty = chall.Difficulty
|
||||||
}
|
}
|
||||||
if rule.Challenge.ReportAs != 0 {
|
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)
|
rule.Challenge.ReportAs = chall.Difficulty
|
||||||
}
|
}
|
||||||
if rule.Challenge.Algorithm == "" {
|
if rule.Challenge.Algorithm == "" {
|
||||||
rule.Challenge.Algorithm = chall.Method
|
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{
|
return cr("default/allow", config.RuleAllow, weight), &policy.Bot{
|
||||||
Challenge: &config.ChallengeRules{
|
Challenge: &config.ChallengeRules{
|
||||||
Difficulty: s.policy.DefaultDifficulty,
|
Difficulty: s.policy.DefaultDifficulty,
|
||||||
|
ReportAs: s.policy.DefaultDifficulty,
|
||||||
Algorithm: config.DefaultAlgorithm,
|
Algorithm: config.DefaultAlgorithm,
|
||||||
},
|
},
|
||||||
Rules: &checker.List{},
|
Rules: &checker.List{},
|
||||||
|
|||||||
@@ -464,6 +464,10 @@ func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
|||||||
if bot.Challenge.Difficulty != i {
|
if bot.Challenge.Difficulty != i {
|
||||||
t.Errorf("Challenge.Difficulty is wrong, wanted %d, got: %d", i, bot.Challenge.Difficulty)
|
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{
|
Challenge: &config.ChallengeRules{
|
||||||
Algorithm: "fast",
|
Algorithm: "fast",
|
||||||
Difficulty: 0,
|
Difficulty: 0,
|
||||||
|
ReportAs: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const challengeStr = "hunter"
|
const challengeStr = "hunter"
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ type fileConfig struct {
|
|||||||
Thresholds []Threshold `json:"thresholds"`
|
Thresholds []Threshold `json:"thresholds"`
|
||||||
StatusCodes StatusCodes `json:"status_codes"`
|
StatusCodes StatusCodes `json:"status_codes"`
|
||||||
DNSBL bool `json:"dnsbl"`
|
DNSBL bool `json:"dnsbl"`
|
||||||
DNSTTL DnsTTL `json:"dns_ttl"`
|
|
||||||
Logging *Logging `json:"logging"`
|
Logging *Logging `json:"logging"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,10 +387,6 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
|||||||
Challenge: http.StatusOK,
|
Challenge: http.StatusOK,
|
||||||
Deny: http.StatusOK,
|
Deny: http.StatusOK,
|
||||||
},
|
},
|
||||||
DNSTTL: DnsTTL{
|
|
||||||
Forward: 300,
|
|
||||||
Reverse: 300,
|
|
||||||
},
|
|
||||||
Store: &Store{
|
Store: &Store{
|
||||||
Backend: "memory",
|
Backend: "memory",
|
||||||
},
|
},
|
||||||
@@ -407,8 +402,7 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result := &Config{
|
result := &Config{
|
||||||
DNSBL: c.DNSBL,
|
DNSBL: c.DNSBL,
|
||||||
DNSTTL: c.DNSTTL,
|
|
||||||
OpenGraph: OpenGraph{
|
OpenGraph: OpenGraph{
|
||||||
Enabled: c.OpenGraph.Enabled,
|
Enabled: c.OpenGraph.Enabled,
|
||||||
ConsiderHost: c.OpenGraph.ConsiderHost,
|
ConsiderHost: c.OpenGraph.ConsiderHost,
|
||||||
@@ -475,29 +469,6 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
|||||||
return result, nil
|
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 {
|
type Config struct {
|
||||||
Impressum *Impressum
|
Impressum *Impressum
|
||||||
Store *Store
|
Store *Store
|
||||||
@@ -507,7 +478,6 @@ type Config struct {
|
|||||||
StatusCodes StatusCodes
|
StatusCodes StatusCodes
|
||||||
Logging *Logging
|
Logging *Logging
|
||||||
DNSBL bool
|
DNSBL bool
|
||||||
DNSTTL DnsTTL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) Valid() error {
|
func (c Config) Valid() error {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ func TestBotValid(t *testing.T) {
|
|||||||
PathRegex: p("Mozilla"),
|
PathRegex: p("Mozilla"),
|
||||||
Challenge: &ChallengeRules{
|
Challenge: &ChallengeRules{
|
||||||
Difficulty: -1,
|
Difficulty: -1,
|
||||||
|
ReportAs: 4,
|
||||||
Algorithm: "fast",
|
Algorithm: "fast",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -123,6 +124,7 @@ func TestBotValid(t *testing.T) {
|
|||||||
PathRegex: p("Mozilla"),
|
PathRegex: p("Mozilla"),
|
||||||
Challenge: &ChallengeRules{
|
Challenge: &ChallengeRules{
|
||||||
Difficulty: 420,
|
Difficulty: 420,
|
||||||
|
ReportAs: 4,
|
||||||
Algorithm: "fast",
|
Algorithm: "fast",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -359,6 +361,7 @@ func TestBotConfigZero(t *testing.T) {
|
|||||||
|
|
||||||
b.Challenge = &ChallengeRules{
|
b.Challenge = &ChallengeRules{
|
||||||
Difficulty: 4,
|
Difficulty: 4,
|
||||||
|
ReportAs: 4,
|
||||||
Algorithm: DefaultAlgorithm,
|
Algorithm: DefaultAlgorithm,
|
||||||
}
|
}
|
||||||
if b.Zero() {
|
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:
|
challenge:
|
||||||
algorithm: metarefresh
|
algorithm: metarefresh
|
||||||
difficulty: 1
|
difficulty: 1
|
||||||
|
report_as: 1
|
||||||
- name: moderate-suspicion
|
- name: moderate-suspicion
|
||||||
expression:
|
expression:
|
||||||
all:
|
all:
|
||||||
@@ -27,9 +28,11 @@ thresholds:
|
|||||||
challenge:
|
challenge:
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 2
|
difficulty: 2
|
||||||
|
report_as: 2
|
||||||
- name: extreme-suspicion
|
- name: extreme-suspicion
|
||||||
expression: weight >= 20
|
expression: weight >= 20
|
||||||
action: CHALLENGE
|
action: CHALLENGE
|
||||||
challenge:
|
challenge:
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 4
|
difficulty: 4
|
||||||
|
report_as: 4
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var (
|
|||||||
Challenge: &ChallengeRules{
|
Challenge: &ChallengeRules{
|
||||||
Algorithm: "fast",
|
Algorithm: "fast",
|
||||||
Difficulty: anubis.DefaultDifficulty,
|
Difficulty: anubis.DefaultDifficulty,
|
||||||
|
ReportAs: anubis.DefaultDifficulty,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func TestThresholdValid(t *testing.T) {
|
|||||||
Challenge: &ChallengeRules{
|
Challenge: &ChallengeRules{
|
||||||
Algorithm: "fast",
|
Algorithm: "fast",
|
||||||
Difficulty: 1,
|
Difficulty: 1,
|
||||||
|
ReportAs: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
"github.com/TecharoHQ/anubis/internal/dns"
|
|
||||||
"github.com/TecharoHQ/anubis/lib/config"
|
"github.com/TecharoHQ/anubis/lib/config"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/expressions"
|
"github.com/TecharoHQ/anubis/lib/policy/expressions"
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
@@ -17,8 +16,8 @@ type CELChecker struct {
|
|||||||
src string
|
src string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker, error) {
|
func NewCELChecker(cfg *config.ExpressionOrList) (*CELChecker, error) {
|
||||||
env, err := expressions.BotEnvironment(dnsObj)
|
env, err := expressions.BotEnvironment()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis/internal/dns"
|
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/common/types"
|
"github.com/google/cel-go/common/types"
|
||||||
"github.com/google/cel-go/common/types/ref"
|
"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
|
// variables and functions that are passed into the CEL scope so that
|
||||||
// Anubis can fail loudly and early when something is invalid instead
|
// Anubis can fail loudly and early when something is invalid instead
|
||||||
// of blowing up at runtime.
|
// of blowing up at runtime.
|
||||||
func BotEnvironment(dnsObj *dns.Dns) (*cel.Env, error) {
|
func BotEnvironment() (*cel.Env, error) {
|
||||||
return New(
|
return New(
|
||||||
// Variables exposed to CEL programs:
|
// Variables exposed to CEL programs:
|
||||||
cel.Variable("remoteAddress", cel.StringType),
|
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.Function("segments",
|
||||||
cel.Overload("segments_string_list_string",
|
cel.Overload("segments_string_list_string",
|
||||||
[]*cel.Type{cel.StringType},
|
[]*cel.Type{cel.StringType},
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
package expressions
|
package expressions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"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"
|
||||||
"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) {
|
func TestBotEnvironment(t *testing.T) {
|
||||||
dnsObj := newTestDNS(300, 300)
|
env, err := BotEnvironment()
|
||||||
env, err := BotEnvironment(dnsObj)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create bot environment: %v", err)
|
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) {
|
func TestThresholdEnvironment(t *testing.T) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
"github.com/TecharoHQ/anubis/internal/dns"
|
|
||||||
"github.com/TecharoHQ/anubis/lib/config"
|
"github.com/TecharoHQ/anubis/lib/config"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||||
"github.com/TecharoHQ/anubis/lib/store"
|
"github.com/TecharoHQ/anubis/lib/store"
|
||||||
@@ -43,8 +42,6 @@ type ParsedConfig struct {
|
|||||||
StatusCodes config.StatusCodes
|
StatusCodes config.StatusCodes
|
||||||
DefaultDifficulty int
|
DefaultDifficulty int
|
||||||
DNSBL bool
|
DNSBL bool
|
||||||
DnsCache *dns.DnsCache
|
|
||||||
Dns *dns.Dns
|
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,45 +66,6 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
result := newParsedConfig(c)
|
result := newParsedConfig(c)
|
||||||
result.DefaultDifficulty = defaultDifficulty
|
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 {
|
for _, b := range c.Bots {
|
||||||
if berr := b.Valid(); berr != nil {
|
if berr := b.Valid(); berr != nil {
|
||||||
validationErrs = append(validationErrs, berr)
|
validationErrs = append(validationErrs, berr)
|
||||||
@@ -158,7 +116,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.Expression != nil {
|
if b.Expression != nil {
|
||||||
c, err := NewCELChecker(b.Expression, result.Dns)
|
c, err := NewCELChecker(b.Expression)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
||||||
} else {
|
} else {
|
||||||
@@ -168,7 +126,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
|
|
||||||
if b.ASNs != nil {
|
if b.ASNs != nil {
|
||||||
if !hasThothClient {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +135,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
|
|
||||||
if b.GeoIP != nil {
|
if b.GeoIP != nil {
|
||||||
if !hasThothClient {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +145,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
if b.Challenge == nil {
|
if b.Challenge == nil {
|
||||||
parsedBot.Challenge = &config.ChallengeRules{
|
parsedBot.Challenge = &config.ChallengeRules{
|
||||||
Difficulty: defaultDifficulty,
|
Difficulty: defaultDifficulty,
|
||||||
|
ReportAs: defaultDifficulty,
|
||||||
Algorithm: "fast",
|
Algorithm: "fast",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -196,7 +155,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if parsedBot.Challenge.Algorithm == "slow" {
|
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 {
|
for _, t := range c.Thresholds {
|
||||||
if t.Challenge != nil && t.Challenge.Algorithm == "slow" {
|
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)
|
slog.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Name == "legacy-anubis-behaviour" && t.Expression.String() == "true" {
|
if t.Name == "legacy-anubis-behaviour" && t.Expression.String() == "true" {
|
||||||
if !warnedAboutThresholds.Load() {
|
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)
|
warnedAboutThresholds.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Challenge.Difficulty = defaultDifficulty
|
t.Challenge.Difficulty = defaultDifficulty
|
||||||
|
t.Challenge.ReportAs = defaultDifficulty
|
||||||
}
|
}
|
||||||
|
|
||||||
threshold, err := ParsedThresholdFromConfig(t)
|
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)
|
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 {
|
if len(validationErrs) > 0 {
|
||||||
return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...))
|
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
|
action: CHALLENGE
|
||||||
challenge:
|
challenge:
|
||||||
difficulty: 16
|
difficulty: 16
|
||||||
|
report_as: 4
|
||||||
algorithm: hunter2 # invalid algorithm
|
algorithm: hunter2 # invalid algorithm
|
||||||
|
|||||||
1
lib/testdata/test_config.yaml
vendored
1
lib/testdata/test_config.yaml
vendored
@@ -42,3 +42,4 @@ thresholds:
|
|||||||
challenge:
|
challenge:
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 1
|
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:
|
challenge:
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 0
|
difficulty: 0
|
||||||
|
report_as: 0
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ bots:
|
|||||||
action: CHALLENGE
|
action: CHALLENGE
|
||||||
challenge:
|
challenge:
|
||||||
difficulty: 2
|
difficulty: 2
|
||||||
|
report_as: 2
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
|
|
||||||
status_codes:
|
status_codes:
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
status.innerHTML = `${t('calculating_difficulty')} ${rules.difficulty}, `;
|
status.innerHTML = `${t('calculating_difficulty')} ${rules.report_as}, `;
|
||||||
progress.style.display = "inline-block";
|
progress.style.display = "inline-block";
|
||||||
|
|
||||||
// the whole text, including "Speed:", as a single node, because some browsers
|
// 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 lastSpeedUpdate = 0;
|
||||||
let showingApology = false;
|
let showingApology = false;
|
||||||
const likelihood = Math.pow(16, -rules.difficulty);
|
const likelihood = Math.pow(16, -rules.report_as);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
|
|||||||
Reference in New Issue
Block a user