bundler-patch 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d81e3f4667e6cb684ba7caf5ff5ab7778712964e
4
- data.tar.gz: f54482f58a5e12f47b740a9bc4dfe0992daa49ca
3
+ metadata.gz: e3c93c22ec89fe88cd7225e53b51f4367109dcc5
4
+ data.tar.gz: db75eca98eb9622a9299668d7c8d5e77868b73f5
5
5
  SHA512:
6
- metadata.gz: 419579e3bbbe1653877d8fe227995bdac95208141804eea933171108851ae317df0d4c5f2a5defbe82aff46342ddb10954c3330f543e0f9e787a805aac37f127
7
- data.tar.gz: 85f1d8f4b1d2b7cb316a62efb85f84fcf041552db77c97ee15c5adecb967514f9470c660c0c9bf2f76158639236ecb1d1ee36f731427abcf8920dff11b0d4dad
6
+ metadata.gz: 2cb458c1bfb71fd70fc1fc5145caa97c391aead8dddbc8fbc292de9ea310bdc47d3b15f5b63c0dc7325e6e53a502a2be0bfc5a5a7c6921aa653407d15e2920fa
7
+ data.tar.gz: 799ce18e825ddc8027b9ac06553874e094262f0f1d23951cf863b28fc402d4f5d1eb37f95c21fecf6d668f19730cd6810242eb533fd6e5241226f4d29d486589
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /pkg/
9
9
  /spec/reports/
10
10
  /tmp/
11
+ zz_debug_spec.rb
data/Gemfile CHANGED
@@ -1,5 +1,4 @@
1
1
  source 'https://rubygems.org'
2
- source 'https://gems.livingsocial.net'
3
2
 
4
3
  # Specify your gem's dependencies in bundler-patch.gemspec
5
4
  gemspec
data/README.md CHANGED
@@ -1,15 +1,12 @@
1
- # Bundler::Patch
1
+ # bundler-patch
2
2
 
3
- `bundler-patch` can update your Gemfile conservatively to deal with vulnerable gems or just get more current.
3
+ `bundler-patch` can update your gems conservatively to deal with vulnerable
4
+ gems or just get more current.
4
5
 
5
- ## Goals
6
-
7
- - Update the Gemfile, .ruby-version and other files to patch an app according to `ruby-advisory-db` content.
8
- - Provide conservative update of select or all gems. Conservative meaning to the latest release (default) or minor (optional) version.
9
- - Don't security patch past the minimum gem version required. (This may change).
10
- - Minimal munging to existing version spec.
11
- - Support a database of custom advisories for internal gems.
12
- - Provide reasonable support for keeping a large number of apps and services up-to-date as automatically as possible.
6
+ By default, "conservatively" means it will prefer the latest releases from the
7
+ current version, over the latest minor releases or the latest major releases.
8
+ This is somewhat opposite from `bundle update` which prefers newest/major
9
+ versions first.
13
10
 
14
11
  ## Installation
15
12
 
@@ -17,120 +14,206 @@
17
14
 
18
15
  ## Usage
19
16
 
