Implement comprehensive audit and security infrastructure: - Immutable audit logs with platform-specific backends (Linux/Other) - Sealed log entries with tamper-evident checksums - Audit alert system for real-time security notifications - Log rotation with retention policies - Checkpoint-based audit verification Add multi-tenant security features: - Tenant manager with quota enforcement - Middleware for tenant authentication/authorization - Per-tenant cryptographic key isolation - Supply chain security for container verification - Cross-platform secure file utilities (Unix/Windows) Add test coverage: - Unit tests for audit alerts and sealed logs - Platform-specific audit backend tests
175 lines
4.4 KiB
Go
175 lines
4.4 KiB
Go
// Package audit provides tamper-evident audit logging with hash chaining
|
|
package audit
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/fileutil"
|
|
)
|
|
|
|
// StateEntry represents a sealed checkpoint entry
|
|
type StateEntry struct {
|
|
Seq uint64 `json:"seq"`
|
|
Hash string `json:"hash"`
|
|
Timestamp time.Time `json:"ts"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// SealedStateManager maintains tamper-evident state checkpoints.
|
|
// It writes to an append-only chain file and an overwritten current file.
|
|
// The chain file is fsynced before returning to ensure crash safety.
|
|
type SealedStateManager struct {
|
|
chainFile string
|
|
currentFile string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewSealedStateManager creates a new sealed state manager
|
|
func NewSealedStateManager(chainFile, currentFile string) *SealedStateManager {
|
|
return &SealedStateManager{
|
|
chainFile: chainFile,
|
|
currentFile: currentFile,
|
|
}
|
|
}
|
|
|
|
// Checkpoint writes current state to sealed files.
|
|
// It writes to the append-only chain file first, fsyncs it, then overwrites the current file.
|
|
// This ordering ensures crash safety: the chain file is always the source of truth.
|
|
func (ssm *SealedStateManager) Checkpoint(seq uint64, hash string) error {
|
|
ssm.mu.Lock()
|
|
defer ssm.mu.Unlock()
|
|
|
|
entry := StateEntry{
|
|
Seq: seq,
|
|
Hash: hash,
|
|
Timestamp: time.Now().UTC(),
|
|
Type: "fsync",
|
|
}
|
|
data, err := json.Marshal(entry)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal state entry: %w", err)
|
|
}
|
|
|
|
// Write to append-only chain file first
|
|
f, err := os.OpenFile(ssm.chainFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("open chain file: %w", err)
|
|
}
|
|
|
|
if _, err := f.Write(append(data, '\n')); err != nil {
|
|
f.Close()
|
|
return fmt.Errorf("write chain entry: %w", err)
|
|
}
|
|
|
|
// CRITICAL: fsync chain before returning — crash safety
|
|
if err := f.Sync(); err != nil {
|
|
f.Close()
|
|
return fmt.Errorf("sync sealed chain: %w", err)
|
|
}
|
|
|
|
if err := f.Close(); err != nil {
|
|
return fmt.Errorf("close chain file: %w", err)
|
|
}
|
|
|
|
// Overwrite current-state file (fast lookup) with crash safety (fsync)
|
|
if err := fileutil.WriteFileSafe(ssm.currentFile, data, 0o600); err != nil {
|
|
return fmt.Errorf("write current file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RecoverState reads last valid state from sealed files.
|
|
// It tries the current file first (fast path), then falls back to scanning the chain file.
|
|
func (ssm *SealedStateManager) RecoverState() (uint64, string, error) {
|
|
// Try current file first (fast path)
|
|
data, err := os.ReadFile(ssm.currentFile)
|
|
if err == nil {
|
|
var entry StateEntry
|
|
if json.Unmarshal(data, &entry) == nil {
|
|
return entry.Seq, entry.Hash, nil
|
|
}
|
|
}
|
|
|
|
// Fall back to scanning chain file for last valid entry
|
|
return ssm.scanChainFileForLastValid()
|
|
}
|
|
|
|
// scanChainFileForLastValid scans the chain file and returns the last valid entry
|
|
func (ssm *SealedStateManager) scanChainFileForLastValid() (uint64, string, error) {
|
|
f, err := os.Open(ssm.chainFile)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return 0, "", nil
|
|
}
|
|
return 0, "", fmt.Errorf("open chain file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
var lastEntry StateEntry
|
|
scanner := bufio.NewScanner(f)
|
|
lineNum := 0
|
|
for scanner.Scan() {
|
|
lineNum++
|
|
line := scanner.Text()
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
var entry StateEntry
|
|
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
|
// Corrupted line - log but continue
|
|
continue
|
|
}
|
|
lastEntry = entry
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return 0, "", fmt.Errorf("scan chain file: %w", err)
|
|
}
|
|
|
|
return lastEntry.Seq, lastEntry.Hash, nil
|
|
}
|
|
|
|
// VerifyChainIntegrity checks that the chain file is intact and returns the number of valid entries
|
|
func (ssm *SealedStateManager) VerifyChainIntegrity() (int, error) {
|
|
f, err := os.Open(ssm.chainFile)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return 0, nil
|
|
}
|
|
return 0, fmt.Errorf("open chain file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
validCount := 0
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
var entry StateEntry
|
|
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
|
continue // Skip corrupted lines
|
|
}
|
|
validCount++
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return validCount, fmt.Errorf("scan chain file: %w", err)
|
|
}
|
|
|
|
return validCount, nil
|
|
}
|
|
|
|
// Close is a no-op for SealedStateManager (state is written immediately)
|
|
func (ssm *SealedStateManager) Close() error {
|
|
return nil
|
|
}
|