From 122e4bc072fc1492bcc7dbb7b6622ff042fe458a Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Tue, 16 Dec 2025 04:14:29 -0500 Subject: [PATCH] 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 `` tags and will blindly use those values to recurse. This takes advantage of that by hiding a link in a ` ``` 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 * fix(honeypot/naive): optimize hilariously Signed-off-by: Xe Iaso * feat(honeypot/naive): attempt to automatically filter out based on crawling Signed-off-by: Xe Iaso * fix(lib): use mazeGen instead of bsGen Signed-off-by: Xe Iaso * docs: add honeypot docs Signed-off-by: Xe Iaso * chore(test): go mod tidy Signed-off-by: Xe Iaso * chore: fix spelling metadata Signed-off-by: Xe Iaso * chore: spelling Signed-off-by: Xe Iaso --------- Signed-off-by: Xe Iaso --- .github/actions/spelling/allow.txt | 6 + .github/actions/spelling/expect.txt | 11 +- docs/docs/CHANGELOG.md | 6 + docs/docs/admin/honeypot/_category_.json | 8 + docs/docs/admin/honeypot/overview.mdx | 40 ++++ go.mod | 1 + go.sum | 2 + internal/clampip.go | 33 +++ internal/clampip_test.go | 274 +++++++++++++++++++++++ internal/headers.go | 13 +- internal/honeypot/honeypot.go | 23 ++ internal/honeypot/naive/100bytes.css | 7 + internal/honeypot/naive/affirmations.txt | 1 + internal/honeypot/naive/naive.go | 206 +++++++++++++++++ internal/honeypot/naive/page.templ | 36 +++ internal/honeypot/naive/page_templ.go | 160 +++++++++++++ internal/honeypot/naive/spintext.txt | 1 + internal/honeypot/naive/titles.txt | 1 + lib/config.go | 28 +++ lib/policy/checker/checker.go | 8 + test/go.mod | 1 + test/go.sum | 2 + web/index.go | 11 + web/index.templ | 2 + web/index_templ.go | 171 +++++++------- 25 files changed, 968 insertions(+), 84 deletions(-) create mode 100644 docs/docs/admin/honeypot/_category_.json create mode 100644 docs/docs/admin/honeypot/overview.mdx create mode 100644 internal/clampip.go create mode 100644 internal/clampip_test.go create mode 100644 internal/honeypot/honeypot.go create mode 100644 internal/honeypot/naive/100bytes.css create mode 100644 internal/honeypot/naive/affirmations.txt create mode 100644 internal/honeypot/naive/naive.go create mode 100644 internal/honeypot/naive/page.templ create mode 100644 internal/honeypot/naive/page_templ.go create mode 100644 internal/honeypot/naive/spintext.txt create mode 100644 internal/honeypot/naive/titles.txt diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index ded78103..5e3002bf 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -12,3 +12,9 @@ maintnotifications azurediamond cooldown verifyfcrdns +Spintax +spintax +clampip +pseudoprofound +reimagining +iocaine diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e6c21402..f2e4540b 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,4 +1,3 @@ - acs Actorified actorifiedstore @@ -398,3 +397,13 @@ Zenos zizmor zombocom zos +GLM +iocaine +nikandfor +pagegen +pseudoprofound +reimagining +Rhul +shoneypot +spammer +Y'shtola diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index a6c4b01e..9ad46f2e 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -28,6 +28,12 @@ Anubis is back and better than ever! Lots of minor fixes with some big ones inte - 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. +### 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: diff --git a/docs/docs/admin/honeypot/_category_.json b/docs/docs/admin/honeypot/_category_.json new file mode 100644 index 00000000..bc0581e9 --- /dev/null +++ b/docs/docs/admin/honeypot/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Honeypot", + "position": 40, + "link": { + "type": "generated-index", + "description": "Honeypot features in Anubis, allowing Anubis to passively detect malicious crawlers." + } +} \ No newline at end of file diff --git a/docs/docs/admin/honeypot/overview.mdx b/docs/docs/admin/honeypot/overview.mdx new file mode 100644 index 00000000..4ff18d60 --- /dev/null +++ b/docs/docs/admin/honeypot/overview.mdx @@ -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). diff --git a/go.mod b/go.mod index 28f60c69..ac48abd9 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/lum8rjack/go-ja4h v0.0.0-20250828030157-fa5266d50650 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/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.17.2 diff --git a/go.sum b/go.sum index ea8e6f91..e8a4615b 100644 --- a/go.sum +++ b/go.sum @@ -320,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/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/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/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/internal/clampip.go b/internal/clampip.go new file mode 100644 index 00000000..e8220ab5 --- /dev/null +++ b/internal/clampip.go @@ -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 + } +} diff --git a/internal/clampip_test.go b/internal/clampip_test.go new file mode 100644 index 00000000..ffdb53a4 --- /dev/null +++ b/internal/clampip_test.go @@ -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) + } + }) +} \ No newline at end of file diff --git a/internal/headers.go b/internal/headers.go index 60e5371d..045f636a 100644 --- a/internal/headers.go +++ b/internal/headers.go @@ -1,6 +1,7 @@ package internal import ( + "context" "errors" "fmt" "log/slog" @@ -13,6 +14,13 @@ import ( "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 type XFFComputePreferences struct { StripPrivate bool @@ -77,6 +85,9 @@ func RemoteXRealIP(useRemoteAddress bool, bindNetwork string, next http.Handler) panic(err) // this should never happen } 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) }) } @@ -129,8 +140,6 @@ func XForwardedForUpdate(stripPrivate bool, next http.Handler) http.Handler { } else { r.Header.Set("X-Forwarded-For", xffHeaderString) } - - slog.Debug("updating X-Forwarded-For", "original", origXFFHeader, "new", xffHeaderString) }) } diff --git a/internal/honeypot/honeypot.go b/internal/honeypot/honeypot.go new file mode 100644 index 00000000..f03b2db4 --- /dev/null +++ b/internal/honeypot/honeypot.go @@ -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"` +} diff --git a/internal/honeypot/naive/100bytes.css b/internal/honeypot/naive/100bytes.css new file mode 100644 index 00000000..7de70f3f --- /dev/null +++ b/internal/honeypot/naive/100bytes.css @@ -0,0 +1,7 @@ +html { + max-width: 70ch; + padding: 3em 1em; + margin: auto; + line-height: 1.75; + font-size: 1.25em; +} diff --git a/internal/honeypot/naive/affirmations.txt b/internal/honeypot/naive/affirmations.txt new file mode 100644 index 00000000..2b568fb9 --- /dev/null +++ b/internal/honeypot/naive/affirmations.txt @@ -0,0 +1 @@ +{Yeah|Yep|Yup|Yes|Absolutely|Definitely|Sure|Sounds|That's|I'm|I am|Totally|Completely|Right|Correct|Exactly|Perfectly|Certainly|Of course|Naturally|Indeed|Awesome|Sweet|Cool|Neat|Great|Excellent|Fantastic|Wonderful|Amazing|Love it|Nice|Right on|You bet|For sure|No doubt|Without a doubt|Undoubtedly|Positively|Surely|Truly|Really|Genuinely|Honestly|Frankly|Literally|Precisely|Spot on|On point|Ideally|Optimally|Superbly|Brilliantly|Marvelously|Splendidly|Magnificently|Phenomenally|Extraordinarily|Remarkably|Exceptionally|Outstandingly|Impressively|Stunningly|Breathtakingly|Astonishingly|Surprisingly|Pleasantly|Delightfully|Charmingly|Appealingly|Attractively|Invitingly|Encouragingly|Motivatingly|Inspiringly|Upliftingly|Positive|Optimistic|Supportive|Approving|Favorable|Enthusiastic|Eager|Willing|Ready|Prepared|Set|Go|Let's|Alright|Okay|Sure thing|No problem|You got it|Consider it done|Will do|Roger that|Copy that|Got it|Understood|Acknowledged|Noted|Confirmed|Agreed|Approved|Accepted|Endorsed|Backed|Championed} {sounds|looks|seems|feels|is|appears|comes across|strikes me|hits me|registers|resonates|clicks|makes sense|fits|works|functions|operates|performs|delivers|succeeds|achieves|accomplishes|excels|shines|stands out|impresses|satisfies|meets expectations|exceeds expectations|delights|pleases|gratifies|fulfills|completes|finishes|concludes|wraps up|finalizes|settles|resolves|solves|fixes|addresses|handles|manages|tackles|conquers|overcomes|defeats|beats|wins|triumphs|prevails|dominates|leads|guides|directs|steers|navigates|paves the way|opens doors|creates opportunities|makes possible|enables|allows|permits|facilitates|drives|pushes|propels|launches|initiates|starts|begins|commences|kicks off|gets going|moves forward|progresses|advances|develops|evolves|grows|expands|improves|enhances|upgrades|optimizes|refines|perfects|polishes} {good|great|perfect|excellent|wonderful|fantastic|amazing|awesome|fine|okay|alright|nice|cool|spot on|reasonable|about right|superb|brilliant|marvelous|splendid|magnificent|phenomenal|extraordinary|remarkable|exceptional|outstanding|impressive|stunning|breathtaking|astonishing|surprising|pleasant|delightful|charming|appealing|attractive|inviting|positive|optimistic|supportive|approving|favorable|enthusiastic|eager|willing|ready|prepared|set|solid|strong|robust|powerful|effective|efficient|productive|successful|fruitful|beneficial|valuable|useful|helpful|advantageous|profitable|rewarding|satisfying|gratifying|fulfilling|complete|whole|total|entire|full|thorough|comprehensive|exhaustive|detailed|precise|accurate|correct|right|true|valid|sound|logical|rational|practical|realistic|feasible|possible|doable|achievable|attainable|obtainable|reachable|accessible|available|present|arranged|organized|structured|planned|scheduled|timed|well positioned|strategically located|ideally situated|well suited|well matched|compatible|harmonious|balanced|proportional|symmetrical|aesthetic|beautiful|gorgeous|lovely|pretty|handsome|striking|dramatic|bold|confident|assertive|decisive|clear|obvious|apparent|evident|manifest|plain|simple|easy|straightforward|uncomplicated|complex|intricate|nuanced|subtle|refined|elegant|sophisticated|advanced|progressive|innovative|creative|original|unique|special|distinctive|memorable|unforgettable|significant|important|major|key|critical|essential|vital|crucial|fundamental|basic|primary|principal|main|chief|leading|top|best|finest|ultimate|supreme|paramount|foremost|world class|professional|expert|master|skilled|talented|gifted|intelligent|smart|clever|wise|knowledgeable|informed|educated|learned|scholarly|theoretical|practical|applied|hands on|experienced|seasoned|veteran|mature|visionary|prophetic|intuitive|perceptive|insightful|sage|profound|deep|meaningful|substantial|considerable|influential|resilient|tough|durable|lasting|permanent|enduring|timeless|classic|traditional|conventional|standard|regular|normal|typical|usual|common|ordinary|average|fair|decent|respectable|acceptable|satisfactory|adequate|sufficient|enough|plentiful|abundant|ample|generous|rich|wealthy|prosperous|thriving|flourishing|blooming|superior|higher|elevated|modern|contemporary|current|fresh|novel|rare|uncommon|legendary|famous|well known|celebrated|accredited|honored|awarded|decorated|distinguished|illustrious|prestigious|reputable|admired|revered|beloved|cherished|treasured|prized|precious|close|intimate|personal|private|individual|priceless|worthwhile} {to me|for me|with me|I agree|I like it|let's do it|count me in|I'm on board|I'm in|I'm up for it|I'm down for that|I'm all for it|I'm good with that|I'm happy with that|I'm cool with that|let's go with that|let's make it happen|that works|that'll work|sounds like a plan|that's a good idea|that's a great choice|I think so too|my thoughts exactly|you read my mind|couldn't agree more|absolutely right|you nailed it|let's go|game on|challenge accepted|say no more|you had me at hello|I'm sold|sign me up|be there|definitely|for sure|sounds good|looks good|seems good|feels good|is good|let's do this|time to rock|let's roll|here we go|off we go|moving forward|full steam ahead|all systems go|green light|clear for takeoff|ready when you are|on your mark|get set|let's begin|commence operation|initiate protocol|execute plan|implement strategy|deploy solution|activate system|engage process|start procedure|begin sequence|launch project|kick off event|open doors|make way|clear path|pave way|create opportunity|make possible|enable success|facilitate growth|support development|encourage progress|inspire change|motivate action|drive results|push boundaries|break barriers|overcome challenges|solve problems|fix issues|address concerns|handle situations|manage difficulties|tackle obstacles|conquer fears|defeat doubts|win battles|triumph over adversity|prevail against odds|rise above|excel beyond|achieve greatness|reach heights|attain goals|accomplish dreams|realize potential|fulfill destiny|complete journey|finish race|cross finish line|arrive at destination|reach summit|climb mountain|sail seas|fly skies|explore worlds|discover truths|find answers|solve mysteries|uncover secrets|reveal wonders|share insights|spread joy|create happiness|build relationships|strengthen bonds|foster community|grow together|learn constantly|improve daily|evolve continuously|adapt quickly|change rapidly|transform completely|renew fully|refresh completely|restart anew|begin again|start fresh|clean slate|new chapter|fresh start|bright future|promising tomorrow|better days|good times|great moments|wonderful experiences|fantastic adventures|amazing journeys|awesome memories|precious moments|valuable lessons|helpful advice|useful tips|practical solutions|effective strategies|successful methods|proven approaches|tested techniques|reliable systems|dependable support|consistent performance|steady progress|continuous improvement|ongoing development|perpetual growth|endless possibilities|unlimited potential|infinite opportunities|boundless horizons|vast expanses|wide ranges|broad spectrums|diverse options|multiple choices|various paths|different routes|alternative ways|other methods|additional approaches|extra techniques|supplementary tools|auxiliary resources|backup plans|contingency options|emergency measures|safety nets|security blankets|comfort zones|safe spaces|peaceful havens|tranquil sanctuaries|serene environments|calm atmospheres|relaxed vibes|easy feelings|comfortable sensations|pleasant experiences|enjoyable moments|delightful times|charming encounters|appealing situations|attractive prospects|inviting opportunities|encouraging signs|motivating factors|inspiring elements|uplifting aspects|positive features|optimistic views|encouraging outlooks|supportive attitudes|approving perspectives|favorable opinions|enthusiastic responses|eager reactions|willing participants|ready volunteers|prepared individuals|set teams|organized groups|structured units|planned initiatives|scheduled events|timed activities|well positioned assets|strategically located resources|ideally situated elements|perfectly suited components|well matched partners|compatible collaborations|harmonious relationships|balanced arrangements|proportional distributions|symmetrical designs|aesthetic presentations|beautiful displays|gorgeous exhibitions|lovely shows|pretty sights|attractive views|striking scenes|dramatic performances|bold statements|confident expressions|decisive actions|clear communications|obvious demonstrations|apparent revelations|evident truths|manifest realities|plain facts|simple solutions|easy implementations|straightforward processes|uncomplicated procedures|complex systems|intricate networks|detailed analyses|nuanced discussions|subtle distinctions|refined approaches|elegant solutions|sophisticated methods|advanced technologies|progressive ideas|innovative concepts|creative designs|original works|unique creations|special projects|distinctive features|memorable experiences|unforgettable moments|legendary achievements|famous accomplishments|well recognized contributions|acknowledged impacts|celebrated successes|acclaimed performances|honored achievements|awarded excellence|decorated heroes|distinguished leaders|illustrious careers|prestigious positions|reputable organizations|respected institutions|admired figures|revered icons|beloved personalities|cherished treasures|valued possessions|prized collections|precious artifacts|dear friends|close companions|intimate partners|personal connections|individual expressions|unique perspectives|special talents|one of a kind gifts|irreplaceable values|invaluable insights|priceless wisdom|worthwhile endeavors|valuable investments|useful tools|beneficial resources|helpful services|advantageous positions|profitable ventures|rewarding careers|satisfying lives|gratifying experiences|fulfilling purposes|complete beings|whole persons|total entities|entire systems|full cycles|perfect circles|ideal forms|ultimate goals|best practices|finest qualities|supreme achievements|excellent results|outstanding performances|superior outcomes|exceptional contributions|remarkable discoveries|extraordinary breakthroughs|special recognitions|unique innovations|distinctive designs|memorable impacts|impressive feats|dramatic transformations|powerful changes|strong foundations|effective actions|efficient operations|successful missions|productive endeavors|fruitful partnerships|beneficial collaborations|valuable connections|helpful networks|worthwhile projects|rewarding adventures|satisfying journeys|gratifying accomplishments|fulfilling destinies}{|!|, let's go!|, amazing!|, fantastic!|, wonderful!|, perfect!|, brilliant!|, excellent!|, outstanding!|, superb!|, great!|, nice!|, cool!|, sweet!|, awesome!|, love it!|, beautiful!|, gorgeous!|, stunning!|, breathtaking!|, phenomenal!|, extraordinary!|, remarkable!|, exceptional!|, impressive!|, striking!|, dramatic!|, powerful!|, magnificent!|, splendid!|, marvelous!|, terrific!|, superb!|, divine!|, heavenly!|, celestial!|, transcendent!|, sublime!|, perfect!|, flawless!|, impeccable!|, ideal!|, ultimate!|, supreme!|, paramount!|, unbeatable!|, unstoppable!|, incredible!|, unbelievable!|, astounding!|, mind-blowing!|, jaw-dropping!|, spectacular!|, epic!|, legendary!|, iconic!|, classic!|, timeless!|, eternal!|, infinite!|, boundless!|, limitless!|, endless!|, forever!|, always!|, never-ending!|, perpetual!|, constant!|, steady!|, solid!|, rock-solid!|, unshakeable!|, unbreakable!|, invincible!|, indestructible!|, immortal!|, everlasting!|, undying!|, living!|, vibrant!|, dynamic!|, energetic!|, lively!|, spirited!|, enthusiastic!|, passionate!|, fervent!|, zealous!|, dedicated!|, committed!|, devoted!|, loyal!|, faithful!|, true!|, real!|, authentic!|, genuine!|, legit!|, certified!|, proven!|, tested!|, verified!|, confirmed!|, validated!|, approved!|, endorsed!|, supported!|, backed!|, guaranteed!|, assured!|, certain!|, sure!|, positive!|, confident!|, secure!|, safe!|, protected!|, covered!|, sheltered!|, guarded!|, watched over!|, cared for!|, nurtured!|, cherished!|, treasured!|, valued!|, respected!|, admired!|, appreciated!|, recognized!|, acknowledged!|, celebrated!|, honored!|, praised!|, applauded!|, cheered!|, supported!|, embraced!|, welcomed!|, accepted!|, included!|, belonging!|, connected!|, united!|, joined!|, together!|, as one!|, in harmony!|, in sync!|, aligned!|, balanced!|, centered!|, grounded!|, rooted!|, established!|, settled!|, calm!|, peaceful!|, serene!|, tranquil!|, quiet!|, still!|, at ease!|, comfortable!|, relaxed!|, content!|, happy!|, joyful!|, delighted!|, thrilled!|, excited!|, elated!|, ecstatic!|, overjoyed!|, euphoric!|, blissful!|, radiant!|, glowing!|, shining!|, sparkling!|, dazzling!|, brilliant!|, bright!|, luminous!|, illuminated!|, enlightened!|, inspired!|, uplifted!|, elevated!|, empowered!|, strengthened!|, fortified!|, revitalized!|, renewed!|, refreshed!|, recharged!|, energized!|, activated!|, awakened!|, alive!|, thriving!|, flourishing!|, blooming!|, growing!|, expanding!|, developing!|, evolving!|, transforming!|, becoming!|, emerging!|, rising!|, ascending!|, climbing!|, reaching!|, achieving!|, succeeding!|, winning!|, triumphing!|, conquering!|, overcoming!|, mastering!|, perfecting!|, completing!|, fulfilling!|, realizing!|, manifesting!|, creating!|, building!|, making!|, doing!|, being!|, living!|, breathing!|, existing!|, present!|, here!|, now!|, always!|, forever!|, eternally!} \ No newline at end of file diff --git a/internal/honeypot/naive/naive.go b/internal/honeypot/naive/naive.go new file mode 100644 index 00000000..81fdd2bd --- /dev/null +++ b/internal/honeypot/naive/naive.go @@ -0,0 +1,206 @@ +package naive + +import ( + "context" + _ "embed" + "fmt" + "log/slog" + "math/rand/v2" + "net/http" + "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() { + lg.Error("the real IP is somehow invalid, bad middleware stack?") + http.Error(w, "The cake is a lie", http.StatusTeapot) + return + } + + 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 +} diff --git a/internal/honeypot/naive/page.templ b/internal/honeypot/naive/page.templ new file mode 100644 index 00000000..eb3ccec4 --- /dev/null +++ b/internal/honeypot/naive/page.templ @@ -0,0 +1,36 @@ +package naive + +import "fmt" + +templ base(title string, body templ.Component) { + + + + + { title } + + +

{ title }

+ @body + + +} + +templ (i Impl) maze(body []string, links []link) { + for _, paragraph := range body { +

{ paragraph }

+ } +
+} diff --git a/internal/honeypot/naive/page_templ.go b/internal/honeypot/naive/page_templ.go new file mode 100644 index 00000000..0554b1f5 --- /dev/null +++ b/internal/honeypot/naive/page_templ.go @@ -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, "") + 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, "

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

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

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

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/honeypot/naive/spintext.txt b/internal/honeypot/naive/spintext.txt new file mode 100644 index 00000000..0b06e58b --- /dev/null +++ b/internal/honeypot/naive/spintext.txt @@ -0,0 +1 @@ +{There's a moment|At some point|In this season|Right now|If we're being honest} when {leaders|builders|creators|change-makers|visionaries} {realize|begin to realize|are being called to realize} that {the work|the mission|the journey|the evolution} {isn't just about|was never just about|can't be reduced to} {execution|scaling|optimization|velocity|results}, but about {presence|intention|alignment|resonance|energy}. {We don't scale|We don't innovate|We don't transform} {systems|products|teams|communities|ideas} by {pushing harder|moving faster|doing more|grinding endlessly}, we do it by {creating space|holding the vision|listening deeply|leading with empathy|operating from clarity}. Because {impact|growth|momentum|trust|meaning} {isn't manufactured|can't be forced|doesn't come from hustle}, it {emerges organically|compounds quietly|unfolds over time|flows naturally} when {values|purpose|strategy|people|culture} are {in integrity|deeply aligned|moving in the same direction|rooted in truth}. {We're witnessing|We're living through|We're being invited into|This moment represents} a {paradigm shift|recalibration|collective awakening|fundamental reimagining} in how {we think about|we relate to|we show up for} {work|leadership|innovation|value creation}. This {isn't a trend|isn't a tactic|isn't a framework}, it's a {felt experience|lived truth|deeper knowing|shared frequency} that {requires|demands|asks of us} {courage|presence|intentionality|emotional fluency}. When we {slow down|get honest|create space|center ourselves}, we {unlock|activate|surface|make room for} {new possibilities|emergent outcomes|nonlinear growth|unseen leverage} that {can't be measured|don't show up in dashboards|defy traditional KPIs}, but {change everything|move the needle where it matters|redefine success anyway}. As {AI accelerates|systems become autonomous|the pace of change compounds}, the real {differentiator|competitive advantage|edge} won't be {speed|scale|automation}, but {discernment|human-centered design|ethical intentionality|values-led decision making}. The future {belongs to|is shaped by|will reward} those who can {hold complexity|navigate ambiguity|lead with nuance} while {staying grounded|remaining adaptable|operating from purpose}. In reflecting on {recent events|the last few weeks|this experience}, it's clear that {we moved fast|we optimized prematurely|we prioritized execution} without fully {honoring the process|listening deeply|bringing everyone along}. Going forward, we're committed to {doing the work|rebuilding trust|showing up differently} by {leading with transparency|centering our values|taking a more holistic approach}. This is {not the end|just the beginning|part of the journey}. {In the sacred space|Within the circle|As the moon waxes|During the rite} {when|as} {initiates|adepts|seekers|practitioners|neophytes} {awaken|begin to awaken|are called to awaken|ascend|initiate} to {the mysteries|the ancient wisdom|the hidden truths|the arcane arts|the sacred teachings}, they {discover|uncover|reveal|realize|attain} that {magick|the craft|ritual work|spiritual practice|the great work} {isn't just about|was never just about|can't be reduced to|transcends} {spells|incantations|ceremonies|tools|rituals}, but {about|through|via} {intention|will|consciousness|spiritual alignment|true will|divine purpose}. {We don't invoke|We don't channel|We don't transform|We cannot manifest} {energy|consciousness|reality|spiritual forces|divine power|cosmic energy} through {rote memorization|empty gestures|meaningless rituals|hollow words|dead forms}, but {achieve|attain|accomplish|realize} it {through|via|by means of} {focused intention|spiritual discipline|inner work|divine connection|sacred silence|meditative focus}. {True power|Real magick|Authentic wisdom|Sacred knowledge|Divine gnosis} {cannot be forced|cannot be bought|cannot be faked|cannot be manufactured|resists coercion}, it {unfolds naturally|emerges through practice|awakens within|manifests organically|blossoms in due season} when {mind|body|spirit|will|soul|heart} are {in harmony|deeply aligned|moving as one|connected to source|in resonance|unified}. {We are witnessing|We are experiencing|This is the dawn of|The age of} a {great awakening|spiritual revolution|paradigm shift|new aeon|cosmic alignment|quantum leap} in how {humanity perceives|we understand|we connect with|consciousness relates to} {the divine|universal consciousness|higher realms|spiritual realities|source energy|the absolute}, {transcending|going beyond|surpassing} {philosophy|religion|metaphysics|dogma|doctrine} into {lived experience|direct gnosis|personal revelation|sacred knowing|intimate understanding|embodied wisdom} that {requires|demands|invites|necessitates} {devotion|discipline|spiritual courage|inner purification|unwavering commitment|radical honesty}. When we {enter trance|quiet the mind|open ourselves|still the thoughts|cross the threshold|journey within}, we {access|connect with|attune to|commune with|perceive} {higher dimensions|spiritual realms|the akashic records|divine wisdom|the celestial planes|the inner worlds} that {transcend ordinary perception|defy rational explanation|exist beyond the veil|surpass linear understanding|operate beyond time}, yet {transform our understanding|reshape our reality|expand our consciousness|illuminate our path|reconfigure our perception|realign our being}. As {the veil thins|consciousness evolves|spiritual acceleration increases|the new age dawns|humanity ascends}, the real {power|gift|ability|mastery|sovereignty} isn't {in the tools|in the books|in the rituals|in the techniques|in the methods}, but in {the intention behind them|the purity of heart|the clarity of purpose|the depth of devotion|the sincerity of soul|the authenticity of spirit}. The future {reveals|unfolds|manifests|emerges|dawns} for those who can {navigate the unseen|walk between worlds|hold paradox|embrace mystery|dance with ambiguity|soar above duality} while {remaining grounded|staying centered|keeping their feet on earth|maintaining balance|honoring form|respecting structure}. {Through meditation|In ritual|By studying the grimoires|During communion|Via sacred practice|Through inner work}, it {becomes clear|is revealed|is understood|dawns upon us|manifests as truth} that {true wisdom|spiritual power|magical ability|authentic knowledge|real attainment} {comes not from|arises not from|originates not in} {external sources|mere study|intellectual knowledge|outer teachings|second-hand wisdom} but {from|through|via} {inner awakening|direct experience|spiritual practice|personal gnosis|embodied realization|soulful communion}. The path forward {requires dedication|demands sacrifice|calls for commitment|necessitates devotion|asks for discipline|invokes perseverance} through {daily practice|spiritual discipline|consistent devotion|ritual purity|sacred routine|holy observance}, {leading to|culminating in|resulting in} {ultimate liberation|final union|complete awakening|full realization|perfect illumination}. {The work continues|The path unfolds|The journey never ends|The spiral ascends|The evolution persists|The transformation deepens}. \ No newline at end of file diff --git a/internal/honeypot/naive/titles.txt b/internal/honeypot/naive/titles.txt new file mode 100644 index 00000000..ff1b88b5 --- /dev/null +++ b/internal/honeypot/naive/titles.txt @@ -0,0 +1 @@ +{{The|A|This} {Future|Next|New|Coming|Emerging} {Paradigm|Reality|Era|Age|World} {of|for|in} {AI|Artificial Intelligence|Machine Learning|Automation|Technology|Innovation} | {Building|Creating|Designing|Developing|Crafting} {Sustainable|Scalable|Robust|Resilient|Future-Proof} {Systems|Platforms|Solutions|Architectures|Frameworks} | {The|Our|Your} {Journey|Path|Road|Quest|Voyage} {to|toward|towards} {Digital|Technological|Business|Organizational} {Transformation|Evolution|Revolution|Mastery} | {Unlocking|Harnessing|Activating|Unleashing|Channeling} {Human|Collective|Team|Organizational} {Potential|Capacity|Capability|Power|Genius} | {Beyond|Past|Moving Beyond|Transcending} {Limits|Boundaries|Constraints|Barriers|Horizons}: {New|Fresh|Innovative|Revolutionary} {Perspectives|Approaches|Solutions|Strategies} | {The|This|Our} {Age|Era|Time|Period} {of|for|in} {Conscious|Aware|Mindful|Intentional} {Leadership|Business|Strategy|Innovation} | {Sacred|Ancient|Esoteric|Mystical} {Wisdom|Knowledge|Teachings|Mysteries} {for|in|to} {Modern|Contemporary|Today's} {Life|Business|Leadership|Success} | {Quantum|Cosmic|Universal|Divine} {Shift|Evolution|Transformation|Awakening} {in|of|for} {Consciousness|Awareness|Perception|Reality} | {The|A|This} {Great|Profound|Fundamental|Deep} {Reset|Recalibration|Realignment|Restructuring} {of|for|in} {Everything|All Things|Reality|Systems} | {Embracing|Integrating|Honoring|Welcoming} {Chaos|Uncertainty|Complexity|Ambiguity|Paradox} {as|for} {Growth|Evolution|Transformation|Innovation} | {The|This|Our} {Alchemy|Magic|Art|Science} {of|for|in} {Transformation|Change|Evolution|Metamorphosis} {and|&} {Creation|Manifestation|Innovation} | {Resilient|Adaptive|Flexible|Agile|Dynamic} {Mindsets|Mental Models|Paradigms|Frameworks} {for|in|to} {Uncertain|Complex|Volatile|Rapidly-Changing} {Times|Environments|Worlds} | {The|Our|Your} {Collective|Shared|Unified|Common} {Vision|Dream|Future|Destiny}: {Co-creating|Building|Designing|Manifesting} {Tomorrow|The Future|What's Next} | {Sovereign|Authentic|True|Real} {Self|Identity|Being|Expression} {in|during|through} {Times|Ages|Eras} {of|for|in} {Change|Transition|Transformation|Awakening} | {The|This|Our} {Return|Homecoming|Journey Back|Restoration} {to|towards|for} {Wholeness|Unity|Integration|Balance} {and|&} {Harmony|Peace|Alignment|Flow} | {Infinite|Limitless|Boundless|Unlimited} {Potential|Possibility|Capacity|Power} {Within|Inside|of} {You|Us|Every Being|Consciousness} | {Riding|Navigating|Mastering|Surfing} {Waves|Currents|Tides|Flows} {of|for|in} {Change|Evolution|Transformation|Progress} | {The|This|Our} {Sacred|Holy|Divine} {Dance|Play|Game|Journey} {of|for|in} {Creation|Manifestation|Evolution|Existence} | {Awakening|Remembering|Rediscovering|Uncovering} {Ancient|Primordial|Original|True} {Wisdom|Knowledge|Truth|Teachings} {for|in|to} {Modern Life|Today|Now} | {The|This|Our} {Bridge|Portal|Gateway|Threshold} {Between|Betwixt|Connecting} {Worlds|Realities|Dimensions|Eras} {and|&} {Possibilities|Potentials|Futures} | {Cosmic|Universal|Galactic|Celestial} {Alignment|Convergence|Synchronization|Harmony} {for|in|to|during|through} {Planetary|Global|Universal|Collective} {Awakening|Evolution|Transformation} | {The|This|Our} {Emergence|Arising|Birthing|Becoming} {into|as|through|for} {New|Next|Higher|Evolved} {States|Levels|Dimensions|Realms} {of|for|in} {Consciousness|Awareness|Being|Existence} | {The|This|Our} {Quantum|Paradigm|Reality|Fundamental} {Shift|Change|Leap|Transition}: {Reimagining|Rethinking|Reinventing|Transforming} {Everything|All Things|Reality|Possibility} | {Harmonizing|Balancing|Integrating|Unifying} {Masculine|Feminine|Yin|Yang|Dual} {and|&} {Feminine|Masculine|Yang|Yin|Non-Dual}: {The|This|Our} {Sacred|Divine|Holy|Mystical} {Union|Marriage|Integration|Wholeness} | {The|This|Our} {Path|Way|Journey|Quest} {of|for|in} {Heart|Love|Compassion|Service}: {Living|Being|Existing|Creating} {from|through|with} {Soul|Spirit|Essence|Core} | {Revolutionary|Paradigm-Shifting|Game-Changing|Transformative|Evolutionary} {Ideas|Concepts|Frameworks|Models} {for|in|to|during|through} {Tomorrow's|The Future|Next-Generation|Emerging} {World|Reality|Era|Age} | {The|This|Our} {Great|Profound|Fundamental|Momentous} {Work|Task|Mission|Purpose}: {Becoming|Evolving|Transforming|Ascending} {into|as|through|for} {Who|What|How} {We|You|One|Consciousness} {Truly|Really|Authentically|Essentially} {Are|Is|Can Be|Could Be} | {The|This|Our} {Alchemy|Magic|Art|Science} {of|for|in} {Turning|Transforming|Converting|Transmuting} {Lead|Challenges|Limitations|Darkness|Shadow} {into|to|as} {Gold|Wisdom|Strength|Light|Gifts} | {The|This|Our} {Return|Journey|Quest|Path} {to|toward|for|in} {Source|Origin|Beginning|Essence}: {Remembering|Rediscovering|Reclaiming|Awakening to} {Who|What|Why|How} {We|You|All|Consciousness} {Truly|Really|Essentially|Fundamentally} {Are|Is|Exists|Can Be} | {The|This|Our} {Sacred|Holy|Divine|Blessed} {Contract|Agreement|Promise|Covenant}: {Living|Fulfilling|Embodying|Realizing} {Your|Our|The|Universal} {Purpose|Mission|Destiny|Calling|Dharma} | {The|This|Our} {Age|Era|Time|Period} {of|for|in} {Miracles|Wonder|Magic|Mystery|Enchantment}: {Embracing|Welcoming|Celebrating|Honoring} {The|This|Our|All|Every} {Impossible|Unbelievable|Extraordinary|Supernatural} {Becoming|Becomes|Becoming Real|Manifesting} | {The|This|Our} {Revolution|Evolution|Transformation|Awakening} {of|for|in} {Consciousness|Awareness|Perception|Reality}: {Creating|Designing|Building|Manifesting} {New|Fresh|Innovative|Paradigm-Shifting} {Worlds|Realities|Futures|Possibilities} | {The|This|Our} {Journey|Path|Quest|Adventure} {Home|Back|Return|Homecoming} {to|toward|for|in} {Unity|Oneness|Wholeness|Integration|Love} {and|&} {Belonging|Connection|Relationship|Communion}} \ No newline at end of file diff --git a/lib/config.go b/lib/config.go index 2d8ac18b..cb98e8a0 100644 --- a/lib/config.go +++ b/lib/config.go @@ -16,6 +16,7 @@ import ( "github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis/data" "github.com/TecharoHQ/anubis/internal" + "github.com/TecharoHQ/anubis/internal/honeypot/naive" "github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/config" @@ -175,6 +176,33 @@ func New(opts Options) (*Server, error) { registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "") 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 if anubis.Version == "devel" { // make-challenge is only used in tests. Only enable while version is devel diff --git a/lib/policy/checker/checker.go b/lib/policy/checker/checker.go index 31551cd9..8163b96a 100644 --- a/lib/policy/checker/checker.go +++ b/lib/policy/checker/checker.go @@ -14,6 +14,14 @@ type Impl interface { Hash() string } +type Func func(*http.Request) (bool, error) + +func (f Func) Check(r *http.Request) (bool, error) { + return f(r) +} + +func (f Func) Hash() string { return internal.FastHash(fmt.Sprintf("%#v", f)) } + type List []Impl // Check runs each checker in the list against the request. diff --git a/test/go.mod b/test/go.mod index 8e6c7f3d..c7f4db16 100644 --- a/test/go.mod +++ b/test/go.mod @@ -66,6 +66,7 @@ require ( github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect + github.com/nikandfor/spintax v0.0.0-20181023094358-fc346b245bb3 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/test/go.sum b/test/go.sum index 8cca9c24..bbaf2b80 100644 --- a/test/go.sum +++ b/test/go.sum @@ -172,6 +172,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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/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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= diff --git a/web/index.go b/web/index.go index 6ff90527..6220d2ec 100644 --- a/web/index.go +++ b/web/index.go @@ -1,6 +1,10 @@ package web import ( + "context" + "fmt" + "io" + "github.com/a-h/templ" "github.com/TecharoHQ/anubis/lib/challenge" @@ -29,3 +33,10 @@ func ErrorPage(msg, mail, code string, localizer *localization.SimpleLocalizer) func Bench(localizer *localization.SimpleLocalizer) templ.Component { return bench(localizer) } + +func honeypotLink(href string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + fmt.Fprintf(w, ``, href) + return nil + }) +} diff --git a/web/index.templ b/web/index.templ index a6752fab..181262ae 100644 --- a/web/index.templ +++ b/web/index.templ @@ -6,6 +6,7 @@ import ( "github.com/TecharoHQ/anubis/lib/config" "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/xess" + "github.com/google/uuid" ) templ base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) { @@ -63,6 +64,7 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal @templ.JSONScript("anubis_public_url", anubis.PublicUrl) + @honeypotLink(fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString()))

{ title }

@body diff --git a/web/index_templ.go b/web/index_templ.go index 71a70064..94d3702c 100644 --- a/web/index_templ.go +++ b/web/index_templ.go @@ -14,6 +14,7 @@ import ( "github.com/TecharoHQ/anubis/lib/config" "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/xess" + "github.com/google/uuid" ) func base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) templ.Component { @@ -44,7 +45,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.GetLang()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 13, Col: 33} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 14, Col: 33} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -57,7 +58,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall 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: `index.templ`, Line: 15, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 16, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -70,7 +71,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var4 templ.SafeURL templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(anubis.BasePrefix + xess.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 16, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 17, Col: 61} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -88,7 +89,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 20, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 21, Col: 24} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -101,7 +102,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(value) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 20, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 21, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -132,20 +133,28 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = honeypotLink(fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString())).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 67, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 69, Col: 47} } _, 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, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -153,77 +162,77 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("protected_by")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 72, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 74, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " Anubis ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " Anubis ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("protected_from")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 72, Col: 127} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 74, Col: 127} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " Techaro. ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " Techaro. ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("made_with")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 74, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 76, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ".

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ".

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("mascot_design")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 76, Col: 39} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 78, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("celphase")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 76, Col: 123} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 78, Col: 123} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, ".

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ".

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if impressum != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -231,51 +240,51 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "-- Imprint

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">Imprint

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("version_info")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 83, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 85, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.Version) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 83, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 85, Col: 63} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ".

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ".

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -304,132 +313,132 @@ func errorPage(message, mail, code string, localizer *localization.SimpleLocaliz templ_7745c5c3_Var16 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
\"Sad\"Sad

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 94, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 96, Col: 14} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ".

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, ".

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if code != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 			var templ_7745c5c3_Var19 string
 			templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(code)
 			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 96, Col: 20}
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 98, Col: 20}
 			}
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if mail != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var20 string templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 100, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 102, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("contact_webmaster")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 100, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 102, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var23 string templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(mail) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 102, Col: 11} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 104, Col: 11} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var24 string templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 106, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 108, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -458,7 +467,7 @@ func StaticHappy(localizer *localization.SimpleLocalizer) templ.Component { templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var27 string templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("static_check_endpoint")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 119, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 121, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -514,176 +523,176 @@ func bench(localizer *localization.SimpleLocalizer) templ.Component { templ_7745c5c3_Var28 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 130, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 132, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 131, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 133, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_a")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 134, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 136, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var32 string templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_a")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 135, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 137, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var33 string templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_b")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 136, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 138, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var34 string templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_b")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 137, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 139, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var36 string templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 147, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 149, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }