gem-guardian 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e343f954f0d514206e5bcc10d935a094d916ee1456cbb4774a113c909e879dc
4
- data.tar.gz: 2230036e48e13af43b32090d1cbe4b933785b5407fda0f8376bebd41763615fe
3
+ metadata.gz: 75caa51bf7916d1feb83062445a665ca34d89b8e476cf015355b5d86566dbb76
4
+ data.tar.gz: 4520cec08edf53406f26988cc5d48cd5794307fd12d59c4b661ee0e94dfc12db
5
5
  SHA512:
6
- metadata.gz: 36bf27a64fb281d8e50a81a9efdadc5e83ee694e81e79cb46c36799f443568f917934001f10568a1f417740688cabd204aa26154fd791145416b7d25c3d837dc
7
- data.tar.gz: 9e5331025935a3a5d007b2f684e59110cf0bbe79deaacb1e93b976aa06075ea9e52f7199c3ff92b2c90c5317b9da7c5eb03e796724b2966a1f23c37c738426e3
6
+ metadata.gz: 15c736bf8890ca30c0fef05508b14ed85b75bcb660c14843773348fa0c413213f6b79ac2ad39bbdd9edf3cc00c62d3f23667529b0f9b3f1231001b6c3804f020
7
+ data.tar.gz: 5f50fc8de20b9691dc3ea84c9ef2eb542f4b58981552ee237ac1314e4860c7d47779dd078f29a936d632a6342b322237d73546dd139f8e6ddde94adadd8ff8b5
@@ -17,7 +17,10 @@ jobs:
17
17
  strategy:
18
18
  matrix:
19
19
  ruby:
20
- - '3.2.11'
20
+ - '3.2'
21
+ - '3.3'
22
+ - '3.4'
23
+ - '4.0'
21
24
 
22
25
  steps:
23
26
  - uses: actions/checkout@v6
@@ -28,5 +31,23 @@ jobs:
28
31
  with:
29
32
  ruby-version: ${{ matrix.ruby }}
30
33
  bundler-cache: true
31
- - name: Run the default task
32
- run: bundle exec rake
34
+ - name: Run the test suite
35
+ run: COVERAGE=true bundle exec rake test
36
+ - name: Validate RBS signatures
37
+ run: bundle exec rake rbs:validate
38
+
39
+ lint:
40
+ runs-on: ubuntu-latest
41
+ name: RuboCop
42
+
43
+ steps:
44
+ - uses: actions/checkout@v6
45
+ with:
46
+ persist-credentials: false
47
+ - name: Set up Ruby
48
+ uses: ruby/setup-ruby@v1
49
+ with:
50
+ ruby-version: "3.2"
51
+ bundler-cache: true
52
+ - name: Run RuboCop
53
+ run: bundle exec rubocop
@@ -0,0 +1,68 @@
1
+ name: Deploy YARD docs to GitHub Pages
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: read
8
+ pages: write
9
+ id-token: write
10
+
11
+ concurrency:
12
+ group: github-pages
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ build:
17
+ name: Build YARD documentation
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v5
23
+
24
+ - name: Set up Ruby
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: "3.4"
28
+ bundler-cache: true
29
+
30
+ - name: Build YARD documentation
31
+ run: bundle exec yard doc
32
+
33
+ - name: Enforce YARD coverage
34
+ shell: bash
35
+ run: |
36
+ stats="$(bundle exec yard stats --no-progress --list-undoc)"
37
+ echo "$stats"
38
+
39
+ coverage="$(printf '%s\n' "$stats" | ruby -ne 'puts($1) if /([0-9]+(?:\.[0-9]+)?)% documented/ =~ $_')"
40
+
41
+ if [ -z "$coverage" ]; then
42
+ echo "Could not determine YARD documentation coverage."
43
+ exit 1
44
+ fi
45
+
46
+ ruby -e 'coverage = ARGV.fetch(0).to_f; abort("YARD documentation coverage #{coverage}% is below 95%") if coverage < 95.0' "$coverage"
47
+
48
+ - name: Disable Jekyll processing
49
+ run: touch doc/.nojekyll
50
+
51
+ - name: Upload Pages artifact
52
+ uses: actions/upload-pages-artifact@v3
53
+ with:
54
+ path: doc
55
+
56
+ deploy:
57
+ name: Deploy to GitHub Pages
58
+ needs: build
59
+ runs-on: ubuntu-latest
60
+
61
+ environment:
62
+ name: github-pages
63
+ url: ${{ steps.deployment.outputs.page_url }}
64
+
65
+ steps:
66
+ - name: Deploy
67
+ id: deployment
68
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,31 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+
12
+ jobs:
13
+ release:
14
+ name: Release gem to RubyGems.org
15
+ runs-on: ubuntu-latest
16
+ environment:
17
+ name: release
18
+ url: https://rubygems.org/gems/gem-guardian
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ with:
22
+ persist-credentials: false
23
+ - name: Set up Ruby
24
+ uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: "3.4"
27
+ bundler-cache: true
28
+ - name: Run tests
29
+ run: bundle exec rake
30
+ - name: Release gem to RubyGems.org
31
+ uses: rubygems/release-gem@v1
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /tmp/
9
9
  Gemfile.lock
