-
-
Save yb66/44b97baecbeec6900e039ffb9461d31e to your computer and use it in GitHub Desktop.
Check gems that can be affected by http://blog.rubygems.org/2016/04/06/gem-replacement-vulnerability-and-mitigation.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'optparse' | |
require 'pathname' | |
require 'psych' | |
options = {} | |
optparse = OptionParser.new do |opts| | |
opts.banner = <<-banner_script | |
This script checks for gems that could be insecure. | |
See http://blog.rubygems.org/2016/04/06/gem-replacement-vulnerability-and-mitigation.html for more. | |
Usage: | |
ruby check_gems.rb [OPTIONS] [DIRECTORIES] | |
The directories are for checking Gemfile.locks, otherwise | |
the current ruby's gems are checked. Gems without a hyphen | |
are skipped, so don't be surprised to get empty results, | |
it just means that Gemfile.lock has no gems with hyphens! | |
Example: | |
ruby check_gems.rb ~/Projects/Sinatra-Partial ~/Projects/sinatra-disqus | |
banner_script | |
opts.on( "-a", "--asap", "Optional. Show the results as soon as they arrive. Default is to wait until exit." ) do |a| | |
options[:asap] = a || false | |
end | |
opts.on( "-q", '--[no-]quiet', "Optional. Run quietly." ) do |q| | |
options[:quiet] = q | |
end | |
opts.on( "-c", '--[no-]cache', "Optional. Use the cache file located at ~/.check-gems-cache.yml. Using the cache is the default." ) do |c| | |
options[:cache] = c | |
end | |
opts.on( '-h', '--help', 'Display this screen') do | |
warn opts | |
exit 1 | |
end | |
end | |
optparse.parse! | |
#http://blog.rubygems.org/2016/04/06/gem-replacement-vulnerability-and-mitigation.html | |
require 'rest-client' | |
require 'bundler' | |
require 'time' | |
require 'json' | |
COLOURS = { | |
:standout => `tput smso`, | |
:normal => `tput sgr0`, | |
:black => `tput setaf 0`, | |
:red => `tput setaf 1`, | |
:green => `tput setaf 2`, | |
:yellow => `tput setaf 3`, | |
:blue => `tput setaf 4`, | |
:magenta => `tput setaf 5`, | |
:cyan => `tput setaf 6`, | |
:white => `tput setaf 7`, | |
} | |
STATUSES = { | |
:not_found => " #{COLOURS[:yellow]}[NOT FOUND]#{COLOURS[:normal]}", | |
:unverified => " #{COLOURS[:red]}[UNVERIFIED]#{COLOURS[:normal]}", | |
:safe => " #{COLOURS[:green]}[SAFE]#{COLOURS[:normal]}", | |
} | |
CHECK_GEMS_BEFORE = Time.parse("Feb 9, 2015") | |
NOT_FOUND = | |
UNVERIFIED = | |
SAFE = | |
class CheckGems | |
def initialize( quiet: false, asap: false, cache: true, dirs: nil) | |
@quiet = quiet | |
@results = {} | |
@asap = asap | |
@root = Pathname(__dir__).realpath | |
if cache | |
@cache_file = Pathname(ENV["HOME"]).join(".check-gems-cache.yml") | |
if @cache_file.exist? | |
@cache = Psych.load_file @cache_file | |
end | |
at_exit do | |
IO.write @cache_file, Psych.dump( self.cache ) | |
end | |
end | |
@cache ||= {} | |
@dirs = | |
if dirs.nil? || dirs.empty? | |
[Pathname(__dir__)] | |
else | |
dirs.map{|d| Pathname(d) } | |
end | |
@dirs.keep_if{|d| d.directory? } | |
@dirs.each {|dir| @results[dir.to_s] = {} } | |
unless @asap | |
at_exit do | |
puts "Done!\n\n" unless @quiet | |
self.results.each do |dir,rs| | |
puts "\n#{dir.to_s}" | |
rs.each do |gem_ver,status| | |
output gem_ver, status | |
end | |
end | |
end | |
end | |
end | |
attr_reader :results, :asap, :quiet, :cache | |
def run! | |
puts "Running…" unless @quiet | |
@dirs.each do |dir| | |
Dir.chdir dir | |
@current_dir = dir.to_s | |
if dir.join("Gemfile.lock").exist? | |
check_bundler | |
else | |
check_local_gems | |
end | |
Dir.chdir @root | |
@current_dir = nil | |
end | |
end | |
def output gem_ver, status | |
_status = STATUSES[status] | |
puts "\t#{gem_ver} #{_status}" | |
end | |
def add_result gem_ver, status | |
@results[@current_dir][gem_ver] = status | |
@cache[gem_ver] ||= status | |
output gem_ver, status if @asap | |
end | |
def check_bundler | |
gems = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock")) | |
gems.specs.each do |spec| | |
gem_name = spec.name | |
version = spec.version.to_s | |
next unless spec.source.kind_of?(Bundler::Source::Rubygems) | |
rubygems = spec.source.remotes.find {|r| r.to_s =~ /rubygems\.org/ } | |
if gem_name !~ /-/ || rubygems == nil | |
next | |
end | |
check_gem(gem_name, version) | |
end | |
end | |
def check_local_gems | |
Gem::Specification.each do |gem| | |
if gem.name =~ /-/ | |
check_gem(gem.name, gem.version.to_s) | |
end | |
end | |
end | |
def check_gem(gem_name, version) | |
gem_ver = "#{gem_name} #{version}" | |
print "." unless @quiet || @asap | |
if status = @cache[gem_ver] | |
add_result gem_ver, status | |
return | |
end | |
response = RestClient.get("https://rubygems.org/api/v1/versions/#{gem_name}.json") {|r| r } | |
if response.code != 200 | |
add_result gem_ver, :not_found | |
return | |
end | |
gem_data = JSON.parse(response) | |
gem = gem_data.find {|gd| gd["number"] == version } | |
if !gem | |
add_result gem_ver, :not_found | |
return | |
end | |
created_at = Time.parse(gem["created_at"]) | |
if created_at < CHECK_GEMS_BEFORE | |
add_result gem_ver, :unverified | |
else | |
add_result gem_ver, :safe | |
end | |
end | |
end | |
checker = CheckGems.new **options, dirs: ARGV | |
trap("INT") do | |
print "\r" | |
puts "Shutting down..." unless checker.quiet | |
puts "Gems checked up to this point will be displayed" unless checker.quiet || checker.asap | |
exit | |
end | |
checker.run! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment