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

Refactor code #4

Merged
merged 21 commits into from
Oct 25, 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
5 changes: 1 addition & 4 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,5 @@ Style/FrozenStringLiteralComment:
Exclude:
- 'exe/polariscope'

Rails/TimeZone:
Enabled: false

Rails/Date:
Rails:
Enabled: false
16 changes: 8 additions & 8 deletions lib/polariscope.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# frozen_string_literal: true

require_relative 'polariscope/version'
require_relative 'polariscope/scanner/codebase_health_score'
require_relative 'polariscope/scanner/gemfile_health_score'
require_relative 'polariscope/scanner/gem_versions'
require_relative 'polariscope/file_content'

module Polariscope
Error = Class.new(StandardError)

class << self
def scan(gemfile_content: nil, gemfile_lock_content: nil, bundler_audit_config_content: nil)
Scanner::CodebaseHealthScore.new(
gemfile_content: gemfile_content || FileContent.for('Gemfile'),
gemfile_lock_content: gemfile_lock_content || FileContent.for('Gemfile.lock'),
bundler_audit_config_content: bundler_audit_config_content || FileContent.for('.bundler-audit.yml')
).health_score
def scan(**opts)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change will enable us to support command-line arguments for all configuration options later on.

Scanner::GemfileHealthScore.new(**opts.merge(
gemfile_content: opts.fetch(:gemfile_content, FileContent.for('Gemfile')),
gemfile_lock_content: opts.fetch(:gemfile_lock_content, FileContent.for('Gemfile.lock')),
bundler_audit_config_content: opts.fetch(:bundler_audit_config_content, FileContent.for('.bundler-audit.yml'))
)).health_score
end

def gem_versions(dependency_names, spec_type: :released)
def gem_versions(dependency_names, spec_type: Scanner::DependencyContext::DEFAULT_SPEC_TYPE)
Scanner::GemVersions.new(dependency_names, spec_type: spec_type)
end
end
Expand Down
28 changes: 28 additions & 0 deletions lib/polariscope/scanner/advisories_health_score.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Polariscope
module Scanner
class AdvisoriesHealthScore
def initialize(dependency_context, calculation_context)
@dependency_context = dependency_context
@calculation_context = calculation_context
end

def health_score
(1 + advisories_penalty)**-Math.log(calculation_context.advisory_severity)
end

private

attr_reader :dependency_context
attr_reader :calculation_context

def advisories_penalty
dependency_context
.advisories
.map(&:criticality)
.sum { |criticality| calculation_context.advisory_penalty_for(criticality) }
end
end
end
end
35 changes: 35 additions & 0 deletions lib/polariscope/scanner/audit_database.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

require 'bundler/audit/database'

module Polariscope
module Scanner
module AuditDatabase
extend self

ONE_DAY = 24 * 60 * 60

def update_if_necessary
update_audit_database! if database_outdated?
end

private

def update_audit_database!
Bundler::Audit::Database.update!(quiet: true)
end

def database_outdated?
audit_db_missing? || audit_db_stale?
end

def audit_db_missing?
!Bundler::Audit::Database.exists?
end

def audit_db_stale?
((Time.now - Bundler::Audit::Database.new.last_updated_at) / ONE_DAY) > 1.0
end
Comment on lines +30 to +32
Copy link
Member Author

@lovro-bikic lovro-bikic Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic was changed for this one to update the DB if it's stale more than one day instead of previously more than seven.

end
end
end
71 changes: 71 additions & 0 deletions lib/polariscope/scanner/calculation_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

module Polariscope
module Scanner
class CalculationContext
DEPENDENCY_PRIORITIES = { rails: 10.0 }.freeze
GROUP_PRIORITIES = { default: 2.0, production: 2.0 }.freeze
DEFAULT_DEPENDENCY_PRIORITY = 1.0

ADVISORY_SEVERITY = 1.09
ADVISORY_PENALTIES = {
none: 0.0,
low: 0.5,
medium: 1.0,
high: 3.0,
critical: 5.0
}.freeze
FALLBACK_ADVISORY_PENALTY = 0.5

MAJOR_VERSION_PENALTY = 1
NEW_VERSIONS_SEVERITY = 1.07
SEGMENT_SEVERITIES = [1.7, 1.15, 1.01].freeze
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, this constant had values [1.7, 1.15, 1.01, 1.005]. In practice, the last segment isn't very useful. For example, comparing versions v7.0.0 and v7.0.0.1 will not cause a health score difference because the former version only has three segments, so comparison only makes sense if you have versions like v7.0.0.1 and v7.0.0.2.

