polariscope 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -4
- data/CHANGELOG.md +5 -0
- data/lib/polariscope/scanner/advisories_health_score.rb +28 -0
- data/lib/polariscope/scanner/audit_database.rb +35 -0
- data/lib/polariscope/scanner/calculation_context.rb +71 -0
- data/lib/polariscope/scanner/dependency_context.rb +119 -0
- data/lib/polariscope/scanner/gem_health_score.rb +29 -25
- data/lib/polariscope/scanner/gem_versions.rb +19 -6
- data/lib/polariscope/scanner/gemfile_health_score.rb +33 -112
- data/lib/polariscope/scanner/ruby_scanner.rb +5 -11
- data/lib/polariscope/scanner/ruby_versions.rb +29 -0
- data/lib/polariscope/version.rb +1 -1
- data/lib/polariscope.rb +8 -8
- metadata +7 -3
- data/lib/polariscope/scanner/codebase_health_score.rb +0 -69
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d0728ac02d87facc2bbaf9297aee04343275420e267c7daf7240c3b3686b834
|
4
|
+
data.tar.gz: daa80c083d6a29aa9709ce9e34a55fea9f5842cc4e85967dd9cdb947ef274f9e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26d92d6f8a81ef010a02dba51ec28deed8ce9d1c1ef0bf92a13694c29ada0cf062a5d375819cad2f7bed5db1a292fe978f7bd24225381530976d6a394c9bebb0
|
7
|
+
data.tar.gz: 24f52577172981309212e6de5ac8f45ef51b26c8ecf075f3d29aec930be1cc383ee8ed7ae9608c943782eed93cb3387dec498932f6041cd86f9dc1b481ba35cd
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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(
|
7
|
-
@
|
8
|
-
|
9
|
-
@
|
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
|
13
|
+
return 1.0 if up_to_date?
|
14
14
|
|
15
|
-
score =
|
16
|
-
score *= (1
|
17
|
-
score *= (1
|
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
|
-
|
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
|
40
|
-
|
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
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
24
|
-
|
25
|
-
end
|
36
|
+
gem_tuples.each { |(name_tuple, _)| gem_versions[name_tuple.name] << name_tuple.version }
|
37
|
+
end
|
26
38
|
|
27
|
-
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
20
|
+
return nil if dependency_context.no_dependencies?
|
51
21
|
|
52
|
-
(
|
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 :
|
58
|
-
attr_reader :
|
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
|
-
|
69
|
-
|
30
|
+
def weighted_major_version_score
|
31
|
+
1.0 - weighted_major_version_penalty
|
70
32
|
end
|
71
33
|
|
72
|
-
def
|
73
|
-
dependency_priorities.zip(major_version_penalties).sum { |a| a
|
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
|
97
|
-
|
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
|
101
|
-
|
42
|
+
def major_version_penalties
|
43
|
+
gem_health_scores.map(&:major_version_penalty)
|
102
44
|
end
|
103
45
|
|
104
|
-
def
|
105
|
-
|
46
|
+
def dependency_health_scores
|
47
|
+
gem_health_scores.map(&:health_score)
|
106
48
|
end
|
107
49
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
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
|
116
|
-
@
|
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
|
-
(
|
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
|
144
|
-
|
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(
|
10
|
-
@
|
8
|
+
def initialize(bundler_ruby_version)
|
9
|
+
@bundler_ruby_version = bundler_ruby_version
|
11
10
|
end
|
12
11
|
|
13
12
|
def version
|
14
|
-
|
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 :
|
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
|
-
|
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
|
data/lib/polariscope/version.rb
CHANGED
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/
|
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(
|
13
|
-
Scanner::
|
14
|
-
gemfile_content: gemfile_content
|
15
|
-
gemfile_lock_content: gemfile_lock_content
|
16
|
-
bundler_audit_config_content: bundler_audit_config_content
|
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:
|
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.
|
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-
|
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/
|
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
|