// Package report - Report generator package report import ( "context" "fmt" "time" "dbbackup/internal/catalog" ) // Generator generates compliance reports type Generator struct { catalog catalog.Catalog config ReportConfig } // NewGenerator creates a new report generator func NewGenerator(cat catalog.Catalog, config ReportConfig) *Generator { return &Generator{ catalog: cat, config: config, } } // Generate creates a compliance report func (g *Generator) Generate() (*Report, error) { report := CreatePeriodReport(g.config.Type, g.config.PeriodStart, g.config.PeriodEnd) report.Title = g.config.Title if g.config.Description != "" { report.Description = g.config.Description } // Collect evidence from catalog evidence, err := g.collectEvidence() if err != nil { return nil, fmt.Errorf("failed to collect evidence: %w", err) } for _, e := range evidence { report.AddEvidence(e) } // Evaluate controls if err := g.evaluateControls(report, evidence); err != nil { return nil, fmt.Errorf("failed to evaluate controls: %w", err) } // Calculate summary report.Calculate() return report, nil } // collectEvidence gathers evidence from the backup catalog func (g *Generator) collectEvidence() ([]Evidence, error) { var evidence []Evidence ctx := context.Background() // Get backup entries in the report period query := &catalog.SearchQuery{ StartDate: &g.config.PeriodStart, EndDate: &g.config.PeriodEnd, Limit: 1000, } entries, err := g.catalog.Search(ctx, query) if err != nil { return nil, fmt.Errorf("failed to search catalog: %w", err) } // Create evidence for backups for _, entry := range entries { e := Evidence{ ID: fmt.Sprintf("BKP-%d", entry.ID), Type: EvidenceBackupLog, Description: fmt.Sprintf("Backup of %s completed", entry.Database), Source: entry.BackupPath, CollectedAt: entry.CreatedAt, Data: map[string]interface{}{ "database": entry.Database, "database_type": entry.DatabaseType, "size": entry.SizeBytes, "sha256": entry.SHA256, "encrypted": entry.Encrypted, "compression": entry.Compression, "status": entry.Status, }, } if entry.SHA256 != "" { e.Hash = entry.SHA256 } evidence = append(evidence, e) // Add verification evidence if entry.VerifiedAt != nil { evidence = append(evidence, Evidence{ ID: fmt.Sprintf("VRF-%d", entry.ID), Type: EvidenceAuditLog, Description: fmt.Sprintf("Verification of backup %s", entry.BackupPath), Source: "verification_system", CollectedAt: *entry.VerifiedAt, Data: map[string]interface{}{ "backup_id": entry.ID, "database": entry.Database, "verified": true, }, }) } // Add drill evidence if entry.DrillTestedAt != nil { evidence = append(evidence, Evidence{ ID: fmt.Sprintf("DRL-%d", entry.ID), Type: EvidenceDrillResult, Description: fmt.Sprintf("DR drill test of backup %s", entry.BackupPath), Source: "drill_system", CollectedAt: *entry.DrillTestedAt, Data: map[string]interface{}{ "backup_id": entry.ID, "database": entry.Database, "passed": true, }, }) } // Add encryption evidence if entry.Encrypted { encryption := "AES-256" if meta, ok := entry.Metadata["encryption_method"]; ok { encryption = meta } evidence = append(evidence, Evidence{ ID: fmt.Sprintf("ENC-%d", entry.ID), Type: EvidenceEncryptionProof, Description: fmt.Sprintf("Encrypted backup %s", entry.BackupPath), Source: entry.BackupPath, CollectedAt: entry.CreatedAt, Data: map[string]interface{}{ "backup_id": entry.ID, "database": entry.Database, "encryption": encryption, }, }) } } // Get catalog statistics for retention evidence stats, err := g.catalog.Stats(ctx) if err == nil { evidence = append(evidence, Evidence{ ID: "RET-STATS", Type: EvidenceRetentionProof, Description: "Backup retention statistics", Source: "catalog", CollectedAt: time.Now(), Data: map[string]interface{}{ "total_backups": stats.TotalBackups, "oldest_backup": stats.OldestBackup, "newest_backup": stats.NewestBackup, "average_size": stats.AvgSize, "total_size": stats.TotalSize, "databases": len(stats.ByDatabase), }, }) } // Check for gaps gapConfig := &catalog.GapDetectionConfig{ ExpectedInterval: 24 * time.Hour, Tolerance: 2 * time.Hour, StartDate: &g.config.PeriodStart, EndDate: &g.config.PeriodEnd, } allGaps, err := g.catalog.DetectAllGaps(ctx, gapConfig) if err == nil { totalGaps := 0 for _, gaps := range allGaps { totalGaps += len(gaps) } if totalGaps > 0 { evidence = append(evidence, Evidence{ ID: "GAP-ANALYSIS", Type: EvidenceAuditLog, Description: "Backup gap analysis", Source: "catalog", CollectedAt: time.Now(), Data: map[string]interface{}{ "gaps_detected": totalGaps, "gaps": allGaps, }, }) } } return evidence, nil } // evaluateControls evaluates compliance controls based on evidence func (g *Generator) evaluateControls(report *Report, evidence []Evidence) error { // Index evidence by type for quick lookup evidenceByType := make(map[EvidenceType][]Evidence) for _, e := range evidence { evidenceByType[e.Type] = append(evidenceByType[e.Type], e) } // Get backup statistics backupEvidence := evidenceByType[EvidenceBackupLog] encryptionEvidence := evidenceByType[EvidenceEncryptionProof] drillEvidence := evidenceByType[EvidenceDrillResult] verificationEvidence := evidenceByType[EvidenceAuditLog] // Evaluate each control for i := range report.Categories { cat := &report.Categories[i] catCompliant := 0 catTotal := 0 for j := range cat.Controls { ctrl := &cat.Controls[j] ctrl.LastChecked = time.Now() catTotal++ // Evaluate based on control type status, notes, evidenceIDs := g.evaluateControl(ctrl, backupEvidence, encryptionEvidence, drillEvidence, verificationEvidence) ctrl.Status = status ctrl.Notes = notes ctrl.Evidence = evidenceIDs if status == StatusCompliant { catCompliant++ } else if status != StatusNotApplicable { // Create finding for non-compliant controls finding := g.createFinding(ctrl, report) if finding != nil { report.AddFinding(*finding) ctrl.Findings = append(ctrl.Findings, finding.ID) } } } // Calculate category score if catTotal > 0 { cat.Score = float64(catCompliant) / float64(catTotal) * 100 if cat.Score >= 100 { cat.Status = StatusCompliant } else if cat.Score >= 70 { cat.Status = StatusPartial } else { cat.Status = StatusNonCompliant } } } return nil } // evaluateControl evaluates a single control func (g *Generator) evaluateControl(ctrl *Control, backups, encryption, drills, verifications []Evidence) (ComplianceStatus, string, []string) { var evidenceIDs []string switch ctrl.ID { // SOC2 Controls case "CC6.1", "GDPR-32", "164.312a2iv", "PCI-3.4", "A.10.1.1": // Encryption at rest if len(encryption) == 0 { return StatusNonCompliant, "No encrypted backups found", nil } encryptedCount := len(encryption) totalCount := len(backups) if totalCount == 0 { return StatusNotApplicable, "No backups in period", nil } rate := float64(encryptedCount) / float64(totalCount) * 100 for _, e := range encryption { evidenceIDs = append(evidenceIDs, e.ID) } if rate >= 100 { return StatusCompliant, fmt.Sprintf("100%% of backups encrypted (%d/%d)", encryptedCount, totalCount), evidenceIDs } if rate >= 90 { return StatusPartial, fmt.Sprintf("%.1f%% of backups encrypted (%d/%d)", rate, encryptedCount, totalCount), evidenceIDs } return StatusNonCompliant, fmt.Sprintf("Only %.1f%% of backups encrypted", rate), evidenceIDs case "A1.1", "164.308a7iA", "A.12.3.1": // Backup policy/plan if len(backups) == 0 { return StatusNonCompliant, "No backups found in period", nil } for _, e := range backups[:min(5, len(backups))] { evidenceIDs = append(evidenceIDs, e.ID) } return StatusCompliant, fmt.Sprintf("%d backups created in period", len(backups)), evidenceIDs case "A1.2", "164.308a7iD", "A.17.1.3": // Backup testing if len(drills) == 0 { return StatusNonCompliant, "No DR drill tests performed", nil } for _, e := range drills { evidenceIDs = append(evidenceIDs, e.ID) } return StatusCompliant, fmt.Sprintf("%d DR drill tests completed", len(drills)), evidenceIDs case "A1.3", "A1.4", "164.308a7iB", "A.17.1.1", "A.17.1.2", "PCI-12.10.1": // DR procedures if len(drills) > 0 { for _, e := range drills { evidenceIDs = append(evidenceIDs, e.ID) } return StatusCompliant, "DR procedures tested", evidenceIDs } return StatusPartial, "DR procedures exist but not tested", nil case "PI1.1", "164.312c1": // Data integrity integrityCount := 0 for _, e := range backups { if data, ok := e.Data.(map[string]interface{}); ok { if checksum, ok := data["checksum"].(string); ok && checksum != "" { integrityCount++ evidenceIDs = append(evidenceIDs, e.ID) } } } if integrityCount == len(backups) && len(backups) > 0 { return StatusCompliant, "All backups have integrity checksums", evidenceIDs } if integrityCount > 0 { return StatusPartial, fmt.Sprintf("%d/%d backups have checksums", integrityCount, len(backups)), evidenceIDs } return StatusNonCompliant, "No integrity checksums found", nil case "C1.2", "GDPR-5.1e", "PCI-3.1": // Data retention for _, e := range verifications { if e.Type == EvidenceRetentionProof { evidenceIDs = append(evidenceIDs, e.ID) } } if len(backups) > 0 { return StatusCompliant, "Retention policy in effect", evidenceIDs } return StatusPartial, "Retention policy needs review", nil default: // Generic evaluation if len(backups) > 0 { return StatusCompliant, "Evidence available", nil } return StatusUnknown, "Requires manual review", nil } } // createFinding creates a finding for a non-compliant control func (g *Generator) createFinding(ctrl *Control, report *Report) *Finding { if ctrl.Status == StatusCompliant || ctrl.Status == StatusNotApplicable { return nil } severity := SeverityMedium findingType := FindingGap // Determine severity based on control switch ctrl.ID { case "CC6.1", "164.312a2iv", "PCI-3.4": severity = SeverityHigh findingType = FindingViolation case "A1.2", "164.308a7iD": severity = SeverityMedium findingType = FindingGap } return &Finding{ ID: fmt.Sprintf("FND-%s-%d", ctrl.ID, time.Now().UnixNano()), ControlID: ctrl.ID, Type: findingType, Severity: severity, Title: fmt.Sprintf("%s: %s", ctrl.Reference, ctrl.Name), Description: ctrl.Notes, Impact: fmt.Sprintf("Non-compliance with %s requirements", report.Type), Recommendation: g.getRecommendation(ctrl.ID), Status: FindingOpen, DetectedAt: time.Now(), Evidence: ctrl.Evidence, } } // getRecommendation returns remediation recommendation for a control func (g *Generator) getRecommendation(controlID string) string { recommendations := map[string]string{ "CC6.1": "Enable encryption for all backups using AES-256", "CC6.7": "Ensure all backup transfers use TLS", "A1.1": "Establish and document backup schedule", "A1.2": "Schedule and perform regular DR drill tests", "A1.3": "Document and test recovery procedures", "A1.4": "Develop and test disaster recovery plan", "PI1.1": "Enable checksum verification for all backups", "C1.2": "Implement and document retention policies", "164.312a2iv": "Enable HIPAA-compliant encryption (AES-256)", "164.308a7iD": "Test backup recoverability quarterly", "PCI-3.4": "Encrypt all backups containing cardholder data", } if rec, ok := recommendations[controlID]; ok { return rec } return "Review and remediate as per compliance requirements" } func min(a, b int) int { if a < b { return a } return b }