package audit import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "os" "sync" "time" "github.com/jfraeys/fetch_ml/internal/logging" ) // EventType represents the type of audit event type EventType string const ( EventAuthAttempt EventType = "authentication_attempt" EventAuthSuccess EventType = "authentication_success" EventAuthFailure EventType = "authentication_failure" EventJobQueued EventType = "job_queued" EventJobStarted EventType = "job_started" EventJobCompleted EventType = "job_completed" EventJobFailed EventType = "job_failed" EventJupyterStart EventType = "jupyter_start" EventJupyterStop EventType = "jupyter_stop" EventExperimentCreated EventType = "experiment_created" EventExperimentDeleted EventType = "experiment_deleted" // HIPAA-specific file access events EventFileRead EventType = "file_read" EventFileWrite EventType = "file_write" EventFileDelete EventType = "file_delete" EventDatasetAccess EventType = "dataset_access" ) // Event represents an audit log event with integrity chain type Event struct { Timestamp time.Time `json:"timestamp"` EventType EventType `json:"event_type"` UserID string `json:"user_id,omitempty"` IPAddress string `json:"ip_address,omitempty"` Resource string `json:"resource,omitempty"` // File path, dataset ID, etc. Action string `json:"action,omitempty"` // read, write, delete Success bool `json:"success"` ErrorMsg string `json:"error,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` // Integrity chain fields for tamper-evident logging (HIPAA requirement) PrevHash string `json:"prev_hash,omitempty"` // SHA-256 of previous event EventHash string `json:"event_hash,omitempty"` // SHA-256 of this event SequenceNum int64 `json:"sequence_num,omitempty"` } // Logger handles audit logging with integrity chain type Logger struct { enabled bool filePath string file *os.File mu sync.Mutex logger *logging.Logger lastHash string sequenceNum int64 } // NewLogger creates a new audit logger func NewLogger(enabled bool, filePath string, logger *logging.Logger) (*Logger, error) { al := &Logger{ enabled: enabled, filePath: filePath, logger: logger, } if enabled && filePath != "" { file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return nil, fmt.Errorf("failed to open audit log file: %w", err) } al.file = file } return al, nil } // Log logs an audit event with integrity chain func (al *Logger) Log(event Event) { if !al.enabled { return } event.Timestamp = time.Now().UTC() al.mu.Lock() defer al.mu.Unlock() // Set sequence number and previous hash for integrity chain al.sequenceNum++ event.SequenceNum = al.sequenceNum event.PrevHash = al.lastHash // Calculate hash of this event for tamper evidence event.EventHash = al.CalculateEventHash(event) al.lastHash = event.EventHash // Marshal to JSON data, err := json.Marshal(event) if err != nil { if al.logger != nil { al.logger.Error("failed to marshal audit event", "error", err) } return } // Write to file if configured if al.file != nil { _, err = al.file.Write(append(data, '\n')) if err != nil && al.logger != nil { al.logger.Error("failed to write audit event", "error", err) } } // Also log via structured logger if al.logger != nil { al.logger.Info("audit_event", "event_type", event.EventType, "user_id", event.UserID, "resource", event.Resource, "success", event.Success, "seq", event.SequenceNum, "hash", event.EventHash[:16], // Log first 16 chars of hash ) } } // CalculateEventHash computes SHA-256 hash of event data for integrity chain // Exported for testing purposes func (al *Logger) CalculateEventHash(event Event) string { // Create a copy without the hash field for hashing eventCopy := event eventCopy.EventHash = "" eventCopy.PrevHash = "" data, err := json.Marshal(eventCopy) if err != nil { // Fallback: hash the timestamp and type data = []byte(fmt.Sprintf("%s:%s:%d", event.Timestamp, event.EventType, event.SequenceNum)) } hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]) } // LogFileAccess logs a file access operation (HIPAA requirement) func (al *Logger) LogFileAccess( eventType EventType, userID, filePath, ipAddr string, success bool, errMsg string, ) { action := "read" switch eventType { case EventFileWrite: action = "write" case EventFileDelete: action = "delete" } al.Log(Event{ EventType: eventType, UserID: userID, IPAddress: ipAddr, Resource: filePath, Action: action, Success: success, ErrorMsg: errMsg, }) } // VerifyChain checks the integrity of the audit log chain // Returns the first sequence number where tampering is detected, or -1 if valid func (al *Logger) VerifyChain(events []Event) (tamperedSeq int, err error) { if len(events) == 0 { return -1, nil } var expectedPrevHash string for _, event := range events { // Verify previous hash chain if event.SequenceNum > 1 && event.PrevHash != expectedPrevHash { return int(event.SequenceNum), fmt.Errorf( "chain break at sequence %d: expected prev_hash=%s, got %s", event.SequenceNum, expectedPrevHash, event.PrevHash, ) } // Verify event hash expectedHash := al.CalculateEventHash(event) if event.EventHash != expectedHash { return int(event.SequenceNum), fmt.Errorf( "hash mismatch at sequence %d: expected %s, got %s", event.SequenceNum, expectedHash, event.EventHash, ) } expectedPrevHash = event.EventHash } return -1, nil } // LogAuthAttempt logs an authentication attempt func (al *Logger) LogAuthAttempt(userID, ipAddr string, success bool, errMsg string) { eventType := EventAuthSuccess if !success { eventType = EventAuthFailure } al.Log(Event{ EventType: eventType, UserID: userID, IPAddress: ipAddr, Success: success, ErrorMsg: errMsg, }) } // LogJobOperation logs a job-related operation func (al *Logger) LogJobOperation( eventType EventType, userID, jobID, ipAddr string, success bool, errMsg string, ) { al.Log(Event{ EventType: eventType, UserID: userID, IPAddress: ipAddr, Resource: jobID, Action: "job_operation", Success: success, ErrorMsg: errMsg, }) } // LogJupyterOperation logs a Jupyter service operation func (al *Logger) LogJupyterOperation( eventType EventType, userID, serviceID, ipAddr string, success bool, errMsg string, ) { al.Log(Event{ EventType: eventType, UserID: userID, IPAddress: ipAddr, Resource: serviceID, Action: "jupyter_operation", Success: success, ErrorMsg: errMsg, }) } // Close closes the audit logger func (al *Logger) Close() error { al.mu.Lock() defer al.mu.Unlock() if al.file != nil { return al.file.Close() } return nil }