423 lines
14 KiB
Go
423 lines
14 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageChainErrorAndLargeRecordCoverage(t *testing.T) {
|
|
dir := t.TempDir()
|
|
closed, err := os.Create(filepath.Join(dir, "closed"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := closed.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := NewChain(dir).SealFromFile(closed, []byte("{}")); err == nil {
|
|
t.Fatal("SealFromFile on a closed file succeeded")
|
|
}
|
|
|
|
newFile := func(name, content string) *os.File {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file, err := os.OpenFile(path, os.O_RDWR, 0o600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = file.Close() })
|
|
return file
|
|
}
|
|
if hash, err := lastRecordHash(newFile("newlines", "\n\r\n")); err != nil || hash != "" {
|
|
t.Fatalf("lastRecordHash(newlines) = %q, %v", hash, err)
|
|
}
|
|
if _, err := lastRecordHash(newFile("invalid", "not-json\n")); err == nil {
|
|
t.Fatal("lastRecordHash(invalid) error = nil")
|
|
}
|
|
large := `{"padding":"` + strings.Repeat("x", 70*1024) + `","hash":"large-hash"}` + "\n"
|
|
if hash, err := lastRecordHash(newFile("large", large)); err != nil || hash != "large-hash" {
|
|
t.Fatalf("lastRecordHash(large) = %q, %v", hash, err)
|
|
}
|
|
if hash, err := lastRecordHashFullScan(newFile("scan-empty", "\n\n")); err != nil || hash != "" {
|
|
t.Fatalf("lastRecordHashFullScan(empty) = %q, %v", hash, err)
|
|
}
|
|
if _, err := lastRecordHashFullScan(newFile("scan-invalid", "not-json\n")); err == nil {
|
|
t.Fatal("lastRecordHashFullScan(invalid) error = nil")
|
|
}
|
|
tooLarge := strings.Repeat("x", 9*1024*1024)
|
|
if _, err := lastRecordHashFullScan(newFile("scan-too-large", tooLarge)); err == nil {
|
|
t.Fatal("lastRecordHashFullScan(oversize) error = nil")
|
|
}
|
|
|
|
if _, _, err := VerifyFile(filepath.Join(dir, "missing")); err == nil {
|
|
t.Fatal("VerifyFile(missing) error = nil")
|
|
}
|
|
if valid, line, err := VerifyFile(filepath.Join(dir, "invalid")); err == nil || valid || line != 1 {
|
|
t.Fatalf("VerifyFile(invalid) = %v, %d, %v", valid, line, err)
|
|
}
|
|
prevMismatch := filepath.Join(dir, "prev-mismatch")
|
|
if err := os.WriteFile(prevMismatch, []byte(`{"prev_hash":"wrong","hash":"wrong"}`+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if valid, line, err := VerifyFile(prevMismatch); err == nil || valid || line != 1 {
|
|
t.Fatalf("VerifyFile(prev mismatch) = %v, %d, %v", valid, line, err)
|
|
}
|
|
oversize := filepath.Join(dir, "verify-too-large")
|
|
if err := os.WriteFile(oversize, []byte(tooLarge), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if valid, _, err := VerifyFile(oversize); err == nil || valid {
|
|
t.Fatalf("VerifyFile(oversize) = %v, %v", valid, err)
|
|
}
|
|
invalidBody := []byte("not-json")
|
|
if got := stripHashFields(invalidBody); string(got) != string(invalidBody) {
|
|
t.Fatalf("stripHashFields(invalid) = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCollectionAndRotationCoverage(t *testing.T) {
|
|
for _, value := range []string{"1", "true", "on", "yes", "y"} {
|
|
t.Setenv(EnvAuditDebug, value)
|
|
if !DebugEnabled() {
|
|
t.Fatalf("DebugEnabled(%q) = false", value)
|
|
}
|
|
}
|
|
t.Setenv(EnvAuditDebug, "off")
|
|
if DebugEnabled() {
|
|
t.Fatal("DebugEnabled(off) = true")
|
|
}
|
|
for _, value := range []string{"0", "false", "off", "no", "n"} {
|
|
t.Setenv(EnvAudit, value)
|
|
if IsEnabled() {
|
|
t.Fatalf("IsEnabled(%q) = true", value)
|
|
}
|
|
}
|
|
t.Setenv(EnvAudit, "unexpected")
|
|
if !IsEnabled() {
|
|
t.Fatal("IsEnabled(unexpected) = false")
|
|
}
|
|
t.Setenv(EnvAudit, "0")
|
|
sink, err := BuildSink(t.TempDir(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := sink.(NopSink); !ok {
|
|
t.Fatalf("disabled BuildSink() = %T", sink)
|
|
}
|
|
|
|
t.Setenv(EnvAudit, "1")
|
|
t.Setenv(EnvRetentionDays, "invalid")
|
|
t.Setenv(EnvForwardURL, "http://127.0.0.1:1")
|
|
t.Setenv(EnvForwardToken, "secret")
|
|
t.Setenv(EnvForwardRedact, "unexpected")
|
|
sink, err = BuildSink(t.TempDir(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sink.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(EnvRetentionDays, "0")
|
|
t.Setenv(EnvForwardRedact, string(RedactHashed))
|
|
sink, err = BuildSink(t.TempDir(), func(string, ...any) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sink.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
badConfig := filepath.Join(t.TempDir(), "file")
|
|
if err := os.WriteFile(badConfig, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(EnvAuditDir, filepath.Join(badConfig, "child"))
|
|
if _, err := BuildSink(badConfig, nil); err == nil {
|
|
t.Fatal("BuildSink(unwritable path) error = nil")
|
|
}
|
|
|
|
if _, err := NewDateRotatingWriter(filepath.Join(badConfig, "child"), 1); err == nil {
|
|
t.Fatal("NewDateRotatingWriter(invalid dir) error = nil")
|
|
}
|
|
lockDir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(lockDir, auditLockFile), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := NewDateRotatingWriter(lockDir, 1); err == nil {
|
|
t.Fatal("NewDateRotatingWriter(lock directory) error = nil")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
w, err := NewDateRotatingWriter(dir, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w.Dir() != dir {
|
|
t.Fatalf("Dir() = %q", w.Dir())
|
|
}
|
|
w.pruneOldFiles()
|
|
file, release, err := w.beginAppend()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
release()
|
|
w.curDate = "old"
|
|
if _, release, err = w.beginAppend(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
release()
|
|
if file == nil {
|
|
t.Fatal("beginAppend returned nil file")
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
openFailureDir := t.TempDir()
|
|
openFailureWriter, err := NewDateRotatingWriter(openFailureDir, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(openFailureDir, "audit-"+time.Now().Format("20060102")+".jsonl"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := openFailureWriter.beginAppend(); err == nil {
|
|
t.Fatal("beginAppend(open failure) error = nil")
|
|
}
|
|
if err := openFailureWriter.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fileErrorWriter, err := NewDateRotatingWriter(t.TempDir(), 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fileErrorWriter.file, err = os.CreateTemp(t.TempDir(), "closed")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fileErrorWriter.file.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := fileErrorWriter.Close(); err == nil {
|
|
t.Fatal("Close(closed data file) error = nil")
|
|
}
|
|
lockErrorWriter, err := NewDateRotatingWriter(t.TempDir(), 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := lockErrorWriter.lock.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := lockErrorWriter.Close(); err == nil {
|
|
t.Fatal("Close(closed lock file) error = nil")
|
|
}
|
|
sinkCloseWriter, err := NewDateRotatingWriter(t.TempDir(), 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sinkCloseWriter.lock.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := NewFileSink(sinkCloseWriter, NewChain(""), nil).Close(); err == nil {
|
|
t.Fatal("FileSink.Close(closed writer) error = nil")
|
|
}
|
|
(&DateRotatingWriter{dir: filepath.Join(t.TempDir(), "missing"), retention: 1}).pruneOldFiles()
|
|
pruneDir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(pruneDir, "audit-bad.jsonl"), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
(&DateRotatingWriter{dir: pruneDir, retention: 1}).pruneOldFiles()
|
|
|
|
if _, err := LatestAuditFile(filepath.Join(dir, "missing")); err == nil {
|
|
t.Fatal("LatestAuditFile(missing) error = nil")
|
|
}
|
|
empty := t.TempDir()
|
|
if _, err := LatestAuditFile(empty); err == nil {
|
|
t.Fatal("LatestAuditFile(empty) error = nil")
|
|
}
|
|
if _, err := AuditFilesInRange(filepath.Join(empty, "missing"), "", ""); err == nil {
|
|
t.Fatal("AuditFilesInRange(missing) error = nil")
|
|
}
|
|
for name := range map[string]bool{
|
|
"audit-20260101.jsonl": true,
|
|
"audit-20260102.jsonl": true,
|
|
"audit-bad.jsonl": true,
|
|
"other": true,
|
|
} {
|
|
if err := os.WriteFile(filepath.Join(empty, name), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
files, err := AuditFilesInRange(empty, "20260102", "20260103")
|
|
if err != nil || len(files) != 1 || !strings.HasSuffix(files[0], "20260102.jsonl") {
|
|
t.Fatalf("AuditFilesInRange() = %v, %v", files, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSinkAndForwarderFailureCoverage(t *testing.T) {
|
|
originalMarshal, originalWrite := sinkMarshal, sinkWrite
|
|
originalForwardMarshal, originalRedact := forwardMarshal, forwardRedactEventJSON
|
|
originalReadAt, originalSeek, originalChainMarshal := chainReadAt, chainSeek, chainMarshal
|
|
originalLock, originalLockTimeout := rotateLockFile, rotateLockTimeout
|
|
t.Cleanup(func() {
|
|
sinkMarshal, sinkWrite = originalMarshal, originalWrite
|
|
forwardMarshal, forwardRedactEventJSON = originalForwardMarshal, originalRedact
|
|
chainReadAt, chainSeek, chainMarshal = originalReadAt, originalSeek, originalChainMarshal
|
|
rotateLockFile, rotateLockTimeout = originalLock, originalLockTimeout
|
|
})
|
|
chainReadAt = func(*os.File, []byte, int64) (int, error) { return 0, errors.New("read") }
|
|
readFailure, err := os.CreateTemp(t.TempDir(), "read-failure")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = readFailure.Close() })
|
|
if _, err := readFailure.WriteString("x"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := lastRecordHash(readFailure); err == nil {
|
|
t.Fatal("lastRecordHash(read failure) error = nil")
|
|
}
|
|
chainReadAt = originalReadAt
|
|
chainSeek = func(*os.File, int64, int) (int64, error) { return 0, errors.New("seek") }
|
|
if _, err := lastRecordHashFullScan(readFailure); err == nil {
|
|
t.Fatal("lastRecordHashFullScan(seek failure) error = nil")
|
|
}
|
|
chainSeek = originalSeek
|
|
chainMarshal = func(any) ([]byte, error) { return nil, errors.New("marshal") }
|
|
validEvent, err := originalMarshal(Event{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := stripHashFields(validEvent); string(got) != string(validEvent) {
|
|
t.Fatalf("stripHashFields(marshal failure) = %q", got)
|
|
}
|
|
chainMarshal = originalChainMarshal
|
|
|
|
writer, err := NewDateRotatingWriter(t.TempDir(), 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sink := NewFileSink(writer, NewChain(""), nil)
|
|
sinkMarshal = func(any) ([]byte, error) { return nil, errors.New("marshal") }
|
|
if err := sink.Emit(&Event{}); err == nil || !strings.Contains(err.Error(), "marshal event") {
|
|
t.Fatalf("Emit(first marshal) error = %v", err)
|
|
}
|
|
calls := 0
|
|
sinkMarshal = func(value any) ([]byte, error) {
|
|
calls++
|
|
if calls == 2 {
|
|
return nil, errors.New("final marshal")
|
|
}
|
|
return originalMarshal(value)
|
|
}
|
|
if err := sink.Emit(&Event{}); err == nil || !strings.Contains(err.Error(), "marshal final event") {
|
|
t.Fatalf("Emit(final marshal) error = %v", err)
|
|
}
|
|
sinkMarshal = originalMarshal
|
|
sinkWrite = func(*os.File, []byte) (int, error) { return 0, errors.New("write") }
|
|
if err := sink.Emit(&Event{}); err == nil || !strings.Contains(err.Error(), "write event") {
|
|
t.Fatalf("Emit(write) error = %v", err)
|
|
}
|
|
sinkWrite = originalWrite
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rotateLockFile = func(*os.File) error { return errors.New("locked") }
|
|
rotateLockTimeout = 0
|
|
blockedWriter, err := NewDateRotatingWriter(t.TempDir(), 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
blockedSink := NewFileSink(blockedWriter, NewChain(""), nil)
|
|
if err := blockedSink.Emit(&Event{}); err == nil || !strings.Contains(err.Error(), "acquire writer") {
|
|
t.Fatalf("Emit(lock failure) error = %v", err)
|
|
}
|
|
rotateLockFile, rotateLockTimeout = originalLock, originalLockTimeout
|
|
if err := blockedWriter.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sealDir := t.TempDir()
|
|
sealWriter, err := NewDateRotatingWriter(sealDir, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
todayFile := filepath.Join(sealDir, "audit-"+time.Now().Format("20060102")+".jsonl")
|
|
if err := os.WriteFile(todayFile, []byte("invalid\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := NewFileSink(sealWriter, NewChain(""), nil).Emit(&Event{}); err == nil || !strings.Contains(err.Error(), "seal event") {
|
|
t.Fatalf("Emit(seal failure) error = %v", err)
|
|
}
|
|
if err := sealWriter.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
forwardServer := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
|
forwarding := NewHTTPForwarder(forwardServer.URL, "", RedactNone, nil)
|
|
forwardWriter, err := NewDateRotatingWriter(t.TempDir(), 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
forwardSink := NewFileSink(forwardWriter, NewChain(""), forwarding)
|
|
if err := forwardSink.Emit(&Event{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := forwardSink.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
forwardServer.Close()
|
|
|
|
var reports []string
|
|
report := func(format string, args ...any) { reports = append(reports, fmt.Sprintf(format, args...)) }
|
|
forwarder := NewHTTPForwarder(":", "", RedactNone, report)
|
|
forwarder.send(Event{})
|
|
forwardMarshal = func(any) ([]byte, error) { return nil, errors.New("marshal") }
|
|
forwarder.send(Event{})
|
|
forwardMarshal = originalForwardMarshal
|
|
forwardRedactEventJSON = func(Event, RedactLevel) ([]byte, error) { return nil, errors.New("redact") }
|
|
forwarder.redact = RedactHashed
|
|
forwarder.send(Event{})
|
|
forwardRedactEventJSON = originalRedact
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "Bearer token" {
|
|
t.Errorf("Authorization = %q", r.Header.Get("Authorization"))
|
|
}
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}))
|
|
defer server.Close()
|
|
forwarder = NewHTTPForwarder(server.URL, "token", RedactMinimal, report)
|
|
forwarder.send(Event{Actor: Actor{Name: "name"}})
|
|
forwarder = NewHTTPForwarder("http://127.0.0.1:1", "", RedactNone, report)
|
|
forwarder.timeout = 20 * time.Millisecond
|
|
forwarder.send(Event{})
|
|
if err := forwarder.Close(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
forwarder.wg.Add(1)
|
|
if err := forwarder.Close(ctx); err == nil {
|
|
t.Fatal("Close(cancelled) error = nil")
|
|
}
|
|
forwarder.wg.Done()
|
|
if len(reports) < 5 {
|
|
t.Fatalf("forward reports = %v", reports)
|
|
}
|
|
if _, err := RedactEventJSON(Event{Actor: Actor{Name: "name"}}, RedactHashed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|