polariscope 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c891a85ae5f5fed5a3cba46ddb78c81657baedfac15bcf31de9002fdae9c6df
4
- data.tar.gz: 3864c4ecf3833289fb1cb1af30df8316a646f62714962f034e60ab42d0ab11f9
3
+ metadata.gz: 0d0728ac02d87facc2bbaf9297aee04343275420e267c7daf7240c3b3686b834
4
+ data.tar.gz: daa80c083d6a29aa9709ce9e34a55fea9f5842cc4e85967dd9cdb947ef274f9e
5
5
  SHA512:
6
- metadata.gz: c008254c44678d2a936e027e33bba206ac12f48f7350d5aa63f5105e8897713fc851664d46afa02f665f11d53ad4459094b6795e3ba4caefaf58b3918d74fd47
7
- data.tar.gz: 8e123d4cc077831e1fc252d846655b0994e3f78836affdc11af73ad9df9ae87bedf3c5ead5012be77f6b010d38585172d7e31800f23f86241f68b74cae3c97d2
6
+ metadata.gz: 26d92d6f8a81ef010a02dba51ec28deed8ce9d1c1ef0bf92a13694c29ada0cf062a5d375819cad2f7bed5db1a292fe978f7bd24225381530976d6a394c9bebb0
7
+ data.tar.gz: 24f52577172981309212e6de5ac8f45ef51b26c8ecf075f3d29aec930be1cc383ee8ed7ae9608c943782eed93cb3387dec498932f6041cd86f9dc1b481ba35cd
data/.rubocop.yml CHANGED
@@ -21,8 +21,5 @@ Style/FrozenStringLiteralComment:
21
21
  Exclude:
22
22
  - 'exe/polariscope'
23
23
 
24
- Rails/TimeZone:
25
- Enabled: false
26
-
27
- Rails/Date:
24
+ Rails:
28
25
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2024-10-25
4
+
5
+ - Count Ruby versions towards health score
6
+ - Update audit database if older than one day
7
+
3
8
  ## [0.3.0] - 2024-10-17
4
9
 
