gem_guard 1.1.2 → 1.2.2

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: 2870725c07a4b4c39c53e01c031663a934db6f9e457ad75f509d2ee6e3e35684
4
- data.tar.gz: 6f630c2ef6939a0c5de8c3b8982781a907065b8c1a3901483f1101910387e244
3
+ metadata.gz: c1d1776e2037eecfee3695b1e520c68812be38a83926d7187f5e3387b0427da9
4
+ data.tar.gz: 0f2b8ea750a6994523425a6b533c3027826e2ee44b2e1ec4a02e4990d6ef9eb3
5
5
  SHA512:
6
- metadata.gz: 6b7c598cd6789dfdf5d3b90c82fc9f2ade69c730418f1d2a9f6ba6c22555fad01b7e4bbd438bbb03d6116f429176a407e9b0f13221c47fd2cd0e9e2175563e98
7
- data.tar.gz: 822e03ed7fa56e17d170abf92db8677fb27f92ff04d0f52ecdc1793c73db9aab3052e37e6cbcb28b7a217f3a30a7fb062a3860169f908067b2c3672964d9f977
6
+ metadata.gz: c95027418ff1983569da3918c4413f764e0e2ffc19e4b6048fc1c8a0540aa4b197339225f1740188105f42f0598df572a70aae0222c3f8b979a25041ef3d008a
7
+ data.tar.gz: 37d6a7539128a0fd30d3025512fe1129a7a1c41a465e19939abe80f84581ef819ac4206f54d2855db3c5e68919449adf39b2fd306d1827ff87c7f3f91d1ec531
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 DHH-style Ruby: pragmatic, intention-revealing, minimal abstractions
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 send an email to **security@wilburhimself.com**. All security vulnerabilities will be promptly addressed.
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
- - **Email**: security@wilburhimself.com
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 security-related questions or concerns, contact:
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/gem_guard.gemspec CHANGED
@@ -15,6 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
16
  spec.metadata["source_code_uri"] = "https://github.com/wilburhimself/gem_guard"
17
17
  spec.metadata["changelog_uri"] = "https://github.com/wilburhimself/gem_guard/blob/main/CHANGELOG.md"
18
+ spec.metadata["rubygems_mfa_required"] = "true"
18
19
 
19
20
  spec.files = Dir.chdir(__dir__) do
20
21
  `git ls-files -z`.split("\x0").reject do |f|
@@ -28,8 +29,11 @@ Gem::Specification.new do |spec|
28
29
 
29
30
  spec.add_dependency "thor", "~> 1.0"
30
31
  spec.add_dependency "json", "~> 2.0"
32
+ spec.add_dependency "tty-prompt", "~> 0.23"
31
33
 
32
34
  spec.add_development_dependency "bundler", ">= 2.0"
33
35
  spec.add_development_dependency "rspec", "~> 3.0"
34
- spec.add_development_dependency "standard", "~> 1.3"
36
+ spec.add_development_dependency "standard", "~> 1.39"
37
+ spec.add_development_dependency "rake", "~> 13.0"
38
+ spec.add_development_dependency "simplecov", "~> 0.22"
35
39
  end
@@ -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
- if interactive && !confirm_fixes(fixes)
36
- return {status: :cancelled, message: "Fix operation cancelled by user."}
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
- applied_fixes = apply_fixes(fixes)
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
- puts "\n🔧 Planned Fixes:"
96
- puts "=" * 50
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
- FileUtils.cp(@lockfile_path, backup_path)
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.each do |fix|
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
- exit 1
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 1
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 Run Results:"
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]}: #{fix[:current_version]} → #{fix[:target_version]}"
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
@@ -3,7 +3,18 @@ require "bundler"
3
3
  module GemGuard
4
4
  class Parser
5
5
  def parse(lockfile_path)
6
- lockfile = Bundler::LockfileParser.new(File.read(lockfile_path))
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
@@ -1,3 +1,3 @@
1
1
  module GemGuard
2
- VERSION = "1.1.2"
2
+ VERSION = "1.2.2"
3
3
  end
data/lib/gem_guard.rb CHANGED
@@ -11,4 +11,8 @@ require_relative "gem_guard/auto_fixer"
11
11
 
12
12
  module GemGuard
13
13
  class Error < StandardError; end
14
+
15
+ class InvalidLockfileError < Error; end
16
+
17
+ class FileError < Error; end
14
18
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gem_guard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wilbur Suero
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-08-10 00:00:00.000000000 Z
10
+ date: 2025-08-18 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: thor
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - "~>"
39
38
  - !ruby/object:Gem::Version
40
39
  version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: tty-prompt
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.23'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.23'
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: bundler
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +85,42 @@ dependencies:
72
85
  requirements:
73
86
  - - "~>"
74
87
  - !ruby/object:Gem::Version
75
- version: '1.3'
88
+ version: '1.39'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.39'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '13.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.22'
76
117
  type: :development
77
118
  prerelease: false
78
119
  version_requirements: !ruby/object:Gem::Requirement
79
120
  requirements:
80
121
  - - "~>"
81
122
  - !ruby/object:Gem::Version
82
- version: '1.3'
123
+ version: '0.22'
83
124
  description: A comprehensive tool to detect, report, and remediate dependency-related
84
125
  security risks in Ruby projects. Includes CVE scanning, SBOM generation, and CI/CD
85
126
  integration.
@@ -108,12 +149,9 @@ files:
108
149
  - lib/gem_guard/typosquat_checker.rb
109
150
  - lib/gem_guard/version.rb
110
151
  - lib/gem_guard/vulnerability_fetcher.rb
111
- - plan.md
112
152
  - templates/circleci-config.yml
113
153
  - templates/github-actions.yml
114
154
  - templates/gitlab-ci.yml
115
- - test_nokogiri.lock
116
- - test_nokogiri.lock.backup.20250810_002252
117
155
  homepage: https://github.com/wilburhimself/gem_guard
118
156
  licenses:
119
157
  - MIT
@@ -121,7 +159,7 @@ metadata:
121
159
  homepage_uri: https://github.com/wilburhimself/gem_guard
122
160
  source_code_uri: https://github.com/wilburhimself/gem_guard
123
161
  changelog_uri: https://github.com/wilburhimself/gem_guard/blob/main/CHANGELOG.md
124
- post_install_message:
162
+ rubygems_mfa_required: 'true'
125
163
  rdoc_options: []
126
164
  require_paths:
127
165
  - lib
@@ -136,8 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
174
  - !ruby/object:Gem::Version
137
175
  version: '0'
138
176
  requirements: []
139
- rubygems_version: 3.5.22
140
- signing_key:
177
+ rubygems_version: 3.6.2
141
178
  specification_version: 4
142
179
  summary: Supply chain security and vulnerability management for Ruby gems
143
180
  test_files: []
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
@@ -1,13 +0,0 @@
1
- GEM
2
- remote: https://rubygems.org/
3
- specs:
4
- nokogiri (1.18.8)
5
-
6
- PLATFORMS
7
- ruby
8
-
9
- DEPENDENCIES
10
- nokogiri
11
-
12
- BUNDLED WITH
13
- 2.4.10
@@ -1,13 +0,0 @@
1
- GEM
2
- remote: https://rubygems.org/
3
- specs:
4
- nokogiri (1.18.8)
5
-
6
- PLATFORMS
7
- ruby
8
-
9
- DEPENDENCIES
10
- nokogiri
11
-
12
- BUNDLED WITH
13
- 2.4.10