gem-guardian 0.2.0 → 0.3.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/release.yml +13 -0
- data/CHANGELOG.md +8 -0
- data/README.md +4 -5
- data/gem-guardian.gemspec +3 -1
- data/lib/gem/guardian/github_client.rb +58 -0
- data/lib/gem/guardian/github_release_verifier.rb +200 -0
- data/lib/gem/guardian/provenance_verifier.rb +26 -9
- data/lib/gem/guardian/report_builder.rb +22 -1
- data/lib/gem/guardian/result_printer.rb +26 -0
- data/lib/gem/guardian/rubygems_client.rb +5 -0
- data/lib/gem/guardian/version.rb +1 -1
- data/lib/gem/guardian.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a18ee9f2f2111d0eca38def30302a7d66b9b6b9b96df06909f883be2978c1bcd
|
|
4
|
+
data.tar.gz: e6a421612c4c50423ef7fe29f74f2bc2503cb624b813befb114bc679940fcd5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3577f45013b8bb9b94905e7f2c081501ef3b863f2913faee08a0604184e5181ed278bd75a57cee9f9d571d00395b26c3f666ab1c126abe251c88cc54f24cb8c3
|
|
7
|
+
data.tar.gz: faa68291c29be60c7b42d7cf1452dc8e5d29222ee8174e22ed7bbb6f913c5405a264555fe16c020e934d23863232aec74ee2befb537a7bb06192ab52d154bffb
|
|
@@ -25,7 +25,20 @@ jobs:
|
|
|
25
25
|
with:
|
|
26
26
|
ruby-version: "3.4"
|
|
27
27
|
bundler-cache: true
|
|
28
|
+
- name: Build gem artifact
|
|
29
|
+
run: gem build gem-guardian.gemspec
|
|
30
|
+
- name: Generate checksum
|
|
31
|
+
run: |
|
|
32
|
+
gem_file=$(ls gem-guardian-*.gem)
|
|
33
|
+
shasum -a 256 "$gem_file" > "$gem_file.sha256"
|
|
28
34
|
- name: Run tests
|
|
29
35
|
run: bundle exec rake
|
|
30
36
|
- name: Release gem to RubyGems.org
|
|
31
37
|
uses: rubygems/release-gem@v1
|
|
38
|
+
- name: Create GitHub release
|
|
39
|
+
uses: softprops/action-gh-release@v2
|
|
40
|
+
with:
|
|
41
|
+
generate_release_notes: true
|
|
42
|
+
files: |
|
|
43
|
+
gem-guardian-*.gem
|
|
44
|
+
gem-guardian-*.gem.sha256
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-06-12
|
|
6
|
+
|
|
7
|
+
- Discover GitHub Release checksum and signature assets.
|
|
8
|
+
- Verify signed Git tags and GitHub release attestations when provenance exposes a GitHub tag.
|
|
9
|
+
- Fall back to version-derived release tags when RubyGems provenance exposes only a commit SHA.
|
|
10
|
+
- Add GitHub release metadata to JSON and human-readable provenance output when available.
|
|
11
|
+
- Package the new GitHub verifier classes into the released gem.
|
|
12
|
+
|
|
5
13
|
## [0.2.0] - 2026-06-12
|
|
6
14
|
|
|
7
15
|
- Add `--json` output for CI-friendly verification reports.
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Consumer-side integrity verification for Ruby gems.
|
|
10
10
|
|
|
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.
|
|
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, including GitHub release checksum/signature discovery and signed-tag attestation checks when the release data exposes them. It stays intentionally small: no Bundler monkeypatching, no install hooks, and no custom publishing flow required.
|
|
12
12
|
|
|
13
13
|
## Why
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ Trusted Publishing provenance verification when available
|
|
|
28
28
|
Actionable report for CI or local review
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
This reports whether your lockfile is using Bundler checksum protection, whether any locked gems are missing expected checksum data,
|
|
31
|
+
This reports whether your lockfile is using Bundler checksum protection, whether any locked gems are missing expected checksum data, whether RubyGems exposes Trusted Publishing provenance for the gem being verified, and whether GitHub release assets and tag attestations are available for the release being inspected.
|
|
32
32
|
|
|
33
33
|
## Installation
|
|
34
34
|
|
|
@@ -111,12 +111,11 @@ Use `--provenance` to inspect Trusted Publishing metadata when RubyGems exposes
|
|
|
111
111
|
- Downloads artifacts from RubyGems.org `/downloads/<gem-file>.gem` only when verification is needed.
|
|
112
112
|
- Caches downloaded artifacts under the system temp directory.
|
|
113
113
|
- Does not integrate into Bundler install hooks.
|
|
114
|
-
-
|
|
114
|
+
- GitHub Release checksum/signature discovery and signed tag/release attestation checks are supported when the release metadata is available.
|
|
115
115
|
|
|
116
116
|
## Roadmap
|
|
117
117
|
|
|
118
|
-
- GitHub
|
|
119
|
-
- Signed tag and release attestation checks.
|
|
118
|
+
- Expand release provenance checks to additional publishing workflows beyond GitHub release provenance.
|
|
120
119
|
|
|
121
120
|
|
|
122
121
|
## License
|
data/gem-guardian.gemspec
CHANGED
|
@@ -24,7 +24,9 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
spec.files = Dir.chdir(__dir__) do
|
|
27
|
-
`git ls-files -z`.split("\x0")
|
|
27
|
+
tracked_files = `git ls-files -z`.split("\x0")
|
|
28
|
+
source_files = Dir["lib/**/*", "exe/*", "README.md", "LICENSE.txt", "CHANGELOG.md"]
|
|
29
|
+
(tracked_files + source_files).uniq.reject do |f|
|
|
28
30
|
f.match(%r{\A(?:test|spec|features)/})
|
|
29
31
|
end
|
|
30
32
|
rescue StandardError
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Gem
|
|
8
|
+
module Guardian
|
|
9
|
+
# Reads GitHub release and tag metadata for provenance checks.
|
|
10
|
+
class GitHubClient
|
|
11
|
+
# Default GitHub API endpoint used by the client.
|
|
12
|
+
DEFAULT_HOST = "https://api.github.com"
|
|
13
|
+
|
|
14
|
+
def initialize(host: DEFAULT_HOST, http: Net::HTTP)
|
|
15
|
+
@host = host.delete_suffix("/")
|
|
16
|
+
@http = http
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns the release payload for +repository+ and +tag+.
|
|
20
|
+
def release(repository, tag)
|
|
21
|
+
fetch_json("/repos/#{repository}/releases/tags/#{tag}")
|
|
22
|
+
rescue StandardError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the tag verification payload for +repository+ and +tag+.
|
|
27
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
28
|
+
def tag_verification(repository, tag)
|
|
29
|
+
ref = fetch_json("/repos/#{repository}/git/ref/tags/#{tag}")
|
|
30
|
+
return unless ref.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
object = ref["object"]
|
|
33
|
+
return unless object.is_a?(Hash)
|
|
34
|
+
return object["verification"] if object["type"] == "tag" && object["verification"].is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
commit = fetch_json("/repos/#{repository}/commits/#{object["sha"]}")
|
|
37
|
+
commit.is_a?(Hash) ? commit["commit"]&.fetch("verification", nil) : nil
|
|
38
|
+
rescue StandardError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def fetch_json(path)
|
|
46
|
+
JSON.parse(get(path))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get(path)
|
|
50
|
+
uri = URI("#{@host}#{path}")
|
|
51
|
+
response = @http.get_response(uri)
|
|
52
|
+
return response.body if response.is_a?(Net::HTTPSuccess)
|
|
53
|
+
|
|
54
|
+
raise Error, "GET #{uri} failed with #{response.code} #{response.message}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gem
|
|
4
|
+
module Guardian
|
|
5
|
+
# Result object for GitHub release provenance checks.
|
|
6
|
+
GitHubReleaseResult = Data.define(
|
|
7
|
+
:dependency, :status, :repository, :tag, :checksum_assets, :signature_assets, :signed_tag,
|
|
8
|
+
:signed_tag_reason, :release_attestation, :release_url, :error
|
|
9
|
+
) do
|
|
10
|
+
# Returns true when the GitHub release checks succeeded.
|
|
11
|
+
def verified?
|
|
12
|
+
status == :verified
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Verifies GitHub release checksum, signature, and attestation metadata.
|
|
17
|
+
# rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/ParameterLists, Metrics/CyclomaticComplexity
|
|
18
|
+
class GitHubReleaseVerifier
|
|
19
|
+
def initialize(client: GitHubClient.new)
|
|
20
|
+
@client = client
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Verifies GitHub release metadata for +provenance+.
|
|
24
|
+
# rubocop:disable Metrics/AbcSize
|
|
25
|
+
def verify(provenance)
|
|
26
|
+
repository = github_repository(provenance.repository)
|
|
27
|
+
tag_candidates = github_tag_candidates(provenance)
|
|
28
|
+
return unsupported_result(provenance, repository, tag_candidates.first) unless repository && tag_candidates.any?
|
|
29
|
+
|
|
30
|
+
release, tag = release_for(repository, tag_candidates)
|
|
31
|
+
return unsupported_result(provenance, repository, tag) unless release
|
|
32
|
+
|
|
33
|
+
checksum_assets = discovered_assets(release, checksum_asset_name?)
|
|
34
|
+
signature_assets = discovered_assets(release, signature_asset_name?)
|
|
35
|
+
tag_verification = @client.tag_verification(repository, tag)
|
|
36
|
+
build_release_result(provenance, repository, tag, checksum_assets, signature_assets, tag_verification, release)
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
error_result(provenance, repository, tag, e)
|
|
39
|
+
end
|
|
40
|
+
# rubocop:enable Metrics/AbcSize
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def release_for(repository, tag_candidates)
|
|
45
|
+
tag_candidates.each do |candidate|
|
|
46
|
+
release = @client.release(repository, candidate)
|
|
47
|
+
return [release, candidate] if release
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
[nil, tag_candidates.first]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_release_result(provenance, repository, tag, checksum_assets, signature_assets, tag_verification,
|
|
54
|
+
release)
|
|
55
|
+
signed_tag = signed_tag?(tag_verification)
|
|
56
|
+
attestation = release_attestation(release)
|
|
57
|
+
status = release_status(signed_tag, attestation, tag_verification)
|
|
58
|
+
GitHubReleaseResult.new(
|
|
59
|
+
dependency: provenance.dependency,
|
|
60
|
+
status: status,
|
|
61
|
+
repository:,
|
|
62
|
+
tag:,
|
|
63
|
+
checksum_assets:,
|
|
64
|
+
signature_assets:,
|
|
65
|
+
signed_tag:,
|
|
66
|
+
signed_tag_reason: verification_reason(tag_verification),
|
|
67
|
+
release_attestation: attestation,
|
|
68
|
+
release_url: release["html_url"],
|
|
69
|
+
error: nil
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def unsupported_result(provenance, repository, tag)
|
|
74
|
+
GitHubReleaseResult.new(
|
|
75
|
+
dependency: provenance.dependency,
|
|
76
|
+
status: :unsupported,
|
|
77
|
+
repository: repository,
|
|
78
|
+
tag: tag,
|
|
79
|
+
checksum_assets: [],
|
|
80
|
+
signature_assets: [],
|
|
81
|
+
signed_tag: nil,
|
|
82
|
+
signed_tag_reason: nil,
|
|
83
|
+
release_attestation: nil,
|
|
84
|
+
release_url: nil,
|
|
85
|
+
error: nil
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def error_result(provenance, repository, tag, error)
|
|
90
|
+
GitHubReleaseResult.new(
|
|
91
|
+
dependency: provenance.dependency,
|
|
92
|
+
status: :error,
|
|
93
|
+
repository: repository,
|
|
94
|
+
tag: tag,
|
|
95
|
+
checksum_assets: [],
|
|
96
|
+
signature_assets: [],
|
|
97
|
+
signed_tag: nil,
|
|
98
|
+
signed_tag_reason: nil,
|
|
99
|
+
release_attestation: nil,
|
|
100
|
+
release_url: nil,
|
|
101
|
+
error: error
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def github_repository(repository)
|
|
106
|
+
return if repository.nil?
|
|
107
|
+
|
|
108
|
+
value = repository.to_s
|
|
109
|
+
return unless value.match?(%r{\Ahttps://github\.com/[^/]+/[^/]+\z}) || value.match?(%r{\A[^/]+/[^/]+\z})
|
|
110
|
+
|
|
111
|
+
value.delete_prefix("https://github.com/")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def github_tag(provenance)
|
|
115
|
+
github_tag_candidates(provenance).first
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def github_tag_candidates(provenance)
|
|
119
|
+
[
|
|
120
|
+
tag_from_ref(provenance.ref),
|
|
121
|
+
tag_from_subject(provenance.subject),
|
|
122
|
+
tag_from_version(provenance.dependency.version),
|
|
123
|
+
provenance.dependency.version.to_s
|
|
124
|
+
].compact.uniq
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def tag_from_ref(ref)
|
|
128
|
+
ref = ref.to_s
|
|
129
|
+
return unless ref.start_with?("refs/tags/")
|
|
130
|
+
|
|
131
|
+
ref.delete_prefix("refs/tags/")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def tag_from_subject(subject)
|
|
135
|
+
match = subject.to_s.match(%r{:ref:refs/tags/([^:]+)\z})
|
|
136
|
+
match && match[1]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def tag_from_version(version)
|
|
140
|
+
version = version.to_s
|
|
141
|
+
return if version.empty?
|
|
142
|
+
|
|
143
|
+
"v#{version}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def discovered_assets(release, matcher)
|
|
147
|
+
Array(release["assets"]).filter_map do |asset|
|
|
148
|
+
name = asset.is_a?(Hash) ? asset["name"].to_s : asset.to_s
|
|
149
|
+
name if matcher.call(name)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def checksum_asset_name?
|
|
154
|
+
@checksum_asset_name ||= lambda do |name|
|
|
155
|
+
name.match?(/\.(?:sha256|sha256sum|checksum|checksums|sha)\z/i) ||
|
|
156
|
+
name.match?(/\ASHA256SUMS(?:\.txt)?\z/i) ||
|
|
157
|
+
name.match?(/\Achecksums(?:\.txt)?\z/i)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def signature_asset_name?
|
|
162
|
+
@signature_asset_name ||= lambda do |name|
|
|
163
|
+
name.match?(/\.(?:sig|asc)\z/i) || name.match?(/\.(?:bundle|intoto\.jsonl)\z/i)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def signed_tag?(verification)
|
|
168
|
+
return nil unless verification.is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
verification["verified"] == true || verification["verified"].to_s.casecmp("true").zero?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def verification_reason(verification)
|
|
174
|
+
return unless verification.is_a?(Hash)
|
|
175
|
+
|
|
176
|
+
verification["reason"]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def release_attestation(release)
|
|
180
|
+
value = release["attestations"] || release["attestation"] || release["provenance"] ||
|
|
181
|
+
release["artifact_attestations"]
|
|
182
|
+
return nil if value.nil?
|
|
183
|
+
|
|
184
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass) ? value : true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def release_status(signed_tag, attestation, verification)
|
|
188
|
+
return :error if verification.is_a?(Hash) &&
|
|
189
|
+
verification["verified"] == true &&
|
|
190
|
+
verification["reason"] == "invalid"
|
|
191
|
+
return :verified if signed_tag == true && attestation == true
|
|
192
|
+
return :mismatch if signed_tag == false
|
|
193
|
+
return :mismatch if attestation == false
|
|
194
|
+
|
|
195
|
+
:unsupported
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/ParameterLists, Metrics/CyclomaticComplexity
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -5,7 +5,7 @@ module Gem
|
|
|
5
5
|
# Result object for a provenance verification attempt.
|
|
6
6
|
ProvenanceResult = Data.define(
|
|
7
7
|
:dependency, :status, :trusted_publishing, :repository, :ref, :workflow, :issuer, :subject,
|
|
8
|
-
:expected_sha256, :actual_sha256, :error, :attestation_url
|
|
8
|
+
:expected_sha256, :actual_sha256, :error, :attestation_url, :github_release
|
|
9
9
|
) do
|
|
10
10
|
# Returns true when provenance verification succeeded.
|
|
11
11
|
def verified?
|
|
@@ -15,8 +15,9 @@ module Gem
|
|
|
15
15
|
|
|
16
16
|
# Verifies RubyGems Trusted Publishing provenance metadata.
|
|
17
17
|
class ProvenanceVerifier
|
|
18
|
-
def initialize(client: RubygemsClient.new)
|
|
18
|
+
def initialize(client: RubygemsClient.new, github_release_verifier: GitHubReleaseVerifier.new)
|
|
19
19
|
@client = client
|
|
20
|
+
@github_release_verifier = github_release_verifier
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# Verifies Trusted Publishing provenance for +dependency+.
|
|
@@ -38,20 +39,21 @@ module Gem
|
|
|
38
39
|
|
|
39
40
|
# rubocop:disable Metrics/MethodLength
|
|
40
41
|
def build_result(dependency, provenance, artifact_sha256)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
))
|
|
42
|
+
github_release = github_release_result(provenance)
|
|
43
|
+
status = combine_status(provenance_status(provenance, artifact_sha256), github_release&.status)
|
|
44
|
+
ProvenanceResult.new(**result_attributes(dependency, provenance, artifact_sha256, status, github_release))
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def unsupported_result(dependency)
|
|
47
|
-
ProvenanceResult.new(**result_attributes(dependency, nil, nil, :unsupported))
|
|
48
|
+
ProvenanceResult.new(**result_attributes(dependency, nil, nil, :unsupported, nil))
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def error_result(dependency, artifact_sha256, error)
|
|
51
|
-
ProvenanceResult.new(**result_attributes(dependency, nil, artifact_sha256, :error, error))
|
|
52
|
+
ProvenanceResult.new(**result_attributes(dependency, nil, artifact_sha256, :error, nil, error))
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
# rubocop:disable Metrics/ParameterLists
|
|
56
|
+
def result_attributes(dependency, provenance, artifact_sha256, status, github_release = nil, error = nil)
|
|
55
57
|
{
|
|
56
58
|
dependency:,
|
|
57
59
|
status:,
|
|
@@ -64,9 +66,11 @@ module Gem
|
|
|
64
66
|
expected_sha256: provenance&.sha256,
|
|
65
67
|
actual_sha256: artifact_sha256,
|
|
66
68
|
error:,
|
|
67
|
-
attestation_url: provenance&.attestation_url
|
|
69
|
+
attestation_url: provenance&.attestation_url,
|
|
70
|
+
github_release:
|
|
68
71
|
}
|
|
69
72
|
end
|
|
73
|
+
# rubocop:enable Metrics/ParameterLists
|
|
70
74
|
# rubocop:enable Metrics/MethodLength
|
|
71
75
|
|
|
72
76
|
def provenance_status(provenance, artifact_sha256)
|
|
@@ -76,6 +80,19 @@ module Gem
|
|
|
76
80
|
secure_compare(provenance.sha256, artifact_sha256) ? :verified : :mismatch
|
|
77
81
|
end
|
|
78
82
|
|
|
83
|
+
def github_release_result(provenance)
|
|
84
|
+
@github_release_verifier.verify(provenance)
|
|
85
|
+
rescue StandardError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def combine_status(provenance_status, github_release_status)
|
|
90
|
+
return github_release_status if %i[mismatch error].include?(github_release_status)
|
|
91
|
+
return provenance_status if github_release_status.nil? || github_release_status == :unsupported
|
|
92
|
+
|
|
93
|
+
provenance_status == :unsupported ? github_release_status : provenance_status
|
|
94
|
+
end
|
|
95
|
+
|
|
79
96
|
def secure_compare(left, right)
|
|
80
97
|
left = left.to_s
|
|
81
98
|
right = right.to_s
|
|
@@ -55,7 +55,8 @@ module Gem
|
|
|
55
55
|
|
|
56
56
|
def provenance_hash(result)
|
|
57
57
|
provenance_fields(result).merge(
|
|
58
|
-
error: error_hash(result.error)
|
|
58
|
+
error: error_hash(result.error),
|
|
59
|
+
github_release: github_release_hash(result.github_release)
|
|
59
60
|
)
|
|
60
61
|
end
|
|
61
62
|
|
|
@@ -77,6 +78,26 @@ module Gem
|
|
|
77
78
|
end
|
|
78
79
|
# rubocop:enable Metrics/MethodLength
|
|
79
80
|
|
|
81
|
+
# Returns the GitHub release details for a provenance result.
|
|
82
|
+
# rubocop:disable Metrics/MethodLength
|
|
83
|
+
def github_release_hash(result)
|
|
84
|
+
return nil unless result
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
status: result.status,
|
|
88
|
+
repository: result.repository,
|
|
89
|
+
tag: result.tag,
|
|
90
|
+
checksum_assets: result.checksum_assets,
|
|
91
|
+
signature_assets: result.signature_assets,
|
|
92
|
+
signed_tag: result.signed_tag,
|
|
93
|
+
signed_tag_reason: result.signed_tag_reason,
|
|
94
|
+
release_attestation: result.release_attestation,
|
|
95
|
+
release_url: result.release_url,
|
|
96
|
+
error: error_hash(result.error)
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
# rubocop:enable Metrics/MethodLength
|
|
100
|
+
|
|
80
101
|
# Returns the checksum payload for a verification result.
|
|
81
102
|
def checksum_hash(result)
|
|
82
103
|
{
|
|
@@ -83,6 +83,7 @@ module Gem
|
|
|
83
83
|
provenance_fields(result).each do |label_name, value|
|
|
84
84
|
@stdout.puts format_provenance_field(label_name, value) if value
|
|
85
85
|
end
|
|
86
|
+
print_github_release_result(result.github_release) if result.github_release
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
# Prints a provenance checksum mismatch.
|
|
@@ -140,6 +141,31 @@ module Gem
|
|
|
140
141
|
]
|
|
141
142
|
end
|
|
142
143
|
|
|
144
|
+
# Returns the GitHub release fields to render for a provenance result.
|
|
145
|
+
# rubocop:disable Metrics/MethodLength
|
|
146
|
+
def github_release_fields(result)
|
|
147
|
+
[
|
|
148
|
+
["github release", result.status],
|
|
149
|
+
["release repo", result.repository],
|
|
150
|
+
["release tag", result.tag],
|
|
151
|
+
["checksum assets", result.checksum_assets.join(", ")],
|
|
152
|
+
["signature assets", result.signature_assets.join(", ")],
|
|
153
|
+
["signed tag", result.signed_tag],
|
|
154
|
+
["tag reason", result.signed_tag_reason],
|
|
155
|
+
["attestation", result.release_attestation],
|
|
156
|
+
["release url", result.release_url]
|
|
157
|
+
]
|
|
158
|
+
end
|
|
159
|
+
# rubocop:enable Metrics/MethodLength
|
|
160
|
+
|
|
161
|
+
# Prints a GitHub release provenance result.
|
|
162
|
+
def print_github_release_result(result)
|
|
163
|
+
@stdout.puts "GITHUB RELEASE #{result.status.to_s.upcase}"
|
|
164
|
+
github_release_fields(result).each do |label_name, value|
|
|
165
|
+
@stdout.puts format_provenance_field(label_name, value) if value
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
143
169
|
# Formats one provenance field line.
|
|
144
170
|
def format_provenance_field(label, value)
|
|
145
171
|
format("%<label>11s %<value>s", label:, value:)
|
|
@@ -15,10 +15,15 @@ module Gem
|
|
|
15
15
|
:trusted_publishing, :repository, :ref, :workflow, :issuer, :subject, :sha256, :attestation_url
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
+
# Matches the `Source Commit` field on the RubyGems provenance page.
|
|
18
19
|
SOURCE_COMMIT_PATTERN = %r{Source Commit\s+([A-Za-z0-9._/-]+@[A-Za-z0-9._-]+)}i
|
|
20
|
+
# Matches the `Build File` field on the RubyGems provenance page.
|
|
19
21
|
BUILD_FILE_PATTERN = /Build File\s+([^\s]+)/i
|
|
22
|
+
# Matches the transparency log URL shown on the RubyGems provenance page.
|
|
20
23
|
LOG_ENTRY_PATTERN = %r{transparency log entry\s*(https?://[^\s]+)}i
|
|
24
|
+
# Matches the SHA256 checksum shown on the RubyGems provenance page.
|
|
21
25
|
SHA256_PATTERN = /SHA 256 checksum\s*([a-f0-9]{64})/i
|
|
26
|
+
# Matches the provenance workflow label shown on the RubyGems provenance page.
|
|
22
27
|
WORKFLOW_PATTERN = /
|
|
23
28
|
Built and signed on\s+
|
|
24
29
|
([A-Za-z0-9 ._-]+?)
|
data/lib/gem/guardian/version.rb
CHANGED
data/lib/gem/guardian.rb
CHANGED
|
@@ -10,6 +10,8 @@ require_relative "guardian/checksum"
|
|
|
10
10
|
require_relative "guardian/dependency"
|
|
11
11
|
require_relative "guardian/lockfile_parser"
|
|
12
12
|
require_relative "guardian/rubygems_client"
|
|
13
|
+
require_relative "guardian/github_client"
|
|
14
|
+
require_relative "guardian/github_release_verifier"
|
|
13
15
|
require_relative "guardian/artifact_store"
|
|
14
16
|
require_relative "guardian/verifier"
|
|
15
17
|
require_relative "guardian/provenance_verifier"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gem-guardian
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kenneth Demanawa
|
|
@@ -43,6 +43,8 @@ files:
|
|
|
43
43
|
- lib/gem/guardian/cli.rb
|
|
44
44
|
- lib/gem/guardian/dependency.rb
|
|
45
45
|
- lib/gem/guardian/error.rb
|
|
46
|
+
- lib/gem/guardian/github_client.rb
|
|
47
|
+
- lib/gem/guardian/github_release_verifier.rb
|
|
46
48
|
- lib/gem/guardian/lockfile_parser.rb
|
|
47
49
|
- lib/gem/guardian/provenance_verifier.rb
|
|
48
50
|
- lib/gem/guardian/report_builder.rb
|