ruborg 0.2.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 +48 -0
 - data/README.md +148 -14
 - data/SECURITY.md +173 -0
 - data/lib/ruborg/backup.rb +56 -7
 - data/lib/ruborg/cli.rb +203 -19
 - data/lib/ruborg/config.rb +87 -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,54 @@ 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 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            ## [0.3.0] - 2025-10-05
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 39 
     | 
    
         
            +
            - Auto-initialization feature: Set `auto_init: true` in config to automatically initialize repositories on first use
         
     | 
| 
      
 40 
     | 
    
         
            +
            - Multi-repository configuration support with per-repository sources
         
     | 
| 
      
 41 
     | 
    
         
            +
            - `--repository` / `-r` option to target specific repository in multi-repo configs
         
     | 
| 
      
 42 
     | 
    
         
            +
            - `--all` option to backup all repositories at once
         
     | 
| 
      
 43 
     | 
    
         
            +
            - Repository-specific Passbolt integration (overrides global settings)
         
     | 
| 
      
 44 
     | 
    
         
            +
            - Per-source exclude patterns in multi-repo configs
         
     | 
| 
      
 45 
     | 
    
         
            +
            - BackupConfig wrapper class for multi-repo compatibility
         
     | 
| 
      
 46 
     | 
    
         
            +
            - Automatic format detection (single vs multi-repo)
         
     | 
| 
      
 47 
     | 
    
         
            +
            - Support for multiple backup sources per repository
         
     | 
| 
      
 48 
     | 
    
         
            +
            - Global settings with per-repository overrides
         
     | 
| 
      
 49 
     | 
    
         
            +
            - `log_file` configuration option to set log path in config file
         
     | 
| 
      
 50 
     | 
    
         
            +
            - Log file priority: CLI option > config file > default
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 53 
     | 
    
         
            +
            - Config class now detects and handles both single-repo and multi-repo formats
         
     | 
| 
      
 54 
     | 
    
         
            +
            - Backup command automatically routes to single or multi-repo implementation
         
     | 
| 
      
 55 
     | 
    
         
            +
            - Archive naming includes repository name for multi-repo configs
         
     | 
| 
      
 56 
     | 
    
         
            +
            - CLI now reads log_file from config if --log option not provided
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
       10 
58 
     | 
    
         
             
            ## [0.2.0] - 2025-10-05
         
     | 
| 
       11 
59 
     | 
    
         | 
| 
       12 
60 
     | 
    
         
             
            ### Added
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -15,7 +15,10 @@ A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg 
     | 
|
| 
       15 
15 
     | 
    
         
             
            - ๐๏ธ **Selective Restore** - Restore individual files or directories from archives
         
     | 
| 
       16 
16 
     | 
    
         
             
            - ๐งน **Auto-cleanup** - Optionally remove source files after successful backup
         
     | 
| 
       17 
17 
     | 
    
         
             
            - ๐ **Logging** - Comprehensive logging with daily rotation
         
     | 
| 
      
 18 
     | 
    
         
            +
            - ๐๏ธ **Multi-Repository** - Manage multiple backup repositories with different sources
         
     | 
| 
      
 19 
     | 
    
         
            +
            - ๐ **Auto-initialization** - Automatically initialize repositories on first use
         
     | 
| 
       18 
20 
     | 
    
         
             
            - โ
 **Well-tested** - Comprehensive test suite with RSpec
         
     | 
| 
      
 21 
     | 
    
         
            +
            - ๐ **Security-focused** - Path validation, safe YAML loading, command injection protection
         
     | 
| 
       19 
22 
     | 
    
         | 
| 
       20 
23 
     | 
    
         
             
            ## Prerequisites
         
     | 
| 
       21 
24 
     | 
    
         | 
| 
         @@ -63,7 +66,9 @@ gem install ruborg 
     | 
|
| 
       63 
66 
     | 
    
         | 
| 
       64 
67 
     | 
    
         
             
            ## Configuration
         
     | 
| 
       65 
68 
     | 
    
         | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
      
 69 
     | 
    
         
            +
            Ruborg supports two configuration formats: **single repository** (legacy) and **multi-repository** (recommended for complex setups).
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
            ### Single Repository Configuration
         
     | 
| 
       67 
72 
     | 
    
         | 
| 
       68 
