Compare commits

...

42 Commits

Author SHA1 Message Date
Jason Cameron
e2249edeba fix(test): remove interactive flag from nginx smoke test docker run command 2025-12-28 22:09:57 -05:00
dependabot[bot]
bcf525dbcf build(deps): bump the github-actions group with 3 updates (#1369)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-12-28 22:04:16 -05:00
Xe Iaso
d748dc9da8 test: basic nginx smoke test (#1365)
* docs: split nginx configuration files to their own directory

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test: add nginx config smoke test based on the config in the docs

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-28 23:18:25 +00:00
p0008874
9b210d795e docs(known-instances): Alphabetical order + Add Valve Corporation (#1352)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-12-26 01:05:26 +00:00
The Ninth
e084e5011e feat(localization): add Polish language translation (#1363)
(cherry picked from commit 1f9c2272e6)

Co-authored-by: bplajzer <b.plajzerr@gmail.com>
2025-12-25 15:14:04 -05:00
dependabot[bot]
2532478abd build(deps): bump the github-actions group with 4 updates (#1355)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-12-24 01:02:48 -05:00
Xe Iaso
6d9c0abe74 chore: tag v1.24.0
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-23 21:17:59 -05:00
Xe Iaso
a37068a423 fix(default-config): remove browser detection logic (#1360)
Looks like these rules don't work anymore.

Closes: #1353

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-24 02:13:54 +00:00
Xe Iaso
9d9be61c24 fix(default-config): must-accept-rule on browsers only (#1350)
TIL docker clients don't include the Accept header all the time. I would
have thought they did that. Oops.

Closes: #1346

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-19 20:42:24 +00:00
Michael
535ed74b17 i18n(de): improve consistency and wording (#1348)
- Use consistent informal address (fix simplified_explanation)
- Translate "protected_from" ("From" → "Von")
- Standardize "Webseite" → "Website"
- Use more natural phrasing:
  - "Berechnung wird durchgeführt" → "Berechnung läuft"
  - "Zur Hauptseite" → "Zur Startseite"
  - Replace awkward "sozialen Vertrag" phrasing
- "Fingerabdruckerkennung" → "Browser-Fingerprinting" (more common)
- Improve sentence structure and punctuation

Signed-off-by: Michael <87752300+michi-onl@users.noreply.github.com>
2025-12-19 00:29:49 +00:00
Xe Iaso
ba8a1b7caf fix(honeypot/naive): right, we want the client IP, not the load balancer IP
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-16 04:44:59 -05:00
Xe Iaso
40afc13d7f fix(honeypot/naive): implement better IP parsing logic
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-16 04:32:45 -05:00
Xe Iaso
122e4bc072 feat: first implementation of honeypot logic (#1342)
* feat: first implementation of honeypot logic

This is a bit of an experiment, stick with me.

The core idea here is that badly written crawlers are that: badly
written. They look for anything that contains `<a href="whatever" />`
tags and will blindly use those values to recurse. This takes advantage
of that by hiding a link in a `<script>` tag like this:

```html
<script type="ignore"><a href="/bots-only">Don't click</a></script>
```

Browsers will ignore it because they have no handler for the "ignore"
script type.

This current draft is very unoptimized (it takes like 7 seconds to
generate a page on my tower), however switching spintax libraries will
make this much faster.

The hope is to make this pluggable with WebAssembly such that we force
administrators to choose a storage method. First we crawl before we
walk.

The AI involvement in this commit is limited to the spintax in
affirmations.txt, spintext.txt, and titles.txt. This generates a bunch
of "pseudoprofound bullshit" like the following:

> This Restoration to Balance & Alignment
>
> There's a moment when creators are being called to realize that the work
> can't be reduced to results, but about energy. We don't innovate products
> by pushing harder, we do it by holding the vision. Because momentum can't
> be forced, it unfolds over time when culture are moving in the same
> direction. We're being invited into a paradigm shift in how we think
> about innovation. [...]

This is intended to "look" like normal article text. As this is a first
draft, this sucks and will be improved upon.

Assisted-by: GLM 4.6, ChatGPT, GPT-OSS 120b
Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(honeypot/naive): optimize hilariously

Signed-off-by: Xe Iaso <me@xeiaso.net>

* feat(honeypot/naive): attempt to automatically filter out based on crawling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(lib): use mazeGen instead of bsGen

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: add honeypot docs

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(test): go mod tidy

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: fix spelling metadata

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-16 04:14:29 -05:00
dependabot[bot]
cb91145352 build(deps): bump the gomod group across 1 directory with 6 updates (#1341)
Co-authored-by: Jason Cameron <jason.cameron@stanwith.me>
2025-12-15 02:43:18 +00:00
dependabot[bot]
5c97d693c1 build(deps): bump the github-actions group across 1 directory with 4 updates (#1340)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-12-15 02:34:45 +00:00
dependabot[bot]
988906bb79 build(deps): bump the npm group with 2 updates (#1339)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-12-14 21:29:42 -05:00
Xe Iaso
9c54aa852f chore: v1.24.0-pre1
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-02 07:58:29 -05:00
dependabot[bot]
cb689ee55b build(deps): bump the gomod group with 5 updates (#1316)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-12-01 03:14:20 +00:00
dependabot[bot]
071b836741 build(deps): bump the github-actions group with 3 updates (#1317)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-11-30 22:12:30 -05:00
Jason Cameron
bbdeee00f7 fix: pin Node.js and Go versions in CI configuration files (#1318)
fixes cache poisoning issues
2025-12-01 03:03:39 +00:00
dependabot[bot]
21d7753b1c build(deps): bump actions-hub/kubectl in the github-actions group (#1303)
Bumps the github-actions group with 1 update: [actions-hub/kubectl](https://github.com/actions-hub/kubectl).


Updates `actions-hub/kubectl` from 1.34.1 to 1.34.2
- [Release notes](https://github.com/actions-hub/kubectl/releases)
- [Commits](f14933a23b...1d2c1e96fe)

---
updated-dependencies:
- dependency-name: actions-hub/kubectl
  dependency-version: 1.34.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-11-30 13:53:32 -05:00
tbodt
43b8658bfd Show how to use subrequest auth with Caddy (#1312)
Signed-off-by: tbodt <tblodt@icloud.com>
2025-11-27 09:04:28 -05:00
The Ninth
00fa939acf Implement FCrDNS and other DNS features (#1308)
* Implement FCrDNS and other DNS features

* Redesign DNS cache and methods

* Fix DNS cache

* Rename regexSafe arg

* Alter verifyFCrDNS(addr) behaviour

* Remove unused dnsCache field from Server struct

* Upd expressions docs

* Update docs/docs/CHANGELOG.md

Signed-off-by: Xe Iaso <me@xeiaso.net>

* refactor(dns): simplify FCrDNS logging

* docs: clarify verifyFCrDNS behavior

Add a note to the documentation for `verifyFCrDNS` to clarify that it returns true when no PTR records are found for the given IP address.

* fix(dns): Improve FCrDNS error handling and tests

The `VerifyFCrDNS` function previously ignored errors returned from reverse DNS lookups. This could lead to incorrect passes when a DNS failure (other than a simple 'not found') occurred. This change ensures that any error from a reverse lookup will cause the FCrDNS check to fail.

The test suite for FCrDNS has been updated to reflect this change. The mock DNS lookups now simulate both 'not found' errors and other generic DNS errors. The test cases have been updated to ensure that the function behaves correctly in both scenarios, resolving a situation where two test cases were effectively duplicates.

* docs: Update FCrDNS documentation and spelling

Corrected a typo in the `verifyFCrDNS` function documentation.

Additionally, updated the spelling exception list to include new terms and remove redundant entries.

* chore: update spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-11-26 22:24:45 -05:00
Xe Iaso
4ead3ed16e fix(config): deprecate the report_as field for challenges (#1311)
* fix(config): deprecate the report_as field for challenges

This was a bad idea when it was added and it is irresponsible to
continue to have it. It causes more UX problems than it fixes with
slight of hand.

Closes: #1310
Closes: #1307
Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(policy): use the new logger for config validation messages

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs(admin/thresholds): remove this report_as setting

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-11-25 23:25:17 -05:00
bplajzer
1f9c2272e6 add Polish language translation (#1309)
* feat(localization): add Polish language translation

* feat(localization): add Polish language translation
2025-11-24 11:55:47 -05:00
Xe Iaso
b11d8132dd chore: add dependabot cooldown (#1302)
* chore: add dependabot cooldown

One of the things I need to worry about with Anubis is the idea that
could pwn a dependency and then get malicious code into prod without
realizing it, a-la Jia Tan. Given that Anubis relies on tools like
Dependabot to manage updating dependencies (good for other reasons),
it makes sense to have Dependabot have a 7 day cooldown for new
versions of dependencies.

This follows the advice from Yossarian on their blog at [1]. Thanks
for the post and easy to copy/paste snippets!

[1]: https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: update spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-11-21 19:05:26 +00:00
Xe Iaso
f032d5d0ac feat: writing logs to the filesystem with rotation support (#1299)
* refactor: move lib/policy/config to lib/config

Signed-off-by: Xe Iaso <me@xeiaso.net>

* refactor: don't set global loggers anymore

Ref #864

You were right @kotx, it is a bad idea to set the global logger
instance.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* feat(config): add log sink support

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: update spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(test): go mod tidy

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: update spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs(admin/policies): add logging block documentation

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(cmd/anubis): revert this change, it's meant to be its own PR

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: go mod tidy

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test: add file logging smoke test

Assisted-by: GLM 4.6 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix: don't expose the old log file time format string

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-11-21 11:46:00 -05:00
Xe Iaso
a709a2b2da ci: add go mod tidy check workflow (#1300)
Assisted-by: GLM 4.6 via Conductor
2025-11-20 16:54:21 +00:00
Lukas Dürrenberger
18d2b4ffff Pass the remote IP to the proxied application (#1298) 2025-11-20 16:32:15 +00:00
Xe Iaso
02989f03d0 feat(store/valkey): Add Redis(R) Sentinel support (#1294)
* feat(internal): add ListOr[T any] type

This is a utility type that lets you decode a JSON T or list of T as a
single value. This will be used with Redis Sentinel config so that you
can specify multiple sentinel addresses.

Ref TecharoHQ/botstopper#24

Assisted-by: GLM 4.6 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

* feat(store/valkey): add Redis(R) Sentinel support

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

check-spelling run (pull_request) for Xe/redis-sentinel

Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
on-behalf-of: @check-spelling <check-spelling-bot@check-spelling.dev>

* chore(store/valkey): remove pointless comments

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: document the Redis™ Sentinel configuration options

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(store/valkey): Redis™ Sentinel doesn't require a password

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
2025-11-18 09:55:19 -05:00
Jason Cameron
69e9023cbb docs: clarify usage of PUBLIC_URL and REDIRECT_DOMAINS in installatio… (#1286) 2025-11-17 12:11:34 -05:00
Jason Cameron
1d91bc99f2 fix(ogtags): respect target host/SNI/insecure flags in OG passthrough (#1283) 2025-11-16 21:32:03 -05:00
dependabot[bot]
c70b939651 build(deps-dev): bump esbuild from 0.25.12 to 0.27.0 in the npm group (#1260)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-11-16 19:13:00 -05:00
Jason Cameron
b5c5e07fc2 test(deps): update dependencies to latest versions (#1289) 2025-11-17 00:09:22 +00:00
dependabot[bot]
26fd86bb9a build(deps): bump github.com/testcontainers/testcontainers-go (#1288)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-11-16 23:47:41 +00:00
Jason Cameron
0258f6b59c build(deps): bump go deps (#1287) 2025-11-16 23:41:20 +00:00
Jason Cameron
56170e4af5 fix(tests): make CVE-2025-24369 regression deterministic (#1285)
* fix(tests): make CVE-2025-24369 regression deterministic

* fix(tests): stabilize CVE-2025-24369 regression test by using invalid proof
2025-11-16 18:34:36 -05:00
Jason Cameron
9dd4de6f1f perf: apply fieldalignement (#1284) 2025-11-16 20:43:07 +00:00
kouhaidev
da1890380e docs: use nginx http2 directive instead of deprecated http2 listen parameter (#1251)
Acked-by: Jason Cameron <git@jasoncameron.dev>
2025-11-16 06:59:16 +00:00
Henri Vasserman
6c8629e3ac test: Valkey test improvements for testcontainers (#1280)
* test: testcontainers improvements

Use the endpoint feature to get the connection URL for the container.

There are cases where localhost is not the correct one, for example when DOCKER_HOST is set to another machine.

Also, don't specify the external port for the mapping so a random unused port is used, in cases when there is already Valkey/Redis running as a container and port mapped externally on 6379.

* also remove this hack, doesn't seem necessary.
2025-11-15 14:32:37 -05:00
DerRockWolf
f6bf98fa28 feat(internal/headers): extend debug logging of X-Forwarded-For middlewares (#1269) 2025-11-15 14:31:43 -05:00
Jason Cameron
97ba84e26d Fix challenge validation panic when follow-up hits ALLOW (#1278)
* fix(localization): correct formatting of Swedish loading message

* fix(main): correct formatting and improve readability in main.go

* fix(challenge): add difficulty and policy rule hash to challenge metadata

* docs(challenge): fix panic when validating challenges in privacy-mode browsers
2025-11-14 19:51:48 -05:00
212 changed files with 5705 additions and 1635 deletions

View File

@@ -8,4 +8,13 @@ msgbox
xeact xeact
ABee ABee
tencent tencent
maintnotifications maintnotifications
azurediamond
cooldown
verifyfcrdns
Spintax
spintax
clampip
pseudoprofound
reimagining
iocaine

View File

@@ -1,385 +1,409 @@
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
fastcgi facebookgo
fediverse Factset
ffprobe fahedouch
financials fastcgi
finfos FCr
Firecrawl fcrdns
flagenv fediverse
Fordola ffprobe
forgejo financials
forwardauth finfos
fsys Firecrawl
fullchain flagenv
gaissmai Fordola
Galvus forgejo
geoip forwardauth
geoipchecker fsys
gha fullchain
GHSA gaissmai
Ghz Galvus
gipc geoip
gitea geoipchecker
godotenv gha
goland GHSA
gomod Ghz
goodbot gipc
googlebot gitea
gopsutil godotenv
govulncheck goland
goyaml gomod
GPG goodbot
GPT googlebot
gptbot gopsutil
Graphene govulncheck
grpcprom goyaml
grw GPG
gzw GPT
Hashcash gptbot
hashrate Graphene
headermap grpcprom
healthcheck grw
healthz gzw
hec Hashcash
helpdesk hashrate
Hetzner headermap
hmc healthcheck
homelab healthz
hostable hec
htmlc helpdesk
htmx Hetzner
httpdebug hmc
Huawei homelab
huawei hostable
hypertext htmlc
iaskspider htmx
iaso httpdebug
iat huawei
ifm hypertext
Imagesift iaskspider
imgproxy iaso
impressum iat
inbox ifm
ingressed Imagesift
inp imgproxy
internets impressum
IPTo inbox
iptoasn ingressed
isp inp
iss internets
isset IPTo
ivh iptoasn
Jenomis isp
JGit iss
jhjj isset
joho ivh
journalctl Jenomis
jshelter JGit
JWTs jhjj
kagi joho
kagibot journalctl
Keyfunc jshelter
keypair JWTs
KHTML kagi
kinda kagibot
KUBECONFIG Keyfunc
lcj keypair
ldflags KHTML
letsencrypt kinda
Lexentale KUBECONFIG
lgbt lcj
licend ldflags
licstart letsencrypt
lightpanda Lexentale
limsa lfc
Linting lgbt
LLU licend
loadbalancer licstart
lol lightpanda
lominsa limsa
maintainership Linting
malware listor
mcr LLU
memes loadbalancer
metarefresh lol
metrix lominsa
mimi maintainership
Minfilia malware
mistralai mcr
mnt memes
Mojeek metarefresh
mojeekbot metrix
mozilla mimi
nbf Minfilia
nepeat mistralai
netsurf mnt
nginx Mojeek
nicksnyder mojeekbot
nobots mozilla
NONINFRINGEMENT myclient
nosleep mymaster
nullglob mypass
oci myuser
OCOB nbf
ogtag nepeat
oklch netsurf
omgili nginx
omgilibot nicksnyder
openai nobots
opengraph NONINFRINGEMENT
openrc nosleep
oswald nullglob
pag oci
palemoon OCOB
Pangu ogtag
parseable oklch
passthrough omgili
Patreon omgilibot
pgrep openai
phrik opendns
pidfile opengraph
pids openrc
pipefail oswald
pki pag
podkova palemoon
podman Pangu
Postgre parseable
poststart passthrough
prebaked Patreon
privkey pgrep
promauto phrik
promhttp pidfile
proofofwork pids
publicsuffix pipefail
purejs pki
pwcmd podkova
pwuser podman
qualys Postgre
qwant poststart
qwantbot prebaked
rac privkey
rawler promauto
rcvar promhttp
rdb proofofwork
redhat publicsuffix
redir purejs
redirectscheme pwcmd
refactors pwuser
reputational qualys
risc qwant
ruleset qwantbot
runlevels rac
RUnlock rawler
runtimedir rcvar
runtimedirectory redhat
Ryzen redir
sas redirectscheme
sasl refactors
screenshots remoteip
searchbot reputational
searx risc
sebest ruleset
secretplans runlevels
Semrush RUnlock
Seo runtimedir
setsebool runtimedirectory
shellcheck Ryzen
shirou sas
shopt sasl
Sidetrade screenshots
simprint searchbot
sitemap searx
sls sebest
sni secretplans
Spambot Semrush
sparkline Seo
spyderbot setsebool
srv shellcheck
stackoverflow shirou
startprecmd shopt
stoppostcmd Sidetrade
storetest simprint
subgrid sitemap
subr sls
subrequest sni
SVCNAME snipster
tagline Spambot
tarballs sparkline
tarrif spyderbot
taviso srv
tbn stackoverflow
tbr startprecmd
techaro stoppostcmd
techarohq storetest
templ subgrid
templruntime subr
testarea subrequest
Thancred SVCNAME
thoth tagline
thothmock tarballs
Tik tarrif
Timpibot taviso
TLog tbn
traefik tbr
trunc techaro
uberspace techarohq
Unbreak telegrambot
unbreakdocker templ
unifiedjs templruntime
unmarshal testarea
unparseable Thancred
uvx thoth
UXP thothmock
valkey Tik
Varis Timpibot
Velen TLog
vendored traefik
vhosts trunc
VKE uberspace
vnd Unbreak
VPS unbreakdocker
Vultr unifiedjs
weblate unmarshal
webmaster unparseable
webpage uvx
websecure UXP
websites valkey
Webzio Varis
whois Velen
wildbase vendored
withthothmock verify
wolfbeast vhosts
wordpress vkbot
Workaround VKE
workaround vnd
workdir VPS
wpbot Vultr
XCircle weblate
xeiaso webmaster
xeserv webpage
xesite websecure
xess websites
xff Webzio
XForwarded whois
XNG wildbase
XOB withthothmock
XOriginal wolfbeast
XReal wordpress
yae workaround
YAMLTo workdir
Yda wpbot
yeet XCircle
yeetfile xeiaso
yourdomain xeserv
yyz xesite
Zenos xess
zizmor xff
zombocom XForwarded
zos XNG
XOB
XOriginal
XReal
yae
YAMLTo
Yda
yeet
yeetfile
yourdomain
yyz
Zenos
zizmor
zombocom
zos
GLM
iocaine
nikandfor
pagegen
pseudoprofound
reimagining
Rhul
shoneypot
spammer
Y'shtola

View File

@@ -8,6 +8,8 @@ updates:
github-actions: github-actions:
patterns: patterns:
- "*" - "*"
cooldown:
default-days: 7
- package-ecosystem: gomod - package-ecosystem: gomod
directory: / directory: /
@@ -17,6 +19,8 @@ updates:
gomod: gomod:
patterns: patterns:
- "*" - "*"
cooldown:
default-days: 7
- package-ecosystem: npm - package-ecosystem: npm
directory: / directory: /
@@ -26,3 +30,5 @@ updates:
npm: npm:
patterns: patterns:
- "*" - "*"
cooldown:
default-days: 7

View File

@@ -13,7 +13,7 @@ jobs:
asset_verification: asset_verification:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -22,13 +22,12 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: latest node-version: '24.11.0'
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: '1.25.4'
- name: install node deps - name: install node deps
run: | run: |

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
@@ -26,19 +26,18 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: latest node-version: '24.11.0'
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: '1.25.4'
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9 - uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with: with:
images: ghcr.io/${{ github.repository }} images: ghcr.io/${{ github.repository }}

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
@@ -36,13 +36,12 @@ jobs:
run: | run: |
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: latest node-version: '24.11.0'
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: '1.25.4'
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9 - uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
@@ -55,7 +54,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with: with:
images: ${{ env.IMAGE }} images: ${{ env.IMAGE }}
@@ -69,7 +68,7 @@ jobs:
SLOG_LEVEL: debug SLOG_LEVEL: debug
- name: Generate artifact attestation - name: Generate artifact attestation
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
with: with:
subject-name: ${{ env.IMAGE }} subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }} subject-digest: ${{ steps.build.outputs.digest }}

View File

@@ -17,12 +17,12 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log into registry - name: Log into registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -33,7 +33,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with: with:
images: ghcr.io/techarohq/anubis/docs images: ghcr.io/techarohq/anubis/docs
tags: | tags: |
@@ -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@f14933a23bc8c582b5aa7d108defd8e2cb9fa86d # v1.34.1 uses: actions-hub/kubectl@f6d776bd78f4523e36d6c74d34f9941c242b2213 # v1.35.0
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@f14933a23bc8c582b5aa7d108defd8e2cb9fa86d # v1.34.1 uses: actions-hub/kubectl@f6d776bd78f4523e36d6c74d34f9941c242b2213 # v1.35.0
env: env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }} KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with: with:

View File

@@ -13,16 +13,16 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with: with:
images: ghcr.io/techarohq/anubis/docs images: ghcr.io/techarohq/anubis/docs
tags: | tags: |

76
.github/workflows/go-mod-tidy-check.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.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"

View File

@@ -15,7 +15,7 @@ jobs:
#runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -24,16 +24,15 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: latest node-version: '24.11.0'
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: '1.25.4'
- name: Cache playwright binaries - name: Cache playwright binaries
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
id: playwright-cache id: playwright-cache
with: with:
path: | path: |

View File

@@ -14,7 +14,7 @@ jobs:
#runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
fetch-tags: true fetch-tags: true
@@ -25,13 +25,12 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: latest node-version: '24.11.0'
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: '1.25.4'
- name: install node deps - name: install node deps
run: | run: |

View File

@@ -15,7 +15,7 @@ jobs:
#runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
fetch-tags: true fetch-tags: true
@@ -26,13 +26,12 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential sudo apt-get install -y build-essential
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: latest node-version: '24.11.0'
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: '1.25.4'
- name: install node deps - name: install node deps
run: | run: |
@@ -42,7 +41,7 @@ jobs:
run: | run: |
go tool yeet go tool yeet
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: packages name: packages
path: var/* path: var/*

View File

@@ -22,23 +22,24 @@ jobs:
- git-push - git-push
- healthcheck - healthcheck
- i18n - i18n
- log-file
- nginx
- palemoon/amd64 - palemoon/amd64
#- palemoon/i386 #- palemoon/i386
- robots_txt - robots_txt
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: latest node-version: "24.11.0"
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: "1.25.4"
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9 - uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
@@ -56,7 +57,7 @@ jobs:
run: echo "ARTIFACT_NAME=${{ matrix.test }}" | sed 's|/|-|g' >> $GITHUB_ENV run: echo "ARTIFACT_NAME=${{ matrix.test }}" | sed 's|/|-|g' >> $GITHUB_ENV
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
if: always() if: always()
with: with:
name: ${{ env.ARTIFACT_NAME }} name: ${{ env.ARTIFACT_NAME }}

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
@@ -30,7 +30,7 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build and push - name: Build and push
run: | run: |
cd ./test/ssh-ci cd ./test/ssh-ci

View File

@@ -22,7 +22,7 @@ jobs:
- aarch64-16k - aarch64-16k
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
@@ -35,9 +35,9 @@ jobs:
name: id_rsa name: id_rsa
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }} known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with: with:
go-version: stable go-version: '1.25.4'
- 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 }}

View File

@@ -16,12 +16,12 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
- name: Run zizmor 🌈 - name: Run zizmor 🌈
run: uvx zizmor --format sarif . > results.sarif run: uvx zizmor --format sarif . > results.sarif
@@ -29,7 +29,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor

View File

@@ -1 +1 @@
1.23.1 1.24.0

View File

@@ -31,8 +31,8 @@ import (
"github.com/TecharoHQ/anubis/data" "github.com/TecharoHQ/anubis/data"
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
libanubis "github.com/TecharoHQ/anubis/lib" libanubis "github.com/TecharoHQ/anubis/lib"
"github.com/TecharoHQ/anubis/lib/config"
botPolicy "github.com/TecharoHQ/anubis/lib/policy" botPolicy "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/thoth" "github.com/TecharoHQ/anubis/lib/thoth"
"github.com/TecharoHQ/anubis/web" "github.com/TecharoHQ/anubis/web"
"github.com/facebookgo/flagenv" "github.com/facebookgo/flagenv"
@@ -83,7 +83,7 @@ var (
versionFlag = flag.Bool("version", false, "print Anubis version") versionFlag = flag.Bool("version", false, "print Anubis version")
publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).") publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).")
xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For") xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For")
customRealIPHeader = flag.String("custom-real-ip-header", "", "if set, read remote IP from header of this name (in case your environment doesn't set X-Real-IP header)") customRealIPHeader = flag.String("custom-real-ip-header", "", "if set, read remote IP from header of this name (in case your environment doesn't set X-Real-IP header)")
thothInsecure = flag.Bool("thoth-insecure", false, "if set, connect to Thoth over plain HTTP/2, don't enable this unless support told you to") thothInsecure = flag.Bool("thoth-insecure", false, "if set, connect to Thoth over plain HTTP/2, don't enable this unless support told you to")
thothURL = flag.String("thoth-url", "", "if set, URL for Thoth, the IP reputation database for Anubis") thothURL = flag.String("thoth-url", "", "if set, URL for Thoth, the IP reputation database for Anubis")
@@ -145,19 +145,19 @@ func parseBindNetFromAddr(address string) (string, string) {
return "", address return "", address
} }
func parseSameSite(s string) (http.SameSite) { func parseSameSite(s string) http.SameSite {
switch strings.ToLower(s) { switch strings.ToLower(s) {
case "none": case "none":
return http.SameSiteNoneMode return http.SameSiteNoneMode
case "lax": case "lax":
return http.SameSiteLaxMode return http.SameSiteLaxMode
case "strict": case "strict":
return http.SameSiteStrictMode return http.SameSiteStrictMode
case "default": case "default":
return http.SameSiteDefaultMode return http.SameSiteDefaultMode
default: default:
log.Fatalf("invalid cookie same-site mode: %s, valid values are None, Lax, Strict, and Default", s) log.Fatalf("invalid cookie same-site mode: %s, valid values are None, Lax, Strict, and Default", s)
} }
return http.SameSiteDefaultMode return http.SameSiteDefaultMode
} }
@@ -273,9 +273,11 @@ func main() {
return return
} }
internal.InitSlog(*slogLevel)
internal.SetHealth("anubis", healthv1.HealthCheckResponse_NOT_SERVING) internal.SetHealth("anubis", healthv1.HealthCheckResponse_NOT_SERVING)
lg := internal.InitSlog(*slogLevel, os.Stderr)
lg.Info("starting up Anubis")
if *healthcheck { if *healthcheck {
log.Println("running healthcheck") log.Println("running healthcheck")
if err := doHealthCheck(); err != nil { if err := doHealthCheck(); err != nil {
@@ -303,7 +305,7 @@ func main() {
if *metricsBind != "" { if *metricsBind != "" {
wg.Add(1) wg.Add(1)
go metricsServer(ctx, wg.Done) go metricsServer(ctx, *lg.With("subsystem", "metrics"), wg.Done)
} }
var rp http.Handler var rp http.Handler
@@ -323,11 +325,11 @@ func main() {
// Thoth configuration // Thoth configuration
switch { switch {
case *thothURL != "" && *thothToken == "": case *thothURL != "" && *thothToken == "":
slog.Warn("THOTH_URL is set but no THOTH_TOKEN is set") lg.Warn("THOTH_URL is set but no THOTH_TOKEN is set")
case *thothURL == "" && *thothToken != "": case *thothURL == "" && *thothToken != "":
slog.Warn("THOTH_TOKEN is set but no THOTH_URL is set") lg.Warn("THOTH_TOKEN is set but no THOTH_URL is set")
case *thothURL != "" && *thothToken != "": case *thothURL != "" && *thothToken != "":
slog.Debug("connecting to Thoth") lg.Debug("connecting to Thoth")
thothClient, err := thoth.New(ctx, *thothURL, *thothToken, *thothInsecure) thothClient, err := thoth.New(ctx, *thothURL, *thothToken, *thothInsecure)
if err != nil { if err != nil {
log.Fatalf("can't dial thoth at %s: %v", *thothURL, err) log.Fatalf("can't dial thoth at %s: %v", *thothURL, err)
@@ -336,15 +338,19 @@ func main() {
ctx = thoth.With(ctx, thothClient) ctx = thoth.With(ctx, thothClient)
} }
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty) lg.Info("loading policy file", "fname", *policyFname)
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel)
if err != nil { if err != nil {
log.Fatalf("can't parse policy file: %v", err) log.Fatalf("can't parse policy file: %v", err)
} }
lg = policy.Logger
lg.Debug("swapped to new logger")
slog.SetDefault(lg)
// Warn if persistent storage is used without a configured signing key // Warn if persistent storage is used without a configured signing key
if policy.Store.IsPersistent() { if policy.Store.IsPersistent() {
if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" { if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" {
slog.Warn("[misconfiguration] persistent storage backend is configured, but no private key is set. " + lg.Warn("[misconfiguration] persistent storage backend is configured, but no private key is set. " +
"Challenges will be invalidated when Anubis restarts. " + "Challenges will be invalidated when Anubis restarts. " +
"Set HS512_SECRET, ED25519_PRIVATE_KEY_HEX, or ED25519_PRIVATE_KEY_HEX_FILE to ensure challenges survive service restarts. " + "Set HS512_SECRET, ED25519_PRIVATE_KEY_HEX, or ED25519_PRIVATE_KEY_HEX_FILE to ensure challenges survive service restarts. " +
"See: https://anubis.techaro.lol/docs/admin/installation#key-generation") "See: https://anubis.techaro.lol/docs/admin/installation#key-generation")
@@ -407,7 +413,7 @@ func main() {
log.Fatalf("failed to generate ed25519 key: %v", err) log.Fatalf("failed to generate ed25519 key: %v", err)
} }
slog.Warn("generating random key, Anubis will have strange behavior when multiple instances are behind the same load balancer target, for more information: see https://anubis.techaro.lol/docs/admin/installation#key-generation") lg.Warn("generating random key, Anubis will have strange behavior when multiple instances are behind the same load balancer target, for more information: see https://anubis.techaro.lol/docs/admin/installation#key-generation")
} }
var redirectDomainsList []string var redirectDomainsList []string
@@ -421,7 +427,7 @@ func main() {
redirectDomainsList = append(redirectDomainsList, strings.TrimSpace(domain)) redirectDomainsList = append(redirectDomainsList, strings.TrimSpace(domain))
} }
} else { } else {
slog.Warn("REDIRECT_DOMAINS is not set, Anubis will only redirect to the same domain a request is coming from, see https://anubis.techaro.lol/docs/admin/configuration/redirect-domains") lg.Warn("REDIRECT_DOMAINS is not set, Anubis will only redirect to the same domain a request is coming from, see https://anubis.techaro.lol/docs/admin/configuration/redirect-domains")
} }
anubis.CookieName = *cookiePrefix + "-auth" anubis.CookieName = *cookiePrefix + "-auth"
@@ -439,26 +445,30 @@ func main() {
} }
s, err := libanubis.New(libanubis.Options{ s, err := libanubis.New(libanubis.Options{
BasePrefix: *basePrefix, BasePrefix: *basePrefix,
StripBasePrefix: *stripBasePrefix, StripBasePrefix: *stripBasePrefix,
Next: rp, Next: rp,
Policy: policy, Policy: policy,
ServeRobotsTXT: *robotsTxt, TargetHost: *targetHost,
ED25519PrivateKey: ed25519Priv, TargetSNI: *targetSNI,
HS512Secret: []byte(*hs512Secret), TargetInsecureSkipVerify: *targetInsecureSkipVerify,
CookieDomain: *cookieDomain, ServeRobotsTXT: *robotsTxt,
CookieDynamicDomain: *cookieDynamicDomain, ED25519PrivateKey: ed25519Priv,
CookieExpiration: *cookieExpiration, HS512Secret: []byte(*hs512Secret),
CookiePartitioned: *cookiePartitioned, CookieDomain: *cookieDomain,
RedirectDomains: redirectDomainsList, CookieDynamicDomain: *cookieDynamicDomain,
Target: *target, CookieExpiration: *cookieExpiration,
WebmasterEmail: *webmasterEmail, CookiePartitioned: *cookiePartitioned,
OpenGraph: policy.OpenGraph, RedirectDomains: redirectDomainsList,
CookieSecure: *cookieSecure, Target: *target,
CookieSameSite: parseSameSite(*cookieSameSite), WebmasterEmail: *webmasterEmail,
PublicUrl: *publicUrl, OpenGraph: policy.OpenGraph,
JWTRestrictionHeader: *jwtRestrictionHeader, CookieSecure: *cookieSecure,
DifficultyInJWT: *difficultyInJWT, CookieSameSite: parseSameSite(*cookieSameSite),
PublicUrl: *publicUrl,
JWTRestrictionHeader: *jwtRestrictionHeader,
Logger: policy.Logger.With("subsystem", "anubis"),
DifficultyInJWT: *difficultyInJWT,
}) })
if err != nil { if err != nil {
log.Fatalf("can't construct libanubis.Server: %v", err) log.Fatalf("can't construct libanubis.Server: %v", err)
@@ -474,7 +484,7 @@ func main() {
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()} srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
listener, listenerUrl := setupListener(*bindNetwork, *bind) listener, listenerUrl := setupListener(*bindNetwork, *bind)
slog.Info( lg.Info(
"listening", "listening",
"url", listenerUrl, "url", listenerUrl,
"difficulty", *challengeDifficulty, "difficulty", *challengeDifficulty,
@@ -508,7 +518,7 @@ func main() {
wg.Wait() wg.Wait()
} }
func metricsServer(ctx context.Context, done func()) { func metricsServer(ctx context.Context, lg slog.Logger, done func()) {
defer done() defer done()
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -534,7 +544,7 @@ func metricsServer(ctx context.Context, done func()) {
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()} srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind) listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
slog.Debug("listening for metrics", "url", metricsUrl) lg.Debug("listening for metrics", "url", metricsUrl)
go func() { go func() {
<-ctx.Done() <-ctx.Done()

View File

@@ -28,7 +28,7 @@ func main() {
flagenv.Parse() flagenv.Parse()
flag.Parse() flag.Parse()
internal.InitSlog(*slogLevel) slog.SetDefault(internal.InitSlog(*slogLevel, os.Stderr))
koDockerRepo := strings.TrimSuffix(*dockerRepo, "/"+filepath.Base(*dockerRepo)) koDockerRepo := strings.TrimSuffix(*dockerRepo, "/"+filepath.Base(*dockerRepo))

View File

@@ -12,7 +12,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )

View File

@@ -22,9 +22,9 @@ type TestCase struct {
type TestOptions struct { type TestOptions struct {
format string format string
action string action string
crawlDelayWeight int
policyName string policyName string
deniedAction string deniedAction string
crawlDelayWeight int
} }
func TestDataFileConversion(t *testing.T) { func TestDataFileConversion(t *testing.T) {

View File

@@ -50,8 +50,7 @@ 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
@@ -96,50 +95,6 @@ bots:
# weight: # weight:
# adjust: -10 # adjust: -10
# Assert behaviour that only genuine browsers display. This ensures that Chrome
# or Firefox versions
- name: realistic-browser-catchall
expression:
all:
- '"User-Agent" in headers'
- '( userAgent.contains("Firefox") ) || ( userAgent.contains("Chrome") ) || ( userAgent.contains("Safari") )'
- '"Accept" in headers'
- '"Sec-Fetch-Dest" in headers'
- '"Sec-Fetch-Mode" in headers'
- '"Sec-Fetch-Site" in headers'
- '"Accept-Encoding" in headers'
- '( headers["Accept-Encoding"].contains("zstd") || headers["Accept-Encoding"].contains("br") )'
- '"Accept-Language" in headers'
action: WEIGH
weight:
adjust: -10
# The Upgrade-Insecure-Requests header is typically sent by browsers, but not always
- name: upgrade-insecure-requests
expression: '"Upgrade-Insecure-Requests" in headers'
action: WEIGH
weight:
adjust: -2
# Chrome should behave like Chrome
- name: chrome-is-proper
expression:
all:
- userAgent.contains("Chrome")
- '"Sec-Ch-Ua" in headers'
- 'headers["Sec-Ch-Ua"].contains("Chromium")'
- '"Sec-Ch-Ua-Mobile" in headers'
- '"Sec-Ch-Ua-Platform" in headers'
action: WEIGH
weight:
adjust: -5
- name: should-have-accept
expression: '!("Accept" in headers)'
action: WEIGH
weight:
adjust: 5
# Generic catchall rule # Generic catchall rule
- name: generic-browser - name: generic-browser
user_agent_regex: >- user_agent_regex: >-
@@ -249,7 +204,6 @@ 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
@@ -262,7 +216,6 @@ 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:
@@ -273,7 +226,6 @@ 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
@@ -282,4 +234,3 @@ 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

View File

@@ -0,0 +1,6 @@
- name: telegrambot
action: ALLOW
expression:
all:
- userAgent.matches("TelegramBot")
- verifyFCrDNS(remoteAddress, "ptr\\.telegram\\.org$")

View File

@@ -0,0 +1,6 @@
- name: vkbot
action: ALLOW
expression:
all:
- userAgent.matches("vkShare[^+]+\\+http\\://vk\\.com/dev/Share")
- verifyFCrDNS(remoteAddress, "^snipster\\d+\\.go\\.mail\\.ru$")

View File

@@ -8,3 +8,4 @@
- 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

View File

@@ -0,0 +1,6 @@
- name: yandexbot
action: ALLOW
expression:
all:
- userAgent.matches("\\+http\\://yandex\\.com/bots")
- verifyFCrDNS(remoteAddress, "^.*\\.yandex\\.(ru|com|net)$")

View File

@@ -35,7 +35,6 @@
# 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
@@ -80,50 +79,6 @@
# weight: # weight:
# adjust: -10 # adjust: -10
# Assert behaviour that only genuine browsers display. This ensures that Chrome
# or Firefox versions
- name: realistic-browser-catchall
expression:
all:
- '"User-Agent" in headers'
- '( userAgent.contains("Firefox") ) || ( userAgent.contains("Chrome") ) || ( userAgent.contains("Safari") )'
- '"Accept" in headers'
- '"Sec-Fetch-Dest" in headers'
- '"Sec-Fetch-Mode" in headers'
- '"Sec-Fetch-Site" in headers'
- '"Accept-Encoding" in headers'
- '( headers["Accept-Encoding"].contains("zstd") || headers["Accept-Encoding"].contains("br") )'
- '"Accept-Language" in headers'
action: WEIGH
weight:
adjust: -10
# The Upgrade-Insecure-Requests header is typically sent by browsers, but not always
- name: upgrade-insecure-requests
expression: '"Upgrade-Insecure-Requests" in headers'
action: WEIGH
weight:
adjust: -2
# Chrome should behave like Chrome
- name: chrome-is-proper
expression:
all:
- userAgent.contains("Chrome")
- '"Sec-Ch-Ua" in headers'
- 'headers["Sec-Ch-Ua"].contains("Chromium")'
- '"Sec-Ch-Ua-Mobile" in headers'
- '"Sec-Ch-Ua-Platform" in headers'
action: WEIGH
weight:
adjust: -5
- name: should-have-accept
expression: '!("Accept" in headers)'
action: WEIGH
weight:
adjust: 5
# Generic catchall rule # Generic catchall rule
- name: generic-browser - name: generic-browser
user_agent_regex: >- user_agent_regex: >-

View File

@@ -0,0 +1,2 @@
- import: (data)/clients/telegram-preview.yaml
- import: (data)/clients/vk-preview.yaml

View File

@@ -13,13 +13,13 @@ func Zilch[T any]() T {
// Impl is a lazy key->value map. It's a wrapper around a map and a mutex. If values exceed their time-to-live, they are pruned at Get time. // Impl is a lazy key->value map. It's a wrapper around a map and a mutex. If values exceed their time-to-live, they are pruned at Get time.
type Impl[K comparable, V any] struct { type Impl[K comparable, V any] struct {
data map[K]decayMapEntry[V] data map[K]decayMapEntry[V]
lock sync.RWMutex
// deleteCh receives decay-deletion requests from readers. // deleteCh receives decay-deletion requests from readers.
deleteCh chan deleteReq[K] deleteCh chan deleteReq[K]
// stopCh stops the background cleanup worker. // stopCh stops the background cleanup worker.
stopCh chan struct{} stopCh chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
lock sync.RWMutex
} }
type decayMapEntry[V any] struct { type decayMapEntry[V any] struct {

View File

@@ -11,8 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- Add Polish locale ([#1292](https://github.com/TecharoHQ/anubis/pull/1309))
<!-- This changes the project to: --> <!-- This changes the project to: -->
## v1.24.0: Y'shtola Rhul
Anubis is back and better than ever! Lots of minor fixes with some big ones interspersed.
- Fix panic when validating challenges after privacy-mode browsers strip headers and the follow-up request matches an `ALLOW` threshold.
- Expose WEIGHT rule matches as Prometheus metrics. - Expose WEIGHT rule matches as Prometheus metrics.
- Allow more OCI registry clients [based on feedback](https://github.com/TecharoHQ/anubis/pull/1253#issuecomment-3506744184). - Allow more OCI registry clients [based on feedback](https://github.com/TecharoHQ/anubis/pull/1253#issuecomment-3506744184).
- Expose services directory in the embedded `(data)` filesystem. - Expose services directory in the embedded `(data)` filesystem.
@@ -20,6 +27,99 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow Renovate as an OCI registry client. - Allow Renovate as an OCI registry client.
- Properly handle 4in6 addresses so that IP matching works with those addresses. - Properly handle 4in6 addresses so that IP matching works with those addresses.
- Add support to simple Valkey/Redis cluster mode - Add support to simple Valkey/Redis cluster mode
- 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.
- Refine the check that ensures the presence of the Accept header to avoid breaking docker clients.
- Removed rules intended to reward actual browsers due to abuse in the wild.
### Dataset poisoning
Anubis has the ability to engage in [dataset poisoning attacks](https://www.anthropic.com/research/small-samples-poison) using the [dataset poisoning subsystem](./admin/honeypot/overview.mdx). This allows every Anubis instance to be a honeypot to attract and flag abusive scrapers so that no administrator action is required to ban them.
There is much more information about this feature in [the dataset poisoning subsystem documentation](./admin/honeypot/overview.mdx). Administrators that are interested in learning how this feature works should consult that documentation.
### Deprecate `report_as` in challenge configuration
Previously Anubis let you lie to users about the difficulty of a challenge to interfere with operators of malicious scrapers as a psychological attack:
```yaml
bots:
# Punish any bot with "bot" in the user-agent string
# This is known to have a high false-positive rate, use at your own risk
- name: generic-bot-catchall
user_agent_regex: (?i:bot|crawler)
action: CHALLENGE
challenge:
difficulty: 16 # impossible
report_as: 4 # lie to the operator
algorithm: slow # intentionally waste CPU cycles and time
```
This has turned out to be a bad idea because it has caused massive user experience problems and has been removed. If you are using this setting, you will get a warning in your logs like this:
```json
{
"time": "2025-11-25T23:10:31.092201549-05:00",
"level": "WARN",
"source": {
"function": "github.com/TecharoHQ/anubis/lib/policy.ParseConfig",
"file": "/home/xe/code/TecharoHQ/anubis/lib/policy/policy.go",
"line": 201
},
"msg": "use of deprecated report_as setting detected, please remove this from your policy file when possible",
"at": "config-validate",
"name": "mild-suspicion"
}
```
To remove this warning, remove this setting from your policy file.
### Logging customization
Anubis now supports the ability to log to multiple backends ("sinks"). This allows you to have Anubis [log to a file](./admin/policies.mdx#file-sink) instead of just logging to standard out. You can also customize the [logging level](./admin/policies.mdx#log-levels) in the policy file:
```yaml
logging:
level: "warn" # much less verbose logging
sink: file # log to a file
parameters:
file: "./var/anubis.log"
maxBackups: 3 # keep at least 3 old copies
maxBytes: 67108864 # each file can have up to 64 Mi of logs
maxAge: 7 # rotate files out every n days
oldFileTimeFormat: 2006-01-02T15-04-05 # RFC 3339-ish
compress: true # gzip-compress old log files
useLocalTime: false # timezone for rotated files is UTC
```
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

View File

@@ -12,7 +12,6 @@ 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
``` ```

View File

@@ -12,7 +12,6 @@ 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
``` ```

View File

@@ -233,6 +233,27 @@ 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.
@@ -266,6 +287,99 @@ 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.

View File

@@ -156,3 +156,68 @@ 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.

View File

@@ -41,7 +41,6 @@ thresholds:
challenge: challenge:
algorithm: metarefresh algorithm: metarefresh
difficulty: 1 difficulty: 1
report_as: 1
- name: moderate-suspicion - name: moderate-suspicion
expression: expression:
@@ -52,7 +51,6 @@ 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
@@ -60,7 +58,6 @@ 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:
@@ -130,7 +127,6 @@ action: CHALLENGE
challenge: challenge:
algorithm: metarefresh algorithm: metarefresh
difficulty: 1 difficulty: 1
report_as: 1
``` ```
</td> </td>

View File

@@ -92,6 +92,11 @@ 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>
``` ```

View File

@@ -1,5 +1,9 @@
# Kubernetes # Kubernetes
:::note
Leave the `PUBLIC_URL` environment variable unset in this sidecar/standalone setup. Setting it here makes redirect construction fail (`redir=null`).
:::
When setting up Anubis in Kubernetes, you want to make sure that you thread requests through Anubis kinda like this: When setting up Anubis in Kubernetes, you want to make sure that you thread requests through Anubis kinda like this:
```mermaid ```mermaid
@@ -90,8 +94,10 @@ containers:
- ALL - ALL
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
``` ```
Then add a Service entry for Anubis: Then add a Service entry for Anubis:
```yaml ```yaml

View File

@@ -1,5 +1,7 @@
# Nginx # Nginx
import CodeBlock from "@theme/CodeBlock";
Anubis is intended to be a filter proxy. The way to integrate this with nginx is to break your configuration up into two parts: TLS termination and then HTTP routing. Consider this diagram: Anubis is intended to be a filter proxy. The way to integrate this with nginx is to break your configuration up into two parts: TLS termination and then HTTP routing. Consider this diagram:
```mermaid ```mermaid
@@ -36,108 +38,26 @@ These examples assume that you are using a setup where your nginx configuration
Assuming that we are protecting `anubistest.techaro.lol`, here's what the server configuration file would look like: Assuming that we are protecting `anubistest.techaro.lol`, here's what the server configuration file would look like:
```nginx import anubisTest from "!!raw-loader!./nginx/server-anubistest-techaro-lol.conf";
# /etc/nginx/conf.d/server-anubistest-techaro-lol.conf
# HTTP - Redirect all HTTP traffic to HTTPS <CodeBlock language="nginx">{anubisTest}</CodeBlock>
server {
listen 80;
listen [::]:80;
server_name anubistest.techaro.lol;
location / {
return 301 https://$host$request_uri;
}
}
# TLS termination server, this will listen over TLS (https) and then
# proxy all traffic to the target via Anubis.
server {
# Listen on TCP port 443 with TLS (https) and HTTP/2
listen 443 ssl http2;
listen [::]:443 ssl http2;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Http-Version $server_protocol;
proxy_pass http://anubis;
}
server_name anubistest.techaro.lol;
ssl_certificate /path/to/your/certs/anubistest.techaro.lol.crt;
ssl_certificate_key /path/to/your/certs/anubistest.techaro.lol.key;
}
# Backend server, this is where your webapp should actually live.
server {
listen unix:/run/nginx/nginx.sock;
server_name anubistest.techaro.lol;
root "/srv/http/anubistest.techaro.lol";
index index.html;
# Get the visiting IP from the TLS termination server
set_real_ip_from unix:;
real_ip_header X-Real-IP;
# Your normal configuration can go here
# location .php { fastcgi...} etc.
}
```
:::tip :::tip
You can copy the `location /` block into a separate file named something like `conf-anubis.inc` and then include it inline to other `server` blocks: You can copy the `location /` block into a separate file named something like `conf-anubis.inc` and then include it inline to other `server` blocks:
```nginx import anubisInclude from "!!raw-loader!./nginx/conf-anubis.inc";
# /etc/nginx/conf.d/conf-anubis.inc
# Forward to anubis <CodeBlock language="nginx">{anubisInclude}</CodeBlock>
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://anubis;
}
```
Then in a server block: Then in a server block:
<details> <details>
<summary>Full nginx config</summary> <summary>Full nginx config</summary>
```nginx import mimiTecharoLol from "!!raw-loader!./nginx/server-mimi-techaro-lol.conf";
# /etc/nginx/conf.d/server-mimi-techaro-lol.conf
server { <CodeBlock language="nginx">{mimiTecharoLol}</CodeBlock>
# Listen on 443 with SSL
listen 443 ssl http2;
listen [::]:443 ssl http2;
# Slipstream via Anubis
include "conf-anubis.inc";
server_name mimi.techaro.lol;
ssl_certificate /path/to/your/certs/mimi.techaro.lol.crt;
ssl_certificate_key /path/to/your/certs/mimi.techaro.lol.key;
}
server {
listen unix:/run/nginx/nginx.sock;
server_name mimi.techaro.lol;
port_in_redirect off;
root "/srv/http/mimi.techaro.lol";
index index.html;
# Your normal configuration can go here
# location .php { fastcgi...} etc.
}
```
</details> </details>
@@ -145,24 +65,9 @@ server {
Create an upstream for Anubis. Create an upstream for Anubis.
```nginx import anubisUpstream from "!!raw-loader!./nginx/upstream-anubis.conf";
# /etc/nginx/conf.d/upstream-anubis.conf
upstream anubis { <CodeBlock language="nginx">{anubisUpstream}</CodeBlock>
# Make sure this matches the values you set for `BIND` and `BIND_NETWORK`.
# If this does not match, your services will not be protected by Anubis.
# Try anubis first over a UNIX socket
server unix:/run/anubis/nginx.sock;
#server 127.0.0.1:8923;
# Optional: fall back to serving the websites directly. This allows your
# websites to be resilient against Anubis failing, at the risk of exposing
# them to the raw internet without protection. This is a tradeoff and can
# be worth it in some edge cases.
#server unix:/run/nginx.sock backup;
}
```
This can be repeated for multiple sites. Anubis does not care about the HTTP `Host` header and will happily cope with multiple websites via the same instance. This can be repeated for multiple sites. Anubis does not care about the HTTP `Host` header and will happily cope with multiple websites via the same instance.

View File

@@ -0,0 +1,8 @@
# /etc/nginx/conf-anubis.inc
# Forward to anubis
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://anubis;
}

View File

@@ -0,0 +1,50 @@
# /etc/nginx/conf.d/server-anubistest-techaro-lol.conf
# HTTP - Redirect all HTTP traffic to HTTPS
server {
listen 80;
listen [::]:80;
server_name anubistest.techaro.lol;
location / {
return 301 https://$host$request_uri;
}
}
# TLS termination server, this will listen over TLS (https) and then
# proxy all traffic to the target via Anubis.
server {
# Listen on TCP port 443 with TLS (https) and HTTP/2
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Http-Version $server_protocol;
proxy_pass http://anubis;
}
server_name anubistest.techaro.lol;
ssl_certificate /path/to/your/certs/anubistest.techaro.lol.crt;
ssl_certificate_key /path/to/your/certs/anubistest.techaro.lol.key;
}
# Backend server, this is where your webapp should actually live.
server {
listen unix:/run/nginx/nginx.sock;
server_name anubistest.techaro.lol;
root "/srv/http/anubistest.techaro.lol";
index index.html;
# Get the visiting IP from the TLS termination server
set_real_ip_from unix:;
real_ip_header X-Real-IP;
# Your normal configuration can go here
# location .php { fastcgi...} etc.
}

View File

@@ -0,0 +1,29 @@
# /etc/nginx/conf.d/server-mimi-techaro-lol.conf
server {
# Listen on 443 with SSL
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
# Slipstream via Anubis
include "conf-anubis.inc";
server_name mimi.techaro.lol;
ssl_certificate /path/to/your/certs/mimi.techaro.lol.crt;
ssl_certificate_key /path/to/your/certs/mimi.techaro.lol.key;
}
server {
listen unix:/run/nginx/nginx.sock;
server_name mimi.techaro.lol;
port_in_redirect off;
root "/srv/http/mimi.techaro.lol";
index index.html;
# Your normal configuration can go here
# location .php { fastcgi...} etc.
}

View File

@@ -0,0 +1,16 @@
# /etc/nginx/conf.d/upstream-anubis.conf
upstream anubis {
# Make sure this matches the values you set for `BIND` and `BIND_NETWORK`.
# If this does not match, your services will not be protected by Anubis.
# Try anubis first over a UNIX socket
server unix:/run/anubis/nginx.sock;
#server 127.0.0.1:8923;
# Optional: fall back to serving the websites directly. This allows your
# websites to be resilient against Anubis failing, at the risk of exposing
# them to the raw internet without protection. This is a tradeoff and can
# be worth it in some edge cases.
#server unix:/run/nginx.sock backup;
}

View File

@@ -0,0 +1,8 @@
{
"label": "Honeypot",
"position": 40,
"link": {
"type": "generated-index",
"description": "Honeypot features in Anubis, allowing Anubis to passively detect malicious crawlers."
}
}

View File

@@ -0,0 +1,40 @@
---
title: Dataset poisoning
---
Anubis offers the ability to participate in [dataset poisoning](https://www.anthropic.com/research/small-samples-poison) attacks similar to what [iocaine](https://iocaine.madhouse-project.org/) and other similar tools offer. Currently this is in a preview state where a lot of details are hard-coded in order to test the viability of this approach.
In essence, when Anubis challenge and error pages are rendered they include a small bit of HTML code that browsers will ignore but scrapers will interpret as a link to ingest. This will then create a small forest of recursive nothing pages that are designed according to the following principles:
- These pages are _cheap_ to render, rendering in at most ten milliseconds on decently specced hardware.
- These pages are _vacuous_, meaning that they essentially are devoid of content such that a human would find it odd and click away, but a scraper would not be able to know that and would continue through the forest.
- These pages are _fairly large_ so that scrapers don't think that the pages are error pages or are otherwise devoid of content.
- These pages are _fully self-contained_ so that they load fast without incurring additional load from resource fetches.
In this limited preview state, Anubis generates pages using [spintax](https://outboundly.ai/blogs/what-is-spintax-and-how-to-use-it/). Spintax is a syntax that is used to create different variants of utterances for use in marketing messages and email spam that evades word filtering. In its current form, Anubis' dataset poisoning has AI generated spintax that generates vapid LinkedIn posts with some western occultism thrown in for good measure. This results in utterances like the following:
> There's a moment when visionaries are being called to realize that the work can't be reduced to optimization, but about resonance. We don't transform products by grinding endlessly, we do it by holding the vision. Because meaning can't be forced, it unfolds over time when culture are in integrity. This moment represents a fundamental reimagining in how we think about work. This isn't a framework, it's a lived truth that requires courage. When we get honest, we activate nonlinear growth that don't show up in dashboards, but redefine success anyway.
This should be fairly transparent to humans that this is pseudoprofound anti-content and is a signal to click away.
## Plans
Future versions of this feature will allow for more customization. In the near future this will be configurable via the following mechanisms:
- WebAssembly logic for customizing how the poisoning data is generated (with examples including the existing spintax method).
- Weight thresholds and logic for how they are interpreted by Anubis.
- Other configuration settings as facts and circumstances dictate.
## Implementation notes
In its current implementation, the Anubis dataset poisoning feature has the following flaws that may hinder production deployments:
- All Anubis instances use the same method for generating dataset poisoning information. This may be easy for malicious actors to detect and ignore.
- Anubis dataset poisoning routes are under the `/.within.website/x/cmd/anubis` URL hierarchy. This may be easy for malicious actors to detect and ignore.
Right now Anubis assigns 30 weight points if the following criteria are met:
- A client's User-Agent has been observed in the dataset poisoning maze at least 25 times.
- The network-clamped IP address (/24 for IPv4 and /48 for IPv6) has been observed in the dataset poisoning maze at least 25 times.
Additionally, when any given client by both User-Agent and network-clamped IP address has been observed, Anubis will emit log lines warning about it so that administrative action can be taken up to and including [filing abuse reports with the network owner](/blog/2025/file-abuse-reports).

View File

@@ -67,7 +67,7 @@ Currently the following settings are configurable via the policy file:
Anubis uses these environment variables for configuration: Anubis uses these environment variables for configuration:
| Environment Variable | Default value | Explanation | | Environment Variable | Default value | Explanation |
| :----------------------------- | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |:-------------------------------|:------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ASSET_LOOKUP_HEADER` | unset | <EO /> If set, use the contents of this header in requests when looking up custom assets in `OVERLAY_FOLDER`. See [Header-based overlay dispatch](./botstopper.mdx#header-based-overlay-dispatch) for more details. | | `ASSET_LOOKUP_HEADER` | unset | <EO /> If set, use the contents of this header in requests when looking up custom assets in `OVERLAY_FOLDER`. See [Header-based overlay dispatch](./botstopper.mdx#header-based-overlay-dispatch) for more details. |
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. | | `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` | | `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
@@ -94,8 +94,8 @@ Anubis uses these environment variables for configuration:
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. | | `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
| `OVERLAY_FOLDER` | unset | <EO /> If set, treat the given path as an [overlay folder](./botstopper.mdx#custom-images-and-css), allowing you to customize CSS, fonts, images, and add other assets to BotStopper deployments. | | `OVERLAY_FOLDER` | unset | <EO /> If set, treat the given path as an [overlay folder](./botstopper.mdx#custom-images-and-css), allowing you to customize CSS, fonts, images, and add other assets to BotStopper deployments. |
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. | | `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
| `PUBLIC_URL` | unset | The externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for Traefik forwardAuth). | | `PUBLIC_URL` | unset | The externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for Traefik forwardAuth). Leave it unset when Anubis terminates traffic directly (sidecar/standalone deployments) or redirect building will fail with `redir=null`. |
| `REDIRECT_DOMAINS` | unset | Comma-separated list of domain names that Anubis should allow redirects to when passing a challenge. See [Redirect Domain Configuration](./configuration/redirect-domains) for more details. | | `REDIRECT_DOMAINS` | unset | Comma-separated list of domain names that Anubis should allow redirects to when passing a challenge. See [Redirect Domain Configuration](./configuration/redirect-domains) for more details. |
| `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. | | `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. |
| `SLOG_LEVEL` | `INFO` | The log level for structured logging. Valid values are `DEBUG`, `INFO`, `WARN`, and `ERROR`. Set to `DEBUG` to see all requests, evaluations, and detailed diagnostic information. | | `SLOG_LEVEL` | `INFO` | The log level for structured logging. Valid values are `DEBUG`, `INFO`, `WARN`, and `ERROR`. Set to `DEBUG` to see all requests, evaluations, and detailed diagnostic information. |
| `SOCKET_MODE` | `0770` | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets. | | `SOCKET_MODE` | `0770` | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets. |

View File

@@ -84,7 +84,6 @@ 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
``` ```
@@ -93,7 +92,6 @@ 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
@@ -225,10 +223,10 @@ Using this backend will cause a lot of S3 operations, at least one for creating
The `s3api` backend takes the following configuration options: The `s3api` backend takes the following configuration options:
| Name | Type | Example | Description | | Name | Type | Example | Description |
| :----------- | :------ | :------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | | :----------- | :------ | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
| `bucketName` | string | The name of the dedicated bucket for Anubis to store information in. | | `bucketName` | string | `anubis-data` | (Required) The name of the dedicated bucket for Anubis to store information in. |
| `pathStyle` | boolean | `false` | If true, use path-style S3 API operations. Please consult your storage provider's documentation if you don't know what you should put here. | | `pathStyle` | boolean | `false` | If true, use path-style S3 API operations. Please consult your storage provider's documentation if you don't know what you should put here. |
:::note :::note
@@ -279,7 +277,7 @@ store:
:::note :::note
You can also use [Redis](http://redis.io/) with Anubis. You can also use [Redis](http://redis.io/) with Anubis.
::: :::
@@ -291,15 +289,17 @@ This backend is ideal if you are running multiple instances of Anubis in a worke
| Does your service get a lot of traffic? | ✅ Yes | | Does your service get a lot of traffic? | ✅ Yes |
| Do you want to store data persistently when Anubis restarts? | ✅ Yes | | Do you want to store data persistently when Anubis restarts? | ✅ Yes |
| Do you run Anubis without mutable filesystem storage? | ✅ Yes | | Do you run Anubis without mutable filesystem storage? | ✅ Yes |
| Do you have Redis or Valkey installed? | ✅ Yes | | Do you have Redis or Valkey installed? | ✅ Yes |
#### Configuration #### Configuration
The `valkey` backend takes the following configuration options: The `valkey` backend takes the following configuration options:
| Name | Type | Example | Description | | Name | Type | Example | Description |
| :---- | :----- | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | | :--------- | :----- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
| `url` | string | `redis://valkey:6379/0` | The URL for the instance of Redis or Valkey that Anubis should store data in. This is in the same format as `REDIS_URL` in many cloud providers. | | `cluster` | bool | `false` | If true, use [Redis™ Clustering](https://redis.io/topics/cluster-spec) for storing Anubis data. |
| `sentinel` | object | `{}` | See [Redis™ Sentinel docs](#redis-sentinel) for more detail and examples |
| `url` | string | `redis://valkey:6379/0` | The URL for the instance of Redis™ or Valkey that Anubis should store data in. This is in the same format as `REDIS_URL` in many cloud providers. |
Example: Example:
@@ -314,6 +314,96 @@ store:
This would have the Valkey client connect to host `valkey.int.techaro.lol` on port `6379` with database `0` (the default database). This would have the Valkey client connect to host `valkey.int.techaro.lol` on port `6379` with database `0` (the default database).
#### Redis™ Sentinel
If you are using [Redis™ Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/) for a high availability setup, you need to configure the `sentinel` object. This object takes the following configuration options:
| Name | Type | Example | Description |
| :----------- | :----------------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `addr` | string or list of string | `10.43.208.130:26379` | (Required) The host and port of the Redis™ Sentinel server. When possible, use DNS names for this. If you have multiple addresses, supply a list of them. |
| `clientName` | string | `Anubis` | The client name reported to Redis™ Sentinel. Set this if you want to track Anubis connections to your Redis™ Sentinel. |
| `masterName` | string | `mymaster` | (Required) The name of the master in the Redis™ Sentinel configuration. This is used to discover where to find client connection hosts/ports. |
| `username` | string | `azurediamond` | The username used to authenticate against the Redis™ Sentinel and Redis™ servers. |
| `password` | string | `hunter2` | The password used to authenticate against the Redis™ Sentinel and Redis™ servers. |
## Logging management
Anubis has very verbose logging out of the box. This is intentional and allows administrators to be sure that it is working merely by watching it work in real time. Some administrators may not appreciate this level of logging out of the box. As such, Anubis lets you customize details about how it logs data.
Anubis uses a practice called [structured logging](https://stackify.com/what-is-structured-logging-and-why-developers-need-it/) to emit log messages with key-value pair context. In order to make analyzing large amounts of log messages easier, Anubis encodes all logs in JSON. This allows you to use any tool that can parse JSON to perform analytics or monitor for issues.
Anubis exposes the following logging settings in the policy file:
| Name | Type | Example | Description |
| :----------- | :----------------------- | :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `level` | [log level](#log-levels) | `info` | The logging level threshold. Any logs that are at or above this threshold will be drained to the sink. Any other logs will be discarded. |
| `sink` | string | `stdio`, `file` | The sink where the logs drain to as they are being recorded in Anubis. |
| `parameters` | object | | Parameters for the given logging sink. This will vary based on the logging sink of choice. See below for more information. |
Anubis supports the following logging sinks:
1. `file`: logs are emitted to a file that is rotated based on size and age. Old log files are compressed with gzip to save space. This allows for better integration with users that decide to use legacy service managers (OpenRC, FreeBSD's init, etc).
2. `stdio`: logs are emitted to the standard error stream of the Anubis process. This allows runtimes such as Docker, Podman, Systemd, and Kubernetes to capture logs with their native logging subsystems without any additional configuration.
### Log levels
Anubis uses Go's [standard library `log/slog` package](https://pkg.go.dev/log/slog) to emit structured logs. By default, Anubis logs at the [Info level](https://pkg.go.dev/log/slog#Level), which is fairly verbose out of the box. Here are the possible logging levels in Anubis:
| Log level | Use in Anubis |
| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEBUG` | The raw unfiltered torrent of doom. Only use this if you are actively working on Anubis or have very good reasons to use it. |
| `INFO` | The default logging level, fairly verbose in order to make it easier for automation to parse. |
| `WARN` | A "more silent" logging level. Much less verbose. Some things that are now at the `info` level need to be moved up to the `warn` level in future patches. |
| `ERROR` | Only log error messages. |
Additionally, you can set a "slightly higher" log level if you need to, such as:
```yaml
logging:
sink: stdio
level: "INFO+1"
```
This isn't currently used by Anubis, but will be in the future for "slightly important" information.
### `file` sink
The `file` sink makes Anubis write its logs to the filesystem and rotate them out when the log file meets certain thresholds. This logging sink takes the following parameters:
| Name | Type | Example | Description |
| :------------- | :-------------- | :-------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `file` | string | `/var/log/anubis.log` | The file where Anubis logs should be written to. Make sure the user Anubis is running as has write and file creation permissions to this directory. |
| `maxBackups` | number | `3` | The number of old log files that should be maintained when log files are rotated out. |
| `maxBytes` | number of bytes | `67108864` (64Mi) | The maximum size of each log file before it is rotated out. |
| `maxAge` | number of days | `7` | If a log file is more than this many days old, rotate it out. |
| `compress` | boolean | `true` | If true, compress old log files with gzip. This should be set to `true` and is only exposed as an option for dealing with legacy workflows where there is magical thinking about log files at play. |
| `useLocalTime` | boolean | `false` | If true, use the system local time zone to create log filenames instead of UTC. This should almost always be set to `false` and is only exposed for legacy workflows where there is magical thinking about time zones at play. |
```yaml
logging:
sink: file
parameters:
file: "./var/anubis.log"
maxBackups: 3 # keep at least 3 old copies
maxBytes: 67108864 # each file can have up to 64 Mi of logs
maxAge: 7 # rotate files out every n days
compress: true # gzip-compress old log files
useLocalTime: false # timezone for rotated files is UTC
```
When files are rotated out, the old files will be named after the rotation timestamp in [RFC 3339 format](https://www.rfc-editor.org/rfc/rfc3339).
### `stdio` sink
By default, Anubis logs everything to the standard error stream of its process. This requires no configuration:
```yaml
logging:
sink: stdio
```
If you use a service orchestration platform that does not capture the standard error stream of processes, you need to use a different logging sink.
## Risk calculation for downstream services ## Risk calculation for downstream services
In case your service needs it for risk calculation reasons, Anubis exposes information about the rules that any requests match using a few headers: In case your service needs it for risk calculation reasons, Anubis exposes information about the rules that any requests match using a few headers:

View File

@@ -4,67 +4,83 @@ title: List of known websites using Anubis
This page contains a non-exhaustive list with all websites using Anubis. This page contains a non-exhaustive list with all websites using Anubis.
- <details> - https://azurlane.koumakan.jp/
<summary>The Linux Foundation</summary>
- https://git.kernel.org/
- https://lore.kernel.org/
</details>
- https://gitlab.gnome.org/
- https://scioly.org/
- https://bugs.winehq.org/ - https://bugs.winehq.org/
- https://bugzilla.proxmox.com
- https://canine.tools/
- https://clew.se/
- https://code.hackerspace.pl/
- https://codeberg.org/
- https://dev.haiku-os.org
- https://dev.sanctum.geek.nz/
- https://ebird.org/
- https://extensions.typo3.org/
- https://fabulous.systems/
- https://git.aya.so/
- https://git.devuan.org/
- https://git.enlightenment.org/
- https://gitea.com/
- https://gitlab.freedesktop.org/
- https://gitlab.gnome.org/
- https://gitlab.postmarketos.org/
- https://hosted.weblate.org/
- https://hydra.nixos.org/
- https://lab.civicrm.org/
- https://marginalia-search.com/
- https://mozillazine.org/
- https://openwrt.org/
- https://pluralpedia.org/
- https://reddit.nerdvpn.de/
- https://repositorio.ufrn.br/home/
- https://rpmfusion.org/
- https://scioly.org/
- https://source.puri.sm/
- https://squirreljme.cc/
- https://superlove.sayitditto.net/
- https://svnweb.freebsd.org/ - https://svnweb.freebsd.org/
- https://trac.ffmpeg.org/ - https://trac.ffmpeg.org/
- https://xeiaso.net/
- https://source.puri.sm/
- https://git.enlightenment.org/
- https://superlove.sayitditto.net/
- https://linktaco.com/
- https://jaredallard.dev/
- https://dev.sanctum.geek.nz/
- https://canine.tools/
- https://git.lupancham.net/
- https://dev.haiku-os.org
- http://code.hackerspace.pl/
- https://wiki.archlinux.org/
- https://git.devuan.org/
- https://hydra.nixos.org/
- https://codeberg.org/
- https://www.cfaarchive.org/
- https://gitlab.freedesktop.org/
- https://bugzilla.proxmox.com
- https://hofstede.io/
- https://www.indiemag.fr/
- https://reddit.nerdvpn.de/
- https://hosted.weblate.org/
- https://gitea.com/
- https://openwrt.org/
- https://minihoot.site
- https://catgirl.click/
- https://wiki.dolphin-emu.org/
- https://squirreljme.cc/
- https://gitlab.postmarketos.org/
- https://wiki.koha-community.org/
- https://extensions.typo3.org/
- https://ebird.org/
- https://fabulous.systems/
- https://coinhoards.org/
- https://pluralpedia.org/
- https://git.aya.so/
- https://marginalia-search.com/
- https://repositorio.ufrn.br/home/
- https://mozillazine.org/
- https://clew.se/
- https://tumfatig.net/ - https://tumfatig.net/
- https://rpmfusion.org/ - https://wiki.archlinux.org/
- https://wiki.dolphin-emu.org/
- https://wiki.freepascal.org/ - https://wiki.freepascal.org/
- https://azurlane.koumakan.jp/ - https://wiki.koha-community.org/
- https://lab.civicrm.org/ - https://www.cfaarchive.org/
- https://git.door43.org/ - https://www.indiemag.fr/
- https://xeiaso.net/
- <details>
<summary>archlinux32.org</summary>
- https://www.archlinux32.org/packages/
- https://bbs.archlinux32.org/
- https://bugs.archlinux32.org/
</details>
- <details>
<summary>Duke University</summary>
- https://repository.duke.edu/
- https://archives.lib.duke.edu/
- https://find.library.duke.edu/
- https://nicholas.duke.edu/
</details>
- <details>
<summary>Forschungszentrum Jülich</summary>
- https://juser.fz-juelich.de/
</details>
- <details> - <details>
<summary>FreeCAD</summary> <summary>FreeCAD</summary>
- https://forum.freecad.org/ - https://forum.freecad.org/
- https://wiki.freecad.org/ - https://wiki.freecad.org/
</details> </details>
- <details>
<summary>HackLab.TO</summary>
- https://hacklab.to/
- https://knowledge.hacklab.to/
</details>
- <details>
<summary>hebis (Alliance of Hessian Libraries)</summary>
- https://ubmr.hds.hebis.de/
- https://tufind.hds.hebis.de/
- https://karla.hds.hebis.de/
- and many more (see https://www.hebis.de/dienste/hebis-discovery-system/)
</details>
- <details> - <details>
<summary>ReactOS</summary> <summary>ReactOS</summary>
- https://reactos.org/forum - https://reactos.org/forum
@@ -77,6 +93,11 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://forums.scummvm.org/ - https://forums.scummvm.org/
- https://wiki.scummvm.org/ - https://wiki.scummvm.org/
</details> </details>
- <details>
<summary>Slackware</summary>
- https://git.slackware.nl/
- https://git.liveslak.org/
</details>
- <details> - <details>
<summary>Sourceware</summary> <summary>Sourceware</summary>
- https://sourceware.org/cgit - https://sourceware.org/cgit
@@ -86,41 +107,16 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://gcc.gnu.org/bugzilla/ - https://gcc.gnu.org/bugzilla/
- https://gcc.gnu.org/cgit - https://gcc.gnu.org/cgit
</details> </details>
- <details>
<summary>The Linux Foundation</summary>
- https://git.kernel.org/
- https://lore.kernel.org/
</details>
- <details> - <details>
<summary>The United Nations</summary> <summary>The United Nations</summary>
- https://policytoolbox.iiep.unesco.org/ - https://policytoolbox.iiep.unesco.org/
</details> </details>
- <details> - <details>
<summary>hebis (Alliance of Hessian Libraries)</summary> <summary>Valve Corporation</summary>
- https://ubmr.hds.hebis.de/ - https://developer.valvesoftware.com/wiki/Main_Page
- https://tufind.hds.hebis.de/
- https://karla.hds.hebis.de/
- and many more (see https://www.hebis.de/dienste/hebis-discovery-system/)
</details>
- <details>
<summary>Duke University</summary>
- https://repository.duke.edu/
- https://archives.lib.duke.edu/
- https://find.library.duke.edu/
- https://nicholas.duke.edu/
</details>
- <details>
<summary>Forschungszentrum Jülich</summary>
- https://juser.fz-juelich.de/
</details>
- <details>
<summary>archlinux32.org</summary>
- https://www.archlinux32.org/packages/
- https://bbs.archlinux32.org/
- https://bugs.archlinux32.org/
</details>
- <details>
<summary>HackLab.TO</summary>
- https://hacklab.to/
- https://knowledge.hacklab.to/
</details>
- <details>
<summary>Slackware</summary>
- https://git.slackware.nl/
- https://git.liveslak.org/
</details> </details>

View File

@@ -49,7 +49,6 @@ 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
@@ -105,7 +104,6 @@ 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
@@ -122,7 +120,6 @@ 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:
@@ -133,7 +130,6 @@ 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
@@ -142,7 +138,6 @@ 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

100
docs/package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0", "prism-react-renderer": "^2.3.0",
"raw-loader": "^4.0.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
@@ -161,6 +162,7 @@
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.29.0.tgz", "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.29.0.tgz",
"integrity": "sha512-cZ0Iq3OzFUPpgszzDr1G1aJV5UMIZ4VygJ2Az252q4Rdf5cQMhYEIKArWY/oUjMhQmosM8ygOovNq7gvA9CdCg==", "integrity": "sha512-cZ0Iq3OzFUPpgszzDr1G1aJV5UMIZ4VygJ2Az252q4Rdf5cQMhYEIKArWY/oUjMhQmosM8ygOovNq7gvA9CdCg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@algolia/client-common": "5.29.0", "@algolia/client-common": "5.29.0",
"@algolia/requester-browser-xhr": "5.29.0", "@algolia/requester-browser-xhr": "5.29.0",
@@ -308,6 +310,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3", "@babel/generator": "^7.28.3",
@@ -2145,6 +2148,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -2167,6 +2171,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -2247,6 +2252,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"
@@ -2610,6 +2616,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"
@@ -3523,6 +3530,7 @@
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.1.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.1.tgz",
"integrity": "sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==", "integrity": "sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@docusaurus/core": "3.8.1", "@docusaurus/core": "3.8.1",
"@docusaurus/logger": "3.8.1", "@docusaurus/logger": "3.8.1",
@@ -4246,6 +4254,7 @@
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz",
"integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/mdx": "^2.0.0" "@types/mdx": "^2.0.0"
}, },
@@ -4558,6 +4567,7 @@
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0", "@svgr/babel-preset": "8.1.0",
@@ -5200,6 +5210,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
"integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -5539,6 +5550,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -5594,6 +5606,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -5639,6 +5652,7 @@
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.29.0.tgz", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.29.0.tgz",
"integrity": "sha512-E2l6AlTWGznM2e7vEE6T6hzObvEyXukxMOlBmVlMyixZyK1umuO/CiVc6sDBbzVH0oEviCE5IfVY1oZBmccYPQ==", "integrity": "sha512-E2l6AlTWGznM2e7vEE6T6hzObvEyXukxMOlBmVlMyixZyK1umuO/CiVc6sDBbzVH0oEviCE5IfVY1oZBmccYPQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@algolia/client-abtesting": "5.29.0", "@algolia/client-abtesting": "5.29.0",
"@algolia/client-analytics": "5.29.0", "@algolia/client-analytics": "5.29.0",
@@ -6092,6 +6106,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001737", "caniuse-lite": "^1.0.30001737",
"electron-to-chromium": "^1.5.211", "electron-to-chromium": "^1.5.211",
@@ -6375,6 +6390,7 @@
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3", "@chevrotain/gast": "11.0.3",
@@ -7079,6 +7095,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"
@@ -7398,6 +7415,7 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz",
"integrity": "sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ==", "integrity": "sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
} }
@@ -7819,6 +7837,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC", "license": "ISC",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -8977,6 +8996,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -13596,6 +13616,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -14170,6 +14191,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -15073,6 +15095,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"
@@ -15845,6 +15868,76 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/raw-loader": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
"license": "MIT",
"dependencies": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^4.0.0 || ^5.0.0"
}
},
"node_modules/raw-loader/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/raw-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/raw-loader/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"license": "MIT"
},
"node_modules/raw-loader/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/rc": { "node_modules/rc": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -15874,6 +15967,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -15883,6 +15977,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.25.0" "scheduler": "^0.25.0"
}, },
@@ -15938,6 +16033,7 @@
"resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz",
"integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/react": "*" "@types/react": "*"
}, },
@@ -15966,6 +16062,7 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.13", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
@@ -17804,6 +17901,7 @@
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -18151,6 +18249,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -18398,6 +18497,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",

View File

@@ -21,6 +21,7 @@
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0", "prism-react-renderer": "^2.3.0",
"raw-loader": "^4.0.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },

109
go.mod
View File

@@ -5,39 +5,41 @@ go 1.24.2
require ( require (
github.com/TecharoHQ/thoth-proto v0.5.0 github.com/TecharoHQ/thoth-proto v0.5.0
github.com/a-h/templ v0.3.960 github.com/a-h/templ v0.3.960
github.com/aws/aws-sdk-go-v2 v1.39.5 github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/config v1.31.16 github.com/aws/aws-sdk-go-v2/config v1.32.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.89.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2
github.com/cespare/xxhash/v2 v2.3.0 github.com/cespare/xxhash/v2 v2.3.0
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
github.com/fahedouch/go-logrotate v0.3.0
github.com/gaissmai/bart v0.26.0 github.com/gaissmai/bart v0.26.0
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/cel-go v0.26.1 github.com/google/cel-go v0.26.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be github.com/lum8rjack/go-ja4h v0.0.0-20250828030157-fa5266d50650
github.com/nicksnyder/go-i18n/v2 v2.6.0 github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/nikandfor/spintax v0.0.0-20181023094358-fc346b245bb3
github.com/playwright-community/playwright-go v0.5200.1 github.com/playwright-community/playwright-go v0.5200.1
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/redis/go-redis/v9 v9.16.0 github.com/redis/go-redis/v9 v9.17.2
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
github.com/shirou/gopsutil/v4 v4.25.10 github.com/shirou/gopsutil/v4 v4.25.11
github.com/testcontainers/testcontainers-go v0.39.0 github.com/testcontainers/testcontainers-go v0.40.0
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/net v0.43.0 golang.org/x/net v0.48.0
golang.org/x/text v0.28.0 golang.org/x/text v0.32.0
google.golang.org/grpc v1.76.0 google.golang.org/grpc v1.77.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.34.1 k8s.io/apimachinery v0.34.3
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
require ( require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect al.essio.dev/pkg/shellescape v1.6.0 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1 // indirect buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
cel.dev/expr v0.24.0 // indirect cel.dev/expr v0.25.1 // indirect
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect github.com/AlekSi/pointer v1.2.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
@@ -52,21 +54,22 @@ require (
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.20 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.12 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.12 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/smithy-go v1.23.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect github.com/cavaliergopher/cpio v1.0.1 // indirect
@@ -86,12 +89,13 @@ require (
github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/docker v28.3.3+incompatible // indirect github.com/docker/docker v28.5.1+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 // indirect github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 // indirect
github.com/ebitengine/purego v0.9.0 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
@@ -110,7 +114,6 @@ require (
github.com/go-stack/stack v1.8.1 // indirect github.com/go-stack/stack v1.8.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-github/v70 v70.0.0 // indirect github.com/google/go-github/v70 v70.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
@@ -148,8 +151,8 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.17.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
@@ -162,41 +165,41 @@ require (
github.com/suzuki-shunsuke/logrus-error v0.1.4 // indirect github.com/suzuki-shunsuke/logrus-error v0.1.4 // indirect
github.com/suzuki-shunsuke/pinact v1.6.0 // indirect github.com/suzuki-shunsuke/pinact v1.6.0 // indirect
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 // indirect github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/ulikunitz/xz v0.5.14 // indirect github.com/ulikunitz/xz v0.5.14 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.41.0 // indirect golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/exp/typeparams v0.0.0-20250718183923-645b1fa84792 // indirect golang.org/x/exp/typeparams v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/mod v0.26.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/telemetry v0.0.0-20250721140356-96f361d9aaf7 // indirect golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect
golang.org/x/term v0.34.0 // indirect golang.org/x/term v0.38.0 // indirect
golang.org/x/tools v0.35.0 // indirect golang.org/x/tools v0.40.0 // indirect
golang.org/x/vuln v1.1.4 // indirect golang.org/x/vuln v1.1.4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
honnef.co/go/tools v0.6.1 // indirect honnef.co/go/tools v0.6.1 // indirect
mvdan.cc/sh/v3 v3.12.0 // indirect mvdan.cc/sh/v3 v3.12.0 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
) )
tool ( tool (

250
go.sum
View File

@@ -1,9 +1,9 @@
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1 h1:Lg6klmCi3v7VvpqeeLEER9/m5S8y9e9DjhqQnSCNy4k= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
@@ -51,42 +51,44 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w= github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 h1:t9yYsydLYNBk9cJ73rgPhPWqOh/52fcWDQB5b1JsKSY= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2/go.mod h1:IusfVNTmiSN3t4rhxWFaBAqn+mcNdwKtPcV16eYdgko= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc= github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8=
github.com/aws/aws-sdk-go-v2/config v1.31.16/go.mod h1:2S9hBElpCyGMifv14WxQ7EfPumgoeCPZUpuPX8VtW34= github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE=
github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q= github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4=
github.com/aws/aws-sdk-go-v2/credentials v1.18.20/go.mod h1:9mCi28a+fmBHSQ0UM79omkz6JtN+PEsvLrnG36uoUv0= github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 h1:VO3FIM2TDbm0kqp6sFNR0PbioXJb/HzCDW6NtIZpIWE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12/go.mod h1:6C39gB8kg82tx3r72muZSrNhHia9rjGkX7ORaS2GKNE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 h1:p/9flfXdoAnwJnuW9xHEAFY22R3A6skYkW19JFF9F+8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12/go.mod h1:ZTLHakoVCTtW8AaLGSwJ3LXqHD9uQKnOcv1TrpO6u2k= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 h1:2lTWFvRcnWFFLzHWmtddu5MTchc5Oj2OOey++99tPZ0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12/go.mod h1:hI92pK+ho8HVcWMHKHrK3Uml4pfG7wvL86FzO0LVtQQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.12 h1:itu4KHu8JK/N6NcLIISlf3LL1LccMqruLUXZ9y7yBZw= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.12/go.mod h1:i+6vTU3xziikTY3vcox23X8pPGW5X3wVgd1VZ7ha+x8= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.3 h1:NEe7FaViguRQEm8zl8Ay/kC/QRsMtWUiCGZajQIsLdc= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.3/go.mod h1:JLuCKu5VfiLBBBl/5IzZILU7rxS0koQpHzMOCzycOJU= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 h1:MM8imH7NZ0ovIVX7D2RxfMDv7Jt9OiUXkcQ+GqywA7M= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12/go.mod h1:gf4OGwdNkbEsb7elw2Sy76odfhwNktWII3WgvQgQQ6w= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.12 h1:R3uW0iKl8rgNEXNjVGliW/oMEh9fO/LlUEV8RvIFr1I= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.12/go.mod h1:XEttbEr5yqsw8ebi7vlDoGJJjMXRez4/s9pibpJyL5s= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.89.1 h1:Dq82AV+Qxpno/fG162eAhnD8d48t9S+GZCfz7yv1VeA= github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2 h1:U3ygWUhCpiSPYSHOrRhb3gOl9T5Y3kB8k5Vjs//57bE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.89.1/go.mod h1:MbKLznDKpf7PnSonNRUVYZzfP0CeLkRIUexeblgKcU4= github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw= github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0/go.mod h1:/e8m+AO6HNPPqMyfKRtzZ9+mBF5/x1Wk8QiDva4m07I= github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M= github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA= github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0/go.mod h1:4EjU+4mIx6+JqKQkruye+CaigV7alL3thVPfDd9VlMs= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
@@ -139,18 +141,20 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 h1:aQYWswi+hRL2zJqGacdCZx32XjKYV8ApXFGntw79XAM= github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 h1:aQYWswi+hRL2zJqGacdCZx32XjKYV8ApXFGntw79XAM=
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -163,6 +167,8 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755SVMyzaUQ=
github.com/fahedouch/go-logrotate v0.3.0/go.mod h1:X49m0bvPLkk71MHNCQ1yEfVEw8W/u+qvHa/hOnhCYf4=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -203,8 +209,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
@@ -243,8 +247,8 @@ github.com/goreleaser/nfpm/v2 v2.43.0 h1:o5oureuZkhu55RK0M9WSN8JLW7hu6MymtMh7Lyp
github.com/goreleaser/nfpm/v2 v2.43.0/go.mod h1:f//PE8PjNHjaPCbd7Jkok+aPKdLTrzM+fuIWg3PfVRg= github.com/goreleaser/nfpm/v2 v2.43.0/go.mod h1:f//PE8PjNHjaPCbd7Jkok+aPKdLTrzM+fuIWg3PfVRg=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
@@ -259,8 +263,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d h1:RnWZeH8N8KXfbwMTex/KKMYMj0FJRCF6tQubUuQ02GM= github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d h1:RnWZeH8N8KXfbwMTex/KKMYMj0FJRCF6tQubUuQ02GM=
github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d/go.mod h1:phT/jsRPBAEqjAibu1BurrabCBNTYiVI+zbmyCZJY6Q= github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d/go.mod h1:phT/jsRPBAEqjAibu1BurrabCBNTYiVI+zbmyCZJY6Q=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -278,8 +280,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be h1:dVIND0nXGXPQnFZYrMXT6CxHhBYhTPMm0GFqcmfaIC4= github.com/lum8rjack/go-ja4h v0.0.0-20250828030157-fa5266d50650 h1:hhx/Mo6+Hk0mAQS5MW311ON1VlSzp0D1cYhY27IcmnI=
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be/go.mod h1:q68TUR45WDa2r3yU4aO6WgxfCc0Vj1qtRaKaRE3yMLM= github.com/lum8rjack/go-ja4h v0.0.0-20250828030157-fa5266d50650/go.mod h1:bMqyXOakqQIdx82d4vcnk5TIZLptZ2gLqju9xmPrWYA=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
@@ -318,6 +320,8 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
github.com/nikandfor/spintax v0.0.0-20181023094358-fc346b245bb3 h1:foZ9X1bz2KmW7b8Yx5V0LAQKhTazdllv5rnGUe6iGTY=
github.com/nikandfor/spintax v0.0.0-20181023094358-fc346b245bb3/go.mod h1:wwDYKfVF3WHdY0rugsAZoIpyQjDA3bn9wEzo/QXPx1Y=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -339,12 +343,12 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -355,8 +359,8 @@ github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA= github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM= github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -393,12 +397,12 @@ github.com/suzuki-shunsuke/pinact v1.6.0 h1:2QvSzREOquwLwKXhF9Hj0AInE/Rl63SZz9dK
github.com/suzuki-shunsuke/pinact v1.6.0/go.mod h1:FDUMck0mmL0mcnNZ23Vjh/aOR5cIdZhF1IIpGksT4dQ= github.com/suzuki-shunsuke/pinact v1.6.0/go.mod h1:FDUMck0mmL0mcnNZ23Vjh/aOR5cIdZhF1IIpGksT4dQ=
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 h1:YGHgrVjGTYHY98II6zijXUHP+OyvrzSCvd8m9iUcaK8= github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 h1:YGHgrVjGTYHY98II6zijXUHP+OyvrzSCvd8m9iUcaK8=
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4/go.mod h1:sSi6xaUaHfaqu32ECLeyE7NTMv+ZM5dW0JikhllaalY= github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4/go.mod h1:sSi6xaUaHfaqu32ECLeyE7NTMv+ZM5dW0JikhllaalY=
github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg= github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
@@ -411,8 +415,6 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAz
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@@ -420,75 +422,64 @@ gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/Yjui
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/exp/typeparams v0.0.0-20250718183923-645b1fa84792 h1:54/e+WfmhvjR2Zuz8Q7dzLGxIBM+s5WZpvo1QfVDGB8= golang.org/x/exp/typeparams v0.0.0-20250718183923-645b1fa84792 h1:54/e+WfmhvjR2Zuz8Q7dzLGxIBM+s5WZpvo1QfVDGB8=
golang.org/x/exp/typeparams v0.0.0-20250718183923-645b1fa84792/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ= golang.org/x/exp/typeparams v0.0.0-20250718183923-645b1fa84792/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -496,6 +487,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -503,17 +495,17 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20250721140356-96f361d9aaf7 h1:Z53b3vgJH20Us6ljHm4MNVLnJzJEjD3KrU+sNcT4vfs= golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
golang.org/x/telemetry v0.0.0-20250721140356-96f361d9aaf7/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -521,38 +513,34 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -569,15 +557,15 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ= pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE= pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE=
pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4= pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=
pault.ag/go/topsort v0.1.1/go.mod h1:r1kc/L0/FZ3HhjezBIPaNVhkqv8L0UJ9bxRuHRVZ0q4= pault.ag/go/topsort v0.1.1/go.mod h1:r1kc/L0/FZ3HhjezBIPaNVhkqv8L0UJ9bxRuHRVZ0q4=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

33
internal/clampip.go Normal file
View File

@@ -0,0 +1,33 @@
package internal
import "net/netip"
func ClampIP(addr netip.Addr) (netip.Prefix, bool) {
switch {
case addr.Is4():
result, err := addr.Prefix(24)
if err != nil {
return netip.Prefix{}, false
}
return result, true
case addr.Is4In6():
// Extract the IPv4 address from IPv4-mapped IPv6 and clamp it
ipv4 := addr.Unmap()
result, err := ipv4.Prefix(24)
if err != nil {
return netip.Prefix{}, false
}
return result, true
case addr.Is6():
result, err := addr.Prefix(48)
if err != nil {
return netip.Prefix{}, false
}
return result, true
default:
return netip.Prefix{}, false
}
}

274
internal/clampip_test.go Normal file
View File

@@ -0,0 +1,274 @@
package internal
import (
"net/netip"
"testing"
)
func TestClampIP(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
// IPv4 addresses
{
name: "IPv4 normal address",
input: "192.168.1.100",
expected: "192.168.1.0/24",
},
{
name: "IPv4 boundary - network address",
input: "192.168.1.0",
expected: "192.168.1.0/24",
},
{
name: "IPv4 boundary - broadcast address",
input: "192.168.1.255",
expected: "192.168.1.0/24",
},
{
name: "IPv4 class A address",
input: "10.0.0.1",
expected: "10.0.0.0/24",
},
{
name: "IPv4 loopback",
input: "127.0.0.1",
expected: "127.0.0.0/24",
},
{
name: "IPv4 link-local",
input: "169.254.0.1",
expected: "169.254.0.0/24",
},
{
name: "IPv4 public address",
input: "203.0.113.1",
expected: "203.0.113.0/24",
},
// IPv6 addresses
{
name: "IPv6 normal address",
input: "2001:db8::1",
expected: "2001:db8::/48",
},
{
name: "IPv6 with full expansion",
input: "2001:0db8:0000:0000:0000:0000:0000:0001",
expected: "2001:db8::/48",
},
{
name: "IPv6 loopback",
input: "::1",
expected: "::/48",
},
{
name: "IPv6 unspecified address",
input: "::",
expected: "::/48",
},
{
name: "IPv6 link-local",
input: "fe80::1",
expected: "fe80::/48",
},
{
name: "IPv6 unique local",
input: "fc00::1",
expected: "fc00::/48",
},
{
name: "IPv6 documentation prefix",
input: "2001:db8:abcd:ef01::1234",
expected: "2001:db8:abcd::/48",
},
{
name: "IPv6 global unicast",
input: "2606:4700:4700::1111",
expected: "2606:4700:4700::/48",
},
{
name: "IPv6 multicast",
input: "ff02::1",
expected: "ff02::/48",
},
// IPv4-mapped IPv6 addresses
{
name: "IPv4-mapped IPv6 address",
input: "::ffff:192.168.1.100",
expected: "192.168.1.0/24",
},
{
name: "IPv4-mapped IPv6 with different format",
input: "::ffff:10.0.0.1",
expected: "10.0.0.0/24",
},
{
name: "IPv4-mapped IPv6 loopback",
input: "::ffff:127.0.0.1",
expected: "127.0.0.0/24",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := netip.MustParseAddr(tt.input)
result, ok := ClampIP(addr)
if !ok {
t.Fatalf("ClampIP(%s) returned false, want true", tt.input)
}
if result.String() != tt.expected {
t.Errorf("ClampIP(%s) = %s, want %s", tt.input, result.String(), tt.expected)
}
})
}
}
func TestClampIPSuccess(t *testing.T) {
// Test that valid inputs return success
tests := []struct {
name string
input string
}{
{
name: "IPv4 address",
input: "192.168.1.100",
},
{
name: "IPv6 address",
input: "2001:db8::1",
},
{
name: "IPv4-mapped IPv6",
input: "::ffff:192.168.1.100",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := netip.MustParseAddr(tt.input)
result, ok := ClampIP(addr)
if !ok {
t.Fatalf("ClampIP(%s) returned false, want true", tt.input)
}
// For valid inputs, we should get the clamped prefix
if addr.Is4() || addr.Is4In6() {
if result.Bits() != 24 {
t.Errorf("Expected 24 bits for IPv4, got %d", result.Bits())
}
} else if addr.Is6() {
if result.Bits() != 48 {
t.Errorf("Expected 48 bits for IPv6, got %d", result.Bits())
}
}
})
}
}
func TestClampIPZeroValue(t *testing.T) {
// Test that when ClampIP fails, it returns zero value
// Note: It's hard to make addr.Prefix() fail with valid inputs,
// so this test demonstrates the expected behavior
addr := netip.MustParseAddr("192.168.1.100")
// Manually create a zero value for comparison
zeroPrefix := netip.Prefix{}
// Call ClampIP - it should succeed with valid input
result, ok := ClampIP(addr)
// Verify the function succeeded
if !ok {
t.Error("ClampIP should succeed with valid input")
}
// Verify that the result is not a zero value
if result == zeroPrefix {
t.Error("Result should not be zero value for successful operation")
}
}
func TestClampIPSpecialCases(t *testing.T) {
tests := []struct {
name string
input string
expectedPrefix int
expectedNetwork string
}{
{
name: "Minimum IPv4",
input: "0.0.0.0",
expectedPrefix: 24,
expectedNetwork: "0.0.0.0",
},
{
name: "Maximum IPv4",
input: "255.255.255.255",
expectedPrefix: 24,
expectedNetwork: "255.255.255.0",
},
{
name: "Minimum IPv6",
input: "::",
expectedPrefix: 48,
expectedNetwork: "::",
},
{
name: "Maximum IPv6 prefix part",
input: "ffff:ffff:ffff::",
expectedPrefix: 48,
expectedNetwork: "ffff:ffff:ffff::",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := netip.MustParseAddr(tt.input)
result, ok := ClampIP(addr)
if !ok {
t.Fatalf("ClampIP(%s) returned false, want true", tt.input)
}
if result.Bits() != tt.expectedPrefix {
t.Errorf("ClampIP(%s) bits = %d, want %d", tt.input, result.Bits(), tt.expectedPrefix)
}
if result.Addr().String() != tt.expectedNetwork {
t.Errorf("ClampIP(%s) network = %s, want %s", tt.input, result.Addr().String(), tt.expectedNetwork)
}
})
}
}
// Benchmark to ensure the function is performant
func BenchmarkClampIP(b *testing.B) {
ipv4 := netip.MustParseAddr("192.168.1.100")
ipv6 := netip.MustParseAddr("2001:db8::1")
ipv4mapped := netip.MustParseAddr("::ffff:192.168.1.100")
b.Run("IPv4", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ClampIP(ipv4)
}
})
b.Run("IPv6", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ClampIP(ipv6)
}
})
b.Run("IPv4-mapped", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ClampIP(ipv4mapped)
}
})
}

70
internal/dns/cache.go Normal file
View File

@@ -0,0 +1,70 @@
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)
}

174
internal/dns/dns.go Normal file
View File

@@ -0,0 +1,174 @@
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
}

308
internal/dns/dns_test.go Normal file
View File

@@ -0,0 +1,308 @@
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)
}
})
}

View File

@@ -1,6 +1,7 @@
package internal package internal
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -13,6 +14,13 @@ import (
"github.com/sebest/xff" "github.com/sebest/xff"
) )
type realIPKey struct{}
func RealIP(r *http.Request) (netip.Addr, bool) {
result, ok := r.Context().Value(realIPKey{}).(netip.Addr)
return result, ok
}
// TODO: move into config // TODO: move into config
type XFFComputePreferences struct { type XFFComputePreferences struct {
StripPrivate bool StripPrivate bool
@@ -77,6 +85,9 @@ func RemoteXRealIP(useRemoteAddress bool, bindNetwork string, next http.Handler)
panic(err) // this should never happen panic(err) // this should never happen
} }
r.Header.Set("X-Real-Ip", host) r.Header.Set("X-Real-Ip", host)
if addr, err := netip.ParseAddr(host); err == nil {
r = r.WithContext(context.WithValue(r.Context(), realIPKey{}, addr))
}
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
@@ -87,8 +98,11 @@ func XForwardedForToXRealIP(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if xffHeader := r.Header.Get("X-Forwarded-For"); r.Header.Get("X-Real-Ip") == "" && xffHeader != "" { if xffHeader := r.Header.Get("X-Forwarded-For"); r.Header.Get("X-Real-Ip") == "" && xffHeader != "" {
ip := xff.Parse(xffHeader) ip := xff.Parse(xffHeader)
slog.Debug("setting x-real-ip", "val", ip) slog.Debug("setting X-Real-Ip from X-Forwarded-For", "to", ip, "x-forwarded-for", xffHeader)
r.Header.Set("X-Real-Ip", ip) r.Header.Set("X-Real-Ip", ip)
if addr, err := netip.ParseAddr(ip); err == nil {
r = r.WithContext(context.WithValue(r.Context(), realIPKey{}, addr))
}
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

View File

@@ -0,0 +1,23 @@
package honeypot
import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var Timings = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "anubis",
Subsystem: "honeypot",
Name: "pagegen_timings",
Help: "The amount of time honeypot page generation takes per method",
Buckets: prometheus.ExponentialBuckets(0.5, 2, 32),
}, []string{"method"})
type Info struct {
CreatedAt time.Time `json:"createdAt"`
UserAgent string `json:"userAgent"`
IPAddress string `json:"ipAddress"`
HitCount int `json:"hitCount"`
}

View File

@@ -0,0 +1,7 @@
html {
max-width: 70ch;
padding: 3em 1em;
margin: auto;
line-height: 1.75;
font-size: 1.25em;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,205 @@
package naive
import (
"context"
_ "embed"
"fmt"
"log/slog"
"math/rand/v2"
"net/http"
"net/netip"
"time"
"github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/honeypot"
"github.com/TecharoHQ/anubis/lib/policy/checker"
"github.com/TecharoHQ/anubis/lib/store"
"github.com/a-h/templ"
"github.com/google/uuid"
"github.com/nikandfor/spintax"
)
//go:generate go tool github.com/a-h/templ/cmd/templ generate
// XXX(Xe): All of this was generated by ChatGPT, GLM 4.6, and GPT-OSS 120b. This is pseudoprofound bullshit in spintax[1] format so that the bullshit generator can emit plausibly human-authored text while being very computationally cheap.
//
// It feels somewhat poetic to use spammer technology in Anubis.
//
// [1]: https://outboundly.ai/blogs/what-is-spintax-and-how-to-use-it/
//
//go:embed spintext.txt
var spintext string
//go:embed titles.txt
var titles string
//go:embed affirmations.txt
var affirmations string
func New(st store.Interface, lg *slog.Logger) (*Impl, error) {
affirmation, err := spintax.Parse(affirmations)
if err != nil {
return nil, fmt.Errorf("can't parse affirmations: %w", err)
}
body, err := spintax.Parse(spintext)
if err != nil {
return nil, fmt.Errorf("can't parse bodies: %w", err)
}
title, err := spintax.Parse(titles)
if err != nil {
return nil, fmt.Errorf("can't parse titles: %w", err)
}
lg.Debug("initialized basic bullshit generator", "affirmations", affirmation.Count(), "bodies", body.Count(), "titles", title.Count())
return &Impl{
st: st,
infos: store.JSON[honeypot.Info]{Underlying: st, Prefix: "honeypot:info"},
uaWeight: store.JSON[int]{Underlying: st, Prefix: "honeypot:user-agent"},
networkWeight: store.JSON[int]{Underlying: st, Prefix: "honeypot:network"},
affirmation: affirmation,
body: body,
title: title,
lg: lg.With("component", "honeypot/naive"),
}, nil
}
type Impl struct {
st store.Interface
infos store.JSON[honeypot.Info]
uaWeight store.JSON[int]
networkWeight store.JSON[int]
lg *slog.Logger
affirmation, body, title spintax.Spintax
}
func (i *Impl) incrementUA(ctx context.Context, userAgent string) int {
result, _ := i.uaWeight.Get(ctx, internal.SHA256sum(userAgent))
result++
i.uaWeight.Set(ctx, internal.SHA256sum(userAgent), result, time.Hour)
return result
}
func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
result, _ := i.networkWeight.Get(ctx, internal.SHA256sum(network))
result++
i.networkWeight.Set(ctx, internal.SHA256sum(network), result, time.Hour)
return result
}
func (i *Impl) CheckUA() checker.Impl {
return checker.Func(func(r *http.Request) (bool, error) {
result, _ := i.uaWeight.Get(r.Context(), internal.SHA256sum(r.UserAgent()))
if result >= 25 {
return true, nil
}
return false, nil
})
}
func (i *Impl) CheckNetwork() checker.Impl {
return checker.Func(func(r *http.Request) (bool, error) {
result, _ := i.uaWeight.Get(r.Context(), internal.SHA256sum(r.UserAgent()))
if result >= 25 {
return true, nil
}
return false, nil
})
}
func (i *Impl) Hash() string {
return internal.SHA256sum("naive honeypot")
}
func (i *Impl) makeAffirmations() []string {
count := rand.IntN(5) + 1
var result []string
for j := 0; j < count; j++ {
result = append(result, i.affirmation.Spin())
}
return result
}
func (i *Impl) makeSpins() []string {
count := rand.IntN(5) + 1
var result []string
for j := 0; j < count; j++ {
result = append(result, i.body.Spin())
}
return result
}
func (i *Impl) makeTitle() string {
return i.title.Spin()
}
func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t0 := time.Now()
lg := internal.GetRequestLogger(i.lg, r)
id := r.PathValue("id")
if id == "" {
id = uuid.NewString()
}
realIP, _ := internal.RealIP(r)
if !realIP.IsValid() {
realIP = netip.MustParseAddr(r.Header.Get("X-Real-Ip"))
}
network, ok := internal.ClampIP(realIP)
if !ok {
lg.Error("clampIP failed", "output", network, "ok", ok)
http.Error(w, "The cake is a lie", http.StatusTeapot)
return
}
networkCount := i.incrementNetwork(r.Context(), network.String())
uaCount := i.incrementUA(r.Context(), r.UserAgent())
stage := r.PathValue("stage")
if stage == "init" {
lg.Debug("found new entrance point", "id", id, "stage", stage, "userAgent", r.UserAgent(), "clampedIP", network)
} else {
switch {
case networkCount%256 == 0, uaCount%256 == 0:
lg.Warn("found possible crawler", "id", id, "network", network)
}
}
spins := i.makeSpins()
affirmations := i.makeAffirmations()
title := i.makeTitle()
var links []link
for _, affirmation := range affirmations {
links = append(links, link{
href: uuid.NewString(),
body: affirmation,
})
}
templ.Handler(
base(title, i.maze(spins, links)),
templ.WithStreaming(),
templ.WithStatus(http.StatusOK),
).ServeHTTP(w, r)
t1 := time.Since(t0)
honeypot.Timings.WithLabelValues("naive").Observe(float64(t1.Milliseconds()))
}
type link struct {
href string
body string
}

View File

@@ -0,0 +1,36 @@
package naive
import "fmt"
templ base(title string, body templ.Component) {
<!DOCTYPE html>
<html>
<head>
<style>
html {
max-width: 70ch;
padding: 3em 1em;
margin: auto;
line-height: 1.75;
font-size: 1.25em;
}
</style>
<title>{ title }</title>
</head>
<body>
<h1>{ title }</h1>
@body
</body>
</html>
}
templ (i Impl) maze(body []string, links []link) {
for _, paragraph := range body {
<p>{ paragraph }</p>
}
<ul>
for _, link := range links {
<li><a href={ templ.SafeURL(fmt.Sprintf("./%s", link.href)) }>{ link.body }</a></li>
}
</ul>
}

160
internal/honeypot/naive/page_templ.go generated Normal file
View File

@@ -0,0 +1,160 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package naive
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
func base(title string, body templ.Component) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html><head><style>\n html {\n max-width: 70ch;\n padding: 3em 1em;\n margin: auto;\n line-height: 1.75;\n font-size: 1.25em;\n }\n </style><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `page.templ`, Line: 18, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title></head><body><h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `page.templ`, Line: 21, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = body.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func (i Impl) maze(body []string, links []link) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
for _, paragraph := range body {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(paragraph)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `page.templ`, Line: 29, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, link := range links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 templ.SafeURL
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("./%s", link.href)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `page.templ`, Line: 33, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(link.body)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `page.templ`, Line: 33, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

39
internal/listor.go Normal file
View File

@@ -0,0 +1,39 @@
package internal
import (
"encoding/json"
)
// ListOr[T any] is a slice that can contain either a single T or multiple T values.
// During JSON unmarshaling, it checks if the first character is '[' to determine
// whether to treat the JSON as an array or a single value.
type ListOr[T any] []T
func (lo *ListOr[T]) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
// Check if first non-whitespace character is '['
firstChar := data[0]
for i := 0; i < len(data); i++ {
if data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
firstChar = data[i]
break
}
}
if firstChar == '[' {
// It's an array, unmarshal directly
return json.Unmarshal(data, (*[]T)(lo))
} else {
// It's a single value, unmarshal as a single item in a slice
var single T
if err := json.Unmarshal(data, &single); err != nil {
return err
}
*lo = ListOr[T]{single}
}
return nil
}

79
internal/listor_test.go Normal file
View File

@@ -0,0 +1,79 @@
package internal
import (
"encoding/json"
"testing"
)
func TestListOr_UnmarshalJSON(t *testing.T) {
t.Run("single value should be unmarshaled as single item", func(t *testing.T) {
var lo ListOr[string]
err := json.Unmarshal([]byte(`"hello"`), &lo)
if err != nil {
t.Fatalf("Failed to unmarshal single string: %v", err)
}
if len(lo) != 1 {
t.Fatalf("Expected 1 item, got %d", len(lo))
}
if lo[0] != "hello" {
t.Errorf("Expected 'hello', got %q", lo[0])
}
})
t.Run("array should be unmarshaled as multiple items", func(t *testing.T) {
var lo ListOr[string]
err := json.Unmarshal([]byte(`["hello", "world"]`), &lo)
if err != nil {
t.Fatalf("Failed to unmarshal array: %v", err)
}
if len(lo) != 2 {
t.Fatalf("Expected 2 items, got %d", len(lo))
}
if lo[0] != "hello" {
t.Errorf("Expected 'hello', got %q", lo[0])
}
if lo[1] != "world" {
t.Errorf("Expected 'world', got %q", lo[1])
}
})
t.Run("single number should be unmarshaled as single item", func(t *testing.T) {
var lo ListOr[int]
err := json.Unmarshal([]byte(`42`), &lo)
if err != nil {
t.Fatalf("Failed to unmarshal single number: %v", err)
}
if len(lo) != 1 {
t.Fatalf("Expected 1 item, got %d", len(lo))
}
if lo[0] != 42 {
t.Errorf("Expected 42, got %d", lo[0])
}
})
t.Run("array of numbers should be unmarshaled as multiple items", func(t *testing.T) {
var lo ListOr[int]
err := json.Unmarshal([]byte(`[1, 2, 3]`), &lo)
if err != nil {
t.Fatalf("Failed to unmarshal number array: %v", err)
}
if len(lo) != 3 {
t.Fatalf("Expected 3 items, got %d", len(lo))
}
if lo[0] != 1 || lo[1] != 2 || lo[2] != 3 {
t.Errorf("Expected [1, 2, 3], got %v", lo)
}
})
}

View File

@@ -2,6 +2,7 @@ package internal
import ( import (
"fmt" "fmt"
"io"
"log" "log"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -9,7 +10,7 @@ import (
"strings" "strings"
) )
func InitSlog(level string) { func InitSlog(level string, sink io.Writer) *slog.Logger {
var programLevel slog.Level var programLevel slog.Level
if err := (&programLevel).UnmarshalText([]byte(level)); err != nil { if err := (&programLevel).UnmarshalText([]byte(level)); err != nil {
fmt.Fprintf(os.Stderr, "invalid log level %s: %v, using info\n", level, err) fmt.Fprintf(os.Stderr, "invalid log level %s: %v, using info\n", level, err)
@@ -19,11 +20,12 @@ func InitSlog(level string) {
leveler := &slog.LevelVar{} leveler := &slog.LevelVar{}
leveler.Set(programLevel) leveler.Set(programLevel)
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ h := slog.NewJSONHandler(sink, &slog.HandlerOptions{
AddSource: true, AddSource: true,
Level: leveler, Level: leveler,
}) })
slog.SetDefault(slog.New(h)) result := slog.New(h)
return result
} }
func GetRequestLogger(base *slog.Logger, r *http.Request) *slog.Logger { func GetRequestLogger(base *slog.Logger, r *http.Request) *slog.Logger {
@@ -39,8 +41,7 @@ func GetRequestLogger(base *slog.Logger, r *http.Request) *slog.Logger {
"user_agent", r.UserAgent(), "user_agent", r.UserAgent(),
"accept_language", r.Header.Get("Accept-Language"), "accept_language", r.Header.Get("Accept-Language"),
"priority", r.Header.Get("Priority"), "priority", r.Header.Get("Priority"),
"x-forwarded-for", "x-forwarded-for", r.Header.Get("X-Forwarded-For"),
r.Header.Get("X-Forwarded-For"),
"x-real-ip", r.Header.Get("X-Real-Ip"), "x-real-ip", r.Header.Get("X-Real-Ip"),
) )
} }

View File

@@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/store" "github.com/TecharoHQ/anubis/lib/store"
"github.com/TecharoHQ/anubis/lib/store/memory" "github.com/TecharoHQ/anubis/lib/store/memory"
) )
@@ -24,7 +24,7 @@ func TestCacheReturnsDefault(t *testing.T) {
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
Override: want, Override: want,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
u, err := url.Parse("https://anubis.techaro.lol") u, err := url.Parse("https://anubis.techaro.lol")
if err != nil { if err != nil {
@@ -52,7 +52,7 @@ func TestCheckCache(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
// Set up test data // Set up test data
urlStr := "http://example.com/page" urlStr := "http://example.com/page"
@@ -115,7 +115,7 @@ func TestGetOGTags(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
// Parse the test server URL // Parse the test server URL
parsedURL, err := url.Parse(ts.URL) parsedURL, err := url.Parse(ts.URL)
@@ -271,7 +271,7 @@ func TestGetOGTagsWithHostConsideration(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: tc.ogCacheConsiderHost, ConsiderHost: tc.ogCacheConsiderHost,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
for i, req := range tc.requests { for i, req := range tc.requests {
ogTags, err := cache.GetOGTags(t.Context(), parsedURL, req.host) ogTags, err := cache.GetOGTags(t.Context(), parsedURL, req.host)

View File

@@ -27,16 +27,29 @@ func (c *OGTagCache) fetchHTMLDocumentWithCache(ctx context.Context, urlStr stri
} }
// Set the Host header to the original host // Set the Host header to the original host
if originalHost != "" { var hostForRequest string
req.Host = originalHost switch {
case c.targetHost != "":
hostForRequest = c.targetHost
case originalHost != "":
hostForRequest = originalHost
}
if hostForRequest != "" {
req.Host = hostForRequest
} }
// Add proxy headers // Add proxy headers
req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("User-Agent", "Anubis-OGTag-Fetcher/1.0") // For tracking purposes req.Header.Set("User-Agent", "Anubis-OGTag-Fetcher/1.0") // For tracking purposes
serverName := hostForRequest
if serverName == "" {
serverName = req.URL.Hostname()
}
client := c.clientForSNI(serverName)
// Send the request // Send the request
resp, err := c.client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
var netErr net.Error var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { if errors.As(err, &netErr) && netErr.Timeout() {

View File

@@ -11,7 +11,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/store/memory" "github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -87,7 +87,7 @@ func TestFetchHTMLDocument(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
doc, err := cache.fetchHTMLDocument(t.Context(), ts.URL, "anything") doc, err := cache.fetchHTMLDocument(t.Context(), ts.URL, "anything")
if tt.expectError { if tt.expectError {
@@ -118,7 +118,7 @@ func TestFetchHTMLDocumentInvalidURL(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
doc, err := cache.fetchHTMLDocument(t.Context(), "http://invalid.url.that.doesnt.exist.example", "anything") doc, err := cache.fetchHTMLDocument(t.Context(), "http://invalid.url.that.doesnt.exist.example", "anything")

View File

@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/store/memory" "github.com/TecharoHQ/anubis/lib/store/memory"
) )
@@ -111,7 +111,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
// Create URL for test // Create URL for test
testURL, _ := url.Parse(ts.URL) testURL, _ := url.Parse(ts.URL)

View File

@@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/store/memory" "github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -31,7 +31,7 @@ func BenchmarkGetTarget(b *testing.B) {
for _, tt := range tests { for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) { b.Run(tt.name, func(b *testing.B) {
cache := NewOGTagCache(tt.target, config.OpenGraph{}, memory.New(b.Context())) cache := NewOGTagCache(tt.target, config.OpenGraph{}, memory.New(b.Context()), TargetOptions{})
urls := make([]*url.URL, len(tt.paths)) urls := make([]*url.URL, len(tt.paths))
for i, path := range tt.paths { for i, path := range tt.paths {
u, _ := url.Parse(path) u, _ := url.Parse(path)
@@ -67,7 +67,7 @@ func BenchmarkExtractOGTags(b *testing.B) {
</head><body><div><p>Content</p></div></body></html>`, </head><body><div><p>Content</p></div></body></html>`,
} }
cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(b.Context())) cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(b.Context()), TargetOptions{})
docs := make([]*html.Node, len(htmlSamples)) docs := make([]*html.Node, len(htmlSamples))
for i, sample := range htmlSamples { for i, sample := range htmlSamples {
@@ -85,7 +85,7 @@ func BenchmarkExtractOGTags(b *testing.B) {
// Memory usage test // Memory usage test
func TestMemoryUsage(t *testing.T) { func TestMemoryUsage(t *testing.T) {
cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(t.Context())) cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(t.Context()), TargetOptions{})
// Force GC and wait for it to complete // Force GC and wait for it to complete
runtime.GC() runtime.GC()

View File

@@ -2,14 +2,16 @@ package ogtags
import ( import (
"context" "context"
"crypto/tls"
"log/slog" "log/slog"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"sync"
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/store" "github.com/TecharoHQ/anubis/lib/store"
) )
@@ -22,21 +24,34 @@ const (
) )
type OGTagCache struct { type OGTagCache struct {
cache store.JSON[map[string]string] ogOverride map[string]string
targetURL *url.URL targetURL *url.URL
client *http.Client client *http.Client
transport *http.Transport
cache store.JSON[map[string]string]
// Pre-built strings for optimization // Pre-built strings for optimization
unixPrefix string // "http://unix" unixPrefix string // "http://unix"
approvedTags []string targetSNI string
targetHost string
approvedPrefixes []string approvedPrefixes []string
approvedTags []string
ogTimeToLive time.Duration ogTimeToLive time.Duration
ogCacheConsiderHost bool
ogPassthrough bool ogPassthrough bool
ogOverride map[string]string ogCacheConsiderHost bool
targetSNIAuto bool
insecureSkipVerify bool
sniClients map[string]*http.Client
transportMu sync.RWMutex
} }
func NewOGTagCache(target string, conf config.OpenGraph, backend store.Interface) *OGTagCache { type TargetOptions struct {
Host string
SNI string
InsecureSkipVerify bool
}
func NewOGTagCache(target string, conf config.OpenGraph, backend store.Interface, targetOpts TargetOptions) *OGTagCache {
// Predefined approved tags and prefixes // Predefined approved tags and prefixes
defaultApprovedTags := []string{"description", "keywords", "author"} defaultApprovedTags := []string{"description", "keywords", "author"}
defaultApprovedPrefixes := []string{"og:", "twitter:", "fediverse:"} defaultApprovedPrefixes := []string{"og:", "twitter:", "fediverse:"}
@@ -62,20 +77,37 @@ func NewOGTagCache(target string, conf config.OpenGraph, backend store.Interface
} }
} }
client := &http.Client{ transport := http.DefaultTransport.(*http.Transport).Clone()
Timeout: httpTimeout,
}
// Configure custom transport for Unix sockets // Configure custom transport for Unix sockets
if parsedTargetURL.Scheme == "unix" { if parsedTargetURL.Scheme == "unix" {
socketPath := parsedTargetURL.Path // For unix scheme, path is the socket path socketPath := parsedTargetURL.Path // For unix scheme, path is the socket path
client.Transport = &http.Transport{ transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", socketPath)
return net.Dial("unix", socketPath)
},
} }
} }
targetSNIAuto := targetOpts.SNI == "auto"
if targetOpts.SNI != "" && !targetSNIAuto {
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.ServerName = targetOpts.SNI
}
if targetOpts.InsecureSkipVerify {
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.InsecureSkipVerify = true
}
client := &http.Client{
Timeout: httpTimeout,
Transport: transport,
}
return &OGTagCache{ return &OGTagCache{
cache: store.JSON[map[string]string]{ cache: store.JSON[map[string]string]{
Underlying: backend, Underlying: backend,
@@ -89,7 +121,13 @@ func NewOGTagCache(target string, conf config.OpenGraph, backend store.Interface
approvedTags: defaultApprovedTags, approvedTags: defaultApprovedTags,
approvedPrefixes: defaultApprovedPrefixes, approvedPrefixes: defaultApprovedPrefixes,
client: client, client: client,
transport: transport,
unixPrefix: "http://unix", unixPrefix: "http://unix",
targetHost: targetOpts.Host,
targetSNI: targetOpts.SNI,
targetSNIAuto: targetSNIAuto,
insecureSkipVerify: targetOpts.InsecureSkipVerify,
sniClients: make(map[string]*http.Client),
} }
} }

View File

@@ -7,7 +7,7 @@ import (
"testing" "testing"
"unicode/utf8" "unicode/utf8"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/store/memory" "github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -48,7 +48,7 @@ func FuzzGetTarget(f *testing.F) {
} }
// Create cache - should not panic // Create cache - should not panic
cache := NewOGTagCache(target, config.OpenGraph{}, memory.New(context.Background())) cache := NewOGTagCache(target, config.OpenGraph{}, memory.New(context.Background()), TargetOptions{})
// Create URL // Create URL
u := &url.URL{ u := &url.URL{
@@ -132,7 +132,7 @@ func FuzzExtractOGTags(f *testing.F) {
return return
} }
cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(context.Background())) cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(context.Background()), TargetOptions{})
// Should not panic // Should not panic
tags := cache.extractOGTags(doc) tags := cache.extractOGTags(doc)
@@ -188,7 +188,7 @@ func FuzzGetTargetRoundTrip(f *testing.F) {
t.Skip() t.Skip()
} }
cache := NewOGTagCache(target, config.OpenGraph{}, memory.New(context.Background())) cache := NewOGTagCache(target, config.OpenGraph{}, memory.New(context.Background()), TargetOptions{})
u := &url.URL{Path: path, RawQuery: query} u := &url.URL{Path: path, RawQuery: query}
result := cache.getTarget(u) result := cache.getTarget(u)
@@ -245,7 +245,7 @@ func FuzzExtractMetaTagInfo(f *testing.F) {
}, },
} }
cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(context.Background())) cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(context.Background()), TargetOptions{})
// Should not panic // Should not panic
property, content := cache.extractMetaTagInfo(node) property, content := cache.extractMetaTagInfo(node)
@@ -298,7 +298,7 @@ func BenchmarkFuzzedGetTarget(b *testing.B) {
for _, input := range inputs { for _, input := range inputs {
b.Run(input.name, func(b *testing.B) { b.Run(input.name, func(b *testing.B) {
cache := NewOGTagCache(input.target, config.OpenGraph{}, memory.New(context.Background())) cache := NewOGTagCache(input.target, config.OpenGraph{}, memory.New(context.Background()), TargetOptions{})
u := &url.URL{Path: input.path, RawQuery: input.query} u := &url.URL{Path: input.path, RawQuery: input.query}
b.ResetTimer() b.ResetTimer()

View File

@@ -2,19 +2,27 @@ package ogtags
import ( import (
"context" "context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"errors" "errors"
"fmt" "fmt"
"math/big"
"net" "net"
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/store/memory" "github.com/TecharoHQ/anubis/lib/store/memory"
) )
@@ -45,7 +53,7 @@ func TestNewOGTagCache(t *testing.T) {
Enabled: tt.ogPassthrough, Enabled: tt.ogPassthrough,
TimeToLive: tt.ogTimeToLive, TimeToLive: tt.ogTimeToLive,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
if cache == nil { if cache == nil {
t.Fatal("expected non-nil cache, got nil") t.Fatal("expected non-nil cache, got nil")
@@ -85,7 +93,7 @@ func TestNewOGTagCache_UnixSocket(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: 5 * time.Minute, TimeToLive: 5 * time.Minute,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
if cache == nil { if cache == nil {
t.Fatal("expected non-nil cache, got nil") t.Fatal("expected non-nil cache, got nil")
@@ -170,7 +178,7 @@ func TestGetTarget(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
u := &url.URL{ u := &url.URL{
Path: tt.path, Path: tt.path,
@@ -243,7 +251,7 @@ func TestIntegrationGetOGTags_UnixSocket(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
// Create a dummy URL for the request (path and query matter) // Create a dummy URL for the request (path and query matter)
testReqURL, _ := url.Parse("/some/page?query=1") testReqURL, _ := url.Parse("/some/page?query=1")
@@ -274,3 +282,244 @@ func TestIntegrationGetOGTags_UnixSocket(t *testing.T) {
t.Errorf("Expected cached OG tags %v, got %v", expectedTags, cachedTags) t.Errorf("Expected cached OG tags %v, got %v", expectedTags, cachedTags)
} }
} }
func TestGetOGTagsWithTargetHostOverride(t *testing.T) {
originalHost := "example.test"
overrideHost := "backend.internal"
seenHosts := make(chan string, 10)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenHosts <- r.Host
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, `<!DOCTYPE html><html><head><meta property="og:title" content="HostOverride" /></head><body>ok</body></html>`)
}))
defer ts.Close()
targetURL, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("failed to parse server URL: %v", err)
}
conf := config.OpenGraph{
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}
t.Run("default host uses original", func(t *testing.T) {
cache := NewOGTagCache(ts.URL, conf, memory.New(t.Context()), TargetOptions{})
if _, err := cache.GetOGTags(t.Context(), targetURL, originalHost); err != nil {
t.Fatalf("GetOGTags failed: %v", err)
}
select {
case host := <-seenHosts:
if host != originalHost {
t.Fatalf("expected host %q, got %q", originalHost, host)
}
case <-time.After(time.Second):
t.Fatal("server did not receive request")
}
})
t.Run("override host respected", func(t *testing.T) {
cache := NewOGTagCache(ts.URL, conf, memory.New(t.Context()), TargetOptions{
Host: overrideHost,
})
if _, err := cache.GetOGTags(t.Context(), targetURL, originalHost); err != nil {
t.Fatalf("GetOGTags failed: %v", err)
}
select {
case host := <-seenHosts:
if host != overrideHost {
t.Fatalf("expected host %q, got %q", overrideHost, host)
}
case <-time.After(time.Second):
t.Fatal("server did not receive request")
}
})
}
func TestGetOGTagsWithInsecureSkipVerify(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, `<!DOCTYPE html><html><head><meta property="og:title" content="Self-Signed" /></head><body>hello</body></html>`)
})
ts := httptest.NewTLSServer(handler)
defer ts.Close()
parsedURL, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("failed to parse server URL: %v", err)
}
conf := config.OpenGraph{
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}
// Without skip verify we should get a TLS error
cacheStrict := NewOGTagCache(ts.URL, conf, memory.New(t.Context()), TargetOptions{})
if _, err := cacheStrict.GetOGTags(t.Context(), parsedURL, parsedURL.Host); err == nil {
t.Fatal("expected TLS verification error without InsecureSkipVerify")
}
cacheSkip := NewOGTagCache(ts.URL, conf, memory.New(t.Context()), TargetOptions{
InsecureSkipVerify: true,
})
tags, err := cacheSkip.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
if err != nil {
t.Fatalf("expected successful fetch with InsecureSkipVerify, got: %v", err)
}
if tags["og:title"] != "Self-Signed" {
t.Fatalf("expected og:title to be %q, got %q", "Self-Signed", tags["og:title"])
}
}
func TestGetOGTagsWithTargetSNI(t *testing.T) {
originalHost := "hecate.test"
conf := config.OpenGraph{
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}
t.Run("explicit SNI override", func(t *testing.T) {
expectedSNI := "backend.internal"
ts, recorder := newSNIServer(t, `<!DOCTYPE html><html><head><meta property="og:title" content="SNI Works" /></head><body>ok</body></html>`)
defer ts.Close()
targetURL, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("failed to parse server URL: %v", err)
}
cacheExplicit := NewOGTagCache(ts.URL, conf, memory.New(t.Context()), TargetOptions{
SNI: expectedSNI,
InsecureSkipVerify: true,
})
if _, err := cacheExplicit.GetOGTags(t.Context(), targetURL, originalHost); err != nil {
t.Fatalf("expected successful fetch with explicit SNI, got: %v", err)
}
if got := recorder.last(); got != expectedSNI {
t.Fatalf("expected server to see SNI %q, got %q", expectedSNI, got)
}
})
t.Run("auto SNI uses original host", func(t *testing.T) {
ts, recorder := newSNIServer(t, `<!DOCTYPE html><html><head><meta property="og:title" content="SNI Auto" /></head><body>ok</body></html>`)
defer ts.Close()
targetURL, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("failed to parse server URL: %v", err)
}
cacheAuto := NewOGTagCache(ts.URL, conf, memory.New(t.Context()), TargetOptions{
SNI: "auto",
InsecureSkipVerify: true,
})
if _, err := cacheAuto.GetOGTags(t.Context(), targetURL, originalHost); err != nil {
t.Fatalf("expected successful fetch with auto SNI, got: %v", err)
}
if got := recorder.last(); got != originalHost {
t.Fatalf("expected server to see SNI %q with auto, got %q", originalHost, got)
}
})
t.Run("default SNI uses backend host", func(t *testing.T) {
ts, recorder := newSNIServer(t, `<!DOCTYPE html><html><head><meta property="og:title" content="SNI Default" /></head><body>ok</body></html>`)
defer ts.Close()
targetURL, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("failed to parse server URL: %v", err)
}
cacheDefault := NewOGTagCache(ts.URL, conf, memory.New(t.Context()), TargetOptions{
InsecureSkipVerify: true,
})
if _, err := cacheDefault.GetOGTags(t.Context(), targetURL, originalHost); err != nil {
t.Fatalf("expected successful fetch without explicit SNI, got: %v", err)
}
wantSNI := ""
if net.ParseIP(targetURL.Hostname()) == nil {
wantSNI = targetURL.Hostname()
}
if got := recorder.last(); got != wantSNI {
t.Fatalf("expected default SNI %q, got %q", wantSNI, got)
}
})
}
func newSNIServer(t *testing.T, body string) (*httptest.Server, *sniRecorder) {
t.Helper()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, body)
})
recorder := &sniRecorder{}
ts := httptest.NewUnstartedServer(handler)
cert := mustCertificateForHost(t, "sni.test")
ts.TLS = &tls.Config{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
recorder.record(hello.ServerName)
return &cert, nil
},
}
ts.StartTLS()
return ts, recorder
}
func mustCertificateForHost(t *testing.T, host string) tls.Certificate {
t.Helper()
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: host,
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
DNSNames: []string{host},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
if err != nil {
t.Fatalf("failed to create certificate: %v", err)
}
return tls.Certificate{
Certificate: [][]byte{der},
PrivateKey: priv,
}
}
type sniRecorder struct {
mu sync.Mutex
names []string
}
func (r *sniRecorder) record(name string) {
r.mu.Lock()
defer r.mu.Unlock()
r.names = append(r.names, name)
}
func (r *sniRecorder) last() string {
r.mu.Lock()
defer r.mu.Unlock()
if len(r.names) == 0 {
return ""
}
return r.names[len(r.names)-1]
}

View File

@@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/store/memory" "github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -18,7 +18,7 @@ func TestExtractOGTags(t *testing.T) {
Enabled: false, Enabled: false,
ConsiderHost: false, ConsiderHost: false,
TimeToLive: time.Minute, TimeToLive: time.Minute,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
// Manually set approved tags/prefixes based on the user request for clarity // Manually set approved tags/prefixes based on the user request for clarity
testCache.approvedTags = []string{"description"} testCache.approvedTags = []string{"description"}
testCache.approvedPrefixes = []string{"og:"} testCache.approvedPrefixes = []string{"og:"}
@@ -199,7 +199,7 @@ func TestExtractMetaTagInfo(t *testing.T) {
Enabled: false, Enabled: false,
ConsiderHost: false, ConsiderHost: false,
TimeToLive: time.Minute, TimeToLive: time.Minute,
}, memory.New(t.Context())) }, memory.New(t.Context()), TargetOptions{})
testCache.approvedTags = []string{"description"} testCache.approvedTags = []string{"description"}
testCache.approvedPrefixes = []string{"og:"} testCache.approvedPrefixes = []string{"og:"}

42
internal/ogtags/sni.go Normal file
View File

@@ -0,0 +1,42 @@
package ogtags
import (
"crypto/tls"
"net/http"
)
// clientForSNI returns a cached client for the given server name, creating one if needed.
func (c *OGTagCache) clientForSNI(serverName string) *http.Client {
if !c.targetSNIAuto || serverName == "" {
return c.client
}
c.transportMu.RLock()
cli, ok := c.sniClients[serverName]
c.transportMu.RUnlock()
if ok {
return cli
}
c.transportMu.Lock()
defer c.transportMu.Unlock()
if cli, ok := c.sniClients[serverName]; ok {
return cli
}
tr := c.transport.Clone()
if tr.TLSClientConfig == nil {
tr.TLSClientConfig = &tls.Config{}
}
tr.TLSClientConfig.ServerName = serverName
if c.insecureSkipVerify {
tr.TLSClientConfig.InsecureSkipVerify = true
}
cli = &http.Client{
Timeout: httpTimeout,
Transport: tr,
}
c.sniClients[serverName] = cli
return cli
}

View File

@@ -595,7 +595,7 @@ func spawnAnubisWithOptions(t *testing.T, basePrefix string) string {
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix()) fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
}) })
policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty) policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -27,10 +27,10 @@ import (
"github.com/TecharoHQ/anubis/internal/dnsbl" "github.com/TecharoHQ/anubis/internal/dnsbl"
"github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/internal/ogtags"
"github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/checker" "github.com/TecharoHQ/anubis/lib/policy/checker"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store" "github.com/TecharoHQ/anubis/lib/store"
// challenge implementations // challenge implementations
@@ -68,14 +68,14 @@ var (
type Server struct { type Server struct {
next http.Handler next http.Handler
store store.Interface
mux *http.ServeMux mux *http.ServeMux
policy *policy.ParsedConfig policy *policy.ParsedConfig
OGTags *ogtags.OGTagCache OGTags *ogtags.OGTagCache
logger *slog.Logger
opts Options
ed25519Priv ed25519.PrivateKey ed25519Priv ed25519.PrivateKey
hs512Secret []byte hs512Secret []byte
opts Options
store store.Interface
logger *slog.Logger
} }
func (s *Server) getTokenKeyfunc() jwt.Keyfunc { func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
@@ -117,10 +117,12 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
} }
chall := challenge.Challenge{ chall := challenge.Challenge{
ID: id.String(), ID: id.String(),
Method: rule.Challenge.Algorithm, Method: rule.Challenge.Algorithm,
RandomData: fmt.Sprintf("%x", randomData), RandomData: fmt.Sprintf("%x", randomData),
IssuedAt: time.Now(), IssuedAt: time.Now(),
Difficulty: rule.Challenge.Difficulty,
PolicyRuleHash: rule.Hash(),
Metadata: map[string]string{ Metadata: map[string]string{
"User-Agent": r.Header.Get("User-Agent"), "User-Agent": r.Header.Get("User-Agent"),
"X-Real-Ip": r.Header.Get("X-Real-Ip"), "X-Real-Ip": r.Header.Get("X-Real-Ip"),
@@ -137,6 +139,44 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
return &chall, err return &chall, err
} }
func (s *Server) hydrateChallengeRule(rule *policy.Bot, chall *challenge.Challenge, lg *slog.Logger) *policy.Bot {
if chall == nil {
return rule
}
if rule == nil {
rule = &policy.Bot{
Rules: &checker.List{},
}
}
if chall.Difficulty == 0 {
// fall back to whatever the policy currently says or the global default
if rule.Challenge != nil && rule.Challenge.Difficulty != 0 {
chall.Difficulty = rule.Challenge.Difficulty
} else {
chall.Difficulty = s.policy.DefaultDifficulty
}
}
if rule.Challenge == nil {
lg.Warn("rule missing challenge configuration; using stored challenge metadata", "rule", rule.Name)
rule.Challenge = &config.ChallengeRules{}
}
if rule.Challenge.Difficulty == 0 {
rule.Challenge.Difficulty = chall.Difficulty
}
if rule.Challenge.ReportAs != 0 {
s.logger.Warn("[DEPRECATION] the report_as field in this bot rule is deprecated, see https://github.com/TecharoHQ/anubis/issues/1310 for more information", "bot_name", rule.Name, "difficulty", rule.Challenge.Difficulty, "report_as", rule.Challenge.ReportAs)
}
if rule.Challenge.Algorithm == "" {
rule.Challenge.Algorithm = chall.Method
}
return rule
}
func (s *Server) maybeReverseProxyHttpStatusOnly(w http.ResponseWriter, r *http.Request) { func (s *Server) maybeReverseProxyHttpStatusOnly(w http.ResponseWriter, r *http.Request) {
s.maybeReverseProxy(w, r, true) s.maybeReverseProxy(w, r, true)
} }
@@ -461,6 +501,8 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
return return
} }
rule = s.hydrateChallengeRule(rule, chall, lg)
impl, ok := challenge.Get(chall.Method) impl, ok := challenge.Get(chall.Method)
if !ok { if !ok {
lg.Error("check failed", "err", err) lg.Error("check failed", "err", err)
@@ -606,7 +648,6 @@ 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{},

View File

@@ -2,6 +2,7 @@ package lib
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -18,8 +19,10 @@ import (
"github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/data" "github.com/TecharoHQ/anubis/data"
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/store"
"github.com/TecharoHQ/anubis/lib/thoth/thothmock" "github.com/TecharoHQ/anubis/lib/thoth/thothmock"
) )
@@ -55,7 +58,7 @@ func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConf
t.Logf("loading policy file: %s", fname) t.Logf("loading policy file: %s", fname)
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty) anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -149,10 +152,34 @@ func handleChallengeZeroDifficulty(t *testing.T, ts *httptest.Server, cli *http.
return resp return resp
} }
func handleChallengeInvalidProof(t *testing.T, ts *httptest.Server, cli *http.Client, chall challengeResp) *http.Response {
t.Helper()
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
if err != nil {
t.Fatalf("can't make request: %v", err)
}
q := req.URL.Query()
q.Set("response", strings.Repeat("f", 64)) // "hash" that never starts with the nonce
q.Set("nonce", "0")
q.Set("redir", "/")
q.Set("elapsedTime", "0")
q.Set("id", chall.ID)
req.URL.RawQuery = q.Encode()
resp, err := cli.Do(req)
if err != nil {
t.Fatalf("can't do request: %v", err)
}
return resp
}
type loggingCookieJar struct { type loggingCookieJar struct {
t *testing.T t *testing.T
lock sync.Mutex
cookies map[string][]*http.Cookie cookies map[string][]*http.Cookie
lock sync.Mutex
} }
func (lcj *loggingCookieJar) Cookies(u *url.URL) []*http.Cookie { func (lcj *loggingCookieJar) Cookies(u *url.URL) []*http.Cookie {
@@ -223,7 +250,7 @@ func TestLoadPolicies(t *testing.T) {
} }
defer fin.Close() defer fin.Close()
if _, err := policy.ParseConfig(t.Context(), fin, fname, 4); err != nil { if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
@@ -244,7 +271,7 @@ func TestCVE2025_24369(t *testing.T) {
cli := httpClient(t) cli := httpClient(t)
chall := makeChallenge(t, ts, cli) chall := makeChallenge(t, ts, cli)
resp := handleChallengeZeroDifficulty(t, ts, cli, chall) resp := handleChallengeInvalidProof(t, ts, cli, chall)
if resp.StatusCode == http.StatusFound { if resp.StatusCode == http.StatusFound {
t.Log("Regression on CVE-2025-24369") t.Log("Regression on CVE-2025-24369")
@@ -437,10 +464,6 @@ 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)
}
}) })
} }
} }
@@ -744,9 +767,9 @@ func TestStripBasePrefixFromRequest(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
basePrefix string basePrefix string
stripBasePrefix bool
requestPath string requestPath string
expectedPath string expectedPath string
stripBasePrefix bool
}{ }{
{ {
name: "strip disabled - no change", name: "strip disabled - no change",
@@ -1027,6 +1050,59 @@ func TestPassChallengeXSS(t *testing.T) {
}) })
} }
func TestPassChallengeNilRuleChallengeFallback(t *testing.T) {
pol := loadPolicies(t, "testdata/zero_difficulty.yaml", 0)
srv := spawnAnubis(t, Options{
Next: http.NewServeMux(),
Policy: pol,
})
allowThreshold, err := policy.ParsedThresholdFromConfig(config.Threshold{
Name: "allow-all",
Expression: &config.ExpressionOrList{
Expression: "true",
},
Action: config.RuleAllow,
})
if err != nil {
t.Fatalf("can't compile test threshold: %v", err)
}
srv.policy.Thresholds = []*policy.Threshold{allowThreshold}
srv.policy.Bots = nil
chall := challenge.Challenge{
ID: "test-challenge",
Method: "metarefresh",
RandomData: "apple cider",
IssuedAt: time.Now().Add(-5 * time.Second),
Difficulty: 1,
}
j := store.JSON[challenge.Challenge]{Underlying: srv.store}
if err := j.Set(context.Background(), "challenge:"+chall.ID, chall, time.Minute); err != nil {
t.Fatalf("can't insert challenge into store: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "https://example.com"+anubis.APIPrefix+"pass-challenge", nil)
q := req.URL.Query()
q.Set("redir", "/")
q.Set("id", chall.ID)
q.Set("challenge", chall.RandomData)
req.URL.RawQuery = q.Encode()
req.Header.Set("X-Real-Ip", "203.0.113.4")
req.Header.Set("User-Agent", "NilChallengeTester/1.0")
req.AddCookie(&http.Cookie{Name: anubis.TestCookieName, Value: chall.ID})
rr := httptest.NewRecorder()
srv.PassChallenge(rr, req)
if rr.Code != http.StatusFound {
t.Fatalf("expected redirect when validating challenge, got %d", rr.Code)
}
}
func TestXForwardedForNoDoubleComma(t *testing.T) { func TestXForwardedForNoDoubleComma(t *testing.T) {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For")) w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))

View File

@@ -4,10 +4,12 @@ import "time"
// Challenge is the metadata about a single challenge issuance. // Challenge is the metadata about a single challenge issuance.
type Challenge struct { type Challenge struct {
ID string `json:"id"` // UUID identifying the challenge IssuedAt time.Time `json:"issuedAt"`
Method string `json:"method"` // Challenge method Metadata map[string]string `json:"metadata"`
RandomData string `json:"randomData"` // The random data the client processes ID string `json:"id"`
IssuedAt time.Time `json:"issuedAt"` // When the challenge was issued Method string `json:"method"`
Metadata map[string]string `json:"metadata"` // Challenge metadata such as IP address and user agent RandomData string `json:"randomData"`
Spent bool `json:"spent"` // Has the challenge already been solved? PolicyRuleHash string `json:"policyRuleHash,omitempty"`
Difficulty int `json:"difficulty,omitempty"`
Spent bool `json:"spent"`
} }

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/google/uuid" "github.com/google/uuid"
@@ -19,5 +20,6 @@ func New(t *testing.T) *challenge.Challenge {
ID: id.String(), ID: id.String(),
RandomData: randomData, RandomData: randomData,
IssuedAt: time.Now(), IssuedAt: time.Now(),
Difficulty: anubis.DefaultDifficulty,
} }
} }

View File

@@ -6,8 +6,8 @@ import (
"sort" "sort"
"sync" "sync"
"github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store" "github.com/TecharoHQ/anubis/lib/store"
"github.com/a-h/templ" "github.com/a-h/templ"
) )

View File

@@ -8,8 +8,8 @@ import (
"testing" "testing"
"github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config"
) )
func mkRequest(t *testing.T, values map[string]string) *http.Request { func mkRequest(t *testing.T, values map[string]string) *http.Request {
@@ -36,7 +36,6 @@ 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"

View File

@@ -16,41 +16,46 @@ import (
"github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/data" "github.com/TecharoHQ/anubis/data"
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/honeypot/naive"
"github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/internal/ogtags"
"github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/web" "github.com/TecharoHQ/anubis/web"
"github.com/TecharoHQ/anubis/xess" "github.com/TecharoHQ/anubis/xess"
"github.com/a-h/templ" "github.com/a-h/templ"
) )
type Options struct { type Options struct {
Next http.Handler Next http.Handler
Policy *policy.ParsedConfig Policy *policy.ParsedConfig
Target string Target string
CookieDynamicDomain bool TargetHost string
CookieDomain string TargetSNI string
CookieExpiration time.Duration TargetInsecureSkipVerify bool
CookiePartitioned bool CookieDynamicDomain bool
BasePrefix string CookieDomain string
WebmasterEmail string CookieExpiration time.Duration
RedirectDomains []string CookiePartitioned bool
ED25519PrivateKey ed25519.PrivateKey BasePrefix string
HS512Secret []byte WebmasterEmail string
StripBasePrefix bool RedirectDomains []string
OpenGraph config.OpenGraph ED25519PrivateKey ed25519.PrivateKey
ServeRobotsTXT bool HS512Secret []byte
CookieSecure bool StripBasePrefix bool
CookieSameSite http.SameSite OpenGraph config.OpenGraph
Logger *slog.Logger ServeRobotsTXT bool
PublicUrl string CookieSecure bool
JWTRestrictionHeader string CookieSameSite http.SameSite
DifficultyInJWT bool Logger *slog.Logger
LogLevel string
PublicUrl string
JWTRestrictionHeader string
DifficultyInJWT bool
} }
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) { func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string) (*policy.ParsedConfig, error) {
var fin io.ReadCloser var fin io.ReadCloser
var err error var err error
@@ -74,7 +79,7 @@ func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty
} }
}(fin) }(fin)
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty) anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel)
if err != nil { if err != nil {
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err) return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
} }
@@ -116,9 +121,13 @@ func New(opts Options) (*Server, error) {
hs512Secret: opts.HS512Secret, hs512Secret: opts.HS512Secret,
policy: opts.Policy, policy: opts.Policy,
opts: opts, opts: opts,
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store), OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store, ogtags.TargetOptions{
store: opts.Policy.Store, Host: opts.TargetHost,
logger: opts.Logger, SNI: opts.TargetSNI,
InsecureSkipVerify: opts.TargetInsecureSkipVerify,
}),
store: opts.Policy.Store,
logger: opts.Logger,
} }
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -167,6 +176,33 @@ func New(opts Options) (*Server, error) {
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "") registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "") registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
mazeGen, err := naive.New(result.store, result.logger)
if err == nil {
registerWithPrefix(anubis.APIPrefix+"honeypot/{id}/{stage}", mazeGen, http.MethodGet)
opts.Policy.Bots = append(
opts.Policy.Bots,
policy.Bot{
Rules: mazeGen.CheckNetwork(),
Action: config.RuleWeigh,
Weight: &config.Weight{
Adjust: 30,
},
Name: "honeypot/network",
},
policy.Bot{
Rules: mazeGen.CheckUA(),
Action: config.RuleWeigh,
Weight: &config.Weight{
Adjust: 30,
},
Name: "honeypot/user-agent",
},
)
} else {
result.logger.Error("can't init honeypot subsystem", "err", err)
}
//goland:noinspection GoBoolExpressions //goland:noinspection GoBoolExpressions
if anubis.Version == "devel" { if anubis.Version == "devel" {
// make-challenge is only used in tests. Only enable while version is devel // make-challenge is only used in tests. Only enable while version is devel

View File

@@ -8,9 +8,9 @@ import (
func TestASNsValid(t *testing.T) { func TestASNsValid(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string
input *ASNs
err error err error
input *ASNs
name string
}{ }{
{ {
name: "basic valid", name: "basic valid",

View File

@@ -62,13 +62,14 @@ type BotConfig struct {
Expression *ExpressionOrList `json:"expression,omitempty" yaml:"expression,omitempty"` Expression *ExpressionOrList `json:"expression,omitempty" yaml:"expression,omitempty"`
Challenge *ChallengeRules `json:"challenge,omitempty" yaml:"challenge,omitempty"` Challenge *ChallengeRules `json:"challenge,omitempty" yaml:"challenge,omitempty"`
Weight *Weight `json:"weight,omitempty" yaml:"weight,omitempty"` Weight *Weight `json:"weight,omitempty" yaml:"weight,omitempty"`
Name string `json:"name" yaml:"name"`
Action Rule `json:"action" yaml:"action"`
RemoteAddr []string `json:"remote_addresses,omitempty" yaml:"remote_addresses,omitempty"`
// Thoth features // Thoth features
GeoIP *GeoIP `json:"geoip,omitempty"` GeoIP *GeoIP `json:"geoip,omitempty"`
ASNs *ASNs `json:"asns,omitempty"` ASNs *ASNs `json:"asns,omitempty"`
Name string `json:"name" yaml:"name"`
Action Rule `json:"action" yaml:"action"`
RemoteAddr []string `json:"remote_addresses,omitempty" yaml:"remote_addresses,omitempty"`
} }
func (b BotConfig) Zero() bool { func (b BotConfig) Zero() bool {
@@ -324,13 +325,15 @@ func (sc StatusCodes) Valid() error {
} }
type fileConfig struct { type fileConfig struct {
Bots []BotOrImport `json:"bots"`
DNSBL bool `json:"dnsbl"`
OpenGraph openGraphFileConfig `json:"openGraph,omitempty"` OpenGraph openGraphFileConfig `json:"openGraph,omitempty"`
Impressum *Impressum `json:"impressum,omitempty"` Impressum *Impressum `json:"impressum,omitempty"`
StatusCodes StatusCodes `json:"status_codes"`
Store *Store `json:"store"` Store *Store `json:"store"`
Bots []BotOrImport `json:"bots"`
Thresholds []Threshold `json:"thresholds"` Thresholds []Threshold `json:"thresholds"`
StatusCodes StatusCodes `json:"status_codes"`
DNSBL bool `json:"dnsbl"`
DNSTTL DnsTTL `json:"dns_ttl"`
Logging *Logging `json:"logging"`
} }
func (c *fileConfig) Valid() error { func (c *fileConfig) Valid() error {
@@ -362,6 +365,10 @@ func (c *fileConfig) Valid() error {
} }
} }
if err := c.Logging.Valid(); err != nil {
errs = append(errs, err)
}
if c.Store != nil { if c.Store != nil {
if err := c.Store.Valid(); err != nil { if err := c.Store.Valid(); err != nil {
errs = append(errs, err) errs = append(errs, err)
@@ -381,9 +388,14 @@ 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",
}, },
Logging: (Logging{}).Default(),
} }
if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&c); err != nil { if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&c); err != nil {
@@ -395,7 +407,8 @@ 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,
@@ -403,6 +416,7 @@ func Load(fin io.Reader, fname string) (*Config, error) {
}, },
StatusCodes: c.StatusCodes, StatusCodes: c.StatusCodes,
Store: c.Store, Store: c.Store,
Logging: c.Logging,
} }
if c.OpenGraph.TimeToLive != "" { if c.OpenGraph.TimeToLive != "" {
@@ -461,14 +475,39 @@ 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
Store *Store
OpenGraph OpenGraph
Bots []BotConfig Bots []BotConfig
Thresholds []Threshold Thresholds []Threshold
DNSBL bool
Impressum *Impressum
OpenGraph OpenGraph
StatusCodes StatusCodes StatusCodes StatusCodes
Store *Store Logging *Logging
DNSBL bool
DNSTTL DnsTTL
} }
func (c Config) Valid() error { func (c Config) Valid() error {

View File

@@ -8,16 +8,16 @@ import (
"testing" "testing"
"github.com/TecharoHQ/anubis/data" "github.com/TecharoHQ/anubis/data"
. "github.com/TecharoHQ/anubis/lib/policy/config" . "github.com/TecharoHQ/anubis/lib/config"
) )
func p[V any](v V) *V { return &v } func p[V any](v V) *V { return &v }
func TestBotValid(t *testing.T) { func TestBotValid(t *testing.T) {
var tests = []struct { var tests = []struct {
bot BotConfig
err error err error
name string name string
bot BotConfig
}{ }{
{ {
name: "simple user agent", name: "simple user agent",
@@ -110,7 +110,6 @@ 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",
}, },
}, },
@@ -124,7 +123,6 @@ 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",
}, },
}, },
@@ -361,7 +359,6 @@ 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() {

View File

@@ -11,10 +11,10 @@ import (
func TestExpressionOrListMarshalJSON(t *testing.T) { func TestExpressionOrListMarshalJSON(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string
input *ExpressionOrList
output []byte
err error err error
input *ExpressionOrList
name string
output []byte
}{ }{
{ {
name: "single expression", name: "single expression",
@@ -74,10 +74,10 @@ func TestExpressionOrListMarshalJSON(t *testing.T) {
func TestExpressionOrListMarshalYAML(t *testing.T) { func TestExpressionOrListMarshalYAML(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string
input *ExpressionOrList
output []byte
err error err error
input *ExpressionOrList
name string
output []byte
}{ }{
{ {
name: "single expression", name: "single expression",
@@ -217,8 +217,8 @@ func TestExpressionOrListUnmarshalJSON(t *testing.T) {
func TestExpressionOrListString(t *testing.T) { func TestExpressionOrListString(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
in ExpressionOrList
out string out string
in ExpressionOrList
}{ }{
{ {
name: "single expression", name: "single expression",

View File

@@ -7,9 +7,9 @@ import (
func TestGeoIPValid(t *testing.T) { func TestGeoIPValid(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string
input *GeoIP
err error err error
input *GeoIP
name string
}{ }{
{ {
name: "basic valid", name: "basic valid",

View File

@@ -8,9 +8,9 @@ import (
func TestImpressumValid(t *testing.T) { func TestImpressumValid(t *testing.T) {
for _, cs := range []struct { for _, cs := range []struct {
name string
inp Impressum
err error err error
inp Impressum
name string
}{ }{
{ {
name: "basic happy path", name: "basic happy path",

124
lib/config/logging.go Normal file
View File

@@ -0,0 +1,124 @@
package config
import (
"errors"
"fmt"
"log/slog"
)
var (
ErrMissingLoggingFileConfig = errors.New("config.Logging: missing value parameters in logging block")
ErrInvalidLoggingSink = errors.New("config.Logging: invalid sink")
ErrInvalidLoggingFileConfig = errors.New("config.LoggingFileConfig: invalid parameters")
ErrOutOfRange = errors.New("config: error out of range")
)
type Logging struct {
Sink string `json:"sink"` // Logging sink, either "stdio" or "file"
Level *slog.Level `json:"level"` // Log level, if set supercedes the level in flags
Parameters *LoggingFileConfig `json:"parameters"` // Logging parameters, to be dynamic in the future
}
const (
LogSinkStdio = "stdio"
LogSinkFile = "file"
)
func (l *Logging) Valid() error {
var errs []error
switch l.Sink {
case LogSinkStdio:
// no validation needed
case LogSinkFile:
if l.Parameters == nil {
errs = append(errs, ErrMissingLoggingFileConfig)
}
if err := l.Parameters.Valid(); err != nil {
errs = append(errs, err)
}
default:
errs = append(errs, fmt.Errorf("%w: sink %s is unknown to me", ErrInvalidLoggingSink, l.Sink))
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}
func (Logging) Default() *Logging {
return &Logging{
Sink: "stdio",
}
}
type LoggingFileConfig struct {
Filename string `json:"file"`
MaxBackups int `json:"maxBackups"`
MaxBytes int64 `json:"maxBytes"`
MaxAge int `json:"maxAge"`
Compress bool `json:"compress"`
UseLocalTime bool `json:"useLocalTime"`
}
func (lfc *LoggingFileConfig) Valid() error {
if lfc == nil {
return fmt.Errorf("logging file config is nil, why are you calling this?")
}
var errs []error
if lfc.Zero() {
errs = append(errs, ErrMissingValue)
}
if lfc.Filename == "" {
errs = append(errs, fmt.Errorf("%w: filename", ErrMissingValue))
}
if lfc.MaxBackups < 0 {
errs = append(errs, fmt.Errorf("%w: max backup count %d is not greater than or equal to zero", ErrOutOfRange, lfc.MaxBackups))
}
if lfc.MaxAge < 0 {
errs = append(errs, fmt.Errorf("%w: max backup count %d is not greater than or equal to zero", ErrOutOfRange, lfc.MaxAge))
}
if len(errs) != 0 {
errs = append([]error{ErrInvalidLoggingFileConfig}, errs...)
return errors.Join(errs...)
}
return nil
}
func (lfc LoggingFileConfig) Zero() bool {
for _, cond := range []bool{
lfc.Filename != "",
lfc.MaxBackups != 0,
lfc.MaxBytes != 0,
lfc.MaxAge != 0,
lfc.Compress,
lfc.UseLocalTime,
} {
if cond {
return false
}
}
return true
}
func (LoggingFileConfig) Default() *LoggingFileConfig {
return &LoggingFileConfig{
Filename: "./var/anubis.log",
MaxBackups: 3,
MaxBytes: 104857600, // 100 Mi
MaxAge: 7, // 7 days
Compress: true,
UseLocalTime: false,
}
}

103
lib/config/logging_test.go Normal file
View File

@@ -0,0 +1,103 @@
package config
import (
"errors"
"testing"
)
func TestLoggingValid(t *testing.T) {
for _, tt := range []struct {
name string
input *Logging
want error
}{
{
name: "simple happy",
input: (Logging{}).Default(),
},
{
name: "default file config",
input: &Logging{
Sink: LogSinkFile,
Parameters: (&LoggingFileConfig{}).Default(),
},
},
{
name: "invalid sink",
input: &Logging{
Sink: "taco invalid",
},
want: ErrInvalidLoggingSink,
},
{
name: "missing parameters",
input: &Logging{
Sink: LogSinkFile,
},
want: ErrMissingLoggingFileConfig,
},
{
name: "invalid parameters",
input: &Logging{
Sink: LogSinkFile,
Parameters: &LoggingFileConfig{},
},
want: ErrInvalidLoggingFileConfig,
},
{
name: "file sink with no filename",
input: &Logging{
Sink: LogSinkFile,
Parameters: &LoggingFileConfig{
Filename: "",
MaxBackups: 3,
MaxBytes: 104857600, // 100 Mi
MaxAge: 7, // 7 days
Compress: true,
UseLocalTime: false,
},
},
want: ErrMissingValue,
},
{
name: "file sink with negative max backups",
input: &Logging{
Sink: LogSinkFile,
Parameters: &LoggingFileConfig{
Filename: "./var/anubis.log",
MaxBackups: -3,
MaxBytes: 104857600, // 100 Mi
MaxAge: 7, // 7 days
Compress: true,
UseLocalTime: false,
},
},
want: ErrOutOfRange,
},
{
name: "file sink with negative max age",
input: &Logging{
Sink: LogSinkFile,
Parameters: &LoggingFileConfig{
Filename: "./var/anubis.log",
MaxBackups: 3,
MaxBytes: 104857600, // 100 Mi
MaxAge: -7, // 7 days
Compress: true,
UseLocalTime: false,
},
},
want: ErrOutOfRange,
},
} {
t.Run(tt.name, func(t *testing.T) {
err := tt.input.Valid()
if !errors.Is(err, tt.want) {
t.Logf("wanted error: %v", tt.want)
t.Logf(" got error: %v", err)
t.Fatal("got wrong error")
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More