bundler-patch 0.6.1 → 0.7.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 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