ruborg 0.4.0 → 0.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 263e32d62c24714f299a20218bc96733cf6f13cffa0e0d844ef5b37c5470dbdf
4
- data.tar.gz: f020b2b5714fd3c02d9372414de7494e7beb637d5312bcf34f05e2be5bf1ecd1
3
+ metadata.gz: 821f1707180870ec6b2ffccda4ef4d901b3ee274d7402c26170ca294e80efb8b
4
+ data.tar.gz: 62e2a5ee53cd75024b78e08e32295c34cf1829cd3bd34be9d6403c7ec1dfb810
5
5
  SHA512:
6
- metadata.gz: 1ad51dcadf03ca1958ff6ecd10b24846d6e501219dd59637280eaf6d37a2d6b6a505ff0bbb9401d5f7ebfd361c24a3e1c3b09bc71f40bf788c6581da2a98832a
7
- data.tar.gz: 5679385c1369eb161235c998dd69526a3bc27dca1cfc13acc22bd0602184f15438c042542495300b1752eb8ce810dfbc467bfe83d10b492641063e84381f3881
6
+ metadata.gz: acd1bcd0b6da0914888c11d50299ea2f365e2bf5a81423edbfc7e935a17d020b841de9043900ca86ed1df2ab8e50e1359806ad2857de3bd849b8f040c2909549
7
+ data.tar.gz: 799d52c64a3bf858b2324f5101255a77dd428778b1eced9bfa11e54d9c2ef23642645a886902f83c05336c89a61d1a80b02ad83185fecf3cf0cac866d18e7c09
data/CHANGELOG.md CHANGED
@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2025-10-08
11
+
12
+ ### Added
13
+ - **Configuration Validation Command**: New `ruborg validate` command to check configuration files for type errors
14
+ - **Automatic Schema Validation**: All commands now validate configuration on startup to catch errors early
15
+ - **Strict Boolean Type Checking**: All boolean config values (auto_init, auto_prune, allow_remove_source, etc.) now require actual boolean types
16
+ - Prevents type confusion attacks where strings like `'true'` or `"false"` bypass security checks
17
+ - Clear error messages show actual type vs expected type
18
+ - Validation runs automatically on config load
19
+ - Comprehensive validation test suite (10 new test cases)
20
+ - Documentation for configuration validation in README
21
+
22
+ ### Changed
23
+ - Boolean configuration values now use strict type checking throughout the codebase
24
+ - `auto_init`: only boolean `true` enables, everything else disables
25
+ - `auto_prune`: only boolean `true` enables, everything else disables
26
+ - `allow_remove_source`: strict checking - only `TrueClass` enables (security-critical)
27
+ - `allow_relocated_repo`: permissive normalization - only `false` disables (backward compatible)
28
+ - `allow_unencrypted_repo`: permissive normalization - only `false` disables (backward compatible)
29
+ - Config class now validates schema by default (can be disabled with `validate_types: false`)
30
+
31
+ ### Security
32
+ - **Type Confusion Protection (CWE-843)**: Strict boolean type checking prevents configuration bypass attacks
33
+ - Before: `allow_remove_source: 'false'` (string) would be truthy and enable deletion
34
+ - After: Only `allow_remove_source: true` (boolean) enables the dangerous operation
35
+ - Enhanced error messages guide users to fix type errors correctly
36
+ - SECURITY.md updated with type confusion findings and mitigations
37
+
38
+ ## [0.5.0] - 2025-10-08
39
+
40
+ ### Added
41
+ - **Hostname Validation**: Optional `hostname` configuration key to restrict backup operations to specific hosts
42
+ - Can be configured globally or per-repository
43
+ - Repository-specific hostname overrides global setting
44
+ - Validates system hostname before backup, list, restore, check operations
45
+ - Prevents accidental execution of backups on wrong machines
46
+ - Displayed in `info` command output
47
+ - Comprehensive test coverage for hostname validation (6 new test cases)
48
+ - Documentation for hostname feature in example config and README
49
+
50
+ ### Changed
51
+ - `info` command now displays hostname when configured (global or per-repository)
52
+
10
53
  ## [0.4.0] - 2025-10-06
11
54
 
12
55
  ### Added
