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:
445
ai-compliance-sdk/internal/api/handlers/audit_handlers.go
Normal file
445
ai-compliance-sdk/internal/api/handlers/audit_handlers.go
Normal file
@@ -0,0 +1,445 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/audit"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AuditHandlers handles audit-related API endpoints
|
||||
type AuditHandlers struct {
|
||||
store *audit.Store
|
||||
exporter *audit.Exporter
|
||||
}
|
||||
|
||||
// NewAuditHandlers creates new audit handlers
|
||||
func NewAuditHandlers(store *audit.Store, exporter *audit.Exporter) *AuditHandlers {
|
||||
return &AuditHandlers{
|
||||
store: store,
|
||||
exporter: exporter,
|
||||
}
|
||||
}
|
||||
|
||||
// QueryLLMAudit queries LLM audit entries
|
||||
func (h *AuditHandlers) QueryLLMAudit(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
filter := &audit.LLMAuditFilter{
|
||||
TenantID: tenantID,
|
||||
Limit: 50,
|
||||
Offset: 0,
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
if nsID := c.Query("namespace_id"); nsID != "" {
|
||||
if id, err := uuid.Parse(nsID); err == nil {
|
||||
filter.NamespaceID = &id
|
||||
}
|
||||
}
|
||||
|
||||
if userID := c.Query("user_id"); userID != "" {
|
||||
if id, err := uuid.Parse(userID); err == nil {
|
||||
filter.UserID = &id
|
||||
}
|
||||
}
|
||||
|
||||
if op := c.Query("operation"); op != "" {
|
||||
filter.Operation = op
|
||||
}
|
||||
|
||||
if model := c.Query("model"); model != "" {
|
||||
filter.Model = model
|
||||
}
|
||||
|
||||
if pii := c.Query("pii_detected"); pii != "" {
|
||||
val := pii == "true"
|
||||
filter.PIIDetected = &val
|
||||
}
|
||||
|
||||
if violations := c.Query("has_violations"); violations == "true" {
|
||||
val := true
|
||||
filter.HasViolations = &val
|
||||
}
|
||||
|
||||
if startDate := c.Query("start_date"); startDate != "" {
|
||||
if t, err := time.Parse(time.RFC3339, startDate); err == nil {
|
||||
filter.StartDate = &t
|
||||
}
|
||||
}
|
||||
|
||||
if endDate := c.Query("end_date"); endDate != "" {
|
||||
if t, err := time.Parse(time.RFC3339, endDate); err == nil {
|
||||
filter.EndDate = &t
|
||||
}
|
||||
}
|
||||
|
||||
if limit := c.Query("limit"); limit != "" {
|
||||
var l int
|
||||
if _, err := parseIntQuery(limit, &l); err == nil && l > 0 && l <= 1000 {
|
||||
filter.Limit = l
|
||||
}
|
||||
}
|
||||
|
||||
if offset := c.Query("offset"); offset != "" {
|
||||
var o int
|
||||
if _, err := parseIntQuery(offset, &o); err == nil && o >= 0 {
|
||||
filter.Offset = o
|
||||
}
|
||||
}
|
||||
|
||||
entries, total, err := h.store.QueryLLMAuditEntries(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"entries": entries,
|
||||
"total": total,
|
||||
"limit": filter.Limit,
|
||||
"offset": filter.Offset,
|
||||
})
|
||||
}
|
||||
|
||||
// QueryGeneralAudit queries general audit entries
|
||||
func (h *AuditHandlers) QueryGeneralAudit(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
filter := &audit.GeneralAuditFilter{
|
||||
TenantID: tenantID,
|
||||
Limit: 50,
|
||||
Offset: 0,
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
if nsID := c.Query("namespace_id"); nsID != "" {
|
||||
if id, err := uuid.Parse(nsID); err == nil {
|
||||
filter.NamespaceID = &id
|
||||
}
|
||||
}
|
||||
|
||||
if userID := c.Query("user_id"); userID != "" {
|
||||
if id, err := uuid.Parse(userID); err == nil {
|
||||
filter.UserID = &id
|
||||
}
|
||||
}
|
||||
|
||||
if action := c.Query("action"); action != "" {
|
||||
filter.Action = action
|
||||
}
|
||||
|
||||
if resourceType := c.Query("resource_type"); resourceType != "" {
|
||||
filter.ResourceType = resourceType
|
||||
}
|
||||
|
||||
if resourceID := c.Query("resource_id"); resourceID != "" {
|
||||
if id, err := uuid.Parse(resourceID); err == nil {
|
||||
filter.ResourceID = &id
|
||||
}
|
||||
}
|
||||
|
||||
if startDate := c.Query("start_date"); startDate != "" {
|
||||
if t, err := time.Parse(time.RFC3339, startDate); err == nil {
|
||||
filter.StartDate = &t
|
||||
}
|
||||
}
|
||||
|
||||
if endDate := c.Query("end_date"); endDate != "" {
|
||||
if t, err := time.Parse(time.RFC3339, endDate); err == nil {
|
||||
filter.EndDate = &t
|
||||
}
|
||||
}
|
||||
|
||||
if limit := c.Query("limit"); limit != "" {
|
||||
var l int
|
||||
if _, err := parseIntQuery(limit, &l); err == nil && l > 0 && l <= 1000 {
|
||||
filter.Limit = l
|
||||
}
|
||||
}
|
||||
|
||||
if offset := c.Query("offset"); offset != "" {
|
||||
var o int
|
||||
if _, err := parseIntQuery(offset, &o); err == nil && o >= 0 {
|
||||
filter.Offset = o
|
||||
}
|
||||
}
|
||||
|
||||
entries, total, err := h.store.QueryGeneralAuditEntries(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"entries": entries,
|
||||
"total": total,
|
||||
"limit": filter.Limit,
|
||||
"offset": filter.Offset,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUsageStats returns LLM usage statistics
|
||||
func (h *AuditHandlers) GetUsageStats(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Default to last 30 days
|
||||
endDate := time.Now().UTC()
|
||||
startDate := endDate.AddDate(0, 0, -30)
|
||||
|
||||
if sd := c.Query("start_date"); sd != "" {
|
||||
if t, err := time.Parse(time.RFC3339, sd); err == nil {
|
||||
startDate = t
|
||||
}
|
||||
}
|
||||
|
||||
if ed := c.Query("end_date"); ed != "" {
|
||||
if t, err := time.Parse(time.RFC3339, ed); err == nil {
|
||||
endDate = t
|
||||
}
|
||||
}
|
||||
|
||||
stats, err := h.store.GetLLMUsageStats(c.Request.Context(), tenantID, startDate, endDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"period_start": startDate.Format(time.RFC3339),
|
||||
"period_end": endDate.Format(time.RFC3339),
|
||||
"stats": stats,
|
||||
})
|
||||
}
|
||||
|
||||
// ExportLLMAudit exports LLM audit entries
|
||||
func (h *AuditHandlers) ExportLLMAudit(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Default to last 30 days
|
||||
endDate := time.Now().UTC()
|
||||
startDate := endDate.AddDate(0, 0, -30)
|
||||
|
||||
if sd := c.Query("start_date"); sd != "" {
|
||||
if t, err := time.Parse(time.RFC3339, sd); err == nil {
|
||||
startDate = t
|
||||
}
|
||||
}
|
||||
|
||||
if ed := c.Query("end_date"); ed != "" {
|
||||
if t, err := time.Parse(time.RFC3339, ed); err == nil {
|
||||
endDate = t
|
||||
}
|
||||
}
|
||||
|
||||
format := audit.FormatJSON
|
||||
if c.Query("format") == "csv" {
|
||||
format = audit.FormatCSV
|
||||
}
|
||||
|
||||
includePII := c.Query("include_pii") == "true"
|
||||
|
||||
opts := &audit.ExportOptions{
|
||||
TenantID: tenantID,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Format: format,
|
||||
IncludePII: includePII,
|
||||
}
|
||||
|
||||
if nsID := c.Query("namespace_id"); nsID != "" {
|
||||
if id, err := uuid.Parse(nsID); err == nil {
|
||||
opts.NamespaceID = &id
|
||||
}
|
||||
}
|
||||
|
||||
if userID := c.Query("user_id"); userID != "" {
|
||||
if id, err := uuid.Parse(userID); err == nil {
|
||||
opts.UserID = &id
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := h.exporter.ExportLLMAudit(c.Request.Context(), &buf, opts); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Set appropriate content type
|
||||
contentType := "application/json"
|
||||
ext := "json"
|
||||
if format == audit.FormatCSV {
|
||||
contentType = "text/csv"
|
||||
ext = "csv"
|
||||
}
|
||||
|
||||
filename := "llm_audit_" + time.Now().Format("20060102") + "." + ext
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
c.Data(http.StatusOK, contentType, buf.Bytes())
|
||||
}
|
||||
|
||||
// ExportGeneralAudit exports general audit entries
|
||||
func (h *AuditHandlers) ExportGeneralAudit(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
endDate := time.Now().UTC()
|
||||
startDate := endDate.AddDate(0, 0, -30)
|
||||
|
||||
if sd := c.Query("start_date"); sd != "" {
|
||||
if t, err := time.Parse(time.RFC3339, sd); err == nil {
|
||||
startDate = t
|
||||
}
|
||||
}
|
||||
|
||||
if ed := c.Query("end_date"); ed != "" {
|
||||
if t, err := time.Parse(time.RFC3339, ed); err == nil {
|
||||
endDate = t
|
||||
}
|
||||
}
|
||||
|
||||
format := audit.FormatJSON
|
||||
if c.Query("format") == "csv" {
|
||||
format = audit.FormatCSV
|
||||
}
|
||||
|
||||
includePII := c.Query("include_pii") == "true"
|
||||
|
||||
opts := &audit.ExportOptions{
|
||||
TenantID: tenantID,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Format: format,
|
||||
IncludePII: includePII,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := h.exporter.ExportGeneralAudit(c.Request.Context(), &buf, opts); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contentType := "application/json"
|
||||
ext := "json"
|
||||
if format == audit.FormatCSV {
|
||||
contentType = "text/csv"
|
||||
ext = "csv"
|
||||
}
|
||||
|
||||
filename := "general_audit_" + time.Now().Format("20060102") + "." + ext
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
c.Data(http.StatusOK, contentType, buf.Bytes())
|
||||
}
|
||||
|
||||
// ExportComplianceReport exports a compliance report
|
||||
func (h *AuditHandlers) ExportComplianceReport(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
endDate := time.Now().UTC()
|
||||
startDate := endDate.AddDate(0, 0, -30)
|
||||
|
||||
if sd := c.Query("start_date"); sd != "" {
|
||||
if t, err := time.Parse(time.RFC3339, sd); err == nil {
|
||||
startDate = t
|
||||
}
|
||||
}
|
||||
|
||||
if ed := c.Query("end_date"); ed != "" {
|
||||
if t, err := time.Parse(time.RFC3339, ed); err == nil {
|
||||
endDate = t
|
||||
}
|
||||
}
|
||||
|
||||
format := audit.FormatJSON
|
||||
if c.Query("format") == "csv" {
|
||||
format = audit.FormatCSV
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := h.exporter.ExportComplianceReport(c.Request.Context(), &buf, tenantID, startDate, endDate, format); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contentType := "application/json"
|
||||
ext := "json"
|
||||
if format == audit.FormatCSV {
|
||||
contentType = "text/csv"
|
||||
ext = "csv"
|
||||
}
|
||||
|
||||
filename := "compliance_report_" + time.Now().Format("20060102") + "." + ext
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
c.Data(http.StatusOK, contentType, buf.Bytes())
|
||||
}
|
||||
|
||||
// GetComplianceReport returns a compliance report as JSON (for dashboard)
|
||||
func (h *AuditHandlers) GetComplianceReport(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
endDate := time.Now().UTC()
|
||||
startDate := endDate.AddDate(0, 0, -30)
|
||||
|
||||
if sd := c.Query("start_date"); sd != "" {
|
||||
if t, err := time.Parse(time.RFC3339, sd); err == nil {
|
||||
startDate = t
|
||||
}
|
||||
}
|
||||
|
||||
if ed := c.Query("end_date"); ed != "" {
|
||||
if t, err := time.Parse(time.RFC3339, ed); err == nil {
|
||||
endDate = t
|
||||
}
|
||||
}
|
||||
|
||||
report, err := h.exporter.GenerateComplianceReport(c.Request.Context(), tenantID, startDate, endDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, report)
|
||||
}
|
||||
|
||||
// Helper function to parse int from query
|
||||
func parseIntQuery(s string, out *int) (int, error) {
|
||||
var i int
|
||||
_, err := fmt.Sscanf(s, "%d", &i)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
*out = i
|
||||
return i, nil
|
||||
}
|
||||
Reference in New Issue
Block a user