73 
     | 
    
         
             
            ```yaml
         
     | 
| 
       69 
74 
     | 
    
         
             
            # Repository path
         
     | 
| 
         @@ -73,15 +78,11 @@ repository: /path/to/borg/repository 
     | 
|
| 
       73 
78 
     | 
    
         
             
            backup_paths:
         
     | 
| 
       74 
79 
     | 
    
         
             
              - /home/user/documents
         
     | 
| 
       75 
80 
     | 
    
         
             
              - /home/user/projects
         
     | 
| 
       76 
     | 
    
         
            -
              - /etc
         
     | 
| 
       77 
81 
     | 
    
         | 
| 
       78 
82 
     | 
    
         
             
            # Exclude patterns
         
     | 
| 
       79 
83 
     | 
    
         
             
            exclude_patterns:
         
     | 
| 
       80 
84 
     | 
    
         
             
              - "*.tmp"
         
     | 
| 
       81 
85 
     | 
    
         
             
              - "*.log"
         
     | 
| 
       82 
     | 
    
         
            -
              - "*/.cache/*"
         
     | 
| 
       83 
     | 
    
         
            -
              - "*/node_modules/*"
         
     | 
| 
       84 
     | 
    
         
            -
              - "*/.git/*"
         
     | 
| 
       85 
86 
     | 
    
         | 
| 
       86 
87 
     | 
    
         
             
            # Compression algorithm (lz4, zstd, zlib, lzma, none)
         
     | 
| 
       87 
88 
     | 
    
         
             
            compression: lz4
         
     | 
| 
         @@ -92,9 +93,70 @@ encryption: repokey 
     | 
|
| 
       92 
93 
     | 
    
         
             
            # Passbolt integration (optional)
         
     | 
| 
       93 
94 
     | 
    
         
             
            passbolt:
         
     | 
| 
       94 
95 
     | 
    
         
             
              resource_id: "your-passbolt-resource-uuid"
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
            # Auto-initialize repository (optional, default: false)
         
     | 
| 
      
 98 
     | 
    
         
            +
            auto_init: true
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
            # Log file path (optional, default: ~/.ruborg/logs/ruborg.log)
         
     | 
| 
      
 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)
         
     | 
| 
      
 107 
     | 
    
         
            +
            ```
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
            ### Multi-Repository Configuration
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
            For managing multiple repositories with different sources:
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
            ```yaml
         
     | 
| 
      
 114 
     | 
    
         
            +
            # Global settings (applied to all repositories unless overridden)
         
     | 
| 
      
 115 
     | 
    
         
            +
            compression: lz4
         
     | 
| 
      
 116 
     | 
    
         
            +
            encryption: repokey
         
     | 
| 
      
 117 
     | 
    
         
            +
            auto_init: true
         
     | 
| 
      
 118 
     | 
    
         
            +
            passbolt:
         
     | 
| 
      
 119 
     | 
    
         
            +
              resource_id: "global-passbolt-id"
         
     | 
| 
      
 120 
     | 
    
         
            +
            borg_options:
         
     | 
| 
      
 121 
     | 
    
         
            +
              allow_relocated_repo: false
         
     | 
| 
      
 122 
     | 
    
         
            +
              allow_unencrypted_repo: false
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
            # Multiple repositories
         
     | 
| 
      
 125 
     | 
    
         
            +
            repositories:
         
     | 
| 
      
 126 
     | 
    
         
            +
              - name: documents
         
     | 
| 
      
 127 
     | 
    
         
            +
                path: /mnt/backup/documents
         
     | 
| 
      
 128 
     | 
    
         
            +
                sources:
         
     | 
| 
      
 129 
     | 
    
         
            +
                  - name: home-docs
         
     | 
| 
      
 130 
     | 
    
         
            +
                    paths:
         
     | 
| 
      
 131 
     | 
    
         
            +
                      - /home/user/documents
         
     | 
| 
      
 132 
     | 
    
         
            +
                    exclude:
         
     | 
| 
      
 133 
     | 
    
         
            +
                      - "*.tmp"
         
     | 
| 
      
 134 
     | 
    
         
            +
                  - name: work-docs
         
     | 
| 
      
 135 
     | 
    
         
            +
                    paths:
         
     | 
| 
      
 136 
     | 
    
         
            +
                      - /home/user/work
         
     | 
| 
      
 137 
     | 
    
         
            +
                    exclude:
         
     | 
| 
      
 138 
     | 
    
         
            +
                      - "*.log"
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
              - name: databases
         
     | 
| 
      
 141 
     | 
    
         
            +
                path: /mnt/backup/databases
         
     | 
| 
      
 142 
     | 
    
         
            +
                # Repository-specific passbolt (overrides global)
         
     | 
| 
      
 143 
     | 
    
         
            +
                passbolt:
         
     | 
| 
      
 144 
     | 
    
         
            +
                  resource_id: "db-specific-passbolt-id"
         
     | 
| 
      
 145 
     | 
    
         
            +
                sources:
         
     | 
| 
      
 146 
     | 
    
         
            +
                  - name: mysql
         
     | 
| 
      
 147 
     | 
    
         
            +
                    paths:
         
     | 
| 
      
 148 
     | 
    
         
            +
                      - /var/lib/mysql/dumps
         
     | 
| 
      
 149 
     | 
    
         
            +
                  - name: postgres
         
     | 
| 
      
 150 
     | 
    
         
            +
                    paths:
         
     | 
| 
      
 151 
     | 
    
         
            +
                      - /var/lib/postgresql/dumps
         
     | 
| 
       95 
152 
     | 
    
         
             
            ```
         
     | 
| 
       96 
153 
     | 
    
         | 
| 
       97 
     | 
    
         
            -
             
     | 
| 
      
 154 
     | 
    
         
            +
            **Multi-repo benefits:**
         
     | 
| 
      
 155 
     | 
    
         
            +
            - Organize backups by type (documents, databases, media)
         
     | 
| 
      
 156 
     | 
    
         
            +
            - Different encryption keys per repository
         
     | 
| 
      
 157 
     | 
    
         
            +
            - Multiple sources per repository
         
     | 
| 
      
 158 
     | 
    
         
            +
            - Per-source exclude patterns
         
     | 
| 
      
 159 
     | 
    
         
            +
            - Repository-specific settings override global ones
         
     | 
| 
       98 
160 
     | 
    
         | 
| 
       99 
161 
     | 
    
         
             
            ## Usage
         
     | 
| 
       100 
162 
     | 
    
         | 
| 
         @@ -110,6 +172,7 @@ ruborg init /path/to/repository --passbolt-id "resource-uuid" 
     | 
|
| 
       110 
172 
     | 
    
         | 
| 
       111 
173 
     | 
    
         
             
            ### Create a Backup
         
     | 
| 
       112 
174 
     | 
    
         | 
| 
      
 175 
     | 
    
         
            +
            **Single repository:**
         
     | 
| 
       113 
176 
     | 
    
         
             
            ```bash
         
     | 
| 
       114 
177 
     | 
    
         
             
            # Using default configuration (ruborg.yml)
         
     | 
| 
       115 
178 
     | 
    
         
             
            ruborg backup
         
     | 
| 
         @@ -124,6 +187,18 @@ ruborg backup --name "my-backup-2025-10-04" 
     | 
|
| 
       124 
187 
     | 
    
         
             
            ruborg backup --remove-source
         
     | 
| 
       125 
188 
     | 
    
         
             
            ```
         
     | 
