package audit import ( "bufio" "encoding/json" "fmt" "os" "time" "github.com/jfraeys/fetch_ml/internal/logging" ) // ChainVerifier provides continuous verification of audit log integrity // by checking the chained hash structure and detecting any tampering. type ChainVerifier struct { logger *logging.Logger } // NewChainVerifier creates a new audit chain verifier func NewChainVerifier(logger *logging.Logger) *ChainVerifier { return &ChainVerifier{ logger: logger, } } // VerificationResult contains the outcome of a chain verification //type VerificationResult struct { // Timestamp time.Time // TotalEvents int // Valid bool // FirstTampered int64 // Sequence number of first tampered event, -1 if none // Error string // Error message if verification failed // ChainRootHash string // Hash of the last valid event (for external verification) //} // VerificationResult contains the outcome of a chain verification type VerificationResult struct { Timestamp time.Time TotalEvents int Valid bool FirstTampered int64 // Sequence number of first tampered event, -1 if none Error string // Error message if verification failed ChainRootHash string // Hash of the last valid event (for external verification) } // VerifyLogFile performs a complete verification of an audit log file. // It checks the integrity chain by verifying each event's hash and // ensuring the previous hash links are unbroken. func (cv *ChainVerifier) VerifyLogFile(logPath string) (*VerificationResult, error) { result := &VerificationResult{ Timestamp: time.Now().UTC(), TotalEvents: 0, Valid: true, FirstTampered: -1, } // Open the log file file, err := os.Open(logPath) if err != nil { if os.IsNotExist(err) { // No log file yet - this is valid (no entries to verify) return result, nil } result.Valid = false result.Error = fmt.Sprintf("failed to open log file: %v", err) return result, err } defer file.Close() // Create a temporary logger to calculate hashes tempLogger, _ := NewLogger(false, "", cv.logger) var events []Event scanner := bufio.NewScanner(file) lineNum := 0 for scanner.Scan() { lineNum++ line := scanner.Text() if line == "" { continue } var event Event if err := json.Unmarshal([]byte(line), &event); err != nil { result.Valid = false result.Error = fmt.Sprintf("failed to parse event at line %d: %v", lineNum, err) return result, fmt.Errorf("parse error at line %d: %w", lineNum, err) } events = append(events, event) result.TotalEvents++ } if err := scanner.Err(); err != nil { result.Valid = false result.Error = fmt.Sprintf("error reading log file: %v", err) return result, err } // Verify the chain tamperedSeq, err := tempLogger.VerifyChain(events) if err != nil { result.Valid = false result.FirstTampered = int64(tamperedSeq) result.Error = err.Error() return result, err } if tamperedSeq != -1 { result.Valid = false result.FirstTampered = int64(tamperedSeq) result.Error = fmt.Sprintf("tampering detected at sequence %d", tamperedSeq) } // Set the chain root hash (hash of the last event) if len(events) > 0 { lastEvent := events[len(events)-1] result.ChainRootHash = lastEvent.EventHash } return result, nil } // ContinuousVerification runs verification at regular intervals and reports any issues. // This should be run as a background goroutine in long-running services. func (cv *ChainVerifier) ContinuousVerification(logPath string, interval time.Duration, alertFunc func(*VerificationResult)) { if interval <= 0 { interval = 15 * time.Minute // Default: 15 minutes for HIPAA, use 1 hour otherwise } ticker := time.NewTicker(interval) defer ticker.Stop() // Run initial verification cv.runAndReport(logPath, alertFunc) for range ticker.C { cv.runAndReport(logPath, alertFunc) } } // runAndReport performs verification and calls the alert function if issues are found func (cv *ChainVerifier) runAndReport(logPath string, alertFunc func(*VerificationResult)) { result, err := cv.VerifyLogFile(logPath) if err != nil { if cv.logger != nil { cv.logger.Error("audit chain verification error", "error", err, "log_path", logPath) } // Still report the error if alertFunc != nil { alertFunc(result) } return } // Report if not valid or if we just want to log successful verification periodically if !result.Valid { if cv.logger != nil { cv.logger.Error("audit chain tampering detected", "first_tampered", result.FirstTampered, "total_events", result.TotalEvents, "chain_root", result.ChainRootHash[:16]) } if alertFunc != nil { alertFunc(result) } } else { if cv.logger != nil { cv.logger.Debug("audit chain verification passed", "total_events", result.TotalEvents, "chain_root", result.ChainRootHash[:16]) } } } // VerifyAndAlert performs a single verification and returns true if tampering detected func (cv *ChainVerifier) VerifyAndAlert(logPath string) (bool, error) { result, err := cv.VerifyLogFile(logPath) if err != nil { return true, err // Treat errors as potential tampering } return !result.Valid, nil } // GetChainRootHash returns the hash of the last event in the chain // This can be published to an external append-only store for independent verification func (cv *ChainVerifier) GetChainRootHash(logPath string) (string, error) { file, err := os.Open(logPath) if err != nil { return "", err } defer file.Close() var lastLine string scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if line != "" { lastLine = line } } if err := scanner.Err(); err != nil { return "", err } if lastLine == "" { return "", fmt.Errorf("no events in log file") } var event Event if err := json.Unmarshal([]byte(lastLine), &event); err != nil { return "", fmt.Errorf("failed to parse last event: %w", err) } return event.EventHash, nil }