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 +4 -4
- data/.github/workflows/main.yml +24 -3
- data/.github/workflows/pages.yml +68 -0
- data/.github/workflows/release.yml +31 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +12 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +18 -1
- data/Gemfile +3 -3
- data/README.md +47 -15
- data/Rakefile +2 -1
- data/gem-guardian.gemspec +3 -1
- data/lib/gem/guardian/artifact_store.rb +4 -0
- data/lib/gem/guardian/checksum.rb +2 -0
- data/lib/gem/guardian/cli.rb +129 -61
- data/lib/gem/guardian/dependency.rb +2 -0
- data/lib/gem/guardian/error.rb +4 -0
- data/lib/gem/guardian/lockfile_parser.rb +95 -13
- data/lib/gem/guardian/provenance_verifier.rb +88 -0
- data/lib/gem/guardian/report_builder.rb +99 -0
- data/lib/gem/guardian/result_printer.rb +150 -0
- data/lib/gem/guardian/rubygems_client.rb +270 -6
- data/lib/gem/guardian/verifier.rb +43 -20
- data/lib/gem/guardian/version.rb +2 -1
- data/lib/gem/guardian.rb +7 -0
- data/sig/gem/guardian/artifact_store.rbs +9 -0
- data/sig/gem/guardian/checksum.rbs +7 -0
- data/sig/gem/guardian/cli.rbs +31 -0
- data/sig/gem/guardian/dependency.rbs +13 -0
- data/sig/gem/guardian/error.rbs +15 -0
- data/sig/gem/guardian/lockfile_parser.rbs +36 -0
- data/sig/gem/guardian/rubygems_client.rbs +21 -0
- data/sig/gem/guardian/verifier.rbs +19 -0
- data/sig/gem/guardian/version.rbs +5 -0
- data/sig/gem/guardian.rbs +4 -0
- metadata +13 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75caa51bf7916d1feb83062445a665ca34d89b8e476cf015355b5d86566dbb76
|
|
4
|
+
data.tar.gz: 4520cec08edf53406f26988cc5d48cd5794307fd12d59c4b661ee0e94dfc12db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15c736bf8890ca30c0fef05508b14ed85b75bcb660c14843773348fa0c413213f6b79ac2ad39bbdd9edf3cc00c62d3f23667529b0f9b3f1231001b6c3804f020
|
|
7
|
+
data.tar.gz: 5f50fc8de20b9691dc3ea84c9ef2eb542f4b58981552ee237ac1314e4860c7d47779dd078f29a936d632a6342b322237d73546dd139f8e6ddde94adadd8ff8b5
|
data/.github/workflows/main.yml
CHANGED
|
@@ -17,7 +17,10 @@ jobs:
|
|
|
17
17
|
strategy:
|
|
18
18
|
matrix:
|
|
19
19
|
ruby:
|
|
20
|
-
- '3.2
|
|
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
|
|
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
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
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|
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,
|
|
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
|
|
17
|
+
This 0.2.0 scope is:
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
|
-
Gemfile.lock
|
|
20
|
+
Gemfile.lock
|
|
21
21
|
↓
|
|
22
|
-
|
|
22
|
+
CHECKSUMS coverage audit
|
|
23
23
|
↓
|
|
24
|
-
|
|
24
|
+
RubyGems.org checksum comparison when needed
|
|
25
25
|
↓
|
|
26
|
-
|
|
26
|
+
Trusted Publishing provenance verification when available
|
|
27
|
+
↓
|
|
28
|
+
Actionable report for CI or local review
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
This
|
|
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.
|
|
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
|
-
-
|
|
76
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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 =
|
|
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)
|
data/lib/gem/guardian/cli.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
163
|
+
@argv.delete_at(index)
|
|
164
|
+
true
|
|
165
|
+
end
|
|
98
166
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
167
|
+
# Returns the report builder for structured output.
|
|
168
|
+
def report_builder
|
|
169
|
+
@report_builder_class.new(version: VERSION)
|
|
170
|
+
end
|
|
103
171
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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"
|
data/lib/gem/guardian/error.rb
CHANGED
|
@@ -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
|