Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
#!/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