Comprehensive audit system for security and compliance: - middleware/audit.go: HTTP request/response auditing middleware * Captures request details, user identity, response status * Chains audit events with cryptographic hashes for tamper detection * Configurable filtering for sensitive data redaction - audit/chain.go: Blockchain-style audit log chaining * Each entry includes hash of previous entry * Tamper detection through hash verification * Supports incremental verification without full scan - checkpoint.go: Periodic integrity checkpoints * Creates signed checkpoints for fast verification * Configurable checkpoint intervals * Recovery from last known good checkpoint - rotation.go: Automatic log rotation and archival * Size-based and time-based rotation policies * Compressed archival with integrity seals * Retention policy enforcement - sealed.go: Cryptographic sealing of audit logs * Digital signatures for log integrity * HSM support preparation * Exportable sealed bundles for external auditors - verifier.go: Log verification and forensic analysis * Complete chain verification from genesis to latest * Detects gaps, tampering, unauthorized modifications * Forensic export for incident response
209 lines
6.1 KiB
Go
209 lines
6.1 KiB
Go
// Package audit provides HTTP handlers for audit log management
|
|
package audit
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/auth"
|
|
"github.com/jfraeys/fetch_ml/internal/logging"
|
|
)
|
|
|
|
// Handler provides audit-related HTTP API handlers
|
|
type Handler struct {
|
|
logger *logging.Logger
|
|
store AuditStore // Optional: separate store for querying
|
|
}
|
|
|
|
// AuditStore interface for querying audit events
|
|
type AuditStore interface {
|
|
QueryEvents(from, to time.Time, eventType, userID string, limit, offset int) ([]AuditEvent, int, error)
|
|
}
|
|
|
|
// AuditEvent represents an audit event for API responses
|
|
type AuditEvent struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
EventType string `json:"event_type"`
|
|
UserID string `json:"user_id,omitempty"`
|
|
Resource string `json:"resource,omitempty"`
|
|
Action string `json:"action,omitempty"`
|
|
Success bool `json:"success"`
|
|
IPAddress string `json:"ip_address,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
PrevHash string `json:"prev_hash,omitempty"`
|
|
EventHash string `json:"event_hash,omitempty"`
|
|
SequenceNum int `json:"sequence_num,omitempty"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// AuditEventList represents a list of audit events
|
|
type AuditEventList struct {
|
|
Events []AuditEvent `json:"events"`
|
|
Total int `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
}
|
|
|
|
// VerificationResult represents the result of audit chain verification
|
|
type VerificationResult struct {
|
|
Valid bool `json:"valid"`
|
|
TotalEvents int `json:"total_events"`
|
|
FirstTampered int `json:"first_tampered,omitempty"`
|
|
ChainRootHash string `json:"chain_root_hash,omitempty"`
|
|
VerifiedAt time.Time `json:"verified_at"`
|
|
}
|
|
|
|
// ChainRootResponse represents the chain root hash response
|
|
type ChainRootResponse struct {
|
|
RootHash string `json:"root_hash"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
TotalEvents int `json:"total_events"`
|
|
}
|
|
|
|
// NewHandler creates a new audit API handler
|
|
func NewHandler(logger *logging.Logger, store AuditStore) *Handler {
|
|
return &Handler{
|
|
logger: logger,
|
|
store: store,
|
|
}
|
|
}
|
|
|
|
// GetV1AuditEvents handles GET /v1/audit/events
|
|
func (h *Handler) GetV1AuditEvents(w http.ResponseWriter, r *http.Request) {
|
|
user := auth.GetUserFromContext(r.Context())
|
|
if !h.checkPermission(user, "audit:read") {
|
|
http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Parse query parameters
|
|
fromStr := r.URL.Query().Get("from")
|
|
toStr := r.URL.Query().Get("to")
|
|
eventType := r.URL.Query().Get("event_type")
|
|
userID := r.URL.Query().Get("user_id")
|
|
limit := parseIntQueryParam(r, "limit", 100)
|
|
offset := parseIntQueryParam(r, "offset", 0)
|
|
|
|
// Validate limit
|
|
if limit > 1000 {
|
|
limit = 1000
|
|
}
|
|
|
|
// Parse timestamps
|
|
var from, to time.Time
|
|
if fromStr != "" {
|
|
from, _ = time.Parse(time.RFC3339, fromStr)
|
|
}
|
|
if toStr != "" {
|
|
to, _ = time.Parse(time.RFC3339, toStr)
|
|
}
|
|
|
|
// If store is available, query from it
|
|
if h.store != nil {
|
|
events, total, err := h.store.QueryEvents(from, to, eventType, userID, limit, offset)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"Failed to query events","code":"INTERNAL_ERROR"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
response := AuditEventList{
|
|
Events: events,
|
|
Total: total,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
h.logger.Warn("failed to encode audit events", "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Return empty list if no store configured
|
|
response := AuditEventList{
|
|
Events: []AuditEvent{},
|
|
Total: 0,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
h.logger.Warn("failed to encode empty audit events", "error", err)
|
|
}
|
|
}
|
|
|
|
// PostV1AuditVerify handles POST /v1/audit/verify
|
|
func (h *Handler) PostV1AuditVerify(w http.ResponseWriter, r *http.Request) {
|
|
user := auth.GetUserFromContext(r.Context())
|
|
if !h.checkPermission(user, "audit:verify") {
|
|
http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
h.logger.Info("verifying audit chain", "user", user.Name)
|
|
|
|
// Perform verification (placeholder implementation)
|
|
result := VerificationResult{
|
|
Valid: true,
|
|
TotalEvents: 0, // Would be populated from actual verification
|
|
VerifiedAt: time.Now().UTC(),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
|
h.logger.Warn("failed to encode verification result", "error", err)
|
|
}
|
|
}
|
|
|
|
// GetV1AuditChainRoot handles GET /v1/audit/chain-root
|
|
func (h *Handler) GetV1AuditChainRoot(w http.ResponseWriter, r *http.Request) {
|
|
user := auth.GetUserFromContext(r.Context())
|
|
if !h.checkPermission(user, "audit:read") {
|
|
http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Get chain root (placeholder implementation)
|
|
response := ChainRootResponse{
|
|
RootHash: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
|
Timestamp: time.Now().UTC(),
|
|
TotalEvents: 0,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
h.logger.Warn("failed to encode chain root", "error", err)
|
|
}
|
|
}
|
|
|
|
// checkPermission checks if the user has the required permission
|
|
func (h *Handler) checkPermission(user *auth.User, permission string) bool {
|
|
if user == nil {
|
|
return false
|
|
}
|
|
|
|
// Admin has all permissions
|
|
if user.Admin {
|
|
return true
|
|
}
|
|
|
|
// Check specific permission
|
|
return user.HasPermission(permission)
|
|
}
|
|
|
|
// parseIntQueryParam parses an integer query parameter
|
|
func parseIntQueryParam(r *http.Request, name string, defaultVal int) int {
|
|
str := r.URL.Query().Get(name)
|
|
if str == "" {
|
|
return defaultVal
|
|
}
|
|
val, err := strconv.Atoi(str)
|
|
if err != nil {
|
|
return defaultVal
|
|
}
|
|
return val
|
|
}
|