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
|
@@ -2,34 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
module Gem
|
|
4
4
|
module Guardian
|
|
5
|
+
# Parses Gemfile.lock and exposes dependencies and checksum data.
|
|
5
6
|
class LockfileParser
|
|
7
|
+
# Matches dependency lines in the specs section.
|
|
6
8
|
GEM_LINE = /^ {4}([A-Za-z0-9_.-]+) \(([^)]+)\)/
|
|
9
|
+
# Matches checksum lines in the CHECKSUMS section.
|
|
10
|
+
CHECKSUM_LINE = /^ {2}([A-Za-z0-9_.-]+) \(([^)]+)\) (.+)$/
|
|
11
|
+
# Parsed lockfile data for the verify command.
|
|
12
|
+
LockfileData = Data.define(:dependencies, :checksums, :checksums_section_present) do
|
|
13
|
+
# Returns the checksum for +dependency+ and +algorithm+, if present.
|
|
14
|
+
def checksum_for(dependency, algorithm = "sha256")
|
|
15
|
+
checksums.fetch(dependency, {}).fetch(algorithm, nil)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns a dependency => sha256 checksum map.
|
|
19
|
+
def sha256_checksums
|
|
20
|
+
checksums.each_with_object({}) do |(dependency, algorithms), memo|
|
|
21
|
+
digest = algorithms["sha256"]
|
|
22
|
+
memo[dependency] = digest if digest
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns dependencies that do not have a sha256 checksum.
|
|
27
|
+
def missing_checksum_dependencies
|
|
28
|
+
dependencies.reject { |dependency| sha256_checksums.key?(dependency) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns true if the lockfile contained a CHECKSUMS section.
|
|
32
|
+
def checksums_present?
|
|
33
|
+
checksums_section_present
|
|
34
|
+
end
|
|
35
|
+
end
|
|
7
36
|
|
|
8
37
|
def initialize(path = "Gemfile.lock")
|
|
9
38
|
@path = path
|
|
10
39
|
end
|
|
11
40
|
|
|
12
|
-
|
|
41
|
+
# Parses the lockfile into dependencies and checksum metadata.
|
|
42
|
+
def parse
|
|
13
43
|
raise LockfileError, "Lockfile not found: #{@path}" unless File.file?(@path)
|
|
14
44
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
specs_section = false if specs_section && line.match?(/^[A-Z]/)
|
|
19
|
-
next unless specs_section
|
|
20
|
-
|
|
21
|
-
match = GEM_LINE.match(line)
|
|
22
|
-
next unless match
|
|
45
|
+
dependencies = []
|
|
46
|
+
checksums = {}
|
|
47
|
+
section = nil
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
49
|
+
File.readlines(@path, chomp: true).each do |line|
|
|
50
|
+
section = section_for(line, section)
|
|
51
|
+
parse_specs_line(line, dependencies) if section == :specs
|
|
52
|
+
parse_checksums_line(line, checksums) if section == :checksums
|
|
28
53
|
end
|
|
54
|
+
|
|
55
|
+
LockfileData.new(dependencies, checksums, checksums.any?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the dependencies listed in the lockfile.
|
|
59
|
+
def dependencies
|
|
60
|
+
parse.dependencies
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the raw checksum map extracted from the lockfile.
|
|
64
|
+
def checksums
|
|
65
|
+
parse.checksums
|
|
29
66
|
end
|
|
30
67
|
|
|
31
68
|
private
|
|
32
69
|
|
|
70
|
+
def section_for(line, current_section)
|
|
71
|
+
case line
|
|
72
|
+
when " specs:"
|
|
73
|
+
:specs
|
|
74
|
+
when "CHECKSUMS"
|
|
75
|
+
:checksums
|
|
76
|
+
when /^[A-Z]/
|
|
77
|
+
nil
|
|
78
|
+
else
|
|
79
|
+
current_section
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_specs_line(line, dependencies)
|
|
84
|
+
match = GEM_LINE.match(line)
|
|
85
|
+
return unless match
|
|
86
|
+
|
|
87
|
+
name = match[1]
|
|
88
|
+
version_and_platform = match[2]
|
|
89
|
+
version, platform = split_version_and_platform(version_and_platform)
|
|
90
|
+
dependencies << Dependency.new(name:, version:, platform:)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_checksums_line(line, checksums)
|
|
94
|
+
match = CHECKSUM_LINE.match(line)
|
|
95
|
+
return unless match
|
|
96
|
+
|
|
97
|
+
name = match[1]
|
|
98
|
+
version_and_platform = match[2]
|
|
99
|
+
checksum_blob = match[3]
|
|
100
|
+
version, platform = split_version_and_platform(version_and_platform)
|
|
101
|
+
dependency = Dependency.new(name:, version:, platform:)
|
|
102
|
+
checksums[dependency] ||= {}
|
|
103
|
+
register_checksum_pairs(checksums[dependency], checksum_blob)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def register_checksum_pairs(checksum_store, checksum_blob)
|
|
107
|
+
checksum_blob.split(",").each do |pair|
|
|
108
|
+
algorithm, digest = pair.split("=", 2).map(&:strip)
|
|
109
|
+
next if algorithm.to_s.empty? || digest.to_s.empty?
|
|
110
|
+
|
|
111
|
+
checksum_store[algorithm] = digest
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
33
115
|
# Bundler renders native platforms as `1.2.3-x86_64-linux` in the spec line.
|
|
34
116
|
# Ruby versions remain plain, for example `1.2.3`.
|
|
35
117
|
def split_version_and_platform(value)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gem
|
|
4
|
+
module Guardian
|
|
5
|
+
# Result object for a provenance verification attempt.
|
|
6
|
+
ProvenanceResult = Data.define(
|
|
7
|
+
:dependency, :status, :trusted_publishing, :repository, :ref, :workflow, :issuer, :subject,
|
|
8
|
+
:expected_sha256, :actual_sha256, :error, :attestation_url
|
|
9
|
+
) do
|
|
10
|
+
# Returns true when provenance verification succeeded.
|
|
11
|
+
def verified?
|
|
12
|
+
status == :verified
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Verifies RubyGems Trusted Publishing provenance metadata.
|
|
17
|
+
class ProvenanceVerifier
|
|
18
|
+
def initialize(client: RubygemsClient.new)
|
|
19
|
+
@client = client
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Verifies Trusted Publishing provenance for +dependency+.
|
|
23
|
+
def verify(dependency, artifact_sha256: nil)
|
|
24
|
+
provenance = @client.trusted_publishing_provenance(dependency)
|
|
25
|
+
return unsupported_result(dependency) unless provenance
|
|
26
|
+
|
|
27
|
+
build_result(dependency, provenance, artifact_sha256)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
error_result(dependency, artifact_sha256, e)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Verifies provenance for each dependency-result pair.
|
|
33
|
+
def verify_all(results)
|
|
34
|
+
results.map { |result| verify(result.dependency, artifact_sha256: result.actual_sha256) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# rubocop:disable Metrics/MethodLength
|
|
40
|
+
def build_result(dependency, provenance, artifact_sha256)
|
|
41
|
+
ProvenanceResult.new(**result_attributes(
|
|
42
|
+
dependency, provenance, artifact_sha256, provenance_status(provenance, artifact_sha256)
|
|
43
|
+
))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unsupported_result(dependency)
|
|
47
|
+
ProvenanceResult.new(**result_attributes(dependency, nil, nil, :unsupported))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def error_result(dependency, artifact_sha256, error)
|
|
51
|
+
ProvenanceResult.new(**result_attributes(dependency, nil, artifact_sha256, :error, error))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def result_attributes(dependency, provenance, artifact_sha256, status, error = nil)
|
|
55
|
+
{
|
|
56
|
+
dependency:,
|
|
57
|
+
status:,
|
|
58
|
+
trusted_publishing: provenance&.trusted_publishing,
|
|
59
|
+
repository: provenance&.repository,
|
|
60
|
+
ref: provenance&.ref,
|
|
61
|
+
workflow: provenance&.workflow,
|
|
62
|
+
issuer: provenance&.issuer,
|
|
63
|
+
subject: provenance&.subject,
|
|
64
|
+
expected_sha256: provenance&.sha256,
|
|
65
|
+
actual_sha256: artifact_sha256,
|
|
66
|
+
error:,
|
|
67
|
+
attestation_url: provenance&.attestation_url
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
# rubocop:enable Metrics/MethodLength
|
|
71
|
+
|
|
72
|
+
def provenance_status(provenance, artifact_sha256)
|
|
73
|
+
return :unsupported unless provenance.trusted_publishing
|
|
74
|
+
return :verified unless provenance.sha256 && artifact_sha256
|
|
75
|
+
|
|
76
|
+
secure_compare(provenance.sha256, artifact_sha256) ? :verified : :mismatch
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def secure_compare(left, right)
|
|
80
|
+
left = left.to_s
|
|
81
|
+
right = right.to_s
|
|
82
|
+
return false unless left.bytesize == right.bytesize
|
|
83
|
+
|
|
84
|
+
left.bytes.zip(right.bytes).reduce(0) { |memo, (a, b)| memo | (a ^ b) }.zero?
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gem
|
|
4
|
+
module Guardian
|
|
5
|
+
# Builds machine-readable verification reports.
|
|
6
|
+
class ReportBuilder
|
|
7
|
+
# @param version [String] gem-guardian version string
|
|
8
|
+
def initialize(version:)
|
|
9
|
+
@version = version
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Returns a JSON-friendly hash for the current verification run.
|
|
13
|
+
def build(results, lockfile_data:, provenance_results: [], lockfile_path: nil)
|
|
14
|
+
{
|
|
15
|
+
version: @version,
|
|
16
|
+
command: "verify",
|
|
17
|
+
mode: lockfile_data ? "lockfile" : "explicit",
|
|
18
|
+
lockfile: lockfile_path,
|
|
19
|
+
checksums: checksum_summary(lockfile_data),
|
|
20
|
+
results: results.map.with_index { |result, index| build_result(result, provenance_results[index]) }
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def checksum_summary(lockfile_data)
|
|
27
|
+
return nil unless lockfile_data
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
coverage: {
|
|
31
|
+
covered: lockfile_data.dependencies.size - lockfile_data.missing_checksum_dependencies.size,
|
|
32
|
+
total: lockfile_data.dependencies.size
|
|
33
|
+
},
|
|
34
|
+
missing: lockfile_data.missing_checksum_dependencies.map do |dependency|
|
|
35
|
+
dependency_hash(dependency)
|
|
36
|
+
end
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_result(result, provenance_result)
|
|
41
|
+
dependency_hash(result.dependency).merge(
|
|
42
|
+
checksum: checksum_hash(result)
|
|
43
|
+
).tap do |hash|
|
|
44
|
+
hash[:provenance] = provenance_hash(provenance_result) if provenance_result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dependency_hash(dependency)
|
|
49
|
+
{
|
|
50
|
+
name: dependency.name,
|
|
51
|
+
version: dependency.version,
|
|
52
|
+
platform: dependency.platform
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def provenance_hash(result)
|
|
57
|
+
provenance_fields(result).merge(
|
|
58
|
+
error: error_hash(result.error)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the non-error provenance fields.
|
|
63
|
+
# rubocop:disable Metrics/MethodLength
|
|
64
|
+
def provenance_fields(result)
|
|
65
|
+
{
|
|
66
|
+
status: result.status,
|
|
67
|
+
trusted_publishing: result.trusted_publishing,
|
|
68
|
+
repository: result.repository,
|
|
69
|
+
ref: result.ref,
|
|
70
|
+
workflow: result.workflow,
|
|
71
|
+
issuer: result.issuer,
|
|
72
|
+
subject: result.subject,
|
|
73
|
+
expected_sha256: result.expected_sha256,
|
|
74
|
+
actual_sha256: result.actual_sha256,
|
|
75
|
+
attestation_url: result.attestation_url
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
# rubocop:enable Metrics/MethodLength
|
|
79
|
+
|
|
80
|
+
# Returns the checksum payload for a verification result.
|
|
81
|
+
def checksum_hash(result)
|
|
82
|
+
{
|
|
83
|
+
status: result.status,
|
|
84
|
+
expected_sha256: result.expected_sha256,
|
|
85
|
+
actual_sha256: result.actual_sha256,
|
|
86
|
+
artifact_path: result.artifact_path,
|
|
87
|
+
checksum_source: result.checksum_source,
|
|
88
|
+
error: error_hash(result.error)
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def error_hash(error)
|
|
93
|
+
return nil unless error
|
|
94
|
+
|
|
95
|
+
{ class: error.class.name, message: error.message }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gem
|
|
4
|
+
module Guardian
|
|
5
|
+
# Formats verification results for human-readable CLI output.
|
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
|
7
|
+
class ResultPrinter
|
|
8
|
+
# @param stdout [IO] output stream for formatted messages
|
|
9
|
+
def initialize(stdout:)
|
|
10
|
+
@stdout = stdout
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Prints a collection of verification results.
|
|
14
|
+
def print_results(results, lockfile_mode:)
|
|
15
|
+
results.each do |result|
|
|
16
|
+
print_result(result, lockfile_mode:)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Prints one verification result.
|
|
21
|
+
def print_result(result, lockfile_mode:)
|
|
22
|
+
label = result_label(result)
|
|
23
|
+
case result.status
|
|
24
|
+
when :ok then print_ok_result(result, label, lockfile_mode)
|
|
25
|
+
when :mismatch then print_mismatch_result(result, label)
|
|
26
|
+
else print_error_result(result, label)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Prints a successful verification result.
|
|
31
|
+
def print_ok_result(result, label, lockfile_mode)
|
|
32
|
+
prefix = lockfile_mode && result.checksum_source == :rubygems ? "FALLBACK" : "PASS"
|
|
33
|
+
@stdout.puts "#{prefix} #{label}"
|
|
34
|
+
@stdout.puts " sha256 #{result.actual_sha256}"
|
|
35
|
+
@stdout.puts " source #{result.checksum_source}" if lockfile_mode && result.checksum_source
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Prints a checksum mismatch.
|
|
39
|
+
def print_mismatch_result(result, label)
|
|
40
|
+
@stdout.puts "FAIL #{label}"
|
|
41
|
+
@stdout.puts " expected #{result.expected_sha256}"
|
|
42
|
+
@stdout.puts " actual #{result.actual_sha256}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Prints an unexpected verifier error.
|
|
46
|
+
def print_error_result(result, label)
|
|
47
|
+
@stdout.puts "ERROR #{label}"
|
|
48
|
+
@stdout.puts " #{result.error.class}: #{result.error.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Prints lockfile checksum coverage.
|
|
52
|
+
def print_lockfile_coverage(lockfile_data)
|
|
53
|
+
covered = lockfile_data.dependencies.size - lockfile_data.missing_checksum_dependencies.size
|
|
54
|
+
total = lockfile_data.dependencies.size
|
|
55
|
+
@stdout.puts "CHECKSUMS coverage: #{covered}/#{total}"
|
|
56
|
+
|
|
57
|
+
lockfile_data.missing_checksum_dependencies.each do |dependency|
|
|
58
|
+
@stdout.puts "MISSING #{dependency.name} #{dependency.version} #{dependency.platform}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Prints provenance verification results.
|
|
63
|
+
def print_provenance_results(results)
|
|
64
|
+
results.each do |result|
|
|
65
|
+
print_provenance_result(result)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Prints one provenance verification result.
|
|
70
|
+
def print_provenance_result(result)
|
|
71
|
+
label = result_label(result)
|
|
72
|
+
case result.status
|
|
73
|
+
when :verified then print_verified_provenance_result(result, label)
|
|
74
|
+
when :mismatch then print_mismatched_provenance_result(result, label)
|
|
75
|
+
else print_unsupported_provenance_result(result, label)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Prints a successful provenance verification result.
|
|
80
|
+
def print_verified_provenance_result(result, label)
|
|
81
|
+
@stdout.puts "PROVENANCE PASS #{label}"
|
|
82
|
+
@stdout.puts " source trusted-publishing"
|
|
83
|
+
provenance_fields(result).each do |label_name, value|
|
|
84
|
+
@stdout.puts format_provenance_field(label_name, value) if value
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Prints a provenance checksum mismatch.
|
|
89
|
+
def print_mismatched_provenance_result(result, label)
|
|
90
|
+
@stdout.puts "PROVENANCE FAIL #{label}"
|
|
91
|
+
@stdout.puts " expected #{result.expected_sha256}"
|
|
92
|
+
@stdout.puts " actual #{result.actual_sha256}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Prints a provenance result when no trusted publishing data is available.
|
|
96
|
+
def print_unsupported_provenance_result(_result, label)
|
|
97
|
+
@stdout.puts "PROVENANCE UNSUPPORTED #{label}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Prints the CLI usage text.
|
|
101
|
+
def usage
|
|
102
|
+
@stdout.puts(USAGE)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# CLI usage text.
|
|
106
|
+
USAGE = <<~USAGE.freeze
|
|
107
|
+
gem-guardian #{VERSION}
|
|
108
|
+
|
|
109
|
+
Usage:
|
|
110
|
+
gem-guardian verify [--lockfile Gemfile.lock] [--json] [--provenance]
|
|
111
|
+
gem-guardian verify GEM:VERSION[:PLATFORM] [GEM:VERSION[:PLATFORM] ...]
|
|
112
|
+
gem-guardian version
|
|
113
|
+
gem-guardian help
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
gem-guardian verify
|
|
117
|
+
gem-guardian verify sidekiq:8.1.6
|
|
118
|
+
gem-guardian verify cdc-sidekiq:0.1.1
|
|
119
|
+
gem-guardian verify nokogiri:1.18.9:x86_64-linux
|
|
120
|
+
gem-guardian verify --json --provenance ratomic:0.4.1
|
|
121
|
+
USAGE
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def result_label(result)
|
|
126
|
+
dependency = result.dependency
|
|
127
|
+
"#{dependency.name} #{dependency.version} #{dependency.platform}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Returns the provenance fields to render for a verified result.
|
|
131
|
+
def provenance_fields(result)
|
|
132
|
+
[
|
|
133
|
+
["repository", result.repository],
|
|
134
|
+
["workflow", result.workflow],
|
|
135
|
+
["ref", result.ref],
|
|
136
|
+
["issuer", result.issuer],
|
|
137
|
+
["subject", result.subject],
|
|
138
|
+
["sha256", result.expected_sha256],
|
|
139
|
+
["attestation", result.attestation_url]
|
|
140
|
+
]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Formats one provenance field line.
|
|
144
|
+
def format_provenance_field(label, value)
|
|
145
|
+
format("%<label>11s %<value>s", label:, value:)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
# rubocop:enable Metrics/ClassLength
|
|
149
|
+
end
|
|
150
|
+
end
|