ruborg 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +26 -0
 - data/README.md +37 -0
 - data/SECURITY.md +173 -0
 - data/lib/ruborg/backup.rb +56 -7
 - data/lib/ruborg/cli.rb +53 -9
 - data/lib/ruborg/config.rb +47 -4
 - data/lib/ruborg/logger.rb +16 -3
 - data/lib/ruborg/passbolt.rb +3 -2
 - data/lib/ruborg/repository.rb +26 -4
 - data/lib/ruborg/version.rb +1 -1
 - metadata +2 -1
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 0bb6c1c5f47b1fbb4538310a929914b0e744bcdb30d5e5635c4246df778e2113
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: bda5cf063fe587a047dd36fa9c5e948d8130e47501a53eabef9c34a3af7ae361
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 8828dacb76519557d51876327133491318bd199e589fb695d08a94af1a73a3c34772f022b6630e199a2c1ab9ab7d78dcce043a402bfa0f418ae51262aad80e8f
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 99b7e79eb2e50240862f7ade79291d9a76469385f2b7e8949df8e6ac3251f9adcbe2f605e42c09364616c028f10575593c3e0a1df1d020bcf8fc7291a9cdb558
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
            ## [0.3.1] - 2025-10-05
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 13 
     | 
    
         
            +
            - `borg_options` configuration for controlling Borg environment variables
         
     | 
| 
      
 14 
     | 
    
         
            +
            - Repository path validation to prevent creation in system directories
         
     | 
| 
      
 15 
     | 
    
         
            +
            - Backup path validation and normalization
         
     | 
| 
      
 16 
     | 
    
         
            +
            - Archive name sanitization (alphanumeric, dash, underscore, dot only)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 19 
     | 
    
         
            +
            - Borg environment variables now configurable via `borg_options` (backward compatible)
         
     | 
| 
      
 20 
     | 
    
         
            +
            - All backup paths are now normalized to absolute paths
         
     | 
| 
      
 21 
     | 
    
         
            +
            - Custom archive names are automatically sanitized
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            ### Security
         
     | 
| 
      
 24 
     | 
    
         
            +
            - Fixed command injection vulnerability in Passbolt CLI execution (now uses Open3.capture3)
         
     | 
| 
      
 25 
     | 
    
         
            +
            - Added path traversal protection for extract operations
         
     | 
| 
      
 26 
     | 
    
         
            +
            - Implemented symlink resolution and system path protection for --remove-source
         
     | 
| 
      
 27 
     | 
    
         
            +
            - Changed to YAML.safe_load_file to prevent arbitrary code execution
         
     | 
| 
      
 28 
     | 
    
         
            +
            - Added log path validation to prevent writing to system directories
         
     | 
| 
      
 29 
     | 
    
         
            +
            - Added repository path validation (prevents /bin, /etc, /usr, etc.)
         
     | 
| 
      
 30 
     | 
    
         
            +
            - Added backup path validation (rejects empty/nil paths)
         
     | 
| 
      
 31 
     | 
    
         
            +
            - Added archive name sanitization (prevents injection attacks)
         
     | 
| 
      
 32 
     | 
    
         
            +
            - Made Borg environment options configurable for enhanced security
         
     | 
| 
      
 33 
     | 
    
         
            +
            - Added SECURITY.md with comprehensive security guidelines and best practices
         
     | 
| 
      
 34 
     | 
    
         
            +
            - Enhanced test coverage for all security features
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
       10 
36 
     | 
    
         
             
            ## [0.3.0] - 2025-10-05
         
     | 
| 
       11 
37 
     | 
    
         | 
| 
       12 
38 
     | 
    
         
             
            ### Added
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -18,6 +18,7 @@ A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg 
     | 
|
| 
       18 
18 
     | 
    
         
             
            - 🗄️ **Multi-Repository** - Manage multiple backup repositories with different sources
         
     | 
| 
       19 
19 
     | 
    
         
             
            - 🔄 **Auto-initialization** - Automatically initialize repositories on first use
         
     | 
| 
       20 
20 
     | 
    
         
             
            - ✅ **Well-tested** - Comprehensive test suite with RSpec
         
     | 
| 
      
 21 
     | 
    
         
            +
            - 🔒 **Security-focused** - Path validation, safe YAML loading, command injection protection
         
     | 
| 
       21 
22 
     | 
    
         | 
| 
       22 
23 
     | 
    
         
             
            ## Prerequisites
         
     | 
| 
       23 
24 
     | 
    
         | 
| 
         @@ -98,6 +99,11 @@ auto_init: true 
     | 
|
| 
       98 
99 
     | 
    
         | 
| 
       99 
100 
     | 
    
         
             
            # Log file path (optional, default: ~/.ruborg/logs/ruborg.log)
         
     | 
| 
       100 
101 
     | 
    
         
             
            log_file: /var/log/ruborg.log
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
            # Borg environment options (optional)
         
     | 
| 
      
 104 
     | 
    
         
            +
            borg_options:
         
     | 
| 
      
 105 
     | 
    
         
            +
              allow_relocated_repo: true  # Allow relocated repositories (default: true)
         
     | 
| 
      
 106 
     | 
    
         
            +
              allow_unencrypted_repo: true  # Allow unencrypted repositories (default: true)
         
     | 
| 
       101 
107 
     | 
    
         
             
            ```
         
     | 
| 
       102 
108 
     | 
    
         | 
| 
       103 
109 
     | 
    
         
             
            ### Multi-Repository Configuration
         
     | 
| 
         @@ -111,6 +117,9 @@ encryption: repokey 
     | 
|
| 
       111 
117 
     | 
    
         
             
            auto_init: true
         
     | 
| 
       112 
118 
     | 
    
         
             
            passbolt:
         
     | 
| 
       113 
119 
     | 
    
         
             
              resource_id: "global-passbolt-id"
         
     | 
| 
      
 120 
     | 
    
         
            +
            borg_options:
         
     | 
| 
      
 121 
     | 
    
         
            +
              allow_relocated_repo: false
         
     | 
| 
      
 122 
     | 
    
         
            +
              allow_unencrypted_repo: false
         
     | 
| 
       114 
123 
     | 
    
         | 
| 
       115 
124 
     | 
    
         
             
            # Multiple repositories
         
     | 
| 
       116 
125 
     | 
    
         
             
            repositories:
         
     | 
| 
         @@ -282,6 +291,33 @@ backup_paths: 
     | 
|
| 
       282 
291 
     | 
    
         | 
| 
       283 
292 
     | 
    
         
             
            When enabled, ruborg will automatically run `borg init` if the repository doesn't exist when you run `backup`, `list`, or `info` commands. The passphrase will be retrieved from Passbolt if configured.
         
     | 
| 
       284 
293 
     | 
    
         | 
| 
      
 294 
     | 
    
         
            +
            ## Security Configuration
         
     | 
| 
      
 295 
     | 
    
         
            +
             
     | 
| 
      
 296 
     | 
    
         
            +
            Ruborg provides configurable security options via `borg_options`:
         
     | 
| 
      
 297 
     | 
    
         
            +
             
     | 
| 
      
 298 
     | 
    
         
            +
            ```yaml
         
     | 
