fetch_ml/tests/unit/audit/verifier_test.go
Jeremie Fraeys d87c556afa
test(all): update test suite for scheduler and security features
Update comprehensive test coverage:
- E2E tests with scheduler integration
- Integration tests with tenant isolation
- Unit tests with security assertions
- Security tests with audit validation
- Audit verification tests
- Auth tests with tenant scoping
- Config validation tests
- Container security tests
- Worker tests with scheduler mock
- Environment pool tests
- Load tests with distributed patterns
- Test fixtures with scheduler support
- Update go.mod/go.sum with new dependencies
2026-02-26 12:08:46 -05:00

239 lines
6.2 KiB
Go

package audit_test
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/jfraeys/fetch_ml/internal/audit"
"github.com/jfraeys/fetch_ml/internal/logging"
)
// replaceAllSubstr replaces all occurrences of old with new in s
func replaceAllSubstr(s, old, new string) string {
return strings.ReplaceAll(s, old, new)
}
func TestChainVerifier_VerifyLogFile_EmptyLog(t *testing.T) {
logger := logging.NewLogger(0, false)
verifier := audit.NewChainVerifier(logger)
// Test with non-existent file (empty log case)
result, err := verifier.VerifyLogFile("/nonexistent/path/audit.log")
if err != nil {
t.Fatalf("expected no error for non-existent file, got: %v", err)
}
if !result.Valid {
t.Error("expected valid result for empty log")
}
if result.TotalEvents != 0 {
t.Errorf("expected 0 events, got %d", result.TotalEvents)
}
}
func TestChainVerifier_VerifyLogFile_ValidChain(t *testing.T) {
logger := logging.NewLogger(0, false)
verifier := audit.NewChainVerifier(logger)
// Create a temporary log file with valid chain
tempDir := t.TempDir()
// Create logger that writes to file (using tempDir as base)
al, err := audit.NewLoggerWithBase(true, "audit.log", logger, tempDir)
if err != nil {
t.Fatalf("failed to create audit logger: %v", err)
}
defer al.Close()
// Log some events
testTime := time.Date(2026, 2, 23, 12, 0, 0, 0, time.UTC)
al.Log(audit.Event{
Timestamp: testTime,
EventType: audit.EventJobStarted,
UserID: "user1",
Success: true,
})
al.Log(audit.Event{
Timestamp: testTime.Add(time.Second),
EventType: audit.EventJobCompleted,
UserID: "user1",
Success: true,
})
al.Log(audit.Event{
Timestamp: testTime.Add(2 * time.Second),
EventType: audit.EventFileRead,
UserID: "user1",
Success: true,
})
// Verify the chain (pass the actual file path)
result, err := verifier.VerifyLogFile(filepath.Join(tempDir, "audit.log"))
if err != nil {
t.Fatalf("verification failed: %v", err)
}
if !result.Valid {
t.Errorf("expected valid chain, got error: %s", result.Error)
}
if result.TotalEvents != 3 {
t.Errorf("expected 3 events, got %d", result.TotalEvents)
}
if result.ChainRootHash == "" {
t.Error("expected non-empty chain root hash")
}
}
func TestChainVerifier_VerifyLogFile_TamperedChain(t *testing.T) {
logger := logging.NewLogger(0, false)
verifier := audit.NewChainVerifier(logger)
// Create a temporary log file
tempDir := t.TempDir()
// Create logger and log events (using tempDir as base)
al, err := audit.NewLoggerWithBase(true, "audit.log", logger, tempDir)
if err != nil {
t.Fatalf("failed to create audit logger: %v", err)
}
testTime := time.Date(2026, 2, 23, 12, 0, 0, 0, time.UTC)
al.Log(audit.Event{
Timestamp: testTime,
EventType: audit.EventJobStarted,
UserID: "user1",
Success: true,
})
al.Log(audit.Event{
Timestamp: testTime.Add(time.Second),
EventType: audit.EventJobCompleted,
UserID: "user1",
Success: true,
})
al.Close()
// Tamper with the file by modifying the second event
data, err := os.ReadFile(filepath.Join(tempDir, "audit.log"))
if err != nil {
t.Fatalf("failed to read log file: %v", err)
}
// Replace "user1" with "attacker" in the content
content := string(data)
// Simple string replacement
tamperedContent := replaceAllSubstr(content, "user1", "attacker")
// Write back tampered data
if err := os.WriteFile(filepath.Join(tempDir, "audit.log"), []byte(tamperedContent), 0600); err != nil {
t.Fatalf("failed to write tampered log: %v", err)
}
// Verify should detect tampering
result, err := verifier.VerifyLogFile(filepath.Join(tempDir, "audit.log"))
if err == nil && result.Valid {
t.Error("expected tampering to be detected")
}
if result.FirstTampered == -1 {
t.Error("expected FirstTampered to be set")
}
}
func TestChainVerifier_GetChainRootHash(t *testing.T) {
logger := logging.NewLogger(0, false)
verifier := audit.NewChainVerifier(logger)
tempDir := t.TempDir()
// Create logger and log events (using tempDir as base)
al, err := audit.NewLoggerWithBase(true, "audit.log", logger, tempDir)
if err != nil {
t.Fatalf("failed to create audit logger: %v", err)
}
testTime := time.Date(2026, 2, 23, 12, 0, 0, 0, time.UTC)
al.Log(audit.Event{
Timestamp: testTime,
EventType: audit.EventJobStarted,
UserID: "user1",
Success: true,
})
al.Close()
// Get chain root hash
rootHash, err := verifier.GetChainRootHash(filepath.Join(tempDir, "audit.log"))
if err != nil {
t.Fatalf("failed to get chain root hash: %v", err)
}
if rootHash == "" {
t.Error("expected non-empty root hash")
}
// Verify it matches the result from VerifyLogFile
result, err := verifier.VerifyLogFile(filepath.Join(tempDir, "audit.log"))
if err != nil {
t.Fatalf("verification failed: %v", err)
}
if rootHash != result.ChainRootHash {
t.Errorf("root hash mismatch: GetChainRootHash=%s, VerifyLogFile=%s", rootHash, result.ChainRootHash)
}
}
func TestChainVerifier_VerifyAndAlert(t *testing.T) {
logger := logging.NewLogger(0, false)
verifier := audit.NewChainVerifier(logger)
tempDir := t.TempDir()
// Create valid log (using tempDir as base)
al, err := audit.NewLoggerWithBase(true, "audit.log", logger, tempDir)
if err != nil {
t.Fatalf("failed to create audit logger: %v", err)
}
al.Log(audit.Event{
Timestamp: time.Now(),
EventType: audit.EventJobStarted,
UserID: "user1",
Success: true,
})
al.Close()
// VerifyAndAlert should return false (no tampering)
tampered, err := verifier.VerifyAndAlert(filepath.Join(tempDir, "audit.log"))
if err != nil {
t.Fatalf("verification failed: %v", err)
}
if tampered {
t.Error("expected no tampering detected for valid log")
}
// Test with non-existent file
tampered, err = verifier.VerifyAndAlert("/nonexistent/path/audit.log")
if err != nil {
t.Fatalf("expected no error for non-existent file: %v", err)
}
if tampered {
t.Error("expected no tampering for empty log")
}
}
// splitLines splits byte slice by newlines
func splitLines(data []byte) [][]byte {
var lines [][]byte
start := 0
for i := 0; i < len(data); i++ {
if data[i] == '\n' {
lines = append(lines, data[start:i])
start = i + 1
}
}
if start < len(data) {
lines = append(lines, data[start:])
}
return lines
}