bundler-checksum 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f30c2035a02d0175319ae3da8ba7c6559bf70c081dc8d26381cfdeabab9d2d54
4
+ data.tar.gz: db6bab7b4479f87a36496f0d6f49b05562b3aa4b7a5f03784fd51d02d1b3a9e3
5
+ SHA512:
6
+ metadata.gz: c8c8697de49be3491b0a9a3486a1630a3e758d2338b328c782ca39588bc0da4a03d11df27816f5c6972fc71a754cae822796ff0079ed22993e82411d4fb227ae
7
+ data.tar.gz: 7d0f2bffafdd409f8fd8084a27bbdc472dab4b6fcde79599485a227b8a3329204af1049a6b8207846711eeff152a92a84090b4e535b0ed917721f7590b8eb672
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler-checksum'
4
+ require 'bundler/checksum/command'
5
+
6
+ Bundler::Checksum::Command.execute(ARGV)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+
6
+ module Bundler::Checksum::Command
7
+ module Helper
8
+ extend self
9
+
10
+ def remote_checksums_for_gem(gem_name, gem_version)
11
+ response = Net::HTTP.get_response(URI(
12
+ "https://rubygems.org/api/v1/versions/#{gem_name}.json"
13
+ ))
14
+
15
+ return [] unless response.code == '200'
16
+
17
+ gem_candidates = JSON.parse(response.body, symbolize_names: true)
18
+ gem_candidates.select! { |g| g[:number] == gem_version.to_s }
19
+
20
+ gem_candidates.map {
21
+ |g| {:name => gem_name, :version => gem_version, :platform => g[:platform], :checksum => g[:sha]}
22
+ }
23
+
24
+ rescue JSON::ParserError
25
+ []
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Bundler::Checksum::Command
6
+ module Init
7
+ extend self
8
+
9
+ def execute
10
+ $stderr.puts "Initializing checksum file #{checksum_file}"
11
+
12
+ checksums = []
13
+
14
+ compact_index_cache = Bundler::Fetcher::CompactIndex
15
+ .new(nil, Bundler::Source::Rubygems::Remote.new(Bundler::URI("https://rubygems.org")), nil)
16
+ .send(:compact_index_client)
17
+ .instance_variable_get(:@cache)
18
+
19
+ seen = []
20
+ Bundler.definition.resolve.sort_by(&:name).each do |spec|
21
+ next unless spec.source.is_a?(Bundler::Source::Rubygems)
22
+
23
+ next if seen.include?(spec.name)
24
+ seen << spec.name
25
+
26
+ $stderr.puts "Adding #{spec.name}==#{spec.version}"
27
+
28
+ compact_index_dependencies = compact_index_cache.dependencies(spec.name).select { |item| item.first == spec.version.to_s }
29
+
30
+ if !compact_index_dependencies.empty?
31
+ compact_index_checksums = compact_index_dependencies.map do |version, platform, dependencies, requirements|
32
+ {
33
+ name: spec.name,
34
+ version: spec.version.to_s,
35
+ platform: Gem::Platform.new(platform).to_s,
36
+ checksum: requirements.detect { |requirement| requirement.first == 'checksum' }.flatten[1]
37
+ }
38
+ end
39
+
40
+ checksums += compact_index_checksums.sort_by { |hash| hash.values }
41
+ else
42
+ remote_checksum = Helper.remote_checksums_for_gem(spec.name, spec.version)
43
+
44
+ if remote_checksum.empty?
45
+ raise "#{spec.name} #{spec.version} not found on Rubygems!"
46
+ end
47
+
48
+ checksums += remote_checksum.sort_by { |hash| hash.values }
49
+ end
50
+ end
51
+
52
+ File.write(checksum_file, JSON.generate(checksums, array_nl: "\n") + "\n")
53
+ end
54
+
55
+ private
56
+
57
+ def checksum_file
58
+ ::Bundler::Checksum.checksum_file
59
+ end
60
+
61
+ def lockfile
62
+ lockfile_path = Bundler.default_lockfile
63
+ lockfile = Bundler::LockfileParser.new(Bundler.read_file(lockfile_path))
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler::Checksum::Command
4
+ module Verify
5
+ extend self
6
+
7
+ def execute
8
+ $stderr.puts 'Verifying bundle checksums'
9
+
10
+ verified = true
11
+
12
+ local_checksums.each do |gem|
13
+ name = gem.fetch(:name)
14
+ version = gem.fetch(:version)
15
+ platform = gem.fetch(:platform)
16
+ checksum = gem.fetch(:checksum)
17
+
18
+ $stderr.puts "Verifying #{name}==#{version} #{platform}"
19
+ unless validate_gem_checksum(name, version, platform, checksum)
20
+ verified = false
21
+ end
22
+ end
23
+
24
+ verified
25
+ end
26
+
27
+ private
28
+
29
+ def local_checksums
30
+ ::Bundler::Checksum.checksums_from_file
31
+ end
32
+
33
+ def validate_gem_checksum(gem_name, gem_version, gem_platform, local_checksum)
34
+ remote_checksums = Helper.remote_checksums_for_gem(gem_name, gem_version)
35
+ if remote_checksums.empty?
36
+ $stderr.puts "#{gem_name} #{gem_version} not found on Rubygems, skipping"
37
+ return false
38
+ end
39
+
40
+ remote_platform_checksum = remote_checksums.find { |g| g[:name] == gem_name && g[:platform] == gem_platform.to_s }
41
+
42
+ if local_checksum == remote_platform_checksum[:checksum]
43
+ true
44
+ else
45
+ $stderr.puts "Gem #{gem_name} #{gem_version} #{gem_platform} failed checksum verification"
46
+ $stderr.puts "LOCAL: #{local_checksum}"
47
+ $stderr.puts "REMOTE: #{remote_platform_checksum[:checksum]}"
48
+ return false
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler::Checksum
4
+ module Command
5
+ autoload :Init, File.expand_path("command/init", __dir__)
6
+ autoload :Verify, File.expand_path("command/verify", __dir__)
7
+ autoload :Helper, File.expand_path("command/helper", __dir__)
8
+
9
+ def self.execute(args)
10
+ if args.empty?
11
+ $stderr.puts 'A command must be given [init,update,verify]'
12
+ end
13
+
14
+ if args.first == 'init'
15
+ Init.execute
16
+ elsif args.first == 'update'
17
+ $stderr.puts 'Not implemented, please use init'
18
+ elsif args.first == 'verify'
19
+ verified = Verify.execute
20
+
21
+ unless verified
22
+ exit 1
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Checksum
5
+ # bundler-checksum version
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ require 'bundler/checksum/version'
5
+ require 'json'
6
+
7
+ module Bundler
8
+ module Patches
9
+ # This module monkey-patches Bundler to check Gemfile.checksum
10
+ # when installing gems that are from RubyGems
11
+ module RubyGemsInstallerPatch
12
+ def pre_install_checks
13
+ super && validate_local_package_checksum
14
+ end
15
+
16
+ private
17
+
18
+ def validate_local_package_checksum
19
+ cached_checksum = fetch_checksum_from_file(spec)
20
+
21
+ if cached_checksum.nil?
22
+ raise SecurityError, "Cached checksum for #{spec.full_name} not found. Please (re-)generate Gemfile.checksum"
23
+ end
24
+
25
+ validate_file_checksum(cached_checksum)
26
+ end
27
+
28
+ def fetch_checksum_from_file(spec)
29
+ ::Bundler::Checksum.checksum_for(spec.name, spec.version.to_s, spec.platform.to_s)
30
+ end
31
+
32
+ # Modified from
33
+ # https://github.com/rubygems/rubygems/blob/243173279e79a38f03e318eea8825d1c8824e119/bundler/lib/bundler/rubygems_gem_installer.rb#L116
34
+ def validate_file_checksum(checksum)
35
+ return true if Bundler.settings[:disable_checksum_validation]
36
+
37
+ source = @package.instance_variable_get(:@gem)
38
+
39
+ # Contary to upstream, we raise instead of silently returning
40
+ raise "#{@package.inspect} does not have :@gem" unless source
41
+ raise "#{source.inspect} does not respond to :with_read_io" unless source.respond_to?(:with_read_io)
42
+
43
+ digest = source.with_read_io do |io|
44
+ digest = SharedHelpers.digest(:SHA256).new
45
+ digest << io.read(16_384) until io.eof?
46
+ io.rewind
47
+ send(checksum_type(checksum), digest)
48
+ end
49
+ unless digest == checksum
50
+ raise SecurityError, <<-MESSAGE
51
+ Bundler cannot continue installing #{spec.name} (#{spec.version}).
52
+ The checksum for the downloaded `#{spec.full_name}.gem` does not match \
53
+ the checksum from the checksum file. This means the contents of the downloaded \
54
+ gem is different from what was recorded in the checksum file, and could be potential security issue.
55
+ gem is different from what was uploaded to the server, and could be a potential security issue.
56
+
57
+ To resolve this issue:
58
+ 1. delete the downloaded gem located at: `#{spec.gem_dir}/#{spec.full_name}.gem`
59
+ 2. run `bundle install`
60
+
61
+ If you wish to continue installing the downloaded gem, and are certain it does not pose a \
62
+ security issue despite the mismatching checksum, do the following:
63
+ 1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification
64
+ 2. run `bundle install`
65
+
66
+ (More info: The expected SHA256 checksum was #{checksum.inspect}, but the \
67
+ checksum for the downloaded gem was #{digest.inspect}.)
68
+ MESSAGE
69
+ end
70
+ true
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ module Bundler
77
+ module Checksum
78
+ class << self
79
+ def checksum_file
80
+ @checksum_file ||= File.join(File.dirname(Bundler.default_gemfile), 'Gemfile.checksum')
81
+ end
82
+
83
+ def checksums_from_file
84
+ @checksums_from_file ||= JSON.parse(File.open(checksum_file).read, symbolize_names: true)
85
+ rescue JSON::ParserError => e
86
+ raise "Invalid checksum file: #{e.message}"
87
+ end
88
+
89
+ def checksum_for(gem_name, gem_version, gem_platform)
90
+ item = checksums_from_file.detect do |item|
91
+ item[:name] == gem_name &&
92
+ item[:platform] == gem_platform &&
93
+ item[:version] == gem_version
94
+ end
95
+
96
+ item&.fetch(:checksum)
97
+ end
98
+
99
+ def patch!
100
+ return if defined?(@patched) && @patched
101
+ @patched = true
102
+
103
+ Bundler.ui.info "Patching bundler with bundler-checksum..."
104
+ require 'bundler/rubygems_gem_installer'
105
+ ::Bundler::RubyGemsGemInstaller.prepend(Bundler::Patches::RubyGemsInstallerPatch)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1 @@
1
+ require 'bundler/checksum'
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bundler-checksum
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - dustinmm80
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-09-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Track checksums locally with Bundler
28
+ email:
29
+ - dcollins@gitlab.com
30
+ executables:
31
+ - bundler-checksum
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - bin/bundler-checksum
36
+ - lib/bundler-checksum.rb
37
+ - lib/bundler/checksum.rb
38
+ - lib/bundler/checksum/command.rb
39
+ - lib/bundler/checksum/command/helper.rb
40
+ - lib/bundler/checksum/command/init.rb
41
+ - lib/bundler/checksum/command/verify.rb
42
+ - lib/bundler/checksum/version.rb
43
+ homepage: https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/bundler-checksum
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.3.16
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Track checksums locally with Bundler
66
+ test_files: []