10
10
  *.gem
11
+ /.ignoreme/
data/.rubocop.yml CHANGED
@@ -1,5 +1,17 @@
1
+ plugins:
2
+ - rubocop-minitest
3
+ - rubocop-rake
4
+
1
5
  AllCops:
6
+ NewCops: disable
2
7
  TargetRubyVersion: 3.2
8
+ Exclude:
9
+ - 'coverage/**/*'
10
+ - 'doc/**/*'
11
+ - 'vendor/**/*'
12
+ - 'sig/**/*'
13
+ - 'tmp/**/*'
14
+ - 'test/**/*'
3
15
 
4
16
  Style/StringLiterals:
5
17
  EnforcedStyle: double_quotes
data/.yardopts ADDED
@@ -0,0 +1,7 @@
1
+ --title "gem-guardian API Documentation"
2
+ --protected
3
+ --markup markdown
4
+ --readme README.md
5
+ 'lib/**/*.rb'
6
+ -
7
+ --files CHANGELOG.md LICENSE.txt CODE_OF_CONDUCT.md
data/CHANGELOG.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0
3
+ ## [Unreleased]
4
+
5
+ ## [0.2.0] - 2026-06-12
6
+
7
+ - Add `--json` output for CI-friendly verification reports.
8
+ - Add opt-in Trusted Publishing provenance verification for RubyGems releases.
9
+ - Verify provenance through RubyGems attestations for supported releases.
10
+
11
+ ## [0.1.1] - 2026-06-12
12
+
13
+ - Parse Bundler `CHECKSUMS` entries from `Gemfile.lock`.
14
+ - Audit lockfiles for missing checksum coverage and report fallback verification.
15
+ - Raise test coverage to 95%+ line and branch.
16
+ - Curate `sig/` outputs so `rbs validate` passes cleanly.
17
+ - Add GitHub Actions Ruby matrix for `3.2`, `3.3`, `3.4`, and `4.0`.
18
+ - Run `rbs:validate` in CI.
19
+
20
+ ## [0.1.0] - 2026-06-12
4
21
 
5
22
  - Initial MVP codebase.
6
23
  - Verify explicit gems or all gems in `Gemfile.lock`.
data/Gemfile CHANGED
@@ -4,12 +4,12 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ gem "minitest", "~> 6.0"
7
8
  gem "pry", "~> 0.16.0"
8
9
  gem "rake", "~> 13.4"
9
- gem "minitest", "~> 6.0"
10
10
  gem "rubocop", "~> 1.87"
11
+ gem "rubocop-minitest", "~> 0.39.1"
12
+ gem "rubocop-rake", "~> 0.7.1"
11
13
  gem "simplecov", "~> 0.22.0"
12
14
  gem "steep", "~> 2.0"
13
15
  gem "yard", "~> 0.9.44"
14
- gem "rubocop-minitest", "~> 0.39.1"
15
- gem "rubocop-rake", "~> 0.7.1"
data/README.md CHANGED
@@ -8,25 +8,27 @@
8
8
 
9
9
  Consumer-side integrity verification for Ruby gems.
10
10
 
11
- `gem-guardian` verifies downloaded `.gem` artifacts against the SHA256 checksum reported by RubyGems.org. It is intentionally small: no Bundler monkeypatching, no install hooks, and no custom publishing flow required.
11
+ `gem-guardian` audits Bundler checksum coverage, verifies `.gem` artifacts against RubyGems SHA256 data when needed, and can verify Trusted Publishing provenance for supported releases. It stays intentionally small: no Bundler monkeypatching, no install hooks, and no custom publishing flow required.
12
12
 
