feat: Add enterprise DBA features for production reliability

New features implemented:

1. Backup Catalog (internal/catalog/)
   - SQLite-based backup tracking
   - Gap detection and RPO monitoring
   - Search and statistics
   - Filesystem sync

2. DR Drill Testing (internal/drill/)
   - Automated restore testing in Docker containers
   - Database validation with custom queries
   - Catalog integration for drill-tested status

3. Smart Notifications (internal/notify/)
   - Event batching with configurable intervals
   - Time-based escalation policies
   - HTML/text/Slack templates

4. Compliance Reports (internal/report/)
   - SOC2, GDPR, HIPAA, PCI-DSS, ISO27001 frameworks
   - Evidence collection from catalog
   - JSON, Markdown, HTML output formats

5. RTO/RPO Calculator (internal/rto/)
   - Recovery objective analysis
   - RTO breakdown by phase
   - Recommendations for improvement

6. Replica-Aware Backup (internal/replica/)
   - Topology detection for PostgreSQL/MySQL
   - Automatic replica selection
   - Configurable selection strategies

7. Parallel Table Backup (internal/parallel/)
   - Concurrent table dumps
   - Worker pool with progress tracking
   - Large table optimization

8. MySQL/MariaDB PITR (internal/pitr/)
   - Binary log parsing and replay
   - Point-in-time recovery support
   - Transaction filtering

CLI commands added: catalog, drill, report, rto

All changes support the goal: reliable 3 AM database recovery.
This commit is contained in:
2025-12-13 20:28:55 +01:00
parent d0d83b61ef
commit f69bfe7071
34 changed files with 13469 additions and 41 deletions

View File