| 
       126 
189 
     | 
    
         | 
| 
      
 190 
     | 
    
         
            +
            **Multi-repository:**
         
     | 
| 
      
 191 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 192 
     | 
    
         
            +
            # Backup specific repository
         
     | 
| 
      
 193 
     | 
    
         
            +
            ruborg backup --repository documents
         
     | 
| 
      
 194 
     | 
    
         
            +
             
     | 
| 
      
 195 
     | 
    
         
            +
            # Backup all repositories
         
     | 
| 
      
 196 
     | 
    
         
            +
            ruborg backup --all
         
     | 
| 
      
 197 
     | 
    
         
            +
             
     | 
| 
      
 198 
     | 
    
         
            +
            # Backup specific repository with custom name
         
     | 
| 
      
 199 
     | 
    
         
            +
            ruborg backup --repository databases --name "db-backup-2025-10-05"
         
     | 
| 
      
 200 
     | 
    
         
            +
            ```
         
     | 
| 
      
 201 
     | 
    
         
            +
             
     | 
| 
       127 
202 
     | 
    
         
             
            ### List Archives
         
     | 
| 
       128 
203 
     | 
    
         | 
| 
       129 
204 
     | 
    
         
             
            ```bash
         
     | 
| 
         @@ -151,13 +226,23 @@ ruborg info 
     | 
|
| 
       151 
226 
     | 
    
         | 
| 
       152 
227 
     | 
    
         
             
            ## Logging
         
     | 
| 
       153 
228 
     | 
    
         | 
| 
       154 
     | 
    
         
            -
            Ruborg automatically logs all operations  
     | 
| 
      
 229 
     | 
    
         
            +
            Ruborg automatically logs all operations with daily rotation. Log file location priority:
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
      
 231 
     | 
    
         
            +
            1. **CLI option** (highest priority): `--log /path/to/custom.log`
         
     | 
| 
      
 232 
     | 
    
         
            +
            2. **Config file**: `log_file: /path/to/log.log`
         
     | 
| 
      
 233 
     | 
    
         
            +
            3. **Default**: `~/.ruborg/logs/ruborg.log`
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
            **Examples:**
         
     | 
| 
       155 
236 
     | 
    
         | 
| 
       156 
237 
     | 
    
         
             
            ```bash
         
     | 
| 
       157 
     | 
    
         
            -
             
     | 
| 
      
 238 
     | 
    
         
            +
            # Use CLI option (overrides config)
         
     | 
| 
      
 239 
     | 
    
         
            +
            ruborg backup --log /var/log/ruborg.log
         
     | 
| 
      
 240 
     | 
    
         
            +
             
     | 
| 
      
 241 
     | 
    
         
            +
            # Or set in config file
         
     | 
| 
      
 242 
     | 
    
         
            +
            log_file: /var/log/ruborg.log
         
     | 
| 
       158 
243 
     | 
    
         
             
            ```
         
     | 
| 
       159 
244 
     | 
    
         | 
| 
       160 
     | 
    
         
            -
            Logs include 
     | 
| 
      
 245 
     | 
    
         
            +
            **Logs include:**
         
     | 
| 
       161 
246 
     | 
    
         
             
            - Operation start/completion timestamps
         
     | 
| 
       162 
247 
     | 
    
         
             
            - Paths being backed up
         
     | 
| 
       163 
248 
     | 
    
         
             
            - Archive names created
         
     | 
| 
         @@ -191,20 +276,68 @@ passbolt: 
     | 
|
| 
       191 
276 
     | 
    
         | 
| 
       192 
277 
     | 
    
         
             
            Ruborg will automatically retrieve the passphrase when performing backup operations.
         
     | 
| 
       193 
278 
     | 
    
         | 
| 
      
 279 
     | 
    
         
            +
            ## Auto-initialization
         
     | 
| 
      
 280 
     | 
    
         
            +
             
     | 
| 
      
 281 
     | 
    
         
            +
            Set `auto_init: true` in your configuration file to automatically initialize the repository on first use:
         
     | 
| 
      
 282 
     | 
    
         
            +
             
     | 
| 
      
 283 
     | 
    
         
            +
            ```yaml
         
     | 
| 
      
 284 
     | 
    
         
            +
            repository: /path/to/borg/repository
         
     | 
| 
      
 285 
     | 
    
         
            +
            auto_init: true
         
     | 
| 
      
 286 
     | 
    
         
            +
            passbolt:
         
     | 
| 
      
 287 
     | 
    
         
            +
              resource_id: "your-passbolt-resource-uuid"
         
     | 
| 
      
 288 
     | 
    
         
            +
            backup_paths:
         
     | 
| 
      
 289 
     | 
    
         
            +
              - /path/to/backup
         
     | 
| 
      
 290 
     | 
    
         
            +
            ```
         
     | 
| 
      
 291 
     | 
    
         
            +
             
     | 
| 
      
 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.
         
     | 
| 
      
 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 
     | 
    
         
            +
             
     | 
| 
       194 
321 
     | 
    
         
             
            ## Command Reference
         
     | 
| 
       195 
322 
     | 
    
         | 
| 
       196 
323 
     | 
    
         
             
            | Command | Description | Options |
         
     | 
| 
       197 
324 
     | 
    
         
             
            |---------|-------------|---------|
         
     | 
| 
       198 
325 
     | 
    
         
             
            | `init REPOSITORY` | Initialize a new Borg repository | `--passphrase`, `--passbolt-id`, `--log` |
         
     | 
| 
       199 
     | 
    
         
            -
            | `backup` | Create a backup using config file | `--config`, `--name`, `--remove-source`, `--log` |
         
     | 
| 
       200 
     | 
    
         
            -
            | `list` | List all archives in repository | `--config`, `--log` |
         
     | 