13
13
  ## Why
14
14
 
15
- RubyGems.org displays SHA256 checksums for published gem artifacts, and modern Bundler can store checksums in `Gemfile.lock`. But there is still room for a simple consumer-side verification workflow that can be run explicitly in CI or locally.
15
+ RubyGems.org displays SHA256 checksums for published gem artifacts, Bundler 2.6 can store and enforce checksums in `Gemfile.lock`, and RubyGems now exposes attestation data for Trusted Publishing releases. That means the most useful current release is an audit and verification tool that tells you whether your bundle and release metadata are actually protected.
16
16
 
17
- This MVP verifies:
17
+ This 0.2.0 scope is:
18
18
 
19
19
  ```text
20
- Gemfile.lock / explicit gem version
20
+ Gemfile.lock
21
21
 
22
- RubyGems.org expected SHA256
22
+ CHECKSUMS coverage audit
23
23
 
24
- Downloaded .gem artifact
24
+ RubyGems.org checksum comparison when needed
25
25
 
26
- Local SHA256 comparison
26
+ Trusted Publishing provenance verification when available
27
+
28
+ Actionable report for CI or local review
27
29
  ```
28
30
 
29
- This proves that the local artifact matches what RubyGems.org serves. It does **not** yet prove source provenance such as signed tag CI build → published gem.
31
+ This reports whether your lockfile is using Bundler checksum protection, whether any locked gems are missing expected checksum data, and whether RubyGems exposes Trusted Publishing provenance for the gem being verified. It does **not** yet prove source provenance for releases that do not publish attestation data.
30
32
 
31
33
  ## Installation
32
34
 
@@ -34,11 +36,32 @@ From a local checkout:
34
36
 
35
37
  ```bash
36
38
  gem build gem-guardian.gemspec
37
- gem install ./gem-guardian-0.1.0.gem
39
+ gem install ./gem-guardian-0.2.0.gem
38
40
  ```
39
41
 
40
42
  ## Usage
41
43
 
44
+ Build and install the current release from a local checkout:
45
+
46
+ ```bash
47
+ gem build gem-guardian.gemspec
48
+ gem install ./gem-guardian-0.2.0.gem
49
+ gem-guardian version
50
+ ```
51
+
52
+ Show the built-in help:
53
+
54
+ ```bash
55
+ gem-guardian help
56
+ gem-guardian --help
57
+ ```
58
+
59
+ Prepare a locked project for checksum auditing:
60
+
61
+ ```bash
62
+ bundle lock --add-checksums
63
+ ```
64
+
42
65
  Verify all gems in `Gemfile.lock`:
43
66
 
44
67
  ```bash
@@ -64,6 +87,17 @@ Use a non-default lockfile:
64
87
  gem-guardian verify --lockfile path/to/Gemfile.lock
65
88
  ```
66
89
 
90
+ Emit JSON for CI:
91
+
92
+ ```bash
93
+ gem-guardian verify --json
94
+ gem-guardian verify --json --provenance
95
+ ```
96
+
97
+ When you verify a lockfile that already contains Bundler `CHECKSUMS`, `gem-guardian` reports coverage and compares the locked checksum to the downloaded artifact. When a checksum is missing, it falls back to RubyGems.org metadata and marks that verification accordingly.
98
+
99
+ Use `--provenance` to inspect Trusted Publishing metadata when RubyGems exposes it. Unsupported gems are reported, but they do not fail the run unless the provenance data is present and mismatched.
100
+
67
101
  ## Exit codes
68
102
 
69
103
  - `0` — all verified artifacts matched
@@ -72,19 +106,17 @@ gem-guardian verify --lockfile path/to/Gemfile.lock
72
106
 
73
107
  ## MVP constraints
74
108
 
75
- - Uses RubyGems.org as the checksum source of truth.
76
- - Downloads artifacts from RubyGems.org `/downloads/<gem-file>.gem`.
109
+ - Audits `Gemfile.lock` for Bundler `CHECKSUMS` coverage.
110
+ - Uses RubyGems.org as a fallback checksum source when the lockfile is incomplete or an explicit gem is supplied.
111
+ - Downloads artifacts from RubyGems.org `/downloads/<gem-file>.gem` only when verification is needed.
77
112
  - Caches downloaded artifacts under the system temp directory.
78
113
  - Does not integrate into Bundler install hooks.
79
114
  - Does not yet verify Sigstore, SLSA, GitHub Actions provenance, or signed git tags.