| 
      
 299 
     | 
    
         
            +
            borg_options:
         
     | 
| 
      
 300 
     | 
    
         
            +
              # Control whether to allow access to relocated repositories
         
     | 
| 
      
 301 
     | 
    
         
            +
              # Set to false in production for enhanced security
         
     | 
| 
      
 302 
     | 
    
         
            +
              allow_relocated_repo: true  # default: true
         
     | 
| 
      
 303 
     | 
    
         
            +
             
     | 
| 
      
 304 
     | 
    
         
            +
              # Control whether to allow access to unencrypted repositories
         
     | 
| 
      
 305 
     | 
    
         
            +
              # Set to false to enforce encryption
         
     | 
| 
      
 306 
     | 
    
         
            +
              allow_unencrypted_repo: true  # default: true
         
     | 
| 
      
 307 
     | 
    
         
            +
            ```
         
     | 
| 
      
 308 
     | 
    
         
            +
             
     | 
| 
      
 309 
     | 
    
         
            +
            **Security Features:**
         
     | 
| 
      
 310 
     | 
    
         
            +
            - **Repository Path Validation**: Prevents creation in system directories (`/bin`, `/etc`, `/usr`, etc.)
         
     | 
| 
      
 311 
     | 
    
         
            +
            - **Backup Path Validation**: Validates and normalizes all backup source paths
         
     | 
| 
      
 312 
     | 
    
         
            +
            - **Archive Name Sanitization**: Automatically sanitizes custom archive names
         
     | 
| 
      
 313 
     | 
    
         
            +
            - **Path Traversal Protection**: Prevents extraction to system directories
         
     | 
| 
      
 314 
     | 
    
         
            +
            - **Symlink Protection**: Resolves and validates symlinks before deletion with `--remove-source`
         
     | 
| 
      
 315 
     | 
    
         
            +
            - **Safe YAML Loading**: Uses `YAML.safe_load_file` to prevent code execution
         
     | 
| 
      
 316 
     | 
    
         
            +
            - **Command Injection Protection**: Uses safe command execution methods
         
     | 
| 
      
 317 
     | 
    
         
            +
            - **Log Path Validation**: Prevents writing logs to system directories
         
     | 
| 
      
 318 
     | 
    
         
            +
             
     | 
| 
      
 319 
     | 
    
         
            +
            See [SECURITY.md](SECURITY.md) for detailed security information and best practices.
         
     | 
| 
      
 320 
     | 
    
         
            +
             
     | 
| 
       285 
321 
     | 
    
         
             
            ## Command Reference
         
     | 
| 
       286 
322 
     | 
    
         | 
| 
       287 
323 
     | 
    
         
             
            | Command | Description | Options |
         
     | 
| 
         @@ -341,6 +377,7 @@ The test suite includes: 
     | 
|
| 
       341 
377 
     | 
    
         
             
            - Passbolt integration (mocked)
         
     | 
| 
       342 
378 
     | 
    
         
             
            - CLI commands
         
     | 
| 
       343 
379 
     | 
    
         
             
            - Logging functionality
         
     | 
| 
      
 380 
     | 
    
         
            +
            - Comprehensive security tests (path validation, sanitization, etc.)
         
     | 
| 
       344 
381 
     | 
    
         | 
| 
       345 
382 
     | 
    
         
             
            ## Contributing
         
     | 
| 
       346 
383 
     | 
    
         | 
    
        data/SECURITY.md
    ADDED
    
    | 
         @@ -0,0 +1,173 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Security Policy
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            ## Security Features
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            Ruborg implements several security measures to protect your backup operations:
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            ### 1. Command Injection Prevention
         
     | 
| 
      
 8 
     | 
    
         
            +
            - Uses `Open3.capture3` for Passbolt CLI execution (no shell interpolation)
         
     | 
| 
      
 9 
     | 
    
         
            +
            - Array-based command construction for Borg commands
         
     | 
| 
      
 10 
     | 
    
         
            +
            - No user input directly interpolated into shell commands
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### 2. Path Traversal Protection
         
     | 
| 
      
 13 
     | 
    
         
            +
            - Validates all destination paths during restore operations
         
     | 
| 
      
 14 
     | 
    
         
            +
            - Prevents extraction to system directories (`/`, `/etc`, `/bin`, `/usr`, etc.)
         
     | 
| 
      
 15 
     | 
    
         
            +
            - Normalizes paths to prevent `../` traversal attacks
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            ### 3. Symlink Protection
         
     | 
| 
      
 18 
     | 
    
         
            +
            - Resolves symlinks before file deletion with `--remove-source`
         
     | 
| 
      
 19 
     | 
    
         
            +
            - Refuses to delete system directories even when targeted via symlinks
         
     | 
| 
      
 20 
     | 
    
         
            +
            - Uses `FileUtils.rm_rf` with `secure: true` option
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            ### 4. Safe YAML Loading
         
     | 
| 
      
 23 
     | 
    
         
            +
            - Uses `YAML.safe_load_file` to prevent arbitrary code execution
         
     | 
| 
      
 24 
     | 
    
         
            +
            - Rejects YAML files containing Ruby objects or other dangerous constructs
         
     | 
| 
      
 25 
     | 
    
         
            +
            - Only permits basic data types and Symbol class
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
            ### 5. Log Path Validation
         
     | 
| 
      
 28 
     | 
    
         
            +
            - Validates log file paths to prevent writing to system directories
         
     | 
| 
      
 29 
     | 
    
         
            +
            - Automatically creates log directories with proper permissions
         
     | 
| 
      
 30 
     | 
    
         
            +
            - Rejects paths in `/bin`, `/etc`, `/usr`, and other sensitive locations
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            ### 6. Passphrase Handling
         
     | 
| 
      
 33 
     | 
    
         
            +
            - Passes passphrases via environment variables (never CLI arguments)
         
     | 
| 
      
 34 
     | 
    
         
            +
            - Prevents passphrase leakage in process listings
         
     | 
| 
      
 35 
     | 
    
         
            +
            - Uses Passbolt integration for secure password retrieval
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            ### 7. Repository Path Validation
         
     | 
| 
      
 38 
     | 
    
         
            +
            - Validates repository paths to prevent creation in system directories
         
     | 
| 
      
 39 
     | 
    
         
            +
            - Rejects empty or nil repository paths
         
     | 
| 
      
 40 
     | 
    
         
            +
            - Prevents accidental repository creation in `/bin`, `/etc`, `/usr`, etc.
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
            ### 8. Backup Path Validation
         
     | 
| 
      
 43 
     | 
    
         
            +
            - Validates all backup source paths before creating archives
         
     | 
| 
      
 44 
     | 
    
         
            +
            - Rejects empty, nil, or whitespace-only paths
         
     | 
| 
      
 45 
     | 
    
         
            +
            - Normalizes relative paths to absolute paths for consistency
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
            ### 9. Archive Name Sanitization
         
     | 
| 
      
 48 
     | 
    
         
            +
            - Sanitizes user-provided archive names to prevent injection attacks
         
     | 
| 
      
 49 
     | 
    
         
            +
            - Allows only alphanumeric characters, dashes, underscores, and dots
         
     | 
| 
      
 50 
     | 
    
         
            +
            - Rejects archive names that would become empty after sanitization
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
            ### 10. Configurable Borg Environment Options
         
     | 
| 
      
 53 
     | 
    
         
            +
            - Allows control over relocated repository access via config
         
     | 
| 
      
 54 
     | 
    
         
            +
            - Allows control over unencrypted repository access via config
         
     | 
| 
      
 55 
     | 
    
         
            +
            - Defaults to safe settings while maintaining backward compatibility
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
            ## Security Best Practices
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
            ### When Using `--remove-source`
         
     | 
| 
      
 60 
     | 
    
         
            +
            ⚠️ **Warning**: The `--remove-source` flag permanently deletes source files after backup.
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
            **Recommendations:**
         
     | 
| 
      
 63 
     | 
    
         
            +
            1. **Test first** without `--remove-source` to verify backups work
         
     | 
| 
      
 64 
     | 
    
         
            +
            2. **Never use on symlinks** to critical system directories
         
     | 
| 
      
 65 
     | 
    
         
            +
            3. **Verify backups** before using this flag in production
         
     | 
| 
      
 66 
     | 
    
         
            +
            4. **Use absolute paths** in configuration to avoid ambiguity
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
            ### Configuration File Security
         
     | 
| 
      
 69 
     | 
    
         
            +
            - Store configuration files with restricted permissions: `chmod 600 ruborg.yml`
         
     | 
| 
      
 70 
     | 
    
         
            +
            - Never commit Passbolt resource IDs to public repositories
         
     | 
| 
      
 71 
     | 
    
         
            +
            - Use environment variables for sensitive paths when possible
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
            ### Repository Security
         
     | 
| 
      
 74 
     | 
    
         
            +
            - Use encrypted repositories (default: `encryption: repokey`)
         
     | 
| 
      
 75 
     | 
    
         
            +
            - Store passphrases in Passbolt, not in config files
         
     | 
| 
      
 76 
     | 
    
         
            +
            - Use different encryption keys for different repository types
         
     | 
| 
      
 77 
     | 
    
         
            +
            - Regularly rotate Passbolt passphrases
         
     | 
| 
      
 78 
     | 
    
         
            +
            - Avoid creating repositories in system directories
         
     | 
| 
      
 79 
     | 
    
         
            +
            - Use absolute paths for repository locations
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
            ### Multi-Repository Considerations
         
     | 
| 
      
 82 
     | 
    
         
            +
            - Each repository can have its own Passbolt resource ID
         
     | 
| 
      
 83 
     | 
    
         
            +
            - Validate all source paths before adding to configuration
         
     | 
| 
      
 84 
     | 
    
         
            +
            - Review exclude patterns to ensure no sensitive files leak
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            ### Archive Naming
         
     | 
| 
      
 87 
     | 
    
         
            +
            - Use default timestamp-based names when possible
         
     | 
| 
      
 88 
     | 
    
         
            +
            - If providing custom names, use only alphanumeric characters, dashes, and underscores
         
     | 
| 
      
 89 
     | 
    
         
            +
            - Avoid special characters or path separators in archive names
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
            ### Borg Environment Options
         
     | 
| 
      
 92 
     | 
    
         
            +
            - Consider disabling `allow_relocated_repo` for production environments
         
     | 
| 
      
 93 
     | 
    
         
            +
            - Consider disabling `allow_unencrypted_repo` for sensitive data
         
     | 
| 
      
 94 
     | 
    
         
            +
            - Configure in `ruborg.yml`:
         
     | 
| 
      
 95 
     | 
    
         
            +
            ```yaml
         
     | 
