Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include Ruby advisories in health score #3

Merged
merged 3 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/polariscope/scanner/gemfile_health_score.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require 'set'
require_relative 'gem_versions'
require_relative 'gem_health_score'
require_relative 'ruby_scanner'

module Polariscope
module Scanner
Expand All @@ -30,6 +31,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
update_audit_database: false, bundler_audit_config_path: ''
)
@lockfile_parser = Bundler::LockfileParser.new(gemfile_lock_content)
@ruby_scanner = RubyScanner.new(@lockfile_parser)
@gemfile_path = gemfile_path
@dependencies = installed_dependencies
@gem_priorities = gem_priorities
Expand All @@ -54,6 +56,7 @@ def health_score

attr_reader :dependencies
attr_reader :lockfile_parser
attr_reader :ruby_scanner
attr_reader :advisory_penalty_map
attr_reader :fallback_advisory_penalty
attr_reader :bundler_audit_config_path
Expand Down Expand Up @@ -127,6 +130,7 @@ def advisories

lockfile_parser.specs
.flat_map { |gem| database.check_gem(gem).to_a }
.concat(ruby_scanner.vulnerable_advisories)
.reject { |advisory| ignored_advisories.intersect?(advisory.identifiers.to_set) }
end

Expand Down
45 changes: 45 additions & 0 deletions lib/polariscope/scanner/ruby_scanner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

require 'bundler'
require 'bundler/audit/database'

module Polariscope
module Scanner
class RubyScanner
def initialize(lockfile_parser)
@lockfile_parser = lockfile_parser
end

def version
lockfile_ruby_version&.gem_version
end

def vulnerable_advisories
version ? advisories.select { |a| a.vulnerable?(version) } : []
end

private

attr_reader :lockfile_parser
attr_reader :bundler_audit_database

def advisories
cve_paths.map { |path| Bundler::Audit::Advisory.load(path) }
end

# see https://github.com/rubysec/ruby-advisory-db?tab=readme-ov-file#directory-structure
# and https://github.com/rubysec/bundler-audit/blob/da0eff072a9521dc2995483a8978d5a7dd4e328a/lib/bundler/audit/database.rb#L364
def cve_paths
Dir.glob(File.join(Bundler::Audit::Database.path, 'rubies', engine, '*.yml'))
end

def engine
lockfile_ruby_version.engine
end

def lockfile_ruby_version
@lockfile_ruby_version ||= Bundler::RubyVersion.from_string(@lockfile_parser.ruby_version)
end
end
end
end
199 changes: 199 additions & 0 deletions spec/files/gemfile.lock_with_ruby_version
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.0)
actionpack (= 7.0.0)
activesupport (= 7.0.0)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.0)
actionpack (= 7.0.0)
activejob (= 7.0.0)
activerecord (= 7.0.0)
activestorage (= 7.0.0)
activesupport (= 7.0.0)
mail (>= 2.7.1)
actionmailer (7.0.0)
actionpack (= 7.0.0)
actionview (= 7.0.0)
activejob (= 7.0.0)
activesupport (= 7.0.0)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (7.0.0)
actionview (= 7.0.0)
activesupport (= 7.0.0)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.0)
actionpack (= 7.0.0)
activerecord (= 7.0.0)
activestorage (= 7.0.0)
activesupport (= 7.0.0)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.0)
activesupport (= 7.0.0)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.0)
activesupport (= 7.0.0)
globalid (>= 0.3.6)
activemodel (7.0.0)
activesupport (= 7.0.0)
activerecord (7.0.0)
activemodel (= 7.0.0)
activesupport (= 7.0.0)
activestorage (7.0.0)
actionpack (= 7.0.0)
activejob (= 7.0.0)
activerecord (= 7.0.0)
activesupport (= 7.0.0)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
builder (3.3.0)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
content_disposition (1.0.0)
crass (1.0.6)
date (3.3.4)
diff-lcs (1.5.1)
down (5.4.2)
addressable (~> 2.8)
erubi (1.13.0)
globalid (1.2.1)
activesupport (>= 6.1)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.24.1)
net-imap (0.4.14)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.0)
net-protocol
nio4r (2.7.3)
nokogiri (1.16.7-aarch64-linux)
racc (~> 1.4)
nokogiri (1.16.7-arm-linux)
racc (~> 1.4)
nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86-linux)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4)
public_suffix (6.0.1)
racc (1.8.1)
rack (2.2.9)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.0)
actioncable (= 7.0.0)
actionmailbox (= 7.0.0)
actionmailer (= 7.0.0)
actionpack (= 7.0.0)
actiontext (= 7.0.0)
actionview (= 7.0.0)
activejob (= 7.0.0)
activemodel (= 7.0.0)
activerecord (= 7.0.0)
activestorage (= 7.0.0)
activesupport (= 7.0.0)
bundler (>= 1.15.0)
railties (= 7.0.0)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.0)
actionpack (= 7.0.0)
activesupport (= 7.0.0)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rake (13.2.1)
redis (4.8.1)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (5.1.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
rspec-core (~> 3.10)
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.13.1)
shrine (3.6.0)
content_disposition (~> 1.0)
down (~> 5.1)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
thor (1.3.1)
timeout (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.6.17)