80
115
 
81
116
  ## Roadmap
82
117
 
83
- - `gem-guardian lock` to emit or update checksum metadata.
84
- - Support Bundler 2.6 `CHECKSUMS` sections as an offline expected-checksum source.
85
- - Provenance verification for gems published through Trusted Publishing.
86
118
  - GitHub Release checksum/signature discovery.
87
- - Machine-readable JSON output for CI.
119
+ - Signed tag and release attestation checks.
88
120
 
89
121
 
90
122
  ## License
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/gem_tasks"
3
4
  require "rake/testtask"
4
5
 
5
6
  Rake::TestTask.new(:test) do |t|
@@ -29,7 +30,7 @@ namespace :rbs do
29
30
 
30
31
  desc "Validate curated RBS signatures with Steep"
31
32
  task :validate do
32
- sh "bundle exec steep check"
33
+ sh "bundle exec rbs validate sig"
33
34
  end
34
35
 
35
36
  desc "Open diff between curated and generated signatures"
data/gem-guardian.gemspec CHANGED
@@ -9,7 +9,9 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["kenneth.c.demanawa@gmail.com"]
10
10
 
11
11
  spec.summary = "Consumer-side integrity verification for Ruby gems."
12
- spec.description = "Verifies Ruby gem artifacts against RubyGems SHA256 checksums using Gemfile.lock or explicit gem names."
12
+ spec.description = <<~DESC
13
+ Audits Bundler checksum coverage and verifies Ruby gem artifacts against RubyGems SHA256 checksums when needed.
14
+ DESC
13
15
  spec.homepage = "https://github.com/kanutocd/gem-guardian"
14
16
  spec.license = "MIT"
15
17
  spec.required_ruby_version = ">= 3.2"
@@ -5,12 +5,16 @@ require "tmpdir"
5
5
 
6
6
  module Gem
7
7
  module Guardian
8
+ # Stores downloaded gem artifacts in a local cache directory.
8
9
  class ArtifactStore
10
+ # @param client [RubygemsClient] downloader used when the artifact is not cached
11
+ # @param cache_dir [String] directory where downloaded artifacts are stored
9
12
  def initialize(client:, cache_dir: File.join(Dir.tmpdir, "gem-guardian"))
10
13
  @client = client
11
14
  @cache_dir = cache_dir
12
15
  end
13
16
 
17
+ # Returns the local path for +dependency+, downloading it if needed.
14
18
  def path_for(dependency)
15
19
  FileUtils.mkdir_p(@cache_dir)
16
20
  path = File.join(@cache_dir, dependency.gem_filename)
@@ -4,9 +4,11 @@ require "digest"
4
4
 
5
5
  module Gem
6
6
  module Guardian
7
+ # Local checksum helpers.
7
8
  module Checksum
8
9
  module_function
9
10
 
11
+ # Returns the SHA256 hex digest for the file at +path+.
10
12
  def sha256_file(path)
11
13
  Digest::SHA256.file(path).hexdigest
12
14
  end
@@ -1,60 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
5
+ # Namespace for gem-guardian CLI code.
3
6
  module Gem
7
+ # Command-line interface and output helpers.
4
8
  module Guardian
9
+ # Command-line entry point for gem-guardian.
10
+ # rubocop:disable Metrics/ClassLength, Metrics/ParameterLists
5
11
  class CLI
12
+ # Starts the CLI with the provided argv.
6
13
  def self.start(argv)
7
14
  new(argv).run
8
15
  end
9
16
 
10
- def initialize(argv, stdout: $stdout, stderr: $stderr)
17
+ def initialize(argv, stdout: $stdout, stderr: $stderr, verifier_class: Verifier,
18
+ lockfile_parser_class: LockfileParser, provenance_verifier_class: ProvenanceVerifier,
19
+ report_builder_class: ReportBuilder)
11
20
  @argv = argv.dup
12
21
  @stdout = stdout
13
22
  @stderr = stderr
23
+ @verifier_class = verifier_class
24
+ @lockfile_parser_class = lockfile_parser_class
25
+ @provenance_verifier_class = provenance_verifier_class
26
+ @report_builder_class = report_builder_class
27
+ @result_printer = ResultPrinter.new(stdout:)
14
28
  end
15
29
 
30
+ # Dispatches the requested subcommand and returns an exit status.
16
31
  def run