5
10
  - Count Ruby advisories towards health score
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polariscope
4
+ module Scanner
5
+ class AdvisoriesHealthScore
6
+ def initialize(dependency_context, calculation_context)
7
+ @dependency_context = dependency_context
8
+ @calculation_context = calculation_context
9
+ end
10
+
11
+ def health_score
12
+ (1 + advisories_penalty)**-Math.log(calculation_context.advisory_severity)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :dependency_context
18
+ attr_reader :calculation_context
19
+
20
+ def advisories_penalty
21
+ dependency_context
22
+ .advisories
23
+ .map(&:criticality)
24
+ .sum { |criticality| calculation_context.advisory_penalty_for(criticality) }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/audit/database'
4
+
5
+ module Polariscope
6
+ module Scanner
7
+ module AuditDatabase
8
+ extend self
9
+
10
+ ONE_DAY = 24 * 60 * 60
11
+
12
+ def update_if_necessary
13
+ update_audit_database! if database_outdated?
14
+ end
15
+
16
+ private
17
+
18
+ def update_audit_database!
19
+ Bundler::Audit::Database.update!(quiet: true)
20
+ end
21
+
22
+ def database_outdated?
23
+ audit_db_missing? || audit_db_stale?
24
+ end
25
+
26
+ def audit_db_missing?
27
+ !Bundler::Audit::Database.exists?
28
+ end
29
+
30
+ def audit_db_stale?
31
+ ((Time.now - Bundler::Audit::Database.new.last_updated_at) / ONE_DAY) > 1.0
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polariscope
4
+ module Scanner
5
+ class CalculationContext
6
+ DEPENDENCY_PRIORITIES = { ruby: 10.0, rails: 10.0 }.freeze
7
+ GROUP_PRIORITIES = { default: 2.0, production: 2.0 }.freeze
8
+ DEFAULT_DEPENDENCY_PRIORITY = 1.0
9
+
10
+ ADVISORY_SEVERITY = 1.09
11
+ ADVISORY_PENALTIES = {
12
+ none: 0.0,
13
+ low: 0.5,
14
+ medium: 1.0,
15
+ high: 3.0,
16
+ critical: 5.0
17
+ }.freeze
18
+ FALLBACK_ADVISORY_PENALTY = 0.5
19
+
20
+ MAJOR_VERSION_PENALTY = 1
21
+ NEW_VERSIONS_SEVERITY = 1.07
22
+ SEGMENT_SEVERITIES = [1.7, 1.15, 1.01].freeze
23
+ FALLBACK_SEGMENT_SEVERITY = 1.0
24
+
25
+ def initialize(**opts)
26
+ @dependency_priorities = opts.fetch(:dependency_priorities, DEPENDENCY_PRIORITIES)
27
+ @group_priorities = opts.fetch(:group_priorities, GROUP_PRIORITIES)
28
+ @default_dependency_priority = opts.fetch(:default_dependency_priority, DEFAULT_DEPENDENCY_PRIORITY)
29
+
30
+ @advisory_severity = opts.fetch(:advisory_severity, ADVISORY_SEVERITY)
31
+ @advisory_penalties = opts.fetch(:advisory_penalties, ADVISORY_PENALTIES)
32
+ @fallback_advisory_penalty = opts.fetch(:fallback_advisory_penalty, FALLBACK_ADVISORY_PENALTY)
33
+
34
+ @major_version_penalty = opts.fetch(:major_version_penalty, MAJOR_VERSION_PENALTY)
35
+ @new_versions_severity = opts.fetch(:new_versions_severity, NEW_VERSIONS_SEVERITY)
36
+ @segment_severities = opts.fetch(:segment_severities, SEGMENT_SEVERITIES)
37
+ @fallback_segment_severity = opts.fetch(:fallback_segment_severity, FALLBACK_SEGMENT_SEVERITY)
38
+ end
39
+
40
+ def priority_for(dependency)
41
+ dependency_priorities[dependency.name.to_sym] ||
42
+ group_priorities[dependency.groups.first] ||
43
+ default_dependency_priority
44
+ end
45
+
46
+ def advisory_penalty_for(criticality)
47
+ advisory_penalties.fetch(criticality, fallback_advisory_penalty)
48
+ end
49
+
50
+ def segment_severity(segment)
51
+ return 1.0 unless segment
52
+
53
+ segment_severities.fetch(segment, fallback_segment_severity)
54
+ end
55
+
56
+ attr_reader :advisory_severity
57
+ attr_reader :new_versions_severity
58
+ attr_reader :major_version_penalty
59
+
60
+ private
61
+
62
+ attr_reader :dependency_priorities
63
+ attr_reader :default_dependency_priority
64
+ attr_reader :group_priorities
65
+ attr_reader :advisory_penalties
66
+ attr_reader :fallback_advisory_penalty
67
+ attr_reader :segment_severities
68
+ attr_reader :fallback_segment_severity
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ruby_scanner'
4
+
5
+ require 'tempfile'
6
+ require 'bundler/audit/configuration'
7
+
8
+ module Polariscope
9
+ module Scanner
10
+ class DependencyContext
11
+ DEFAULT_SPEC_TYPE = :released
12
+
13
+ def initialize(**opts)
14
+ @gemfile_content = opts.fetch(:gemfile_content, nil)
15
+ @gemfile_lock_content = opts.fetch(:gemfile_lock_content, nil)
16
+ @bundler_audit_config_content = opts.fetch(:bundler_audit_config_content, '')
17
+ @spec_type = opts.fetch(:spec_type, DEFAULT_SPEC_TYPE)
18
+ end
19
+
20
+ def no_dependencies?
21
+ blank_value?(gemfile_content) || blank_value?(gemfile_lock_content) || dependencies.empty?
22
+ end
23
+
24
+ def dependencies
25
+ @dependencies ||= dependencies_with_ruby
26
+ end
27
+
28
+ def dependency_versions(dependency)
29
+ [current_dependency_version(dependency), gem_versions.versions_for(dependency.name)]
30
+ end
31
+
32
+ def advisories
33
+ specs
34
+ .flat_map { |gem| audit_database.check_gem(gem).to_a }
35
+ .concat(ruby_scanner.vulnerable_advisories)
36
+ .reject { |advisory| ignored_advisories.intersect?(advisory.identifiers.to_set) }
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :gemfile_content
42
+ attr_reader :gemfile_lock_content
43
+ attr_reader :bundler_audit_config_content
44
+ attr_reader :spec_type
45
+
46
+ def ruby_scanner
47
+ @ruby_scanner ||= RubyScanner.new(bundle_definition.locked_ruby_version_object)
48
+ end
49
+
50
+ def gem_versions
51
+ @gem_versions ||= GemVersions.new(dependencies.map(&:name), spec_type: spec_type)
52
+ end
53
+
54
+ def bundle_definition
55
+ @bundle_definition ||=
56
+ ::Tempfile.create do |gemfile|
57
+ ::Tempfile.create do |gemfile_lock|
58
+ gemfile.puts parseable_gemfile_content
59
+ gemfile.rewind
60
+
61
+ gemfile_lock.puts gemfile_lock_content
62
+ gemfile_lock.rewind
63
+
64
+ Bundler::Definition.build(gemfile.path, gemfile_lock.path, false)
65
+ end
66
+ end
67
+ end
68
+
69
+ def current_dependency_version(dependency)
70
+ return ruby_scanner.version if dependency.name == GemVersions::RUBY_NAME
71
+
72
+ specs.find { |spec| dependency.name == spec.name }.version
73
+ end
74
+
75
+ def dependencies_with_ruby
76
+ return installed_dependencies unless ruby_scanner.version
77
+
78
+ installed_dependencies + [Bundler::Dependency.new(GemVersions::RUBY_NAME, false)]
79
+ end
80
+
81
+ def installed_dependencies
82
+ spec_names = specs.to_set(&:name)
83
+
84
+ bundle_definition.dependencies.select { |dependency| spec_names.include?(dependency.name) }
85
+ end
86
+
87
+ def specs
88
+ bundle_definition.locked_gems.specs
89
+ end
90
+
91
+ def ignored_advisories
92
+ audit_configuration.ignore
93
+ end
94
+
95
+ def audit_configuration
96
+ @audit_configuration ||= Tempfile.create do |file|
97
+ file.puts bundler_audit_config_content
98
+ file.rewind
99
+
100
+ Bundler::Audit::Configuration.load(file.path)
101
+ rescue StandardError
102
+ Bundler::Audit::Configuration.new
103
+ end
104
+ end
105
+
106
+ def audit_database
107
+ @audit_database ||= Bundler::Audit::Database.new
108
+ end
109
+
110
+ def parseable_gemfile_content
111
+ gemfile_content.gsub("gemspec\n", '').gsub(/^ruby.*$\R/, '')
112
+ end
113
+
114
+ def blank_value?(value)
115
+ value.nil? || value.empty?
116
+ end
117
+ end
118
+ end
119
+ end
@@ -3,29 +3,37 @@
3
3
  module Polariscope
