polariscope 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +165 -35
- data/lib/polariscope/scanner/dependency_context.rb +3 -1
- data/lib/polariscope/version.rb +1 -1
- data/lib/polariscope.rb +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70b3d555c09bbe2378d8771d0975b34427f089700b9e7852917ffe80e6a79259
|
4
|
+
data.tar.gz: 1e87403b8f56622b02b11b5aab0439d503ae6410c88dcb4fa7213464be2e7ffe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19874d31ca4475d6f3976935270d7c80816f12094b5fc31f25fa80571ddc161ca61a3645b250d91ce8b8bed4b6fbb73e057c90fc2fc15c6b8c99bce4e34b0dda
|
7
|
+
data.tar.gz: 8e71615763f0a844074a2b8c214b6c2b7698344d238774c9c2245fe118808cdeed9be611e54c2d723678a03208f6a5c1e77497cb7962e2dc281fea9a71beefa0
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,74 +1,204 @@
|
|
1
|
-
# Polariscope
|
1
|
+
# Polariscope 🔬
|
2
2
|
|
3
|
-
Polariscope is a Ruby gem
|
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
|
-
|
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
|
-
|
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
|
-
|
11
|
+
Add it to your Gemfile:
|
14
12
|
|
15
13
|
$ bundle add polariscope
|
16
14
|
|
17
|
-
|
15
|
+
or install standalone:
|
18
16
|
|
19
17
|
$ gem install polariscope
|
20
18
|
|
21
|
-
|
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
|
-
|
31
|
-
^^^^^^^^^^
|
32
|
-
```
|
21
|
+
Polariscope can be used on the CLI and in code.
|
33
22
|
|
34
|
-
|
23
|
+
### CLI
|
35
24
|
|
36
|
-
|
25
|
+
Position yourself at the root of your Ruby application and run:
|
37
26
|
|
38
|
-
|
27
|
+
$ [bundle exec] polariscope scan
|
28
|
+
=> 87.4
|
39
29
|
|
40
|
-
|
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
|
-
|
32
|
+
### In code
|
43
33
|
|
44
|
-
```
|
45
|
-
|
34
|
+
```ruby
|
35
|
+
health_score = Polariscope.scan
|
46
36
|
```
|
47
37
|
|
48
|
-
|
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
|
-
|
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(['
|
65
|
-
gem_specs.versions_for('
|
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(['
|
69
|
-
gem_specs.versions_for('
|
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.
|
@@ -51,7 +51,7 @@ module Polariscope
|
|
51
51
|
@gem_versions ||= GemVersions.new(dependencies.map(&:name), spec_type: spec_type)
|
52
52
|
end
|
53
53
|
|
54
|
-
def bundle_definition
|
54
|
+
def bundle_definition # rubocop:disable Metrics/MethodLength
|
55
55
|
@bundle_definition ||=
|
56
56
|
::Tempfile.create do |gemfile|
|
57
57
|
::Tempfile.create do |gemfile_lock|
|
@@ -64,6 +64,8 @@ module Polariscope
|
|
64
64
|
Bundler::Definition.build(gemfile.path, gemfile_lock.path, false)
|
65
65
|
end
|
66
66
|
end
|
67
|
+
rescue Bundler::Dsl::DSLError => e
|
68
|
+
raise Polariscope::Error, "Unable to parse the provided Gemfile/Gemfile.lock: #{e.message}"
|
67
69
|
end
|
68
70
|
|
69
71
|
def current_dependency_version(dependency)
|
data/lib/polariscope/version.rb
CHANGED
data/lib/polariscope.rb
CHANGED
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.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-
|
11
|
+
date: 2024-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|