Compare commits

..

10 Commits

Author SHA1 Message Date
Xe Iaso 8480175eac test: refactor cluster creation to a shell script
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 03:20:39 -04:00
Xe Iaso c082cd89dc test(i18n): fix backoff-retry invocation
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 03:12:17 -04:00
Xe Iaso 03bf695dff test: add i18n to tekton pipeline
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 03:07:29 -04:00
Xe Iaso 51ae340a7b test(robost_txt): make tekton specific test execution flow
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 03:02:09 -04:00
Xe Iaso 430e262c84 test: add robots.txt pass to tekton
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 02:51:39 -04:00
Xe Iaso a47efe31b0 test: add tekton specific test scripts that point to the k3k cluster
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 02:38:30 -04:00
Xe Iaso 763c896b63 test(default-config): create self-contained venv
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 02:33:22 -04:00
Xe Iaso a426230698 test: start k3k provisioning
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 01:53:47 -04:00
Xe Iaso 6c3fc188fb test: iterate a little more
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 01:32:17 -04:00
Xe Iaso a0589d3c7a test: start working on anubis tekton pipeline
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-05-16 01:22:35 -04:00
33 changed files with 444 additions and 269 deletions
-1
View File
@@ -318,7 +318,6 @@ screenshots
searchbot
searx
sebest
seccomp
secretplans
selfsigned
Semrush
-9
View File
@@ -52,15 +52,6 @@ jobs:
platforms: linux/amd64
push: true
- name: Pin docs image to built digest
working-directory: docs/manifest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
KUSTOMIZE_VERSION=v5.4.3
curl -fsSL "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz" | tar -xz
./kustomize edit set image "ghcr.io/techarohq/anubis/docs=ghcr.io/techarohq/anubis/docs@${DIGEST}"
- name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@934aaa4354bbbc3d2176ae8d7cae92d515032dff # v1.35.3
env:
+35
View File
@@ -0,0 +1,35 @@
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: anubis-m-
namespace: ci
spec:
params:
- name: commit
value: "Xe/tekton"
- name: branch
value: main
pipelineRef:
name: anubis-build-test
taskRunTemplate:
serviceAccountName: anubis-k3k
timeouts:
pipeline: 1h0m0s
workspaces:
- name: repo
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 4Gi
- name: go-mod-cache
persistentVolumeClaim:
claimName: go-mod-cache
- name: dockerconfig-atcr
secret:
secretName: atcr
- name: dockerconfig-ghcr
secret:
secretName: ghcr
+217
View File
@@ -0,0 +1,217 @@
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: anubis-build-test
namespace: ci
spec:
description: |
The CI/CD pipeline for Anubis
params:
- name: repo-url
type: string
description: "Git repo to clone"
default: "https://github.com/TecharoHQ/anubis"
- name: "branch"
type: string
description: "Git branch to operate against"
- name: "commit"
type: string
description: "Git revision to check out"
- name: "actor"
type: string
description: "Tangled actor"
default: "did:web:anubis.techaro.lol"
- name: docker-image-base
type: string
description: string prefix for production docker images
default: "registry.int.xeserv.us/techarohq"
- name: docker-cache
type: string
description: docker repo to store cache files
default: "registry.int.xeserv.us/techarohq/anubis/cache"
- name: go-version
type: string
description: "Go version to use"
default: "1.26.3"
workspaces:
- name: repo
description: |
Cloned repo files.
- name: dockerconfig-atcr
description: |
Docker config for pushing images to atcr
- name: dockerconfig-ghcr
description: |
Docker config for pushing images to ghcr
tasks:
- name: fix-permissions
taskRef:
name: fix-permissions
workspaces:
- name: dir
workspace: repo
- name: clone-repo
runAfter: ["fix-permissions"]
taskRef:
name: git-clone-naive
workspaces:
- name: output
workspace: repo
params:
- name: url
value: $(params.repo-url)
- name: revision
value: $(params.commit)
- name: docker-build-ci
runAfter: ["clone-repo"]
workspaces:
- name: source
workspace: repo
taskRef:
name: kaniko
params:
- name: IMAGE
value: $(params.docker-image-base)/anubis/ci:$(tasks.clone-repo.results.version)
- name: DOCKERFILE
value: ./test/ssh-ci/Dockerfile
- name: EXTRA_ARGS
value:
[
"--build-arg=GO_VERSION=$(params.go-version)",
"--cache",
"--cache-copy-layers",
"--cache-run-layers",
"--cache-repo=$(params.docker-cache)",
"--label=org.tangled.actor=$(params.actor)",
"--snapshot-mode=redo",
"--use-new-run",
]
- name: provision-test-cluster
runAfter: ["docker-build-ci"]
taskSpec:
workspaces:
- name: repo
mountPath: /src
results:
- name: cluster-name
description: "k3k cluster name object in k8s"
steps:
- name: create-cluster
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
workingDir: $(workspaces.repo.path)/repo
env:
- name: NAMESPACE
value: $(context.pipelineRun.namespace)
- name: PIPELINE_NAME
value: $(context.pipeline.name)
- name: PIPELINERUN_NAME
value: $(context.pipelineRun.name)
- name: PIPELINERUN_UID
value: $(context.pipelineRun.uid)
- name: KUBECONFIG_OUT
value: $(workspaces.repo.path)/kube/config
script: |
#!/usr/bin/env bash
set -euo pipefail
./test/k3k/create-cluster.sh > "$(results.cluster-name.path)"
- name: build-assets
runAfter: ["docker-build-ci"]
taskSpec:
workspaces:
- name: repo
mountPath: /src
steps:
- name: test
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
workingDir: $(workspaces.repo.path)/repo
script: |
npm ci
npm run assets
workspaces:
- name: repo
workspace: repo
- name: go-test
runAfter: ["build-assets"]
taskSpec:
workspaces:
- name: repo
mountPath: /src
steps:
- name: test
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
workingDir: $(workspaces.repo.path)/repo
script: |
SKIP_INTEGRATION=1 go test ./...
workspaces:
- name: repo
workspace: repo
- name: test-anubis
runAfter: ["build-assets"]
taskRef:
name: ko
workspaces:
- name: source
workspace: repo
params:
- name: VERSION
value: $(tasks.clone-repo.results.version)
- name: SOURCE_DATE_EPOCH
value: $(tasks.clone-repo.results.source-date-epoch)
- name: KO_DOCKER_REPO
value: $(params.docker-image-base)
- name: extra-args
value:
[
"--platform=all",
"--base-import-paths",
"--tags=$(tasks.clone-repo.results.version)",
"--image-label=org.tangled.actor=$(params.actor)",
]
- name: packages
value:
- ./cmd/anubis
- name: integration
runAfter:
- "provision-test-cluster"
- "build-assets"
- "test-anubis"
matrix:
params:
- name: test-case
value:
- default-config-macro
- i18n
- robots_txt
taskSpec:
params:
- name: test-case
type: string
workspaces:
- name: repo
mountPath: /src
steps:
- name: exec
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
workingDir: $(workspaces.repo.path)/repo/test/$(params.test-case)
script: |
./tekton.sh
env:
- name: KUBECONFIG
value: "$(workspaces.repo.path)/kube/config"
finally:
- name: teardown-cluster
when:
- input: "$(tasks.provision-test-cluster.status)"
operator: in
values: ["Succeeded"]
taskSpec:
workspaces:
- name: repo
mountPath: /src
steps:
- name: delete
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
workingDir: $(workspaces.repo.path)/repo
script: |
kubectl delete --ignore-not-found -n $(context.pipelineRun.namespace) clusters.k3k.io/"$(tasks.provision-test-cluster.results.cluster-name)"
+4
View File
@@ -0,0 +1,4 @@
namespace: ci
resources:
- anubis-test.yaml
- rbac.yaml
+32
View File
@@ -0,0 +1,32 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: anubis-k3k
namespace: ci
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: anubis-k3k
namespace: ci
rules:
- apiGroups: ["k3k.io"]
resources: ["clusters"]
verbs: ["*"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: anubis-k3k
namespace: ci
subjects:
- kind: ServiceAccount
name: anubis-k3k
namespace: ci
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: anubis-k3k
+2 -2
View File
@@ -1,11 +1,11 @@
FROM docker.io/library/node:22.22.3-alpine AS build
FROM docker.io/library/node:lts AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM ghcr.io/xe/nginx-micro:v1.29.0
FROM ghcr.io/xe/nginx-micro
COPY --from=build /app/build /www
COPY ./manifest/cfg/nginx/nginx.conf /conf
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
+1 -9
View File
@@ -28,16 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580).
- Add config option to add ASN to logs/metrics.
- Log weight when issuing challenge.
- Gate pprof endpoints behind `metrics.debug` in the policy file.
- Limit naive honeypot r9k delay to one second.
- Fix an obscure case where adding query values to a subrequest match could cause an invalid rule match when using path based matching for protected resources.
- Fix an edge case where load average expression values could nil pointer dereference when Anubis just started up.
- Fix an obscure case where Anubis in subrequest mode could allow redirects to invalid domains with strange instructions.
- Log weight when issuing challenge
- Fix `path_regex` and CEL `path` rules not matching when using Traefik `forwardAuth` middleware. Anubis now checks `X-Forwarded-Uri` (Traefik) in addition to `X-Original-URI` (nginx) when resolving the request path in subrequest mode ([#1628](https://github.com/TecharoHQ/anubis/issues/1628)).
- Validate bounds in the CEL `randInt` helper so non-positive or platform-overflowing arguments surface a typed CEL error instead of an evaluator panic.
- Harden the public docs deployment: add a pod-level security context to the nginx container (non-root uid 101, dropped capabilities, read-only root filesystem, `RuntimeDefault` seccomp) and rebind it to unprivileged port `8080`.
- Pin docs deployment images to immutable digests with `imagePullPolicy: IfNotPresent`, and have the docs-deploy workflow overlay the just-built digest via `kustomize edit set image` so each rollout references an auditable artifact instead of a floating `:main` tag. The docs `Dockerfile` now pins `node` and `nginx-micro` base images to specific versions.
## v1.25.0: Necron
-18
View File
@@ -138,24 +138,6 @@ metrics:
socketMode: "0700" # must be a string
```
### Debug routes
Anubis' metrics server supports [pprof](https://pkg.go.dev/runtime/pprof), the Go standard library tool for profiling Go applications. This is very useful for debugging how Anubis works in the wild with regards to CPU, multicore, and RAM usage. pprof is very powerful and can expose command line arguments as part of the debugging setup (inside Google, everything is done with command line flags).
Prior versions of Anubis exposed pprof endpoints on all TCP bindhosts by default. This means that machines with incorrectly configured firewalls can expose command line arguments to the public internet in the right conditions.
In order to enable pprof profiling endpoints on the Metrics server, set the `debug` flag under the `metrics` block:
```yaml
metrics:
bind: ":9090"
network: "tcp"
debug: true
```
To err on the side of caution, this defaults to disabled. If this defaults migration breaks your configuration, please let us know in a ticket.
### TLS
If you want to serve the metrics server over TLS, use the `tls` block:
+2 -8
View File
@@ -1,7 +1,7 @@
user nginx;
worker_processes 2;
error_log /dev/stdout warn;
pid /tmp/nginx.pid;
pid /nginx.pid;
events {
worker_connections 1024;
@@ -12,17 +12,11 @@ http {
default_type application/octet-stream;
access_log /dev/stdout;
client_body_temp_path /tmp/client_body;
proxy_temp_path /tmp/proxy;
fastcgi_temp_path /tmp/fastcgi;
uwsgi_temp_path /tmp/uwsgi;
scgi_temp_path /tmp/scgi;
sendfile on;
keepalive_timeout 65;
server {
listen 8080 default_server;
listen 80 default_server;
server_name _;
error_page 404 /404.html;
+8 -23
View File
@@ -20,12 +20,10 @@ spec:
name: nginx-cfg
- name: temporary-data
emptyDir: {}
- name: nginx-tmp
emptyDir: {}
containers:
- name: anubis-docs
image: ghcr.io/techarohq/anubis/docs@sha256:f13a7c862bbcba8e19feba9f157120c6f03e23b03367ace4ca55da69dc894e12
imagePullPolicy: IfNotPresent
image: ghcr.io/techarohq/anubis/docs:main
imagePullPolicy: Always
resources:
limits:
memory: "128Mi"
@@ -36,36 +34,23 @@ spec:
volumeMounts:
- name: nginx
mountPath: /conf
- name: nginx-tmp
mountPath: /tmp
ports:
- containerPort: 8080
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 8080
port: 80
initialDelaySeconds: 1
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 8080
port: 80
initialDelaySeconds: 10
periodSeconds: 20
securityContext:
runAsUser: 101
runAsGroup: 101
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
- name: anubis
image: ghcr.io/techarohq/anubis@sha256:533e57956ae3afd1612dab16f02dd4db937ca14fad5399208f403686e14feed5
imagePullPolicy: IfNotPresent
image: ghcr.io/techarohq/anubis:main
imagePullPolicy: Always
env:
- name: "BIND"
value: ":8081"
@@ -80,7 +65,7 @@ spec:
- name: "SERVE_ROBOTS_TXT"
value: "false"
- name: "TARGET"
value: "http://localhost:8080"
value: "http://localhost:80"
# - name: "SLOG_LEVEL"
# value: "debug"
volumeMounts:
+1 -1
View File
@@ -7,7 +7,7 @@ spec:
app: anubis-docs
ports:
- port: 80
targetPort: 8080
targetPort: 80
name: http
- port: 8081
targetPort: 8081
+1 -1
View File
@@ -169,7 +169,7 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
millisecondAmount := min(math.Pow(float64(networkCount), 2), 1000)
millisecondAmount := math.Pow(float64(networkCount), 2)
time.Sleep(time.Duration(millisecondAmount) * time.Millisecond)
spins := i.makeSpins()
-1
View File
@@ -32,7 +32,6 @@ type Metrics struct {
Network string `json:"network" yaml:"network"`
SocketMode string `json:"socketMode" yaml:"socketMode"`
TLS *MetricsTLS `json:"tls" yaml:"tls"`
Debug bool `json:"debug" yaml:"debug"`
BasicAuth *MetricsBasicAuth `json:"basicAuth" yaml:"basicAuth"`
}
+7 -8
View File
@@ -403,15 +403,14 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
localizer := localization.GetLocalizer(r)
redir := r.FormValue("redir")
urlParsed, err := url.Parse(redir)
urlParsed, err := url.ParseRequestURI(redir)
if err != nil {
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
return
}
if urlParsed.Opaque != "" || (urlParsed.Scheme == "" && strings.HasPrefix(redir, "//")) {
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
return
// if ParseRequestURI fails, try as relative URL
urlParsed, err = r.URL.Parse(redir)
if err != nil {
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
return
}
}
// validate URL scheme to prevent javascript:, data:, file:, tel:, etc.
-14
View File
@@ -223,17 +223,3 @@ func TestNoCacheOnError(t *testing.T) {
})
}
}
func TestRejectsHostlessRedirect(t *testing.T) {
pol := loadPolicies(t, "testdata/useragent.yaml", 0)
srv := spawnAnubis(t, Options{Policy: pol, RedirectDomains: []string{"allowed.example"}})
req := httptest.NewRequest(http.MethodGet, "https://anubis.example/.within.website/?redir=%2f%2fevil.example%2fphish", nil)
rr := httptest.NewRecorder()
srv.ServeHTTPNext(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected hostless redirect to be rejected, got HTTP %d body %q", rr.Code, rr.Body.String())
}
if got := rr.Header().Get("Location"); got != "" {
t.Fatalf("expected no Location header on rejected redirect, got %q", got)
}
}
+5 -9
View File
@@ -34,15 +34,11 @@ func (s *Server) Run(ctx context.Context, done func()) {
func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
mux := http.NewServeMux()
if s.Config.Debug {
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile)
mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace)
}
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile)
mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace)
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
st, ok := internal.GetHealth("anubis")
-49
View File
@@ -1,49 +0,0 @@
package metrics
import (
"context"
"io"
"log/slog"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/TecharoHQ/anubis/lib/config"
)
func TestMetricsPprofCmdlineExposedWithoutAuthentication(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
addr := ln.Addr().String()
_ = ln.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan struct{})
srv := &Server{
Config: &config.Metrics{Network: "tcp", Bind: addr},
Log: slog.Default(),
}
go srv.Run(ctx, func() { close(done) })
url := "http://" + addr + "/debug/pprof/cmdline"
var body []byte
resp, err := http.Get(url)
if err == nil {
body, err = io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("can't read body: %v", err)
}
defer resp.Body.Close()
}
time.Sleep(50 * time.Millisecond)
if strings.Contains(string(body), "metrics.test") {
t.Fatalf("pprof is enabled by default, cmdline process arguments: %q", string(body))
}
cancel()
<-done
}
-4
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"net/http"
"net/netip"
"net/url"
"regexp"
"strings"
@@ -115,9 +114,6 @@ func (pc *PathChecker) Check(r *http.Request) (bool, error) {
originalUrl = r.Header.Get("X-Forwarded-Uri")
}
if originalUrl != "" {
if parsed, err := url.ParseRequestURI(originalUrl); err == nil {
originalUrl = parsed.Path
}
if pc.regexp.MatchString(originalUrl) {
return true, nil
}
+1 -10
View File
@@ -222,16 +222,7 @@ func New(opts ...cel.EnvOption) (*cel.Env, error) {
return types.ValOrErr(val, "value is not an integer, but is %T", val)
}
if n <= 0 {
return types.NewErr("randInt bound must be positive, got %d", int64(n))
}
bound := int(n)
if types.Int(bound) != n {
return types.NewErr("randInt bound %d overflows platform int", int64(n))
}
return types.Int(rand.IntN(bound))
return types.Int(rand.IntN(int(n)))
}),
),
),
@@ -9,7 +9,6 @@ import (
"github.com/TecharoHQ/anubis/internal/dns"
"github.com/TecharoHQ/anubis/lib/store/memory"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
@@ -689,14 +688,6 @@ func TestNewEnvironment(t *testing.T) {
description: "should return values in correct range",
shouldCompile: true,
},
{
name: "randInt-large-bound",
expression: `randInt(2147483647) >= 0`,
variables: map[string]any{},
expectBool: boolPtr(true),
description: "should accept int32-max bounds without overflow",
shouldCompile: true,
},
{
name: "strings-extension-size",
expression: `"hello".size() == 5`,
@@ -759,65 +750,3 @@ func TestNewEnvironment(t *testing.T) {
func boolPtr(b bool) *bool {
return &b
}
func TestRandIntInvalidBounds(t *testing.T) {
env, err := New(cel.Variable("contentLength", cel.IntType))
if err != nil {
t.Fatalf("failed to create environment: %v", err)
}
tests := []struct {
name string
expression string
variables map[string]any
wantErrText string
description string
}{
{
name: "zero-bound-literal",
expression: `randInt(0)`,
variables: map[string]any{},
wantErrText: "randInt bound must be positive",
description: "randInt(0) should return a CEL error, not panic",
},
{
name: "negative-bound-literal",
expression: `randInt(-5)`,
variables: map[string]any{},
wantErrText: "randInt bound must be positive",
description: "randInt(-5) should return a CEL error, not panic",
},
{
name: "zero-bound-from-variable",
expression: `randInt(contentLength)`,
variables: map[string]any{"contentLength": 0},
wantErrText: "randInt bound must be positive",
description: "attacker-controlled zero contentLength should error gracefully",
},
{
name: "negative-bound-from-variable",
expression: `randInt(contentLength)`,
variables: map[string]any{"contentLength": -1},
wantErrText: "randInt bound must be positive",
description: "attacker-controlled negative contentLength should error gracefully",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prog, err := Compile(env, tt.expression)
if err != nil {
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
}
result, _, err := prog.Eval(tt.variables)
if err == nil {
t.Fatalf("%s: expected an evaluation error, got result %v", tt.description, result)
}
if !strings.Contains(err.Error(), tt.wantErrText) {
t.Errorf("%s: expected error containing %q, got %q", tt.description, tt.wantErrText, err.Error())
}
})
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ var (
)
func init() {
globalLoadAvg = &loadAvg{data: &load.AvgStat{}}
globalLoadAvg = &loadAvg{}
go globalLoadAvg.updateThread(context.Background())
}
-26
View File
@@ -1,8 +1,6 @@
package policy
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
@@ -87,27 +85,3 @@ func TestBadConfigs(t *testing.T) {
})
}
}
func TestPathCheckerStripsForwardedURIQuery(t *testing.T) {
checker, err := NewPathChecker("^/admin$", true)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "https://anubis.local/.within.website/x/cmd/anubis/api/check", nil)
req.Header.Set("X-Forwarded-Uri", "/admin?x=1")
matched, err := checker.Check(req)
if err != nil {
t.Fatal(err)
}
if !matched {
t.Fatalf("expected exact path checker to match forwarded URI when query string is appended")
}
req.Header.Set("X-Forwarded-Uri", "/admin")
matched, err = checker.Check(req)
if err != nil {
t.Fatal(err)
}
if !matched {
t.Fatalf("expected exact path checker to match forwarded URI without query string")
}
}
+3 -1
View File
@@ -15,7 +15,9 @@
"package": "go tool yeet",
"lint": "make lint",
"prepare": "husky && go mod download",
"format": "prettier -w . 2>&1 >/dev/null && go run goimports -w ."
"format": "prettier -w . 2>&1 >/dev/null && go run goimports -w .",
"deploy:ci": "kubectl apply -k .tekton -n ci --context admin@alrest",
"deploy:ci:invoke": "npm run deploy:ci && kubectl create -f .tekton/anubis-pipelinerun.yaml -n ci --context admin@alrest"
},
"author": "",
"license": "ISC",
+1
View File
@@ -0,0 +1 @@
.env
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
exec ./test.sh
+6 -1
View File
@@ -3,5 +3,10 @@
set -euo pipefail
cd "$(dirname "$0")"
python3 -m venv .env
source .env/bin/activate
pip install pyyaml
python3 -c 'import yaml'
python3 ./compare_bots.py
python3 ./compare_bots.py
+1
View File
@@ -104,5 +104,6 @@ require (
tool (
github.com/TecharoHQ/anubis/cmd/anubis
github.com/TecharoHQ/anubis/utils/cmd/backoff-retry
github.com/jsha/minica
)
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
function cleanup() {
pkill -P $$
}
trap cleanup EXIT SIGINT
go tool anubis --help 2>/dev/null || :
go run ../cmd/unixhttpd &
go tool anubis \
--policy-fname ./anubis.yaml \
--use-remote-address \
--target=unix://$(pwd)/unixhttpd.sock &
go tool backoff-retry node ./test.mjs
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Create a k3k cluster, wait for it to be Ready, and write its kubeconfig.
# Prints the generated cluster name to stdout on success.
#
# Required env:
# NAMESPACE Kubernetes namespace to create the cluster in
# KUBECONFIG_OUT Path to write the resulting kubeconfig
#
# Optional env (set under Tekton to enable ownerReference-based GC + labels):
# PIPELINE_NAME Tekton Pipeline name
# PIPELINERUN_NAME Tekton PipelineRun name
# PIPELINERUN_UID Tekton PipelineRun UID
set -euo pipefail
: "${NAMESPACE:?NAMESPACE must be set}"
: "${KUBECONFIG_OUT:?KUBECONFIG_OUT must be set}"
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
cluster_name=$(kubectl create -n "${NAMESPACE}" -f "${script_dir}/test-cluster.yaml" -ojson | jq -r '.metadata.name')
if [[ -n "${PIPELINERUN_NAME:-}" && -n "${PIPELINERUN_UID:-}" ]]; then
owner_ref=$(jo \
apiVersion=tekton.dev/v1 \
kind=PipelineRun \
name="${PIPELINERUN_NAME}" \
uid="${PIPELINERUN_UID}" \
blockOwnerDeletion=false)
patch=$(jo metadata=$(jo "ownerReferences[]=${owner_ref}"))
kubectl patch -n "${NAMESPACE}" "clusters.k3k.io/${cluster_name}" --type=merge -p "${patch}" >&2
kubectl label -n "${NAMESPACE}" "clusters.k3k.io/${cluster_name}" \
"tekton.dev/memberOf=tasks" \
"tekton.dev/pipeline=${PIPELINE_NAME:-}" \
"tekton.dev/pipelineRun=${PIPELINERUN_NAME}" \
"tekton.dev/pipelineRunUID=${PIPELINERUN_UID}" >&2
fi
kubectl wait --for=condition=Ready "clusters.k3k.io/${cluster_name}" -n "${NAMESPACE}" --timeout 5m >&2
kubectl wait --for=create "secret/k3k-${cluster_name}-kubeconfig" -n "${NAMESPACE}" --timeout 5m >&2
mkdir -p "$(dirname "${KUBECONFIG_OUT}")"
kubectl get -ojson -n "${NAMESPACE}" "secret/k3k-${cluster_name}-kubeconfig" \
| jq -r '.data["kubeconfig.yaml"]' \
| base64 -d > "${KUBECONFIG_OUT}"
echo "${cluster_name}"
+5
View File
@@ -0,0 +1,5 @@
apiVersion: k3k.io/v1beta1
kind: Cluster
metadata:
generateName: anubis-test-
namespace: ci
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
function cleanup() {
pkill -P $$
}
trap cleanup EXIT SIGINT
go tool anubis --help 2>/dev/null || :
go run ../cmd/unixhttpd &
go tool anubis \
--policy-fname ./anubis.yaml \
--use-remote-address \
--serve-robots-txt \
--target=unix://$(pwd)/unixhttpd.sock &
go tool backoff-retry node ./test.mjs
+12 -2
View File
@@ -1,5 +1,15 @@
ARG ALPINE_VERSION=3.22
ARG GO_VERSION=1.26.3
# Go toolchain bootstrapper
FROM golang:${GO_VERSION} AS go
RUN CGO_ENABLED=0 go install golang.org/dl/go1.23.6@latest \
&& mkdir -p /app/bin \
&& mv /go/bin/go1.23.6 /app/bin/go
FROM alpine:${ALPINE_VERSION}
RUN apk add -U go nodejs git build-base git npm bash zstd brotli gzip
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
COPY --from=go /app/bin/go /usr/local/bin/go
RUN apk add -U nodejs git build-base git npm bash zstd brotli gzip jq jo kubectl python3 py3-pip py3-virtualenv \
&& go download