4
4
  module Scanner
5
5
  class GemHealthScore
6
- def initialize(all_versions:, current_version:, severities: [])
7
- @all_versions = all_versions
8
- @current_version = current_version
9
- @severities = severities
6
+ def initialize(dependency_context, calculation_context, dependency)
7
+ @calculation_context = calculation_context
8
+
9
+ @current_version, @all_versions = dependency_context.dependency_versions(dependency)
10
10
  end
11
11
 
12
12
  def health_score
13
- return 100 if up_to_date?
13
+ return 1.0 if up_to_date?
14
14
 
15
- score = 100
16
- score *= (1.0 + first_outdated_segment)**-Math.log(first_outdated_segment_severity)
17
- score *= (1.0 + new_versions.count)**-Math.log(1.07)
15
+ score = 1.0
16
+ score *= (1 + first_outdated_segment)**-Math.log(first_outdated_segment_severity)
17
+ score *= (1 + new_versions.count)**-Math.log(calculation_context.new_versions_severity)
18
18
  score
19
19
  end
20
20
 
21
+ def major_version_penalty
22
+ major_version_outdated? ? calculation_context.major_version_penalty : 0
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :calculation_context
28
+ attr_reader :current_version
29
+ attr_reader :all_versions
30
+
21
31
  def up_to_date?
