gem_guard 1.1.2 → 1.2.4
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 +30 -0
- data/README.md +92 -1
- data/SECURITY.md +10 -8
- data/lib/gem_guard/auto_fixer.rb +35 -26
- data/lib/gem_guard/cli.rb +49 -5
- data/lib/gem_guard/parser.rb +55 -1
- data/lib/gem_guard/version.rb +1 -1
- data/lib/gem_guard.rb +4 -0
- metadata +61 -8
- data/gem_guard.gemspec +0 -35
- data/plan.md +0 -167
- data/test_nokogiri.lock +0 -13
- data/test_nokogiri.lock.backup.20250810_002252 +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b97a15143a123482699d1aa0bb8c4458c19dd9d61bc4c5044ffb305064a6dc24
|
4
|
+
data.tar.gz: 1aa0c21a8f0b0f6d5ff78af7c75759c1f5f5a777329090eae1a27ca55488893c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a2ac4bc9d7f793a603d694a105b0a235f1663ab4e91ae189729097cf2ea2e2c2159ebb08fd2c5fb8e6d15b99a32754cf4a310fef4416af8f684a46189c8ca24
|
7
|
+
data.tar.gz: 23fc743bebe7451d7e30a7b405aa8498b98be33c529317be4084af278fced5761b2a379496b07ee32b4ba58a5c5258618b838bf6c1fec132209cdf67cfcccbf8
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [1.2.0] - 2025-08-17
|
11
|
+
|
12
|
+
### Added
|
13
|
+
- Interactive fix flow: `gem_guard fix --interactive` prompts per gem (via `tty-prompt`).
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
- Dry-run output refined to: `✅ Would update <gem> <from> → <to>` for clarity.
|
17
|
+
|
18
|
+
### Dependencies
|
19
|
+
- Add runtime dependency: `tty-prompt ~> 0.23`.
|
20
|
+
|
21
|
+
## [1.1.2] - 2025-08-11
|
22
|
+
|
23
|
+
### Added
|
24
|
+
- Auto-Fix Mode: `gem_guard fix` to automatically update vulnerable gems with safe versions
|
25
|
+
- SBOM generation: SPDX and CycloneDX outputs via `gem_guard sbom`
|
26
|
+
- Typosquat detection with fuzzy matching and RubyGems API integration
|
27
|
+
|
28
|
+
### Changed
|
29
|
+
- Standardized CLI exit codes (0: clean, 1: vulnerabilities, 2: error)
|
30
|
+
- Improved integration test mocking; tests run fast without external calls
|
31
|
+
- Dropped Ruby 3.0 from CI matrix; now testing 3.1, 3.2, 3.3
|
32
|
+
- SECURITY.md updated to use GitHub Security Advisories for private reports (no direct email)
|
33
|
+
|
34
|
+
### Fixed
|
35
|
+
- Deduplicated platform-specific gem vulnerabilities in reports
|
36
|
+
- Resolved config state leakage across tests via deep copy of defaults
|
37
|
+
- CI/CD publishing issues (RubyGems token permissions, bundler as dev dependency)
|
38
|
+
- CLI integration test exit code handling and minor lint issues
|
39
|
+
|
10
40
|
## [0.1.0] - 2025-08-08
|
11
41
|
|
12
42
|
### Added
|
data/README.md
CHANGED
@@ -98,6 +98,8 @@ gem_guard scan --format json --output vulnerabilities.json
|
|
98
98
|
gem_guard scan --fail-on-vulnerabilities --severity-threshold high
|
99
99
|
```
|
100
100
|
|
101
|
+
> Exit codes: 0 = success, 1 = vulnerabilities found (only when `--fail-on-vulnerabilities` is set), 2 = error. See [Exit Codes](#exit-codes).
|
102
|
+
|
101
103
|
**Example output:**
|
102
104
|
```
|
103
105
|
🚨 Security Vulnerabilities Found
|
@@ -122,6 +124,40 @@ Details:
|
|
122
124
|
🔧 Fix: bundle update thor --to 1.4.0
|
123
125
|
```
|
124
126
|
|
127
|
+
### 🛠 Auto-fix Vulnerable Dependencies
|
128
|
+
|
129
|
+
Use `fix` to apply recommended upgrades. Start with a dry run to preview changes.
|
130
|
+
|
131
|
+
```bash
|
132
|
+
# Preview only — shows what would change, does not modify files
|
133
|
+
gem_guard fix --dry-run
|
134
|
+
|
135
|
+
# Interactively confirm each upgrade (uses tty-prompt)
|
136
|
+
gem_guard fix --interactive
|
137
|
+
|
138
|
+
# Apply fixes non-interactively
|
139
|
+
gem_guard fix
|
140
|
+
```
|
141
|
+
|
142
|
+
Dry run output example:
|
143
|
+
|
144
|
+
```
|
145
|
+
🔍 Dry run — no files will be modified.
|
146
|
+
========================================
|
147
|
+
✅ Would update nokogiri 1.12.0 → 1.14.3
|
148
|
+
|
149
|
+
Dry run completed. 1 fixes planned.
|
150
|
+
Run without --dry-run to apply these fixes.
|
151
|
+
```
|
152
|
+
|
153
|
+
Behavior notes:
|
154
|
+
|
155
|
+
- **Interactive**: You’ll be asked per gem: `Upgrade nokogiri 1.12.0 → 1.14.3?` Answering “no” skips that gem.
|
156
|
+
- **Backups**: A `Gemfile.lock.backup.YYYYMMDD_HHMMSS` is created only if at least one fix is approved/applied.
|
157
|
+
- **Requirements**: `Gemfile` and `Gemfile.lock` must exist. Interactive prompts require a TTY-capable environment.
|
158
|
+
|
159
|
+
> Exit codes: 0 = success, 2 = error. See [Exit Codes](#exit-codes). Use `--verbose` for diagnostics if a file/permission error occurs.
|
160
|
+
|
125
161
|
### 🎯 Typosquat Detection
|
126
162
|
|
127
163
|
**Basic typosquat check:**
|
@@ -151,6 +187,8 @@ gem_guard typosquat --format json --output typosquats.json
|
|
151
187
|
🔧 Consider: Did you mean 'rails'? Review this dependency carefully.
|
152
188
|
```
|
153
189
|
|
190
|
+
> Exit codes: 0 = no high/critical risks, 1 = high/critical typosquat risks detected, 2 = error. See [Exit Codes](#exit-codes). Use `--verbose` for diagnostics on file errors.
|
191
|
+
|
154
192
|
### 📋 SBOM Generation
|
155
193
|
|
156
194
|
**Generate SPDX SBOM:**
|
@@ -185,6 +223,8 @@ gem_guard sbom --project my-app --output sbom.json
|
|
185
223
|
}
|
186
224
|
```
|
187
225
|
|
226
|
+
> Exit codes: 0 = success, 2 = error. See [Exit Codes](#exit-codes). Use `--verbose` for diagnostics on file errors.
|
227
|
+
|
188
228
|
## ⚙️ Configuration
|
189
229
|
|
190
230
|
GemGuard supports project-level configuration via `.gemguard.yml`:
|
@@ -313,6 +353,57 @@ jobs:
|
|
313
353
|
path: sbom.json
|
314
354
|
```
|
315
355
|
|
356
|
+
## ❗️ Troubleshooting
|
357
|
+
|
358
|
+
### InvalidLockfileError: Invalid Gemfile.lock
|
359
|
+
|
360
|
+
GemGuard raises `GemGuard::InvalidLockfileError` when the `Gemfile.lock` is malformed (e.g., truncated file, malformed `DEPENDENCIES` entries, or dependencies not present in the `specs` section).
|
361
|
+
|
362
|
+
Common fixes:
|
363
|
+
|
364
|
+
- Run `bundle install` to regenerate `Gemfile.lock`.
|
365
|
+
- If issues persist, delete `Gemfile.lock` and run `bundle install` again to fully regenerate.
|
366
|
+
- Ensure the file wasn’t manually edited. `Gemfile.lock` should be managed by Bundler.
|
367
|
+
- Verify Bundler version compatibility (see the `BUNDLED WITH` section at the bottom of your lockfile).
|
368
|
+
|
369
|
+
If you believe the lockfile is valid but still see this error, please open an issue with your `Gemfile.lock` attached (sanitized if needed).
|
370
|
+
|
371
|
+
### FileError: Filesystem or Permission Issues
|
372
|
+
|
373
|
+
GemGuard raises `GemGuard::FileError` when it cannot read or write required files (e.g., missing `Gemfile`, missing `Gemfile.lock`, or permission denied when creating backups or writing reports).
|
374
|
+
|
375
|
+
Common scenarios and fixes:
|
376
|
+
|
377
|
+
- **Gemfile not found** (auto-fix): Ensure a `Gemfile` exists at the project root or pass `--gemfile PATH`.
|
378
|
+
|
379
|
+
- **Gemfile.lock not found**:
|
380
|
+
- Run `bundle install` to generate it, or pass `--lockfile PATH` to point to the correct file.
|
381
|
+
|
382
|
+
- **Permission denied when creating backup (fix)**:
|
383
|
+
- The `fix` command creates a backup like `Gemfile.lock.backup.YYYYMMDD_HHMMSS` when applying changes.
|
384
|
+
- Ensure the working directory is writable by your user: `chmod u+w .` or run within a writable workspace.
|
385
|
+
- In CI, ensure the job user has write access to the repo checkout directory.
|
386
|
+
|
387
|
+
- **Permission denied when writing output files (scan/typosquat/sbom)**:
|
388
|
+
- Specify a writable path using `--output`, e.g.: `gem_guard scan --output tmp/vulns.json`.
|
389
|
+
- Create the directory first: `mkdir -p tmp`.
|
390
|
+
|
391
|
+
- **Read-only mounted directories in containers/CI**:
|
392
|
+
- Write outputs to a mounted writable volume, e.g., `/tmp`, and upload artifacts from there.
|
393
|
+
|
394
|
+
If you continue to see `FileError`, re-run with verbose shell tracing to confirm permissions:
|
395
|
+
|
396
|
+
```bash
|
397
|
+
set -x
|
398
|
+
gem_guard scan --output tmp/vulns.json
|
399
|
+
```
|
400
|
+
|
401
|
+
You can also add GemGuard diagnostics:
|
402
|
+
|
403
|
+
```bash
|
404
|
+
gem_guard scan --verbose --output tmp/vulns.json
|
405
|
+
```
|
406
|
+
|
316
407
|
## Development
|
317
408
|
|
318
409
|
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bundle exec rake standard` to run the linter.
|
@@ -355,7 +446,7 @@ We welcome contributions! Here's how you can help:
|
|
355
446
|
|
356
447
|
### Development Guidelines
|
357
448
|
|
358
|
-
- Follow
|
449
|
+
- Follow pragmatic, intention-revealing, minimal abstractions
|
359
450
|
- Use strict outside-in TDD with RSpec
|
360
451
|
- Maintain 100% test coverage
|
361
452
|
- Follow StandardRB for code style
|
data/SECURITY.md
CHANGED
@@ -10,7 +10,11 @@ We actively support the following versions of GemGuard:
|
|
10
10
|
|
11
11
|
## Reporting a Vulnerability
|
12
12
|
|
13
|
-
If you discover a security vulnerability within GemGuard, please
|
13
|
+
If you discover a security vulnerability within GemGuard, please submit a private report via GitHub Security Advisories:
|
14
|
+
|
15
|
+
https://github.com/wilburhimself/gem_guard/security/advisories/new
|
16
|
+
|
17
|
+
All security vulnerabilities will be promptly addressed through the advisory workflow.
|
14
18
|
|
15
19
|
**Please do not report security vulnerabilities through public GitHub issues.**
|
16
20
|
|
@@ -78,16 +82,15 @@ We provide security updates through:
|
|
78
82
|
|
79
83
|
- **GitHub Security Advisories** for critical vulnerabilities
|
80
84
|
- **RubyGems.org releases** with security patches
|
81
|
-
- **Email notifications** to security@wilburhimself.com subscribers
|
82
85
|
- **GitHub releases** with detailed changelogs
|
83
86
|
|
84
87
|
## Contact
|
85
88
|
|
86
|
-
For security-related inquiries:
|
89
|
+
For security-related inquiries and reports, use GitHub Security Advisories to contact maintainers privately:
|
90
|
+
|
91
|
+
https://github.com/wilburhimself/gem_guard/security/advisories/new
|
87
92
|
|
88
|
-
|
89
|
-
- **PGP Key**: Available upon request
|
90
|
-
- **Response Time**: 48 hours for initial response
|
93
|
+
We do not monitor any dedicated security email inbox. Initial response within 48 hours.
|
91
94
|
|
92
95
|
---
|
93
96
|
|
@@ -97,6 +100,5 @@ For security-related inquiries:
|
|
97
100
|
|
98
101
|
## Contact
|
99
102
|
|
100
|
-
For
|
101
|
-
- Email: security@wilburhimself.com
|
103
|
+
For general questions, reach out via GitHub Discussions or issues as appropriate:
|
102
104
|
- GitHub: [@wilburhimself](https://github.com/wilburhimself)
|
data/lib/gem_guard/auto_fixer.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "bundler"
|
2
2
|
require "fileutils"
|
3
|
+
require "tty-prompt"
|
3
4
|
|
4
5
|
module GemGuard
|
5
6
|
class AutoFixer
|
@@ -15,11 +16,11 @@ module GemGuard
|
|
15
16
|
create_backup = options.fetch(:backup, true)
|
16
17
|
|
17
18
|
unless File.exist?(@gemfile_path)
|
18
|
-
raise "Gemfile not found at #{@gemfile_path}. Auto-fix requires a Gemfile."
|
19
|
+
raise GemGuard::FileError, "Gemfile not found at #{@gemfile_path}. Auto-fix requires a Gemfile."
|
19
20
|
end
|
20
21
|
|
21
22
|
unless File.exist?(@lockfile_path)
|
22
|
-
raise "Gemfile.lock not found at #{@lockfile_path}. Run 'bundle install' first."
|
23
|
+
raise GemGuard::FileError, "Gemfile.lock not found at #{@lockfile_path}. Run 'bundle install' first."
|
23
24
|
end
|
24
25
|
|
25
26
|
fixes = plan_fixes(vulnerable_dependencies)
|
@@ -32,13 +33,12 @@ module GemGuard
|
|
32
33
|
return {status: :dry_run, fixes: fixes, message: "Dry run completed. #{fixes.length} fixes planned."}
|
33
34
|
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
create_lockfile_backup if create_backup
|
36
|
+
# Apply fixes with optional per-gem confirmation
|
37
|
+
applied_fixes, cancelled = apply_fixes(fixes, interactive: interactive, backup: create_backup)
|
40
38
|
|
41
|
-
|
39
|
+
if cancelled
|
40
|
+
return {status: :cancelled, message: "No fixes approved."}
|
41
|
+
end
|
42
42
|
|
43
43
|
{
|
44
44
|
status: :completed,
|
@@ -92,20 +92,8 @@ module GemGuard
|
|
92
92
|
end
|
93
93
|
|
94
94
|
def confirm_fixes(fixes)
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
fixes.each do |fix|
|
99
|
-
severity_emoji = severity_emoji(fix[:severity])
|
100
|
-
puts "#{severity_emoji} #{fix[:gem_name]}: #{fix[:current_version]} → #{fix[:target_version]}"
|
101
|
-
puts " Fixes: #{fix[:vulnerability_id]}"
|
102
|
-
end
|
103
|
-
|
104
|
-
puts "\n⚠️ This will modify your Gemfile.lock and may require bundle install."
|
105
|
-
print "Do you want to proceed? (y/N): "
|
106
|
-
|
107
|
-
response = $stdin.gets.chomp.downcase
|
108
|
-
response == "y" || response == "yes"
|
95
|
+
# Deprecated: kept for compatibility but not used with per-gem prompts
|
96
|
+
true
|
109
97
|
end
|
110
98
|
|
111
99
|
def severity_emoji(severity)
|
@@ -125,15 +113,36 @@ module GemGuard
|
|
125
113
|
return if @backup_created
|
126
114
|
|
127
115
|
backup_path = "#{@lockfile_path}.backup.#{Time.now.strftime("%Y%m%d_%H%M%S")}"
|
128
|
-
|
116
|
+
begin
|
117
|
+
FileUtils.cp(@lockfile_path, backup_path)
|
118
|
+
rescue Errno::EACCES, Errno::EPERM => e
|
119
|
+
raise GemGuard::FileError, "Cannot write backup for #{@lockfile_path}: #{e.message}. Check file permissions."
|
120
|
+
end
|
129
121
|
@backup_created = true
|
130
122
|
puts "📦 Created backup: #{backup_path}"
|
131
123
|
end
|
132
124
|
|
133
|
-
def apply_fixes(fixes)
|
125
|
+
def apply_fixes(fixes, interactive: false, backup: true)
|
134
126
|
applied_fixes = []
|
135
127
|
|
136
|
-
fixes
|
128
|
+
# Determine which fixes to apply
|
129
|
+
selected_fixes = if interactive
|
130
|
+
prompt = TTY::Prompt.new
|
131
|
+
fixes.select do |fix|
|
132
|
+
question = "Upgrade #{fix[:gem_name]} #{fix[:current_version]} → #{fix[:target_version]}?"
|
133
|
+
prompt.yes?(question)
|
134
|
+
end
|
135
|
+
else
|
136
|
+
fixes
|
137
|
+
end
|
138
|
+
|
139
|
+
# If no fixes were approved, signal cancellation
|
140
|
+
return [applied_fixes, true] if selected_fixes.empty?
|
141
|
+
|
142
|
+
# Create backup only if we will actually apply at least one fix
|
143
|
+
create_lockfile_backup if backup
|
144
|
+
|
145
|
+
selected_fixes.each do |fix|
|
137
146
|
if apply_single_fix(fix)
|
138
147
|
applied_fixes << fix
|
139
148
|
puts "✅ Updated #{fix[:gem_name]} to #{fix[:target_version]}"
|
@@ -148,7 +157,7 @@ module GemGuard
|
|
148
157
|
system("bundle install")
|
149
158
|
end
|
150
159
|
|
151
|
-
applied_fixes
|
160
|
+
[applied_fixes, false]
|
152
161
|
end
|
153
162
|
|
154
163
|
def apply_single_fix(fix)
|
data/lib/gem_guard/cli.rb
CHANGED
@@ -8,6 +8,9 @@ module GemGuard
|
|
8
8
|
EXIT_VULNERABILITIES_FOUND = 1
|
9
9
|
EXIT_ERROR = 2
|
10
10
|
|
11
|
+
# Global options
|
12
|
+
class_option :verbose, type: :boolean, desc: "Print extra diagnostics on errors"
|
13
|
+
|
11
14
|
desc "scan", "Scan dependencies for known vulnerabilities"
|
12
15
|
option :format, type: :string, desc: "Output format (table, json)"
|
13
16
|
option :lockfile, type: :string, desc: "Path to Gemfile.lock"
|
@@ -27,6 +30,7 @@ module GemGuard
|
|
27
30
|
|
28
31
|
unless File.exist?(lockfile_path)
|
29
32
|
puts "Error: #{lockfile_path} not found"
|
33
|
+
verbose_diagnostics([lockfile_path])
|
30
34
|
exit EXIT_ERROR
|
31
35
|
end
|
32
36
|
|
@@ -56,6 +60,14 @@ module GemGuard
|
|
56
60
|
else
|
57
61
|
exit EXIT_SUCCESS
|
58
62
|
end
|
63
|
+
rescue GemGuard::InvalidLockfileError => e
|
64
|
+
puts "Invalid Gemfile.lock: #{e.message}"
|
65
|
+
puts "Tip: Run 'bundle install' to regenerate your lockfile."
|
66
|
+
exit EXIT_ERROR
|
67
|
+
rescue GemGuard::FileError => e
|
68
|
+
puts "File error: #{e.message}"
|
69
|
+
verbose_diagnostics([lockfile_path, output_file].compact)
|
70
|
+
exit EXIT_ERROR
|
59
71
|
rescue => e
|
60
72
|
puts "Error: #{e.message}"
|
61
73
|
exit EXIT_ERROR
|
@@ -72,7 +84,8 @@ module GemGuard
|
|
72
84
|
|
73
85
|
unless File.exist?(lockfile_path)
|
74
86
|
puts "Error: #{lockfile_path} not found"
|
75
|
-
|
87
|
+
verbose_diagnostics([lockfile_path])
|
88
|
+
exit EXIT_ERROR
|
76
89
|
end
|
77
90
|
|
78
91
|
dependencies = Parser.new.parse(lockfile_path)
|
@@ -85,7 +98,7 @@ module GemGuard
|
|
85
98
|
generator.generate_cyclone_dx(dependencies, options[:project])
|
86
99
|
else
|
87
100
|
puts "Error: Unsupported format '#{options[:format]}'. Use 'spdx' or 'cyclone-dx'"
|
88
|
-
exit
|
101
|
+
exit EXIT_ERROR
|
89
102
|
end
|
90
103
|
|
91
104
|
output_json = JSON.pretty_generate(sbom_data)
|
@@ -112,6 +125,7 @@ module GemGuard
|
|
112
125
|
|
113
126
|
unless File.exist?(lockfile_path)
|
114
127
|
puts "Error: #{lockfile_path} not found"
|
128
|
+
verbose_diagnostics([lockfile_path])
|
115
129
|
exit EXIT_ERROR
|
116
130
|
end
|
117
131
|
|
@@ -134,6 +148,14 @@ module GemGuard
|
|
134
148
|
else
|
135
149
|
exit EXIT_SUCCESS
|
136
150
|
end
|
151
|
+
rescue GemGuard::InvalidLockfileError => e
|
152
|
+
puts "Invalid Gemfile.lock: #{e.message}"
|
153
|
+
puts "Tip: Run 'bundle install' to regenerate your lockfile."
|
154
|
+
exit EXIT_ERROR
|
155
|
+
rescue GemGuard::FileError => e
|
156
|
+
puts "File error: #{e.message}"
|
157
|
+
verbose_diagnostics([lockfile_path, output_file].compact)
|
158
|
+
exit EXIT_ERROR
|
137
159
|
rescue => e
|
138
160
|
puts "Error: #{e.message}"
|
139
161
|
exit EXIT_ERROR
|
@@ -208,11 +230,13 @@ module GemGuard
|
|
208
230
|
|
209
231
|
unless File.exist?(lockfile_path)
|
210
232
|
puts "Error: #{lockfile_path} not found"
|
233
|
+
verbose_diagnostics([lockfile_path])
|
211
234
|
exit EXIT_ERROR
|
212
235
|
end
|
213
236
|
|
214
237
|
unless File.exist?(gemfile_path)
|
215
238
|
puts "Error: #{gemfile_path} not found. Auto-fix requires a Gemfile."
|
239
|
+
verbose_diagnostics([gemfile_path])
|
216
240
|
exit EXIT_ERROR
|
217
241
|
end
|
218
242
|
|
@@ -241,11 +265,10 @@ module GemGuard
|
|
241
265
|
puts "ℹ️ #{result[:message]}"
|
242
266
|
exit EXIT_SUCCESS
|
243
267
|
when :dry_run
|
244
|
-
puts "🔍 Dry
|
268
|
+
puts "🔍 Dry run — no files will be modified."
|
245
269
|
puts "=" * 40
|
246
270
|
result[:fixes].each do |fix|
|
247
|
-
puts "#{fix[:gem_name]}
|
248
|
-
puts " Fixes: #{fix[:vulnerability_id]} (#{fix[:severity]})"
|
271
|
+
puts "✅ Would update #{fix[:gem_name]} #{fix[:current_version]} → #{fix[:target_version]}"
|
249
272
|
end
|
250
273
|
puts "\n#{result[:message]}"
|
251
274
|
puts "Run without --dry-run to apply these fixes."
|
@@ -265,6 +288,13 @@ module GemGuard
|
|
265
288
|
puts "❌ Unexpected error during fix operation"
|
266
289
|
exit EXIT_ERROR
|
267
290
|
end
|
291
|
+
rescue GemGuard::InvalidLockfileError => e
|
292
|
+
puts "Invalid Gemfile.lock: #{e.message}"
|
293
|
+
puts "Tip: Run 'bundle install' to regenerate your lockfile."
|
294
|
+
exit EXIT_ERROR
|
295
|
+
rescue GemGuard::FileError => e
|
296
|
+
puts "File error: #{e.message}"
|
297
|
+
exit EXIT_ERROR
|
268
298
|
rescue => e
|
269
299
|
puts "Error: #{e.message}"
|
270
300
|
exit EXIT_ERROR
|
@@ -384,5 +414,19 @@ module GemGuard
|
|
384
414
|
def number_with_commas(number)
|
385
415
|
number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
386
416
|
end
|
417
|
+
|
418
|
+
def verbose_diagnostics(paths)
|
419
|
+
return unless options[:verbose]
|
420
|
+
puts "\n[Diagnostics]"
|
421
|
+
puts "cwd: #{Dir.pwd}"
|
422
|
+
Array(paths).each do |p|
|
423
|
+
exists = File.exist?(p)
|
424
|
+
readable = exists ? File.readable?(p) : false
|
425
|
+
writable_dir = File.writable?(File.directory?(p) ? p : File.dirname(p))
|
426
|
+
puts "- #{p}: exists=#{exists}, readable=#{readable}, writable_dir=#{writable_dir}"
|
427
|
+
rescue => ex
|
428
|
+
puts "- #{p}: (error checking permissions: #{ex.message})"
|
429
|
+
end
|
430
|
+
end
|
387
431
|
end
|
388
432
|
end
|
data/lib/gem_guard/parser.rb
CHANGED
@@ -3,7 +3,18 @@ require "bundler"
|
|
3
3
|
module GemGuard
|
4
4
|
class Parser
|
5
5
|
def parse(lockfile_path)
|
6
|
-
|
6
|
+
content = File.read(lockfile_path)
|
7
|
+
begin
|
8
|
+
lockfile = Bundler::LockfileParser.new(content)
|
9
|
+
rescue => e
|
10
|
+
# Wrap any parsing errors from Bundler with a clearer custom error
|
11
|
+
raise GemGuard::InvalidLockfileError, "Invalid Gemfile.lock at #{lockfile_path}: #{e.message}"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Basic structural validation to catch truncated files quickly
|
15
|
+
unless content.include?("\nBUNDLED WITH") || content.end_with?("BUNDLED WITH\n")
|
16
|
+
raise GemGuard::InvalidLockfileError, "Invalid Gemfile.lock at #{lockfile_path}: missing 'BUNDLED WITH' section"
|
17
|
+
end
|
7
18
|
|
8
19
|
dependencies = []
|
9
20
|
|
@@ -16,6 +27,9 @@ module GemGuard
|
|
16
27
|
)
|
17
28
|
end
|
18
29
|
|
30
|
+
# Validate DEPENDENCIES section formatting and presence in specs
|
31
|
+
validate_dependencies_section!(content, dependencies.map(&:name), lockfile_path)
|
32
|
+
|
19
33
|
# Deduplicate dependencies by name to handle platform-specific gems
|
20
34
|
# (e.g., nokogiri-arm64-darwin, nokogiri-x86_64-darwin, etc.)
|
21
35
|
dependencies.uniq { |dep| dep.name }
|
@@ -23,6 +37,46 @@ module GemGuard
|
|
23
37
|
|
24
38
|
private
|
25
39
|
|
40
|
+
def validate_dependencies_section!(content, spec_names, lockfile_path)
|
41
|
+
lines = content.lines
|
42
|
+
start_index = lines.index { |l| l.strip == "DEPENDENCIES" }
|
43
|
+
return unless start_index # If no section, let Bundler rules apply
|
44
|
+
|
45
|
+
# Collect until blank line or next all-caps heading
|
46
|
+
deps = []
|
47
|
+
i = start_index + 1
|
48
|
+
while i < lines.length
|
49
|
+
line = lines[i]
|
50
|
+
break if line.strip.empty?
|
51
|
+
break if line == line.upcase && line.match?(/^[A-Z\s]+$/)
|
52
|
+
|
53
|
+
# Ignore comments on the line
|
54
|
+
stripped = line.split("#", 2).first.to_s.rstrip
|
55
|
+
if stripped.strip.empty?
|
56
|
+
i += 1
|
57
|
+
next
|
58
|
+
end
|
59
|
+
|
60
|
+
# Expect indentation then a gem name optionally with version in parens
|
61
|
+
if !/^\s{2,}[a-z0-9_\-]+(\s*\([^)]*\))?\s*$/i.match?(stripped)
|
62
|
+
raise GemGuard::InvalidLockfileError, "Invalid Gemfile.lock at #{lockfile_path}: malformed DEPENDENCIES entry '#{line.strip}'"
|
63
|
+
end
|
64
|
+
|
65
|
+
name = stripped.strip.split.first
|
66
|
+
# remove optional version tuple e.g., rails, or rails(=7.0.0) case without space
|
67
|
+
name = name.split("(").first
|
68
|
+
|
69
|
+
unless spec_names.include?(name)
|
70
|
+
raise GemGuard::InvalidLockfileError, "Invalid Gemfile.lock at #{lockfile_path}: dependency '#{name}' not found in specs"
|
71
|
+
end
|
72
|
+
|
73
|
+
deps << name
|
74
|
+
i += 1
|
75
|
+
end
|
76
|
+
|
77
|
+
deps
|
78
|
+
end
|
79
|
+
|
26
80
|
def extract_source(spec)
|
27
81
|
if spec.source.respond_to?(:uri)
|
28
82
|
spec.source.uri.to_s
|
data/lib/gem_guard/version.rb
CHANGED
data/lib/gem_guard.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gem_guard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilbur Suero
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: tty-prompt
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.23'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.23'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,14 +86,56 @@ dependencies:
|
|
72
86
|
requirements:
|
73
87
|
- - "~>"
|
74
88
|
- !ruby/object:Gem::Version
|
75
|
-
version: '1.
|
89
|
+
version: '1.39'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.39'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '13.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '13.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec-snapshot
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '2.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.22'
|
76
132
|
type: :development
|
77
133
|
prerelease: false
|
78
134
|
version_requirements: !ruby/object:Gem::Requirement
|
79
135
|
requirements:
|
80
136
|
- - "~>"
|
81
137
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
138
|
+
version: '0.22'
|
83
139
|
description: A comprehensive tool to detect, report, and remediate dependency-related
|
84
140
|
security risks in Ruby projects. Includes CVE scanning, SBOM generation, and CI/CD
|
85
141
|
integration.
|
@@ -96,7 +152,6 @@ files:
|
|
96
152
|
- Rakefile
|
97
153
|
- SECURITY.md
|
98
154
|
- exe/gem_guard
|
99
|
-
- gem_guard.gemspec
|
100
155
|
- lib/gem_guard.rb
|
101
156
|
- lib/gem_guard/analyzer.rb
|
102
157
|
- lib/gem_guard/auto_fixer.rb
|
@@ -108,12 +163,9 @@ files:
|
|
108
163
|
- lib/gem_guard/typosquat_checker.rb
|
109
164
|
- lib/gem_guard/version.rb
|
110
165
|
- lib/gem_guard/vulnerability_fetcher.rb
|
111
|
-
- plan.md
|
112
166
|
- templates/circleci-config.yml
|
113
167
|
- templates/github-actions.yml
|
114
168
|
- templates/gitlab-ci.yml
|
115
|
-
- test_nokogiri.lock
|
116
|
-
- test_nokogiri.lock.backup.20250810_002252
|
117
169
|
homepage: https://github.com/wilburhimself/gem_guard
|
118
170
|
licenses:
|
119
171
|
- MIT
|
@@ -121,6 +173,7 @@ metadata:
|
|
121
173
|
homepage_uri: https://github.com/wilburhimself/gem_guard
|
122
174
|
source_code_uri: https://github.com/wilburhimself/gem_guard
|
123
175
|
changelog_uri: https://github.com/wilburhimself/gem_guard/blob/main/CHANGELOG.md
|
176
|
+
rubygems_mfa_required: 'true'
|
124
177
|
post_install_message:
|
125
178
|
rdoc_options: []
|
126
179
|
require_paths:
|
data/gem_guard.gemspec
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
require_relative "lib/gem_guard/version"
|
2
|
-
|
3
|
-
Gem::Specification.new do |spec|
|
4
|
-
spec.name = "gem_guard"
|
5
|
-
spec.version = GemGuard::VERSION
|
6
|
-
spec.authors = ["Wilbur Suero"]
|
7
|
-
spec.email = ["wilbur@example.com"]
|
8
|
-
|
9
|
-
spec.summary = "Supply chain security and vulnerability management for Ruby gems"
|
10
|
-
spec.description = "A comprehensive tool to detect, report, and remediate dependency-related security risks in Ruby projects. Includes CVE scanning, SBOM generation, and CI/CD integration."
|
11
|
-
spec.homepage = "https://github.com/wilburhimself/gem_guard"
|
12
|
-
spec.license = "MIT"
|
13
|
-
spec.required_ruby_version = ">= 3.0.0"
|
14
|
-
|
15
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
-
spec.metadata["source_code_uri"] = "https://github.com/wilburhimself/gem_guard"
|
17
|
-
spec.metadata["changelog_uri"] = "https://github.com/wilburhimself/gem_guard/blob/main/CHANGELOG.md"
|
18
|
-
|
19
|
-
spec.files = Dir.chdir(__dir__) do
|
20
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
21
|
-
(File.expand_path(f) == __FILE__) ||
|
22
|
-
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
23
|
-
end
|
24
|
-
end
|
25
|
-
spec.bindir = "exe"
|
26
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
27
|
-
spec.require_paths = ["lib"]
|
28
|
-
|
29
|
-
spec.add_dependency "thor", "~> 1.0"
|
30
|
-
spec.add_dependency "json", "~> 2.0"
|
31
|
-
|
32
|
-
spec.add_development_dependency "bundler", ">= 2.0"
|
33
|
-
spec.add_development_dependency "rspec", "~> 3.0"
|
34
|
-
spec.add_development_dependency "standard", "~> 1.3"
|
35
|
-
end
|
data/plan.md
DELETED
@@ -1,167 +0,0 @@
|
|
1
|
-
# Supply Chain Security & Vulnerability Management Gem – Plan
|
2
|
-
|
3
|
-
## 1. Overview
|
4
|
-
|
5
|
-
**Working Name:** `gem_guard`
|
6
|
-
**Goal:** Provide Ruby developers with a one-stop tool to detect, report, and remediate dependency-related security risks.
|
7
|
-
**Core Capabilities:**
|
8
|
-
- Scan dependency tree (including transient deps)
|
9
|
-
- Detect known CVEs from public and private vulnerability databases
|
10
|
-
- Suggest safe upgrades and patches
|
11
|
-
- Generate SBOM (Software Bill of Materials) in SPDX/CycloneDX format
|
12
|
-
- Integrate with CI/CD to prevent unsafe deployments
|
13
|
-
|
14
|
-
---
|
15
|
-
|
16
|
-
## 2. Problems Being Solved
|
17
|
-
|
18
|
-
1. **Typosquatting & brand-jacking detection** – accidental installs of malicious gems with similar names.
|
19
|
-
2. **Unpatched dependencies** – gems with known vulnerabilities not updated.
|
20
|
-
3. **Lack of visibility** – no SBOM or complete dependency inventory.
|
21
|
-
4. **CI/CD security gap** – insecure builds proceed unnoticed.
|
22
|
-
|
23
|
-
---
|
24
|
-
|
25
|
-
## 3. Target Users
|
26
|
-
|
27
|
-
- Ruby and Rails developers
|
28
|
-
- DevOps engineers managing Ruby apps in production
|
29
|
-
- Security-conscious teams using Ruby for internal tooling
|
30
|
-
|
31
|
-
---
|
32
|
-
|
33
|
-
## 4. Features & Requirements
|
34
|
-
|
35
|
-
### Phase 1 – Core CLI Scanner
|
36
|
-
- Command: `gem_guard scan`
|
37
|
-
- Parse `Gemfile.lock` and detect:
|
38
|
-
- Direct & transitive dependencies
|
39
|
-
- Gem source URLs
|
40
|
-
- Query vulnerability sources:
|
41
|
-
- [OSV.dev](https://osv.dev)
|
42
|
-
- [Ruby Advisory Database](https://github.com/rubysec/ruby-advisory-db)
|
43
|
-
- Output:
|
44
|
-
- Table of vulnerable gems, CVE IDs, severity, fixed versions
|
45
|
-
- Recommended fix commands (e.g., `bundle update <gem>`)
|
46
|
-
|
47
|
-
### Phase 2 – SBOM Generation
|
48
|
-
- Command: `gem_guard sbom`
|
49
|
-
- Output formats:
|
50
|
-
- SPDX JSON
|
51
|
-
- CycloneDX JSON
|
52
|
-
- Include metadata:
|
53
|
-
- Gem name, version, source URL, license, checksum
|
54
|
-
|
55
|
-
### Phase 3 – CI/CD Integration
|
56
|
-
- Exit with non-zero status if vulnerabilities above a severity threshold are found
|
57
|
-
- Optional GitHub Action and GitLab CI template
|
58
|
-
- Config file `.gem_guard.yml` to set:
|
59
|
-
- Allowed severity levels
|
60
|
-
- Ignored CVEs
|
61
|
-
- Output format
|
62
|
-
|
63
|
-
### Phase 4 – Typosquat Detection
|
64
|
-
- Fuzzy matching gem names against known gems in RubyGems API
|
65
|
-
- Flag suspicious dependencies
|
66
|
-
|
67
|
-
### Phase 5 – Auto-Fix Mode
|
68
|
-
- Command: `gem_guard fix`
|
69
|
-
- Automatically updates vulnerable gems within safe version constraints
|
70
|
-
|
71
|
-
---
|
72
|
-
|
73
|
-
## 5. Architecture
|
74
|
-
|
75
|
-
### Modules
|
76
|
-
1. **Parser**
|
77
|
-
- Reads `Gemfile.lock`
|
78
|
-
- Builds dependency graph
|
79
|
-
2. **VulnerabilityFetcher**
|
80
|
-
- Fetches advisories from APIs or local DB
|
81
|
-
3. **Analyzer**
|
82
|
-
- Matches dependencies with advisories
|
83
|
-
- Assesses severity and suggests fixes
|
84
|
-
4. **Reporter**
|
85
|
-
- Formats output (table, JSON, markdown, SBOM)
|
86
|
-
5. **CIAdapter**
|
87
|
-
- Reads config
|
88
|
-
- Sets exit codes for pipelines
|
89
|
-
6. **TyposquatChecker**
|
90
|
-
- Fuzzy matches gem names
|
91
|
-
7. **Updater**
|
92
|
-
- Runs safe updates for vulnerable gems
|
93
|
-
|
94
|
-
---
|
95
|
-
|
96
|
-
## 6. Implementation Stack
|
97
|
-
|
98
|
-
- **Language:** Ruby (≥ 3.0)
|
99
|
-
- **Key Libraries:**
|
100
|
-
- `bundler` – parsing Gemfile.lock
|
101
|
-
- `json` / `oj` – output formatting
|
102
|
-
- `net/http` or `httpx` – API calls
|
103
|
-
- `thor` – CLI interface
|
104
|
-
- `fuzzy_match` – typosquat detection
|
105
|
-
- **Test Framework:** RSpec
|
106
|
-
- **Static Analysis:** RuboCop
|
107
|
-
|
108
|
-
---
|
109
|
-
|
110
|
-
## 7. Development Roadmap
|
111
|
-
|
112
|
-
### Milestone 1 – MVP Scanner
|
113
|
-
- Parse Gemfile.lock
|
114
|
-
- Fetch & match CVEs
|
115
|
-
- CLI with human-readable output
|
116
|
-
- Tests + RuboCop
|
117
|
-
|
118
|
-
### Milestone 2 – SBOM Output
|
119
|
-
- Generate SPDX and CycloneDX JSON
|
120
|
-
- CLI flags for format selection
|
121
|
-
|
122
|
-
### Milestone 3 – CI/CD Integration
|
123
|
-
- Config file support
|
124
|
-
- Exit codes for severity thresholds
|
125
|
-
- GitHub Action template
|
126
|
-
|
127
|
-
### Milestone 4 – Typosquat Detection
|
128
|
-
- Implement fuzzy match against RubyGems API
|
129
|
-
- Add to scan output
|
130
|
-
|
131
|
-
### Milestone 5 – Auto-Fix Mode
|
132
|
-
- Implement safe dependency update logic
|
133
|
-
|
134
|
-
---
|
135
|
-
|
136
|
-
## 8. Distribution & Adoption
|
137
|
-
|
138
|
-
- Publish to RubyGems.org
|
139
|
-
- Create GitHub repo with:
|
140
|
-
- Badges (Gem Version, Build Status, License)
|
141
|
-
- README with quickstart and examples
|
142
|
-
- Security policy
|
143
|
-
- Write blog post on Ruby security gaps
|
144
|
-
- Submit to Ruby Weekly
|
145
|
-
- Post to dev.to and Hacker News for feedback
|
146
|
-
|
147
|
-
---
|
148
|
-
|
149
|
-
## 9. License
|
150
|
-
|
151
|
-
MIT or Apache 2.0 (lean towards MIT for broad adoption)
|
152
|
-
|
153
|
-
---
|
154
|
-
|
155
|
-
## 10. Risks & Mitigation
|
156
|
-
|
157
|
-
- **API rate limits** – cache advisories locally
|
158
|
-
- **False positives** – allow ignore list in config
|
159
|
-
- **Slow scans** – async fetching with caching
|
160
|
-
|
161
|
-
---
|
162
|
-
|
163
|
-
## 11. Success Criteria
|
164
|
-
|
165
|
-
- MVP used in CI by at least 10 open source projects within 3 months
|
166
|
-
- Detects >95% of known vulnerabilities from Ruby Advisory DB
|
167
|
-
- SBOM passes validation in major tools (e.g., CycloneDX CLI)
|
data/test_nokogiri.lock
DELETED