Because of that, and the fact that Semver doesn't define a fourth segment and its meaning is defined by dependency owners (like Rails, which uses it for security releases), I've decided to remove it. In the case of Rails, health score will still be impacted by advisories because v7.0.0.1 will have an advisory that v7.0.0.2 won't.

FALLBACK_SEGMENT_SEVERITY = 1.0

def initialize(**opts)
@dependency_priorities = opts.fetch(:dependency_priorities, DEPENDENCY_PRIORITIES)
@group_priorities = opts.fetch(:group_priorities, GROUP_PRIORITIES)
@default_dependency_priority = opts.fetch(:default_dependency_priority, DEFAULT_DEPENDENCY_PRIORITY)

@advisory_severity = opts.fetch(:advisory_severity, ADVISORY_SEVERITY)
@advisory_penalties = opts.fetch(:advisory_penalties, ADVISORY_PENALTIES)
@fallback_advisory_penalty = opts.fetch(:fallback_advisory_penalty, FALLBACK_ADVISORY_PENALTY)

@major_version_penalty = opts.fetch(:major_version_penalty, MAJOR_VERSION_PENALTY)
@new_versions_severity = opts.fetch(:new_versions_severity, NEW_VERSIONS_SEVERITY)
@segment_severities = opts.fetch(:segment_severities, SEGMENT_SEVERITIES)
@fallback_segment_severity = opts.fetch(:fallback_segment_severity, FALLBACK_SEGMENT_SEVERITY)
end

def priority_for(dependency)
dependency_priorities[dependency.name.to_sym] ||
group_priorities[dependency.groups.first] ||
default_dependency_priority
end

def advisory_penalty_for(criticality)
advisory_penalties.fetch(criticality, fallback_advisory_penalty)
end

def segment_severity(segment)
return 1.0 unless segment

segment_severities.fetch(segment, fallback_segment_severity)
end

attr_reader :advisory_severity
attr_reader :new_versions_severity
attr_reader :major_version_penalty

private

attr_reader :dependency_priorities
attr_reader :default_dependency_priority
attr_reader :group_priorities
attr_reader :advisory_penalties
attr_reader :fallback_advisory_penalty
attr_reader :segment_severities
attr_reader :fallback_segment_severity
end
end
end
69 changes: 0 additions & 69 deletions lib/polariscope/scanner/codebase_health_score.rb

This file was deleted.

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

require_relative 'ruby_scanner'

require 'tempfile'
require 'bundler/audit/configuration'

module Polariscope
module Scanner
class DependencyContext
DEFAULT_SPEC_TYPE = :released

def initialize(**opts)
@gemfile_content = opts.fetch(:gemfile_content, nil)
@gemfile_lock_content = opts.fetch(:gemfile_lock_content, nil)
@bundler_audit_config_content = opts.fetch(:bundler_audit_config_content, '')
@spec_type = opts.fetch(:spec_type, DEFAULT_SPEC_TYPE)
end

def no_dependencies?
blank_value?(gemfile_content) || blank_value?(gemfile_lock_content) || dependencies.empty?
end

def dependencies
bundle_definition.dependencies
end

def dependency_versions(dependency)
[current_dependency_version(dependency), gem_versions.versions_for(dependency.name)]
end

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

private

attr_reader :gemfile_content
attr_reader :gemfile_lock_content
attr_reader :bundler_audit_config_content
attr_reader :spec_type

def ruby_scanner
@ruby_scanner ||= RubyScanner.new(bundle_definition.locked_ruby_version_object)
end

def gem_versions
@gem_versions ||= GemVersions.new(dependencies.map(&:name), spec_type: spec_type)
end

def bundle_definition
@bundle_definition ||=
::Tempfile.create do |gemfile|
::Tempfile.create do |gemfile_lock|
gemfile.puts parseable_gemfile_content
gemfile.rewind

gemfile_lock.puts gemfile_lock_content
gemfile_lock.rewind

Bundler::Definition.build(gemfile.path, gemfile_lock.path, false)
end
end
end

def current_dependency_version(dependency)
specs.find { |spec| dependency.name == spec.name }.version
end

def specs
bundle_definition.locked_gems.specs
end

def ignored_advisories
audit_configuration.ignore
end

def audit_configuration
@audit_configuration ||= Tempfile.create do |file|
file.puts bundler_audit_config_content
file.rewind

Bundler::Audit::Configuration.load(file.path)
rescue StandardError
Bundler::Audit::Configuration.new
end
end

def audit_database
@audit_database ||= Bundler::Audit::Database.new
end

def parseable_gemfile_content
gemfile_content.gsub("gemspec\n", '').gsub(/^ruby.*$\R/, '')
end

def blank_value?(value)
value.nil? || value.empty?
end
end
end
end
Loading