A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
338 lines
9.3 KiB
Go
338 lines
9.3 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TrailBuilder helps construct structured audit entries
|
|
type TrailBuilder struct {
|
|
store *Store
|
|
}
|
|
|
|
// NewTrailBuilder creates a new trail builder
|
|
func NewTrailBuilder(store *Store) *TrailBuilder {
|
|
return &TrailBuilder{store: store}
|
|
}
|
|
|
|
// LLMEntryBuilder builds LLM audit entries
|
|
type LLMEntryBuilder struct {
|
|
entry *LLMAuditEntry
|
|
store *Store
|
|
}
|
|
|
|
// NewLLMEntry creates a new LLM audit entry builder
|
|
func (tb *TrailBuilder) NewLLMEntry() *LLMEntryBuilder {
|
|
return &LLMEntryBuilder{
|
|
entry: &LLMAuditEntry{
|
|
ID: uuid.New(),
|
|
PIITypesDetected: []string{},
|
|
PolicyViolations: []string{},
|
|
DataCategoriesAccessed: []string{},
|
|
RequestMetadata: make(map[string]any),
|
|
CreatedAt: time.Now().UTC(),
|
|
},
|
|
store: tb.store,
|
|
}
|
|
}
|
|
|
|
// WithTenant sets the tenant ID
|
|
func (b *LLMEntryBuilder) WithTenant(tenantID uuid.UUID) *LLMEntryBuilder {
|
|
b.entry.TenantID = tenantID
|
|
return b
|
|
}
|
|
|
|
// WithNamespace sets the namespace ID
|
|
func (b *LLMEntryBuilder) WithNamespace(namespaceID uuid.UUID) *LLMEntryBuilder {
|
|
b.entry.NamespaceID = &namespaceID
|
|
return b
|
|
}
|
|
|
|
// WithUser sets the user ID
|
|
func (b *LLMEntryBuilder) WithUser(userID uuid.UUID) *LLMEntryBuilder {
|
|
b.entry.UserID = userID
|
|
return b
|
|
}
|
|
|
|
// WithSession sets the session ID
|
|
func (b *LLMEntryBuilder) WithSession(sessionID string) *LLMEntryBuilder {
|
|
b.entry.SessionID = sessionID
|
|
return b
|
|
}
|
|
|
|
// WithOperation sets the operation type
|
|
func (b *LLMEntryBuilder) WithOperation(operation string) *LLMEntryBuilder {
|
|
b.entry.Operation = operation
|
|
return b
|
|
}
|
|
|
|
// WithModel sets the model used
|
|
func (b *LLMEntryBuilder) WithModel(model, provider string) *LLMEntryBuilder {
|
|
b.entry.ModelUsed = model
|
|
b.entry.Provider = provider
|
|
return b
|
|
}
|
|
|
|
// WithPrompt sets prompt-related fields
|
|
func (b *LLMEntryBuilder) WithPrompt(hash string, length int) *LLMEntryBuilder {
|
|
b.entry.PromptHash = hash
|
|
b.entry.PromptLength = length
|
|
return b
|
|
}
|
|
|
|
// WithResponse sets response-related fields
|
|
func (b *LLMEntryBuilder) WithResponse(length int) *LLMEntryBuilder {
|
|
b.entry.ResponseLength = length
|
|
return b
|
|
}
|
|
|
|
// WithUsage sets token usage and duration
|
|
func (b *LLMEntryBuilder) WithUsage(tokens int, durationMS int) *LLMEntryBuilder {
|
|
b.entry.TokensUsed = tokens
|
|
b.entry.DurationMS = durationMS
|
|
return b
|
|
}
|
|
|
|
// WithPII sets PII detection fields
|
|
func (b *LLMEntryBuilder) WithPII(detected bool, types []string, redacted bool) *LLMEntryBuilder {
|
|
b.entry.PIIDetected = detected
|
|
b.entry.PIITypesDetected = types
|
|
b.entry.PIIRedacted = redacted
|
|
return b
|
|
}
|
|
|
|
// WithPolicy sets policy-related fields
|
|
func (b *LLMEntryBuilder) WithPolicy(policyID *uuid.UUID, violations []string) *LLMEntryBuilder {
|
|
b.entry.PolicyID = policyID
|
|
b.entry.PolicyViolations = violations
|
|
return b
|
|
}
|
|
|
|
// WithDataCategories sets accessed data categories
|
|
func (b *LLMEntryBuilder) WithDataCategories(categories []string) *LLMEntryBuilder {
|
|
b.entry.DataCategoriesAccessed = categories
|
|
return b
|
|
}
|
|
|
|
// WithError sets error message
|
|
func (b *LLMEntryBuilder) WithError(errMsg string) *LLMEntryBuilder {
|
|
b.entry.ErrorMessage = errMsg
|
|
return b
|
|
}
|
|
|
|
// WithMetadata sets request metadata
|
|
func (b *LLMEntryBuilder) WithMetadata(metadata map[string]any) *LLMEntryBuilder {
|
|
b.entry.RequestMetadata = metadata
|
|
return b
|
|
}
|
|
|
|
// AddMetadata adds a key-value pair to metadata
|
|
func (b *LLMEntryBuilder) AddMetadata(key string, value any) *LLMEntryBuilder {
|
|
if b.entry.RequestMetadata == nil {
|
|
b.entry.RequestMetadata = make(map[string]any)
|
|
}
|
|
b.entry.RequestMetadata[key] = value
|
|
return b
|
|
}
|
|
|
|
// Build returns the built entry
|
|
func (b *LLMEntryBuilder) Build() *LLMAuditEntry {
|
|
return b.entry
|
|
}
|
|
|
|
// Save persists the entry to the database
|
|
func (b *LLMEntryBuilder) Save(ctx context.Context) error {
|
|
return b.store.CreateLLMAuditEntry(ctx, b.entry)
|
|
}
|
|
|
|
// GeneralEntryBuilder builds general audit entries
|
|
type GeneralEntryBuilder struct {
|
|
entry *GeneralAuditEntry
|
|
store *Store
|
|
}
|
|
|
|
// NewGeneralEntry creates a new general audit entry builder
|
|
func (tb *TrailBuilder) NewGeneralEntry() *GeneralEntryBuilder {
|
|
return &GeneralEntryBuilder{
|
|
entry: &GeneralAuditEntry{
|
|
ID: uuid.New(),
|
|
OldValues: make(map[string]any),
|
|
NewValues: make(map[string]any),
|
|
CreatedAt: time.Now().UTC(),
|
|
},
|
|
store: tb.store,
|
|
}
|
|
}
|
|
|
|
// WithTenant sets the tenant ID
|
|
func (b *GeneralEntryBuilder) WithTenant(tenantID uuid.UUID) *GeneralEntryBuilder {
|
|
b.entry.TenantID = tenantID
|
|
return b
|
|
}
|
|
|
|
// WithNamespace sets the namespace ID
|
|
func (b *GeneralEntryBuilder) WithNamespace(namespaceID uuid.UUID) *GeneralEntryBuilder {
|
|
b.entry.NamespaceID = &namespaceID
|
|
return b
|
|
}
|
|
|
|
// WithUser sets the user ID
|
|
func (b *GeneralEntryBuilder) WithUser(userID uuid.UUID) *GeneralEntryBuilder {
|
|
b.entry.UserID = userID
|
|
return b
|
|
}
|
|
|
|
// WithAction sets the action
|
|
func (b *GeneralEntryBuilder) WithAction(action string) *GeneralEntryBuilder {
|
|
b.entry.Action = action
|
|
return b
|
|
}
|
|
|
|
// WithResource sets the resource type and ID
|
|
func (b *GeneralEntryBuilder) WithResource(resourceType string, resourceID *uuid.UUID) *GeneralEntryBuilder {
|
|
b.entry.ResourceType = resourceType
|
|
b.entry.ResourceID = resourceID
|
|
return b
|
|
}
|
|
|
|
// WithOldValues sets the old values
|
|
func (b *GeneralEntryBuilder) WithOldValues(values map[string]any) *GeneralEntryBuilder {
|
|
b.entry.OldValues = values
|
|
return b
|
|
}
|
|
|
|
// WithNewValues sets the new values
|
|
func (b *GeneralEntryBuilder) WithNewValues(values map[string]any) *GeneralEntryBuilder {
|
|
b.entry.NewValues = values
|
|
return b
|
|
}
|
|
|
|
// WithClient sets client information
|
|
func (b *GeneralEntryBuilder) WithClient(ipAddress, userAgent string) *GeneralEntryBuilder {
|
|
b.entry.IPAddress = ipAddress
|
|
b.entry.UserAgent = userAgent
|
|
return b
|
|
}
|
|
|
|
// WithReason sets the reason for the action
|
|
func (b *GeneralEntryBuilder) WithReason(reason string) *GeneralEntryBuilder {
|
|
b.entry.Reason = reason
|
|
return b
|
|
}
|
|
|
|
// Build returns the built entry
|
|
func (b *GeneralEntryBuilder) Build() *GeneralAuditEntry {
|
|
return b.entry
|
|
}
|
|
|
|
// Save persists the entry to the database
|
|
func (b *GeneralEntryBuilder) Save(ctx context.Context) error {
|
|
return b.store.CreateGeneralAuditEntry(ctx, b.entry)
|
|
}
|
|
|
|
// Common audit action types
|
|
const (
|
|
ActionCreate = "create"
|
|
ActionUpdate = "update"
|
|
ActionDelete = "delete"
|
|
ActionRead = "read"
|
|
ActionExport = "export"
|
|
ActionGrant = "grant"
|
|
ActionRevoke = "revoke"
|
|
ActionLogin = "login"
|
|
ActionLogout = "logout"
|
|
ActionFailed = "failed"
|
|
)
|
|
|
|
// Common resource types
|
|
const (
|
|
ResourceTenant = "tenant"
|
|
ResourceNamespace = "namespace"
|
|
ResourceRole = "role"
|
|
ResourceUserRole = "user_role"
|
|
ResourcePolicy = "llm_policy"
|
|
ResourceAPIKey = "api_key"
|
|
ResourceEvidence = "evidence"
|
|
ResourceControl = "control"
|
|
)
|
|
|
|
// Convenience methods for common operations
|
|
|
|
// LogRoleAssignment creates an audit entry for role assignment
|
|
func (tb *TrailBuilder) LogRoleAssignment(ctx context.Context, tenantID, userID, targetUserID, roleID uuid.UUID, grantedBy uuid.UUID, ipAddress, userAgent string) error {
|
|
return tb.NewGeneralEntry().
|
|
WithTenant(tenantID).
|
|
WithUser(grantedBy).
|
|
WithAction(ActionGrant).
|
|
WithResource(ResourceUserRole, &roleID).
|
|
WithNewValues(map[string]any{
|
|
"target_user_id": targetUserID.String(),
|
|
"role_id": roleID.String(),
|
|
}).
|
|
WithClient(ipAddress, userAgent).
|
|
Save(ctx)
|
|
}
|
|
|
|
// LogRoleRevocation creates an audit entry for role revocation
|
|
func (tb *TrailBuilder) LogRoleRevocation(ctx context.Context, tenantID, userID, targetUserID, roleID uuid.UUID, revokedBy uuid.UUID, reason, ipAddress, userAgent string) error {
|
|
return tb.NewGeneralEntry().
|
|
WithTenant(tenantID).
|
|
WithUser(revokedBy).
|
|
WithAction(ActionRevoke).
|
|
WithResource(ResourceUserRole, &roleID).
|
|
WithOldValues(map[string]any{
|
|
"target_user_id": targetUserID.String(),
|
|
"role_id": roleID.String(),
|
|
}).
|
|
WithReason(reason).
|
|
WithClient(ipAddress, userAgent).
|
|
Save(ctx)
|
|
}
|
|
|
|
// LogPolicyChange creates an audit entry for LLM policy changes
|
|
func (tb *TrailBuilder) LogPolicyChange(ctx context.Context, tenantID, userID, policyID uuid.UUID, action string, oldValues, newValues map[string]any, ipAddress, userAgent string) error {
|
|
return tb.NewGeneralEntry().
|
|
WithTenant(tenantID).
|
|
WithUser(userID).
|
|
WithAction(action).
|
|
WithResource(ResourcePolicy, &policyID).
|
|
WithOldValues(oldValues).
|
|
WithNewValues(newValues).
|
|
WithClient(ipAddress, userAgent).
|
|
Save(ctx)
|
|
}
|
|
|
|
// LogNamespaceAccess creates an audit entry for namespace access
|
|
func (tb *TrailBuilder) LogNamespaceAccess(ctx context.Context, tenantID, userID, namespaceID uuid.UUID, action string, ipAddress, userAgent string) error {
|
|
return tb.NewGeneralEntry().
|
|
WithTenant(tenantID).
|
|
WithUser(userID).
|
|
WithNamespace(namespaceID).
|
|
WithAction(action).
|
|
WithResource(ResourceNamespace, &namespaceID).
|
|
WithClient(ipAddress, userAgent).
|
|
Save(ctx)
|
|
}
|
|
|
|
// LogDataExport creates an audit entry for data export
|
|
func (tb *TrailBuilder) LogDataExport(ctx context.Context, tenantID, userID uuid.UUID, namespaceID *uuid.UUID, resourceType, format string, recordCount int, ipAddress, userAgent string) error {
|
|
builder := tb.NewGeneralEntry().
|
|
WithTenant(tenantID).
|
|
WithUser(userID).
|
|
WithAction(ActionExport).
|
|
WithResource(resourceType, nil).
|
|
WithNewValues(map[string]any{
|
|
"format": format,
|
|
"record_count": recordCount,
|
|
}).
|
|
WithClient(ipAddress, userAgent)
|
|
|
|
if namespaceID != nil {
|
|
builder.WithNamespace(*namespaceID)
|
|
}
|
|
|
|
return builder.Save(ctx)
|
|
}
|