| 
      
 96 
     | 
    
         
            +
            borg_options:
         
     | 
| 
      
 97 
     | 
    
         
            +
              allow_relocated_repo: false  # Reject relocated repositories
         
     | 
| 
      
 98 
     | 
    
         
            +
              allow_unencrypted_repo: false  # Reject unencrypted repositories
         
     | 
| 
      
 99 
     | 
    
         
            +
            ```
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
            ## Reporting Security Issues
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
            If you discover a security vulnerability, please:
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
            1. **Do NOT** open a public issue
         
     | 
| 
      
 106 
     | 
    
         
            +
            2. Email security concerns to: mpantel@aegean.gr
         
     | 
| 
      
 107 
     | 
    
         
            +
            3. Include:
         
     | 
| 
      
 108 
     | 
    
         
            +
               - Description of the vulnerability
         
     | 
| 
      
 109 
     | 
    
         
            +
               - Steps to reproduce
         
     | 
| 
      
 110 
     | 
    
         
            +
               - Potential impact
         
     | 
| 
      
 111 
     | 
    
         
            +
               - Suggested fix (if any)
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
            We will respond within 48 hours and work with you to address the issue.
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
            ## Security Audit History
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
            - **v0.3.1** (2025-10-05): Comprehensive security hardening
         
     | 
| 
      
 118 
     | 
    
         
            +
              - Fixed command injection in Passbolt CLI execution (uses Open3.capture3)
         
     | 
| 
      
 119 
     | 
    
         
            +
              - Added path traversal protection for extract operations
         
     | 
| 
      
 120 
     | 
    
         
            +
              - Implemented symlink protection for file deletion with --remove-source
         
     | 
| 
      
 121 
     | 
    
         
            +
              - Switched to safe YAML loading (YAML.safe_load_file)
         
     | 
| 
      
 122 
     | 
    
         
            +
              - Added log path validation to prevent writing to system directories
         
     | 
| 
      
 123 
     | 
    
         
            +
              - Added repository path validation to prevent creation in system directories
         
     | 
| 
      
 124 
     | 
    
         
            +
              - Added backup path validation and normalization
         
     | 
| 
      
 125 
     | 
    
         
            +
              - Implemented archive name sanitization
         
     | 
| 
      
 126 
     | 
    
         
            +
              - Made Borg environment variables configurable
         
     | 
| 
      
 127 
     | 
    
         
            +
              - Enhanced test coverage for all security features
         
     | 
| 
      
 128 
     | 
    
         
            +
              - Created comprehensive SECURITY.md documentation
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
            - **v0.3.0** (2025-10-05): Multi-repository and auto-initialization features
         
     | 
| 
      
 131 
     | 
    
         
            +
              - Added multi-repository configuration support
         
     | 
| 
      
 132 
     | 
    
         
            +
              - Added auto-initialization feature
         
     | 
| 
      
 133 
     | 
    
         
            +
              - Added configurable log file paths
         
     | 
| 
      
 134 
     | 
    
         
            +
              - No security-specific changes in this version
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
            ## Dependency Security
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
            Ruborg relies on:
         
     | 
| 
      
 139 
     | 
    
         
            +
            - **Borg Backup**: Industry-standard backup tool with strong encryption
         
     | 
| 
      
 140 
     | 
    
         
            +
            - **Passbolt CLI**: Secure password management
         
     | 
| 
      
 141 
     | 
    
         
            +
            - **Ruby stdlib**: No external gems for core functionality (only Thor for CLI)
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
            Keep dependencies updated:
         
     | 
| 
      
 144 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 145 
     | 
    
         
            +
            # Update Borg
         
     | 
| 
      
 146 
     | 
    
         
            +
            brew upgrade borgbackup  # macOS
         
     | 
| 
      
 147 
     | 
    
         
            +
            sudo apt update && sudo apt upgrade borgbackup  # Ubuntu
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
            # Update Passbolt CLI
         
     | 
| 
      
 150 
     | 
    
         
            +
            # Follow https://github.com/passbolt/go-passbolt-cli
         
     | 
| 
      
 151 
     | 
    
         
            +
             
     | 
| 
      
 152 
     | 
    
         
            +
            # Update Ruby gems
         
     | 
| 
      
 153 
     | 
    
         
            +
            bundle update
         
     | 
| 
      
 154 
     | 
    
         
            +
            ```
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
            ## Security Checklist
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
            Before deploying ruborg in production:
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
            - [ ] Review all paths in configuration files
         
     | 