17
- command = @argv.shift
18
- case command
19
- when "verify"
20
- verify
21
- when "version", "--version", "-v"
22
- @stdout.puts VERSION
23
- 0
24
- when "help", "--help", "-h", nil
25
- usage
26
- 0
27
- else
28
- @stderr.puts "Unknown command: #{command}"
29
- usage(@stderr)
30
- 2
31
- end
32
+ dispatch(@argv.shift)
32
33
  end
33
34
 
34
35
  private
35
36
 
36
- def verify
37
- lockfile = option_value("--lockfile") || "Gemfile.lock"
38
- gems = @argv
39
- dependencies = if gems.empty?
40
- LockfileParser.new(lockfile).dependencies
41
- else
42
- gems.map { |spec| parse_gem_spec(spec) }
43
- end
44
-
45
- if dependencies.empty?
46
- @stderr.puts "No gems found to verify."
47
- return 1
37
+ def dispatch(command)
38
+ case command
39
+ when "verify" then verify
40
+ when "version", "--version", "-v" then print_version
41
+ when "help", "--help", "-h", nil then usage
42
+ else
43
+ unknown_command(command)
48
44
  end
45
+ end
49
46
 
50
- results = Verifier.new.verify_all(dependencies)
51
- print_results(results)
52
- results.all?(&:ok?) ? 0 : 1
47
+ # Runs the verify subcommand.
48
+ # rubocop:disable Metrics/MethodLength
49
+ def verify
50
+ json_output = flag?("--json")
51
+ provenance_mode = flag?("--provenance")
52
+ lockfile_data, dependencies, lockfile_path = resolve_dependencies
53
+ return no_dependencies if dependencies.empty?
54
+
55
+ results = verifier_for(lockfile_data).verify_all(dependencies)
56
+ provenance_results = provenance_results_for(results, provenance_mode)
57
+ output_verification(results, lockfile_data, provenance_results, json_output, lockfile_path)
58
+ verification_exit_status(results, lockfile_data, provenance_results)
53
59
  rescue Error => e
54
60
  @stderr.puts e.message
55
61
  1
56
62
  end
63
+ # rubocop:enable Metrics/MethodLength
57
64
 
65
+ # Parses a GEM:VERSION[:PLATFORM] spec string.
58
66
  def parse_gem_spec(spec)
59
67
  name, version, platform = spec.split(":", 3)
60
68
  raise Error, "Expected GEM:VERSION[:PLATFORM], got: #{spec}" if name.to_s.empty? || version.to_s.empty?
@@ -62,6 +70,80 @@ module Gem
62
70
  Dependency.new(name:, version:, platform: platform || "ruby")
63
71
  end
64
72
 
73
+ def resolve_dependencies
74
+ lockfile = option_value("--lockfile") || "Gemfile.lock"
75
+ return [nil, @argv.map { |spec| parse_gem_spec(spec) }, nil] unless @argv.empty?
76
+
77
+ lockfile_data = @lockfile_parser_class.new(lockfile).parse
78
+ [lockfile_data, lockfile_data.dependencies, lockfile]
79
+ end
80
+
81
+ def verifier_for(lockfile_data)
82
+ expected_checksums = lockfile_data&.sha256_checksums || {}
83
+ @verifier_class.new(expected_checksums:)
84
+ end
85
+
86
+ def provenance_verifier_for
87
+ @provenance_verifier_class.new
88
+ end
89
+
90
+ def provenance_results_for(results, provenance_mode)
91
+ return [] unless provenance_mode
92
+
93
+ provenance_verifier_for.verify_all(results)
94
+ end
95
+
96
+ def output_verification(results, lockfile_data, provenance_results, json_output, lockfile_path)
97
+ if json_output
98
+ write_json_report(results, lockfile_data, provenance_results, lockfile_path)
99
+ else
100
+ write_human_report(results, lockfile_data, provenance_results)
101
+ end
102
+ end
103
+
104
+ def write_json_report(results, lockfile_data, provenance_results, lockfile_path)
105
+ @stdout.puts JSON.pretty_generate(
106
+ report_builder.build(results, lockfile_data:, provenance_results:, lockfile_path:)
107
+ )
108
+ end
109
+
110
+ def write_human_report(results, lockfile_data, provenance_results)
111
+ print_verification_report(results, lockfile_data)
112
+ @result_printer.print_provenance_results(provenance_results) unless provenance_results.empty?
113
+ end
114
+
115
+ def print_verification_report(results, lockfile_data)
116
+ lockfile_mode = !lockfile_data.nil?
117
+ @result_printer.print_results(results, lockfile_mode:)
118
+ return unless lockfile_data
119
+
120
+ @result_printer.print_lockfile_coverage(lockfile_data)
121
+ end
122
+
123
+ def verification_exit_status(results, lockfile_data, provenance_results = [])
124
+ all_ok = results.all?(&:ok?)
125
+ all_covered = lockfile_data.nil? || lockfile_data.missing_checksum_dependencies.empty?
126
+ provenance_ok = provenance_results.all? { |result| !%i[mismatch error].include?(result.status) }
127
+ all_ok && all_covered && provenance_ok ? 0 : 1
128
+ end
129
+
130
+ def no_dependencies
131
+ @stderr.puts "No gems found to verify."
132
+ 1
133
+ end
134
+
135
+ def print_version
136
+ @stdout.puts VERSION
137
+ 0
138
+ end
139
+
140
+ def unknown_command(command)
141
+ @stderr.puts "Unknown command: #{command}"
142
+ usage(@stderr)
143
+ 2
144
+ end
145
+
146
+ # Returns and removes an option value from argv.
65
147
  def option_value(name)
