fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
444
ai-compliance-sdk/internal/audit/exporter.go
Normal file
444
ai-compliance-sdk/internal/audit/exporter.go
Normal file
@@ -0,0 +1,444 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ExportFormat represents the export format
|
||||
type ExportFormat string
|
||||
|
||||
const (
|
||||
FormatCSV ExportFormat = "csv"
|
||||
FormatJSON ExportFormat = "json"
|
||||
)
|
||||
|
||||
// Exporter exports audit data in various formats
|
||||
type Exporter struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// NewExporter creates a new exporter
|
||||
func NewExporter(store *Store) *Exporter {
|
||||
return &Exporter{store: store}
|
||||
}
|
||||
|
||||
// ExportOptions defines export options
|
||||
type ExportOptions struct {
|
||||
TenantID uuid.UUID
|
||||
NamespaceID *uuid.UUID
|
||||
UserID *uuid.UUID
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
Format ExportFormat
|
||||
IncludePII bool // If false, redact PII fields
|
||||
}
|
||||
|
||||
// ExportLLMAudit exports LLM audit entries
|
||||
func (e *Exporter) ExportLLMAudit(ctx context.Context, w io.Writer, opts *ExportOptions) error {
|
||||
filter := &LLMAuditFilter{
|
||||
TenantID: opts.TenantID,
|
||||
NamespaceID: opts.NamespaceID,
|
||||
UserID: opts.UserID,
|
||||
StartDate: &opts.StartDate,
|
||||
EndDate: &opts.EndDate,
|
||||
Limit: 0, // No limit for export
|
||||
}
|
||||
|
||||
entries, _, err := e.store.QueryLLMAuditEntries(ctx, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query audit entries: %w", err)
|
||||
}
|
||||
|
||||
switch opts.Format {
|
||||
case FormatCSV:
|
||||
return e.exportLLMAuditCSV(w, entries, opts.IncludePII)
|
||||
case FormatJSON:
|
||||
return e.exportLLMAuditJSON(w, entries, opts.IncludePII)
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", opts.Format)
|
||||
}
|
||||
}
|
||||
|
||||
// ExportGeneralAudit exports general audit entries
|
||||
func (e *Exporter) ExportGeneralAudit(ctx context.Context, w io.Writer, opts *ExportOptions) error {
|
||||
filter := &GeneralAuditFilter{
|
||||
TenantID: opts.TenantID,
|
||||
NamespaceID: opts.NamespaceID,
|
||||
UserID: opts.UserID,
|
||||
StartDate: &opts.StartDate,
|
||||
EndDate: &opts.EndDate,
|
||||
Limit: 0,
|
||||
}
|
||||
|
||||
entries, _, err := e.store.QueryGeneralAuditEntries(ctx, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query audit entries: %w", err)
|
||||
}
|
||||
|
||||
switch opts.Format {
|
||||
case FormatCSV:
|
||||
return e.exportGeneralAuditCSV(w, entries, opts.IncludePII)
|
||||
case FormatJSON:
|
||||
return e.exportGeneralAuditJSON(w, entries, opts.IncludePII)
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", opts.Format)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Exporter) exportLLMAuditCSV(w io.Writer, entries []*LLMAuditEntry, includePII bool) error {
|
||||
writer := csv.NewWriter(w)
|
||||
defer writer.Flush()
|
||||
|
||||
// Write header
|
||||
header := []string{
|
||||
"ID", "Tenant ID", "Namespace ID", "User ID", "Session ID",
|
||||
"Operation", "Model", "Provider", "Prompt Hash", "Prompt Length", "Response Length",
|
||||
"Tokens Used", "Duration (ms)", "PII Detected", "PII Types", "PII Redacted",
|
||||
"Policy ID", "Policy Violations", "Data Categories", "Error", "Created At",
|
||||
}
|
||||
if err := writer.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write entries
|
||||
for _, entry := range entries {
|
||||
namespaceID := ""
|
||||
if entry.NamespaceID != nil {
|
||||
namespaceID = entry.NamespaceID.String()
|
||||
}
|
||||
|
||||
policyID := ""
|
||||
if entry.PolicyID != nil {
|
||||
policyID = entry.PolicyID.String()
|
||||
}
|
||||
|
||||
userID := entry.UserID.String()
|
||||
sessionID := entry.SessionID
|
||||
if !includePII {
|
||||
userID = "[REDACTED]"
|
||||
sessionID = "[REDACTED]"
|
||||
}
|
||||
|
||||
row := []string{
|
||||
entry.ID.String(),
|
||||
entry.TenantID.String(),
|
||||
namespaceID,
|
||||
userID,
|
||||
sessionID,
|
||||
entry.Operation,
|
||||
entry.ModelUsed,
|
||||
entry.Provider,
|
||||
entry.PromptHash,
|
||||
fmt.Sprintf("%d", entry.PromptLength),
|
||||
fmt.Sprintf("%d", entry.ResponseLength),
|
||||
fmt.Sprintf("%d", entry.TokensUsed),
|
||||
fmt.Sprintf("%d", entry.DurationMS),
|
||||
fmt.Sprintf("%t", entry.PIIDetected),
|
||||
strings.Join(entry.PIITypesDetected, ";"),
|
||||
fmt.Sprintf("%t", entry.PIIRedacted),
|
||||
policyID,
|
||||
strings.Join(entry.PolicyViolations, ";"),
|
||||
strings.Join(entry.DataCategoriesAccessed, ";"),
|
||||
entry.ErrorMessage,
|
||||
entry.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if err := writer.Write(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Exporter) exportLLMAuditJSON(w io.Writer, entries []*LLMAuditEntry, includePII bool) error {
|
||||
// Redact PII if needed
|
||||
if !includePII {
|
||||
for _, entry := range entries {
|
||||
entry.UserID = uuid.Nil
|
||||
entry.SessionID = "[REDACTED]"
|
||||
entry.RequestMetadata = nil
|
||||
}
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(map[string]any{
|
||||
"type": "llm_audit_export",
|
||||
"exported_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"count": len(entries),
|
||||
"entries": entries,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *Exporter) exportGeneralAuditCSV(w io.Writer, entries []*GeneralAuditEntry, includePII bool) error {
|
||||
writer := csv.NewWriter(w)
|
||||
defer writer.Flush()
|
||||
|
||||
// Write header
|
||||
header := []string{
|
||||
"ID", "Tenant ID", "Namespace ID", "User ID",
|
||||
"Action", "Resource Type", "Resource ID",
|
||||
"IP Address", "User Agent", "Reason", "Created At",
|
||||
}
|
||||
if err := writer.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write entries
|
||||
for _, entry := range entries {
|
||||
namespaceID := ""
|
||||
if entry.NamespaceID != nil {
|
||||
namespaceID = entry.NamespaceID.String()
|
||||
}
|
||||
|
||||
resourceID := ""
|
||||
if entry.ResourceID != nil {
|
||||
resourceID = entry.ResourceID.String()
|
||||
}
|
||||
|
||||
userID := entry.UserID.String()
|
||||
ipAddress := entry.IPAddress
|
||||
userAgent := entry.UserAgent
|
||||
if !includePII {
|
||||
userID = "[REDACTED]"
|
||||
ipAddress = "[REDACTED]"
|
||||
userAgent = "[REDACTED]"
|
||||
}
|
||||
|
||||
row := []string{
|
||||
entry.ID.String(),
|
||||
entry.TenantID.String(),
|
||||
namespaceID,
|
||||
userID,
|
||||
entry.Action,
|
||||
entry.ResourceType,
|
||||
resourceID,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
entry.Reason,
|
||||
entry.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if err := writer.Write(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Exporter) exportGeneralAuditJSON(w io.Writer, entries []*GeneralAuditEntry, includePII bool) error {
|
||||
// Redact PII if needed
|
||||
if !includePII {
|
||||
for _, entry := range entries {
|
||||
entry.UserID = uuid.Nil
|
||||
entry.IPAddress = "[REDACTED]"
|
||||
entry.UserAgent = "[REDACTED]"
|
||||
}
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(map[string]any{
|
||||
"type": "general_audit_export",
|
||||
"exported_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"count": len(entries),
|
||||
"entries": entries,
|
||||
})
|
||||
}
|
||||
|
||||
// ExportUsageStats exports LLM usage statistics
|
||||
func (e *Exporter) ExportUsageStats(ctx context.Context, w io.Writer, tenantID uuid.UUID, startDate, endDate time.Time, format ExportFormat) error {
|
||||
stats, err := e.store.GetLLMUsageStats(ctx, tenantID, startDate, endDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get usage stats: %w", err)
|
||||
}
|
||||
|
||||
report := map[string]any{
|
||||
"type": "llm_usage_report",
|
||||
"tenant_id": tenantID.String(),
|
||||
"period_start": startDate.Format(time.RFC3339),
|
||||
"period_end": endDate.Format(time.RFC3339),
|
||||
"generated_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"total_requests": stats.TotalRequests,
|
||||
"total_tokens": stats.TotalTokens,
|
||||
"total_duration_ms": stats.TotalDurationMS,
|
||||
"requests_with_pii": stats.RequestsWithPII,
|
||||
"policy_violations": stats.PolicyViolations,
|
||||
"models_used": stats.ModelsUsed,
|
||||
}
|
||||
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(report)
|
||||
case FormatCSV:
|
||||
writer := csv.NewWriter(w)
|
||||
defer writer.Flush()
|
||||
|
||||
// Write summary as CSV
|
||||
if err := writer.Write([]string{"Metric", "Value"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows := [][]string{
|
||||
{"Total Requests", fmt.Sprintf("%d", stats.TotalRequests)},
|
||||
{"Total Tokens", fmt.Sprintf("%d", stats.TotalTokens)},
|
||||
{"Total Duration (ms)", fmt.Sprintf("%d", stats.TotalDurationMS)},
|
||||
{"Requests with PII", fmt.Sprintf("%d", stats.RequestsWithPII)},
|
||||
{"Policy Violations", fmt.Sprintf("%d", stats.PolicyViolations)},
|
||||
}
|
||||
|
||||
for model, count := range stats.ModelsUsed {
|
||||
rows = append(rows, []string{fmt.Sprintf("Model: %s", model), fmt.Sprintf("%d", count)})
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if err := writer.Write(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
// ComplianceReport generates a compliance report
|
||||
type ComplianceReport struct {
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
PeriodStart time.Time `json:"period_start"`
|
||||
PeriodEnd time.Time `json:"period_end"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
TotalLLMRequests int `json:"total_llm_requests"`
|
||||
TotalTokensUsed int `json:"total_tokens_used"`
|
||||
PIIIncidents int `json:"pii_incidents"`
|
||||
PIIRedactionRate float64 `json:"pii_redaction_rate"`
|
||||
PolicyViolations int `json:"policy_violations"`
|
||||
ModelsUsed map[string]int `json:"models_used"`
|
||||
TopUsers []UserActivity `json:"top_users,omitempty"`
|
||||
NamespaceBreakdown []NamespaceStats `json:"namespace_breakdown,omitempty"`
|
||||
}
|
||||
|
||||
// UserActivity represents user activity in the report
|
||||
type UserActivity struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
RequestCount int `json:"request_count"`
|
||||
TokensUsed int `json:"tokens_used"`
|
||||
PIIIncidents int `json:"pii_incidents"`
|
||||
}
|
||||
|
||||
// NamespaceStats represents namespace statistics
|
||||
type NamespaceStats struct {
|
||||
NamespaceID uuid.UUID `json:"namespace_id"`
|
||||
NamespaceName string `json:"namespace_name,omitempty"`
|
||||
RequestCount int `json:"request_count"`
|
||||
TokensUsed int `json:"tokens_used"`
|
||||
}
|
||||
|
||||
// GenerateComplianceReport generates a detailed compliance report
|
||||
func (e *Exporter) GenerateComplianceReport(ctx context.Context, tenantID uuid.UUID, startDate, endDate time.Time) (*ComplianceReport, error) {
|
||||
// Get basic stats
|
||||
stats, err := e.store.GetLLMUsageStats(ctx, tenantID, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Calculate PII redaction rate
|
||||
var redactionRate float64
|
||||
if stats.RequestsWithPII > 0 {
|
||||
// Query entries with PII to check redaction
|
||||
filter := &LLMAuditFilter{
|
||||
TenantID: tenantID,
|
||||
PIIDetected: boolPtr(true),
|
||||
StartDate: &startDate,
|
||||
EndDate: &endDate,
|
||||
}
|
||||
entries, _, err := e.store.QueryLLMAuditEntries(ctx, filter)
|
||||
if err == nil {
|
||||
redactedCount := 0
|
||||
for _, e := range entries {
|
||||
if e.PIIRedacted {
|
||||
redactedCount++
|
||||
}
|
||||
}
|
||||
redactionRate = float64(redactedCount) / float64(len(entries)) * 100
|
||||
}
|
||||
}
|
||||
|
||||
report := &ComplianceReport{
|
||||
TenantID: tenantID,
|
||||
PeriodStart: startDate,
|
||||
PeriodEnd: endDate,
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
TotalLLMRequests: stats.TotalRequests,
|
||||
TotalTokensUsed: stats.TotalTokens,
|
||||
PIIIncidents: stats.RequestsWithPII,
|
||||
PIIRedactionRate: redactionRate,
|
||||
PolicyViolations: stats.PolicyViolations,
|
||||
ModelsUsed: stats.ModelsUsed,
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// ExportComplianceReport exports a compliance report
|
||||
func (e *Exporter) ExportComplianceReport(ctx context.Context, w io.Writer, tenantID uuid.UUID, startDate, endDate time.Time, format ExportFormat) error {
|
||||
report, err := e.GenerateComplianceReport(ctx, tenantID, startDate, endDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(report)
|
||||
case FormatCSV:
|
||||
writer := csv.NewWriter(w)
|
||||
defer writer.Flush()
|
||||
|
||||
// Write header
|
||||
if err := writer.Write([]string{"Metric", "Value"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows := [][]string{
|
||||
{"Report Type", "AI Compliance Report"},
|
||||
{"Tenant ID", report.TenantID.String()},
|
||||
{"Period Start", report.PeriodStart.Format(time.RFC3339)},
|
||||
{"Period End", report.PeriodEnd.Format(time.RFC3339)},
|
||||
{"Generated At", report.GeneratedAt.Format(time.RFC3339)},
|
||||
{"Total LLM Requests", fmt.Sprintf("%d", report.TotalLLMRequests)},
|
||||
{"Total Tokens Used", fmt.Sprintf("%d", report.TotalTokensUsed)},
|
||||
{"PII Incidents", fmt.Sprintf("%d", report.PIIIncidents)},
|
||||
{"PII Redaction Rate", fmt.Sprintf("%.2f%%", report.PIIRedactionRate)},
|
||||
{"Policy Violations", fmt.Sprintf("%d", report.PolicyViolations)},
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if err := writer.Write(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
472
ai-compliance-sdk/internal/audit/store.go
Normal file
472
ai-compliance-sdk/internal/audit/store.go
Normal file
@@ -0,0 +1,472 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Store provides database operations for audit logs
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewStore creates a new audit store
|
||||
func NewStore(pool *pgxpool.Pool) *Store {
|
||||
return &Store{pool: pool}
|
||||
}
|
||||
|
||||
// LLMAuditEntry represents an LLM audit log entry
|
||||
type LLMAuditEntry struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
NamespaceID *uuid.UUID `json:"namespace_id,omitempty" db:"namespace_id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
SessionID string `json:"session_id,omitempty" db:"session_id"`
|
||||
Operation string `json:"operation" db:"operation"`
|
||||
ModelUsed string `json:"model_used" db:"model_used"`
|
||||
Provider string `json:"provider" db:"provider"`
|
||||
PromptHash string `json:"prompt_hash" db:"prompt_hash"`
|
||||
PromptLength int `json:"prompt_length" db:"prompt_length"`
|
||||
ResponseLength int `json:"response_length,omitempty" db:"response_length"`
|
||||
TokensUsed int `json:"tokens_used" db:"tokens_used"`
|
||||
DurationMS int `json:"duration_ms" db:"duration_ms"`
|
||||
PIIDetected bool `json:"pii_detected" db:"pii_detected"`
|
||||
PIITypesDetected []string `json:"pii_types_detected,omitempty" db:"pii_types_detected"`
|
||||
PIIRedacted bool `json:"pii_redacted" db:"pii_redacted"`
|
||||
PolicyID *uuid.UUID `json:"policy_id,omitempty" db:"policy_id"`
|
||||
PolicyViolations []string `json:"policy_violations,omitempty" db:"policy_violations"`
|
||||
DataCategoriesAccessed []string `json:"data_categories_accessed,omitempty" db:"data_categories_accessed"`
|
||||
ErrorMessage string `json:"error_message,omitempty" db:"error_message"`
|
||||
RequestMetadata map[string]any `json:"request_metadata,omitempty" db:"request_metadata"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// GeneralAuditEntry represents a general audit trail entry
|
||||
type GeneralAuditEntry struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
NamespaceID *uuid.UUID `json:"namespace_id,omitempty" db:"namespace_id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Action string `json:"action" db:"action"`
|
||||
ResourceType string `json:"resource_type" db:"resource_type"`
|
||||
ResourceID *uuid.UUID `json:"resource_id,omitempty" db:"resource_id"`
|
||||
OldValues map[string]any `json:"old_values,omitempty" db:"old_values"`
|
||||
NewValues map[string]any `json:"new_values,omitempty" db:"new_values"`
|
||||
IPAddress string `json:"ip_address,omitempty" db:"ip_address"`
|
||||
UserAgent string `json:"user_agent,omitempty" db:"user_agent"`
|
||||
Reason string `json:"reason,omitempty" db:"reason"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// CreateLLMAuditEntry creates a new LLM audit log entry
|
||||
func (s *Store) CreateLLMAuditEntry(ctx context.Context, entry *LLMAuditEntry) error {
|
||||
if entry.ID == uuid.Nil {
|
||||
entry.ID = uuid.New()
|
||||
}
|
||||
if entry.CreatedAt.IsZero() {
|
||||
entry.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
|
||||
metadataJSON, _ := json.Marshal(entry.RequestMetadata)
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO compliance_llm_audit_log (
|
||||
id, tenant_id, namespace_id, user_id, session_id,
|
||||
operation, model_used, provider, prompt_hash, prompt_length, response_length,
|
||||
tokens_used, duration_ms, pii_detected, pii_types_detected, pii_redacted,
|
||||
policy_id, policy_violations, data_categories_accessed, error_message,
|
||||
request_metadata, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)
|
||||
`,
|
||||
entry.ID, entry.TenantID, entry.NamespaceID, entry.UserID, entry.SessionID,
|
||||
entry.Operation, entry.ModelUsed, entry.Provider, entry.PromptHash, entry.PromptLength, entry.ResponseLength,
|
||||
entry.TokensUsed, entry.DurationMS, entry.PIIDetected, entry.PIITypesDetected, entry.PIIRedacted,
|
||||
entry.PolicyID, entry.PolicyViolations, entry.DataCategoriesAccessed, entry.ErrorMessage,
|
||||
metadataJSON, entry.CreatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateGeneralAuditEntry creates a new general audit entry
|
||||
func (s *Store) CreateGeneralAuditEntry(ctx context.Context, entry *GeneralAuditEntry) error {
|
||||
if entry.ID == uuid.Nil {
|
||||
entry.ID = uuid.New()
|
||||
}
|
||||
if entry.CreatedAt.IsZero() {
|
||||
entry.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
|
||||
oldValuesJSON, _ := json.Marshal(entry.OldValues)
|
||||
newValuesJSON, _ := json.Marshal(entry.NewValues)
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO compliance_audit_trail (
|
||||
id, tenant_id, namespace_id, user_id, action, resource_type, resource_id,
|
||||
old_values, new_values, ip_address, user_agent, reason, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
`,
|
||||
entry.ID, entry.TenantID, entry.NamespaceID, entry.UserID,
|
||||
entry.Action, entry.ResourceType, entry.ResourceID,
|
||||
oldValuesJSON, newValuesJSON, entry.IPAddress, entry.UserAgent,
|
||||
entry.Reason, entry.CreatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LLMAuditFilter defines filters for LLM audit queries
|
||||
type LLMAuditFilter struct {
|
||||
TenantID uuid.UUID
|
||||
NamespaceID *uuid.UUID
|
||||
UserID *uuid.UUID
|
||||
Operation string
|
||||
Model string
|
||||
PIIDetected *bool
|
||||
HasViolations *bool
|
||||
StartDate *time.Time
|
||||
EndDate *time.Time
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
// QueryLLMAuditEntries queries LLM audit entries with filters
|
||||
func (s *Store) QueryLLMAuditEntries(ctx context.Context, filter *LLMAuditFilter) ([]*LLMAuditEntry, int, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, namespace_id, user_id, session_id,
|
||||
operation, model_used, provider, prompt_hash, prompt_length, response_length,
|
||||
tokens_used, duration_ms, pii_detected, pii_types_detected, pii_redacted,
|
||||
policy_id, policy_violations, data_categories_accessed, error_message,
|
||||
request_metadata, created_at
|
||||
FROM compliance_llm_audit_log
|
||||
WHERE tenant_id = $1
|
||||
`
|
||||
|
||||
countQuery := `SELECT COUNT(*) FROM compliance_llm_audit_log WHERE tenant_id = $1`
|
||||
|
||||
args := []any{filter.TenantID}
|
||||
argIndex := 2
|
||||
|
||||
if filter.NamespaceID != nil {
|
||||
query += fmt.Sprintf(" AND namespace_id = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND namespace_id = $%d", argIndex)
|
||||
args = append(args, *filter.NamespaceID)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.UserID != nil {
|
||||
query += fmt.Sprintf(" AND user_id = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND user_id = $%d", argIndex)
|
||||
args = append(args, *filter.UserID)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.Operation != "" {
|
||||
query += fmt.Sprintf(" AND operation = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND operation = $%d", argIndex)
|
||||
args = append(args, filter.Operation)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.Model != "" {
|
||||
query += fmt.Sprintf(" AND model_used = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND model_used = $%d", argIndex)
|
||||
args = append(args, filter.Model)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.PIIDetected != nil {
|
||||
query += fmt.Sprintf(" AND pii_detected = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND pii_detected = $%d", argIndex)
|
||||
args = append(args, *filter.PIIDetected)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.HasViolations != nil && *filter.HasViolations {
|
||||
query += " AND array_length(policy_violations, 1) > 0"
|
||||
countQuery += " AND array_length(policy_violations, 1) > 0"
|
||||
}
|
||||
|
||||
if filter.StartDate != nil {
|
||||
query += fmt.Sprintf(" AND created_at >= $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND created_at >= $%d", argIndex)
|
||||
args = append(args, *filter.StartDate)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.EndDate != nil {
|
||||
query += fmt.Sprintf(" AND created_at <= $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND created_at <= $%d", argIndex)
|
||||
args = append(args, *filter.EndDate)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
// Get total count
|
||||
var totalCount int
|
||||
if err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&totalCount); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Add ordering and pagination
|
||||
query += " ORDER BY created_at DESC"
|
||||
|
||||
if filter.Limit > 0 {
|
||||
query += fmt.Sprintf(" LIMIT $%d", argIndex)
|
||||
args = append(args, filter.Limit)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.Offset > 0 {
|
||||
query += fmt.Sprintf(" OFFSET $%d", argIndex)
|
||||
args = append(args, filter.Offset)
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []*LLMAuditEntry
|
||||
for rows.Next() {
|
||||
var entry LLMAuditEntry
|
||||
var metadataJSON []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&entry.ID, &entry.TenantID, &entry.NamespaceID, &entry.UserID, &entry.SessionID,
|
||||
&entry.Operation, &entry.ModelUsed, &entry.Provider, &entry.PromptHash, &entry.PromptLength, &entry.ResponseLength,
|
||||
&entry.TokensUsed, &entry.DurationMS, &entry.PIIDetected, &entry.PIITypesDetected, &entry.PIIRedacted,
|
||||
&entry.PolicyID, &entry.PolicyViolations, &entry.DataCategoriesAccessed, &entry.ErrorMessage,
|
||||
&metadataJSON, &entry.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadataJSON != nil {
|
||||
json.Unmarshal(metadataJSON, &entry.RequestMetadata)
|
||||
}
|
||||
|
||||
entries = append(entries, &entry)
|
||||
}
|
||||
|
||||
return entries, totalCount, nil
|
||||
}
|
||||
|
||||
// GeneralAuditFilter defines filters for general audit queries
|
||||
type GeneralAuditFilter struct {
|
||||
TenantID uuid.UUID
|
||||
NamespaceID *uuid.UUID
|
||||
UserID *uuid.UUID
|
||||
Action string
|
||||
ResourceType string
|
||||
ResourceID *uuid.UUID
|
||||
StartDate *time.Time
|
||||
EndDate *time.Time
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
// QueryGeneralAuditEntries queries general audit entries with filters
|
||||
func (s *Store) QueryGeneralAuditEntries(ctx context.Context, filter *GeneralAuditFilter) ([]*GeneralAuditEntry, int, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, namespace_id, user_id, action, resource_type, resource_id,
|
||||
old_values, new_values, ip_address, user_agent, reason, created_at
|
||||
FROM compliance_audit_trail
|
||||
WHERE tenant_id = $1
|
||||
`
|
||||
|
||||
countQuery := `SELECT COUNT(*) FROM compliance_audit_trail WHERE tenant_id = $1`
|
||||
|
||||
args := []any{filter.TenantID}
|
||||
argIndex := 2
|
||||
|
||||
if filter.NamespaceID != nil {
|
||||
query += fmt.Sprintf(" AND namespace_id = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND namespace_id = $%d", argIndex)
|
||||
args = append(args, *filter.NamespaceID)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.UserID != nil {
|
||||
query += fmt.Sprintf(" AND user_id = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND user_id = $%d", argIndex)
|
||||
args = append(args, *filter.UserID)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.Action != "" {
|
||||
query += fmt.Sprintf(" AND action = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND action = $%d", argIndex)
|
||||
args = append(args, filter.Action)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.ResourceType != "" {
|
||||
query += fmt.Sprintf(" AND resource_type = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND resource_type = $%d", argIndex)
|
||||
args = append(args, filter.ResourceType)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.ResourceID != nil {
|
||||
query += fmt.Sprintf(" AND resource_id = $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND resource_id = $%d", argIndex)
|
||||
args = append(args, *filter.ResourceID)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.StartDate != nil {
|
||||
query += fmt.Sprintf(" AND created_at >= $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND created_at >= $%d", argIndex)
|
||||
args = append(args, *filter.StartDate)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.EndDate != nil {
|
||||
query += fmt.Sprintf(" AND created_at <= $%d", argIndex)
|
||||
countQuery += fmt.Sprintf(" AND created_at <= $%d", argIndex)
|
||||
args = append(args, *filter.EndDate)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
// Get total count
|
||||
var totalCount int
|
||||
if err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&totalCount); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Add ordering and pagination
|
||||
query += " ORDER BY created_at DESC"
|
||||
|
||||
if filter.Limit > 0 {
|
||||
query += fmt.Sprintf(" LIMIT $%d", argIndex)
|
||||
args = append(args, filter.Limit)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.Offset > 0 {
|
||||
query += fmt.Sprintf(" OFFSET $%d", argIndex)
|
||||
args = append(args, filter.Offset)
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []*GeneralAuditEntry
|
||||
for rows.Next() {
|
||||
var entry GeneralAuditEntry
|
||||
var oldValuesJSON, newValuesJSON []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&entry.ID, &entry.TenantID, &entry.NamespaceID, &entry.UserID,
|
||||
&entry.Action, &entry.ResourceType, &entry.ResourceID,
|
||||
&oldValuesJSON, &newValuesJSON, &entry.IPAddress, &entry.UserAgent,
|
||||
&entry.Reason, &entry.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if oldValuesJSON != nil {
|
||||
json.Unmarshal(oldValuesJSON, &entry.OldValues)
|
||||
}
|
||||
if newValuesJSON != nil {
|
||||
json.Unmarshal(newValuesJSON, &entry.NewValues)
|
||||
}
|
||||
|
||||
entries = append(entries, &entry)
|
||||
}
|
||||
|
||||
return entries, totalCount, nil
|
||||
}
|
||||
|
||||
// GetLLMUsageStats retrieves aggregated LLM usage statistics
|
||||
func (s *Store) GetLLMUsageStats(ctx context.Context, tenantID uuid.UUID, startDate, endDate time.Time) (*LLMUsageStats, error) {
|
||||
var stats LLMUsageStats
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
COUNT(*) as total_requests,
|
||||
COALESCE(SUM(tokens_used), 0) as total_tokens,
|
||||
COALESCE(SUM(duration_ms), 0) as total_duration_ms,
|
||||
COUNT(*) FILTER (WHERE pii_detected = TRUE) as requests_with_pii,
|
||||
COUNT(*) FILTER (WHERE array_length(policy_violations, 1) > 0) as policy_violations
|
||||
FROM compliance_llm_audit_log
|
||||
WHERE tenant_id = $1 AND created_at >= $2 AND created_at <= $3
|
||||
`, tenantID, startDate, endDate).Scan(
|
||||
&stats.TotalRequests,
|
||||
&stats.TotalTokens,
|
||||
&stats.TotalDurationMS,
|
||||
&stats.RequestsWithPII,
|
||||
&stats.PolicyViolations,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get model usage breakdown
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT model_used, COUNT(*) as count
|
||||
FROM compliance_llm_audit_log
|
||||
WHERE tenant_id = $1 AND created_at >= $2 AND created_at <= $3
|
||||
GROUP BY model_used
|
||||
`, tenantID, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
stats.ModelsUsed = make(map[string]int)
|
||||
for rows.Next() {
|
||||
var model string
|
||||
var count int
|
||||
if err := rows.Scan(&model, &count); err == nil {
|
||||
stats.ModelsUsed[model] = count
|
||||
}
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// LLMUsageStats represents aggregated LLM usage statistics
|
||||
type LLMUsageStats struct {
|
||||
TotalRequests int `json:"total_requests"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
TotalDurationMS int64 `json:"total_duration_ms"`
|
||||
RequestsWithPII int `json:"requests_with_pii"`
|
||||
PolicyViolations int `json:"policy_violations"`
|
||||
ModelsUsed map[string]int `json:"models_used"`
|
||||
}
|
||||
|
||||
// CleanupOldEntries removes audit entries older than the retention period
|
||||
func (s *Store) CleanupOldEntries(ctx context.Context, retentionDays int) (int, int, error) {
|
||||
cutoff := time.Now().UTC().AddDate(0, 0, -retentionDays)
|
||||
|
||||
// Cleanup LLM audit log
|
||||
llmResult, err := s.pool.Exec(ctx, `
|
||||
DELETE FROM compliance_llm_audit_log WHERE created_at < $1
|
||||
`, cutoff)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// Cleanup general audit trail
|
||||
generalResult, err := s.pool.Exec(ctx, `
|
||||
DELETE FROM compliance_audit_trail WHERE created_at < $1
|
||||
`, cutoff)
|
||||
if err != nil {
|
||||
return int(llmResult.RowsAffected()), 0, err
|
||||
}
|
||||
|
||||
return int(llmResult.RowsAffected()), int(generalResult.RowsAffected()), nil
|
||||
}
|
||||
337
ai-compliance-sdk/internal/audit/trail_builder.go
Normal file
337
ai-compliance-sdk/internal/audit/trail_builder.go
Normal file
@@ -0,0 +1,337 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user