| 
      
 161 
     | 
    
         
            +
            - [ ] Set proper file permissions on config (600) and logs (640)
         
     | 
| 
      
 162 
     | 
    
         
            +
            - [ ] Use Passbolt for all passphrases (never hardcode)
         
     | 
| 
      
 163 
     | 
    
         
            +
            - [ ] Test restore operations before relying on backups
         
     | 
| 
      
 164 
     | 
    
         
            +
            - [ ] Never use `--remove-source` without thorough testing
         
     | 
| 
      
 165 
     | 
    
         
            +
            - [ ] Keep Borg and Passbolt CLI up to date
         
     | 
| 
      
 166 
     | 
    
         
            +
            - [ ] Review exclude patterns for sensitive data
         
     | 
| 
      
 167 
     | 
    
         
            +
            - [ ] Use absolute paths in configuration
         
     | 
| 
      
 168 
     | 
    
         
            +
            - [ ] Enable auto_init only for trusted repository locations
         
     | 
| 
      
 169 
     | 
    
         
            +
            - [ ] Regularly audit backup logs for anomalies
         
     | 
| 
      
 170 
     | 
    
         
            +
            - [ ] Validate repository paths are not in system directories
         
     | 
| 
      
 171 
     | 
    
         
            +
            - [ ] Configure borg_options for your security requirements
         
     | 
| 
      
 172 
     | 
    
         
            +
            - [ ] Use default archive names or sanitized custom names only
         
     | 
| 
      
 173 
     | 
    
         
            +
            - [ ] Ensure backup paths don't contain empty or nil values
         
     | 
    
        data/lib/ruborg/backup.rb
    CHANGED
    
    | 
         @@ -28,8 +28,12 @@ module Ruborg 
     | 
|
| 
       28 
28 
     | 
    
         
             
                  # Change to destination directory if specified
         
     | 
| 
       29 
29 
     | 
    
         
             
                  if destination != "."
         
     | 
| 
       30 
30 
     | 
    
         
             
                    require "fileutils"
         
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
       32 
     | 
    
         
            -
                     
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                    # Validate and normalize destination path
         
     | 
| 
      
 33 
     | 
    
         
            +
                    validated_dest = validate_destination_path(destination)
         
     | 
| 
      
 34 
     | 
    
         
            +
                    FileUtils.mkdir_p(validated_dest) unless File.directory?(validated_dest)
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                    Dir.chdir(validated_dest) do
         
     | 
| 
       33 
37 
     | 
    
         
             
                      execute_borg_command(cmd)
         
     | 
| 
       34 
38 
     | 
    
         
             
                    end
         
     | 
| 
       35 
39 
     | 
    
         
             
                  else
         
     | 
| 
         @@ -57,7 +61,10 @@ module Ruborg 
     | 
|
| 
       57 
61 
     | 
    
         
             
                  end
         
     | 
| 
       58 
62 
     | 
    
         | 
| 
       59 
63 
     | 
    
         
             
                  cmd << "#{@repository.path}::#{archive_name}"
         
     | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  # Validate and normalize backup paths
         
     | 
| 
      
 66 
     | 
    
         
            +
                  validated_paths = validate_backup_paths(@config.backup_paths)
         
     | 
| 
      
 67 
     | 
    
         
            +
                  cmd += validated_paths
         
     | 
| 
       61 
68 
     | 
    
         | 
| 
       62 
69 
     | 
    
         
             
                  cmd
         
     | 
| 
       63 
70 
     | 
    
         
             
                end
         
     | 
| 
         @@ -79,11 +86,53 @@ module Ruborg 
     | 
|
| 
       79 
86 
     | 
    
         
             
                  require "fileutils"
         
     | 
| 
       80 
87 
     | 
    
         | 
| 
       81 
88 
     | 
    
         
             
                  @config.backup_paths.each do |path|
         
     | 
| 
       82 
     | 
    
         
            -
                     
     | 
| 
       83 
     | 
    
         
            -
             
     | 
| 
       84 
     | 
    
         
            -
             
     | 
| 
       85 
     | 
    
         
            -
             
     | 
| 
      
 89 
     | 
    
         
            +
                    # Resolve symlinks and validate path
         
     | 
| 
      
 90 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 91 
     | 
    
         
            +
                      real_path = File.realpath(path)
         
     | 
| 
      
 92 
     | 
    
         
            +
                    rescue Errno::ENOENT
         
     | 
| 
      
 93 
     | 
    
         
            +
                      # Path doesn't exist, skip
         
     | 
| 
      
 94 
     | 
    
         
            +
                      next
         
     | 
| 
      
 95 
     | 
    
         
            +
                    end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                    # Security check: ensure path hasn't been tampered with
         
     | 
| 
      
 98 
     | 
    
         
            +
                    unless File.exist?(real_path)
         
     | 
| 
      
 99 
     | 
    
         
            +
                      next
         
     | 
| 
       86 
100 
     | 
    
         
             
                    end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    # Additional safety: don't delete root or system directories
         
     | 
| 
      
 103 
     | 
    
         
            +
                    if real_path == "/" || real_path.start_with?("/bin", "/sbin", "/usr", "/etc", "/sys", "/proc")
         
     | 
| 
      
 104 
     | 
    
         
            +
                      raise BorgError, "Refusing to delete system path: #{real_path}"
         
     | 
| 
      
 105 
     | 
    
         
            +
                    end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                    if File.directory?(real_path)
         
     | 
| 
      
 108 
     | 
    
         
            +
                      FileUtils.rm_rf(real_path, secure: true)
         
     | 
| 
      
 109 
     | 
    
         
            +
                    elsif File.file?(real_path)
         
     | 
