Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS 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
|
||||
}
|
||||
Reference in New Issue
Block a user