polariscope 0.3.0 → 0.5.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: 70b3d555c09bbe2378d8771d0975b34427f089700b9e7852917ffe80e6a79259
4
+ data.tar.gz: 1e87403b8f56622b02b11b5aab0439d503ae6410c88dcb4fa7213464be2e7ffe
5
5
  SHA512:
6
- metadata.gz: c008254c44678d2a936e027e33bba206ac12f48f7350d5aa63f5105e8897713fc851664d46afa02f665f11d53ad4459094b6795e3ba4caefaf58b3918d74fd47
7
- data.tar.gz: 8e123d4cc077831e1fc252d846655b0994e3f78836affdc11af73ad9df9ae87bedf3c5ead5012be77f6b010d38585172d7e31800f23f86241f68b74cae3c97d2
6
+ metadata.gz: 19874d31ca4475d6f3976935270d7c80816f12094b5fc31f25fa80571ddc161ca61a3645b250d91ce8b8bed4b6fbb73e057c90fc2fc15c6b8c99bce4e34b0dda
7
+ data.tar.gz: 8e71615763f0a844074a2b8c214b6c2b7698344d238774c9c2245fe118808cdeed9be611e54c2d723678a03208f6a5c1e77497cb7962e2dc281fea9a71beefa0
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,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2024-10-30
4
+
5
+ - Fix regression for standalone installation
6
+ - Raise `Polariscope::Error` on unparseable Gemfile
7
+
8
+ ## [0.4.0] - 2024-10-25
9
+
10
+ - Count Ruby versions towards health score
11
+ - Update audit database if older than one day
12
+
3
13
  ## [0.3.0] - 2024-10-17
4
14
 
5
15
  - Count Ruby advisories towards health score
data/README.md CHANGED
@@ -1,74 +1,204 @@
1
- # Polariscope
1
+ # Polariscope 🔬
2
2
 
