bundler-patch 0.6.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
+ SHA1:
3
+ metadata.gz: 11622ac1ecc12f9b1357768a9d8548fa5b9d54fd
4
+ data.tar.gz: 9f824a7f9b6fe2c3478700d24281ef5006ce2102
5
+ SHA512:
6
+ metadata.gz: 272b7fef46e02020fb40ea6888501c704aaf0e93f428505d43f8b512829b0e383efef0a2e1f6aec6a08f972ee80fa826df6059cb137a3bfbe1a2524bc758e661
7
+ data.tar.gz: b71751b264518c672f6fde631f7becc3307874a9ae547437849015438d8842b74cb4548fb85f359ef3d4d1c524e1607749f88e6dd4f80f94b76f7b76f282c6d9
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /bin/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --backtrace
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.4
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.4
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ source 'https://gems.livingsocial.net'
3
+
4
+ # Specify your gem's dependencies in bundler-patch.gemspec
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 LivingSocial
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # Bundler::Patch
2
+
3
+ `bundler-patch` can update your Gemfile conservatively to deal with vulnerable gems or just get more current.
4
+
5
+ ## Goals
6
+
7
+ - Update the Gemfile, .ruby-version and other files to patch an app according to `ruby-advisory-db` content.
8
+ - Don't upgrade past the minimum gem version required.
9
+ - Minimal munging to existing version spec.
10
+ - Support a database of custom advisories for internal gems.
11
+
12
+ ## Installation
13
+
14
+ $ gem install bundler-patch
15
+
16
+ ## Usage
17
+
18
+ ### Scan / Patch Security Vulnerable Gems
19
+
20
+ To output the list of detected vulnerabilities in the current project:
21
+
22
+ $ bundle-patch scan
23
+
24
+ To specify the path to an optional advisory database:
25
+
26
+ $ bundle-patch scan -a ~/.my-custom-db
27
+
28
+ _*NOTE*: `gems` will be appended to the end of the provided advisory database path._
29
+
30
+ To attempt to patch the detected vulnerabilities, use the `patch` command instead of `scan`:
31
+
32
+ $ bundle-patch patch
33
+
34
+ Same options apply. Read the next section for details on how bumps to release, minor and major versions work.
35
+
36
+ For help:
37
+
38
+ $ bundle-patch help scan
39
+ $ bundle-patch help patch
40
+
41
+ ### Conservatively Update All Gems
42
+
43
+ To update any gem conservatively, use the `update` command:
44
+
45
+ $ bundle-patch update 'foo bar'
46
+
47
+ This will attempt to upgrade the `foo` and `bar` gems to the latest release version. (e.g. if the current version is
48
+ `1.4.3` and the latest available `1.4.x` version is `1.4.8`, it will attempt to upgrade it to `1.4.8`). If any
49
+ dependent gems need to be upgraded to a new minor or even major version, then it will do those as well, presuming the
50
+ gem requirements specified in the `Gemfile` also allow it.
51
+
52
+ If you want to restrict _any_ gem from being upgraded past the most recent release version, use `--strict` mode:
53
+
54
+ $ bundle-patch update 'foo bar' --strict
55
+
56
+ This will eliminate any newer minor or major releases for any gem. If Bundler is unable to upgrade the requested gems
57
+ due to the limitations, it will leave the requested gems at their current version.
58
+
59
+ If you want to allow minor release upgrades (e.g. to allow an upgrade from `1.4.3` to `1.6.1`) use the `--minor_allowed`
60
+ option.
61
+
62
+ `--minor_allowed` (alias `-m`) and `--strict` (alias `-s`) can be used together or independently.
63
+
64
+ While `--minor_allowed` is most useful in combination with the `--strict` option, it can also influence behavior when
65
+ not in strict mode.
66
+
67
+ For example, if an update to `foo` is requested, current version is `1.4.3` and `foo` has both `1.4.8` and `1.6.1`
68
+ available, then without `--minor_allowed` and without `--strict`, `foo` itself will only be upgraded to `1.4.8`, though
69
+ any gems further down the dependency tree could be upgraded to a new minor or major version if they have to be to use
70
+ `foo 1.4.8`.
71
+
72
+ Continuing the example, _with_ `--minor_allowed` (but still without `--strict`) `foo` itself would be upgraded to
73
+ `1.6.1`, and as before any gems further down the dependency tree could be upgraded to a new minor or major version if
74
+ they have to.
75
+
76
+ To request conservative updates for the entire Gemfile, simply call `update`:
77
+
78
+ $ bundle-patch update
79
+
80
+ There's no option to allow major version upgrades as this is the default behavior of `bundle update` in Bundler itself.
81
+
82
+
83
+ ### Troubleshooting
84
+
85
+ First tip: make sure the current `bundle` command runs to completion on its own without any problems.
86
+
87
+ The most frequent problems with this tool involve expectations around what gems should or shouldn't be upgraded. This
88
+ can quickly get complicated as even a small dependency tree can involve many moving parts, and Bundler works hard to
89
+ find a combination that satisfies all of the dependencies and requirements.
90
+
91
+ You can get a (very verbose) look into how Bundler's resolution algorithm is working by setting the `DEBUG_RESOLVER`
92
+ environment variable. While it can be tricky to dig through, it should explain how it came to the conclusions it came
93
+ to.
94
+
95
+ Adding to the usual Bundler complexity, `bundler-patch` is injecting its own logic to the resolution process to achieve
96
+ its goals. If there's a bug involved, it's almost certainly in the `bundler-patch` code as Bundler has been around a
97
+ long time and has thorough testing and real world experience.
98
+
99
+ In particular, grep for 'Unwinding for conflict' to isolate some key issues that may be preventing the outcome you
100
+ expect.
101
+
102
+ `bundler-patch` can dump its own debug output, potentially helpful, with `DEBUG_PATCH_RESOLVER`.
103
+
104
+ To get additional Bundler debugging output, enable the `DEBUG` env variable. This will include all of the details of
105
+ the downloading the full dependency data from remote sources.
106
+
107
+
108
+ ## Development
109
+
110
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
111
+ also run `bin/console` for an interactive prompt that will allow you to experiment.
112
+
113
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
114
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
115
+ push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
116
+
117
+ ## Contributing
118
+
119
+ Bug reports and pull requests are welcome on GitHub at https://github.com/livingsocial/bundler-patch.
120
+
121
+
122
+ ## License
123
+
124
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
125
+
126
+
127
+ ## Misc
128
+
129
+ None of these do what we need, but may have some code doing some similar work in places.
130
+
131
+ - http://www.rubydoc.info/gems/bundler-auto-update/0.1.0 (runs tests after each gem upgrade)
132
+ - http://www.rubydoc.info/gems/bundler-updater/0.0.3 (interactive prompt for what's available to upgrade to)
133
+ - https://github.com/rosylilly/bundler-add (outputs Gemfile line for adding a gem)
134
+
135
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
6
+
7
+ require 'bundler/gem_tasks'
data/bin/bundle-patch ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ lib_dir = File.expand_path(File.join(File.dirname(__FILE__),'..','lib'))
6
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
7
+
8
+ require 'bundler/patch'
9
+
10
+ Bundler::Patch::Scanner.start
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "bundler/patch"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bundler/patch/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'bundler-patch'
8
+ spec.version = Bundler::Patch::VERSION
9
+ spec.authors = ['chrismo']
10
+ spec.email = ['chrismo@clabs.org']
11
+
12
+ spec.summary = %q{Conservative bundler updates}
13
+ # spec.description = ''
14
+ spec.homepage = 'https://github.com/livingsocial/bundler-patch'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'bin'
19
+ spec.executables = ['bundle-patch']
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'bundler-advise', '~> 1.0', '>= 1.0.3'
23
+ spec.add_dependency 'boson'
24
+ spec.add_dependency 'bundler', '~> 1.10'
25
+
26
+ spec.add_development_dependency 'bundler-fixture', '~> 1.1'
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ spec.add_development_dependency 'rspec'
30
+ end
@@ -0,0 +1,73 @@
1
+ module Bundler::Patch
2
+ class AdvisoryConsolidator
3
+ def initialize(options={}, all_ads=nil)
4
+ @options = options
5
+ @all_ads = all_ads || [].tap do |a|
6
+ a << Bundler::Advise::Advisories.new unless options[:skip_bundler_advise]
7
+ a << Bundler::Advise::Advisories.new(dir: options[:advisory_db_path], repo: nil) if options[:advisory_db_path]
8
+ end
9
+ end
10
+
11
+ def vulnerable_gems
12
+ @all_ads.map do |ads|
13
+ ads.update if ads.repo
14
+ Bundler::Advise::GemAdviser.new(advisories: ads).scan_lockfile
15
+ end.flatten.map do |advisory|
16
+ patched = advisory.patched_versions.map do |pv|
17
+ # this is a little stupid for compound requirements, but works itself out in consolidate_gemfiles
18
+ pv.requirements.map { |_, v| v.to_s }
19
+ end.flatten
20
+ Gemfile.new(gem_name: advisory.gem, patched_versions: patched)
21
+ end.group_by do |gemfile|
22
+ gemfile.gem_name
23
+ end.map do |_, gemfiles|
24
+ consolidate_gemfiles(gemfiles)
25
+ end.flatten
26
+ end
27
+
28
+ def patch_gemfile_and_get_gem_specs_to_patch
29
+ gem_update_specs = vulnerable_gems
30
+ locked = Bundler::LockfileParser.new(Bundler.read_file(Bundler.default_lockfile)).specs
31
+
32
+ gem_update_specs.map(&:update) # modify requirements in Gemfile if necessary
33
+
34
+ gem_update_specs.map do |up_spec|
35
+ old_version = locked.detect { |s| s.name == up_spec.gem_name }.version.to_s
36
+ new_version = up_spec.calc_new_version(old_version)
37
+ if new_version
38
+ GemPatch.new(gem_name: up_spec.gem_name, old_version: old_version,
39
+ new_version: new_version, patched_versions: up_spec.patched_versions)
40
+ else
41
+ GemPatch.new(gem_name: up_spec.gem_name, old_version: old_version, patched_versions: up_spec.patched_versions)
42
+ end
43
+ end.partition { |gp| !gp.new_version.nil? }
44
+ end
45
+
46
+ private
47
+
48
+ def consolidate_gemfiles(gemfiles)
49
+ gemfiles if gemfiles.length == 1
50
+ all_gem_names = gemfiles.map(&:gem_name).uniq
51
+ raise 'Must be all same gem name' unless all_gem_names.length == 1
52
+ highest_minor_patched = gemfiles.map do |g|
53
+ g.patched_versions
54
+ end.flatten.group_by do |v|
55
+ Gem::Version.new(v).segments[0..1].join('.')
56
+ end.map do |_, all|
57
+ all.sort.last
58
+ end
59
+ Gemfile.new(gem_name: all_gem_names.first, patched_versions: highest_minor_patched)
60
+ end
61
+ end
62
+
63
+ class GemPatch
64
+ attr_reader :gem_name, :old_version, :new_version, :patched_versions
65
+
66
+ def initialize(gem_name:, old_version: nil, new_version: nil, patched_versions: nil)
67
+ @gem_name = gem_name
68
+ @old_version = old_version
69
+ @new_version = new_version
70
+ @patched_versions = patched_versions
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,122 @@
1
+ module Bundler::Patch
2
+ module ConservativeDefinition
3
+ attr_accessor :gems_to_update
4
+
5
+ # pass-through options to ConservativeResolver
6
+ attr_accessor :strict, :minor_allowed
7
+
8
+ # This copies more code than I'd like out of Bundler::Definition, but for now seems the least invasive way in.
9
+ # Backing up and intervening into the creation of a Definition instance itself involves a lot more code, a lot
10
+ # more preliminary data has to be gathered first.
11
+ def resolve
12
+ @resolve ||= begin
13
+ last_resolve = converge_locked_specs
14
+ if Bundler.settings[:frozen] || (!@unlocking && nothing_changed?)
15
+ last_resolve
16
+ else
17
+ # Run a resolve against the locally available gems
18
+ base = last_resolve.is_a?(Bundler::SpecSet) ? Bundler::SpecSet.new(last_resolve) : []
19
+ resolver = ConservativeResolver.new(index, source_requirements, base)
20
+ locked_specs = if @unlocking && @locked_specs.length == 0
21
+ # Have to grab these again. Default behavior is to not store any
22
+ # locked_specs if updating all gems, because behavior is the same
23
+ # with no lockfile OR lockfile but update them all. In our case,
24
+ # we need to know the locked versions for conservative comparison.
25
+ locked = Bundler::LockfileParser.new(@lockfile_contents)
26
+ Bundler::SpecSet.new(locked.specs)
27
+ else
28
+ @locked_specs
29
+ end
30
+
31
+ resolver.gems_to_update = @gems_to_update
32
+ resolver.locked_specs = locked_specs
33
+ resolver.strict = @strict
34
+ resolver.minor_allowed = @minor_allowed
35
+ result = resolver.start(expanded_dependencies)
36
+ spec_set = Bundler::SpecSet.new(result)
37
+
38
+ last_resolve.merge spec_set
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ class DefinitionPrep
45
+ attr_reader :bundler_def
46
+
47
+ def initialize(bundler_def, gem_patches, options)
48
+ @bundler_def = bundler_def
49
+ @gems_to_update = GemsToUpdate.new(gem_patches, options)
50
+ @options = options
51
+ end
52
+
53
+ def prep
54
+ @bundler_def ||= Bundler.definition(@gems_to_update.to_bundler_definition)
55
+ @bundler_def.extend ConservativeDefinition
56
+ @bundler_def.gems_to_update = @gems_to_update
57
+ @bundler_def.strict = @options[:strict]
58
+ @bundler_def.minor_allowed = @options[:minor_allowed]
59
+ fixup_empty_remotes if @gems_to_update.to_bundler_definition === true
60
+ @bundler_def
61
+ end
62
+
63
+ # This may only matter in cases like sidekiq where the sidekiq-pro gem is served
64
+ # from their gem server and depends on the open-source sidekiq gem served from
65
+ # rubygems.org, and when patching those, without the appropriate remotes being
66
+ # set in rubygems_aggregrate, it won't work.
67
+ #
68
+ # I've seen some other weird cases where a remote source index had no entry for a
69
+ # gem and would trip up bundler-audit. I couldn't pin them down at the time though.
70
+ # But I want to keep this in case.
71
+ #
72
+ # The underlying issue in Bundler 1.10 appears to be when the Definition
73
+ # constructor receives `true` as the `unlock` parameter, then @locked_sources
74
+ # is initialized to empty array, and the related rubygems_aggregrate
75
+ # source instance ends up with no @remotes set in it, which I think happens during
76
+ # converge_sources. Without those set, then the index will list no gem versions in
77
+ # some cases. (It was complicated enough to discover this patch, I haven't fully
78
+ # worked out the flaw, which still could be on my side of the fence).
79
+ def fixup_empty_remotes
80
+ b_sources = @bundler_def.send(:sources)
81
+ empty_remotes = b_sources.rubygems_sources.detect { |s| s.remotes.empty? }
82
+ empty_remotes.remotes.push(*b_sources.rubygems_remotes) if empty_remotes
83
+ end
84
+ end
85
+
86
+ class GemsToUpdate
87
+ attr_reader :gem_patches, :patching
88
+
89
+ def initialize(gem_patches, options={})
90
+ @gem_patches = Array(gem_patches)
91
+ @patching = options[:patching]
92
+ end
93
+
94
+ def to_bundler_definition
95
+ unlocking_all? ? true : {gems: to_gem_names}
96
+ end
97
+
98
+ def to_gem_names
99
+ @gem_patches.map(&:gem_name)
100
+ end
101
+
102
+ def patching_gem?(gem_name)
103
+ @patching && to_gem_names.include?(gem_name)
104
+ end
105
+
106
+ def patching_but_not_this_gem?(gem_name)
107
+ @patching && !to_gem_names.include?(gem_name)
108
+ end
109
+
110
+ def gem_patch_for(gem_name)
111
+ @gem_patches.detect { |gp| gp.gem_name == gem_name }
112
+ end
113
+
114
+ def unlocking_all?
115
+ @patching || @gem_patches.empty?
116
+ end
117
+
118
+ def unlocking_gem?(gem_name)
119
+ unlocking_all? || to_gem_names.include?(gem_name)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,109 @@
1
+ module Bundler::Patch
2
+ class ConservativeResolver < Bundler::Resolver
3
+ attr_accessor :locked_specs, :gems_to_update, :strict, :minor_allowed
4
+
5
+ def search_for(dependency)
6
+ res = super(dependency)
7
+
8
+ dep = dependency.dep unless dependency.is_a? Gem::Dependency
9
+ @conservative_search_for ||= {}
10
+ #@conservative_search_for[dep] ||= # TODO turning off caching allowed a real-world sample to work, dunno why yet.
11
+ begin
12
+ gem_name = dep.name
13
+
14
+ # An Array per version returned, different entries for different platforms.
15
+ # We just need the version here so it's ok to hard code this to the first instance.
16
+ locked_spec = @locked_specs[gem_name].first
17
+
18
+ (@strict ?
19
+ filter_specs(res, locked_spec) :
20
+ sort_specs(res, locked_spec)).tap do |res|
21
+ if ENV['DEBUG_PATCH_RESOLVER']
22
+ # TODO: if we keep this, gotta go through Bundler.ui
23
+ begin
24
+ if res
25
+ p debug_format_result(dep, res)
26
+ else
27
+ p "No res for #{dep.to_s}. Orig res: #{super(dependency)}"
28
+ end
29
+ rescue => e
30
+ p [e.message, e.backtrace[0..5]]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def debug_format_result(dep, res)
38
+ a = [dep.to_s,
39
+ res.map { |sg| [sg.version, sg.dependencies_for_activated_platforms.map { |dp| [dp.name, dp.requirement.to_s] }] }]
40
+ [a.first, a.last.map { |sg_data| [sg_data.first.version, sg_data.last.map { |aa| aa.join(' ') }] }]
41
+ end
42
+
43
+ def filter_specs(specs, locked_spec)
44
+ res = specs.select do |sg|
45
+ # SpecGroup is grouped by name/version, multiple entries for multiple platforms.
46
+ # We only need the name, which will be the same, so hard coding to first is ok.
47
+ gem_spec = sg.first
48
+
49
+ if locked_spec
50
+ gsv = gem_spec.version
51
+ lsv = locked_spec.version
52
+
53
+ must_match = @minor_allowed ? [0] : [0, 1]
54
+
55
+ matches = must_match.map { |idx| gsv.segments[idx] == lsv.segments[idx] }
56
+ (matches.uniq == [true]) ? gsv.send(:>=, lsv) : false
57
+ else
58
+ true
59
+ end
60
+ end
61
+
62
+ sort_specs(res, locked_spec)
63
+ end
64
+
65
+ def sort_specs(specs, locked_spec)
66
+ return specs unless locked_spec
67
+ gem_name = locked_spec.name
68
+ locked_version = locked_spec.version
69
+
70
+ filtered = specs.select { |s| s.first.version >= locked_version }
71
+
72
+ filtered.sort do |a, b|
73
+ a_ver = a.first.version
74
+ b_ver = b.first.version
75
+ case
76
+ when a_ver.segments[0] != b_ver.segments[0]
77
+ b_ver <=> a_ver
78
+ when !@minor_allowed && (a_ver.segments[1] != b_ver.segments[1])
79
+ b_ver <=> a_ver
80
+ when @gems_to_update.patching_but_not_this_gem?(gem_name)
81
+ b_ver <=> a_ver
82
+ else
83
+ a_ver <=> b_ver
84
+ end
85
+ end.tap do |result|
86
+ if @gems_to_update.unlocking_gem?(gem_name)
87
+ if @gems_to_update.patching_gem?(gem_name)
88
+ # this logic will keep a gem from updating past the patched version
89
+ # if a more recent release (or minor, if enabled) version exists.
90
+ # TODO: not sure if we want this special logic to remain or not.
91
+ new_version = @gems_to_update.gem_patch_for(gem_name).new_version
92
+ swap_version_to_end(specs, new_version, result) if new_version
93
+ end
94
+ else
95
+ # make sure the current locked version is last in list.
96
+ swap_version_to_end(specs, locked_version, result)
97
+ end
98
+ end
99
+ end
100
+
101
+ def swap_version_to_end(specs, version, result)
102
+ spec_group = specs.detect { |s| s.first.version.to_s == version.to_s }
103
+ if spec_group
104
+ result.reject! { |s| s.first.version.to_s === version.to_s }
105
+ result << spec_group
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,104 @@
1
+ module Bundler::Patch
2
+ class Gemfile < UpdateSpec
3
+ attr_reader :gem_name
4
+
5
+ def initialize(target_dir: Dir.pwd,
6
+ gem_name:,
7
+ patched_versions: [])
8
+ super(target_file: 'Gemfile',
9
+ target_dir: target_dir,
10
+ patched_versions: patched_versions)
11
+ @gem_name = gem_name
12
+ end
13
+
14
+ def to_s
15
+ "#{@gem_name} #{patched_versions}"
16
+ end
17
+
18
+ def update
19
+ # Bundler evals the whole Gemfile in Bundler::Dsl.evaluate
20
+ # It has a few magics to parse all possible calls to `gem`
21
+ # command. It doesn't have anything to output the entire
22
+ # Gemfile, I don't think it ever does that. (There is code
23
+ # to init a Gemfile from a gemspec, but it doesn't look
24
+ # like it's intended to recreate one just evaled - I don't
25
+ # see any code that would handle additional sources or
26
+ # groups - see lib/bundler/rubygems_ext.rb #to_gemfile).
27
+ #
28
+ # So without something in Bundler that round-trips from
29
+ # Gemfile back to disk and maintains integrity, then we
30
+ # couldn't re-use it to make modifications to the Gemfile
31
+ # like we'd want to, so we'll do this ourselves.
32
+ #
33
+ # We'll still instance_eval the gem line though, to properly
34
+ # handle the various options and possible multiple reqs.
35
+ @target_file = 'Gemfile'
36
+ @regexes = /^\s*gem.*['"]\s*#{@gem_name}\s*['"].*$/
37
+ file_replace do |match, re|
38
+ update_to_new_gem_version(match)
39
+ end
40
+ end
41
+
42
+ def update_to_new_gem_version(match)
43
+ dep = instance_eval(match)
44
+ req = dep.requirement
45
+
46
+ prefix = req.exact? ? '' : req.specific? ? '~> ' : '>= '
47
+
48
+ current_version = req.requirements.first.last.to_s
49
+ new_version = calc_new_version(current_version)
50
+
51
+ # return match if req.satisfied_by?(Gem::Version.new(new_version))
52
+ #
53
+ # TODO: This ^^ could be acceptable, slightly less complex, to not ever
54
+ # touch the requirement if it already covers the patched version.
55
+ # But I can't recall Bundler behavior in all these cases, to ensure
56
+ # at least the patch version is updated and/or we would like to be as
57
+ # conservative as possible in updating - can't recall how much influence
58
+ # we have over `bundle update` (not much)
59
+
60
+ return match if req.compound? && req.satisfied_by?(Gem::Version.new(new_version))
61
+
62
+ if new_version && prefix =~ /~/
63
+ # could Gem::Version#approximate_recommendation work here?
64
+
65
+ # match segments. if started with ~> 1.2 and new_version is 3 segments, replace with 2 segments.
66
+ count = current_version.split(/\./).length
67
+ new_version = new_version.split(/\./)[0..(count-1)].join('.')
68
+ end
69
+
70
+ if new_version
71
+ match.sub(requirements_args_regexp, " '#{prefix}#{new_version}'").tap { |s| "Updating to #{s}" }
72
+ else
73
+ match
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def requirements_args_regexp
80
+ ops = Gem::Requirement::OPS.keys.join "|"
81
+ re = /(\s*['\"]\s*(#{ops})?\s*#{Gem::Version::VERSION_PATTERN}\s*['"],*)+/
82
+ end
83
+
84
+ # See Bundler::Dsl for reference
85
+ def gem(name, *args)
86
+ # we're not concerned with options here.
87
+ _options = args.last.is_a?(Hash) ? args.pop.dup : {}
88
+ version = args || ['>= 0']
89
+
90
+ # there is a normalize_options step that DOES involve
91
+ # the args captured in version for `git` and `path`
92
+ # sources that's skipped here ... need to dig into that
93
+ # at some point.
94
+
95
+ Gem::Dependency.new(name, version)
96
+ end
97
+ end
98
+ end
99
+
100
+ class Gem::Requirement
101
+ def compound?
102
+ @requirements.length > 1
103
+ end
104
+ end
@@ -0,0 +1,25 @@
1
+ module Bundler::Patch
2
+ class RubyVersion < UpdateSpec
3
+ def self.files
4
+ {
5
+ '.ruby-version' => [/.*/],
6
+ '.jenkins.xml' => [/\<string\>(.*)\<\/string\>/, /rvm.*\>ruby-(.*)@/, /version.*rbenv.*\>(.*)\</]
7
+ }
8
+ end
9
+
10
+ def initialize(target_dir: Dir.pwd, patched_versions: [])
11
+ super(target_file: target_file,
12
+ target_dir: target_dir,
13
+ regexes: regexes,
14
+ patched_versions: patched_versions)
15
+ end
16
+
17
+ def update
18
+ self.class.files.each_pair do |file, regexes|
19
+ @target_file = file
20
+ @regexes = regexes
21
+ file_replace
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,87 @@
1
+ require 'bundler/advise'
2
+ require 'boson/runner'
3
+
4
+ module Bundler::Patch
5
+ class Scanner < Boson::Runner
6
+ def initialize
7
+ @no_vulns_message = 'No known vulnerabilities to update.'
8
+ end
9
+
10
+ option :advisory_db_path, type: :string, desc: 'Optional custom advisory db path.'
11
+ desc 'Scans current directory for known vulnerabilities and outputs them.'
12
+
13
+ def scan(options={}) # TODO: Revamp the commands now that we've broadened into security specific and generic
14
+ header
15
+ gem_patches = AdvisoryConsolidator.new(options).vulnerable_gems
16
+
17
+ if gem_patches.empty?
18
+ puts @no_vulns_message
19
+ else
20
+ puts # extra line to separate from advisory db update text
21
+ puts 'Detected vulnerabilities:'
22
+ puts '-------------------------'
23
+ gem_patches.each do |gp|
24
+ puts "Need to update #{gp.gem_name}: #{gp.old_version} => #{gp.new_version}" # TODO: Bundler.ui
25
+ end
26
+ end
27
+ end
28
+
29
+ option :strict, type: :boolean, desc: 'Do not allow any gem to be upgraded past most recent release (or minor if -m used). Sometimes raises VersionConflict.'
30
+ option :minor_allowed, type: :boolean, desc: 'Upgrade to the latest minor.release version.'
31
+ option :advisory_db_path, type: :string, desc: 'Optional custom advisory db path.'
32
+ desc 'Scans current directory for known vulnerabilities and attempts to patch your files to fix them.'
33
+
34
+ def patch(options={}) # TODO: Revamp the commands now that we've broadened into security specific and generic
35
+ header
36
+
37
+ gem_patches, warnings = AdvisoryConsolidator.new(options).patch_gemfile_and_get_gem_specs_to_patch
38
+
39
+ unless warnings.empty?
40
+ warnings.each do |gp|
41
+ # TODO: Bundler.ui
42
+ puts "* Could not attempt upgrade for #{gp.gem_name} from #{gp.old_version} to any patched versions " \
43
+ + "#{gp.patched_versions.join(', ')}. Most often this is because a major version increment would be " \
44
+ + "required and it's safer for a major version increase to be done manually."
45
+ end
46
+ end
47
+
48
+ if gem_patches.empty?
49
+ puts @no_vulns_message
50
+ else
51
+ gem_patches.each do |gp|
52
+ puts "Attempting #{gp.gem_name}: #{gp.old_version} => #{gp.new_version}" # TODO: Bundler.ui
53
+ end
54
+
55
+ puts "Updating '#{gem_patches.map(&:gem_name).join(' ')}' to address vulnerabilities"
56
+ conservative_update(gem_patches, options.merge(patching: true))
57
+ end
58
+ end
59
+
60
+ option :strict, type: :boolean, desc: 'Do not allow any gem to be upgraded past most recent release (or minor if -m used). Sometimes raises VersionConflict.'
61
+ option :minor_allowed, type: :boolean, desc: 'Upgrade to the latest minor.release version.'
62
+ # TODO: be nice to support array w/o quotes like real `bundle update`
63
+ option :gems_to_update, type: :array, split: ' ', desc: 'Optional list of gems to update, in quotes, space delimited'
64
+ desc 'Update spec gems to the latest release version. Required gems could be upgraded to latest minor or major if necessary.'
65
+ config default_option: 'gems_to_update'
66
+
67
+ def update(options={}) # TODO: Revamp the commands now that we've broadened into security specific and generic
68
+ header
69
+ gem_patches = (options.delete(:gems_to_update) || []).map { |gem_name| GemPatch.new(gem_name: gem_name) }
70
+ conservative_update(gem_patches, options.merge(updating: true))
71
+ end
72
+
73
+ private
74
+
75
+ def header
76
+ puts "Bundler Patch Version #{Bundler::Patch::VERSION}"
77
+ end
78
+
79
+ def conservative_update(gem_patches, options={}, bundler_def=nil)
80
+ Bundler.ui = Bundler::UI::Shell.new
81
+
82
+ prep = DefinitionPrep.new(bundler_def, gem_patches, options).tap { |p| p.prep }
83
+
84
+ Bundler::Installer.install(Bundler.root, prep.bundler_def)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,85 @@
1
+ module Bundler::Patch
2
+ class UpdateSpec
3
+ attr_accessor :target_file, :target_dir, :regexes, :patched_versions
4
+
5
+ def initialize(target_file: '',
6
+ target_dir: Dir.pwd,
7
+ regexes: [/.*/],
8
+ patched_versions: [])
9
+ @target_file = target_file
10
+ @target_dir = target_dir
11
+ @regexes = regexes
12
+ @patched_versions = patched_versions
13
+ end
14
+
15
+ def target_path_fn
16
+ File.join(@target_dir, @target_file)
17
+ end
18
+
19
+ def calc_new_version(old_version)
20
+ old = old_version
21
+ all = @patched_versions.dup
22
+ return old_version if all.include?(old)
23
+
24
+ all << old
25
+ all.sort!
26
+ all.delete_if { |v| v.split(/\./).first != old.split(/\./).first } # strip non-matching major revs
27
+ all[all.index(old) + 1]
28
+ end
29
+
30
+ def file_replace
31
+ filename = target_path_fn
32
+ unless File.exist?(filename)
33
+ puts "Cannot find #{filename}"
34
+ return
35
+ end
36
+
37
+ guts = File.read(filename)
38
+ any_changes = false
39
+ [@regexes].flatten.each do |re|
40
+ any_changes = guts.gsub!(re) do |match|
41
+ if block_given?
42
+ yield match, re
43
+ else
44
+ update_to_new_version(match, re)
45
+ end
46
+ end || any_changes
47
+ end
48
+
49
+ if any_changes
50
+ File.open(filename, 'w') { |f| f.print guts }
51
+ verbose_puts "Updated #{filename}"
52
+ else
53
+ verbose_puts "No changes for #{filename}"
54
+ end
55
+ end
56
+
57
+ def update_to_new_version(match, re)
58
+ current_version = match.scan(re).join
59
+ new_version = calc_new_version(current_version)
60
+ if new_version
61
+ match.sub(current_version, new_version).tap { |s| puts "Updating to #{s}" }
62
+ else
63
+ match
64
+ end
65
+ end
66
+
67
+ alias_method :update, :file_replace
68
+
69
+ def verbose_puts(text)
70
+ puts text if @verbose
71
+ end
72
+ end
73
+ end
74
+
75
+ # def prep_git_checkout(spec)
76
+ # Dir.chdir(spec.target_dir) do
77
+ # status_first_line = `git status`.split("\n").first
78
+ # raise "Not on master: #{status_first_line}" unless status_first_line == '# On branch master'
79
+ #
80
+ # raise 'Uncommitted files' unless `git status --porcelain`.chomp.empty?
81
+ #
82
+ # verbose_puts `git pull`
83
+ # end
84
+ # end
85
+
@@ -0,0 +1,5 @@
1
+ module Bundler
2
+ module Patch
3
+ VERSION = '0.6.0'
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ require 'bundler'
2
+
3
+ module Bundler
4
+ module Patch
5
+ end
6
+ end
7
+
8
+ require 'bundler/patch/updater'
9
+ require 'bundler/patch/gemfile'
10
+ require 'bundler/patch/ruby_version'
11
+ require 'bundler/patch/advisory_consolidator'
12
+ require 'bundler/patch/conservative_definition'
13
+ require 'bundler/patch/conservative_resolver'
14
+ require 'bundler/patch/scanner'
15
+ require 'bundler/patch/version'
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bundler-patch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - chrismo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler-advise
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.3
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.3
33
+ - !ruby/object:Gem::Dependency
34
+ name: boson
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.10'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.10'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler-fixture
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.1'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.1'
75
+ - !ruby/object:Gem::Dependency
76
+ name: pry
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '10.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '10.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ description:
118
+ email:
119
+ - chrismo@clabs.org
120
+ executables:
121
+ - bundle-patch
122
+ extensions: []
123
+ extra_rdoc_files: []
124
+ files:
125
+ - ".gitignore"
126
+ - ".rspec"
127
+ - ".ruby-version"
128
+ - ".travis.yml"
129
+ - Gemfile
130
+ - LICENSE.txt
131
+ - README.md
132
+ - Rakefile
133
+ - bin/bundle-patch
134
+ - bin/console
135
+ - bin/setup
136
+ - bundler-patch.gemspec
137
+ - lib/bundler/patch.rb
138
+ - lib/bundler/patch/advisory_consolidator.rb
139
+ - lib/bundler/patch/conservative_definition.rb
140
+ - lib/bundler/patch/conservative_resolver.rb
141
+ - lib/bundler/patch/gemfile.rb
142
+ - lib/bundler/patch/ruby_version.rb
143
+ - lib/bundler/patch/scanner.rb
144
+ - lib/bundler/patch/updater.rb
145
+ - lib/bundler/patch/version.rb
146
+ homepage: https://github.com/livingsocial/bundler-patch
147
+ licenses:
148
+ - MIT
149
+ metadata: {}
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project:
166
+ rubygems_version: 2.4.5.1
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: Conservative bundler updates
170
+ test_files: []