bundler-patch 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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: []