Files
breakpilot-lehrer/edu-search-service/internal/policy/audit.go
Benjamin Boenisch 414e0f5ec0
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
feat: edu-search-service migriert, voice-service/geo-service entfernt
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor)
- opensearch + edu-search-service in docker-compose.yml hinzugefuegt
- voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core)
- geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt)
- CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt
  (Go lint, test mit go mod download, build, SBOM)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 18:36:38 +01:00

256 lines
8.8 KiB
Go

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
}