e2e/audit_test.go

v0.2.0
gitbay/e2e/audit_test.go history · blame · raw

97 lines · 3997 bytes

 1package e2e
 2
 3import (
 4	"crypto/rand"
 5	"os"
 6	"path/filepath"
 7	"strings"
 8	"testing"
 9)
10
11func TestAuditAndHardening(t *testing.T) {
12	inst := startInstanceWith(t, "[limits]\nssh_auth_rate = 3\nmax_pack_bytes = 2000\n")
13	adminKey := inst.newKey(t, "root")
14	aliceKey := inst.newKey(t, "alice")
15	bobKey := inst.newKey(t, "bob")
16	inst.admin(t, "admin", "user", "create", "root", "--key", adminKey+".pub", "--admin")
17	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
18	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
19
20	// Mutating commands land in the audit log with source fingerprints;
21	// reads do not. Admin-only over SSH; host admin command works too.
22	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
23		t.Fatal("repo create failed")
24	}
25	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/app", "bob", "write"); code != 0 {
26		t.Fatal("grant failed")
27	}
28	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "list"); code != 0 {
29		t.Fatal("repo list failed")
30	}
31	if _, _, code := inst.ssh(t, aliceKey, "", "audit"); code != 4 {
32		t.Fatal("non-admin read the audit log")
33	}
34	out, _, code := inst.ssh(t, adminKey, "", "audit", "--json")
35	if code != 0 || !strings.Contains(out, "cmd repo create") ||
36		!strings.Contains(out, "cmd repo access grant") ||
37		!strings.Contains(out, `SHA256:`) || // key fingerprint as source
38		!strings.Contains(out, "admin user.created") {
39		t.Fatalf("audit content: %s", out)
40	}
41	if strings.Contains(out, "cmd repo list") {
42		t.Fatal("read-only command audited")
43	}
44	if out := inst.admin(t, "admin", "audit", "--limit", "5"); !strings.Contains(out, "cmd repo") {
45		t.Fatalf("host audit: %s", out)
46	}
47
48	// Disable: everything refused, sessions dropped, nothing deleted.
49	inst.admin(t, "admin", "user", "disable", "bob")
50	if _, errOut, code := inst.ssh(t, bobKey, "", "whoami"); code != 4 || !strings.Contains(errOut, "disabled") {
51		t.Fatalf("disabled ssh: exit %d, %s", code, errOut)
52	}
53	inst.admin(t, "admin", "user", "enable", "bob")
54	if _, _, code := inst.ssh(t, bobKey, "", "whoami"); code != 0 {
55		t.Fatal("re-enabled user still refused")
56	}
57
58	// max_pack_bytes: an oversized push is refused by receive-pack.
59	work := t.TempDir()
60	env := inst.gitEnv(aliceKey)
61	mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
62	dir := filepath.Join(work, "w")
63	big := make([]byte, 200_000)
64	rand.Read(big) // incompressible: the pack must exceed max_pack_bytes
65	os.WriteFile(filepath.Join(dir, "big.bin"), big, 0o644)
66	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
67	mustGit(t, dir, env, "add", ".")
68	mustGit(t, dir, env, "commit", "-q", "-m", "big")
69	if out, code := gitRun(t, dir, env, "push", "origin", "main"); code == 0 || !strings.Contains(out, "max") {
70		t.Fatalf("oversized push accepted: exit %d\n%s", code, out)
71	}
72	// A normal-sized push still works.
73	mustGit(t, dir, env, "rm", "-q", "big.bin")
74	os.WriteFile(filepath.Join(dir, "small.txt"), []byte("ok\n"), 0o644)
75	mustGit(t, dir, env, "add", ".")
76	mustGit(t, dir, env, "commit", "-q", "--amend", "-m", "small")
77	mustGit(t, dir, env, "push", "-q", "origin", "main")
78
79	// Auth rate limit, LAST because it locks out this whole IP: a burst
80	// of unknown-key failures throttles further auth — even a valid key
81	// — until the window passes. (Registration is closed, so unknown
82	// keys fail auth.) The audit is read host-locally: SSH is locked.
83	strangerKey := inst.newKey(t, "stranger")
84	for i := 0; i < 5; i++ {
85		inst.ssh(t, strangerKey, "", "whoami")
86	}
87	if _, _, code := inst.ssh(t, adminKey, "", "whoami"); code == 0 {
88		t.Fatal("valid key not throttled after failure burst")
89	}
90	auditOut := inst.admin(t, "admin", "audit")
91	if !strings.Contains(auditOut, "auth.failed") || !strings.Contains(auditOut, "auth.throttled") {
92		t.Fatalf("burst not audited:\n%s", auditOut)
93	}
94	if strings.Count(auditOut, "auth.throttled") != 1 {
95		t.Fatal("throttle audited more than once per window")
96	}
97}