20
- ### Scan / Patch Security Vulnerable Gems
17
+ With the `bundler-patch` binary available, both `bundler-patch` and `bundle
18
+ patch` can be used to execute.
19
+
20
+ Without any options, all gems will be conservatively updated. An attempt to
21
+ upgrade any vulnerable gem (according to
22
+ https://github.com/rubysec/ruby-advisory-db) to a patched version will be
23
+ made.
24
+
25
+ $ bundle patch
26
+
27
+ "Conservatively" means it will sort all available versions to prefer the
28
+ latest releases from the current version, then the latest minor releases and
29
+ then the latest major releases.
30
+
31
+ Gem requirements as defined in the Gemfile will restrict the available version
32
+ options.
33
+
34
+ For example, if gem 'foo' is locked at 1.0.2, with no gem requirement defined
35
+ in the Gemfile, and versions 1.0.3, 1.0.4, 1.1.0, 1.1.1, 2.0.0 all exist, the
36
+ default order of preference will be "1.0.4, 1.0.3, 1.0.2, 1.1.1, 1.1.0,
37
+ 2.0.0".
38
+
39
+ "Prefer" means that no available versions are removed from consideration*, to
40
+ help ensure a suitable dependency graph can be reconciled. This does mean some
41
+ gems cannot be upgraded or will be upgraded to unexpected versions.
21
42
 
22
- To output the list of detected vulnerabilities in the current project:
43
+ _*That's a white-lie. bundler-patch will actually remove from consideration
44
+ any versions older than the currently locked version, which `bundle update`
45
+ will not do. It's not common, but it is possible for `bundle update` to
46
+ regress a gem to an older version, if necessary to reconcile the dependency
47
+ graph._
23
48
 
24
- $ bundle-patch scan
49
+ With no gem names provided on the command line, all gems will be unlocked and
50
+ open for updating. A list of gem names can be passed to restrict to just those
51
+ gems.
25
52
 
26
- To specify the path to an optional advisory database:
53
+ $ bundle patch foo bar
27
54
 
28
- $ bundle-patch scan -a ~/.my-custom-db
55
+ * `-m/--minor_preferred` option will give preference for minor versions over
56
+ release versions.
29
57
 
30
- _*NOTE*: `gems` will be appended to the end of the provided advisory database path._
58
+ * `-p/--prefer_minimal` option will reverse the preference order within
59
+ release, minor, major groups to just 'the next' version. In the prior
60
+ example, the order of preference changes to "1.0.3, 1.0.4, 1.0.2, 1.1.0,
61
+ 1.1.1, 2.0.0"
31
62
 
32
- To attempt to patch the detected vulnerabilities, use the `patch` command instead of `scan`:
63
+ * `-s/--strict_updates` option will actually remove from consideration
64
+ versions outside either the current release (or minor version if `-m`
65
+ specified). This increases the chances of Bundler being unable to
66
+ reconcile the dependency graph and could raise a `VersionConflict`.
33
67
 
34
- $ bundle-patch patch
68
+ `bundler-patch` will also check for vulnerabilities based on the
69
+ `ruby-advisory-db`, but also will _modify_ (if necessary) the gem requirement
70
+ in the Gemfile on vulnerable gems to ensure they can be upgraded.
35
71
 
36
- Same options apply. Read the next section for details on how bumps to release, minor and major versions work.
72
+ * `-l/--list` option will just list vulnerable gems. No updates will be
73
+ performed.
37
74
 
38
- For help:
75
+ * `-a/--advisory_db_path` option can provide the path to an additional
76
+ custom ruby-advisory-db styled directory. The path should not include the
77
+ final `gems` directory, that will be appended automatically. This can be
78
+ used for flagging necessary updates for custom/internal gems.
39
79
 
40
- $ bundle-patch help scan
41
- $ bundle-patch help patch
80
+ The rules for updating vulnerable gems are almost identical to the general
81
+ `bundler-patch` behavior described above, and abide by the same options (`-m`,
82
+ `-p`, and `-s`) though there are some tweaks to encourage getting to at least
83
+ a patched version of the gem. Keep in mind Bundler may choose unexpected
84
+ versions in order to satisfy the dependency graph.
42
85
 
43
- ### Conservatively Update All Gems
86
+ * `-v/--vulnerable_gems_only` option will automatically restrict the gems
87
+ to update list to currently vulnerable gems. If a combination of `-v` and
88
+ a list of gem names are passed, the `-v` option is ignored in favor of
89
+ the listed gem names.
44
90
 
45
- To update any gem conservatively, use the `update` command:
46
91
 
47
- $ bundle-patch update 'foo bar'
92
+ ## Examples
48
93
 
49
- This will attempt to upgrade the `foo` and `bar` gems to the latest release version. (e.g. if the current version is
50
- `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
51
- dependent gems need to be upgraded to a new minor or even major version, then it will do those as well, presuming the
52
- gem requirements specified in the `Gemfile` also allow it.
94
+ ### Single Gem
53
95
 
54
- If you want to restrict _any_ gem from being upgraded past the most recent release version, use `--strict` mode:
96
+ | Requirements| Locked | Available | Options | Result |
97
+ |-------------|---------|-----------------------------|----------|--------|
98
+ | foo | 1.4.3 | 1.4.4, 1.4.5, 1.5.0, 1.5.1 | | 1.4.5 |
99
+ | foo | 1.4.3 | 1.4.4, 1.4.5, 1.5.0, 1.5.1 | -m | 1.5.1 |
100
+ | foo | 1.4.3 | 1.4.4, 1.4.5, 1.5.0, 1.5.1 | -p | 1.4.4 |
101
+ | foo | 1.4.3 | 1.4.4, 1.4.5, 1.5.0, 1.5.1 | -m -p | 1.5.0 |
55
102
 
56
- $ bundle-patch update 'foo bar' --strict
103
+ ### Two Gems
57
104
 
58
- This will eliminate any newer minor or major releases for any gem. If Bundler is unable to upgrade the requested gems
59
- due to the limitations, it will leave the requested gems at their current version.
105
+ Given the following gem specifications:
60
106
 
61
- 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`
62
- option.
107
+ - foo 1.4.3, requires: ~> bar 2.0
108
+ - foo 1.4.4, requires: ~> bar 2.0
109
+ - foo 1.4.5, requires: ~> bar 2.1
110
+ - foo 1.5.0, requires: ~> bar 2.1
111
+ - foo 1.5.1, requires: ~> bar 3.0
112
+ - bar with versions 2.0.3, 2.0.4, 2.1.0, 2.1.1, 3.0.0
63
113
 
64
- `--minor_allowed` (alias `-m`) and `--strict` (alias `-s`) can be used together or independently.
114
+ Gemfile:
65
115
 
66
- While `--minor_allowed` is most useful in combination with the `--strict` option, it can also influence behavior when
67
- not in strict mode.
116
+ gem 'foo'
68
117
 
69
- 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`
70
- available, then without `--minor_allowed` and without `--strict`, `foo` itself will only be upgraded to `1.4.8`, though
71
- any gems further down the dependency tree could be upgraded to a new minor or major version if they have to be to use
72
- `foo 1.4.8`.
118
+ Gemfile.lock:
73
119
 
74
- Continuing the example, _with_ `--minor_allowed` (but still without `--strict`) `foo` itself would be upgraded to
75
- `1.6.1`, and as before any gems further down the dependency tree could be upgraded to a new minor or major version if
76
- they have to.
120
+ foo (1.4.3)
121
+ bar (~> 2.0)
122
+ bar (2.0.3)
77
123
 
78
- To request conservative updates for the entire Gemfile, simply call `update`:
124
+ | # | Command Line | Result |
125
+ |---|--------------|---------------------------|
126
+ | 1 | | 'foo 1.4.5', 'bar 2.1.1' |
127
+ | 2 | foo | 'foo 1.4.4', 'bar 2.0.3' |
128
+ | 3 | -m | 'foo 1.5.1', 'bar 3.0.0' |
129
+ | 4 | -m -s | 'foo 1.5.0', 'bar 2.1.1' |
130
+ | 5 | -s | 'foo 1.4.4', 'bar 2.0.4' |
131
+ | 6 | -p | 'foo 1.4.4', 'bar 2.0.4' |
132
+ | 7 | -p -m | 'foo 1.5.0', 'bar 2.1.0' |
79
133
 
80
- $ bundle-patch update
134
+ In case 1, `bar` is upgraded to 2.1.0, a minor version increase, because the
135
+ dependency from `foo` 1.4.5 required it.
81
136
 
82
- There's no option to allow major version upgrades as this is the default behavior of `bundle update` in Bundler itself.
137
+ In case 2, only `foo` is unlocked, so `bar` can only go to 1.4.4 to satisfy
138
+ the dependency from `foo`.
139
+
140
+ In case 3, `bar` goes up a whole major release, because a minor increase is
141
+ preferred now for `foo`.
142
+
143
+ In case 4, `foo` is preferred up to a 1.5.x, but 1.5.1 won't work because the
144
+ strict `-s` flag removes `bar` 3.0.0 from consideration since it's a major
145
+ increment.
146
+
147
+ In case 5, both `foo` and `bar` have any minor or major increments removed
148
+ from consideration, so the most they can move is up to 1.4.4 and 2.0.4.
149
+
150
+ In case 6, the prefer minimal switch `-p` means they only increment to the
151
+ next available release.
152
+
153
+ In case 7, the `-p` and `-m` switches allow both to move to just the next
154
+ available minor version.
83
155
 
84
156
 
85
157
  ### Troubleshooting
86
158
 
87
- First tip: make sure the current `bundle` command runs to completion on its own without any problems.
159
+ First, make sure the current `bundle` command itself runs to completion on its
160
+ own without any problems.
88
161
 
89
- The most frequent problems with this tool involve expectations around what gems should or shouldn't be upgraded. This
90
- can quickly get complicated as even a small dependency tree can involve many moving parts, and Bundler works hard to
91
- find a combination that satisfies all of the dependencies and requirements.
162
+ The most frequent problems with this tool involve expectations around what
163
+ gems should or shouldn't be upgraded. This can quickly get complicated as even
164
+ a small dependency tree can involve many moving parts, and Bundler works hard
165
+ to find a combination that satisfies all of the dependencies and requirements.
92
166
 
93
- You can get a (very verbose) look into how Bundler's resolution algorithm is working by setting the `DEBUG_RESOLVER`
94
- environment variable. While it can be tricky to dig through, it should explain how it came to the conclusions it came
95
- to.
167
+ You can get a (very verbose) look into how Bundler's resolution algorithm is
168
+ working by setting the `DEBUG_RESOLVER` environment variable. While it can be
169
+ tricky to dig through, it should explain how it came to the conclusions it
170
+ came to.
96
171
 
97
- Adding to the usual Bundler complexity, `bundler-patch` is injecting its own logic to the resolution process to achieve
98
- its goals. If there's a bug involved, it's almost certainly in the `bundler-patch` code as Bundler has been around a
99
- long time and has thorough testing and real world experience.
172
+ Adding to the usual Bundler complexity, `bundler-patch` is injecting its own
173
+ logic to the resolution process to achieve its goals. If there's a bug
174
+ involved, it's almost certainly in the `bundler-patch` code as Bundler has
175
+ been around a long time and has thorough testing and real world experience.
100
176
 
101
- In particular, grep for 'Unwinding for conflict' to isolate some key issues that may be preventing the outcome you
102
- expect.
177
+ In particular, grep for 'Unwinding for conflict' to isolate some key issues
178
+ that may be preventing the outcome you expect.
103
179
 
104
- `bundler-patch` can dump its own debug output, potentially helpful, with `DEBUG_PATCH_RESOLVER`.
180
+ `bundler-patch` can dump its own debug output, potentially helpful, with
181
+ `DEBUG_PATCH_RESOLVER`.
105
182
 
106
- To get additional Bundler debugging output, enable the `DEBUG` env variable. This will include all of the details of
107
- the downloading the full dependency data from remote sources.
183
+ To get additional Bundler debugging output, enable the `DEBUG` env variable.
184
+ This will include all of the details of the downloading the full dependency
185
+ data from remote sources.
108
186
 
109
187
 
110
188
  ## Development
111
189
 
112
190
  ### Status
113
191
 
114
- 0.x versions are subject to breaking changes, there's a fair amount of experimenting going on and some future plans to
115
- not only revisit the command names but also investigate making this a proper Bundler plugin.
192
+ 0.x versions are subject to breaking changes, there's a fair amount of
193
+ experimenting going on.
116
194
 
117
- We'd love to get real world scenarios where things don't go as planned to help flesh out varying details of what many
118
- believe a conservative update should be.
195
+ We'd love to get real world scenarios where things don't go as planned to help
196
+ flesh out varying details of what many believe a conservative update should
197
+ be.
119
198
 
120
199
  ### How To
121
200
 
122
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
123
- also run `bin/console` for an interactive prompt that will allow you to experiment.
201
+ After checking out the repo, run `bin/setup` to install dependencies. Then,
202
+ run `rake spec` to run the tests. You can also run `bin/console` for an
203
+ interactive prompt that will allow you to experiment.
124
204
 
125
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
126
- version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
127
- push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
205
+ To install this gem onto your local machine, run `bundle exec rake install`.
206
+ To release a new version, update the version number in `version.rb`, and then
207
+ run `bundle exec rake release`, which will create a git tag for the version,
208
+ push git commits and tags, and push the `.gem` file to
209
+ [rubygems.org](https://rubygems.org).
128
210
 
129
211
  ## Contributing
130
212
 
131
- Bug reports and pull requests are welcome on GitHub at https://github.com/livingsocial/bundler-patch.
132
-
213
+ Bug reports and pull requests are welcome on GitHub at
214
+ https://github.com/livingsocial/bundler-patch.
133
215
 
134
216
  ## License
135
217
 
136
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
218
+ The gem is available as open source under the terms of the [MIT
219
+ License](http://opensource.org/licenses/MIT).
@@ -7,4 +7,4 @@ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
7
7
 
8
8
  require 'bundler/patch'
9
9
 
10
- Bundler::Patch::Scanner.start
10
+ Bundler::Patch::CLI.execute
@@ -16,14 +16,14 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
18
  spec.bindir = 'bin'
19
- spec.executables = ['bundle-patch']
19
+ spec.executables = ['bundler-patch']
20
20
  spec.require_paths = ['lib']
21
21
 
22
22
  spec.add_dependency 'bundler-advise', '~> 1.0', '>= 1.0.3'
23
- spec.add_dependency 'boson'
24
- spec.add_dependency 'bundler', '~> 1.10'
23
+ spec.add_dependency 'slop', '~> 4.0'
24
+ spec.add_dependency 'bundler', '~> 1.10.0' # TODO: does not work with 1.11.x yet
25
25
 
26
- spec.add_development_dependency 'bundler-fixture', '~> 1.1'
26
+ spec.add_development_dependency 'bundler-fixture', '~> 1.3'
27
27
  spec.add_development_dependency 'pry'
28
28
  spec.add_development_dependency 'rake', '~> 10.0'
29
29
  spec.add_development_dependency 'rspec'
@@ -40,7 +40,7 @@ module Bundler::Patch
40
40
  else
41
41
  GemPatch.new(gem_name: up_spec.gem_name, old_version: old_version, patched_versions: up_spec.patched_versions)
42
42
  end
43
- end.partition { |gp| !gp.new_version.nil? }
43
+ end
44
44
  end
45
45
 
46
46
  private
@@ -61,13 +61,27 @@ module Bundler::Patch
61
61
  end
62
62
 
63
63
  class GemPatch
64
+ include Comparable
65
+
64
66
  attr_reader :gem_name, :old_version, :new_version, :patched_versions
65
67
 
66
68
  def initialize(gem_name:, old_version: nil, new_version: nil, patched_versions: nil)
67
69
  @gem_name = gem_name
68
- @old_version = old_version
69
- @new_version = new_version
70
+ @old_version = Gem::Version.new(old_version) if old_version
71
+ @new_version = Gem::Version.new(new_version) if new_version
70
72
  @patched_versions = patched_versions
71
73
  end
74
+
75
+ def <=>(other)
76
+ self.gem_name <=> other.gem_name
77
+ end
78
+
79
+ def hash
80
+ @gem_name.hash
81
+ end
82
+
83
+ def eql?(other)
84
+ @gem_name.eql?(other.gem_name)
85
+ end
72
86
  end
73
87
  end
@@ -0,0 +1,112 @@
1
+ require 'bundler/advise'
2
+ require 'slop'
3
+
4
+ module Bundler::Patch
5
+ class CLI
6
+ def self.execute
7
+ opts = Slop.parse do |o|
8
+ o.banner = "Bundler Patch Version #{Bundler::Patch::VERSION}\nUsage: bundle patch [options] [gems_to_update]"
9
+ o.separator ''
10
+ o.separator 'bundler-patch attempts to update gems conservatively.'
11
+ o.separator ''
12
+ o.bool '-m', '--minor_preferred', 'Prefer update to the latest minor.release version.' # TODO: change to --minor_preferred
13
+ o.bool '-p', '--prefer_minimal', 'Prefer minimal version updates over most recent release (or minor if -m used).'
14
+ o.bool '-s', '--strict_updates', 'Restrict any gem to be upgraded past most recent release (or minor if -m used).'
15
+ o.bool '-l', '--list', 'List vulnerable gems and new version target. No updates will be performed.'
16
+ o.bool '-v', '--vulnerable_gems_only', 'Only update vulnerable gems.'
17
+ o.string '-a', '--advisory_db_path', 'Optional custom advisory db path. `gems` dir will be appended to this path.'
18
+ o.on('-h', 'Show this help') { show_help(o) }
19
+ o.on('--help', 'Show README.md') { show_readme }
20
+ end
21
+
22
+ show_readme if opts.arguments.include?('help')
23
+ options = opts.to_hash
24
+ options[:gems_to_update] = opts.arguments
25
+
26
+ CLI.new.patch(options)
27
+ end
28
+
29
+ def self.show_help(opts)
30
+ puts opts.to_s(prefix: ' ')
31
+ exit
32
+ end
33
+
34
+ def self.show_readme
35
+ Kernel.exec "less '#{File.expand_path('../../../../README.md', __FILE__)}'"
36
+ exit
37
+ end
38
+
39
+ def initialize
40
+ @no_vulns_message = 'No known vulnerabilities to update.'
41
+ end
42
+
43
+ def patch(options={})
44
+ Bundler.ui = Bundler::UI::Shell.new
45
+
46
+ return list(options) if options[:list]
47
+
48
+ _patch(options)
49
+ end
50
+
51
+ private
52
+
53
+ def conservative_update(gem_patches, options={}, bundler_def=nil)
54
+ prep = DefinitionPrep.new(bundler_def, gem_patches, options).tap { |p| p.prep }
55
+
56
+ # update => true is very important, otherwise without any Gemfile changes, the installer
57
+ # may end up concluding everything can be resolved locally, nothing is changing,
58
+ # and then nothing is done. lib/bundler/cli/update.rb also hard-codes this.
59
+ Bundler::Installer.install(Bundler.root, prep.bundler_def, {'update' => true})
60
+ end
61
+
62
+ def list(options)
63
+ gem_patches = AdvisoryConsolidator.new(options).vulnerable_gems
64
+
65
+ if gem_patches.empty?
66
+ Bundler.ui.info @no_vulns_message
67
+ else
68
+ Bundler.ui.info '' # extra line to separate from advisory db update text
69
+ Bundler.ui.info 'Detected vulnerabilities:'
70
+ Bundler.ui.info '-------------------------'
71
+ Bundler.ui.info gem_patches.map(&:to_s).uniq.sort.join("\n")
72
+ end
73
+ end
74
+
75
+ def _patch(options)
76
+ vulnerable_patches = AdvisoryConsolidator.new(options).patch_gemfile_and_get_gem_specs_to_patch
77
+ requested_patches = (options.delete(:gems_to_update) || []).map { |gem_name| GemPatch.new(gem_name: gem_name) }
78
+
79
+ all_gem_patches = GemsToPatchReconciler.new(vulnerable_patches, requested_patches).reconciled_patches
80
+ all_gem_patches.push(*vulnerable_patches) if options[:vulnerable_gems_only] && all_gem_patches.empty?
81
+
82
+ vulnerable_patches, warnings = vulnerable_patches.partition { |gp| !gp.new_version.nil? }
83
+
84
+ unless warnings.empty?
85
+ warnings.each do |gp|
86
+ Bundler.ui.warn "* Could not attempt upgrade for #{gp.gem_name} from #{gp.old_version} to any patched versions " \
87
+ + "#{gp.patched_versions.join(', ')}. Most often this is because a major version increment would be " \
88
+ + "required and it's safer for a major version increase to be done manually."
89
+ end
90
+ end
91
+
92
+ if vulnerable_patches.empty?
93
+ Bundler.ui.info @no_vulns_message
94
+ else
95
+ vulnerable_patches.each do |gp|
96
+ Bundler.ui.info "Attempting conservative update for vulnerable gem '#{gp.gem_name}': #{gp.old_version} => #{gp.new_version}"
97
+ end
98
+ end
99
+
100
+ if all_gem_patches.empty?
101
+ Bundler.ui.info 'Updating all gems conservatively.'
102
+ else
103
+ Bundler.ui.info "Updating '#{all_gem_patches.map(&:gem_name).join(' ')}' conservatively."
104
+ end
105
+ conservative_update(all_gem_patches, options)
106
+ end
107
+ end
108
+ end
109
+
110
+ if __FILE__ == $0
111
+ Bundler::Patch::CLI.execute
112
+ end
@@ -3,7 +3,7 @@ module Bundler::Patch
3
3
  attr_accessor :gems_to_update
4
4
 
5
5
  # pass-through options to ConservativeResolver
6
- attr_accessor :strict, :minor_allowed
6
+ attr_accessor :strict, :minor_preferred, :prefer_minimal
7
7
 
8
8
  # This copies more code than I'd like out of Bundler::Definition, but for now seems the least invasive way in.
9
9
  # Backing up and intervening into the creation of a Definition instance itself involves a lot more code, a lot
@@ -31,7 +31,8 @@ module Bundler::Patch
31
31
  resolver.gems_to_update = @gems_to_update
32
32
  resolver.locked_specs = locked_specs
33
33
  resolver.strict = @strict
34
- resolver.minor_allowed = @minor_allowed
34
+ resolver.minor_preferred = @minor_preferred
35
+ resolver.prefer_minimal = @prefer_minimal
35
36
  result = resolver.start(expanded_dependencies)
36
37
  spec_set = Bundler::SpecSet.new(result)
37
38
 
@@ -46,7 +47,7 @@ module Bundler::Patch
46
47
 
47
48
  def initialize(bundler_def, gem_patches, options)
48
49
  @bundler_def = bundler_def
49
- @gems_to_update = GemsToUpdate.new(gem_patches, options)
50
+ @gems_to_update = GemsToPatch.new(gem_patches)
50
51
  @options = options
51
52
  end
52
53
 
@@ -54,41 +55,39 @@ module Bundler::Patch
54
55
  @bundler_def ||= Bundler.definition(@gems_to_update.to_bundler_definition)
55
56
  @bundler_def.extend ConservativeDefinition
56
57
  @bundler_def.gems_to_update = @gems_to_update
57
- @bundler_def.strict = @options[:strict]
58
- @bundler_def.minor_allowed = @options[:minor_allowed]
58
+ @bundler_def.strict = @options[:strict_updates]
59
+ @bundler_def.minor_preferred = @options[:minor_preferred]
60
+ @bundler_def.prefer_minimal = @options[:prefer_minimal]
59
61
  fixup_empty_remotes if @gems_to_update.to_bundler_definition === true
60
62
  @bundler_def
61
63
  end
62
64
 
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.
65
+ # This came out a real-life case with sidekiq and sidekiq-pro where the sidekiq-pro gem is served from their gem
66
+ # server and depends on the open-source sidekiq gem served from rubygems.org, and when patching those, without
67
+ # the appropriate remotes being set in rubygems_aggregrate, it won't work.
67
68
  #
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).
69
+ # The underlying issue in Bundler 1.10 appears to be when the Definition constructor receives `true` as the
70
+ # `unlock` parameter, then @locked_sources is initialized to empty array, and the related rubygems_aggregrate
71
+ # source instance ends up with no @remotes set in it, which I think happens during converge_sources. Without
72
+ # those set, then the index will list no gem versions in some cases. (It was complicated enough to discover this
73
+ # patch, I haven't fully worked out the flaw, though I believe I recreated the problem with plain ol `bundle
74
+ # update`).
79
75
  def fixup_empty_remotes
76
+ STDERR.puts 'fixing empty remotes' if ENV['DEBUG_PATCH_RESOLVER']
80
77
  b_sources = @bundler_def.send(:sources)
81
78
  empty_remotes = b_sources.rubygems_sources.detect { |s| s.remotes.empty? }
79
+ STDERR.puts "empty_remotes: <#{empty_remotes}>" if ENV['DEBUG_PATCH_RESOLVER']
82
80
  empty_remotes.remotes.push(*b_sources.rubygems_remotes) if empty_remotes
81
+ empty_remotes = b_sources.rubygems_sources.detect { |s| s.remotes.empty? }
82
+ STDERR.puts "empty_remotes after fixed: <#{empty_remotes}>" if ENV['DEBUG_PATCH_RESOLVER']
83
83
  end
84
84
  end
85
85
 
86
- class GemsToUpdate
87
- attr_reader :gem_patches, :patching
86
+ class GemsToPatch
87
+ attr_reader :gem_patches
88
88
 
89
- def initialize(gem_patches, options={})
89
+ def initialize(gem_patches)
90
90
  @gem_patches = Array(gem_patches)
91
- @patching = options[:patching]
92
91
  end
93
92
 
94
93
  def to_bundler_definition
@@ -99,20 +98,12 @@ module Bundler::Patch
99
98
  @gem_patches.map(&:gem_name)
100
99
  end
101
100
 
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
101
  def gem_patch_for(gem_name)
111
102
  @gem_patches.detect { |gp| gp.gem_name == gem_name }
112
103
  end
113
104
 
114
105
  def unlocking_all?
115
- @patching || @gem_patches.empty?
106
+ @gem_patches.empty?
116
107
  end
117
108
 
118
109
  def unlocking_gem?(gem_name)
@@ -1,14 +1,14 @@
1
1
  module Bundler::Patch
2
2
  class ConservativeResolver < Bundler::Resolver
3
- attr_accessor :locked_specs, :gems_to_update, :strict, :minor_allowed
3
+ attr_accessor :locked_specs, :gems_to_update, :strict, :minor_preferred, :prefer_minimal
4
4
 
5
5
  def search_for(dependency)
6
6
  res = super(dependency)
7
7
 
8
8
  dep = dependency.dep unless dependency.is_a? Gem::Dependency
9
+
9
10
  @conservative_search_for ||= {}
10
- #@conservative_search_for[dep] ||= # TODO turning off caching allowed a real-world sample to work, dunno why yet.
11
- begin
11
+ res = @conservative_search_for[dep] ||= begin
12
12
  gem_name = dep.name
13
13
 
14
14
  # An Array per version returned, different entries for different platforms.
@@ -18,20 +18,14 @@ module Bundler::Patch
18
18
  (@strict ?
19
19
  filter_specs(res, locked_spec) :
20
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
21
+ STDERR.puts debug_format_result(dep, res).inspect if ENV['DEBUG_PATCH_RESOLVER']
33
22
  end
34
23
  end
24
+
25
+ # dup is important, in weird (large) cases Bundler will empty the result array corrupting the cache.
26
+ # Bundler itself doesn't have this problem because the super search_for does a select on its cached
27
+ # search results, effectively duping it.
28
+ res.dup
35
29
  end
36
30
 
37
31
  def debug_format_result(dep, res)
@@ -50,7 +44,7 @@ module Bundler::Patch
50
44
  gsv = gem_spec.version
51
45
  lsv = locked_spec.version
52
46
 
53
- must_match = @minor_allowed ? [0] : [0, 1]
47
+ must_match = @minor_preferred ? [0] : [0, 1]
54
48
 
55
49
  matches = must_match.map { |idx| gsv.segments[idx] == lsv.segments[idx] }
56
50
  (matches.uniq == [true]) ? gsv.send(:>=, lsv) : false
@@ -62,6 +56,7 @@ module Bundler::Patch
62
56
  sort_specs(res, locked_spec)
63
57
  end
64
58
 
59
+ # reminder: sort still filters anything older than locked version
65
60
  def sort_specs(specs, locked_spec)
66
61
  return specs unless locked_spec
67
62
  gem_name = locked_spec.name
@@ -72,33 +67,35 @@ module Bundler::Patch
72
67
  filtered.sort do |a, b|
73
68
  a_ver = a.first.version
74
69
  b_ver = b.first.version
70
+ gem_patch = @gems_to_update.gem_patch_for(gem_name)
71
+ new_version = gem_patch ? gem_patch.new_version : nil
75
72
  case
76
73
  when a_ver.segments[0] != b_ver.segments[0]
77
74
  b_ver <=> a_ver
78
- when !@minor_allowed && (a_ver.segments[1] != b_ver.segments[1])
75
+ when !@minor_preferred && (a_ver.segments[1] != b_ver.segments[1])
76
+ b_ver <=> a_ver
77
+ when @prefer_minimal && !@gems_to_update.unlocking_gem?(gem_name)
79
78
  b_ver <=> a_ver
80
- when @gems_to_update.patching_but_not_this_gem?(gem_name)
79
+ when @prefer_minimal && @gems_to_update.unlocking_gem?(gem_name) &&
80
+ (![a_ver, b_ver].include?(locked_version) &&
81
+ (!new_version || (new_version && a_ver >= new_version && b_ver >= new_version)))
81
82
  b_ver <=> a_ver
82
83
  else
83
84
  a_ver <=> b_ver
84
85
  end
85
86
  end.tap do |result|
86
87
  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
88
+ gem_patch = @gems_to_update.gem_patch_for(gem_name)
89
+ if gem_patch && gem_patch.new_version && @prefer_minimal
90
+ move_version_to_end(specs, gem_patch.new_version, result)
93
91
  end
94
92
  else
95
- # make sure the current locked version is last in list.
96
- swap_version_to_end(specs, locked_version, result)
93
+ move_version_to_end(specs, locked_version, result)
97
94
  end
98
95
  end
99
96
  end
100
97
 
101
- def swap_version_to_end(specs, version, result)
98
+ def move_version_to_end(specs, version, result)
102
99
  spec_group = specs.detect { |s| s.first.version.to_s == version.to_s }
103
100
  if spec_group
104
101
  result.reject! { |s| s.first.version.to_s === version.to_s }
@@ -0,0 +1,22 @@
1
+ class GemsToPatchReconciler
2
+ attr_reader :reconciled_patches
3
+
4
+ def initialize(vulnerable_patches, requested_patches=[])
5
+ @vulnerable_patches = vulnerable_patches
6
+ @requested_patches = requested_patches
7
+ reconcile
8
+ end
9
+
10
+ private
11
+
12
+ def reconcile
13
+ @reconciled_patches = []
14
+ unless @requested_patches.empty?
15
+ @vulnerable_patches.reject! { |gp| !@requested_patches.include?(gp) }
16
+ @reconciled_patches.push(*((@vulnerable_patches + @requested_patches).uniq))
17
+ end
18
+ end
19
+ end
20
+
21
+
22
+
@@ -1,5 +1,5 @@
1
1
  module Bundler
2
2
  module Patch
3
- VERSION = '0.6.1'
3
+ VERSION = '0.7.0'
4
4
  end
5
5
  end
data/lib/bundler/patch.rb CHANGED
@@ -11,5 +11,6 @@ require 'bundler/patch/ruby_version'
11
11
  require 'bundler/patch/advisory_consolidator'
12
12
  require 'bundler/patch/conservative_definition'
13
13
  require 'bundler/patch/conservative_resolver'
14
- require 'bundler/patch/scanner'
14
+ require 'bundler/patch/gems_to_patch_reconciler'
15
+ require 'bundler/patch/cli'
15
16
  require 'bundler/patch/version'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler-patch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - chrismo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-11 00:00:00.000000000 Z
11
+ date: 2016-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-advise
@@ -31,47 +31,47 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: 1.0.3
33
33
  - !ruby/object:Gem::Dependency
34
- name: boson
34
+ name: slop
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: '4.0'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0'
46
+ version: '4.0'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.10'
53
+ version: 1.10.0
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '1.10'
60
+ version: 1.10.0
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: bundler-fixture
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '1.1'
67
+ version: '1.3'
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '1.1'
74
+ version: '1.3'
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: pry
77
77
  requirement: !ruby/object:Gem::Requirement
@@ -118,7 +118,7 @@ description:
118
118
  email:
119
119
  - chrismo@clabs.org
120
120
  executables:
121
- - bundle-patch
121
+ - bundler-patch
122
122
  extensions: []
123
123
  extra_rdoc_files: []
124
124
  files:
@@ -130,17 +130,18 @@ files:
130
130
  - LICENSE.txt
131
131
  - README.md
132
132
  - Rakefile
133
- - bin/bundle-patch
133
+ - bin/bundler-patch
134
134
  - bin/console
135
135
  - bin/setup
136
136
  - bundler-patch.gemspec
137
137
  - lib/bundler/patch.rb
138
138
  - lib/bundler/patch/advisory_consolidator.rb
139
+ - lib/bundler/patch/cli.rb
139
140
  - lib/bundler/patch/conservative_definition.rb
140
141
  - lib/bundler/patch/conservative_resolver.rb
141
142
  - lib/bundler/patch/gemfile.rb
143
+ - lib/bundler/patch/gems_to_patch_reconciler.rb
142
144
  - lib/bundler/patch/ruby_version.rb
143
- - lib/bundler/patch/scanner.rb
144
145
  - lib/bundler/patch/updater.rb
145
146
  - lib/bundler/patch/version.rb
146
147
  homepage: https://github.com/livingsocial/bundler-patch
@@ -1,85 +0,0 @@
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
- puts gem_patches.map(&:to_s).uniq.sort.join("\n")
24
- end
25
- end
26
-
27
- 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.'
28
- option :minor_allowed, type: :boolean, desc: 'Upgrade to the latest minor.release version.'
29
- option :advisory_db_path, type: :string, desc: 'Optional custom advisory db path.'
30
- desc 'Scans current directory for known vulnerabilities and attempts to patch your files to fix them.'
31
-
32
- def patch(options={}) # TODO: Revamp the commands now that we've broadened into security specific and generic
33
- header
34
-
35
- gem_patches, warnings = AdvisoryConsolidator.new(options).patch_gemfile_and_get_gem_specs_to_patch
36
-
37
- unless warnings.empty?
38
- warnings.each do |gp|
39
- # TODO: Bundler.ui
40
- puts "* Could not attempt upgrade for #{gp.gem_name} from #{gp.old_version} to any patched versions " \
41
- + "#{gp.patched_versions.join(', ')}. Most often this is because a major version increment would be " \
42
- + "required and it's safer for a major version increase to be done manually."
43
- end
44
- end
45
-
46
- if gem_patches.empty?
47
- puts @no_vulns_message
48
- else
49
- gem_patches.each do |gp|
50
- puts "Attempting #{gp.gem_name}: #{gp.old_version} => #{gp.new_version}" # TODO: Bundler.ui
51
- end
52
-
53
- puts "Updating '#{gem_patches.map(&:gem_name).join(' ')}' to address vulnerabilities"
54
- conservative_update(gem_patches, options.merge(patching: true))
55
- end
56
- end
57
-
58
- 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.'
59
- option :minor_allowed, type: :boolean, desc: 'Upgrade to the latest minor.release version.'
60
- # TODO: be nice to support array w/o quotes like real `bundle update`
61
- option :gems_to_update, type: :array, split: ' ', desc: 'Optional list of gems to update, in quotes, space delimited'
62
- desc 'Update spec gems to the latest release version. Required gems could be upgraded to latest minor or major if necessary.'
63
- config default_option: 'gems_to_update'
64
-
65
- def update(options={}) # TODO: Revamp the commands now that we've broadened into security specific and generic
66
- header
67
- gem_patches = (options.delete(:gems_to_update) || []).map { |gem_name| GemPatch.new(gem_name: gem_name) }
68
- conservative_update(gem_patches, options.merge(updating: true))
69
- end
70
-
71
- private
72
-
73
- def header
74
- puts "Bundler Patch Version #{Bundler::Patch::VERSION}"
75
- end
76
-
77
- def conservative_update(gem_patches, options={}, bundler_def=nil)
78
- Bundler.ui = Bundler::UI::Shell.new
79
-
80
- prep = DefinitionPrep.new(bundler_def, gem_patches, options).tap { |p| p.prep }
81
-
82
- Bundler::Installer.install(Bundler.root, prep.bundler_def)
83
- end
84
- end
85
- end