66
148
  index = @argv.index(name)
67
149
  return unless index
@@ -73,40 +155,26 @@ module Gem
73
155
  value
74
156
  end
75
157
 
76
- def print_results(results)
77
- results.each do |result|
78
- dependency = result.dependency
79
- label = "#{dependency.name} #{dependency.version} #{dependency.platform}"
80
- case result.status
81
- when :ok
82
- @stdout.puts "PASS #{label}"
83
- @stdout.puts " sha256 #{result.actual_sha256}"
84
- when :mismatch
85
- @stdout.puts "FAIL #{label}"
86
- @stdout.puts " expected #{result.expected_sha256}"
87
- @stdout.puts " actual #{result.actual_sha256}"
88
- else
89
- @stdout.puts "ERROR #{label}"
90
- @stdout.puts " #{result.error.class}: #{result.error.message}"
91
- end
92
- end
93
- end
158
+ # Returns true when +name+ is present and removes it from argv.
159
+ def flag?(name)
160
+ index = @argv.index(name)
161
+ return false unless index
94
162
 
95
- def usage(io = @stdout)
96
- io.puts <<~USAGE
97
- gem-guardian #{VERSION}
163
+ @argv.delete_at(index)
164
+ true
165
+ end
98
166
 
99
- Usage:
100
- gem-guardian verify [--lockfile Gemfile.lock]
101
- gem-guardian verify GEM:VERSION[:PLATFORM] [GEM:VERSION[:PLATFORM] ...]
102
- gem-guardian version
167
+ # Returns the report builder for structured output.
168
+ def report_builder
169
+ @report_builder_class.new(version: VERSION)
170
+ end
103
171
 
104
- Examples:
105
- gem-guardian verify
106
- gem-guardian verify sidekiq:8.0.8
107
- gem-guardian verify nokogiri:1.18.9:x86_64-linux
108
- USAGE
172
+ # Prints usage text.
173
+ def usage(_io = @stdout)
174
+ @result_printer.usage
175
+ 0
109
176
  end
110
177
  end
178
+ # rubocop:enable Metrics/ClassLength, Metrics/ParameterLists
111
179
  end
112
180
  end
@@ -2,7 +2,9 @@
2
2
 
3
3
  module Gem
4
4
  module Guardian
5
+ # A gem dependency identified by name, version, and platform.
5
6
  Dependency = Data.define(:name, :version, :platform) do
7
+ # Returns the canonical .gem filename for this dependency.
6
8
  def gem_filename
7
9
  platform_suffix = platform && platform != "ruby" ? "-#{platform}" : ""
8
10
  "#{name}-#{version}#{platform_suffix}.gem"
@@ -2,9 +2,13 @@
2
2
 
3
3
  module Gem
4
4
  module Guardian
5
+ # Base error type for gem-guardian failures.
5
6
  Error = Class.new(StandardError)
7
+ # Raised when RubyGems does not expose a checksum for a gem version.
6
8
  ChecksumNotFound = Class.new(Error)
9
+ # Raised when downloading or writing a gem artifact fails.
7
10
  ArtifactFetchError = Class.new(Error)
11
+ # Raised when a lockfile cannot be read or parsed.
8
12
  LockfileError = Class.new(Error)
9
13
  end
10
14
  end