22
32
  current_version == latest_version
23
33
  end
24
34
 
25
35
  def first_outdated_segment_severity
26
- return 1 if first_outdated_segment_index.nil?
27
-
28
- severities[first_outdated_segment_index]
36
+ calculation_context.segment_severity(first_outdated_segment_index)
29
37
  end
30
38
 
31
39
  def first_outdated_segment_index
@@ -36,17 +44,15 @@ module Polariscope
36
44
  segments_delta.find(&:positive?) || 0
37
45
  end
38
46
 
39
- def segments_delta
40
- current_version.segments.grep(Integer).zip(latest_version.segments.grep(Integer))
41
- .map { |current, latest| current && latest ? latest - current : 0 }
47
+ def major_version_outdated?
48
+ segments_delta.first.positive?
42
49
  end
43
50
 
44
- def major_version_penalty
45
- major_outdated? ? 1 : 0
46
- end
47
-
48
- def major_outdated?
49
- latest_version.segments[0] > current_version.segments[0]
51
+ def segments_delta
52
+ @segments_delta ||=
53
+ version_segments(latest_version)
54
+ .zip(version_segments(current_version))
55
+ .map { |latest, current| latest && current ? latest - current : 0 }
50
56
  end
51
57
 
52
58
  def latest_version
@@ -57,11 +63,9 @@ module Polariscope
57
63
  @new_versions ||= all_versions.select { |version| version > current_version }
58
64
  end
59
65
 
60
- private
61
-
62
- attr_reader :all_versions
63
- attr_reader :current_version
64
- attr_reader :severities
66
+ def version_segments(version)
67
+ version.segments.grep(Integer)
68
+ end
65
69
  end
66
70
  end
67
71
  end
@@ -1,30 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'ruby_versions'
4
+
3
5
  require 'set'
4
6
 
5
7
  module Polariscope
6
8
  module Scanner
7
9
  class GemVersions
10
+ RUBY_NAME = 'ruby'
11
+
8
12
  def initialize(dependency_names, spec_type:)
9
13
  @dependency_names = dependency_names.to_set
10
14
  @spec_type = spec_type
11
- @gem_versions = Hash.new { |h, k| h[k] = [] }
15
+ @gem_versions = Hash.new { |h, k| h[k] = Set.new }
12
16
 
13
17
  fetch_gems
18
+ fetch_ruby_versions if dependency_names.include?(RUBY_NAME)
14
19
  end
15
20
 
16
21
  def versions_for(gem_name)
17
- @gem_versions[gem_name]
22
+ gem_versions[gem_name]
18
23
  end
19
24
 
20
25
  private
21
26
 
27
+ attr_reader :dependency_names
28
+ attr_reader :spec_type
29
+ attr_reader :gem_versions
30
+
31
+ def fetch_ruby_versions
32
+ gem_versions[RUBY_NAME] = RubyVersions.available_versions
33
+ end
34
+
22
35
  def fetch_gems
23
- gem_tuples = Gem::SpecFetcher.fetcher.detect(@spec_type) do |name_tuple|
24
- @dependency_names.include?(name_tuple.name)
25
- end
36
+ gem_tuples.each { |(name_tuple, _)| gem_versions[name_tuple.name] << name_tuple.version }
37
+ end
26
38
 