| 
       201 
     | 
    
         
            -
            | `restore ARCHIVE` | Restore files from archive | `--config`, `--destination`, `--path`, `--log` |
         
     | 
| 
       202 
     | 
    
         
            -
            | `info` | Show repository information | `--config`, `--log` |
         
     | 
| 
      
 326 
     | 
    
         
            +
            | `backup` | Create a backup using config file | `--config`, `--name`, `--remove-source`, `--repository`, `--all`, `--log` |
         
     | 
| 
      
 327 
     | 
    
         
            +
            | `list` | List all archives in repository | `--config`, `--repository`, `--log` |
         
     | 
| 
      
 328 
     | 
    
         
            +
            | `restore ARCHIVE` | Restore files from archive | `--config`, `--destination`, `--path`, `--repository`, `--log` |
         
     | 
| 
      
 329 
     | 
    
         
            +
            | `info` | Show repository information | `--config`, `--repository`, `--log` |
         
     | 
| 
       203 
330 
     | 
    
         | 
| 
       204 
331 
     | 
    
         
             
            ### Global Options
         
     | 
| 
       205 
332 
     | 
    
         | 
| 
       206 
333 
     | 
    
         
             
            - `--config`: Path to configuration file (default: `ruborg.yml`)
         
     | 
| 
       207 
     | 
    
         
            -
            - `--log`: Path to log file (default: `~/.ruborg/logs/ruborg.log`)
         
     | 
| 
      
 334 
     | 
    
         
            +
            - `--log`: Path to log file (overrides config, default: `~/.ruborg/logs/ruborg.log`)
         
     | 
| 
      
 335 
     | 
    
         
            +
            - `--repository` / `-r`: Repository name (required for multi-repo configs)
         
     | 
| 
      
 336 
     | 
    
         
            +
             
     | 
| 
      
 337 
     | 
    
         
            +
            ### Multi-Repository Options
         
     | 
| 
      
 338 
     | 
    
         
            +
             
     | 
| 
      
 339 
     | 
    
         
            +
            - `--all`: Backup all repositories (multi-repo config only)
         
     | 
| 
      
 340 
     | 
    
         
            +
            - `--repository NAME`: Target specific repository by name
         
     | 
| 
       208 
341 
     | 
    
         | 
| 
       209 
342 
     | 
    
         
             
            ## Development
         
     | 
| 
       210 
343 
     | 
    
         | 
| 
         @@ -244,6 +377,7 @@ The test suite includes: 
     | 
|
| 
       244 
377 
     | 
    
         
             
            - Passbolt integration (mocked)
         
     | 
| 
       245 
378 
     | 
    
         
             
            - CLI commands
         
     | 
| 
       246 
379 
     | 
    
         
             
            - Logging functionality
         
     | 
| 
      
 380 
     | 
    
         
            +
            - Comprehensive security tests (path validation, sanitization, etc.)
         
     | 
| 
       247 
381 
     | 
    
         | 
| 
       248 
382 
     | 
    
         
             
            ## Contributing
         
     | 
| 
       249 
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
    
    | 
         @@ -7,10 +7,25 @@ module Ruborg 
     | 
|
| 
       7 
7 
     | 
    
         
             
              class CLI < Thor
         
     | 
| 
       8 
8 
     | 
    
         
             
                class_option :config, type: :string, default: "ruborg.yml", desc: "Path to configuration file"
         
     | 
| 
       9 
9 
     | 
    
         
             
                class_option :log, type: :string, desc: "Path to log file"
         
     | 
| 
      
 10 
     | 
    
         
            +
                class_option :repository, type: :string, aliases: "-r", desc: "Repository name (for multi-repo configs)"
         
     | 
| 
       10 
11 
     | 
    
         | 
| 
       11 
12 
     | 
    
         
             
                def initialize(*args)
         
     | 
| 
       12 
13 
     | 
    
         
             
                  super
         
     | 
| 
       13 
     | 
    
         
            -
                   
     | 
| 
      
 14 
     | 
    
         
            +
                  # Priority: CLI option > config file > default
         
     | 
| 
      
 15 
     | 
    
         
            +
                  log_path = options[:log]
         
     | 
| 
      
 16 
     | 
    
         
            +
                  unless log_path
         
     | 
| 
      
 17 
     | 
    
         
            +
                    # Try to load config to get log_file setting
         
     | 
| 
      
 18 
     | 
    
         
            +
                    config_path = options[:config] || "ruborg.yml"
         
     | 
| 
      
 19 
     | 
    
         
            +
                    if File.exist?(config_path)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      config_data = YAML.safe_load_file(config_path, permitted_classes: [Symbol], aliases: true) rescue {}
         
     | 
| 
      
 21 
     | 
    
         
            +
                      log_path = config_data["log_file"]
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  # Validate log path if provided
         
     | 
| 
      
 26 
     | 
    
         
            +
                  log_path = validate_log_path(log_path) if log_path
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  @logger = RuborgLogger.new(log_file: log_path)
         
     | 
| 
       14 
29 
     | 
    
         
             
                end
         
     | 
| 
       15 
30 
     | 
    
         | 
| 
       16 
31 
     | 
    
         
             
                desc "init REPOSITORY", "Initialize a new Borg repository"
         
     | 
| 
         @@ -31,26 +46,16 @@ module Ruborg 
     | 
|
| 
       31 
46 
     | 
    
         
             
                desc "backup", "Create a backup using configuration file"
         
     | 
| 
       32 
47 
     | 
    
         
             
                option :name, type: :string, desc: "Archive name"
         
     | 
| 
       33 
48 
     | 
    
         
             
                option :remove_source, type: :boolean, default: false, desc: "Remove source files after successful backup"
         
     | 
| 
      
 49 
     | 
    
         
            +
                option :all, type: :boolean, default: false, desc: "Backup all repositories (multi-repo config only)"
         
     | 
