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 +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +150 -4
- data/SECURITY.md +34 -0
- data/lib/ruborg/cli.rb +143 -4
- data/lib/ruborg/config.rb +77 -2
- data/lib/ruborg/repository.rb +8 -0
- data/lib/ruborg/version.rb +1 -1
- data/ruborg.gemspec +46 -0
- data/ruborg.yml.example +3 -0
- 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: 821f1707180870ec6b2ffccda4ef4d901b3ee274d7402c26170ca294e80efb8b
|
|
4
|
+
data.tar.gz: 62e2a5ee53cd75024b78e08e32295c34cf1829cd3bd34be9d6403c7ec1dfb810
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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
|
-
- **
|
|
165
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/ruborg/repository.rb
CHANGED
|
@@ -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"
|
data/lib/ruborg/version.rb
CHANGED
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
|
+
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:
|