27
- gem_tuples.each { |gem_tuple| @gem_versions[gem_tuple.first.name] << gem_tuple.first.version }
39
+ def gem_tuples
40
+ Gem::SpecFetcher.fetcher.detect(spec_type) { |name_tuple| dependency_names.include?(name_tuple.name) }
28
41
  end
29
42
  end
30
43
  end
@@ -1,147 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler'
4
- require 'bundler/audit/configuration'
5
- require 'bundler/audit/database'
6
- require 'set'
7
- require_relative 'gem_versions'
3
+ require_relative 'advisories_health_score'
4
+ require_relative 'audit_database'
5
+ require_relative 'calculation_context'
6
+ require_relative 'dependency_context'
8
7
  require_relative 'gem_health_score'
9
- require_relative 'ruby_scanner'
10
8
 
11
9
  module Polariscope
12
10
  module Scanner
13
- class GemfileHealthScore # rubocop:disable Metrics/ClassLength
14
- GEM_PRIORITIES = { rails: 10.0 }.freeze
15
- DEFAULT_PRIORITY = 1.0
16
- GROUP_PRIORITIES = { default: 2.0, production: 2.0 }.freeze
17
- SEVERITIES = [1.7, 1.15, 1.01, 1.005].freeze
18
- FALLBACK_ADVISORY_PENALTY = 0.5
19
- ADVISORY_PENALTY_MAP = {
20
- none: 0.0,
21
- low: 0.5,
22
- medium: 1.0,
23
- high: 3.0,
24
- critical: 5.0
25
- }.freeze
26
-
27
- def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
28
- gemfile_path:, gemfile_lock_content:, gem_priorities: GEM_PRIORITIES, default_priority: DEFAULT_PRIORITY,
29
- group_priorities: GROUP_PRIORITIES, severities: SEVERITIES, spec_type: :released,
30
- advisory_penalty_map: ADVISORY_PENALTY_MAP, fallback_advisory_penalty: FALLBACK_ADVISORY_PENALTY,
31
- update_audit_database: false, bundler_audit_config_path: ''
32
- )
33
- @lockfile_parser = Bundler::LockfileParser.new(gemfile_lock_content)
34
- @ruby_scanner = RubyScanner.new(@lockfile_parser)
35
- @gemfile_path = gemfile_path
36
- @dependencies = installed_dependencies
37
- @gem_priorities = gem_priorities
38
- @default_priority = default_priority
39
- @group_priorities = group_priorities
40
- @severities = severities
41
- @spec_type = spec_type
42
- @advisory_penalty_map = advisory_penalty_map
43
- @fallback_advisory_penalty = fallback_advisory_penalty
44
- @bundler_audit_config_path = bundler_audit_config_path
45
-
46
- update_audit_database! if update_audit_database
11
+ class GemfileHealthScore
12
+ def initialize(**opts)
13
+ @dependency_context = DependencyContext.new(**opts)
14
+ @calculation_context = CalculationContext.new(**opts)
15
+
16
+ AuditDatabase.update_if_necessary
47
17
  end
48
18
 
49
19
  def health_score
50
- return nil if dependencies.empty?
20
+ return nil if dependency_context.no_dependencies?
51
21
 
52
- ((1.0 - major_version_penalty_score) * weighted_gem_health_score * advisories_score).round(2)
22
+ (100.0 * weighted_major_version_score * weighted_dependency_health_score * advisories_score).round(2)
53
23
  end
54
24
 
55
25
  private
56
26
 
57
- attr_reader :dependencies
58
- attr_reader :lockfile_parser
59
- attr_reader :ruby_scanner
60
- attr_reader :advisory_penalty_map
61
- attr_reader :fallback_advisory_penalty
62
- attr_reader :bundler_audit_config_path
63
-
64
- def major_version_penalties
65
- dependencies.map do |dependency|
66
- current_version, all_versions = dependency_versions(dependency)
27
+ attr_reader :dependency_context
28
+ attr_reader :calculation_context
67
29
 