| 
       34 
50 
     | 
    
         
             
                def backup
         
     | 
| 
       35 
51 
     | 
    
         
             
                  @logger.info("Starting backup operation with config: #{options[:config]}")
         
     | 
| 
       36 
52 
     | 
    
         
             
                  config = Config.new(options[:config])
         
     | 
| 
       37 
     | 
    
         
            -
                  @logger.info("Backing up paths: #{config.backup_paths.join(', ')}")
         
     | 
| 
       38 
     | 
    
         
            -
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
       39 
53 
     | 
    
         | 
| 
       40 
     | 
    
         
            -
                   
     | 
| 
       41 
     | 
    
         
            -
             
     | 
| 
       42 
     | 
    
         
            -
             
     | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
                  @logger.info("Creating archive: #{archive_name}")
         
     | 
| 
       45 
     | 
    
         
            -
                  backup.create(name: options[:name], remove_source: options[:remove_source])
         
     | 
| 
       46 
     | 
    
         
            -
                  @logger.info("Backup created successfully: #{archive_name}")
         
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
                  if options[:remove_source]
         
     | 
| 
       49 
     | 
    
         
            -
                    @logger.info("Removed source files: #{config.backup_paths.join(', ')}")
         
     | 
| 
      
 54 
     | 
    
         
            +
                  if config.multi_repo?
         
     | 
| 
      
 55 
     | 
    
         
            +
                    backup_multi_repo(config)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  else
         
     | 
| 
      
 57 
     | 
    
         
            +
                    backup_single_repo(config)
         
     | 
| 
       50 
58 
     | 
    
         
             
                  end
         
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
                  puts "Backup created successfully"
         
     | 
| 
       53 
     | 
    
         
            -
                  puts "Source files removed" if options[:remove_source]
         
     | 
| 
       54 
59 
     | 
    
         
             
                rescue Error => e
         
     | 
| 
       55 
60 
     | 
    
         
             
                  @logger.error("Backup failed: #{e.message}")
         
     | 
| 
       56 
61 
     | 
    
         
             
                  error_exit(e)
         
     | 
| 
         @@ -62,7 +67,15 @@ module Ruborg 
     | 
|
| 
       62 
67 
     | 
    
         
             
                  config = Config.new(options[:config])
         
     | 
| 
       63 
68 
     | 
    
         
             
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
       64 
69 
     | 
    
         | 
| 
       65 
     | 
    
         
            -
                  repo = Repository.new(config.repository, passphrase: passphrase)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                  # Auto-initialize repository if configured
         
     | 
| 
      
 73 
     | 
    
         
            +
                  if config.auto_init? && !repo.exists?
         
     | 
| 
      
 74 
     | 
    
         
            +
                    @logger.info("Auto-initializing repository at #{config.repository}")
         
     | 
| 
      
 75 
     | 
    
         
            +
                    repo.create
         
     | 
| 
      
 76 
     | 
    
         
            +
                    puts "Repository auto-initialized at #{config.repository}"
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
       66 
79 
     | 
    
         
             
                  repo.list
         
     | 
| 
       67 
80 
     | 
    
         
             
                  @logger.info("Successfully listed archives")
         
     | 
| 
       68 
81 
     | 
    
         
             
                rescue Error => e
         
     | 
| 
         @@ -79,7 +92,7 @@ module Ruborg 
     | 
|
| 
       79 
92 
     | 
    
         
             
                  config = Config.new(options[:config])
         
     | 
| 
       80 
93 
     | 
    
         
             
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
       81 
94 
     | 
    
         | 
| 
       82 
     | 
    
         
            -
                  repo = Repository.new(config.repository, passphrase: passphrase)
         
     | 
| 
      
 95 
     | 
    
         
            +
                  repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
         
     | 
| 
       83 
96 
     | 
    
         
             
                  backup = Backup.new(repo, config: config)
         
     | 
| 
       84 
97 
     | 
    
         | 
| 
       85 
98 
     | 
    
         
             
                  backup.extract(archive_name, destination: options[:destination], path: options[:path])
         
     | 
| 
         @@ -101,7 +114,15 @@ module Ruborg 
     | 
|
| 
       101 
114 
     | 
    
         
             
                  config = Config.new(options[:config])
         
     | 
| 
       102 
115 
     | 
    
         
             
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
       103 
116 
     | 
    
         | 
| 
       104 
     | 
    
         
            -
                  repo = Repository.new(config.repository, passphrase: passphrase)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                  # Auto-initialize repository if configured
         
     | 
| 
      
 120 
     | 
    
         
            +
                  if config.auto_init? && !repo.exists?
         
     | 
| 
      
 121 
     | 
    
         
            +
                    @logger.info("Auto-initializing repository at #{config.repository}")
         
     | 
| 
      
 122 
     | 
    
         
            +
                    repo.create
         
     | 
| 
      
 123 
     | 
    
         
            +
                    puts "Repository auto-initialized at #{config.repository}"
         
     | 
| 
      
 124 
     | 
    
         
            +
                  end
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
       105 
126 
     | 
    
         
             
                  repo.info
         
     | 
| 
       106 
127 
     | 
    
         
             
                  @logger.info("Successfully retrieved repository information")
         
     | 
| 
       107 
128 
     | 
    
         
             
                rescue Error => e
         
     | 
| 
         @@ -129,5 +150,168 @@ module Ruborg 
     | 
|
| 
       129 
150 
     | 
    
         
             
                  puts "Error: #{error.message}"
         
     | 
| 
       130 
151 
     | 
    
         
             
                  exit 1
         
     | 
| 
       131 
152 
     | 
    
         
             
                end
         
     | 
| 
      
 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 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
                # Single repository backup (legacy)
         
     | 
| 
      
 180 
     | 
    
         
            +
                def backup_single_repo(config)
         
     | 
