codeclimate/codeclimate-bundler-audit

View on GitHub
lib/cc/engine/bundler_audit/analyzer.rb

Summary

Maintainability
A
55 mins
Test Coverage
require "tmpdir"

module CC
  module Engine
    module BundlerAudit
      class Analyzer
        GemfileLockNotFound = Class.new(StandardError)
        DEFAULT_CONFIG_PATH = "/config.json".freeze
        GEMFILE = "Gemfile".freeze
        GEMFILE_LOCK = "Gemfile.lock".freeze

        def initialize(directory:, engine_config_path: DEFAULT_CONFIG_PATH, stdout: STDOUT, stderr: STDERR)
          @directory = directory
          @engine_config_path = engine_config_path
          @stdout = stdout
          @stderr = stderr
        end

        def run
          raise(GemfileLockNotFound, "No Gemfile.lock found.") unless gemfile_lock_exists?
          return unless gemfile_lock_in_include_paths?

          Dir.mktmpdir do |dir|
            FileUtils.cp(gemfile_lock_path, File.join(dir, GEMFILE_LOCK))
            FileUtils.cp(gemfile_path, File.join(dir, GEMFILE))

            Dir.chdir(dir) do
              Bundler::Audit::Scanner.new.scan(ignore: ignored_advisories) do |vulnerability|
                if (issue = issue_for_vulerability(vulnerability))
                  stdout.print("#{issue.to_json}\0")
                else
                  stderr.print("Unsupported vulnerability: #{vulnerability.class.name}")
                end
              end
            end
          end
        end

        private

        attr_reader :directory, :engine_config_path, :stdout, :stderr

        def issue_for_vulerability(vulnerability)
          case vulnerability
          when Bundler::Audit::Scanner::UnpatchedGem
            UnpatchedGemIssue.new(vulnerability, gemfile_lock_relative_path, gemfile_lock_lines)
          when Bundler::Audit::Scanner::InsecureSource
            InsecureSourceIssue.new(vulnerability, gemfile_lock_relative_path, gemfile_lock_lines)
          end
        end

        def gemfile_lock_lines
          # N.B. this runs within the temporary directory, where the lock file
          # has been moved to ./Gemfile.lock so the scan will work.
          @gemfile_lock_lines ||= File.open(GEMFILE_LOCK).each_line.to_a
        end

        def gemfile_lock_exists?
          File.exist?(gemfile_lock_path)
        end

        def gemfile_lock_in_include_paths?
          include_paths = engine_config.fetch("include_paths", ["./"])
          include_paths.include?("./") || include_paths.include?(gemfile_lock_relative_path)
        end

        def engine_config
          @engine_config ||=
            if File.exist?(engine_config_path)
              JSON.parse(File.read(engine_config_path))
            else
              {}
            end
        end

        def ignored_advisories
          engine_config.fetch("config", {}).fetch("ignore", [])
        end

        def gemfile_lock_path
          File.join(directory, gemfile_lock_relative_path)
        end

        def gemfile_lock_relative_path
          engine_config.fetch("config", {}).fetch("path", GEMFILE_LOCK)
        end

        def gemfile_path
          File.join(directory, gemfile_relative_path)
        end

        def gemfile_relative_path
          gemfile_lock_relative_path.sub(/\.lock$/, "")
        end
      end
    end
  end
end