68
- GemHealthScore.new(all_versions: all_versions, current_version: current_version).major_version_penalty
69
- end
30
+ def weighted_major_version_score
31
+ 1.0 - weighted_major_version_penalty
70
32
  end
71
33
 
72
- def major_version_penalty_score
73
- dependency_priorities.zip(major_version_penalties).sum { |a| a.inject(:*) } / dependency_priorities.sum
74
- end
75
-
76
- def weighted_gem_health_score
77
- dependency_priorities.zip(dependency_health_scores).sum { |a| a.inject(:*) } / dependency_priorities.sum
78
- end
79
-
80
- def dependency_health_scores
81
- dependencies.map do |dependency|
82
- current_version, all_versions = dependency_versions(dependency)
83
-
84
- GemHealthScore.new(
85
- all_versions: all_versions,
86
- current_version: current_version,
87
- severities: @severities
88
- ).health_score
89
- end
90
- end
91
-
92
- def dependency_priorities
93
- @dependency_priorities ||= dependencies.map { |dependency| dependency_priority(dependency) }
34
+ def weighted_major_version_penalty
35
+ dependency_priorities.zip(major_version_penalties).sum { |a, b| a * b } / dependency_priorities.sum
94
36
  end
95
37
 
96
- def dependency_priority(dependency)
97
- @gem_priorities[dependency.name.to_sym] || @group_priorities[dependency.groups.first] || @default_priority
38
+ def weighted_dependency_health_score
39
+ dependency_priorities.zip(dependency_health_scores).sum { |a, b| a * b } / dependency_priorities.sum
98
40
  end
99
41
 
100
- def dependency_versions(dependency)
101
- [current_dependency_version(dependency), gem_versions.versions_for(dependency.name)]
42
+ def major_version_penalties
43
+ gem_health_scores.map(&:major_version_penalty)
102
44
  end
103
45
 
104
- def current_dependency_version(dependency)
105
- lockfile_parser.specs.find { |spec| dependency.name == spec.name }.version
46
+ def dependency_health_scores
47
+ gem_health_scores.map(&:health_score)
106
48
  end
107
49
 
108
- def installed_dependencies
109
- spec_names = @lockfile_parser.specs.to_set(&:name)
110
- dependencies = Bundler::Definition.build(@gemfile_path, nil, nil).dependencies
111
-
112
- dependencies.select { |dependency| spec_names.include?(dependency.name) }
50
+ def gem_health_scores
51
+ @gem_health_scores ||= dependencies.map do |dependency|
52
+ GemHealthScore.new(dependency_context, calculation_context, dependency)
53
+ end
113
54
  end
114
55
 
115
- def gem_versions
116
- @gem_versions ||= GemVersions.new(dependencies.map(&:name), spec_type: @spec_type)
56
+ def dependency_priorities
57
+ @dependency_priorities ||= dependencies.map { |dependency| calculation_context.priority_for(dependency) }
117
58
  end
118
59
 
119
60
  def advisories_score
120
- (1 + advisories_penalty)**-Math.log(1.09)
121
- end
122
-
123
- def advisories_penalty
124
- advisories.map(&:criticality)
125
- .sum { |criticality| advisory_penalty_map.fetch(criticality, fallback_advisory_penalty) }
126
- end
127
-
128
- def advisories
129
- database = Bundler::Audit::Database.new
130
-
131
- lockfile_parser.specs
132
- .flat_map { |gem| database.check_gem(gem).to_a }
133
- .concat(ruby_scanner.vulnerable_advisories)
134
- .reject { |advisory| ignored_advisories.intersect?(advisory.identifiers.to_set) }
135
- end
136
-
137
- def ignored_advisories
138
- @ignored_advisories ||= Bundler::Audit::Configuration.load(bundler_audit_config_path).ignore.to_set
139
- rescue Bundler::Audit::Configuration::FileNotFound, Bundler::Audit::Configuration::InvalidConfigurationError
140
- @ignored_advisories = Set.new
61
+ AdvisoriesHealthScore.new(dependency_context, calculation_context).health_score
141
62
  end
