security: Implement HIGH priority security improvements
HIGH Priority Security Features: - Path sanitization with filepath.Clean() for all user paths - Path traversal attack prevention in backup/restore operations - Secure config file permissions (0600 instead of 0644) - SHA-256 checksum generation for all backup archives - Checksum verification during restore operations - Comprehensive audit logging for compliance New Security Module (internal/security/): - paths.go: ValidateBackupPath() and ValidateArchivePath() - checksum.go: ChecksumFile(), VerifyChecksum(), LoadAndVerifyChecksum() - audit.go: AuditLogger with structured event tracking Integration Points: - Backup engine: Path validation, checksum generation - Restore engine: Path validation, checksum verification - All backup/restore operations: Audit logging - Configuration saves: Audit logging Security Enhancements: - .dbbackup.conf now created with 0600 permissions (owner-only) - All archive files get .sha256 checksum files - Restore warns if checksum verification fails but continues - Audit events logged for all administrative operations - User tracking via $USER/$USERNAME environment variables Compliance Features: - Audit trail for backups, restores, config changes - Structured logging with timestamps, users, actions, results - Event details include paths, sizes, durations, errors Testing: - All code compiles successfully - Cross-platform build verified - Ready for integration testing
This commit is contained in:
234
internal/security/audit.go
Normal file
234
internal/security/audit.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"dbbackup/internal/logger"
|
||||
)
|
||||
|
||||
// AuditEvent represents an auditable event
|
||||
type AuditEvent struct {
|
||||
Timestamp time.Time
|
||||
User string
|
||||
Action string
|
||||
Resource string
|
||||
Result string
|
||||
Details map[string]interface{}
|
||||
}
|
||||
|
||||
// AuditLogger provides audit logging functionality
|
||||
type AuditLogger struct {
|
||||
log logger.Logger
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewAuditLogger creates a new audit logger
|
||||
func NewAuditLogger(log logger.Logger, enabled bool) *AuditLogger {
|
||||
return &AuditLogger{
|
||||
log: log,
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// LogBackupStart logs backup operation start
|
||||
func (a *AuditLogger) LogBackupStart(user, database, backupType string) {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
event := AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: "BACKUP_START",
|
||||
Resource: database,
|
||||
Result: "INITIATED",
|
||||
Details: map[string]interface{}{
|
||||
"backup_type": backupType,
|
||||
},
|
||||
}
|
||||
|
||||
a.logEvent(event)
|
||||
}
|
||||
|
||||
// LogBackupComplete logs successful backup completion
|
||||
func (a *AuditLogger) LogBackupComplete(user, database, archivePath string, sizeBytes int64) {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
event := AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: "BACKUP_COMPLETE",
|
||||
Resource: database,
|
||||
Result: "SUCCESS",
|
||||
Details: map[string]interface{}{
|
||||
"archive_path": archivePath,
|
||||
"size_bytes": sizeBytes,
|
||||
},
|
||||
}
|
||||
|
||||
a.logEvent(event)
|
||||
}
|
||||
|
||||
// LogBackupFailed logs backup failure
|
||||
func (a *AuditLogger) LogBackupFailed(user, database string, err error) {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
event := AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: "BACKUP_FAILED",
|
||||
Resource: database,
|
||||
Result: "FAILURE",
|
||||
Details: map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
a.logEvent(event)
|
||||
}
|
||||
|
||||
// LogRestoreStart logs restore operation start
|
||||
func (a *AuditLogger) LogRestoreStart(user, database, archivePath string) {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
event := AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: "RESTORE_START",
|
||||
Resource: database,
|
||||
Result: "INITIATED",
|
||||
Details: map[string]interface{}{
|
||||
"archive_path": archivePath,
|
||||
},
|
||||
}
|
||||
|
||||
a.logEvent(event)
|
||||
}
|
||||
|
||||
// LogRestoreComplete logs successful restore completion
|
||||
func (a *AuditLogger) LogRestoreComplete(user, database string, duration time.Duration) {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
event := AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: "RESTORE_COMPLETE",
|
||||
Resource: database,
|
||||
Result: "SUCCESS",
|
||||
Details: map[string]interface{}{
|
||||
"duration_seconds": duration.Seconds(),
|
||||
},
|
||||
}
|
||||
|
||||
a.logEvent(event)
|
||||
}
|
||||
|
||||
// LogRestoreFailed logs restore failure
|
||||
func (a *AuditLogger) LogRestoreFailed(user, database string, err error) {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
event := AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: "RESTORE_FAILED",
|
||||
Resource: database,
|
||||
Result: "FAILURE",
|
||||
Details: map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
a.logEvent(event)
|
||||
}
|
||||
|
||||
// LogConfigChange logs configuration changes
|
||||
func (a *AuditLogger) LogConfigChange(user, setting, oldValue, newValue string) {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
event := AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: "CONFIG_CHANGE",
|
||||
Resource: setting,
|
||||
Result: "SUCCESS",
|
||||
Details: map[string]interface{}{
|
||||
"old_value": oldValue,
|
||||
"new_value": newValue,
|
||||
},
|
||||
}
|
||||
|
||||
a.logEvent(event)
|
||||
}
|
||||
|
||||
// LogConnectionAttempt logs database connection attempts
|
||||
func (a *AuditLogger) LogConnectionAttempt(user, host string, success bool, err error) {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
result := "SUCCESS"
|
||||
details := map[string]interface{}{
|
||||
"host": host,
|
||||
}
|
||||
|
||||
if !success {
|
||||
result = "FAILURE"
|
||||
if err != nil {
|
||||
details["error"] = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
event := AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
User: user,
|
||||
Action: "DB_CONNECTION",
|
||||
Resource: host,
|
||||
Result: result,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
a.logEvent(event)
|
||||
}
|
||||
|
||||
// logEvent writes the audit event to log
|
||||
func (a *AuditLogger) logEvent(event AuditEvent) {
|
||||
fields := map[string]interface{}{
|
||||
"audit": true,
|
||||
"timestamp": event.Timestamp.Format(time.RFC3339),
|
||||
"user": event.User,
|
||||
"action": event.Action,
|
||||
"resource": event.Resource,
|
||||
"result": event.Result,
|
||||
}
|
||||
|
||||
// Merge event details
|
||||
for k, v := range event.Details {
|
||||
fields[k] = v
|
||||
}
|
||||
|
||||
a.log.WithFields(fields).Info("AUDIT")
|
||||
}
|
||||
|
||||
// GetCurrentUser returns the current system user
|
||||
func GetCurrentUser() string {
|
||||
if user := os.Getenv("USER"); user != "" {
|
||||
return user
|
||||
}
|
||||
if user := os.Getenv("USERNAME"); user != "" {
|
||||
return user
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
91
internal/security/checksum.go
Normal file
91
internal/security/checksum.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ChecksumFile calculates SHA-256 checksum of a file
|
||||
func ChecksumFile(path string) (string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", fmt.Errorf("failed to calculate checksum: %w", err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// VerifyChecksum verifies a file's checksum against expected value
|
||||
func VerifyChecksum(path string, expectedChecksum string) error {
|
||||
actualChecksum, err := ChecksumFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if actualChecksum != expectedChecksum {
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveChecksum saves checksum to a .sha256 file alongside the archive
|
||||
func SaveChecksum(archivePath string, checksum string) error {
|
||||
checksumPath := archivePath + ".sha256"
|
||||
content := fmt.Sprintf("%s %s\n", checksum, archivePath)
|
||||
|
||||
if err := os.WriteFile(checksumPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to save checksum: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadChecksum loads checksum from a .sha256 file
|
||||
func LoadChecksum(archivePath string) (string, error) {
|
||||
checksumPath := archivePath + ".sha256"
|
||||
|
||||
data, err := os.ReadFile(checksumPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read checksum file: %w", err)
|
||||
}
|
||||
|
||||
// Parse "checksum filename" format
|
||||
parts := []byte{}
|
||||
for i, b := range data {
|
||||
if b == ' ' {
|
||||
parts = data[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return "", fmt.Errorf("invalid checksum file format")
|
||||
}
|
||||
|
||||
return string(parts), nil
|
||||
}
|
||||
|
||||
// LoadAndVerifyChecksum loads checksum from .sha256 file and verifies the archive
|
||||
// Returns nil if checksum file doesn't exist (optional verification)
|
||||
// Returns error if checksum file exists but verification fails
|
||||
func LoadAndVerifyChecksum(archivePath string) error {
|
||||
expectedChecksum, err := LoadChecksum(archivePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // Checksum file doesn't exist, skip verification
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return VerifyChecksum(archivePath, expectedChecksum)
|
||||
}
|
||||
72
internal/security/paths.go
Normal file
72
internal/security/paths.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CleanPath sanitizes a file path to prevent path traversal attacks
|
||||
func CleanPath(path string) (string, error) {
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("path cannot be empty")
|
||||
}
|
||||
|
||||
// Clean the path (removes .., ., //)
|
||||
cleaned := filepath.Clean(path)
|
||||
|
||||
// Detect path traversal attempts
|
||||
if strings.Contains(cleaned, "..") {
|
||||
return "", fmt.Errorf("path traversal detected: %s", path)
|
||||
}
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
// ValidateBackupPath ensures backup path is safe
|
||||
func ValidateBackupPath(path string) (string, error) {
|
||||
cleaned, err := CleanPath(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(cleaned)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
// ValidateArchivePath validates an archive file path
|
||||
func ValidateArchivePath(path string) (string, error) {
|
||||
cleaned, err := CleanPath(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Must have a valid archive extension
|
||||
ext := strings.ToLower(filepath.Ext(cleaned))
|
||||
validExtensions := []string{".dump", ".sql", ".gz", ".tar"}
|
||||
|
||||
valid := false
|
||||
for _, validExt := range validExtensions {
|
||||
if strings.HasSuffix(cleaned, validExt) {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return "", fmt.Errorf("invalid archive extension: %s (must be .dump, .sql, .gz, or .tar)", ext)
|
||||
}
|
||||
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(cleaned)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
return absPath, nil
|
||||
}
|
||||
Reference in New Issue
Block a user