Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a14bfc-9643-7872-aa12-98326dcf25e7
323 lines
9.7 KiB
Go
323 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
if code, handled := internalProcess(os.Args[1:]); handled {
|
|
os.Exit(code)
|
|
}
|
|
if len(os.Args) == 2 && os.Args[1] == "--test-storage-refusal" {
|
|
storage, err := newVolatileStorage()
|
|
if err == nil {
|
|
storage.close()
|
|
os.Exit(3)
|
|
}
|
|
json.NewEncoder(os.Stdout).Encode(map[string]string{"status": "unverified", "reason": err.Error()})
|
|
os.Exit(0)
|
|
}
|
|
if len(os.Args) == 2 && os.Args[1] == "--test-storage-parent" {
|
|
storage, err := newVolatileStorage()
|
|
if err != nil {
|
|
os.Exit(3)
|
|
}
|
|
if os.WriteFile(filepath.Join(storage.path, "input"), []byte("synthetic private test input"), 0600) != nil {
|
|
os.Exit(3)
|
|
}
|
|
json.NewEncoder(os.Stdout).Encode(map[string]any{"path": storage.path, "watchdog": storage.command.Process.Pid})
|
|
child := exec.Command("/proc/self/exe", "--test-storage-reader", storage.path)
|
|
child.Stdout = os.Stdout
|
|
if runChild(child) != nil {
|
|
os.Exit(4)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
if len(os.Args) == 3 && os.Args[1] == "--test-storage-reader" {
|
|
f, err := os.Open(filepath.Join(os.Args[2], "input"))
|
|
if err != nil {
|
|
os.Exit(3)
|
|
}
|
|
defer f.Close()
|
|
fmt.Fprintln(os.Stdout, os.Getpid())
|
|
time.Sleep(time.Minute)
|
|
os.Exit(0)
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestVolatileStorageLifecycle(t *testing.T) {
|
|
storage, err := newVolatileStorage()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !storage.live() {
|
|
t.Fatal("watchdog did not remain live")
|
|
}
|
|
if err = os.WriteFile(filepath.Join(storage.path, "input"), []byte("synthetic input"), 0600); err != nil {
|
|
t.Fatal("cannot write private fixture")
|
|
}
|
|
if err = storage.close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = os.Lstat(storage.path); !os.IsNotExist(err) {
|
|
t.Fatal("private storage retained")
|
|
}
|
|
}
|
|
|
|
func TestVolatileIdentityRefusesReplacement(t *testing.T) {
|
|
storage, err := newVolatileStorage()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Keep the original inode alive so a new directory cannot reuse it.
|
|
original := storage.path + "-original"
|
|
if os.Rename(storage.path, original) != nil || os.Mkdir(storage.path, 0700) != nil {
|
|
t.Fatal("cannot prepare replacement")
|
|
}
|
|
defer os.RemoveAll(original)
|
|
defer os.RemoveAll(storage.path)
|
|
if err = storage.close(); err == nil {
|
|
t.Fatal("replacement directory accepted")
|
|
}
|
|
if _, err = os.Stat(storage.path); err != nil {
|
|
t.Fatal("replacement directory was removed")
|
|
}
|
|
}
|
|
|
|
func TestReceiverDeathCleansStorageAndStopsReader(t *testing.T) {
|
|
cmd := exec.Command("/proc/self/exe", "--test-storage-parent")
|
|
pipe, err := cmd.StdoutPipe()
|
|
if err != nil || cmd.Start() != nil {
|
|
t.Fatal("cannot start disposable parent")
|
|
}
|
|
defer cmd.Process.Kill()
|
|
reader := bufio.NewReader(pipe)
|
|
line, err := reader.ReadBytes('\n')
|
|
var receipt struct {
|
|
Path string
|
|
Watchdog int
|
|
}
|
|
if err != nil || json.Unmarshal(line, &receipt) != nil || receipt.Path == "" {
|
|
t.Fatal("disposable parent did not prepare storage")
|
|
}
|
|
var childPID int
|
|
if _, err = fmt.Fscanln(reader, &childPID); err != nil || childPID < 2 {
|
|
t.Fatal("disposable reader did not open input")
|
|
}
|
|
if cmd.Process.Kill() != nil {
|
|
t.Fatal("cannot kill disposable parent")
|
|
}
|
|
cmd.Wait()
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
_, pathErr := os.Lstat(receipt.Path)
|
|
stat, procErr := os.ReadFile(fmt.Sprintf("/proc/%d/stat", childPID))
|
|
terminal := os.IsNotExist(procErr)
|
|
if procErr == nil {
|
|
// Zombies are terminal; the private PID namespace reaps them.
|
|
fields := strings.Fields(string(stat)[strings.LastIndex(string(stat), ")")+1:])
|
|
terminal = len(fields) > 0 && fields[0] == "Z"
|
|
}
|
|
if os.IsNotExist(pathErr) && terminal {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatal("receiver loss retained private input or live reader")
|
|
}
|
|
|
|
func TestSmallInputParity(t *testing.T) {
|
|
scanner := os.Getenv("FORGE_TEST_GITLEAKS")
|
|
if scanner == "" {
|
|
t.Skip("requires explicitly admitted disposable Gitleaks binary")
|
|
}
|
|
f, err := pinScanner(scanner)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
storage, err := newVolatileStorage()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := storage.close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
config := t.TempDir()
|
|
write(t, filepath.Join(config, "scanner.toml"), []byte("[extend]\nuseDefault = true\n"))
|
|
write(t, filepath.Join(config, "ignore"), nil)
|
|
privateKey := key(t)
|
|
var random [32]byte
|
|
if _, err := rand.Read(random[:]); err != nil {
|
|
t.Fatal("cannot generate synthetic token")
|
|
}
|
|
canary := []byte("glpat-" + hex.EncodeToString(random[:]))
|
|
for _, tc := range []struct {
|
|
name string
|
|
input []byte
|
|
want bool
|
|
}{
|
|
{"empty", nil, false},
|
|
{"clean", []byte("reviewed clean input\n"), false},
|
|
{"exact-limit", bytes.Repeat([]byte("x"), directoryInputBytes), false},
|
|
{"private-key", privateKey, true},
|
|
{"exact-limit-private-key", append(bytes.Repeat([]byte("\n"), directoryInputBytes-len(privateKey)), privateKey...), true},
|
|
{"binary-private-key", append([]byte{0, 1, 0}, privateKey...), true},
|
|
{"token", canary, true},
|
|
{"binary-token", append([]byte{0, 0}, canary...), true},
|
|
{"inline-suppression", append([]byte("# gitleaks:allow\n"), privateKey...), true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
stdin, e := scan(context.Background(), f, config, tc.input)
|
|
if e != nil || stdin != tc.want {
|
|
t.Fatal("stdin reference outcome differs", e)
|
|
}
|
|
name := strings.Repeat("a", 40)
|
|
blocked, e := scanSmall(context.Background(), f, config, storage, []scanInput{{name, tc.input}})
|
|
if e != nil || (blocked != "") != stdin || blocked != "" && blocked != name {
|
|
t.Fatal("directory and original stdin detection differ", e)
|
|
}
|
|
})
|
|
}
|
|
t.Run("sixty-four-distinct-inputs", func(t *testing.T) {
|
|
var inputs []scanInput
|
|
for i := 0; i < directoryBatchInputs; i++ {
|
|
inputs = append(inputs, scanInput{fmt.Sprintf("%040x", i), []byte("clean")})
|
|
}
|
|
inputs[63].content = privateKey
|
|
blocked, e := scanSmall(context.Background(), f, config, storage, inputs)
|
|
if e != nil || blocked != inputs[63].name {
|
|
t.Fatal("last batch input was not detected", e)
|
|
}
|
|
})
|
|
t.Run("expired-context", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
_, e := scanSmall(ctx, f, config, storage, []scanInput{{strings.Repeat("b", 40), []byte("clean")}})
|
|
if e == nil || e.Error() != "scan-deadline-exceeded" {
|
|
t.Fatal("expired scan did not fail closed")
|
|
}
|
|
})
|
|
t.Run("invalid-inputs", func(t *testing.T) {
|
|
name := strings.Repeat("c", 40)
|
|
for _, inputs := range [][]scanInput{
|
|
{{"../escape", []byte("clean")}},
|
|
{{name, bytes.Repeat([]byte("x"), directoryInputBytes+1)}},
|
|
{{name, []byte("clean")}, {name, []byte("clean")}},
|
|
} {
|
|
if _, e := scanSmall(context.Background(), f, config, storage, inputs); e == nil {
|
|
t.Fatal("invalid batch input accepted")
|
|
}
|
|
if _, e := os.Lstat(filepath.Join(storage.path, "payload")); !os.IsNotExist(e) {
|
|
t.Fatal("invalid batch left input files")
|
|
}
|
|
}
|
|
})
|
|
t.Run("scanner-failure", func(t *testing.T) {
|
|
write(t, filepath.Join(config, "scanner.toml"), []byte("invalid = ["))
|
|
if _, e := scanSmall(context.Background(), f, config, storage, []scanInput{{strings.Repeat("d", 40), []byte("clean")}}); e == nil {
|
|
t.Fatal("scanner failure accepted")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBatchAndLargerInputPushBoundaries(t *testing.T) {
|
|
adapter, scanner := binary(t)
|
|
for _, tc := range []struct {
|
|
name string
|
|
size int
|
|
secret bool
|
|
}{
|
|
{"exact-limit-key", directoryInputBytes, true},
|
|
{"larger-stdin-key", directoryInputBytes + 1, true},
|
|
{"larger-stdin-clean", 70 * 1024, false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
f := setup(t, adapter, scanner)
|
|
content := []byte("reviewed clean input\n")
|
|
want := 0
|
|
if tc.secret {
|
|
content = key(t)
|
|
want = 1
|
|
}
|
|
content = append(content, bytes.Repeat([]byte("\n"), tc.size-len(content))...)
|
|
f.commit(t, "input.bin", content, "disposable boundary fixture")
|
|
f.push(t, want, "HEAD:refs/heads/main")
|
|
})
|
|
}
|
|
t.Run("multiple-ref-name-detection", func(t *testing.T) {
|
|
f := setup(t, adapter, scanner)
|
|
var random [32]byte
|
|
if _, err := rand.Read(random[:]); err != nil {
|
|
t.Fatal("fixture generation failed")
|
|
}
|
|
out := f.push(t, 1, "HEAD:refs/heads/clean-new", "HEAD:refs/heads/glpat-"+hex.EncodeToString(random[:]))
|
|
if !bytes.Contains(out, []byte(`"reason":"ref-name-detection"`)) {
|
|
t.Fatal("multi-ref name was not scanned")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBatchRefusesUnverifiedReportsAndDiagnostics(t *testing.T) {
|
|
known := map[string]string{"payload/ref-0": "ref-0"}
|
|
for _, tc := range []struct {
|
|
code int
|
|
raw string
|
|
}{
|
|
{0, "payload/ref-0\n"}, {20, ""}, {0, "\n"},
|
|
{20, "payload/ref-0"}, {20, "payload/ref-0\n\n"},
|
|
{20, "payload/ref-unknown\n"}, {20, "../payload/ref-0\n"},
|
|
{20, "payload/ref-0\r\n"}, {2, ""},
|
|
} {
|
|
if _, err := batchFindings(tc.code, []byte(tc.raw), known); err == nil {
|
|
t.Fatal("unverified report accepted")
|
|
}
|
|
}
|
|
for _, tc := range []struct {
|
|
code int
|
|
raw string
|
|
}{
|
|
{0, ""}, {20, "payload/ref-0\n"}, {20, "payload/ref-0\npayload/ref-0\n"},
|
|
} {
|
|
if _, err := batchFindings(tc.code, []byte(tc.raw), known); err != nil {
|
|
t.Fatal("valid report refused")
|
|
}
|
|
}
|
|
for _, tc := range []struct {
|
|
code int
|
|
text string
|
|
}{
|
|
{0, "12:03PM WRN incomplete scan\n"}, {20, "12:03PM WRN incomplete scan\n"},
|
|
{20, "12:03PM WRN leaks found: 1\n12:03PM WRN incomplete scan\n"},
|
|
{0, "12:03PM WRN leaks found: 1\n"}, {2, ""},
|
|
} {
|
|
if validateBatchDiagnostics(tc.code, []byte(tc.text)) == nil {
|
|
t.Fatal("unverified diagnostic accepted")
|
|
}
|
|
}
|
|
for _, tc := range []struct {
|
|
code int
|
|
text string
|
|
}{
|
|
{0, ""}, {20, ""}, {20, "12:03PM WRN leaks found: 1\n"},
|
|
} {
|
|
if validateBatchDiagnostics(tc.code, []byte(tc.text)) != nil {
|
|
t.Fatal("valid diagnostic refused")
|
|
}
|
|
}
|
|
}
|