data/README.md CHANGED
@@ -24,7 +24,8 @@ e- ⏰ **Retention Policies** - Configure backup retention (hourly, daily, weekl
24
24
  - 📋 **Repository Descriptions** - Document each repository's purpose
25
25
  - 📈 **Summary View** - Quick overview of all repositories and their configurations
26
26
  - 🔧 **Custom Borg Path** - Support for custom Borg executable paths per repository
27
- - **Well-tested** - Comprehensive test suite with RSpec (147 tests)
27
+ - 🏠 **Hostname Validation** - NEW! Restrict backups to specific hosts (global or per-repository)
28
+ - ✅ **Well-tested** - Comprehensive test suite with RSpec (178+ tests)
28
29
  - 🔒 **Security-focused** - Path validation, safe YAML loading, command injection protection
29
30
 
30
31
  ## Prerequisites
@@ -127,6 +128,7 @@ repositories:
127
128
  - name: databases
128
129
  description: "MySQL and PostgreSQL database dumps"
129
130
  path: /mnt/backup/databases
131
+ hostname: dbserver.local # Optional: repository-specific hostname override
130
132
  # Repository-specific passbolt (overrides global)
131
133
  passbolt:
132
134
  resource_id: "db-specific-passbolt-id"
@@ -160,14 +162,55 @@ repositories:
160
162
  ```
161
163
 
162
164
  **Configuration Features:**
165
+ - **Automatic Type Validation**: Configuration is validated on startup to catch type errors early
166
+ - **Validation Command**: Run `ruborg validate` to check configuration files for errors
163
167
  - **Descriptions**: Add `description` field to document each repository's purpose
164
- - **Global Settings**: Compression, encryption, auto_init, log_file, borg_path, borg_options, and retention apply to all repositories
165
- - **Per-Repository Overrides**: Any global setting can be overridden at the repository level (including custom borg_path per repository)
168
+ - **Hostname Validation**: Optional `hostname` field to restrict backups to specific hosts (global or per-repository)
169
+ - **Source Deletion Safety**: `allow_remove_source` flag to explicitly enable `--remove-source` option (default: disabled)
170
+ - **Type-Safe Booleans**: Strict boolean validation prevents configuration errors (must use `true`/`false`, not strings)
171
+ - **Global Settings**: Hostname, compression, encryption, auto_init, allow_remove_source, log_file, borg_path, borg_options, and retention apply to all repositories
172
+ - **Per-Repository Overrides**: Any global setting can be overridden at the repository level (including hostname, allow_remove_source, and custom borg_path)
166
173
  - **Custom Borg Path**: Specify a custom Borg executable path if borg is not in PATH or to use a specific version
167
174
  - **Retention Policies**: Define how many backups to keep (hourly, daily, weekly, monthly, yearly)
168
175
  - **Multiple Sources**: Each repository can have multiple backup sources with their own exclude patterns
169
176
  - **Flexible Organization**: Organize backups by type (documents, databases, media) with different policies
170
177
 
178
+ ## Configuration Validation
179
+
180
+ Ruborg automatically validates your configuration on startup. All commands check for type errors and structural issues before executing.
181
+
182
+ ### Validate Configuration
183
+
184
+ Check your configuration file for errors:
185
+
186
+ ```bash
187
+ ruborg validate --config ruborg.yml
188
+ ```
189
+
190
+ **Validation checks:**
191
+ - Boolean types (must be `true` or `false`, not strings like `'true'`)
192
+ - Valid compression values (lz4, zstd, zlib, lzma, none)
193
+ - Valid encryption modes
194
+ - Required repository fields (name, path)
195
+ - Correct borg_options values
196
+
197
+ **Example validation output:**
198
+
199
+ ```
200
+ ✓ Configuration is valid
201
+ No type errors or warnings found
202
+ ```
203
+
204
+ Or with errors:
205
+
206
+ ```
207
+ ❌ ERRORS FOUND (2):
208
+ - global/auto_init: must be boolean (true or false), got String: "true"
209
+ - test-repo/allow_remove_source: must be boolean (true or false), got Integer: 1
210
+
211
+ Configuration has errors that must be fixed.
212
+ ```
213
+
171
214
  ## Usage
172
215
 
173
216
  ### Initialize a Repository
@@ -195,10 +238,48 @@ ruborg backup --repository databases --name "db-backup-2025-10-05"
195
238
  # Using custom configuration file
196
239
  ruborg backup --config /path/to/config.yml --repository documents
197
240
 
198
- # Remove source files after successful backup
241
+ # Remove source files after successful backup (requires allow_remove_source: true)
199
242
  ruborg backup --repository documents --remove-source
200
243
  ```
201
244
 
245
+ **IMPORTANT: Source File Deletion Safety**
246
+
247
+ The `--remove-source` option is disabled by default for safety. To use it, you must explicitly enable it in your configuration:
248
+
249
+ ```yaml
250
+ # Global setting - applies to all repositories
251
+ allow_remove_source: true
252
+
253
+ # OR per-repository setting
254
+ repositories:
255
+ - name: temp-backups
256
+ allow_remove_source: true # Only for this repository
257
+ ...
258
+ ```
259
+
260
+ **⚠️ TYPE SAFETY WARNING:** The value MUST be a boolean `true`, not a string:
261
+
262
+ ```yaml
263
+ # ✅ CORRECT - Boolean true
264
+ allow_remove_source: true
265
+
266
+ # ❌ WRONG - String 'true' (will be rejected)
267
+ allow_remove_source: 'true'
268
+ allow_remove_source: "true"
269
+
270
+ # ❌ WRONG - Other truthy values (will be rejected)
271
+ allow_remove_source: 1
272
+ allow_remove_source: yes
273
+ ```
274
+
275
+ Ruborg uses strict type checking to prevent configuration errors. Only the boolean value `true` (unquoted) will enable source deletion. Any other value, including string `'true'` or `"true"`, will be rejected with a detailed error message showing the actual type received.
276
+
277
+ Without `allow_remove_source: true` configured, using `--remove-source` will result in an error:
278
+ ```
279
+ Error: Cannot use --remove-source: 'allow_remove_source' must be true (boolean).
280
+ Current value: "true" (String). Set 'allow_remove_source: true' in configuration.
281
+ ```
282
+
202
283
  ### List Archives
203
284
 
204
285
  ```bash
@@ -338,6 +419,70 @@ repositories:
338
419
 
339
420
  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.
340
421
 
422
+ ## Hostname Validation
423
+
424
+ Restrict backup operations to specific hosts using the optional `hostname` configuration key. This prevents accidental execution of backups on the wrong machine.
425
+
426
+ ### Global Hostname
427
+
428
+ Apply hostname restriction to all repositories:
429
+
430
+ ```yaml
431
+ # Global hostname - applies to all repositories
432
+ hostname: myserver.local
433
+
434
+ repositories:
435
+ - name: documents
436
+ path: /mnt/backup/documents
437
+ sources:
438
+ - name: main
439
+ paths:
440
+ - /home/user/documents
441
+ ```
442
+
443
+ ### Per-Repository Hostname
444
+
445
+ Override global hostname for specific repositories:
446
+
447
+ ```yaml
448
+ # Global hostname for most repositories
449
+ hostname: mainserver.local
450
+
451
+ repositories:
452
+ # Uses global hostname (mainserver.local)
453
+ - name: documents
454
+ path: /mnt/backup/documents
455
+ sources:
456
+ - name: main
457
+ paths:
458
+ - /home/user/documents
459
+
460
+ # Override with repository-specific hostname
461
+ - name: databases
462
+ hostname: dbserver.local # Only runs on dbserver.local
463
+ path: /mnt/backup/databases
464
+ sources:
465
+ - name: mysql
466
+ paths:
467
+ - /var/lib/mysql/dumps
468
+ ```
469
+
470
+ **How it works:**
471
+ - Before backup, list, restore, or check operations, Ruborg validates the system hostname
472
+ - If configured hostname doesn't match the current hostname, the operation fails with an error
473
+ - Repository-specific hostname takes precedence over global hostname
474
+ - If no hostname is configured, validation is skipped
475
+
476
+ **Example error:**
477
+ ```
478
+ Error: Hostname mismatch: configuration is for 'dbserver.local' but current hostname is 'mainserver.local'
479
+ ```
480
+
481
+ **Use cases:**
482
+ - **Multi-server environments**: Different servers backup to different repositories
483
+ - **Development vs Production**: Prevent production config from running on dev machines
484
+ - **Safety**: Avoid accidentally running wrong backups on shared configuration files
485
+
341
486
  ## Security Configuration
342
487
 
343
488
  Ruborg provides configurable security options via `borg_options`:
@@ -370,6 +515,7 @@ See [SECURITY.md](SECURITY.md) for detailed security information and best practi
370
515
  | Command | Description | Options |
371
516
  |---------|-------------|---------|
372
517
  | `init REPOSITORY` | Initialize a new Borg repository | `--passphrase`, `--passbolt-id`, `--log` |
518
+ | `validate` | Validate configuration file for type errors | `--config`, `--log` |
373
519
  | `backup` | Create a backup using config file | `--config`, `--repository`, `--all`, `--name`, `--remove-source`, `--log` |
374
520
  | `list` | List all archives in repository | `--config`, `--repository`, `--log` |
375
521
  | `restore ARCHIVE` | Restore files from archive | `--config`, `--repository`, `--destination`, `--path`, `--log` |
data/SECURITY.md CHANGED
@@ -66,6 +66,15 @@ Ruborg implements several security measures to protect your backup operations:
66
66
  - Integrated into development workflow
67
67
  - Run: `bundle exec bundle-audit check`
68
68
 
69
+ ### 13. Boolean Type Safety (Type Confusion Protection)
70
+ - **Critical safety flag validation** for `allow_remove_source` configuration
71
+ - Uses strict type checking (`is_a?(TrueClass)`) to prevent type confusion attacks
72
+ - Rejects truthy values like strings `'true'`, `"false"`, integers `1`, or `"yes"`
73
+ - Only boolean `true` enables dangerous operations like `--remove-source`
74
+ - Provides detailed error messages showing actual type received vs expected
75
+ - Prevents configuration errors that could lead to unintended data loss
76
+ - **CWE-843 Mitigation**: Protects against type confusion vulnerabilities
77
+
69
78
  ## Security Best Practices
70
79
 
71
80
  ### When Using `--remove-source`
@@ -76,6 +85,16 @@ Ruborg implements several security measures to protect your backup operations:
76
85
  2. **Never use on symlinks** to critical system directories
77
86
  3. **Verify backups** before using this flag in production
78
87
  4. **Use absolute paths** in configuration to avoid ambiguity
88
+ 5. **Use boolean values** for `allow_remove_source` - NEVER use quoted strings:
89
+ ```yaml
90
+ # ✅ CORRECT
91
+ allow_remove_source: true
92
+
93
+ # ❌ WRONG - Will be rejected
94
+ allow_remove_source: 'true'
95
+ allow_remove_source: "true"
96
+ allow_remove_source: 1
97
+ ```
79
98
 
80
99
  ### Configuration File Security
81
100
 
@@ -144,6 +163,19 @@ We will respond within 48 hours and work with you to address the issue.
144
163
 
145
164
  ## Security Audit History
146
165
 
166
+ - **v0.6.0** (2025-10-08): Configuration validation and type confusion protection
167
+ - **SECURITY FIX**: Implemented strict boolean type checking for `allow_remove_source`
168
+ - Prevents type confusion attacks (CWE-843) where string values bypass safety checks
169
+ - Added configuration validation command (`ruborg validate`) for proactive error detection
170
+ - Automatic schema validation on config load catches type errors early
171
+ - Added 10 comprehensive test cases for validation and type confusion scenarios
172
+ - Enhanced error messages to show actual type vs expected type
173
+ - Updated documentation with type safety warnings and examples
174
+
175
+ - **v0.5.0** (2025-10-08): Hostname validation
176
+ - Added hostname validation feature (optional global or per-repository)
177
+ - Prevents accidental execution of backups on wrong machines
178
+
147
179
  - **v0.4.0** (2025-10-06): Complete command injection elimination
148
180
  - **CRITICAL**: Fixed all remaining command injection vulnerabilities in repository.rb
149
181
  - Replaced all backtick execution with Open3.capture3/capture2e methods
@@ -210,3 +242,5 @@ Before deploying ruborg in production:
210
242
  - [ ] Configure borg_options for your security requirements
211
243
  - [ ] Use default archive names or sanitized custom names only
212
244
  - [ ] Ensure backup paths don't contain empty or nil values
245
+ - [ ] Use boolean `true` (not strings) for `allow_remove_source` configuration
246
+ - [ ] Configure hostname validation for multi-server environments
data/lib/ruborg/cli.rb CHANGED
@@ -54,6 +54,7 @@ module Ruborg
54
54
  def backup
55
55
  @logger.info("Starting backup operation with config: #{options[:config]}")
56
56
  config = Config.new(options[:config])
57
+ validate_hostname(config.global_settings)
57
58
  backup_repositories(config)
58
59
  rescue Error => e
59
60
  @logger.error("Backup failed: #{e.message}")
@@ -72,6 +73,7 @@ module Ruborg
72
73
 
73
74
  global_settings = config.global_settings
74
75
  merged_config = global_settings.merge(repo_config)
76
+ validate_hostname(merged_config)
75
77
  passphrase = fetch_passphrase_for_repo(merged_config)
76
78
  borg_opts = merged_config["borg_options"] || {}
77
79
  borg_path = merged_config["borg_path"]
@@ -79,7 +81,9 @@ module Ruborg
79
81
  repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path)
80
82
 
81
83
  # Auto-initialize repository if configured
82
- auto_init = merged_config["auto_init"] || false
84
+ # Use strict boolean checking: only true enables, everything else disables
85
+ auto_init = merged_config["auto_init"]
86
+ auto_init = false unless auto_init == true
83
87
  if auto_init && !repo.exists?
84
88
  @logger.info("Auto-initializing repository at #{repo_config["path"]}")
85
89
  repo.create
@@ -108,6 +112,7 @@ module Ruborg
108
112
 
109
113
  global_settings = config.global_settings
110
114
  merged_config = global_settings.merge(repo_config)
115
+ validate_hostname(merged_config)
111
116
  passphrase = fetch_passphrase_for_repo(merged_config)
112
117
  borg_opts = merged_config["borg_options"] || {}
113
118
  borg_path = merged_config["borg_path"]
@@ -154,7 +159,9 @@ module Ruborg
154
159
  repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path)
155
160
 
156
161
  # Auto-initialize repository if configured
157
- auto_init = merged_config["auto_init"] || false
162
+ # Use strict boolean checking: only true enables, everything else disables
163
+ auto_init = merged_config["auto_init"]
164
+ auto_init = false unless auto_init == true
158
165
  if auto_init && !repo.exists?
159
166
  @logger.info("Auto-initializing repository at #{repo_config["path"]}")
160
167
  repo.create
@@ -168,6 +175,78 @@ module Ruborg
168
175
  error_exit(e)
169
176
  end
170
177
 
178
+ desc "validate", "Validate configuration file for errors and type issues"
179
+ def validate_config
180
+ @logger.info("Validating configuration file: #{options[:config]}")
181
+ config = Config.new(options[:config])
182
+
183
+ puts "\n═══════════════════════════════════════════════════════════════"
184
+ puts " CONFIGURATION VALIDATION"
185
+ puts "═══════════════════════════════════════════════════════════════\n\n"
186
+
187
+ errors = []
188
+ warnings = []
189
+
190
+ # Validate global boolean settings
191
+ global_settings = config.global_settings
192
+ errors.concat(validate_boolean_setting(global_settings, "auto_init", "global"))
193
+ errors.concat(validate_boolean_setting(global_settings, "auto_prune", "global"))
194
+ errors.concat(validate_boolean_setting(global_settings, "allow_remove_source", "global"))
195
+
196
+ # Validate borg_options booleans
197
+ if global_settings["borg_options"]
198
+ warnings.concat(validate_borg_option(global_settings["borg_options"], "allow_relocated_repo", "global"))
199
+ warnings.concat(validate_borg_option(global_settings["borg_options"], "allow_unencrypted_repo", "global"))
200
+ end
201
+
202
+ # Validate per-repository settings
203
+ config.repositories.each do |repo|
204
+ repo_name = repo["name"]
205
+ errors.concat(validate_boolean_setting(repo, "auto_init", repo_name))
206
+ errors.concat(validate_boolean_setting(repo, "auto_prune", repo_name))
207
+ errors.concat(validate_boolean_setting(repo, "allow_remove_source", repo_name))
208
+
209
+ if repo["borg_options"]
210
+ warnings.concat(validate_borg_option(repo["borg_options"], "allow_relocated_repo", repo_name))
211
+ warnings.concat(validate_borg_option(repo["borg_options"], "allow_unencrypted_repo", repo_name))
212
+ end
213
+ end
214
+
215
+ # Display results
216
+ if errors.empty? && warnings.empty?
217
+ puts "✓ Configuration is valid"
218
+ puts " No type errors or warnings found\n\n"
219
+ else
220
+ unless errors.empty?
221
+ puts "❌ ERRORS FOUND (#{errors.size}):"
222
+ errors.each do |error|
223
+ puts " - #{error}"
224
+ end
225
+ puts ""
226
+ end
227
+
228
+ unless warnings.empty?
229
+ puts "⚠️ WARNINGS (#{warnings.size}):"
230
+ warnings.each do |warning|
231
+ puts " - #{warning}"
232
+ end
233
+ puts ""
234
+ end
235
+
236
+ if errors.any?
237
+ puts "Configuration has errors that must be fixed.\n\n"
238
+ exit 1
239
+ else
240
+ puts "Configuration is valid but has warnings.\n\n"
241
+ end
242
+ end
243
+
244
+ @logger.info("Configuration validation completed")
245
+ rescue Error => e
246
+ @logger.error("Validation failed: #{e.message}")
247
+ error_exit(e)
248
+ end
249
+
171
250
  desc "check", "Check repository integrity and compatibility"
172
251
  option :verify_data, type: :boolean, default: false, desc: "Verify repository data (slower)"
173
252
  option :all, type: :boolean, default: false, desc: "Check all repositories"
@@ -175,6 +254,7 @@ module Ruborg
175
254
  @logger.info("Checking repository compatibility")
176
255
  config = Config.new(options[:config])
177
256
  global_settings = config.global_settings
257
+ validate_hostname(global_settings)
178
258
 
179
259
  # Show Borg version first
180
260
  borg_version = Repository.borg_version
@@ -207,6 +287,7 @@ module Ruborg
207
287
  @logger.info("Checking repository: #{repo_name}")
208
288
 
209
289
  merged_config = global_settings.merge(repo_config)
290
+ validate_hostname(merged_config)
210
291
  passphrase = fetch_passphrase_for_repo(merged_config)
211
292
  borg_opts = merged_config["borg_options"] || {}
212
293
  borg_path = merged_config["borg_path"]
@@ -267,6 +348,7 @@ module Ruborg
267
348
 
268
349
  # Show global settings
269
350
  puts "Global Settings:"
351
+ puts " Hostname: #{global_settings["hostname"]}" if global_settings["hostname"]
270
352
  puts " Compression: #{global_settings["compression"] || "lz4 (default)"}"
271
353
  puts " Encryption: #{global_settings["encryption"] || "repokey (default)"}"
272
354
  puts " Auto-init: #{global_settings["auto_init"] || false}"
@@ -284,6 +366,7 @@ module Ruborg
284
366
  puts " Description: #{repo["description"]}" if repo["description"]
285
367
 
286
368
  # Show repo-specific overrides
369
+ puts " Hostname: #{repo["hostname"]}" if repo["hostname"]
287
370
  puts " Compression: #{repo["compression"]}" if repo["compression"]
288
371
  puts " Encryption: #{repo["encryption"]}" if repo["encryption"]
289
372
  puts " Auto-init: #{repo["auto_init"]}" unless repo["auto_init"].nil?
@@ -389,6 +472,7 @@ module Ruborg
389
472
 
390
473
  # Merge global settings with repo-specific settings (repo-specific takes precedence)
391
474
  merged_config = global_settings.merge(repo_config)
475
+ validate_hostname(merged_config)
392
476
 
393
477
  passphrase = fetch_passphrase_for_repo(merged_config)
394
478
  borg_opts = merged_config["borg_options"] || {}
@@ -396,7 +480,9 @@ module Ruborg
396
480
  repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path)
397
481
 
398
482
  # Auto-initialize if configured
399
- auto_init = merged_config["auto_init"] || false
483
+ # Use strict boolean checking: only true enables, everything else disables
484
+ auto_init = merged_config["auto_init"]
485
+ auto_init = false unless auto_init == true
400
486
  if auto_init && !repo.exists?
401
487
  @logger.info("Auto-initializing repository at #{repo_config["path"]}")
402
488
  repo.create
@@ -406,6 +492,17 @@ module Ruborg
406
492
  # Get retention mode (defaults to standard)
407
493
  retention_mode = merged_config["retention_mode"] || "standard"
408
494
 
495
+ # Validate remove_source permission with strict type checking
496
+ if options[:remove_source]
497
+ allow_remove_source = merged_config["allow_remove_source"]
498
+ unless allow_remove_source.is_a?(TrueClass)
499
+ raise ConfigError,
500
+ "Cannot use --remove-source: 'allow_remove_source' must be true (boolean). " \
501
+ "Current value: #{allow_remove_source.inspect} (#{allow_remove_source.class}). " \
502
+ "Set 'allow_remove_source: true' in configuration to allow source deletion."
503
+ end
504
+ end
505
+
409
506
  # Create backup config wrapper
410
507
  backup_config = BackupConfig.new(repo_config, merged_config)
411
508
  backup = Backup.new(repo, config: backup_config, retention_mode: retention_mode, repo_name: repo_name)
@@ -427,7 +524,9 @@ module Ruborg
427
524
  puts " Sources removed" if options[:remove_source]
428
525
 
429
526
  # Auto-prune if configured and retention policy exists
430
- auto_prune = merged_config["auto_prune"] || false
527
+ # Use strict boolean checking: only true enables, everything else disables
528
+ auto_prune = merged_config["auto_prune"]
529
+ auto_prune = false unless auto_prune == true
431
530
  retention_policy = merged_config["retention"]
432
531
 
433
532
  return unless auto_prune && retention_policy && !retention_policy.empty?
@@ -460,6 +559,46 @@ module Ruborg
460
559
  name.gsub(/[^a-zA-Z0-9._-]/, "_")
461
560
  end
462
561
 
562
+ def validate_hostname(config)
563
+ configured_hostname = config["hostname"]
564
+ return if configured_hostname.nil? || configured_hostname.empty?
565
+
566
+ current_hostname = `hostname`.strip
567
+ return if current_hostname == configured_hostname
568
+
569
+ raise ConfigError,
570
+ "Hostname mismatch: configuration is for '#{configured_hostname}' " \
571
+ "but current hostname is '#{current_hostname}'"
572
+ end
573
+
574
+ # Validate boolean configuration settings
575
+ def validate_boolean_setting(config, key, context)
576
+ errors = []
577
+ value = config[key]
578
+
579
+ return errors if value.nil? # Not set is OK
580
+
581
+ unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
582
+ errors << "#{context}/#{key}: must be boolean (true/false), got #{value.class}: #{value.inspect}"
583
+ end
584
+
585
+ errors
586
+ end
587
+
588
+ # Validate borg_options boolean settings (these have different defaults)
589
+ def validate_borg_option(borg_options, key, context)
590
+ warnings = []
591
+ value = borg_options[key]
592
+
593
+ return warnings if value.nil? # Not set is OK (uses default)
594
+
595
+ unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
596
+ warnings << "#{context}/borg_options/#{key}: should be boolean (true/false), got #{value.class}: #{value.inspect}"
597
+ end
598
+
599
+ warnings
600
+ end
601
+
463
602
  # Wrapper class to adapt repository config to existing Backup class
464
603
  class BackupConfig
465
604
  def initialize(repo_config, merged_settings)
data/lib/ruborg/config.rb CHANGED
@@ -9,10 +9,12 @@ module Ruborg
9
9
  class Config
10
10
  attr_reader :data
11
11
 
12
- def initialize(config_path)
12
+ def initialize(config_path, validate_types: true)
13
13
  @config_path = config_path
14
+ @validate_types = validate_types
14
15
  load_config
15
16
  validate_format
17
+ validate_schema if @validate_types
16
18
  end
17
19
 
18
20
  def load_config
@@ -39,7 +41,7 @@ module Ruborg
39
41
 
40
42
  def global_settings
41
43
  @data.slice("passbolt", "compression", "encryption", "auto_init", "borg_options", "log_file", "retention",
42
- "auto_prune")
44
+ "auto_prune", "hostname", "allow_remove_source")
43
45
  end
44
46
 
45
47
  private
@@ -84,5 +86,78 @@ module Ruborg
84
86
 
85
87
  patterns
86
88
  end
89
+
90
+ # Validate YAML schema for type correctness
91
+ def validate_schema
92
+ errors = []
93
+
94
+ # Validate global boolean settings
95
+ errors.concat(validate_boolean_config(@data, "auto_init", "global"))
96
+ errors.concat(validate_boolean_config(@data, "auto_prune", "global"))
97
+ errors.concat(validate_boolean_config(@data, "allow_remove_source", "global"))
98
+
99
+ # Validate global borg_options
100
+ if @data["borg_options"]
101
+ errors.concat(validate_boolean_config(@data["borg_options"], "allow_relocated_repo", "global/borg_options"))
102
+ errors.concat(validate_boolean_config(@data["borg_options"], "allow_unencrypted_repo", "global/borg_options"))
103
+ end
104
+
105
+ # Validate compression and encryption if present
106
+ if @data["compression"] && !VALID_COMPRESSION.include?(@data["compression"])
107
+ errors << "global/compression: invalid value '#{@data["compression"]}'"
108
+ end
109
+
110
+ if @data["encryption"] && !VALID_ENCRYPTION.include?(@data["encryption"])
111
+ errors << "global/encryption: invalid value '#{@data["encryption"]}'"
112
+ end
113
+
114
+ # Validate per-repository settings
115
+ repositories.each do |repo|
116
+ repo_name = repo["name"] || "unnamed"
117
+
118
+ errors.concat(validate_boolean_config(repo, "auto_init", repo_name))
119
+ errors.concat(validate_boolean_config(repo, "auto_prune", repo_name))
120
+ errors.concat(validate_boolean_config(repo, "allow_remove_source", repo_name))
121
+
122
+ if repo["borg_options"]
123
+ errors.concat(validate_boolean_config(repo["borg_options"], "allow_relocated_repo",
124
+ "#{repo_name}/borg_options"))
125
+ errors.concat(validate_boolean_config(repo["borg_options"], "allow_unencrypted_repo",
126
+ "#{repo_name}/borg_options"))
127
+ end
128
+
129
+ # Validate compression and encryption if present
130
+ if repo["compression"] && !VALID_COMPRESSION.include?(repo["compression"])
131
+ errors << "#{repo_name}/compression: invalid value '#{repo["compression"]}'"
132
+ end
133
+
134
+ if repo["encryption"] && !VALID_ENCRYPTION.include?(repo["encryption"])
135
+ errors << "#{repo_name}/encryption: invalid value '#{repo["encryption"]}'"
136
+ end
137
+
138
+ # Validate repository structure
139
+ errors << "#{repo_name}: missing 'path' key" unless repo["path"]
140
+ errors << "#{repo_name}: 'sources' must be an array" if repo["sources"] && !repo["sources"].is_a?(Array)
141
+ end
142
+
143
+ return if errors.empty?
144
+
145
+ raise ConfigError,
146
+ "Configuration validation failed:\n - #{errors.join("\n - ")}\n\n" \
147
+ "Run 'ruborg validate' for detailed validation information."
148
+ end
149
+
150
+ def validate_boolean_config(config, key, context)
151
+ errors = []
152
+ value = config[key]
153
+
154
+ return errors if value.nil? # Not set is OK
155
+
156
+ unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
157
+ errors << "#{context}/#{key}: must be boolean (true or false), got #{value.class}: #{value.inspect}"
158
+ end
159
+
160
+ errors
161
+ end
87
162
  end
88
163
  end
@@ -189,8 +189,12 @@ module Ruborg
189
189
  env = {}
190
190
  env["BORG_PASSPHRASE"] = @passphrase if @passphrase
191
191
 
192
+ # Use strict boolean checking (only true/false allowed, default to true for backward compatibility)
192
193
  allow_relocated = @borg_options.fetch("allow_relocated_repo", true)
194
+ allow_relocated = true unless allow_relocated == false # Normalize: only false disables, everything else enables
195
+
193
196
  allow_unencrypted = @borg_options.fetch("allow_unencrypted_repo", true)
197
+ allow_unencrypted = true unless allow_unencrypted == false # Normalize: only false disables, everything else enables
194
198
 
195
199
  env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = allow_relocated ? "yes" : "no"
196
200
  env["BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK"] = allow_unencrypted ? "yes" : "no"
@@ -327,8 +331,12 @@ module Ruborg
327
331
  env["BORG_PASSPHRASE"] = @passphrase if @passphrase
328
332
 
329
333
  # Apply Borg environment options from config (defaults to yes for backward compatibility)
334
+ # Use strict boolean checking (only true/false allowed, default to true for backward compatibility)
330
335
  allow_relocated = @borg_options.fetch("allow_relocated_repo", true)
336
+ allow_relocated = true unless allow_relocated == false # Normalize: only false disables, everything else enables
337
+
331
338
  allow_unencrypted = @borg_options.fetch("allow_unencrypted_repo", true)
339
+ allow_unencrypted = true unless allow_unencrypted == false # Normalize: only false disables, everything else enables
332
340
 
333
341
  env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = allow_relocated ? "yes" : "no"
334
342
  env["BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK"] = allow_unencrypted ? "yes" : "no"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruborg
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/ruborg.gemspec ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ruborg/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ruborg"
7
+ spec.version = Ruborg::VERSION
8
+ spec.authors = ["Michail Pantelelis"]
9
+ spec.email = ["mpantel@aegean.gr"]
10
+
11
+ spec.summary = "A friendly Ruby frontend for Borg backup"
12
+ spec.description = "Ruborg provides a user-friendly interface to Borg backup. " \
13
+ "It reads YAML configuration files and orchestrates backup operations, " \
14
+ "supporting repository creation, backup management, and Passbolt integration."
15
+ spec.homepage = "https://github.com/mpantel/ruborg"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 3.2.0"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
21
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (File.expand_path(f) == __FILE__) ||
28
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
29
+ end
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ # Dependencies
36
+ spec.add_dependency "psych", "~> 5.0"
37
+ spec.add_dependency "thor", "~> 1.3"
38
+
39
+ # Development dependencies
40
+ spec.add_development_dependency "bundler", "~> 2.0"
41
+ spec.add_development_dependency "bundler-audit", "~> 0.9"
42
+ spec.add_development_dependency "rake", "~> 13.0"
43
+ spec.add_development_dependency "rspec", "~> 3.0"
44
+ spec.add_development_dependency "rubocop", "~> 1.0"
45
+ spec.add_development_dependency "rubocop-rspec", "~> 3.0"
46
+ end
data/ruborg.yml.example CHANGED
@@ -6,10 +6,12 @@
6
6
  # # Edit ruborg.yml with your settings
7
7
 
8
8
  # Global settings (applied to all repositories unless overridden)
9
+ hostname: myserver.local # Optional: restrict configuration to specific hostname
9
10
  compression: lz4 # Options: lz4 (fast), zstd (balanced), lzma (high compression), none
10
11
  encryption: repokey # Options: repokey, keyfile, none (NOT recommended)
11
12
  auto_init: true # Automatically initialize repositories on first use
12
13
  auto_prune: true # Automatically prune old backups after each backup
14
+ allow_remove_source: false # Allow --remove-source flag to delete source files after backup (default: false)
13
15
  log_file: ~/.ruborg/logs/ruborg.log # Log file path (optional)
14
16
 
15
17
  # Custom Borg executable path (optional)
@@ -65,6 +67,7 @@ repositories:
65
67
  - name: databases
66
68
  description: "MySQL and PostgreSQL database dumps"
67
69
  path: /mnt/backup/borg-databases
70
+ hostname: dbserver.local # Optional: repository-specific hostname override
68
71
  retention_mode: per_file # Each file gets its own archive
69
72
  # Repository-specific passbolt (overrides global)
70
73
  passbolt:
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.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michail Pantelelis
@@ -148,6 +148,7 @@ files:
148
148
  - lib/ruborg/passbolt.rb
149
149
  - lib/ruborg/repository.rb
150
150
  - lib/ruborg/version.rb
151
+ - ruborg.gemspec
151
152
  - ruborg.yml.example
152
153
  homepage: https://github.com/mpantel/ruborg
153
154
  licenses: