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 }