PLATFORMS
aarch64-linux
arm-linux
arm64-darwin
x86-linux
x86_64-darwin
x86_64-linux

DEPENDENCIES
rails (~> 7.0.0.0)
rspec-rails (~> 5)
shrine
sidekiq (~> 6)

RUBY VERSION
ruby 3.0.0p100

BUNDLED WITH
2.5.17
14 changes: 14 additions & 0 deletions spec/files/gemfile_with_ruby_version
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true
lovro-bikic marked this conversation as resolved.
Show resolved Hide resolved

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.0.0'

gem 'rails', '~> 7.0.0.0'
gem 'shrine'
gem 'sidekiq', '~> 6'

group :development, :test do
gem 'rspec-rails', '~> 5'
end
46 changes: 33 additions & 13 deletions spec/lib/polariscope/scanner/gemfile_health_score_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,43 @@
it { is_expected.to be_nil }
end

context 'when dependencies found and some new versions ignored' do
let(:gemfile_path) { File.join(Dir.pwd, 'spec/files/gemfile_with_dependencies') }
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_dependencies') }
let(:bundler_audit_config_path) { File.join(Dir.pwd, 'spec/files/bundler-audit') }
# these tests run in the order they're defined and assert that each next test
# has a score lower than the previous one
describe 'scores', order: :defined do
last_score = nil

it 'returns a score' do
expect(score).to be <= 43.32
context 'when dependencies found and some new versions ignored' do
let(:gemfile_path) { File.join(Dir.pwd, 'spec/files/gemfile_with_dependencies') }
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_dependencies') }
let(:bundler_audit_config_path) { File.join(Dir.pwd, 'spec/files/bundler-audit') }

it 'returns a score' do
expect(score).to be <= 43.32
cilim marked this conversation as resolved.
Show resolved Hide resolved

last_score = score
end
end
end

context 'when dependencies found and no new version is ignored' do
let(:gemfile_path) { File.join(Dir.pwd, 'spec/files/gemfile_with_dependencies') }
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_dependencies') }
let(:bundler_audit_config_path) { '' }
context 'when dependencies found and no new version is ignored' do
let(:gemfile_path) { File.join(Dir.pwd, 'spec/files/gemfile_with_dependencies') }
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_dependencies') }
let(:bundler_audit_config_path) { '' }

it 'returns a score' do
expect(score).to be < last_score

last_score = score
end
end

context 'when Gemfile.lock has a Ruby version' do
let(:gemfile_path) { File.join(Dir.pwd, 'spec/files/gemfile_with_ruby_version') }
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_ruby_version') }
let(:bundler_audit_config_path) { '' }

it 'returns a score' do
expect(score).to be <= 42.96
it 'returns a score' do
expect(score).to be < last_score
end
end
end

Expand Down
44 changes: 44 additions & 0 deletions spec/lib/polariscope/scanner/ruby_scanner_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

RSpec.describe Polariscope::Scanner::RubyScanner do
subject(:scanner) { described_class.new(lockfile_parser) }

let(:lockfile_parser) { Bundler::LockfileParser.new(gemfile_lock_content) }

describe '#version' do
context 'when Gemfile.lock has Ruby version information' do
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_ruby_version') }

it 'returns Ruby version' do
expect(scanner.version).to eq(Gem::Version.new('3.0.0'))
end
end

context "when Gemfile.lock doesn't have Ruby version information" do
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_no_dependencies') }

it 'returns nil' do
expect(scanner.version).to be_nil
end
end
end

describe '#vulnerable_advisories' do
context 'when Gemfile.lock has Ruby version information' do
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_ruby_version') }

it 'returns relevant advisories' do
expect(scanner.vulnerable_advisories).not_to be_empty
expect(scanner.vulnerable_advisories.map(&:class).uniq).to eq([Bundler::Audit::Advisory])
end
end

context "when Gemfile.lock doesn't have Ruby version information" do
let(:gemfile_lock_content) { File.read('spec/files/gemfile.lock_with_no_dependencies') }

it 'returns an empty array' do
expect(scanner.vulnerable_advisories).to eq([])
end
end
end
end