142
63
 
143
- def update_audit_database!
144
- Bundler::Audit::Database.update!(quiet: true)
64
+ def dependencies
65
+ dependency_context.dependencies
145
66
  end
146
67
  end
147
68
  end
@@ -1,17 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler'
4
3
  require 'bundler/audit/database'
5
4
 
6
5
  module Polariscope
7
6
  module Scanner
8
7
  class RubyScanner
9
- def initialize(lockfile_parser)
10
- @lockfile_parser = lockfile_parser
8
+ def initialize(bundler_ruby_version)
9
+ @bundler_ruby_version = bundler_ruby_version
11
10
  end
12
11
 
13
12
  def version
14
- lockfile_ruby_version&.gem_version
13
+ bundler_ruby_version&.gem_version
15
14
  end
16
15
 
17
16
  def vulnerable_advisories
@@ -20,8 +19,7 @@ module Polariscope
20
19
 
21
20
  private
22
21
 
23
- attr_reader :lockfile_parser
24
- attr_reader :bundler_audit_database
22
+ attr_reader :bundler_ruby_version
25
23
 
26
24
  def advisories
27
25
  cve_paths.map { |path| Bundler::Audit::Advisory.load(path) }
@@ -34,11 +32,7 @@ module Polariscope
34
32
  end
35
33
 
36
34
  def engine
37
- lockfile_ruby_version.engine
38
- end
39
-
40
- def lockfile_ruby_version
41
- @lockfile_ruby_version ||= Bundler::RubyVersion.from_string(@lockfile_parser.ruby_version)
35
+ bundler_ruby_version.engine
42
36
  end
43
37
  end
44
38
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open-uri'
4
+
5
+ module Polariscope
6
+ module Scanner
7
+ module RubyVersions
8
+ VERSIONS_INDEX_FILE_URL = 'https://cache.ruby-lang.org/pub/ruby/index.txt'
9
+ MINIMUM_RUBY_VERSION = Gem::Version.new('1.0.0')
10
+ OPEN_TIMEOUT = 5
11
+ READ_TIMEOUT = 5
12
+
13
+ module_function
14
+
15
+ def available_versions # rubocop:disable Metrics/AbcSize
16
+ URI
17
+ .parse(VERSIONS_INDEX_FILE_URL)
18
+ .open(open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT, &:readlines)
19
+ .drop(1) # header row
20
+ .map { |line| line.split("\t").first.sub('ruby-', 'ruby ') } # ruby-2.3.4 -> ruby 2.3.4
21
+ .filter_map { |ruby_version| Bundler::RubyVersion.from_string(ruby_version)&.gem_version }
22
+ .select { |gem_version| gem_version >= MINIMUM_RUBY_VERSION && gem_version.segments.size == 3 }
23
+ .to_set
24
+ rescue Timeout::Error
25
+ Set.new
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polariscope
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/polariscope.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'polariscope/version'
4
- require_relative 'polariscope/scanner/codebase_health_score'
4
+ require_relative 'polariscope/scanner/gemfile_health_score'
5
5
  require_relative 'polariscope/scanner/gem_versions'
6
6
  require_relative 'polariscope/file_content'
7
7
 
@@ -9,15 +9,15 @@ module Polariscope
9
9
  Error = Class.new(StandardError)
10
10
 
11
11
  class << self
12
- def scan(gemfile_content: nil, gemfile_lock_content: nil, bundler_audit_config_content: nil)
13
- Scanner::CodebaseHealthScore.new(
14
- gemfile_content: gemfile_content || FileContent.for('Gemfile'),
15
- gemfile_lock_content: gemfile_lock_content || FileContent.for('Gemfile.lock'),
16
- bundler_audit_config_content: bundler_audit_config_content || FileContent.for('.bundler-audit.yml')
17
- ).health_score
12
+ def scan(**opts)
13
+ Scanner::GemfileHealthScore.new(**opts.merge(
14
+ gemfile_content: opts.fetch(:gemfile_content, FileContent.for('Gemfile')),
15
+ gemfile_lock_content: opts.fetch(:gemfile_lock_content, FileContent.for('Gemfile.lock')),
16
+ bundler_audit_config_content: opts.fetch(:bundler_audit_config_content, FileContent.for('.bundler-audit.yml'))
17
+ )).health_score
18
18
  end
