bastion 0.54.0
ci / personal-data (push) Successful in 4s
ci / fuzz (push) Successful in 2m44s
ci / test (push) Successful in 2m11s

This commit is contained in:
Privatae
2026-10-21 22:29:10 +00:00
parent e4c9b69b46
commit d62da8adb4
58 changed files with 6944 additions and 124 deletions
+103
View File
@@ -0,0 +1,103 @@
package main
import (
"errors"
"fmt"
"sync/atomic"
"testing"
"time"
)
// Invented SIDs: the domain part is made up and names no real domain.
const (
testSIDAlice = "S-1-5-21-1000000001-1000000002-1000000003-1101"
testSIDBob = "S-1-5-21-1000000001-1000000002-1000000003-1102"
)
// One scan asks each SID once, and the account label (winTokenUser) and the
// owner's domain (ownerFactsReader) share the answer.
func TestSIDNamesAsksEachSIDOncePerScan(t *testing.T) {
var calls int32
lookup := func(sid string) (string, string, error) {
atomic.AddInt32(&calls, 1)
switch sid {
case testSIDAlice:
return "alice", "EXAMPLECORP", nil
case testSIDBob:
return "bob", "EXAMPLECORP", nil
}
return "", "", errors.New("none mapped")
}
names := newSIDNames(lookup, time.Second, make(chan struct{}, 1))
for i := 0; i < 40; i++ {
if got := winAccountLabel(testSIDAlice, names); got != `EXAMPLECORP\alice` {
t.Fatalf("label = %q", got)
}
if got := winAccountDomain(testSIDAlice, names); got != "EXAMPLECORP" {
t.Fatalf("domain = %q", got)
}
winAccountLabel(testSIDBob, names)
}
if got := winAccountLabel("S-1-5-21-9-9-9-9", names); got != "S-1-5-21-9-9-9-9" {
t.Fatalf("an unmapped SID must be shown as itself, got %q", got)
}
winAccountDomain("S-1-5-21-9-9-9-9", names)
if n := atomic.LoadInt32(&calls); n != 3 {
t.Fatalf("%d lookups for 3 distinct SIDs in one scan", n)
}
}
// A domain controller that does not answer: the first lookup hangs. The scan
// must stop waiting at its budget, every later SID must come back unresolved
// at once (no second lookup started), the owner's class must read unknown,
// and the next scan must not stack a second blocked lookup behind the first.
func TestSIDNamesUnreachableDirectoryDegradesWithinBudget(t *testing.T) {
release := make(chan struct{})
defer close(release)
var calls int32
lookup := func(sid string) (string, string, error) {
atomic.AddInt32(&calls, 1)
<-release
return "", "", errors.New("released")
}
slots := make(chan struct{}, 1)
const budget = 150 * time.Millisecond
for scan := 0; scan < 3; scan++ {
names := newSIDNames(lookup, budget, slots)
start := time.Now()
for i := 0; i < 30; i++ {
sid := fmt.Sprintf("S-1-5-21-1000000001-1000000002-1000000003-%d", 2000+i)
if got := winAccountLabel(sid, names); got != sid {
t.Fatalf("scan %d: unresolved SID shown as %q", scan, got)
}
o := classifyWinOwner(winOwner{SID: sid, Domain: winAccountDomain(sid, names),
Computer: "DESKTOP-TEST", Groups: map[string]bool{}, GroupsKnown: true})
if o.Class != ownerUnknown {
t.Fatalf("scan %d: class %q for an account whose domain could not be read", scan, o.Class)
}
}
if took := time.Since(start); took > budget+time.Second {
t.Fatalf("scan %d spent %v on lookups, budget %v", scan, took, budget)
}
}
if n := atomic.LoadInt32(&calls); n != 1 {
t.Fatalf("%d lookups started while the first was still blocked", n)
}
}
// A slow but answering lookup is used, and only what it took counts against
// the budget.
func TestSIDNamesSlowLookupWithinBudgetIsUsed(t *testing.T) {
lookup := func(sid string) (string, string, error) {
time.Sleep(20 * time.Millisecond)
return "carol", "EXAMPLECORP", nil
}
names := newSIDNames(lookup, 2*time.Second, make(chan struct{}, 1))
for i := 0; i < 5; i++ {
sid := fmt.Sprintf("S-1-5-21-1000000001-1000000002-1000000003-%d", 3000+i)
if d := winAccountDomain(sid, names); d != "EXAMPLECORP" {
t.Fatalf("lookup %d within budget came back %q", i, d)
}
}
}