@@ -0,0 +1,424 @@
// Package report - SOC2 framework controls
package report
import (
"time"
)
// SOC2Framework returns SOC2 Trust Service Criteria controls
func SOC2Framework() []Category {
return []Category{
soc2Security(),
soc2Availability(),
soc2ProcessingIntegrity(),
soc2Confidentiality(),
}
}
func soc2Security() Category {
return Category{
ID: "soc2-security",
Name: "Security",
Description: "Protection of system resources against unauthorized access",
Weight: 1.0,
Controls: []Control{
{
ID: "CC6.1",
Reference: "SOC2 CC6.1",
Name: "Encryption at Rest",
Description: "Data is protected at rest using encryption",
},
{
ID: "CC6.7",
Reference: "SOC2 CC6.7",
Name: "Encryption in Transit",
Description: "Data is protected in transit using encryption",
},
{
ID: "CC6.2",
Reference: "SOC2 CC6.2",
Name: "Access Control",
Description: "Logical access to data and system components is restricted",
},
{
ID: "CC6.3",
Reference: "SOC2 CC6.3",
Name: "Authorized Access",
Description: "Only authorized users can access data and systems",
},
},
}
}
func soc2Availability() Category {
return Category{
ID: "soc2-availability",
Name: "Availability",
Description: "System availability for operation and use as agreed",
Weight: 1.0,
Controls: []Control{
{
ID: "A1.1",
Reference: "SOC2 A1.1",
Name: "Backup Policy",
Description: "Backup policies and procedures are established and operating",
},
{
ID: "A1.2",
Reference: "SOC2 A1.2",
Name: "Backup Testing",
Description: "Backups are tested for recoverability",
},
{
ID: "A1.3",
Reference: "SOC2 A1.3",
Name: "Recovery Procedures",
Description: "Recovery procedures are documented and tested",
},
{
ID: "A1.4",
Reference: "SOC2 A1.4",
Name: "Disaster Recovery",
Description: "DR plans are maintained and tested",
},
},
}
}
func soc2ProcessingIntegrity() Category {
return Category{
ID: "soc2-processing-integrity",
Name: "Processing Integrity",
Description: "System processing is complete, valid, accurate, timely, and authorized",
Weight: 0.75,
Controls: []Control{
{
ID: "PI1.1",
Reference: "SOC2 PI1.1",
Name: "Data Integrity",
Description: "Checksums and verification ensure data integrity",
},
{
ID: "PI1.2",
Reference: "SOC2 PI1.2",
Name: "Error Handling",
Description: "Errors are identified and corrected in a timely manner",
},
},
}
}
func soc2Confidentiality() Category {
return Category{
ID: "soc2-confidentiality",
Name: "Confidentiality",
Description: "Information designated as confidential is protected",
Weight: 1.0,
Controls: []Control{
{
ID: "C1.1",
Reference: "SOC2 C1.1",
Name: "Data Classification",
Description: "Confidential data is identified and classified",
},
{
ID: "C1.2",
Reference: "SOC2 C1.2",
Name: "Data Retention",
Description: "Data retention policies are implemented",
},
{
ID: "C1.3",
Reference: "SOC2 C1.3",
Name: "Data Disposal",
Description: "Data is securely disposed when no longer needed",
},
},
}
}
// GDPRFramework returns GDPR-related controls
func GDPRFramework() []Category {
return []Category{
{
ID: "gdpr-data-protection",
Name: "Data Protection",
Description: "Protection of personal data",
Weight: 1.0,
Controls: []Control{
{
ID: "GDPR-25",
Reference: "GDPR Article 25",
Name: "Data Protection by Design",
Description: "Data protection measures are implemented by design",
},
{
ID: "GDPR-32",
Reference: "GDPR Article 32",
Name: "Security of Processing",
Description: "Appropriate technical measures to ensure data security",
},
{
ID: "GDPR-33",
Reference: "GDPR Article 33",
Name: "Breach Notification",
Description: "Procedures for breach detection and notification",
},
},
},
{
ID: "gdpr-data-retention",
Name: "Data Retention",
Description: "Lawful data retention practices",
Weight: 1.0,
Controls: []Control{
{
ID: "GDPR-5.1e",
Reference: "GDPR Article 5(1)(e)",
Name: "Storage Limitation",
Description: "Personal data not kept longer than necessary",
},
{
ID: "GDPR-17",
Reference: "GDPR Article 17",
Name: "Right to Erasure",
Description: "Ability to delete personal data on request",
},
},
},
}
}
// HIPAAFramework returns HIPAA-related controls
func HIPAAFramework() []Category {
return []Category{
{
ID: "hipaa-administrative",
Name: "Administrative Safeguards",
Description: "Administrative policies and procedures",
Weight: 1.0,
Controls: []Control{
{
ID: "164.308a7",
Reference: "HIPAA 164.308(a)(7)",
Name: "Contingency Plan",
Description: "Data backup and disaster recovery procedures",
},
{
ID: "164.308a7iA",
Reference: "HIPAA 164.308(a)(7)(ii)(A)",
Name: "Data Backup Plan",
Description: "Procedures for retrievable exact copies of ePHI",
},
{
ID: "164.308a7iB",
Reference: "HIPAA 164.308(a)(7)(ii)(B)",
Name: "Disaster Recovery Plan",
Description: "Procedures to restore any loss of data",
},
{
ID: "164.308a7iD",
Reference: "HIPAA 164.308(a)(7)(ii)(D)",
Name: "Testing and Revision",
Description: "Testing of contingency plans",
},
},
},
{
ID: "hipaa-technical",
Name: "Technical Safeguards",
Description: "Technical security measures",
Weight: 1.0,
Controls: []Control{
{
ID: "164.312a2iv",
Reference: "HIPAA 164.312(a)(2)(iv)",
Name: "Encryption",
Description: "Encryption of ePHI",
},
{
ID: "164.312c1",
Reference: "HIPAA 164.312(c)(1)",
Name: "Integrity Controls",
Description: "Mechanisms to ensure ePHI is not improperly altered",
},
{
ID: "164.312e1",
Reference: "HIPAA 164.312(e)(1)",
Name: "Transmission Security",
Description: "Technical measures to guard against unauthorized access",
},
},
},
}
}
// PCIDSSFramework returns PCI-DSS related controls
func PCIDSSFramework() []Category {
return []Category{
{
ID: "pci-protect",
Name: "Protect Stored Data",
Description: "Protect stored cardholder data",
Weight: 1.0,
Controls: []Control{
{
ID: "PCI-3.1",
Reference: "PCI-DSS 3.1",
Name: "Data Retention Policy",
Description: "Retention policy limits storage time",
},
{
ID: "PCI-3.4",
Reference: "PCI-DSS 3.4",
Name: "Encryption",
Description: "Render PAN unreadable anywhere it is stored",
},
{
ID: "PCI-3.5",
Reference: "PCI-DSS 3.5",
Name: "Key Management",
Description: "Protect cryptographic keys",
},
},
},
{
ID: "pci-maintain",
Name: "Maintain Security",
Description: "Maintain security policies and procedures",
Weight: 1.0,
Controls: []Control{
{
ID: "PCI-12.10.1",
Reference: "PCI-DSS 12.10.1",
Name: "Incident Response Plan",
Description: "Incident response plan includes data recovery",
},
},
},
}
}
// ISO27001Framework returns ISO 27001 related controls
func ISO27001Framework() []Category {
return []Category{
{
ID: "iso-operations",
Name: "Operations Security",
Description: "A.12 Operations Security controls",
Weight: 1.0,
Controls: []Control{
{
ID: "A.12.3.1",
Reference: "ISO 27001 A.12.3.1",
Name: "Information Backup",
Description: "Backup copies taken and tested regularly",
},
},
},
{
ID: "iso-continuity",
Name: "Business Continuity",
Description: "A.17 Business Continuity controls",
Weight: 1.0,
Controls: []Control{
{
ID: "A.17.1.1",
Reference: "ISO 27001 A.17.1.1",
Name: "Planning Continuity",
Description: "Information security continuity planning",
},
{
ID: "A.17.1.2",
Reference: "ISO 27001 A.17.1.2",
Name: "Implementing Continuity",
Description: "Implementation of security continuity",
},
{
ID: "A.17.1.3",
Reference: "ISO 27001 A.17.1.3",
Name: "Verify and Review",
Description: "Verify and review continuity controls",
},
},
},
{
ID: "iso-cryptography",
Name: "Cryptography",
Description: "A.10 Cryptographic controls",
Weight: 1.0,
Controls: []Control{
{
ID: "A.10.1.1",
Reference: "ISO 27001 A.10.1.1",
Name: "Cryptographic Controls",
Description: "Policy on use of cryptographic controls",
},
{
ID: "A.10.1.2",
Reference: "ISO 27001 A.10.1.2",
Name: "Key Management",
Description: "Policy on cryptographic key management",
},
},
},
}
}
// GetFramework returns the appropriate framework for a report type
func GetFramework(reportType ReportType) []Category {
switch reportType {
case ReportSOC2:
return SOC2Framework()
case ReportGDPR:
return GDPRFramework()
case ReportHIPAA:
return HIPAAFramework()
case ReportPCIDSS:
return PCIDSSFramework()
case ReportISO27001:
return ISO27001Framework()
default:
return nil
}
}
// CreatePeriodReport creates a report for a specific time period
func CreatePeriodReport(reportType ReportType, start, end time.Time) *Report {
title := ""
desc := ""
switch reportType {
case ReportSOC2:
title = "SOC 2 Type II Compliance Report"
desc = "Trust Service Criteria compliance assessment"
case ReportGDPR:
title = "GDPR Data Protection Compliance Report"
desc = "General Data Protection Regulation compliance assessment"
case ReportHIPAA:
title = "HIPAA Security Compliance Report"
desc = "Health Insurance Portability and Accountability Act compliance assessment"
case ReportPCIDSS:
title = "PCI-DSS Compliance Report"
desc = "Payment Card Industry Data Security Standard compliance assessment"
case ReportISO27001:
title = "ISO 27001 Compliance Report"
desc = "Information Security Management System compliance assessment"
default:
title = "Custom Compliance Report"
desc = "Custom compliance assessment"
}
report := NewReport(reportType, title)
report.Description = desc
report.PeriodStart = start
report.PeriodEnd = end
// Load framework controls
framework := GetFramework(reportType)
for _, cat := range framework {
report.AddCategory(cat)
}
return report
}

