fetch_ml/cmd/audit-verifier/main.go
Jeremie Fraeys 58c1a5fa58
feat(audit): Tamper-evident audit chain verification system
Add ChainVerifier for cryptographic audit log verification:
- VerifyLogFile(): Validates entire audit chain integrity
- Detects tampering at specific event index (FirstTampered)
- Returns chain root hash for external verification
- GetChainRootHash(): Standalone hash computation
- VerifyAndAlert(): Boolean tampering detection with logging

Add audit-verifier CLI tool:
- Standalone binary for audit chain verification
- Takes log path argument and reports tampering

Update audit logger for chain integrity:
- Each event includes sequence number and hash chain
- SHA-256 linking: hash_n = SHA-256(prev_hash || event_n)
- Tamper detection through hash chain validation

Add comprehensive test coverage:
- Empty log handling
- Valid chain verification
- Tampering detection with modification
- Root hash consistency
- Alert mechanism tests

Part of: V.7 audit verification from security plan
2026-02-23 19:43:50 -05:00

95 lines
2.5 KiB
Go

// Package main implements the audit-verifier standalone verification tool
package main
import (
"flag"
"fmt"
"log/slog"
"os"
"time"
"github.com/jfraeys/fetch_ml/internal/audit"
"github.com/jfraeys/fetch_ml/internal/logging"
)
func main() {
var (
logPath string
interval time.Duration
continuous bool
verbose bool
)
flag.StringVar(&logPath, "log-path", "", "Path to audit log file to verify (required)")
flag.DurationVar(&interval, "interval", 15*time.Minute, "Verification interval for continuous mode")
flag.BoolVar(&continuous, "continuous", false, "Run continuous verification in a loop")
flag.BoolVar(&verbose, "verbose", false, "Enable verbose output")
flag.Parse()
if logPath == "" {
fmt.Fprintln(os.Stderr, "Error: -log-path is required")
flag.Usage()
os.Exit(1)
}
// Setup logging
logLevel := slog.LevelInfo
if verbose {
logLevel = slog.LevelDebug
}
logger := logging.NewLogger(logLevel, false)
verifier := audit.NewChainVerifier(logger)
if continuous {
fmt.Printf("Starting continuous audit verification every %v...\n", interval)
fmt.Printf("Press Ctrl+C to stop\n\n")
// Run with alert function that prints to stdout
verifier.ContinuousVerification(logPath, interval, func(result *audit.VerificationResult) {
printResult(result)
if !result.Valid {
// In continuous mode, we don't exit on tampering - we keep monitoring
// The alert function should notify appropriate channels (email, slack, etc.)
fmt.Println("\n*** TAMPERING DETECTED - INVESTIGATE IMMEDIATELY ***")
}
})
} else {
// Single verification run
fmt.Printf("Verifying audit log: %s\n", logPath)
result, err := verifier.VerifyLogFile(logPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Verification failed: %v\n", err)
os.Exit(1)
}
printResult(result)
if !result.Valid {
fmt.Println("\n*** VERIFICATION FAILED - AUDIT CHAIN TAMPERING DETECTED ***")
os.Exit(2)
}
fmt.Println("\n✓ Audit chain integrity verified")
}
}
func printResult(result *audit.VerificationResult) {
fmt.Printf("\nVerification Time: %s\n", result.Timestamp.Format(time.RFC3339))
fmt.Printf("Total Events: %d\n", result.TotalEvents)
fmt.Printf("Valid: %v\n", result.Valid)
if result.ChainRootHash != "" {
fmt.Printf("Chain Root Hash: %s...\n", result.ChainRootHash[:16])
}
if !result.Valid {
if result.FirstTampered != -1 {
fmt.Printf("First Tampered Event: %d\n", result.FirstTampered)
}
if result.Error != "" {
fmt.Printf("Error: %s\n", result.Error)
}
}
}