| 
      
 181 
     | 
    
         
            +
                  @logger.info("Backing up paths: #{config.backup_paths.join(', ')}")
         
     | 
| 
      
 182 
     | 
    
         
            +
                  passphrase = fetch_passphrase_from_config(config)
         
     | 
| 
      
 183 
     | 
    
         
            +
             
     | 
| 
      
 184 
     | 
    
         
            +
                  repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
         
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
      
 186 
     | 
    
         
            +
                  # Auto-initialize repository if configured
         
     | 
| 
      
 187 
     | 
    
         
            +
                  if config.auto_init? && !repo.exists?
         
     | 
| 
      
 188 
     | 
    
         
            +
                    @logger.info("Auto-initializing repository at #{config.repository}")
         
     | 
| 
      
 189 
     | 
    
         
            +
                    repo.create
         
     | 
| 
      
 190 
     | 
    
         
            +
                    puts "Repository auto-initialized at #{config.repository}"
         
     | 
| 
      
 191 
     | 
    
         
            +
                  end
         
     | 
| 
      
 192 
     | 
    
         
            +
             
     | 
| 
      
 193 
     | 
    
         
            +
                  backup = Backup.new(repo, config: config)
         
     | 
| 
      
 194 
     | 
    
         
            +
             
     | 
| 
      
 195 
     | 
    
         
            +
                  archive_name = options[:name] ? sanitize_archive_name(options[:name]) : Time.now.strftime("%Y-%m-%d_%H-%M-%S")
         
     | 
| 
      
 196 
     | 
    
         
            +
                  @logger.info("Creating archive: #{archive_name}")
         
     | 
| 
      
 197 
     | 
    
         
            +
                  backup.create(name: archive_name, remove_source: options[:remove_source])
         
     | 
| 
      
 198 
     | 
    
         
            +
                  @logger.info("Backup created successfully: #{archive_name}")
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                  if options[:remove_source]
         
     | 
| 
      
 201 
     | 
    
         
            +
                    @logger.info("Removed source files: #{config.backup_paths.join(', ')}")
         
     | 
| 
      
 202 
     | 
    
         
            +
                  end
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
                  puts "Backup created successfully"
         
     | 
| 
      
 205 
     | 
    
         
            +
                  puts "Source files removed" if options[:remove_source]
         
     | 
| 
      
 206 
     | 
    
         
            +
                end
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
                # Multi-repository backup
         
     | 
| 
      
 209 
     | 
    
         
            +
                def backup_multi_repo(config)
         
     | 
| 
      
 210 
     | 
    
         
            +
                  global_settings = config.global_settings
         
     | 
| 
      
 211 
     | 
    
         
            +
                  repos_to_backup = if options[:all]
         
     | 
| 
      
 212 
     | 
    
         
            +
                                      config.repositories
         
     | 
| 
      
 213 
     | 
    
         
            +
                                    elsif options[:repository]
         
     | 
| 
      
 214 
     | 
    
         
            +
                                      repo_config = config.get_repository(options[:repository])
         
     | 
| 
      
 215 
     | 
    
         
            +
                                      raise ConfigError, "Repository '#{options[:repository]}' not found" unless repo_config
         
     | 
| 
      
 216 
     | 
    
         
            +
                                      [repo_config]
         
     | 
| 
      
 217 
     | 
    
         
            +
                                    else
         
     | 
| 
      
 218 
     | 
    
         
            +
                                      raise ConfigError, "Please specify --repository or --all for multi-repo config"
         
     | 
| 
      
 219 
     | 
    
         
            +
                                    end
         
     | 
| 
      
 220 
     | 
    
         
            +
             
     | 
| 
      
 221 
     | 
    
         
            +
                  repos_to_backup.each do |repo_config|
         
     | 
| 
      
 222 
     | 
    
         
            +
                    backup_repository(repo_config, global_settings)
         
     | 
| 
      
 223 
     | 
    
         
            +
                  end
         
     | 
| 
      
 224 
     | 
    
         
            +
                end
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
                def backup_repository(repo_config, global_settings)
         
     | 
| 
      
 227 
     | 
    
         
            +
                  repo_name = repo_config["name"]
         
     | 
| 
      
 228 
     | 
    
         
            +
                  puts "\n--- Backing up repository: #{repo_name} ---"
         
     | 
| 
      
 229 
     | 
    
         
            +
                  @logger.info("Backing up repository: #{repo_name}")
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
      
 231 
     | 
    
         
            +
                  # Merge global settings with repo-specific settings (repo-specific takes precedence)
         
     | 
| 
      
 232 
     | 
    
         
            +
                  merged_config = global_settings.merge(repo_config)
         
     | 
| 
      
 233 
     | 
    
         
            +
             
     | 
| 
      
 234 
     | 
    
         
            +
                  passphrase = fetch_passphrase_for_repo(merged_config)
         
     | 
| 
      
 235 
     | 
    
         
            +
                  borg_opts = merged_config["borg_options"] || {}
         
     | 
| 
      
 236 
     | 
    
         
            +
                  repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts)
         
     | 
| 
      
 237 
     | 
    
         
            +
             
     | 
| 
      
 238 
     | 
    
         
            +
                  # Auto-initialize if configured
         
     | 
| 
      
 239 
     | 
    
         
            +
                  auto_init = merged_config["auto_init"] || false
         
     | 
| 
      
 240 
     | 
    
         
            +
                  if auto_init && !repo.exists?
         
     | 
| 
      
 241 
     | 
    
         
            +
                    @logger.info("Auto-initializing repository at #{repo_config['path']}")
         
     | 
| 
      
 242 
     | 
    
         
            +
                    repo.create
         
     | 
| 
      
 243 
     | 
    
         
            +
                    puts "Repository auto-initialized at #{repo_config['path']}"
         
     | 