| 
      
 110 
     | 
    
         
            +
                      FileUtils.rm(real_path)
         
     | 
| 
      
 111 
     | 
    
         
            +
                    end
         
     | 
| 
      
 112 
     | 
    
         
            +
                  end
         
     | 
| 
      
 113 
     | 
    
         
            +
                end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                def validate_destination_path(destination)
         
     | 
| 
      
 116 
     | 
    
         
            +
                  # Expand and normalize the path
         
     | 
| 
      
 117 
     | 
    
         
            +
                  normalized_path = File.expand_path(destination)
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                  # Security check: prevent path traversal to sensitive directories
         
     | 
| 
      
 120 
     | 
    
         
            +
                  forbidden_paths = ["/", "/bin", "/sbin", "/usr", "/etc", "/sys", "/proc", "/boot"]
         
     | 
| 
      
 121 
     | 
    
         
            +
                  forbidden_paths.each do |forbidden|
         
     | 
| 
      
 122 
     | 
    
         
            +
                    if normalized_path == forbidden || normalized_path.start_with?("#{forbidden}/")
         
     | 
| 
      
 123 
     | 
    
         
            +
                      raise BorgError, "Invalid destination: refusing to extract to system directory #{normalized_path}"
         
     | 
| 
      
 124 
     | 
    
         
            +
                    end
         
     | 
| 
      
 125 
     | 
    
         
            +
                  end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                  normalized_path
         
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                def validate_backup_paths(paths)
         
     | 
| 
      
 131 
     | 
    
         
            +
                  raise BorgError, "No backup paths specified" if paths.nil? || paths.empty?
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                  paths.map do |path|
         
     | 
| 
      
 134 
     | 
    
         
            +
                    raise BorgError, "Empty backup path specified" if path.nil? || path.to_s.strip.empty?
         
     | 
| 
      
 135 
     | 
    
         
            +
                    File.expand_path(path)
         
     | 
| 
       87 
136 
     | 
    
         
             
                  end
         
     | 
| 
       88 
137 
     | 
    
         
             
                end
         
     | 
| 
       89 
138 
     | 
    
         
             
              end
         
     | 
    
        data/lib/ruborg/cli.rb
    CHANGED
    
    | 
         @@ -17,10 +17,14 @@ module Ruborg 
     | 
|
| 
       17 
17 
     | 
    
         
             
                    # Try to load config to get log_file setting
         
     | 
| 
       18 
18 
     | 
    
         
             
                    config_path = options[:config] || "ruborg.yml"
         
     | 
| 
       19 
19 
     | 
    
         
             
                    if File.exist?(config_path)
         
     | 
| 
       20 
     | 
    
         
            -
                      config_data = YAML. 
     | 
| 
      
 20 
     | 
    
         
            +
                      config_data = YAML.safe_load_file(config_path, permitted_classes: [Symbol], aliases: true) rescue {}
         
     | 
| 
       21 
21 
     | 
    
         
             
                      log_path = config_data["log_file"]
         
     | 
| 
       22 
22 
     | 
    
         
             
                    end
         
     | 
| 
       23 
23 
     | 
    
         
             
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  # Validate log path if provided
         
     | 
| 
      
 26 
     | 
    
         
            +
                  log_path = validate_log_path(log_path) if log_path
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
       24 
28 
     | 
    
         
             
                  @logger = RuborgLogger.new(log_file: log_path)
         
     | 
| 
       25 
29 
     | 
    
         
             
                end
         
     | 
| 
       26 
30 
     | 
    
         | 
| 
         @@ -63,7 +67,7 @@ module Ruborg 
     | 
|
| 
       63 
67 
     | 
    
         
             
                  config = Config.new(options[:config])
         
     | 
| 
       64 
68 
     | 
    
         
             
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
       65 
69 
     | 
    
         | 
| 
       66 
     | 
    
         
            -
                  repo = Repository.new(config.repository, passphrase: passphrase)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
         
     | 
| 
       67 
71 
     | 
    
         | 
| 
       68 
72 
     | 
    
         
             
                  # Auto-initialize repository if configured
         
     | 
| 
       69 
73 
     | 
    
         
             
                  if config.auto_init? && !repo.exists?
         
     | 
| 
         @@ -88,7 +92,7 @@ module Ruborg 
     | 
|
| 
       88 
92 
     | 
    
         
             
                  config = Config.new(options[:config])
         
     | 
| 
       89 
93 
     | 
    
         
             
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
       90 
94 
     | 
    
         | 
| 
       91 
     | 
    
         
            -
                  repo = Repository.new(config.repository, passphrase: passphrase)
         
     | 
| 
      
 95 
     | 
    
         
            +
                  repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
         
     | 
| 
       92 
96 
     | 
    
         
             
                  backup = Backup.new(repo, config: config)
         
     | 
| 
       93 
97 
     | 
    
         | 
| 
       94 
98 
     | 
    
         
             
                  backup.extract(archive_name, destination: options[:destination], path: options[:path])
         
     | 
| 
         @@ -110,7 +114,7 @@ module Ruborg 
     | 
|
| 
       110 
114 
     | 
    
         
             
                  config = Config.new(options[:config])
         
     | 
| 
       111 
115 
     | 
    
         
             
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
       112 
116 
     | 
    
         | 
| 
       113 
     | 
    
         
            -
                  repo = Repository.new(config.repository, passphrase: passphrase)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
         
     | 
| 
       114 
118 
     | 
    
         | 
| 
       115 
119 
     | 
    
         
             
                  # Auto-initialize repository if configured
         
     | 
| 
       116 
120 
     | 
    
         
             
                  if config.auto_init? && !repo.exists?
         
     | 
| 
         @@ -147,12 +151,37 @@ module Ruborg 
     | 
|
| 
       147 
151 
     | 
    
         
             
                  exit 1
         
     | 
| 
       148 
152 
     | 
    
         
             
                end
         
     | 
| 
       149 
153 
     | 
    
         | 
| 
      
 154 
     | 
    
         
            +
                def validate_log_path(log_path)
         
     | 
| 
      
 155 
     | 
    
         
            +
                  # Expand to absolute path
         
     | 
| 
      
 156 
     | 
    
         
            +
                  normalized_path = File.expand_path(log_path)
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                  # Prevent writing to sensitive system directories
         
     | 
| 
      
 159 
     | 
    
         
            +
                  forbidden_paths = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot"]
         
     | 
| 
      
 160 
     | 
    
         
            +
                  forbidden_paths.each do |forbidden|
         
     | 
| 
      
 161 
     | 
    
         
            +
                    if normalized_path.start_with?("#{forbidden}/")
         
     | 
| 
      
 162 
     | 
    
         
            +
                      raise ConfigError, "Invalid log path: refusing to write to system directory #{normalized_path}"
         
     | 
| 
      
 163 
     | 
    
         
            +
                    end
         
     | 
| 
      
 164 
     | 
    
         
            +
                  end
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                  # Ensure parent directory exists or can be created
         
     | 
| 
      
 167 
     | 
    
         
            +
                  log_dir = File.dirname(normalized_path)
         
     | 
