Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
retention.go37.1 kB
// Package retention provides data lifecycle and retention policy management for NornicDB. // // This package implements compliance-driven data retention following major regulatory frameworks: // - GDPR Art.5(1)(e): Storage limitation principle // - GDPR Art.17: Right to erasure ("right to be forgotten") // - HIPAA §164.530(j): Record retention (6 years minimum) // - FISMA AU-11: Audit Record Retention // - SOC2 CC7.4: Records retention requirements // - SOX: Financial records (7 years) // // Key Features: // - Configurable retention policies per data category // - Automatic data expiration and cleanup // - Legal hold support (prevents deletion during litigation) // - GDPR Art.17 erasure requests ("right to be forgotten") // - Archive-before-delete option for compliance // - Policy persistence (save/load from JSON) // // Example Usage: // // // Create retention manager // manager := retention.NewManager() // // // Add default compliance policies // for _, policy := range retention.DefaultPolicies() { // manager.AddPolicy(policy) // } // // // Set callbacks // manager.SetDeleteCallback(func(record *retention.DataRecord) error { // return database.Delete(record.ID) // }) // manager.SetArchiveCallback(func(record *retention.DataRecord, path string) error { // return archiveSystem.Store(record, path) // }) // // // Process records according to policies // record := &retention.DataRecord{ // ID: "record-123", // SubjectID: "user-456", // Category: retention.CategoryPII, // CreatedAt: time.Now().Add(-4 * 365 * 24 * time.Hour), // 4 years old // } // // if err := manager.ProcessRecord(ctx, record); err != nil { // log.Fatal(err) // May be deleted if beyond retention period // } // // // Handle GDPR erasure request // req, err := manager.CreateErasureRequest("user-456", "user@example.com") // if err != nil { // log.Fatal(err) // } // // // Find all user's data // records := findAllUserData("user-456") // // // Process erasure (respects legal holds) // if err := manager.ProcessErasure(ctx, req.ID, records); err != nil { // log.Fatal(err) // } // // fmt.Printf("Erased %d records, retained %d (legal hold)\n", // req.ItemsErased, req.ItemsRetained) // // Compliance Notes: // // GDPR Requirements: // - Art.5(1)(e): Data minimization - don't keep data longer than necessary // - Art.17: Right to erasure - users can request deletion of their data // - Art.30: Records of processing - audit trail of what was deleted // - 30-day deadline: Must respond to erasure requests within 30 days // // HIPAA Requirements: // - §164.530(j)(2): Retain PHI for 6 years from creation or last use // - §164.308(a)(1)(ii)(D): Information system activity review (audit logs) // - Must document retention policies and procedures // // SOX Requirements: // - §802: Retain financial records for 7 years // - §1102: Criminal penalties for document destruction // // ELI12 (Explain Like I'm 12): // // Think of data retention like your school locker: // // 1. Some things you need to keep all year (textbooks = SYSTEM data) // 2. Some things you can throw away after the semester (old homework = ANALYTICS) // 3. Some things have rules about how long to keep them (report cards = AUDIT logs) // 4. Sometimes the principal says "don't throw away ANYTHING!" (legal hold) // 5. If you want your stuff deleted, you can ask and they have to do it (GDPR erasure) // // The retention manager is like a locker monitor who makes sure old stuff gets // thrown away at the right time, important stuff is archived first, and nobody // throws away things they're not supposed to! package retention import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "sync" "time" ) // Errors var ( ErrPolicyNotFound = errors.New("retention: policy not found") ErrLegalHold = errors.New("retention: data under legal hold cannot be deleted") ErrInvalidPolicy = errors.New("retention: invalid policy configuration") ErrAlreadyExists = errors.New("retention: policy already exists") ErrErasureInProgress = errors.New("retention: erasure already in progress") ) // DataCategory represents a category of data for retention purposes. type DataCategory string const ( // Standard categories CategorySystem DataCategory = "SYSTEM" // System/infrastructure data CategoryAudit DataCategory = "AUDIT" // Audit logs CategoryUser DataCategory = "USER" // User-created data CategoryAnalytics DataCategory = "ANALYTICS" // Analytics/metrics data CategoryBackup DataCategory = "BACKUP" // Backup data CategoryArchive DataCategory = "ARCHIVE" // Archived data // Compliance-specific categories CategoryPHI DataCategory = "PHI" // Protected Health Information (HIPAA) CategoryPII DataCategory = "PII" // Personally Identifiable Information (GDPR) CategoryFinancial DataCategory = "FINANCIAL" // Financial records (SOX) CategoryLegal DataCategory = "LEGAL" // Legal documents ) // RetentionPeriod defines a time-based retention period. type RetentionPeriod struct { Duration time.Duration // How long to retain Indefinite bool // Retain forever } // Policy defines a retention policy for a data category. type Policy struct { // Unique policy identifier ID string `json:"id"` // Human-readable name Name string `json:"name"` // Data category this policy applies to Category DataCategory `json:"category"` // How long to retain data RetentionPeriod RetentionPeriod `json:"retention_period"` // Archive data before deletion ArchiveBeforeDelete bool `json:"archive_before_delete"` // Archive location path ArchivePath string `json:"archive_path,omitempty"` // Compliance frameworks this policy satisfies ComplianceFrameworks []string `json:"compliance_frameworks,omitempty"` // Whether policy is active Active bool `json:"active"` // When policy was created CreatedAt time.Time `json:"created_at"` // When policy was last modified UpdatedAt time.Time `json:"updated_at"` // Description/notes Description string `json:"description,omitempty"` } // Validate checks if the policy is valid. func (p *Policy) Validate() error { if p.ID == "" { return fmt.Errorf("%w: ID required", ErrInvalidPolicy) } if p.Category == "" { return fmt.Errorf("%w: category required", ErrInvalidPolicy) } if !p.RetentionPeriod.Indefinite && p.RetentionPeriod.Duration <= 0 { return fmt.Errorf("%w: retention period required", ErrInvalidPolicy) } if p.ArchiveBeforeDelete && p.ArchivePath == "" { return fmt.Errorf("%w: archive path required when archiving", ErrInvalidPolicy) } return nil } // IsExpired returns true if data created at the given time should be deleted. func (p *Policy) IsExpired(createdAt time.Time) bool { if p.RetentionPeriod.Indefinite { return false } return time.Now().After(createdAt.Add(p.RetentionPeriod.Duration)) } // LegalHold represents a legal hold on data. type LegalHold struct { // Unique identifier ID string `json:"id"` // Description of the hold Description string `json:"description"` // Matter/case reference Matter string `json:"matter,omitempty"` // Who placed the hold PlacedBy string `json:"placed_by"` // When the hold was placed PlacedAt time.Time `json:"placed_at"` // When the hold expires (zero = indefinite) ExpiresAt time.Time `json:"expires_at,omitempty"` // Data subject IDs under hold SubjectIDs []string `json:"subject_ids,omitempty"` // Data categories under hold Categories []DataCategory `json:"categories,omitempty"` // Whether hold is active Active bool `json:"active"` } // IsActive returns true if the legal hold is currently active. func (h *LegalHold) IsActive() bool { if !h.Active { return false } if h.ExpiresAt.IsZero() { return true } return time.Now().Before(h.ExpiresAt) } // CoversData returns true if the hold covers the given subject and category. func (h *LegalHold) CoversData(subjectID string, category DataCategory) bool { if !h.IsActive() { return false } // Check subject subjectMatch := len(h.SubjectIDs) == 0 // Empty = all subjects for _, id := range h.SubjectIDs { if id == subjectID { subjectMatch = true break } } // Check category categoryMatch := len(h.Categories) == 0 // Empty = all categories for _, cat := range h.Categories { if cat == category { categoryMatch = true break } } return subjectMatch && categoryMatch } // ErasureRequest represents a data subject erasure request (GDPR Art.17). type ErasureRequest struct { // Unique request ID ID string `json:"id"` // Subject ID (user) requesting erasure SubjectID string `json:"subject_id"` // Email/identifier for verification SubjectEmail string `json:"subject_email,omitempty"` // When request was received RequestedAt time.Time `json:"requested_at"` // Deadline for completion (GDPR: 30 days) Deadline time.Time `json:"deadline"` // Current status Status ErasureStatus `json:"status"` // Items found for erasure ItemsFound int `json:"items_found"` // Items erased ItemsErased int `json:"items_erased"` // Items retained (with reason) ItemsRetained int `json:"items_retained"` // Reason for any retained items RetainedReason string `json:"retained_reason,omitempty"` // When processing started StartedAt time.Time `json:"started_at,omitempty"` // When processing completed CompletedAt time.Time `json:"completed_at,omitempty"` // Error message if failed Error string `json:"error,omitempty"` // Whether subject was notified of completion SubjectNotified bool `json:"subject_notified"` } // ErasureStatus represents the status of an erasure request. type ErasureStatus string const ( ErasureStatusPending ErasureStatus = "PENDING" ErasureStatusInProgress ErasureStatus = "IN_PROGRESS" ErasureStatusCompleted ErasureStatus = "COMPLETED" ErasureStatusFailed ErasureStatus = "FAILED" ErasureStatusPartial ErasureStatus = "PARTIAL" // Some items retained ) // DataRecord represents a record that may be subject to retention. type DataRecord struct { // Unique record ID ID string `json:"id"` // Subject ID (owner/user) SubjectID string `json:"subject_id,omitempty"` // Data category Category DataCategory `json:"category"` // When record was created CreatedAt time.Time `json:"created_at"` // When record was last accessed LastAccessedAt time.Time `json:"last_accessed_at,omitempty"` // Record metadata Metadata map[string]string `json:"metadata,omitempty"` } // Manager manages retention policies and data lifecycle. type Manager struct { mu sync.RWMutex policies map[string]*Policy holds map[string]*LegalHold erasures map[string]*ErasureRequest // Callbacks onDelete func(record *DataRecord) error onArchive func(record *DataRecord, archivePath string) error // Configuration defaultPolicy *Policy } // NewManager creates a new retention manager with empty policies and holds. // // The manager starts with no policies, legal holds, or erasure requests. // Use AddPolicy() or load DefaultPolicies() to configure retention rules. // // Example: // // manager := retention.NewManager() // // // Add default compliance policies // for _, policy := range retention.DefaultPolicies() { // if err := manager.AddPolicy(policy); err != nil { // log.Fatal(err) // } // } // // // Set deletion callback // manager.SetDeleteCallback(func(record *retention.DataRecord) error { // return db.Delete(record.ID) // }) // // Returns a new Manager ready for policy configuration. // // Example 1 - GDPR Compliance Setup: // // manager := retention.NewManager() // // // Add GDPR-compliant policies // for _, policy := range retention.DefaultPolicies() { // manager.AddPolicy(policy) // } // // // Set callbacks for data operations // manager.SetDeleteCallback(func(record *retention.DataRecord) error { // log.Printf("Deleting record: %s (age: %v)", record.ID, time.Since(record.CreatedAt)) // return database.Delete(record.ID) // }) // // manager.SetArchiveCallback(func(record *retention.DataRecord, path string) error { // log.Printf("Archiving to: %s", path) // return archiveSystem.Store(record, path) // }) // // Example 2 - HIPAA Healthcare Application: // // manager := retention.NewManager() // // // PHI retention: 6 years minimum // phiPolicy := &retention.Policy{ // ID: "phi-6y", // Name: "PHI Retention", // Category: retention.CategoryPHI, // RetentionPeriod: retention.RetentionPeriod{ // Duration: 6 * 365 * 24 * time.Hour, // }, // ArchiveBeforeDelete: true, // ArchivePath: "/secure-archive/phi", // ComplianceFrameworks: []string{"HIPAA"}, // Active: true, // } // manager.AddPolicy(phiPolicy) // // // Audit logs: 7 years // auditPolicy := &retention.Policy{ // ID: "audit-7y", // Category: retention.CategoryAudit, // RetentionPeriod: retention.RetentionPeriod{ // Duration: 7 * 365 * 24 * time.Hour, // }, // ArchiveBeforeDelete: true, // } // manager.AddPolicy(auditPolicy) // // Example 3 - With Legal Holds: // // manager := retention.NewManager() // manager.AddPolicy(retention.DefaultPolicies()[0]) // // // Create legal hold for litigation // hold, err := manager.CreateLegalHold( // "litigation-2024-001", // "Smith v. Company - Employment Case", // []string{"legal", "hr"}, // ) // if err != nil { // log.Fatal(err) // } // // // Records matching these tags won't be deleted // record := &retention.DataRecord{ // ID: "email-123", // Category: retention.CategoryUser, // Tags: []string{"hr", "employment"}, // CreatedAt: time.Now().Add(-5 * 365 * 24 * time.Hour), // } // // // Won't delete - protected by legal hold // err = manager.ProcessRecord(ctx, record) // // Example 4 - GDPR Right to Erasure: // // manager := retention.NewManager() // // // User requests data deletion // erasureReq, err := manager.CreateErasureRequest( // "user-456", // "user@example.com", // ) // if err != nil { // log.Fatal(err) // } // // // Find all user's data across systems // records := []*retention.DataRecord{ // {ID: "profile-456", SubjectID: "user-456"}, // {ID: "orders-456", SubjectID: "user-456"}, // {ID: "analytics-456", SubjectID: "user-456"}, // } // // // Process erasure (30-day GDPR deadline) // err = manager.ProcessErasure(ctx, erasureReq.ID, records) // if err != nil { // log.Fatal(err) // } // // fmt.Printf("Erased: %d, Retained: %d (legal hold)\n", // erasureReq.ItemsErased, erasureReq.ItemsRetained) // // ELI12: // // NewManager is like hiring a librarian for your school: // // - The librarian keeps track of rules: "Throw away old magazines after 3 months" // - They archive important stuff before throwing it away // - They handle requests: "I want my book reports deleted" (GDPR erasure) // - They respect "DO NOT THROW AWAY" signs (legal holds) // // Why do we need this? // - GDPR: European law says "delete old data you don't need" // - HIPAA: US health law says "keep medical records for 6 years" // - SOX: Financial law says "keep money records for 7 years" // - Storage costs: Old data costs money to store // // How it works: // 1. Set up policies: "Keep PHI for 6 years, analytics for 90 days" // 2. Manager checks records: "Is this record too old?" // 3. If yes: Archive first (if policy says so), then delete // 4. If legal hold: "Can't delete this, it's in a lawsuit!" // // Real-world Use: // - Healthcare: Manage patient records (HIPAA compliance) // - SaaS: Handle user data deletion requests (GDPR) // - Finance: Retain transaction records (SOX compliance) // - E-commerce: Clean up old analytics data // // Compliance Benefits: // - GDPR Art.5(1)(e): Storage limitation ✓ // - GDPR Art.17: Right to erasure ✓ // - HIPAA §164.530(j): 6-year retention ✓ // - SOX: 7-year financial records ✓ // - Automatic audit trail of deletions ✓ // // Performance: // - Policy lookup: O(1) hash map // - Record processing: O(1) per record // - Legal hold check: O(n) where n = number of holds (usually <10) // - Erasure request: O(m) where m = records to erase // // Thread Safety: // All methods are thread-safe for concurrent access. func NewManager() *Manager { return &Manager{ policies: make(map[string]*Policy), holds: make(map[string]*LegalHold), erasures: make(map[string]*ErasureRequest), } } // SetDefaultPolicy sets the default policy for data without a specific policy. func (m *Manager) SetDefaultPolicy(policy *Policy) error { if err := policy.Validate(); err != nil { return err } m.mu.Lock() defer m.mu.Unlock() m.defaultPolicy = policy return nil } // SetDeleteCallback sets the function called when data should be deleted. func (m *Manager) SetDeleteCallback(fn func(record *DataRecord) error) { m.mu.Lock() defer m.mu.Unlock() m.onDelete = fn } // SetArchiveCallback sets the function called when data should be archived. func (m *Manager) SetArchiveCallback(fn func(record *DataRecord, archivePath string) error) { m.mu.Lock() defer m.mu.Unlock() m.onArchive = fn } // AddPolicy adds a retention policy. func (m *Manager) AddPolicy(policy *Policy) error { if err := policy.Validate(); err != nil { return err } m.mu.Lock() defer m.mu.Unlock() if _, exists := m.policies[policy.ID]; exists { return ErrAlreadyExists } if policy.CreatedAt.IsZero() { policy.CreatedAt = time.Now().UTC() } policy.UpdatedAt = time.Now().UTC() m.policies[policy.ID] = policy return nil } // GetPolicy retrieves a policy by ID. func (m *Manager) GetPolicy(id string) (*Policy, error) { m.mu.RLock() defer m.mu.RUnlock() policy, ok := m.policies[id] if !ok { return nil, ErrPolicyNotFound } return policy, nil } // GetPolicyForCategory finds the policy for a data category. func (m *Manager) GetPolicyForCategory(category DataCategory) (*Policy, error) { m.mu.RLock() defer m.mu.RUnlock() for _, p := range m.policies { if p.Active && p.Category == category { return p, nil } } if m.defaultPolicy != nil { return m.defaultPolicy, nil } return nil, ErrPolicyNotFound } // UpdatePolicy updates an existing policy. func (m *Manager) UpdatePolicy(policy *Policy) error { if err := policy.Validate(); err != nil { return err } m.mu.Lock() defer m.mu.Unlock() if _, exists := m.policies[policy.ID]; !exists { return ErrPolicyNotFound } policy.UpdatedAt = time.Now().UTC() m.policies[policy.ID] = policy return nil } // DeletePolicy removes a policy. func (m *Manager) DeletePolicy(id string) error { m.mu.Lock() defer m.mu.Unlock() if _, exists := m.policies[id]; !exists { return ErrPolicyNotFound } delete(m.policies, id) return nil } // ListPolicies returns all policies. func (m *Manager) ListPolicies() []*Policy { m.mu.RLock() defer m.mu.RUnlock() policies := make([]*Policy, 0, len(m.policies)) for _, p := range m.policies { policies = append(policies, p) } return policies } // PlaceLegalHold places a legal hold to prevent data deletion during litigation. // // Legal holds (also called "litigation holds") preserve data that may be relevant // to pending or anticipated legal proceedings. Data under legal hold CANNOT be // deleted, even if retention policies would normally require deletion. // // The hold can target: // - Specific data subjects (users) // - Specific data categories (e.g., all emails) // - All data (leave SubjectIDs and Categories empty) // // Parameters: // - hold: LegalHold configuration // // Returns: // - nil on success // - Error if hold is invalid // // Example: // // // Litigation started - preserve all data for user-123 // hold := &retention.LegalHold{ // ID: "hold-2024-001", // Description: "Smith v. Company lawsuit", // Matter: "Case #2024-CV-12345", // PlacedBy: "legal@company.com", // SubjectIDs: []string{"user-123"}, // // No expiration - hold until manually released // } // // if err := manager.PlaceLegalHold(hold); err != nil { // log.Fatal(err) // } // // fmt.Println("Legal hold placed - data preserved") // // // Later, when litigation ends... // manager.ReleaseLegalHold("hold-2024-001") // // Warning: // Failure to preserve data under legal hold can result in sanctions, // adverse inference instructions, or case dismissal. func (m *Manager) PlaceLegalHold(hold *LegalHold) error { if hold.ID == "" { return fmt.Errorf("%w: ID required", ErrInvalidPolicy) } m.mu.Lock() defer m.mu.Unlock() if hold.PlacedAt.IsZero() { hold.PlacedAt = time.Now().UTC() } hold.Active = true m.holds[hold.ID] = hold return nil } // ReleaseLegalHold releases a legal hold. func (m *Manager) ReleaseLegalHold(holdID string) error { m.mu.Lock() defer m.mu.Unlock() hold, ok := m.holds[holdID] if !ok { return ErrPolicyNotFound } hold.Active = false return nil } // GetLegalHold retrieves a legal hold by ID. func (m *Manager) GetLegalHold(id string) (*LegalHold, error) { m.mu.RLock() defer m.mu.RUnlock() hold, ok := m.holds[id] if !ok { return nil, ErrPolicyNotFound } return hold, nil } // ListLegalHolds returns all legal holds. func (m *Manager) ListLegalHolds() []*LegalHold { m.mu.RLock() defer m.mu.RUnlock() holds := make([]*LegalHold, 0, len(m.holds)) for _, h := range m.holds { holds = append(holds, h) } return holds } // IsUnderLegalHold checks if data is under any active legal hold. func (m *Manager) IsUnderLegalHold(subjectID string, category DataCategory) bool { m.mu.RLock() defer m.mu.RUnlock() for _, hold := range m.holds { if hold.CoversData(subjectID, category) { return true } } return false } // ShouldDelete determines if a record should be deleted based on policies and holds. func (m *Manager) ShouldDelete(record *DataRecord) (bool, string) { // Check legal holds first if m.IsUnderLegalHold(record.SubjectID, record.Category) { return false, "under legal hold" } // Get applicable policy policy, err := m.GetPolicyForCategory(record.Category) if err != nil { return false, "no policy found" } if !policy.Active { return false, "policy inactive" } // Check if expired if policy.IsExpired(record.CreatedAt) { return true, "retention period expired" } return false, "within retention period" } // ProcessRecord processes a record according to retention policies. func (m *Manager) ProcessRecord(ctx context.Context, record *DataRecord) error { shouldDelete, reason := m.ShouldDelete(record) if !shouldDelete { return nil } // Get policy for archiving settings policy, _ := m.GetPolicyForCategory(record.Category) // Archive if configured if policy != nil && policy.ArchiveBeforeDelete { m.mu.RLock() archiveFn := m.onArchive m.mu.RUnlock() if archiveFn != nil { if err := archiveFn(record, policy.ArchivePath); err != nil { return fmt.Errorf("archive failed: %w", err) } } } // Delete m.mu.RLock() deleteFn := m.onDelete m.mu.RUnlock() if deleteFn != nil { if err := deleteFn(record); err != nil { return fmt.Errorf("delete failed (%s): %w", reason, err) } } return nil } // CreateErasureRequest creates a GDPR Art.17 erasure request for a data subject. // // This implements the "right to be forgotten" - EU citizens can request // deletion of all their personal data. // // The request is given a 30-day deadline per GDPR requirements. Processing // the request will delete all data for the subject EXCEPT: // - Data under legal hold // - Data required by law to retain (e.g., financial records) // // Parameters: // - subjectID: Unique identifier for the data subject (user) // - subjectEmail: Email for verification and notification // // Returns: // - ErasureRequest with PENDING status // - ErrErasureInProgress if another erasure is already processing for this subject // // Example: // // // User requests data deletion // req, err := manager.CreateErasureRequest("user-123", "user@example.com") // if err != nil { // return err // } // // fmt.Printf("Erasure request %s created\n", req.ID) // fmt.Printf("Deadline: %s (30 days)\n", req.Deadline) // // // Find all user's data // records := database.FindBySubject("user-123") // // // Process the erasure // if err := manager.ProcessErasure(ctx, req.ID, records); err != nil { // return err // } // // // Notify user of completion // if req.Status == retention.ErasureStatusCompleted { // notifyUser(req.SubjectEmail, "Your data has been deleted") // } // // Compliance: // GDPR Art.17 requires processing within 30 days without undue delay. func (m *Manager) CreateErasureRequest(subjectID, subjectEmail string) (*ErasureRequest, error) { m.mu.Lock() defer m.mu.Unlock() // Check if erasure already in progress for this subject for _, req := range m.erasures { if req.SubjectID == subjectID && req.Status == ErasureStatusInProgress { return nil, ErrErasureInProgress } } now := time.Now().UTC() req := &ErasureRequest{ ID: fmt.Sprintf("erasure-%d", now.UnixNano()), SubjectID: subjectID, SubjectEmail: subjectEmail, RequestedAt: now, Deadline: now.Add(30 * 24 * time.Hour), // GDPR: 30 days Status: ErasureStatusPending, } m.erasures[req.ID] = req return req, nil } // GetErasureRequest retrieves an erasure request by ID. func (m *Manager) GetErasureRequest(id string) (*ErasureRequest, error) { m.mu.RLock() defer m.mu.RUnlock() req, ok := m.erasures[id] if !ok { return nil, ErrPolicyNotFound } return req, nil } // ListErasureRequests returns all erasure requests. func (m *Manager) ListErasureRequests() []*ErasureRequest { m.mu.RLock() defer m.mu.RUnlock() reqs := make([]*ErasureRequest, 0, len(m.erasures)) for _, r := range m.erasures { reqs = append(reqs, r) } return reqs } // ProcessErasure processes an erasure request with the given records. func (m *Manager) ProcessErasure(ctx context.Context, requestID string, records []*DataRecord) error { m.mu.Lock() req, ok := m.erasures[requestID] if !ok { m.mu.Unlock() return ErrPolicyNotFound } req.Status = ErasureStatusInProgress req.StartedAt = time.Now().UTC() req.ItemsFound = len(records) m.mu.Unlock() erased := 0 retained := 0 var retainedReasons []string for _, record := range records { select { case <-ctx.Done(): m.updateErasureStatus(requestID, ErasureStatusFailed, erased, retained, "context cancelled", retainedReasons) return ctx.Err() default: } // Check legal holds if m.IsUnderLegalHold(record.SubjectID, record.Category) { retained++ retainedReasons = append(retainedReasons, fmt.Sprintf("%s: legal hold", record.ID)) continue } // Delete m.mu.RLock() deleteFn := m.onDelete m.mu.RUnlock() if deleteFn != nil { if err := deleteFn(record); err != nil { m.updateErasureStatus(requestID, ErasureStatusFailed, erased, retained, err.Error(), retainedReasons) return err } } erased++ } // Determine final status status := ErasureStatusCompleted if retained > 0 { status = ErasureStatusPartial } m.updateErasureStatus(requestID, status, erased, retained, "", retainedReasons) return nil } func (m *Manager) updateErasureStatus(requestID string, status ErasureStatus, erased, retained int, errMsg string, retainedReasons []string) { m.mu.Lock() defer m.mu.Unlock() req, ok := m.erasures[requestID] if !ok { return } req.Status = status req.ItemsErased = erased req.ItemsRetained = retained req.Error = errMsg if len(retainedReasons) > 0 { req.RetainedReason = fmt.Sprintf("%d items retained: %v", retained, retainedReasons) } if status == ErasureStatusCompleted || status == ErasureStatusPartial || status == ErasureStatusFailed { req.CompletedAt = time.Now().UTC() } } // SavePolicies saves all policies to a JSON file. func (m *Manager) SavePolicies(path string) error { m.mu.RLock() defer m.mu.RUnlock() policies := make([]*Policy, 0, len(m.policies)) for _, p := range m.policies { policies = append(policies, p) } data, err := json.MarshalIndent(policies, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } return os.WriteFile(path, data, 0644) } // LoadPolicies loads policies from a JSON file. func (m *Manager) LoadPolicies(path string) error { data, err := os.ReadFile(path) if err != nil { return err } var policies []*Policy if err := json.Unmarshal(data, &policies); err != nil { return err } m.mu.Lock() defer m.mu.Unlock() for _, p := range policies { if err := p.Validate(); err != nil { continue // Skip invalid } m.policies[p.ID] = p } return nil } // DefaultPolicies returns a set of pre-configured compliance-ready retention policies. // // These policies satisfy common regulatory requirements: // - Audit logs: 7 years (HIPAA, SOX, FISMA) // - PHI: 6 years (HIPAA §164.530(j)) // - PII: 3 years (GDPR data minimization) // - Financial: 7 years (SOX, IRS) // - User data: 1 year (reasonable default) // - Analytics: 90 days (short-term operational data) // - System: Indefinite (configuration data) // // Returns a slice of Policy objects ready to use with AddPolicy(). // // Example: // // manager := retention.NewManager() // // // Load all default policies // for _, policy := range retention.DefaultPolicies() { // if err := manager.AddPolicy(policy); err != nil { // log.Printf("Failed to add policy %s: %v", policy.Name, err) // } // } // // // Or selectively add policies // for _, policy := range retention.DefaultPolicies() { // if policy.Category == retention.CategoryPHI { // manager.AddPolicy(policy) // HIPAA compliance // } // } // // // Save policies for persistence // manager.SavePolicies("./config/retention-policies.json") // // Customization: // These are starting points. Adjust retention periods based on your: // - Industry regulations // - Geographic requirements // - Business needs // - Legal counsel recommendations // // Example 1 - Use All Default Policies: // // manager := retention.NewManager() // // // Add all pre-configured compliance policies // for _, policy := range retention.DefaultPolicies() { // if err := manager.AddPolicy(policy); err != nil { // log.Printf("Failed to add policy %s: %v", policy.ID, err) // } // } // // // Now manager has HIPAA, GDPR, SOX policies configured // // Example 2 - Selective Policy Usage: // // policies := retention.DefaultPolicies() // manager := retention.NewManager() // // // Only add policies relevant to your industry // for _, policy := range policies { // // Healthcare app - need HIPAA policies // if contains(policy.ComplianceFrameworks, "HIPAA") { // manager.AddPolicy(policy) // } // // // European app - need GDPR policies // if contains(policy.ComplianceFrameworks, "GDPR") { // manager.AddPolicy(policy) // } // } // // Example 3 - Customize Default Policies: // // policies := retention.DefaultPolicies() // manager := retention.NewManager() // // // Find and customize specific policy // for _, policy := range policies { // if policy.Category == retention.CategoryPII { // // Shorter retention for GDPR minimization // policy.RetentionPeriod.Duration = 1 * 365 * 24 * time.Hour // policy.Description = "Aggressive GDPR data minimization - 1 year" // } // manager.AddPolicy(policy) // } // // Example 4 - Override with Custom Policies: // // manager := retention.NewManager() // // // Start with defaults // for _, policy := range retention.DefaultPolicies() { // manager.AddPolicy(policy) // } // // // Add industry-specific policy // customPolicy := &retention.Policy{ // ID: "telemetry-30d", // Name: "Device Telemetry", // Category: retention.CategoryAnalytics, // RetentionPeriod: retention.RetentionPeriod{ // Duration: 30 * 24 * time.Hour, // }, // Active: true, // Description: "IoT device telemetry - 30 days", // } // manager.AddPolicy(customPolicy) // // ELI12: // // DefaultPolicies is like getting a starter rulebook for your library: // // "Here are the most common rules schools use:" // - Keep report cards for 7 years (important!) // - Keep homework for 1 year (useful) // - Throw away scratch paper after 3 months (not important) // - Keep textbooks forever (system data) // // Instead of making up all the rules yourself, you get a proven set // that follows the law! // // Included Policies: // // 1. **Audit Logs (7 years)** - audit-7y // - HIPAA §164.530(j), SOX §802, FISMA // - Archives before deletion // - Critical for compliance audits // // 2. **PHI/Health Data (6 years)** - phi-6y // - HIPAA §164.530(j) requirement // - Archives before deletion // - Protected health information // // 3. **PII/Personal Data (3 years)** - pii-gdpr // - GDPR Art.5(1)(e) minimization // - No archival (privacy-focused) // - Personally identifiable information // // 4. **Financial Records (7 years)** - financial-7y // - SOX §802, IRS requirements // - Archives before deletion // - Tax and audit compliance // // 5. **User Data (1 year)** - user-1y // - General user content // - No archival by default // - Configurable based on needs // // 6. **Analytics (90 days)** - analytics-90d // - Short-term metrics // - No archival // - Quick cleanup // // 7. **System Data (indefinite)** - system-indefinite // - Core configuration // - Never deleted // - Essential operations // // When to Modify: // - Your industry has stricter requirements // - Operating in specific jurisdictions (EU, US, etc.) // - Business needs longer/shorter retention // - Legal counsel recommends changes // // Compliance Coverage: // - GDPR (EU): ✓ Data minimization, erasure rights // - HIPAA (US Healthcare): ✓ 6-year PHI retention // - SOX (US Finance): ✓ 7-year financial records // - FISMA (US Federal): ✓ Audit log retention // // Performance: // - Returns static slice: O(1) // - 7 pre-configured policies // - No I/O or computation // // Thread Safety: // Returns new policy instances - safe to modify. func DefaultPolicies() []*Policy { now := time.Now().UTC() return []*Policy{ { ID: "audit-7y", Name: "Audit Logs (7 Years)", Category: CategoryAudit, RetentionPeriod: RetentionPeriod{ Duration: 7 * 365 * 24 * time.Hour, }, ArchiveBeforeDelete: true, ArchivePath: "/archive/audit", ComplianceFrameworks: []string{"HIPAA", "SOX", "FISMA"}, Active: true, CreatedAt: now, UpdatedAt: now, Description: "Retain audit logs for 7 years per HIPAA/SOX requirements", }, { ID: "phi-6y", Name: "PHI Retention (6 Years)", Category: CategoryPHI, RetentionPeriod: RetentionPeriod{ Duration: 6 * 365 * 24 * time.Hour, }, ArchiveBeforeDelete: true, ArchivePath: "/archive/phi", ComplianceFrameworks: []string{"HIPAA"}, Active: true, CreatedAt: now, UpdatedAt: now, Description: "Retain PHI for 6 years per HIPAA §164.530(j)", }, { ID: "pii-gdpr", Name: "PII (GDPR Minimization)", Category: CategoryPII, RetentionPeriod: RetentionPeriod{ Duration: 3 * 365 * 24 * time.Hour, // 3 years default }, ArchiveBeforeDelete: false, ComplianceFrameworks: []string{"GDPR"}, Active: true, CreatedAt: now, UpdatedAt: now, Description: "Retain PII only as long as necessary per GDPR Art.5(1)(e)", }, { ID: "financial-7y", Name: "Financial Records (7 Years)", Category: CategoryFinancial, RetentionPeriod: RetentionPeriod{ Duration: 7 * 365 * 24 * time.Hour, }, ArchiveBeforeDelete: true, ArchivePath: "/archive/financial", ComplianceFrameworks: []string{"SOX", "IRS"}, Active: true, CreatedAt: now, UpdatedAt: now, Description: "Retain financial records for 7 years per SOX/IRS requirements", }, { ID: "user-1y", Name: "User Data (1 Year)", Category: CategoryUser, RetentionPeriod: RetentionPeriod{ Duration: 365 * 24 * time.Hour, }, ArchiveBeforeDelete: false, Active: true, CreatedAt: now, UpdatedAt: now, Description: "Default user data retention - 1 year", }, { ID: "analytics-90d", Name: "Analytics (90 Days)", Category: CategoryAnalytics, RetentionPeriod: RetentionPeriod{ Duration: 90 * 24 * time.Hour, }, ArchiveBeforeDelete: false, Active: true, CreatedAt: now, UpdatedAt: now, Description: "Analytics data - 90 days", }, { ID: "system-indefinite", Name: "System Data", Category: CategorySystem, RetentionPeriod: RetentionPeriod{ Indefinite: true, }, Active: true, CreatedAt: now, UpdatedAt: now, Description: "System configuration and metadata - retain indefinitely", }, } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server