| 
      
 244 
     | 
    
         
            +
                  end
         
     | 
| 
      
 245 
     | 
    
         
            +
             
     | 
| 
      
 246 
     | 
    
         
            +
                  # Create backup config wrapper
         
     | 
| 
      
 247 
     | 
    
         
            +
                  backup_config = BackupConfig.new(repo_config, merged_config)
         
     | 
| 
      
 248 
     | 
    
         
            +
                  backup = Backup.new(repo, config: backup_config)
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
                  archive_name = options[:name] ? sanitize_archive_name(options[:name]) : "#{repo_name}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}"
         
     | 
| 
      
 251 
     | 
    
         
            +
                  @logger.info("Creating archive: #{archive_name}")
         
     | 
| 
      
 252 
     | 
    
         
            +
             
     | 
| 
      
 253 
     | 
    
         
            +
                  sources = repo_config["sources"] || []
         
     | 
| 
      
 254 
     | 
    
         
            +
                  @logger.info("Backing up #{sources.size} source(s)")
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
                  backup.create(name: archive_name, remove_source: options[:remove_source])
         
     | 
| 
      
 257 
     | 
    
         
            +
                  @logger.info("Backup created successfully: #{archive_name}")
         
     | 
| 
      
 258 
     | 
    
         
            +
             
     | 
| 
      
 259 
     | 
    
         
            +
                  puts "โ Backup created: #{archive_name}"
         
     | 
| 
      
 260 
     | 
    
         
            +
                  puts "  Sources removed" if options[:remove_source]
         
     | 
| 
      
 261 
     | 
    
         
            +
                end
         
     | 
| 
      
 262 
     | 
    
         
            +
             
     | 
| 
      
 263 
     | 
    
         
            +
                def fetch_passphrase_for_repo(repo_config)
         
     | 
| 
      
 264 
     | 
    
         
            +
                  passbolt_config = repo_config["passbolt"]
         
     | 
| 
      
 265 
     | 
    
         
            +
                  return nil if passbolt_config.nil? || passbolt_config.empty?
         
     | 
| 
      
 266 
     | 
    
         
            +
             
     | 
| 
      
 267 
     | 
    
         
            +
                  Passbolt.new(resource_id: passbolt_config["resource_id"]).get_password
         
     | 
| 
      
 268 
     | 
    
         
            +
                end
         
     | 
| 
      
 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 
     | 
    
         
            +
             
     | 
| 
      
 284 
     | 
    
         
            +
                # Wrapper class to adapt multi-repo config to existing Backup class
         
     | 
| 
      
 285 
     | 
    
         
            +
                class BackupConfig
         
     | 
| 
      
 286 
     | 
    
         
            +
                  def initialize(repo_config, merged_settings)
         
     | 
| 
      
 287 
     | 
    
         
            +
                    @repo_config = repo_config
         
     | 
| 
      
 288 
     | 
    
         
            +
                    @merged_settings = merged_settings
         
     | 
| 
      
 289 
     | 
    
         
            +
                  end
         
     | 
| 
      
 290 
     | 
    
         
            +
             
     | 
| 
      
 291 
     | 
    
         
            +
                  def backup_paths
         
     | 
| 
      
 292 
     | 
    
         
            +
                    sources = @repo_config["sources"] || []
         
     | 
| 
      
 293 
     | 
    
         
            +
                    sources.flat_map do |source|
         
     | 
| 
      
 294 
     | 
    
         
            +
                      source["paths"] || []
         
     | 
| 
      
 295 
     | 
    
         
            +
                    end
         
     | 
| 
      
 296 
     | 
    
         
            +
                  end
         
     | 
| 
      
 297 
     | 
    
         
            +
             
     | 
| 
      
 298 
     | 
    
         
            +
                  def exclude_patterns
         
     | 
| 
      
 299 
     | 
    
         
            +
                    patterns = []
         
     | 
| 
      
 300 
     | 
    
         
            +
                    sources = @repo_config["sources"] || []
         
     | 
| 
      
 301 
     | 
    
         
            +
                    sources.each do |source|
         
     | 
| 
      
 302 
     | 
    
         
            +
                      patterns += (source["exclude"] || [])
         
     | 
| 
      
 303 
     | 
    
         
            +
                    end
         
     | 
| 
      
 304 
     | 
    
         
            +
                    patterns += (@merged_settings["exclude_patterns"] || [])
         
     | 
| 
      
 305 
     | 
    
         
            +
                    patterns.uniq
         
     | 
| 
      
 306 
     | 
    
         
            +
                  end
         
     | 
| 
      
 307 
     | 
    
         
            +
             
     | 
| 
      
 308 
     | 
    
         
            +
                  def compression
         
     | 
| 
      
 309 
     | 
    
         
            +
                    @merged_settings["compression"] || "lz4"
         
     | 
| 
      
 310 
     | 
    
         
            +
                  end
         
     | 
| 
      
 311 
     | 
    
         
            +
             
     | 
| 
      
 312 
     | 
    
         
            +
                  def encryption_mode
         
     | 
| 
      
 313 
     | 
    
         
            +
                    @merged_settings["encryption"] || "repokey"
         
     | 
| 
      
 314 
     | 
    
         
            +
                  end
         
     | 
| 
      
 315 
     | 
    
         
            +
                end
         
     | 
| 
       132 
316 
     | 
    
         
             
              end
         
     | 
| 
       133 
317 
     | 
    
         
             
            end
         
     | 
    
        data/lib/ruborg/config.rb
    CHANGED
    
    | 
         @@ -11,16 +11,20 @@ module Ruborg 
     | 
|
| 
       11 
11 
     | 
    
         
             
                def initialize(config_path)
         
     | 
| 
       12 
12 
     | 
    
         
             
                  @config_path = config_path
         
     | 
| 
       13 
13 
     | 
    
         
             
                  load_config
         
     | 