View File

@@ -0,0 +1,420 @@
// 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
}

544
internal/report/output.go Normal file
View File

@@ -0,0 +1,544 @@
// Package report - Output formatters
package report
import (
"encoding/json"
"fmt"
"io"
"strings"
"text/template"
"time"
)
// Formatter formats reports for output
type Formatter interface {
Format(report *Report, w io.Writer) error
}
// JSONFormatter formats reports as JSON
type JSONFormatter struct {
Indent bool
}
// Format writes the report as JSON
func (f *JSONFormatter) Format(report *Report, w io.Writer) error {
var data []byte
var err error
if f.Indent {
data, err = json.MarshalIndent(report, "", " ")
} else {
data, err = json.Marshal(report)
}
if err != nil {
return err
}
_, err = w.Write(data)
return err
}
// MarkdownFormatter formats reports as Markdown
type MarkdownFormatter struct{}
// Format writes the report as Markdown
func (f *MarkdownFormatter) Format(report *Report, w io.Writer) error {
tmpl := template.Must(template.New("report").Funcs(template.FuncMap{
"statusIcon": StatusIcon,
"severityIcon": SeverityIcon,
"formatTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") },
"formatDate": func(t time.Time) string { return t.Format("2006-01-02") },
"upper": strings.ToUpper,
}).Parse(markdownTemplate))
return tmpl.Execute(w, report)
}
const markdownTemplate = `# {{.Title}}
**Generated:** {{formatTime .GeneratedAt}}
**Period:** {{formatDate .PeriodStart}} to {{formatDate .PeriodEnd}}
**Overall Status:** {{statusIcon .Status}} {{.Status}}
**Compliance Score:** {{printf "%.1f" .Score}}%
---
## Executive Summary
{{.Description}}
| Metric | Value |
|--------|-------|
| Total Controls | {{.Summary.TotalControls}} |
| Compliant | {{.Summary.CompliantControls}} |
| Non-Compliant | {{.Summary.NonCompliantControls}} |
| Partial | {{.Summary.PartialControls}} |
| Compliance Rate | {{printf "%.1f" .Summary.ComplianceRate}}% |
| Open Findings | {{.Summary.OpenFindings}} |
| Risk Score | {{printf "%.1f" .Summary.RiskScore}} |
---
## Compliance Categories
{{range .Categories}}
### {{statusIcon .Status}} {{.Name}}
**Status:** {{.Status}} | **Score:** {{printf "%.1f" .Score}}%
{{.Description}}
| Control | Reference | Status | Notes |
|---------|-----------|--------|-------|
{{range .Controls}}| {{.Name}} | {{.Reference}} | {{statusIcon .Status}} | {{.Notes}} |
{{end}}
{{end}}
---
## Findings
{{if .Findings}}
| ID | Severity | Title | Status |
|----|----------|-------|--------|
{{range .Findings}}| {{.ID}} | {{severityIcon .Severity}} {{.Severity}} | {{.Title}} | {{.Status}} |
{{end}}
### Finding Details
{{range .Findings}}
#### {{severityIcon .Severity}} {{.Title}}
- **ID:** {{.ID}}
- **Control:** {{.ControlID}}
- **Severity:** {{.Severity}}
- **Type:** {{.Type}}
- **Status:** {{.Status}}
- **Detected:** {{formatTime .DetectedAt}}
**Description:** {{.Description}}
**Impact:** {{.Impact}}
**Recommendation:** {{.Recommendation}}
---
{{end}}
{{else}}
No open findings.
{{end}}
---
## Evidence Summary
{{if .Evidence}}
| ID | Type | Description | Collected |
|----|------|-------------|-----------|
{{range .Evidence}}| {{.ID}} | {{.Type}} | {{.Description}} | {{formatTime .CollectedAt}} |
{{end}}
{{else}}
No evidence collected.
{{end}}
---
*Report generated by dbbackup compliance module*
`
// HTMLFormatter formats reports as HTML
type HTMLFormatter struct{}
// Format writes the report as HTML
func (f *HTMLFormatter) Format(report *Report, w io.Writer) error {
tmpl := template.Must(template.New("report").Funcs(template.FuncMap{
"statusIcon": StatusIcon,
"statusClass": statusClass,
"severityIcon": SeverityIcon,
"severityClass": severityClass,
"formatTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") },
"formatDate": func(t time.Time) string { return t.Format("2006-01-02") },
}).Parse(htmlTemplate))
return tmpl.Execute(w, report)
}
func statusClass(s ComplianceStatus) string {
switch s {
case StatusCompliant:
return "status-compliant"
case StatusNonCompliant:
return "status-noncompliant"
case StatusPartial:
return "status-partial"
default:
return "status-unknown"
}
}
func severityClass(s FindingSeverity) string {
switch s {
case SeverityCritical:
return "severity-critical"
case SeverityHigh:
return "severity-high"
case SeverityMedium:
return "severity-medium"
case SeverityLow:
return "severity-low"
default:
return "severity-unknown"
}
}
const htmlTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<style>
:root {
--color-compliant: #27ae60;
--color-noncompliant: #e74c3c;
--color-partial: #f39c12;
--color-unknown: #95a5a6;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.report-header {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.report-header h1 {
margin: 0 0 10px 0;
color: #2c3e50;
}
.report-meta {
color: #7f8c8d;
font-size: 0.9em;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-weight: bold;
font-size: 0.85em;
}
.status-compliant { background: var(--color-compliant); color: white; }
.status-noncompliant { background: var(--color-noncompliant); color: white; }
.status-partial { background: var(--color-partial); color: white; }
.status-unknown { background: var(--color-unknown); color: white; }
.score-display {
font-size: 48px;
font-weight: bold;
color: #2c3e50;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin: 20px 0;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.summary-card .value {
font-size: 32px;
font-weight: bold;
color: #2c3e50;
}
.summary-card .label {
color: #7f8c8d;
font-size: 0.9em;
}
.section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.section h2 {
margin-top: 0;
color: #2c3e50;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.category {
margin-bottom: 30px;
}
.category h3 {
display: flex;
align-items: center;
gap: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ecf0f1;
}
th {
background: #f8f9fa;
font-weight: 600;
}
tr:hover {
background: #f8f9fa;
}
.finding-card {
border-left: 4px solid var(--color-unknown);
padding: 15px;
margin: 15px 0;
background: #f8f9fa;
border-radius: 0 8px 8px 0;
}
.finding-card.severity-critical { border-color: #e74c3c; }
.finding-card.severity-high { border-color: #e67e22; }
.finding-card.severity-medium { border-color: #f39c12; }
.finding-card.severity-low { border-color: #27ae60; }
.finding-card h4 {
margin: 0 0 10px 0;
}
.finding-meta {
font-size: 0.85em;
color: #7f8c8d;
}
.progress-bar {
height: 8px;
background: #ecf0f1;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-compliant);
transition: width 0.3s ease;
}
.footer {
text-align: center;
color: #7f8c8d;
padding: 20px;
font-size: 0.85em;
}
</style>
</head>
<body>
<div class="report-header">
<h1>{{.Title}}</h1>
<p class="report-meta">
Generated: {{formatTime .GeneratedAt}} |
Period: {{formatDate .PeriodStart}} to {{formatDate .PeriodEnd}}
</p>
<div style="display: flex; align-items: center; gap: 20px; margin-top: 20px;">
<div class="score-display">{{printf "%.0f" .Score}}%</div>
<div>
<span class="status-badge {{statusClass .Status}}">{{.Status}}</span>
<p style="margin: 10px 0 0 0; color: #7f8c8d;">{{.Description}}</p>
</div>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<div class="value">{{.Summary.TotalControls}}</div>
<div class="label">Total Controls</div>
</div>
<div class="summary-card">
<div class="value" style="color: var(--color-compliant);">{{.Summary.CompliantControls}}</div>
<div class="label">Compliant</div>
</div>
<div class="summary-card">
<div class="value" style="color: var(--color-noncompliant);">{{.Summary.NonCompliantControls}}</div>
<div class="label">Non-Compliant</div>
</div>
<div class="summary-card">
<div class="value" style="color: var(--color-partial);">{{.Summary.PartialControls}}</div>
<div class="label">Partial</div>
</div>
<div class="summary-card">
<div class="value">{{.Summary.OpenFindings}}</div>
<div class="label">Open Findings</div>
</div>
<div class="summary-card">
<div class="value">{{printf "%.1f" .Summary.RiskScore}}</div>
<div class="label">Risk Score</div>
</div>
</div>
<div class="section">
<h2>Compliance Categories</h2>
{{range .Categories}}
<div class="category">
<h3>
{{statusIcon .Status}} {{.Name}}
<span class="status-badge {{statusClass .Status}}">{{printf "%.0f" .Score}}%</span>
</h3>
<p style="color: #7f8c8d;">{{.Description}}</p>
<div class="progress-bar">
<div class="progress-fill" style="width: {{printf "%.0f" .Score}}%;"></div>
</div>
<table>
<thead>
<tr>
<th>Control</th>
<th>Reference</th>
<th>Status</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{{range .Controls}}
<tr>
<td>{{.Name}}</td>
<td>{{.Reference}}</td>
<td>{{statusIcon .Status}}</td>
<td>{{.Notes}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
{{if .Findings}}
<div class="section">
<h2>Findings ({{len .Findings}})</h2>
{{range .Findings}}
<div class="finding-card {{severityClass .Severity}}">
<h4>{{severityIcon .Severity}} {{.Title}}</h4>
<div class="finding-meta">
<strong>ID:</strong> {{.ID}} |
<strong>Severity:</strong> {{.Severity}} |
<strong>Status:</strong> {{.Status}} |
<strong>Detected:</strong> {{formatTime .DetectedAt}}
</div>
<p><strong>Description:</strong> {{.Description}}</p>
<p><strong>Impact:</strong> {{.Impact}}</p>
<p><strong>Recommendation:</strong> {{.Recommendation}}</p>
</div>
{{end}}
</div>
{{end}}
{{if .Evidence}}
<div class="section">
<h2>Evidence ({{len .Evidence}} items)</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Description</th>
<th>Collected</th>
</tr>
</thead>
<tbody>
{{range .Evidence}}
<tr>
<td>{{.ID}}</td>
<td>{{.Type}}</td>
<td>{{.Description}}</td>
<td>{{formatTime .CollectedAt}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<div class="footer">
<p>Report generated by dbbackup compliance module</p>
<p>Report ID: {{.ID}}</p>
</div>
</body>
</html>`
// GetFormatter returns a formatter for the given format
func GetFormatter(format OutputFormat) Formatter {
switch format {
case FormatJSON:
return &JSONFormatter{Indent: true}
case FormatMarkdown:
return &MarkdownFormatter{}
case FormatHTML:
return &HTMLFormatter{}
default:
return &JSONFormatter{Indent: true}
}
}
// ConsoleFormatter formats reports for terminal output
type ConsoleFormatter struct{}
// Format writes the report to console
func (f *ConsoleFormatter) Format(report *Report, w io.Writer) error {
// Header
fmt.Fprintf(w, "\n%s\n", strings.Repeat("=", 60))
fmt.Fprintf(w, " %s\n", report.Title)
fmt.Fprintf(w, "%s\n\n", strings.Repeat("=", 60))
fmt.Fprintf(w, " Generated: %s\n", report.GeneratedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(w, " Period: %s to %s\n",
report.PeriodStart.Format("2006-01-02"),
report.PeriodEnd.Format("2006-01-02"))
fmt.Fprintf(w, " Status: %s %s\n", StatusIcon(report.Status), report.Status)
fmt.Fprintf(w, " Score: %.1f%%\n\n", report.Score)
// Summary
fmt.Fprintf(w, " SUMMARY\n")
fmt.Fprintf(w, " %s\n", strings.Repeat("-", 40))
fmt.Fprintf(w, " Controls: %d total, %d compliant, %d non-compliant\n",
report.Summary.TotalControls,
report.Summary.CompliantControls,
report.Summary.NonCompliantControls)
fmt.Fprintf(w, " Compliance: %.1f%%\n", report.Summary.ComplianceRate)
fmt.Fprintf(w, " Open Findings: %d (critical: %d, high: %d)\n",
report.Summary.OpenFindings,
report.Summary.CriticalFindings,
report.Summary.HighFindings)
fmt.Fprintf(w, " Risk Score: %.1f\n\n", report.Summary.RiskScore)
// Categories
fmt.Fprintf(w, " CATEGORIES\n")
fmt.Fprintf(w, " %s\n", strings.Repeat("-", 40))
for _, cat := range report.Categories {
fmt.Fprintf(w, " %s %-25s %.0f%%\n", StatusIcon(cat.Status), cat.Name, cat.Score)
}
fmt.Fprintln(w)
// Findings
if len(report.Findings) > 0 {
fmt.Fprintf(w, " FINDINGS\n")
fmt.Fprintf(w, " %s\n", strings.Repeat("-", 40))
for _, f := range report.Findings {
fmt.Fprintf(w, " %s [%s] %s\n", SeverityIcon(f.Severity), f.Severity, f.Title)
fmt.Fprintf(w, " %s\n", f.Description)
}
fmt.Fprintln(w)
}
fmt.Fprintf(w, "%s\n", strings.Repeat("=", 60))
return nil
}

325
internal/report/report.go Normal file
View File

@@ -0,0 +1,325 @@
// Package report provides compliance report generation
package report
import (
"encoding/json"
"fmt"
"time"
)
// ReportType represents the compliance framework type
type ReportType string
const (
ReportSOC2 ReportType = "soc2"
ReportGDPR ReportType = "gdpr"
ReportHIPAA ReportType = "hipaa"
ReportPCIDSS ReportType = "pci-dss"
ReportISO27001 ReportType = "iso27001"
ReportCustom ReportType = "custom"
)
// ComplianceStatus represents the status of a compliance check
type ComplianceStatus string
const (
StatusCompliant ComplianceStatus = "compliant"
StatusNonCompliant ComplianceStatus = "non_compliant"
StatusPartial ComplianceStatus = "partial"
StatusNotApplicable ComplianceStatus = "not_applicable"
StatusUnknown ComplianceStatus = "unknown"
)
// Report represents a compliance report
type Report struct {
ID string `json:"id"`
Type ReportType `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
GeneratedAt time.Time `json:"generated_at"`
GeneratedBy string `json:"generated_by"`
PeriodStart time.Time `json:"period_start"`
PeriodEnd time.Time `json:"period_end"`
Status ComplianceStatus `json:"overall_status"`
Score float64 `json:"score"` // 0-100
Categories []Category `json:"categories"`
Summary Summary `json:"summary"`
Findings []Finding `json:"findings"`
Evidence []Evidence `json:"evidence"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// Category represents a compliance category
type Category struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Status ComplianceStatus `json:"status"`
Score float64 `json:"score"`
Weight float64 `json:"weight"`
Controls []Control `json:"controls"`
}
// Control represents a compliance control
type Control struct {
ID string `json:"id"`
Reference string `json:"reference"` // e.g., "SOC2 CC6.1"
Name string `json:"name"`
Description string `json:"description"`
Status ComplianceStatus `json:"status"`
Evidence []string `json:"evidence_ids,omitempty"`
Findings []string `json:"finding_ids,omitempty"`
LastChecked time.Time `json:"last_checked"`
Notes string `json:"notes,omitempty"`
}
// Finding represents a compliance finding
type Finding struct {
ID string `json:"id"`
ControlID string `json:"control_id"`
Type FindingType `json:"type"`
Severity FindingSeverity `json:"severity"`
Title string `json:"title"`
Description string `json:"description"`
Impact string `json:"impact"`
Recommendation string `json:"recommendation"`
Status FindingStatus `json:"status"`
DetectedAt time.Time `json:"detected_at"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
Evidence []string `json:"evidence_ids,omitempty"`
}
// FindingType represents the type of finding
type FindingType string
const (
FindingGap FindingType = "gap"
FindingViolation FindingType = "violation"
FindingObservation FindingType = "observation"
FindingRecommendation FindingType = "recommendation"
)
// FindingSeverity represents finding severity
type FindingSeverity string
const (
SeverityLow FindingSeverity = "low"
SeverityMedium FindingSeverity = "medium"
SeverityHigh FindingSeverity = "high"
SeverityCritical FindingSeverity = "critical"
)
// FindingStatus represents finding status
type FindingStatus string
const (
FindingOpen FindingStatus = "open"
FindingAccepted FindingStatus = "accepted"
FindingResolved FindingStatus = "resolved"
)
// Evidence represents compliance evidence
type Evidence struct {
ID string `json:"id"`
Type EvidenceType `json:"type"`
Description string `json:"description"`
Source string `json:"source"`
CollectedAt time.Time `json:"collected_at"`
Hash string `json:"hash,omitempty"`
Data interface{} `json:"data,omitempty"`
}
// EvidenceType represents the type of evidence
type EvidenceType string
const (
EvidenceBackupLog EvidenceType = "backup_log"
EvidenceRestoreLog EvidenceType = "restore_log"
EvidenceDrillResult EvidenceType = "drill_result"
EvidenceEncryptionProof EvidenceType = "encryption_proof"
EvidenceRetentionProof EvidenceType = "retention_proof"
EvidenceAccessLog EvidenceType = "access_log"
EvidenceAuditLog EvidenceType = "audit_log"
EvidenceConfiguration EvidenceType = "configuration"
EvidenceScreenshot EvidenceType = "screenshot"
EvidenceOther EvidenceType = "other"
)
// Summary provides a high-level overview
type Summary struct {
TotalControls int `json:"total_controls"`
CompliantControls int `json:"compliant_controls"`
NonCompliantControls int `json:"non_compliant_controls"`
PartialControls int `json:"partial_controls"`
NotApplicable int `json:"not_applicable"`
OpenFindings int `json:"open_findings"`
CriticalFindings int `json:"critical_findings"`
HighFindings int `json:"high_findings"`
MediumFindings int `json:"medium_findings"`
LowFindings int `json:"low_findings"`
ComplianceRate float64 `json:"compliance_rate"`
RiskScore float64 `json:"risk_score"`
}
// ReportConfig configures report generation
type ReportConfig struct {
Type ReportType `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
PeriodStart time.Time `json:"period_start"`
PeriodEnd time.Time `json:"period_end"`
IncludeDatabases []string `json:"include_databases,omitempty"`
ExcludeDatabases []string `json:"exclude_databases,omitempty"`
CatalogPath string `json:"catalog_path"`
OutputFormat OutputFormat `json:"output_format"`
OutputPath string `json:"output_path"`
IncludeEvidence bool `json:"include_evidence"`
CustomControls []Control `json:"custom_controls,omitempty"`
}
// OutputFormat represents report output format
type OutputFormat string
const (
FormatJSON OutputFormat = "json"
FormatHTML OutputFormat = "html"
FormatPDF OutputFormat = "pdf"
FormatMarkdown OutputFormat = "markdown"
)
// NewReport creates a new report
func NewReport(reportType ReportType, title string) *Report {
return &Report{
ID: generateID(),
Type: reportType,
Title: title,
GeneratedAt: time.Now(),
Categories: make([]Category, 0),
Findings: make([]Finding, 0),
Evidence: make([]Evidence, 0),
Metadata: make(map[string]string),
}
}
// AddCategory adds a category to the report
func (r *Report) AddCategory(cat Category) {
r.Categories = append(r.Categories, cat)
}
// AddFinding adds a finding to the report
func (r *Report) AddFinding(f Finding) {
r.Findings = append(r.Findings, f)
}
// AddEvidence adds evidence to the report
func (r *Report) AddEvidence(e Evidence) {
r.Evidence = append(r.Evidence, e)
}
// Calculate computes the summary and overall status
func (r *Report) Calculate() {
var totalWeight float64
var weightedScore float64
for _, cat := range r.Categories {
totalWeight += cat.Weight
weightedScore += cat.Score * cat.Weight
for _, ctrl := range cat.Controls {
r.Summary.TotalControls++
switch ctrl.Status {
case StatusCompliant:
r.Summary.CompliantControls++
case StatusNonCompliant:
r.Summary.NonCompliantControls++
case StatusPartial:
r.Summary.PartialControls++
case StatusNotApplicable:
r.Summary.NotApplicable++
}
}
}
for _, f := range r.Findings {
if f.Status == FindingOpen {
r.Summary.OpenFindings++
switch f.Severity {
case SeverityCritical:
r.Summary.CriticalFindings++
case SeverityHigh:
r.Summary.HighFindings++
case SeverityMedium:
r.Summary.MediumFindings++
case SeverityLow:
r.Summary.LowFindings++
}
}
}
if totalWeight > 0 {
r.Score = weightedScore / totalWeight
}
applicable := r.Summary.TotalControls - r.Summary.NotApplicable
if applicable > 0 {
r.Summary.ComplianceRate = float64(r.Summary.CompliantControls) / float64(applicable) * 100
}
// Calculate risk score based on findings
r.Summary.RiskScore = float64(r.Summary.CriticalFindings)*10 +
float64(r.Summary.HighFindings)*5 +
float64(r.Summary.MediumFindings)*2 +
float64(r.Summary.LowFindings)*1
// Determine overall status
if r.Summary.NonCompliantControls == 0 && r.Summary.CriticalFindings == 0 {
if r.Summary.PartialControls == 0 {
r.Status = StatusCompliant
} else {
r.Status = StatusPartial
}
} else {
r.Status = StatusNonCompliant
}
}
// ToJSON converts the report to JSON
func (r *Report) ToJSON() ([]byte, error) {
return json.MarshalIndent(r, "", " ")
}
func generateID() string {
return fmt.Sprintf("RPT-%d", time.Now().UnixNano())
}
// StatusIcon returns an icon for a compliance status
func StatusIcon(s ComplianceStatus) string {
switch s {
case StatusCompliant:
return "✅"
case StatusNonCompliant:
return "❌"
case StatusPartial:
return "⚠️"
case StatusNotApplicable:
return ""
default:
return "❓"
}
}
// SeverityIcon returns an icon for a finding severity
func SeverityIcon(s FindingSeverity) string {
switch s {
case SeverityCritical:
return "🔴"
case SeverityHigh:
return "🟠"
case SeverityMedium:
return "🟡"
case SeverityLow:
return "🟢"
default:
return "⚪"
}
}