3
- Polariscope is a Ruby gem designed to evaluate the overall health of your Ruby application by analyzing its dependencies. It calculates a health score based on how many dependencies are outdated, meaning there are newer versions available. Keeping dependencies up-to-date is crucial for maintaining application security, performance, and compatibility. This gem provides a quick and easy way to gauge the state of your project's dependencies and take proactive measures to improve its health.
3
+ Polariscope is a Ruby gem to evaluate the overall health of your Ruby application by analyzing its dependencies. It calculates a [health score](#health-score-formula) based on which dependencies are outdated and vulnerable to security issues.
4
4
 
5
- ### Health Score Algorithm
5
+ Keeping dependencies up-to-date is crucial for maintaining application security, performance, and compatibility. This gem provides a quick and easy way to gauge the state of your project's dependencies and take measures to improve its health (more on this in the [Motivation section](#motivation)).
6
6
 
7
- The health score calculation is based on the following mathematical formula:
8
-
9
- ![Health Score Algorithm](docs/algorithm.png)
7
+ Think of this gem as a way to score outputs of `bundle outdated` and `bundle-audit check`.
10
8
 
11
9
  ## Installation
12
10
 
13
- Install the gem and add to the application's Gemfile by executing:
11
+ Add it to your Gemfile:
14
12
 
15
13
  $ bundle add polariscope
16
14
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
15
+ or install standalone:
18
16
 
19
17
  $ gem install polariscope
20
18
 
21
- ### Known issue
22
-
23
- If your default Ruby version is 3.1.2, you might get this error when installing polariscope:
24
-
25
- ```bash
26
- .rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rdoc-6.7.0/lib/rdoc/version.rb:8: warning: already initialized constant RDoc::VERSION
27
- ERROR: While executing gem ... (NameError)
28
- uninitialized constant RDoc::Markdown
19
+ ## Usage
29
20
 
30
- 'markdown' => RDoc::Markdown,
31
- ^^^^^^^^^^
32
- ```
21
+ Polariscope can be used on the CLI and in code.
33
22
 
34
- You can ignore this error. It doesn't occur in other Ruby versions and it doesn't prevent you from using polariscope.
23
+ ### CLI
35
24
 
36
- ## Usage
25
+ Position yourself at the root of your Ruby application and run:
37
26
 
38
- Polariscope can be used in 2 ways.
27
+ $ [bundle exec] polariscope scan
28
+ => 87.4
39
29
 
40
- ### CLI
30
+ The command will read the contents of `Gemfile`, `Gemfile.lock` and [`.bundler-audit.yml`](https://github.com/rubysec/bundler-audit?tab=readme-ov-file#configuration-file) (optional, to ignore advisories) in the current directory and output the calculated health score.
41
31
 
42
- Position yourself in a Ruby application and run:
32
+ ### In code
43
33
 
44
- ```bash
45
- polariscope scan
34
+ ```ruby
35
+ health_score = Polariscope.scan
46
36
  ```
47
37
 
48
- ### IRB / Rails
38
+ Without arguments, it will do the same as above. Optionally, you can override various parameters:
49
39
 
50
40
  ```ruby
51
- Polariscope.scan
41
+ Polariscope.scan(
42
+ gemfile_content: '', # e.g. File.read('Gemfile')
43
+ gemfile_lock_content: '', # e.g. File.read('Gemfile.lock')
44
+ bundler_audit_config_content: '', # e.g. File.read('.bundler-audit.yml')
45
+ spec_type: :latest, # see https://docs.ruby-lang.org/en/master/Gem/SpecFetcher.html#method-i-available_specs
46
+ dependency_priorities: { ruby: 5.0, devise: 10.0 }, # hash of dependency priorities
47
+ group_priorities: { default: 5.0, test: 2.0 }, # hash of bundler group priorities
48
+ default_dependency_priority: 2.0,
49
+ advisory_severity: 1.09, # number >= 1
50
+ advisory_penalties: { medium: 2.0, critical: 5.0 }, # hash of advisory penalties by criticality
51
+ fallback_advisory_penalty: 2.0, # used if value not found in previous hash
52
+ major_version_penalty: 0.5, # number in range [0, 1]
53
+ new_versions_severity: 1.09, # number >= 1
54
+ segment_severities: [1.7, 1.15, 1.01], # ordered by segments: [major, minor, patch]
55
+ fallback_segment_severity: 1.01, # in case dependency versions have more segments than in segment_severities
56
+ )
52
57
  ```
53
58
 
54
- The return value will indicate how healthy your project is on a scale from 0 to 100.
59
+ For details on what these parameters mean, consult [this section](#health-score-formula).
55
60
 
56
61
  #### Additional features
57
62
 
58
- ##### Gem versions
59
-
60
63
  Get the released or latest version of gems with:
61
64
 
62
65
  ```ruby
63
66
  # released versions
64
- gem_specs = Polariscope.gem_versions(['gem_name_1', 'gem_name_2'])
65
- gem_specs.versions_for('gem_name_1') # => returns potentially many versions
67
+ gem_specs = Polariscope.gem_versions(['devise', 'pundit'])
68
+ gem_specs.versions_for('devise')
69
+ # => returns potentially many versions
66
70
 
67
71
  # latest version
68
- gem_specs = Polariscope.gem_versions(['gem_name_1', 'gem_name_2'], spec_type: :latest)
69
- gem_specs.versions_for('gem_name_1') # => returns latest version
72
+ gem_specs = Polariscope.gem_versions(['devise', 'punt'], spec_type: :latest)
73
+ gem_specs.versions_for('pundit')
74
+ # => returns only the latest version
70
75
  ```
71
76
 
77
+ ## Health Score Formula
78
+
79
+ Health score is calculated with a formula that takes the contents of `Gemfile` and `Gemfile.lock` and produces a decimal number in the $[0,100]$ range. 100 means everything is up-to-date and there are no security issues, and it lowers as newer versions are released or security issues are discovered.
80
+
81
+ By design, health score is most useful as a relative measure of application health: if your health score suddenly drops one day from 100 to 90, it signals a serious issue (e.g. a new vulnerability in your Ruby version). If it drops from 100 to 95, it may signal that a new minor version of Rails has been released, for example. If it drops from 100 to 99.5, it may mean a gem like Pundit has a new patch version with a bug fix.
82
+
83
+ How much the score changes depends on various factors:
84
+ - dependency priority (by default, [Ruby and Rails have a higher priority](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L6) than other dependencies)
85
+ - [bundler group](https://bundler.io/guides/groups.html) priority (by default, [`:default` and `:production` groups have a higher priority](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L7))
86
+ - number of versions between the current and the latest version of a dependency
87
+ - the kind of outdatedness according to [SemVer](https://semver.org/); if there's a new major version, that will cause a sharper drop in the score than a new minor version
88
+ - the number of active security advisories
89
+ - [advisory severity](https://nvd.nist.gov/vuln-metrics/cvss) (e.g. a High severity advisory will cause a sharper drop in score than one that is Low)
90
+
91
+ ### Formula
92
+
93
+ [This is the complete formula](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gemfile_health_score.rb#L22) (it's simpler than it may seem):
94
+ ```math
95
+ {HS}_G =
96
+ 100
97
+ \cdot
98
+ \underbrace{\left(1-\frac{\sum_{d \in G_{dd}}w_d \cdot mp_d}{\sum_{d \in G_{dd}}w_d}\right)}_{\text{major versions score}}
99
+ \cdot
100
+ \underbrace{\left(\frac{\sum_{d \in G_{dd}}w_d \cdot {dhs}_d}{\sum_{d \in G_{dd}}w_d}\right)}_{\text{versions score}}
101
+ \cdot
102
+ \underbrace{\left(1 +\sum_{d \in G} \sum_{a \in d} p_a\right)^{-\ln{S_A}}}_{\text{advisories score}}
103
+ ```
104
+
105
+ ```math
106
+ \begin{array}{ll}
107
+ G & \text{Gemfile} \\
108
+ G_{dd} & \text{subset of Gemfile with direct dependencies only} \\
109
+ d & \text{dependency} \\
110
+ \dotso & \text{see below for other symbols}
111
+ \end{array}
112
+ ```
113
+
114
+ It's comprised of several scores in the $[0,1]$ range multiplied together and then finally by 100 to produce the final score. Score formulas are described in the following sections.
115
+
116
+ Note that, by design, health score can never be higher than the lowest of its scores. For example, if your major versions score is 0.75, then health score can never be higher than 75, regardless of other scores being 1.
117
+
118
+ ### Major versions score
119
+
120
+ Score that signals how many dependencies have outdated major versions (it doesn't care about minor or patch versions). Score 1 means no dependency has an outdated major while score 0 means all have an outdated major. Other combinations fall in between those extremes.
121
+
122
+ [The formula](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gemfile_health_score.rb#L30-L36) $1-\frac{\sum_{d \in G_{dd}}w_d \cdot mp_d}{\sum_{d \in G_{dd}}w_d}$ starts with score 1 and is subtracted by the [weighted arithmetic mean](https://en.wikipedia.org/wiki/Weighted_arithmetic_mean) of major version penalties for all direct dependencies (only dependencies specified in the `Gemfile` and not dependencies of dependencies present in `Gemfile.lock`). The penalty controls how much the score drops when the major of a dependency is outdated, and the priority proportions that penalty in relation to other dependencies.
123
+
124
+ [Dependency priority (weight)](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L40-L44) $w_d$ is set to either a custom dependency priority, bundler group priority if dependency doesn't have a custom priority, or default priority if dependency's group doesn't have a defined priority ([default values](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L6-L8)).
125
+
126
+ [Major version penalty](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gem_health_score.rb#L21-L23) ${mp}_d$ is a number in range $[0,1]$; [by default](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L20) it equals 1. When the major isn't outdated, there is no penalty (penalty equals 0).
127
+
128
+ ### Versions score
129
+
130
+ Score that represents how outdated direct dependencies are based on the number of new versions and the kind of outdatedness. Score 1 means all dependencies are up-to-date. As dependencies get outdated, it starts to lower. Unlike major versions score, this score can never reach 0, it only gravitates towards it.
131
+
132
+ [The formula](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gemfile_health_score.rb#L38-L40) $`\frac{\sum_{d \in G_{dd}}w_d \cdot {dhs}_d}{\sum_{d \in G_{dd}}w_d}`$ is a weighted arithmetic mean of dependency health scores. Same dependency priority $w_d$ is used as for major versions score.
133
+
134
+ [Dependency health score](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gem_health_score.rb#L15-L18) ${dhs}_d$ is calculated with the following formula:
135
+ ```math
136
+ {dhs}_d=
137
+ \underbrace{(1+{sp}_d)^{-\ln{{ss}_d}}}_{\text{segment subscore}}
138
+ \cdot
139
+ \underbrace{(1+{vp}_d)^{-\ln{S_{V}}}}_{\text{versions subscore}}
140
+ ```
141
+
142
+ Both subscores use a version of the power function. See [this section](#penalty-and-severity-function) for more details on its interpretation.
143
+
144
+ #### Segment subscore
145
+
146
+ [Score](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gem_health_score.rb#L16) in the $(0,1]$ range that represents how outdated is the **first** outdated segment (major, minor or patch) of a dependency. When the current version is also the latest, the score equals 1, and it starts to drop towards 0 with the release of new versions.
147
+
148
+ [Segment penalty](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gem_health_score.rb#L43-L45) ${sp}_d$ is defined as the number of new versions for the first outdated segment. Take this example: your dependency is on `v1.0.0`, but `v1.1.0`, `v2.0.0` and `v3.0.0` have been released in the meantime. The first outdated segment is major (minor is also outdated, but it comes after major, so it's not the first). ${sp}_d$ is then the number of new majors, in this case 2.
149
+
150
+ [Segment severity](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gem_health_score.rb#L35-L37) ${ss}_d$ is a number selected based on the first outdated segment. Default list of severities can be found [here](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L22) (order `[major, minor, patch]`). For example, if major is outdated, first value in the list is used.
151
+
152
+ #### Versions subscore
153
+
154
+ [Score](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/gem_health_score.rb#L17) in the $(0,1]$ range that represents how many new versions have been released for the dependency since the current version. When the current version is also the latest, the score equals 1, and it lowers with every new version.
155
+
156
+ Penalty $`{vp}_d`$ is defined as the total number of versions between the current and the latest version (inclusive). Severity $`S_{V}`$ is a constant ([default value](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L21)).
157
+
158
+ ### Advisories score
159
+
160
+ Score in the $(0,1]$ range that represents how many security advisories impact your dependencies, taking into account their severities. Unlike previous scores, this score looks at all dependencies, direct or indirect (basically, everything in `Gemfile.lock`). Score 1 means no dependency has an active advisory, and it drops with each new advisory.
161
+
162
+ [The formula](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/advisories_health_score.rb#L12) $\left(1 +\sum_{d \in G} \sum_{a \in d} p_a\right)^{-\ln{S_A}}$ in essence sums advisory penalties $p_a$ for all advisories of all dependencies (+1) and raises it to some power. See the next section for a detailed explanation.
163
+
164
+ [Advisory penalty](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L46-L48) $p_a$ is a number selected based on the criticality (severity score mapped to a name) of the advisory ([default mapping](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L11-L17)). Generally, a higher criticality results in a higher penalty. If criticality is unknown, fallback penalty is used ([default value](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L18)). Severity $S_A$ is a constant ([default value](https://github.com/infinum/polariscope/blob/master/lib/polariscope/scanner/calculation_context.rb#L10)).
165
+
166
+ ### Penalty and severity function
167
+
168
+ Function used for several scores is of type $f(x)=(1+x)^{-S}$, where $S$ is some positive constant.
169
+
170
+ See this graph for various values $S$ (we'll focus on case $x\ge0$):
171
+ ![graph plots f(x) for three values of S: 0.05, 0.17, 0.5](./docs/severity_function_graph.png)
172
+ and notice several interesting properties:
173
+ 1. $f(0)=1$
174
+ 2. $f(x+1) \lt f(x)$
175
+ 3. $\lim_{x \to \infty} f(x)=0$
176
+ 4. bigger $S$ -> more severe "drop"
177
+
178
+ The function returns values in range $(0,1]$ (props 1-3). It begins with value 1 (prop 1) which drops the further away we move from $x=0$ (prop 2). Property 4 allows us to control how quickly the value drops.
179
+
180
+ This can be used as a simple but an okay way to model certain scores. For scoring purposes we will refer to $x$ as penalty and $S$ as severity. Take for example the [versions subscore](#versions-subscore), which uses this function: penalty is the number of new versions for a dependency, so the more new versions there are, the lower the score.
181
+
182
+ $^*$ In all formulas, severity is a natural logarithm $ln$ of some constant greater than 1. This is purely because actual constants $S$ need to be small enough (smaller than 0.1) to not cause too sharp a drop in the score too fast. It's easier to work with bigger numbers, so instead of $(1+x)^{-S}$ we work with $(1+x)^{-\ln(S)}$.
183
+
184
+ ## Motivation
185
+
186
+ Who is this tool for? What does it accomplish?
187
+
188
+ Agencies like [Infinum](https://infinum.com/) are at any point in time working on multiple projects, e.g. multiple Ruby applications. Without a monitoring process, it would be necessary to manually check each project for security vulnerabilities and new dependency versions (e.g. a new major version with a breaking change). With scale, this becomes time-consuming.
189
+
190
+ Health score is a way to monitor these things. Instead of manually checking each project for outdated dependencies (output of `bundle outdated`) and security advisories (output of `bundle-audit check`), health score informs you whether those outputs require immediate action.
191
+
192
+ As was said above, health score is most useful as a relative measure. It starts at value 100 and it drops as new versions/security issues arise. Your project might have a score of 99 one day, but suddenly drop to 90 the next — this signals something significant happened, probably a security advisory in an important dependency like Rails, or a new major version of Ruby. On the other hand, if it drops from 99 to 97, it could mean some dependency has a new minor version.
193
+
194
+ It's up to you to decide when to take action: either when the score drops suddenly (to fix immediate issues) or when it drops below a certain threshold (to update multiple dependencies in one go).
195
+
196
+ At Infinum, Polariscope is used as part of a monitoring tool that (among other things) calculates health scores for all Ruby projects daily. Part of the project table looks like this:<br />
197
+ <img src="./docs/health_score_table.png" alt="table with project health scores" width="450">
198
+
199
+ The health score is also shown as a badge on the repository README:<br />
200
+ <img src="./docs/health_score_badge.png" alt="repo readme with health score badge" width="300">
201
+
72
202
  ## Development
73
203
 
74
204
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -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,121 @@
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 # rubocop:disable Metrics/MethodLength
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
+ rescue Bundler::Dsl::DSLError => e
68
+ raise Polariscope::Error, "Unable to parse the provided Gemfile/Gemfile.lock: #{e.message}"
69
+ end
70
+
71
+ def current_dependency_version(dependency)
72
+ return ruby_scanner.version if dependency.name == GemVersions::RUBY_NAME
73
+
74
+ specs.find { |spec| dependency.name == spec.name }.version
75
+ end
76
+
77
+ def dependencies_with_ruby
78
+ return installed_dependencies unless ruby_scanner.version
79
+
80
+ installed_dependencies + [Bundler::Dependency.new(GemVersions::RUBY_NAME, false)]
81
+ end
82
+
83
+ def installed_dependencies
84
+ spec_names = specs.to_set(&:name)
85
+
86
+ bundle_definition.dependencies.select { |dependency| spec_names.include?(dependency.name) }
87
+ end
88
+
89
+ def specs
90
+ bundle_definition.locked_gems.specs
91
+ end
92
+
93
+ def ignored_advisories
94
+ audit_configuration.ignore
95
+ end
96
+
97
+ def audit_configuration
98
+ @audit_configuration ||= Tempfile.create do |file|
99
+ file.puts bundler_audit_config_content
100
+ file.rewind
101
+
102
+ Bundler::Audit::Configuration.load(file.path)
103
+ rescue StandardError
104
+ Bundler::Audit::Configuration.new
105
+ end
106
+ end
107
+
108
+ def audit_database
109
+ @audit_database ||= Bundler::Audit::Database.new
110
+ end
111
+
112
+ def parseable_gemfile_content
113
+ gemfile_content.gsub("gemspec\n", '').gsub(/^ruby.*$\R/, '')
114
+ end
115
+
116
+ def blank_value?(value)
117
+ value.nil? || value.empty?
118
+ end
119
+ end
120
+ end
121
+ 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.5.0'
5
5
  end
data/lib/polariscope.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bundler'
4
+
3
5
  require_relative 'polariscope/version'
4
- require_relative 'polariscope/scanner/codebase_health_score'
6
+ require_relative 'polariscope/scanner/gemfile_health_score'
5
7
  require_relative 'polariscope/scanner/gem_versions'
6
8
  require_relative 'polariscope/file_content'
7
9
 
@@ -9,15 +11,15 @@ module Polariscope
9
11
  Error = Class.new(StandardError)
10
12
 
11
13
  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
14
+ def scan(**opts)
15
+ Scanner::GemfileHealthScore.new(**opts.merge(
16
+ gemfile_content: opts.fetch(:gemfile_content, FileContent.for('Gemfile')),
17
+ gemfile_lock_content: opts.fetch(:gemfile_lock_content, FileContent.for('Gemfile.lock')),
18
+ bundler_audit_config_content: opts.fetch(:bundler_audit_config_content, FileContent.for('.bundler-audit.yml'))
19
+ )).health_score
18
20
  end
19
21
 
20
- def gem_versions(dependency_names, spec_type: :released)
22
+ def gem_versions(dependency_names, spec_type: Scanner::DependencyContext::DEFAULT_SPEC_TYPE)
21
23
  Scanner::GemVersions.new(dependency_names, spec_type: spec_type)
22
24
  end
23
25
  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.5.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-30 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