| 
      
 14 
     | 
    
         
            +
                  detect_format
         
     | 
| 
       14 
15 
     | 
    
         
             
                end
         
     | 
| 
       15 
16 
     | 
    
         | 
| 
       16 
17 
     | 
    
         
             
                def load_config
         
     | 
| 
       17 
18 
     | 
    
         
             
                  raise ConfigError, "Configuration file not found: #{@config_path}" unless File.exist?(@config_path)
         
     | 
| 
       18 
19 
     | 
    
         | 
| 
       19 
     | 
    
         
            -
                  @data = YAML. 
     | 
| 
      
 20 
     | 
    
         
            +
                  @data = YAML.safe_load_file(@config_path, permitted_classes: [Symbol], aliases: true)
         
     | 
| 
       20 
21 
     | 
    
         
             
                rescue Psych::SyntaxError => e
         
     | 
| 
       21 
22 
     | 
    
         
             
                  raise ConfigError, "Invalid YAML syntax: #{e.message}"
         
     | 
| 
      
 23 
     | 
    
         
            +
                rescue Psych::DisallowedClass => e
         
     | 
| 
      
 24 
     | 
    
         
            +
                  raise ConfigError, "Invalid YAML content: #{e.message}"
         
     | 
| 
       22 
25 
     | 
    
         
             
                end
         
     | 
| 
       23 
26 
     | 
    
         | 
| 
      
 27 
     | 
    
         
            +
                # Legacy single-repo accessors (for backward compatibility)
         
     | 
| 
       24 
28 
     | 
    
         
             
                def repository
         
     | 
| 
       25 
29 
     | 
    
         
             
                  @data["repository"]
         
     | 
| 
       26 
30 
     | 
    
         
             
                end
         
     | 
| 
         @@ -30,19 +34,98 @@ module Ruborg 
     | 
|
| 
       30 
34 
     | 
    
         
             
                end
         
     | 
| 
       31 
35 
     | 
    
         | 
| 
       32 
36 
     | 
    
         
             
                def exclude_patterns
         
     | 
| 
       33 
     | 
    
         
            -
                  @data["exclude_patterns"] || []
         
     | 
| 
      
 37 
     | 
    
         
            +
                  patterns = @data["exclude_patterns"] || []
         
     | 
| 
      
 38 
     | 
    
         
            +
                  validate_exclude_patterns(patterns)
         
     | 
| 
       34 
39 
     | 
    
         
             
                end
         
     | 
| 
       35 
40 
     | 
    
         | 
| 
       36 
41 
     | 
    
         
             
                def compression
         
     | 
| 
       37 
     | 
    
         
            -
                  @data["compression"] || "lz4"
         
     | 
| 
      
 42 
     | 
    
         
            +
                  value = @data["compression"] || "lz4"
         
     | 
| 
      
 43 
     | 
    
         
            +
                  validate_compression(value)
         
     | 
| 
       38 
44 
     | 
    
         
             
                end
         
     | 
| 
       39 
45 
     | 
    
         | 
| 
       40 
46 
     | 
    
         
             
                def encryption_mode
         
     | 
| 
       41 
     | 
    
         
            -
                  @data["encryption"] || "repokey"
         
     | 
| 
      
 47 
     | 
    
         
            +
                  value = @data["encryption"] || "repokey"
         
     | 
| 
      
 48 
     | 
    
         
            +
                  validate_encryption(value)
         
     | 
| 
       42 
49 
     | 
    
         
             
                end
         
     | 
| 
       43 
50 
     | 
    
         | 
| 
       44 
51 
     | 
    
         
             
                def passbolt_integration
         
     | 
| 
       45 
52 
     | 
    
         
             
                  @data["passbolt"] || {}
         
     | 
| 
       46 
53 
     | 
    
         
             
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                def auto_init?
         
     | 
| 
      
 56 
     | 
    
         
            +
                  @data["auto_init"] || false
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                def log_file
         
     | 
| 
      
 60 
     | 
    
         
            +
                  @data["log_file"]
         
     | 
| 
      
 61 
     | 
    
         
            +
                end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                def borg_options
         
     | 
| 
      
 64 
     | 
    
         
            +
                  @data["borg_options"] || {}
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                # New multi-repo support
         
     | 
| 
      
 68 
     | 
    
         
            +
                def multi_repo?
         
     | 
| 
      
 69 
     | 
    
         
            +
                  @multi_repo
         
     | 
| 
      
 70 
     | 
    
         
            +
                end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                def repositories
         
     | 
| 
      
 73 
     | 
    
         
            +
                  return [] unless multi_repo?
         
     | 
| 
      
 74 
     | 
    
         
            +
                  @data["repositories"] || []
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                def get_repository(name)
         
     | 
| 
      
 78 
     | 
    
         
            +
                  return nil unless multi_repo?
         
     | 
| 
      
 79 
     | 
    
         
            +
                  repositories.find { |r| r["name"] == name }
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                def repository_names
         
     | 
| 
      
 83 
     | 
    
         
            +
                  return [] unless multi_repo?
         
     | 
| 
      
 84 
     | 
    
         
            +
                  repositories.map { |r| r["name"] }
         
     | 
| 
      
 85 
     | 
    
         
            +
                end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                def global_settings
         
     | 
| 
      
 88 
     | 
    
         
            +
                  @data.slice("passbolt", "compression", "encryption", "auto_init")
         
     | 
| 
      
 89 
     | 
    
         
            +
                end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                private
         
     | 
| 
      
 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 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                def detect_format
         
     | 
| 
      
 98 
     | 
    
         
            +
                  @multi_repo = @data.key?("repositories")
         
     | 
| 
      
 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
         
     | 
| 
       47 
130 
     | 
    
         
             
              end
         
     | 
| 
       48 
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. 
     | 
| 
      
 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
         
     |