package policy import ( "context" "encoding/json" "github.com/google/uuid" ) // Auditor provides audit logging functionality for the policy system. type Auditor struct { store *Store } // NewAuditor creates a new Auditor instance. func NewAuditor(store *Store) *Auditor { return &Auditor{store: store} } // LogChange logs a policy change to the audit trail. func (a *Auditor) LogChange(ctx context.Context, action AuditAction, entityType AuditEntityType, entityID *uuid.UUID, oldValue, newValue interface{}, userEmail, ipAddress, userAgent *string) error { entry := &PolicyAuditLog{ Action: action, EntityType: entityType, EntityID: entityID, UserEmail: userEmail, IPAddress: ipAddress, UserAgent: userAgent, } if oldValue != nil { entry.OldValue = toJSON(oldValue) } if newValue != nil { entry.NewValue = toJSON(newValue) } return a.store.CreateAuditLog(ctx, entry) } // LogBlocked logs a blocked URL to the blocked content log. func (a *Auditor) LogBlocked(ctx context.Context, url, domain string, reason BlockReason, ruleID *uuid.UUID, details map[string]interface{}) error { entry := &BlockedContentLog{ URL: url, Domain: domain, BlockReason: reason, MatchedRuleID: ruleID, } if details != nil { entry.Details = toJSON(details) } return a.store.CreateBlockedContentLog(ctx, entry) } // ============================================================================= // CONVENIENCE METHODS // ============================================================================= // LogPolicyCreated logs a policy creation event. func (a *Auditor) LogPolicyCreated(ctx context.Context, policy *SourcePolicy, userEmail *string) error { return a.LogChange(ctx, AuditActionCreate, AuditEntitySourcePolicy, &policy.ID, nil, policy, userEmail, nil, nil) } // LogPolicyUpdated logs a policy update event. func (a *Auditor) LogPolicyUpdated(ctx context.Context, oldPolicy, newPolicy *SourcePolicy, userEmail *string) error { return a.LogChange(ctx, AuditActionUpdate, AuditEntitySourcePolicy, &newPolicy.ID, oldPolicy, newPolicy, userEmail, nil, nil) } // LogPolicyDeleted logs a policy deletion event. func (a *Auditor) LogPolicyDeleted(ctx context.Context, policy *SourcePolicy, userEmail *string) error { return a.LogChange(ctx, AuditActionDelete, AuditEntitySourcePolicy, &policy.ID, policy, nil, userEmail, nil, nil) } // LogPolicyActivated logs a policy activation event. func (a *Auditor) LogPolicyActivated(ctx context.Context, policy *SourcePolicy, userEmail *string) error { return a.LogChange(ctx, AuditActionActivate, AuditEntitySourcePolicy, &policy.ID, nil, policy, userEmail, nil, nil) } // LogPolicyDeactivated logs a policy deactivation event. func (a *Auditor) LogPolicyDeactivated(ctx context.Context, policy *SourcePolicy, userEmail *string) error { return a.LogChange(ctx, AuditActionDeactivate, AuditEntitySourcePolicy, &policy.ID, policy, nil, userEmail, nil, nil) } // LogSourceCreated logs a source creation event. func (a *Auditor) LogSourceCreated(ctx context.Context, source *AllowedSource, userEmail *string) error { return a.LogChange(ctx, AuditActionCreate, AuditEntityAllowedSource, &source.ID, nil, source, userEmail, nil, nil) } // LogSourceUpdated logs a source update event. func (a *Auditor) LogSourceUpdated(ctx context.Context, oldSource, newSource *AllowedSource, userEmail *string) error { return a.LogChange(ctx, AuditActionUpdate, AuditEntityAllowedSource, &newSource.ID, oldSource, newSource, userEmail, nil, nil) } // LogSourceDeleted logs a source deletion event. func (a *Auditor) LogSourceDeleted(ctx context.Context, source *AllowedSource, userEmail *string) error { return a.LogChange(ctx, AuditActionDelete, AuditEntityAllowedSource, &source.ID, source, nil, userEmail, nil, nil) } // LogOperationUpdated logs an operation permission update event. func (a *Auditor) LogOperationUpdated(ctx context.Context, oldOp, newOp *OperationPermission, userEmail *string) error { return a.LogChange(ctx, AuditActionUpdate, AuditEntityOperationPermission, &newOp.ID, oldOp, newOp, userEmail, nil, nil) } // LogPIIRuleCreated logs a PII rule creation event. func (a *Auditor) LogPIIRuleCreated(ctx context.Context, rule *PIIRule, userEmail *string) error { return a.LogChange(ctx, AuditActionCreate, AuditEntityPIIRule, &rule.ID, nil, rule, userEmail, nil, nil) } // LogPIIRuleUpdated logs a PII rule update event. func (a *Auditor) LogPIIRuleUpdated(ctx context.Context, oldRule, newRule *PIIRule, userEmail *string) error { return a.LogChange(ctx, AuditActionUpdate, AuditEntityPIIRule, &newRule.ID, oldRule, newRule, userEmail, nil, nil) } // LogPIIRuleDeleted logs a PII rule deletion event. func (a *Auditor) LogPIIRuleDeleted(ctx context.Context, rule *PIIRule, userEmail *string) error { return a.LogChange(ctx, AuditActionDelete, AuditEntityPIIRule, &rule.ID, rule, nil, userEmail, nil, nil) } // LogContentBlocked logs a blocked content event with details. func (a *Auditor) LogContentBlocked(ctx context.Context, url, domain string, reason BlockReason, matchedPatterns []string, ruleID *uuid.UUID) error { details := map[string]interface{}{ "matched_patterns": matchedPatterns, } return a.LogBlocked(ctx, url, domain, reason, ruleID, details) } // LogPIIBlocked logs content blocked due to PII detection. func (a *Auditor) LogPIIBlocked(ctx context.Context, url, domain string, matches []PIIMatch) error { matchDetails := make([]map[string]interface{}, len(matches)) var ruleID *uuid.UUID for i, m := range matches { matchDetails[i] = map[string]interface{}{ "rule_name": m.RuleName, "severity": m.Severity, "match": maskPII(m.Match), // Mask the actual PII in logs } if ruleID == nil { ruleID = &m.RuleID } } details := map[string]interface{}{ "pii_matches": matchDetails, "match_count": len(matches), } return a.LogBlocked(ctx, url, domain, BlockReasonPIIDetected, ruleID, details) } // ============================================================================= // HELPERS // ============================================================================= // toJSON converts a value to JSON. func toJSON(v interface{}) json.RawMessage { data, err := json.Marshal(v) if err != nil { return nil } return data } // maskPII masks PII data for safe logging. func maskPII(pii string) string { if len(pii) <= 4 { return "****" } // Show first 2 and last 2 characters return pii[:2] + "****" + pii[len(pii)-2:] } // ============================================================================= // AUDIT REPORT GENERATION // ============================================================================= // AuditReport represents an audit report for compliance. type AuditReport struct { GeneratedAt string `json:"generated_at"` PeriodStart string `json:"period_start"` PeriodEnd string `json:"period_end"` Summary AuditReportSummary `json:"summary"` PolicyChanges []PolicyAuditLog `json:"policy_changes"` BlockedContent []BlockedContentLog `json:"blocked_content"` Stats *PolicyStats `json:"stats"` } // AuditReportSummary contains summary statistics for the audit report. type AuditReportSummary struct { TotalPolicyChanges int `json:"total_policy_changes"` TotalBlocked int `json:"total_blocked"` ChangesByAction map[string]int `json:"changes_by_action"` BlocksByReason map[string]int `json:"blocks_by_reason"` } // GenerateAuditReport generates a compliance audit report. func (a *Auditor) GenerateAuditReport(ctx context.Context, filter *AuditLogFilter, blockedFilter *BlockedContentFilter) (*AuditReport, error) { // Get audit logs auditLogs, _, err := a.store.ListAuditLogs(ctx, filter) if err != nil { return nil, err } // Get blocked content blockedLogs, _, err := a.store.ListBlockedContent(ctx, blockedFilter) if err != nil { return nil, err } // Get stats stats, err := a.store.GetStats(ctx) if err != nil { return nil, err } // Build summary summary := AuditReportSummary{ TotalPolicyChanges: len(auditLogs), TotalBlocked: len(blockedLogs), ChangesByAction: make(map[string]int), BlocksByReason: make(map[string]int), } for _, log := range auditLogs { summary.ChangesByAction[string(log.Action)]++ } for _, log := range blockedLogs { summary.BlocksByReason[string(log.BlockReason)]++ } // Build report periodStart := "" periodEnd := "" if filter.FromDate != nil { periodStart = filter.FromDate.Format("2006-01-02") } if filter.ToDate != nil { periodEnd = filter.ToDate.Format("2006-01-02") } report := &AuditReport{ GeneratedAt: uuid.New().String()[:19], // Timestamp placeholder PeriodStart: periodStart, PeriodEnd: periodEnd, Summary: summary, PolicyChanges: auditLogs, BlockedContent: blockedLogs, Stats: stats, } return report, nil }