| 
      
 168 
     | 
    
         
            +
                  unless File.directory?(log_dir)
         
     | 
| 
      
 169 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 170 
     | 
    
         
            +
                      FileUtils.mkdir_p(log_dir)
         
     | 
| 
      
 171 
     | 
    
         
            +
                    rescue => e
         
     | 
| 
      
 172 
     | 
    
         
            +
                      raise ConfigError, "Cannot create log directory #{log_dir}: #{e.message}"
         
     | 
| 
      
 173 
     | 
    
         
            +
                    end
         
     | 
| 
      
 174 
     | 
    
         
            +
                  end
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
                  normalized_path
         
     | 
| 
      
 177 
     | 
    
         
            +
                end
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
       150 
179 
     | 
    
         
             
                # Single repository backup (legacy)
         
     | 
| 
       151 
180 
     | 
    
         
             
                def backup_single_repo(config)
         
     | 
| 
       152 
181 
     | 
    
         
             
                  @logger.info("Backing up paths: #{config.backup_paths.join(', ')}")
         
     | 
| 
       153 
182 
     | 
    
         
             
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
       154 
183 
     | 
    
         | 
| 
       155 
     | 
    
         
            -
                  repo = Repository.new(config.repository, passphrase: passphrase)
         
     | 
| 
      
 184 
     | 
    
         
            +
                  repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
         
     | 
| 
       156 
185 
     | 
    
         | 
| 
       157 
186 
     | 
    
         
             
                  # Auto-initialize repository if configured
         
     | 
| 
       158 
187 
     | 
    
         
             
                  if config.auto_init? && !repo.exists?
         
     | 
| 
         @@ -163,9 +192,9 @@ module Ruborg 
     | 
|
| 
       163 
192 
     | 
    
         | 
| 
       164 
193 
     | 
    
         
             
                  backup = Backup.new(repo, config: config)
         
     | 
| 
       165 
194 
     | 
    
         | 
| 
       166 
     | 
    
         
            -
                  archive_name = options[:name]  
     | 
| 
      
 195 
     | 
    
         
            +
                  archive_name = options[:name] ? sanitize_archive_name(options[:name]) : Time.now.strftime("%Y-%m-%d_%H-%M-%S")
         
     | 
| 
       167 
196 
     | 
    
         
             
                  @logger.info("Creating archive: #{archive_name}")
         
     | 
| 
       168 
     | 
    
         
            -
                  backup.create(name:  
     | 
| 
      
 197 
     | 
    
         
            +
                  backup.create(name: archive_name, remove_source: options[:remove_source])
         
     | 
| 
       169 
198 
     | 
    
         
             
                  @logger.info("Backup created successfully: #{archive_name}")
         
     | 
| 
       170 
199 
     | 
    
         | 
| 
       171 
200 
     | 
    
         
             
                  if options[:remove_source]
         
     | 
| 
         @@ -203,7 +232,8 @@ module Ruborg 
     | 
|
| 
       203 
232 
     | 
    
         
             
                  merged_config = global_settings.merge(repo_config)
         
     | 
| 
       204 
233 
     | 
    
         | 
| 
       205 
234 
     | 
    
         
             
                  passphrase = fetch_passphrase_for_repo(merged_config)
         
     | 
| 
       206 
     | 
    
         
            -
                   
     | 
| 
      
 235 
     | 
    
         
            +
                  borg_opts = merged_config["borg_options"] || {}
         
     | 
| 
      
 236 
     | 
    
         
            +
                  repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts)
         
     | 
| 
       207 
237 
     | 
    
         | 
| 
       208 
238 
     | 
    
         
             
                  # Auto-initialize if configured
         
     | 
| 
       209 
239 
     | 
    
         
             
                  auto_init = merged_config["auto_init"] || false
         
     | 
| 
         @@ -217,7 +247,7 @@ module Ruborg 
     | 
|
| 
       217 
247 
     | 
    
         
             
                  backup_config = BackupConfig.new(repo_config, merged_config)
         
     | 
| 
       218 
248 
     | 
    
         
             
                  backup = Backup.new(repo, config: backup_config)
         
     | 
| 
       219 
249 
     | 
    
         | 
| 
       220 
     | 
    
         
            -
                  archive_name = options[:name]  
     | 
| 
      
 250 
     | 
    
         
            +
                  archive_name = options[:name] ? sanitize_archive_name(options[:name]) : "#{repo_name}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}"
         
     | 
| 
       221 
251 
     | 
    
         
             
                  @logger.info("Creating archive: #{archive_name}")
         
     | 
| 
       222 
252 
     | 
    
         | 
| 
       223 
253 
     | 
    
         
             
                  sources = repo_config["sources"] || []
         
     | 
| 
         @@ -237,6 +267,20 @@ module Ruborg 
     | 
|
| 
       237 
267 
     | 
    
         
             
                  Passbolt.new(resource_id: passbolt_config["resource_id"]).get_password
         
     | 
| 
       238 
268 
     | 
    
         
             
                end
         
     | 
| 
       239 
269 
     | 
    
         | 
| 
      
 270 
     | 
    
         
            +
                def sanitize_archive_name(name)
         
     | 
| 
      
 271 
     | 
    
         
            +
                  raise ConfigError, "Archive name cannot be empty" if name.nil? || name.strip.empty?
         
     | 
| 
      
 272 
     | 
    
         
            +
             
     | 
| 
      
 273 
     | 
    
         
            +
                  # Check if name contains at least one valid character before sanitization
         
     | 
| 
      
 274 
     | 
    
         
            +
                  unless name =~ /[a-zA-Z0-9._-]/
         
     | 
| 
      
 275 
     | 
    
         
            +
                    raise ConfigError, "Invalid archive name: must contain at least one valid character (alphanumeric, dot, dash, or underscore)"
         
     | 
| 
      
 276 
     | 
    
         
            +
                  end
         
     | 
| 
      
 277 
     | 
    
         
            +
             
     | 
| 
      
 278 
     | 
    
         
            +
                  # Allow only alphanumeric, dash, underscore, and dot
         
     | 
| 
      
 279 
     | 
    
         
            +
                  sanitized = name.gsub(/[^a-zA-Z0-9._-]/, '_')
         
     | 
| 
      
 280 
     | 
    
         
            +
             
     | 
| 
      
 281 
     | 
    
         
            +
                  sanitized
         
     | 
| 
      
 282 
     | 
    
         
            +
                end
         
     | 
| 
      
 283 
     | 
    
         
            +
             
     | 
| 
       240 
284 
     | 
    
         
             
                # Wrapper class to adapt multi-repo config to existing Backup class
         
     | 
| 
       241 
285 
     | 
    
         
             
                class BackupConfig
         
     | 
| 
       242 
286 
     | 
    
         
             
                  def initialize(repo_config, merged_settings)
         
     | 
    
        data/lib/ruborg/config.rb
    CHANGED
    
    | 
         @@ -17,9 +17,11 @@ module Ruborg 
     | 
