diff --git a/README.md b/README.md index 4aa3f7a6..eeb66ff2 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,16 @@ Patch-level verification for [bundler]. ## Features -* Checks for vulnerable versions of gems in `Gemfile.lock`. +* Checks for vulnerable versions of gems in `Gemfile.lock` or `gems.locked`. * Checks for insecure gem sources (`http://` and `git://`). * Allows ignoring certain advisories that have been manually worked around. * Prints advisory information. * Does not require a network connection. +* Supports both traditional (`Gemfile`/`Gemfile.lock`) and modern (`gems.rb`/`gems.locked`) file naming conventions. ## Synopsis -Audit a project's `Gemfile.lock`: +Audit a project's `Gemfile.lock` or `gems.locked`: $ bundle-audit Name: actionpack @@ -109,11 +110,11 @@ Update the [ruby-advisory-db] that `bundle audit` uses: create mode 100644 gems/wicked/OSVDB-98270.yml ruby-advisory-db: 64 advisories -Update the [ruby-advisory-db] and check `Gemfile.lock` (useful for CI runs): +Update the [ruby-advisory-db] and check `Gemfile.lock` or `gems.locked` (useful for CI runs): $ bundle-audit check --update -Checking the `Gemfile.lock` without updating the [ruby-advisory-db]: +Checking the `Gemfile.lock` or `gems.locked` without updating the [ruby-advisory-db]: $ bundle-audit check --no-update @@ -121,10 +122,14 @@ Ignore specific advisories: $ bundle-audit check --ignore OSVDB-108664 -Checking a custom `Gemfile.lock` file: +Checking a custom lock file: $ bundle-audit check --gemfile Gemfile.custom.lock +Or with gems.locked: + + $ bundle-audit check --gemfile gems.locked + Output the audit's results in JSON: $ bundle-audit check --format json diff --git a/gemspec.yml b/gemspec.yml index f6bc66a7..cb8e8524 100644 --- a/gemspec.yml +++ b/gemspec.yml @@ -6,6 +6,13 @@ authors: Postmodern email: postmodern.mod3@gmail.com homepage: https://github.com/rubysec/bundler-audit#readme +metadata: + documentation_uri: https://rubydoc.info/gems/bundler-audit + source_code_uri: https://github.com/rubysec/bundler-audit.rb + bug_tracker_uri: https://github.com/rubysec/bundler-audit.rb/issues + changelog_uri: https://github.com/rubysec/bundler-audit.rb/blob/master/ChangeLog.md + rubygems_mfa_required: 'true' + required_ruby_version: ">= 2.0.0" required_rubygems_version: ">= 1.8.0" diff --git a/lib/bundler/audit/cli.rb b/lib/bundler/audit/cli.rb index 5f496f11..76caeafe 100644 --- a/lib/bundler/audit/cli.rb +++ b/lib/bundler/audit/cli.rb @@ -33,7 +33,7 @@ class CLI < ::Thor default_task :check map '--version' => :version - desc 'check [DIR]', 'Checks the Gemfile.lock for insecure dependencies' + desc 'check [DIR]', 'Checks the Gemfile.lock/gems.locked for insecure dependencies' method_option :quiet, type: :boolean, aliases: '-q' method_option :verbose, type: :boolean, aliases: '-v' method_option :ignore, type: :array, aliases: '-i' @@ -42,8 +42,8 @@ class CLI < ::Thor default: Database::USER_PATH method_option :format, type: :string, default: 'text', aliases: '-F' method_option :config, type: :string, aliases: '-c', default: '.bundler-audit.yml' - method_option :gemfile_lock, type: :string, aliases: '-G', - default: 'Gemfile.lock' + method_option :gemfile_lock, type: :string, aliases: '-G', + desc: 'Path to the lock file (Gemfile.lock or gems.locked)' method_option :output, type: :string, aliases: '-o' def check(dir=Dir.pwd) diff --git a/lib/bundler/audit/scanner.rb b/lib/bundler/audit/scanner.rb index 731a93ee..0435fda5 100644 --- a/lib/bundler/audit/scanner.rb +++ b/lib/bundler/audit/scanner.rb @@ -32,7 +32,7 @@ module Bundler module Audit # - # Scans a `Gemfile.lock` for security issues. + # Scans a `Gemfile.lock` or `gems.locked` for security issues. # class Scanner @@ -44,7 +44,7 @@ class Scanner # Project root directory attr_reader :root - # The parsed `Gemfile.lock` from the project + # The parsed `Gemfile.lock` or `gems.locked` from the project # # @return [Bundler::LockfileParser] attr_reader :lockfile @@ -60,8 +60,8 @@ class Scanner # @param [String] root # The path to the project root. # - # @param [String] gemfile_lock - # Alternative name for the `Gemfile.lock` file. + # @param [String] lock_file + # Alternative name for the lock file (Gemfile.lock or gems.locked). # # @param [Database] database # The database to scan against. @@ -70,20 +70,35 @@ class Scanner # The file name of the bundler-audit config file. # # @raise [Bundler::GemfileLockNotFound] - # The `gemfile_lock` file could not be found within the `root` - # directory. + # The `lock_file` file could not be found within the `root` + # directory, or neither `Gemfile.lock` nor `gems.locked` exist + # within `root`. # - def initialize(root=Dir.pwd,gemfile_lock='Gemfile.lock',database=Database.new,config_dot_file='.bundler-audit.yml') + def initialize(root=Dir.pwd,lock_file=nil,database=Database.new,config_dot_file='.bundler-audit.yml') @root = File.expand_path(root) @database = database - gemfile_lock_path = File.join(@root,gemfile_lock) + if lock_file + lock_file_path = File.join(@root,lock_file) - unless File.file?(gemfile_lock_path) - raise(Bundler::GemfileLockNotFound,"Could not find #{gemfile_lock.inspect} in #{@root.inspect}") + unless File.file?(lock_file_path) + raise(Bundler::GemfileLockNotFound,"Could not find #{lock_file.inspect} in #{@root.inspect}") + end + else + unless (lock_file_path = detect_lock_file) + # Provide more helpful error message + gemfile_path = detect_gemfile + if gemfile_path + gemfile_name = File.basename(gemfile_path) + expected_lock_file = gemfile_name == 'gems.rb' ? 'gems.locked' : 'Gemfile.lock' + raise(Bundler::GemfileLockNotFound,"#{gemfile_name} found but #{expected_lock_file} is missing. Run 'bundle install' to generate it.") + else + raise(Bundler::GemfileLockNotFound,"neither Gemfile.lock nor gems.locked found in #{@root.inspect}") + end + end end - @lockfile = LockfileParser.new(File.read(gemfile_lock_path)) + @lockfile = LockfileParser.new(File.read(lock_file_path)) config_dot_file_full_path = File.absolute_path(config_dot_file, @root) @@ -235,6 +250,50 @@ def scan_specs(options={}) private + # Supported lock files. + LOCK_FILES = %w[Gemfile.lock gems.locked] + + # Supported gemfiles. + GEMFILES = %w[Gemfile gems.rb] + + # + # Detects `Gemfile.lock` or `gems.locked` files within {#root}. + # + # @return [String, nil] + # The path to `Gemfile.lock` or `gems.locked`. `nil` is returned + # if neither `Gemfile.lock` or `gems.locked` were found. + # + def detect_lock_file + LOCK_FILES.each do |name| + path = File.join(@root,name) + + if File.file?(path) + return path + end + end + + return nil + end + + # + # Detects `Gemfile` or `gems.rb` files within {#root}. + # + # @return [String, nil] + # The path to `Gemfile` or `gems.rb`. `nil` is returned + # if neither `Gemfile` or `gems.rb` were found. + # + def detect_gemfile + GEMFILES.each do |name| + path = File.join(@root,name) + + if File.file?(path) + return path + end + end + + return nil + end + # # Determines whether a source is internal. # diff --git a/lib/bundler/audit/version.rb b/lib/bundler/audit/version.rb index 8cb45c17..dc05bc09 100644 --- a/lib/bundler/audit/version.rb +++ b/lib/bundler/audit/version.rb @@ -18,6 +18,6 @@ module Bundler module Audit # bundler-audit version - VERSION = '0.9.0.1' + VERSION = '0.10.0' end end diff --git a/spec/cli_spec.rb b/spec/cli_spec.rb index c240b02d..be81617e 100644 --- a/spec/cli_spec.rb +++ b/spec/cli_spec.rb @@ -175,4 +175,70 @@ end end end -end + + describe "#check with gems.locked support" do + let(:fixtures_dir) { File.join(__dir__, 'fixtures') } + let(:bundle_dir) { File.join(fixtures_dir, 'gems_locked_cli_test') } + + before do + FileUtils.mkdir_p(bundle_dir) + File.write(File.join(bundle_dir, 'gems.locked'), <<~LOCKFILE) + GEM + remote: https://rubygems.org/ + specs: + thor (1.1.0) + + PLATFORMS + ruby + + DEPENDENCIES + thor + + BUNDLED WITH + 2.2.33 + LOCKFILE + end + + after do + FileUtils.rm_rf(bundle_dir) + end + + it "should auto-detect gems.locked file" do + cli = described_class.new + expect { + cli.check(bundle_dir) + }.to output(/thor/).to_stdout + end + + it "should work with explicit --gemfile-lock gems.locked" do + cli = described_class.new([], { gemfile_lock: 'gems.locked' }) + expect { + cli.check(bundle_dir) + }.to output(/thor/).to_stdout + end + end + + describe "#check with gems.rb support" do + let(:fixtures_dir) { File.join(__dir__, 'fixtures') } + let(:gems_rb_dir) { File.join(fixtures_dir, 'gems_rb_test') } + + before do + FileUtils.mkdir_p(gems_rb_dir) + File.write(File.join(gems_rb_dir, 'gems.rb'), <<~GEMFILE) + source 'https://rubygems.org' + gem 'thor' + GEMFILE + end + + after do + FileUtils.rm_rf(gems_rb_dir) + end + + it "should provide helpful error when gems.locked is missing" do + cli = described_class.new + expect { + cli.check(gems_rb_dir) + }.to output(/gems.rb found but gems.locked is missing/).to_stderr + end + end +end \ No newline at end of file diff --git a/spec/gems_locked_spec.rb b/spec/gems_locked_spec.rb new file mode 100644 index 00000000..485af436 --- /dev/null +++ b/spec/gems_locked_spec.rb @@ -0,0 +1,152 @@ +require 'spec_helper' +require 'bundler/audit/scanner' + +describe "gems.rb and gems.locked support" do + let(:fixtures_dir) { File.join(__dir__, 'fixtures') } + + describe Bundler::Audit::Scanner do + context "when only gems.locked exists" do + let(:bundle_dir) { File.join(fixtures_dir, 'gems_locked_only') } + + before do + FileUtils.mkdir_p(bundle_dir) + File.write(File.join(bundle_dir, 'gems.locked'), <<~LOCKFILE) + GEM + remote: https://rubygems.org/ + specs: + thor (1.1.0) + + PLATFORMS + ruby + + DEPENDENCIES + thor + + BUNDLED WITH + 2.2.33 + LOCKFILE + end + + after do + FileUtils.rm_rf(bundle_dir) + end + + it "should detect and use gems.locked file" do + scanner = described_class.new(bundle_dir) + expect(scanner.lockfile).to be_a(Bundler::LockfileParser) + expect(scanner.lockfile.specs.map(&:name)).to include('thor') + end + end + + context "when both Gemfile.lock and gems.locked exist" do + let(:bundle_dir) { File.join(fixtures_dir, 'both_lock_files') } + + before do + FileUtils.mkdir_p(bundle_dir) + File.write(File.join(bundle_dir, 'Gemfile.lock'), <<~LOCKFILE) + GEM + remote: https://rubygems.org/ + specs: + rake (13.0.6) + + PLATFORMS + ruby + + DEPENDENCIES + rake + + BUNDLED WITH + 2.2.33 + LOCKFILE + + File.write(File.join(bundle_dir, 'gems.locked'), <<~LOCKFILE) + GEM + remote: https://rubygems.org/ + specs: + thor (1.1.0) + + PLATFORMS + ruby + + DEPENDENCIES + thor + + BUNDLED WITH + 2.2.33 + LOCKFILE + end + + after do + FileUtils.rm_rf(bundle_dir) + end + + it "should prioritize Gemfile.lock over gems.locked" do + scanner = described_class.new(bundle_dir) + expect(scanner.lockfile.specs.map(&:name)).to include('rake') + expect(scanner.lockfile.specs.map(&:name)).not_to include('thor') + end + end + + context "when gems.rb exists but gems.locked is missing" do + let(:bundle_dir) { File.join(fixtures_dir, 'gems_rb_no_lock') } + + before do + FileUtils.mkdir_p(bundle_dir) + File.write(File.join(bundle_dir, 'gems.rb'), <<~GEMFILE) + source 'https://rubygems.org' + gem 'thor' + GEMFILE + end + + after do + FileUtils.rm_rf(bundle_dir) + end + + it "should provide helpful error message" do + expect { + described_class.new(bundle_dir) + }.to raise_error(Bundler::GemfileLockNotFound, /gems.rb found but gems.locked is missing/) + end + end + + context "when Gemfile exists but Gemfile.lock is missing" do + let(:bundle_dir) { File.join(fixtures_dir, 'gemfile_no_lock') } + + before do + FileUtils.mkdir_p(bundle_dir) + File.write(File.join(bundle_dir, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem 'thor' + GEMFILE + end + + after do + FileUtils.rm_rf(bundle_dir) + end + + it "should provide helpful error message" do + expect { + described_class.new(bundle_dir) + }.to raise_error(Bundler::GemfileLockNotFound, /Gemfile found but Gemfile.lock is missing/) + end + end + + context "when neither gemfile nor lock files exist" do + let(:bundle_dir) { File.join(fixtures_dir, 'empty_dir') } + + before do + FileUtils.mkdir_p(bundle_dir) + end + + after do + FileUtils.rm_rf(bundle_dir) + end + + it "should provide standard error message" do + expect { + described_class.new(bundle_dir) + }.to raise_error(Bundler::GemfileLockNotFound, /neither Gemfile.lock nor gems.locked found/) + end + end + end +end \ No newline at end of file