19
19
 
20
- def gem_versions(dependency_names, spec_type: :released)
20
+ def gem_versions(dependency_names, spec_type: Scanner::DependencyContext::DEFAULT_SPEC_TYPE)
21
21
  Scanner::GemVersions.new(dependency_names, spec_type: spec_type)
22
22
  end
23
23
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polariscope
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-17 00:00:00.000000000 Z
11
+ date: 2024-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -56,11 +56,15 @@ files:
56
56
  - exe/polariscope
57
57
  - lib/polariscope.rb
58
58
  - lib/polariscope/file_content.rb
59
- - lib/polariscope/scanner/codebase_health_score.rb
59
+ - lib/polariscope/scanner/advisories_health_score.rb
60
+ - lib/polariscope/scanner/audit_database.rb
61
+ - lib/polariscope/scanner/calculation_context.rb
62
+ - lib/polariscope/scanner/dependency_context.rb
60
63
  - lib/polariscope/scanner/gem_health_score.rb
61
64
  - lib/polariscope/scanner/gem_versions.rb
62
65
  - lib/polariscope/scanner/gemfile_health_score.rb
63
66
  - lib/polariscope/scanner/ruby_scanner.rb
67
+ - lib/polariscope/scanner/ruby_versions.rb
64
68
  - lib/polariscope/version.rb
65
69
  - polariscope.gemspec
66
70
  homepage: https://github.com/infinum/polariscope
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'tempfile'
4
- require_relative 'gemfile_health_score'
5
-
6
- module Polariscope
7
- module Scanner
8
- class CodebaseHealthScore
9
- def initialize(gemfile_content:, gemfile_lock_content:, bundler_audit_config_content:)
10
- @gemfile_content = gemfile_content
11
- @gemfile_lock_content = gemfile_lock_content
12
- @bundler_audit_config_content = bundler_audit_config_content
13
- end
14
-
15
- def health_score
16
- return nil if blank?(gemfile_content) || blank?(gemfile_lock_content)
17
-
18
- begin
19
- GemfileHealthScore.new(gemfile_path: gemfile_file.path, gemfile_lock_content: gemfile_lock_content,
20
- bundler_audit_config_path: bundler_audit_config_file.path,
21
- update_audit_database: update_audit_database?).health_score
22
- ensure
23
- gemfile_file.unlink
24
- bundler_audit_config_file.unlink
25
- end
26
- end
27
-
28
- private
29
-
30
- attr_reader :gemfile_content
31
- attr_reader :gemfile_lock_content
32
- attr_reader :bundler_audit_config_content
33
-
34
- def gemfile_file
35
- @gemfile_file ||= begin
36
- file = Tempfile.new('Gemfile')
37
- file.write(gemfile_content.gsub("gemspec\n", '').gsub(/^ruby.*$\R/, ''))
38
- file.close
39
- file
40
- end
41
- end
42
-
43
- def bundler_audit_config_file
44
- @bundler_audit_config_file ||= begin
45
- file = Tempfile.new('.bundler-audit.yml')
46
- file.write(bundler_audit_config_content)
47
- file.close
48
- file
49
- end
50
- end
51
-
52
- def blank?(value)
53
- value.nil? || value == ''
54
- end
55
-
56
- def update_audit_database?
57
- audit_db_missing? || audit_db_stale?
58
- end
59
-
60
- def audit_db_missing?
61
- !Bundler::Audit::Database.exists?
62
- end
63
-
64
- def audit_db_stale?
65
- ((Time.now - Bundler::Audit::Database.new.last_updated_at) / 86_400) > 7
66
- end
67
- end
68
- end
69
- end