|
| 
       17 
17 
     | 
    
         
             
                def load_config
         
     | 
| 
       18 
18 
     | 
    
         
             
                  raise ConfigError, "Configuration file not found: #{@config_path}" unless File.exist?(@config_path)
         
     | 
| 
       19 
19 
     | 
    
         | 
| 
       20 
     | 
    
         
            -
                  @data = YAML. 
     | 
| 
      
 20 
     | 
    
         
            +
                  @data = YAML.safe_load_file(@config_path, permitted_classes: [Symbol], aliases: true)
         
     | 
| 
       21 
21 
     | 
    
         
             
                rescue Psych::SyntaxError => e
         
     | 
| 
       22 
22 
     | 
    
         
             
                  raise ConfigError, "Invalid YAML syntax: #{e.message}"
         
     | 
| 
      
 23 
     | 
    
         
            +
                rescue Psych::DisallowedClass => e
         
     | 
| 
      
 24 
     | 
    
         
            +
                  raise ConfigError, "Invalid YAML content: #{e.message}"
         
     | 
| 
       23 
25 
     | 
    
         
             
                end
         
     | 
| 
       24 
26 
     | 
    
         | 
| 
       25 
27 
     | 
    
         
             
                # Legacy single-repo accessors (for backward compatibility)
         
     | 
| 
         @@ -32,15 +34,18 @@ module Ruborg 
     | 
|
| 
       32 
34 
     | 
    
         
             
                end
         
     | 
| 
       33 
35 
     | 
    
         | 
| 
       34 
36 
     | 
    
         
             
                def exclude_patterns
         
     | 
| 
       35 
     | 
    
         
            -
                  @data["exclude_patterns"] || []
         
     | 
| 
      
 37 
     | 
    
         
            +
                  patterns = @data["exclude_patterns"] || []
         
     | 
| 
      
 38 
     | 
    
         
            +
                  validate_exclude_patterns(patterns)
         
     | 
| 
       36 
39 
     | 
    
         
             
                end
         
     | 
| 
       37 
40 
     | 
    
         | 
| 
       38 
41 
     | 
    
         
             
                def compression
         
     | 
| 
       39 
     | 
    
         
            -
                  @data["compression"] || "lz4"
         
     | 
| 
      
 42 
     | 
    
         
            +
                  value = @data["compression"] || "lz4"
         
     | 
| 
      
 43 
     | 
    
         
            +
                  validate_compression(value)
         
     | 
| 
       40 
44 
     | 
    
         
             
                end
         
     | 
| 
       41 
45 
     | 
    
         | 
| 
       42 
46 
     | 
    
         
             
                def encryption_mode
         
     | 
| 
       43 
     | 
    
         
            -
                  @data["encryption"] || "repokey"
         
     | 
| 
      
 47 
     | 
    
         
            +
                  value = @data["encryption"] || "repokey"
         
     | 
| 
      
 48 
     | 
    
         
            +
                  validate_encryption(value)
         
     | 
| 
       44 
49 
     | 
    
         
             
                end
         
     | 
| 
       45 
50 
     | 
    
         | 
| 
       46 
51 
     | 
    
         
             
                def passbolt_integration
         
     | 
| 
         @@ -55,6 +60,10 @@ module Ruborg 
     | 
|
| 
       55 
60 
     | 
    
         
             
                  @data["log_file"]
         
     | 
| 
       56 
61 
     | 
    
         
             
                end
         
     | 
| 
       57 
62 
     | 
    
         | 
| 
      
 63 
     | 
    
         
            +
                def borg_options
         
     | 
| 
      
 64 
     | 
    
         
            +
                  @data["borg_options"] || {}
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
       58 
67 
     | 
    
         
             
                # New multi-repo support
         
     | 
| 
       59 
68 
     | 
    
         
             
                def multi_repo?
         
     | 
| 
       60 
69 
     | 
    
         
             
                  @multi_repo
         
     | 
| 
         @@ -81,8 +90,42 @@ module Ruborg 
     | 
|
| 
       81 
90 
     | 
    
         | 
| 
       82 
91 
     | 
    
         
             
                private
         
     | 
| 
       83 
92 
     | 
    
         | 
| 
      
 93 
     | 
    
         
            +
                VALID_COMPRESSION = ["lz4", "zstd", "zlib", "lzma", "none"].freeze
         
     | 
| 
      
 94 
     | 
    
         
            +
                VALID_ENCRYPTION = ["repokey", "keyfile", "none", "authenticated", "repokey-blake2",
         
     | 
| 
      
 95 
     | 
    
         
            +
                                    "keyfile-blake2", "authenticated-blake2"].freeze
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
       84 
97 
     | 
    
         
             
                def detect_format
         
     | 
| 
       85 
98 
     | 
    
         
             
                  @multi_repo = @data.key?("repositories")
         
     | 
| 
       86 
99 
     | 
    
         
             
                end
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                def validate_compression(compression)
         
     | 
| 
      
 102 
     | 
    
         
            +
                  unless VALID_COMPRESSION.include?(compression)
         
     | 
| 
      
 103 
     | 
    
         
            +
                    raise ConfigError, "Invalid compression '#{compression}'. Must be one of: #{VALID_COMPRESSION.join(', ')}"
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
                  compression
         
     | 
| 
      
 106 
     | 
    
         
            +
                end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                def validate_encryption(encryption)
         
     | 
| 
      
 109 
     | 
    
         
            +
                  unless VALID_ENCRYPTION.include?(encryption)
         
     | 
| 
      
 110 
     | 
    
         
            +
                    raise ConfigError, "Invalid encryption mode '#{encryption}'. Must be one of: #{VALID_ENCRYPTION.join(', ')}"
         
     | 
| 
      
 111 
     | 
    
         
            +
                  end
         
     | 
| 
      
 112 
     | 
    
         
            +
                  encryption
         
     | 
| 
      
 113 
     | 
    
         
            +
                end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                def validate_exclude_patterns(patterns)
         
     | 
| 
      
 116 
     | 
    
         
            +
                  return patterns if patterns.empty?
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                  patterns.each do |pattern|
         
     | 
| 
      
 119 
     | 
    
         
            +
                    if pattern.nil? || pattern.to_s.strip.empty?
         
     | 
| 
      
 120 
     | 
    
         
            +
                      raise ConfigError, "Exclude pattern cannot be empty or nil"
         
     | 
| 
      
 121 
     | 
    
         
            +
                    end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                    if pattern.length > 1000
         
     | 
| 
      
 124 
     | 
    
         
            +
                      raise ConfigError, "Exclude pattern too long (max 1000 characters): #{pattern[0..50]}..."
         
     | 
| 
      
 125 
     | 
    
         
            +
                    end
         
     | 
| 
      
 126 
     | 
    
         
            +
                  end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                  patterns
         
     | 
| 
      
 129 
     | 
    
         
            +
                end
         
     | 
| 
       87 
130 
     | 
    
         
             
              end
         
     | 
| 
       88 
131 
     | 
    
         
             
            end
         
     | 
    
        data/lib/ruborg/logger.rb
    CHANGED
    
    | 
         @@ -10,7 +10,7 @@ module Ruborg 
     | 
|
| 
       10 
10 
     | 
    
         | 
| 
       11 
11 
     | 
    
         
             
                def initialize(log_file: nil)
         
     | 
| 
       12 
12 
     | 
    
         
             
                  @log_file = log_file || default_log_file
         
     | 
| 
       13 
     | 
    
         
            -
                   
     | 
| 
      
 13 
     | 
    
         
            +
                  validate_and_ensure_log_directory
         
     | 
| 
       14 
14 
     | 
    
         
             
                  @logger = Logger.new(@log_file, "daily")
         
     | 
| 
       15 
15 
     | 
    
         
             
                  @logger.level = Logger::INFO
         
     | 
| 
       16 
16 
     | 
    
         
             
                  @logger.formatter = proc do |severity, datetime, progname, msg|
         
     | 
| 
         @@ -45,8 +45,21 @@ module Ruborg 
     | 
|
| 
       45 
45 
     | 
    
         
             
                  dir
         
     | 
| 
       46 
46 
     | 
    
         
             
                end
         
     | 
| 
       47 
47 
     | 
    
         | 
| 
       48 
     | 
    
         
            -
                def  
     | 
| 
       49 
     | 
    
         
            -
                   
     | 
| 
      
 48 
     | 
    
         
            +
                def validate_and_ensure_log_directory
         
     | 
| 
      
 49 
     | 
    
         
            +
                  # Validate log file path for security
         
     | 
| 
      
 50 
     | 
    
         
            +
                  normalized_path = File.expand_path(@log_file)
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  # Prevent writing to sensitive system directories
         
     | 
| 
      
 53 
     | 
    
         
            +
                  forbidden_paths = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot"]
         
     | 
| 
      
 54 
     | 
    
         
            +
                  forbidden_paths.each do |forbidden|
         
     | 
| 
      
 55 
     | 
    
         
            +
                    if normalized_path.start_with?("#{forbidden}/")
         
     | 
| 
      
 56 
     | 
    
         
            +
                      raise ConfigError, "Invalid log path: refusing to write to system directory #{normalized_path}"
         
     | 
| 
      
 57 
     | 
    
         
            +
                    end
         
     | 
| 
      
 58 
     | 
    
         
            +
                  end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                  # Ensure log directory exists
         
     | 
| 
      
 61 
     | 
    
         
            +
                  log_dir = File.dirname(normalized_path)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  FileUtils.mkdir_p(log_dir) unless File.directory?(log_dir)
         
     | 
| 
       50 
63 
     | 
    
         
             
                end
         
     | 
| 
       51 
64 
     | 
    
         
             
              end
         
     | 
| 
       52 
65 
     | 
    
         
             
            end
         
     | 
    
        data/lib/ruborg/passbolt.rb
    CHANGED
    
    
    
        data/lib/ruborg/repository.rb
    CHANGED
    
    | 
         @@ -5,9 +5,10 @@ module Ruborg 
     | 
|
| 
       5 
5 
     | 
    
         
             
              class Repository
         
     | 
| 
       6 
6 
     | 
    
         
             
                attr_reader :path
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
     | 
    
         
            -
                def initialize(path, passphrase: nil)
         
     | 
| 
       9 
     | 
    
         
            -
                  @path = path
         
     | 
| 
      
 8 
     | 
    
         
            +
                def initialize(path, passphrase: nil, borg_options: {})
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @path = validate_repo_path(path)
         
     | 
| 
       10 
10 
     | 
    
         
             
                  @passphrase = passphrase
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @borg_options = borg_options
         
     | 
| 
       11 
12 
     | 
    
         
             
                end
         
     | 
| 
       12 
13 
     | 
    
         | 
| 
       13 
14 
     | 
    
         
             
                def exists?
         
     | 
| 
         @@ -37,11 +38,32 @@ module Ruborg 
     | 
|
| 
       37 
38 
     | 
    
         | 
| 
       38 
39 
     | 
    
         
             
                private
         
     | 
| 
       39 
40 
     | 
    
         | 
| 
      
 41 
     | 
    
         
            +
                def validate_repo_path(path)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  raise BorgError, "Repository path cannot be empty" if path.nil? || path.empty?
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  normalized = File.expand_path(path)
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  # Prevent repository creation in critical system directories
         
     | 
| 
      
 47 
     | 
    
         
            +
                  forbidden = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot", "/dev"]
         
     | 
| 
      
 48 
     | 
    
         
            +
                  forbidden.each do |forbidden_path|
         
     | 
| 
      
 49 
     | 
    
         
            +
                    if normalized == forbidden_path || normalized.start_with?("#{forbidden_path}/")
         
     | 
| 
      
 50 
     | 
    
         
            +
                      raise BorgError, "Invalid repository path: refusing to use system directory #{normalized}"
         
     | 
| 
      
 51 
     | 
    
         
            +
                    end
         
     | 
| 
      
 52 
     | 
    
         
            +
                  end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  normalized
         
     | 
| 
      
 55 
     | 
    
         
            +
                end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
       40 
57 
     | 
    
         
             
                def execute_borg_command(cmd)
         
     | 
| 
       41 
58 
     | 
    
         
             
                  env = {}
         
     | 
| 
       42 
59 
     | 
    
         
             
                  env["BORG_PASSPHRASE"] = @passphrase if @passphrase
         
     | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
                   
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  # Apply Borg environment options from config (defaults to yes for backward compatibility)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  allow_relocated = @borg_options.fetch("allow_relocated_repo", true)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  allow_unencrypted = @borg_options.fetch("allow_unencrypted_repo", true)
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = allow_relocated ? "yes" : "no"
         
     | 
| 
      
 66 
     | 
    
         
            +
                  env["BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK"] = allow_unencrypted ? "yes" : "no"
         
     | 
| 
       45 
67 
     | 
    
         | 
| 
       46 
68 
     | 
    
         
             
                  # Redirect stdin from /dev/null to prevent interactive prompts
         
     | 
| 
       47 
69 
     | 
    
         
             
                  result = system(env, *cmd, in: "/dev/null")
         
     | 
    
        data/lib/ruborg/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: ruborg
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0.3. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.3.1
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Michail Pantelelis
         
     | 
| 
         @@ -110,6 +110,7 @@ files: 
     | 
|
| 
       110 
110 
     | 
    
         
             
            - LICENSE
         
     | 
| 
       111 
111 
     | 
    
         
             
            - README.md
         
     | 
| 
       112 
112 
     | 
    
         
             
            - Rakefile
         
     | 
| 
      
 113 
     | 
    
         
            +
            - SECURITY.md
         
     | 
| 
       113 
114 
     | 
    
         
             
            - exe/ruborg
         
     | 
| 
       114 
115 
     | 
    
         
             
            - lib/ruborg.rb
         
     | 
| 
       115 
116 
     | 
    
         
             
            - lib/ruborg/backup.rb
         
     |