cvss-suite 4.1.4 → 5.0.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/{CHANGES.md → CHANGELOG.md} +30 -3
- data/LICENSE.md +2 -1
- data/README.md +80 -99
- data/docs/upgrading-to-5.md +117 -0
- data/docs/usage.md +234 -0
- data/lib/cvss_suite/cvss.rb +20 -3
- data/lib/cvss_suite/cvss2/cvss2.rb +3 -0
- data/lib/cvss_suite/cvss2/cvss2_base.rb +15 -4
- data/lib/cvss_suite/cvss2/cvss2_environmental.rb +3 -1
- data/lib/cvss_suite/cvss2/cvss2_temporal.rb +2 -0
- data/lib/cvss_suite/cvss3/cvss3.rb +4 -0
- data/lib/cvss_suite/cvss3/cvss3_base.rb +11 -1
- data/lib/cvss_suite/cvss3/cvss3_environmental.rb +43 -60
- data/lib/cvss_suite/cvss3/cvss3_temporal.rb +2 -0
- data/lib/cvss_suite/cvss31/cvss31.rb +4 -0
- data/lib/cvss_suite/cvss31/cvss31_base.rb +15 -1
- data/lib/cvss_suite/cvss31/cvss31_environmental.rb +46 -60
- data/lib/cvss_suite/cvss31/cvss31_temporal.rb +2 -0
- data/lib/cvss_suite/cvss40/cvss40.rb +10 -0
- data/lib/cvss_suite/cvss40/cvss40_all_up.rb +3 -1
- data/lib/cvss_suite/cvss40/cvss40_base.rb +8 -0
- data/lib/cvss_suite/cvss40/cvss40_calc_helper.rb +12 -8
- data/lib/cvss_suite/cvss40/cvss40_constants_levels.rb +2 -0
- data/lib/cvss_suite/cvss40/cvss40_constants_macro_vector_lookup.rb +2 -0
- data/lib/cvss_suite/cvss40/cvss40_constants_max_composed.rb +2 -0
- data/lib/cvss_suite/cvss40/cvss40_constants_max_severity.rb +2 -0
- data/lib/cvss_suite/cvss40/cvss40_environmental.rb +2 -0
- data/lib/cvss_suite/cvss40/cvss40_environmental_security.rb +2 -0
- data/lib/cvss_suite/cvss40/cvss40_supplemental.rb +2 -0
- data/lib/cvss_suite/cvss40/cvss40_threat.rb +2 -0
- data/lib/cvss_suite/cvss_31_and_before.rb +5 -3
- data/lib/cvss_suite/cvss_40_and_later.rb +3 -1
- data/lib/cvss_suite/cvss_metric.rb +4 -2
- data/lib/cvss_suite/cvss_property.rb +8 -6
- data/lib/cvss_suite/errors.rb +75 -9
- data/lib/cvss_suite/helpers/cvss31_helper.rb +6 -0
- data/lib/cvss_suite/helpers/cvss3_helper.rb +4 -0
- data/lib/cvss_suite/invalid_cvss.rb +26 -2
- data/lib/cvss_suite/version.rb +3 -1
- data/lib/cvss_suite.rb +86 -13
- metadata +9 -129
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -21
- data/.github/ISSUE_TEMPLATE/custom.md +0 -7
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
- data/.github/workflows/push.yml +0 -32
- data/.github/workflows/rspec.yml +0 -23
- data/.github/workflows/rubocop.yml +0 -21
- data/.gitignore +0 -14
- data/.rspec +0 -3
- data/.rubocop.yml +0 -69
- data/.rubocop_todo.yml +0 -59
- data/CNAME +0 -1
- data/CODE_OF_CONDUCT.md +0 -79
- data/Gemfile +0 -9
- data/PULL_REQUEST_TEMPLATE.md +0 -24
- data/Rakefile +0 -2
- data/_config.yml +0 -1
- data/bin/console +0 -14
- data/bin/setup +0 -7
- data/cvss_suite.gemspec +0 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38b3fbdc1354c4d34347faa2978c7049ed17e29ee0ed918c0ef0005a10bd9122
|
|
4
|
+
data.tar.gz: 6891802700da8fb10e65a1cb9ed46c6715cad8358961c7b903d3d68087d6f1e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9efc55c3235da09d62c3e31853075961e48b8ce1fe25e2eef32da0f3858a618571e1dc3f66e5589825001c7a1b6e79db7e80276fb1575138efe71ded4360d689
|
|
7
|
+
data.tar.gz: 9ee51323adbef6afc920dd8c44327b8e91772721ebbc3e81aacc723c7634b7868782e9dac0ca098550be1094657f9adb14b881f7d9e8d7bf1d64c9d1c5f2b60f
|
data/{CHANGES.md → CHANGELOG.md}
RENAMED
|
@@ -1,10 +1,37 @@
|
|
|
1
|
-
> [!WARNING]
|
|
2
|
-
> Starting with version 5.0.0, changes are no longer documented in this file. Please see the [Releases page](../../releases) for the changelog going forward.
|
|
3
|
-
|
|
4
1
|
# Change Log
|
|
5
2
|
All notable changes to this project will be documented in this file.
|
|
6
3
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
4
|
|
|
5
|
+
From 5.0.0 onwards, entries are generated at release time from the pull requests
|
|
6
|
+
merged since the previous release, and mirror the notes on the
|
|
7
|
+
[Releases page](https://github.com/0llirocks/cvss-suite/releases).
|
|
8
|
+
|
|
9
|
+
## 5.0.0 (2026-09-06)
|
|
10
|
+
|
|
11
|
+
## What's Changed
|
|
12
|
+
* Require Ruby >= 3.3 and modernize toolchain (v5 baseline) by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/62
|
|
13
|
+
* Bump actions/checkout from 5 to 7 by @dependabot[bot] in https://github.com/0llirocks/cvss-suite/pull/63
|
|
14
|
+
* Update simplecov requirement from ~> 0.22 to ~> 1.0 by @dependabot[bot] in https://github.com/0llirocks/cvss-suite/pull/65
|
|
15
|
+
* RuboCop 1.88 cleanup + frozen string literals (v5) by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/64
|
|
16
|
+
* deps: remove the bigdecimal upper bound by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/67
|
|
17
|
+
* feat: expose base_score on CVSS 4.0 for NVD/GHSA comparison by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/69
|
|
18
|
+
* build: package only runtime files in the gem by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/70
|
|
19
|
+
* docs: cite the CVSS spec formula behind each scoring calculation by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/71
|
|
20
|
+
* Update simplecov requirement from ~> 0.22 to ~> 1.0 by @dependabot[bot] in https://github.com/0llirocks/cvss-suite/pull/66
|
|
21
|
+
* feat: add CvssSuite.metrics for static metric introspection by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/68
|
|
22
|
+
* feat!: add CvssSuite.parse and make the parsing internals private by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/72
|
|
23
|
+
* docs: rewrite the README, and fix the scoring bug that writing it exposed by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/73
|
|
24
|
+
* ci: automate the changelog and release with release-please by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/75
|
|
25
|
+
* refactor: keep the metric lookup tables private by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/77
|
|
26
|
+
* refactor!: give every error one rescuable ancestor by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/78
|
|
27
|
+
* feat: name the rejected vector when one is not valid by @SeanLF in https://github.com/0llirocks/cvss-suite/pull/79
|
|
28
|
+
|
|
29
|
+
## New Contributors
|
|
30
|
+
* @SeanLF made their first contribution in https://github.com/0llirocks/cvss-suite/pull/62
|
|
31
|
+
* @dependabot[bot] made their first contribution in https://github.com/0llirocks/cvss-suite/pull/63
|
|
32
|
+
|
|
33
|
+
**Full Changelog**: https://github.com/0llirocks/cvss-suite/compare/v4.1.4...v5.0.0
|
|
34
|
+
|
|
8
35
|
## [4.1.4] - 2026-07-05
|
|
9
36
|
|
|
10
37
|
### Fixes
|
data/LICENSE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2016-2022 Siemens AG\
|
|
4
|
-
Copyright (c) 2022-
|
|
4
|
+
Copyright (c) 2022-2026 0llirocks
|
|
5
5
|
|
|
6
6
|
Author: 0llirocks <https://github.com/0llirocks>
|
|
7
7
|
|
|
@@ -15,6 +15,7 @@ Contributors:
|
|
|
15
15
|
- Adam Hess <https://github.com/HParker>
|
|
16
16
|
- Jason Garber <https://github.com/jgarber>
|
|
17
17
|
- Sachin Sandhu <https://github.com/sachin-sandhu>
|
|
18
|
+
- Sean Floyd <https://github.com/SeanLF>
|
|
18
19
|
|
|
19
20
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
20
21
|
this software and associated documentation files (the "Software"), to deal in
|
data/README.md
CHANGED
|
@@ -1,138 +1,119 @@
|
|
|
1
1
|
# CvssSuite for Ruby
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://rubygems.org/gems/cvss-suite)
|
|
5
|
-
[](https://www.first.org/cvss/v2/guide)
|
|
6
|
-
[](https://www.first.org/cvss/v3.0/user-guide)
|
|
7
|
-
[](https://www.first.org/cvss/v3.1/user-guide)
|
|
8
|
-
[](https://www.first.org/cvss/v4.0/user-guide)
|
|
9
|
-
[](https://github.com/0llirocks/cvss-suite/actions)
|
|
3
|
+
Score a CVSS vector string, and read back every metric in it.
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
CvssSuite turns a vector string into scores and severity, and lets you ask which option each metric
|
|
6
|
+
selected. It handles four CVSS versions behind one interface, so code that ingests advisories from
|
|
7
|
+
more than one era does not need a branch per specification version.
|
|
8
|
+
|
|
9
|
+
The parsed vector stays addressable, which is what you need to render a vector back to a user, drive
|
|
10
|
+
an input form, or explain a score rather than just report it.
|
|
13
11
|
|
|
14
12
|
> [!IMPORTANT]
|
|
15
13
|
> This project could need some new maintainer(s). I am having less time and motivation to support this gem. Support for v4 was only possible with the help of the community and I am sure I will not implement any v4.x or v5.x support by myself. Since this gem is used in some projects I will not step down without any kind of support. If you are interested in CVSS and ruby, feel free to work on upcoming issues and let me ([@Ollirocks](https://github.com/0llirocks)) know if you are willing to become a maintainer. As of today there are only a very few issues each year but each new version of CVSS results in quite a lot of work. I am fine with staying the owner of this project until someone is willing to take over completely. I will not vanish from GitHub once and or all :smile: The same applies to the ruby gems account, I am willing to push new versions to rubygems.org until someone trustworthy is found to take over.
|
|
16
14
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
Add this line to your application's Gemfile:
|
|
15
|
+
## Example
|
|
20
16
|
|
|
21
17
|
```ruby
|
|
22
|
-
|
|
23
|
-
```
|
|
18
|
+
require 'cvss_suite'
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
cvss = CvssSuite.parse('CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N')
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
cvss.version # => 4.0
|
|
23
|
+
cvss.overall_score # => 9.3
|
|
24
|
+
cvss.severity # => 'Critical'
|
|
25
|
+
cvss.base.attack_vector.name # => 'Attack Vector'
|
|
26
|
+
cvss.base.attack_vector.selected_value[:name] # => 'Network'
|
|
29
27
|
```
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
`CvssSuite.new` is the non-raising counterpart, unchanged since 4.x: it returns an object whose
|
|
30
|
+
`valid?` reports the problem instead. CVSS 2 and 3.x also expose `base_score`, `temporal_score` and
|
|
31
|
+
`environmental_score`. See [Usage](https://github.com/0llirocks/cvss-suite/blob/master/docs/usage.md)
|
|
32
|
+
for both.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
## What you get
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
- **Four specification versions, one API.** CVSS [2](https://www.first.org/cvss/v2/guide),
|
|
37
|
+
[3.0](https://www.first.org/cvss/v3.0/user-guide), [3.1](https://www.first.org/cvss/v3.1/user-guide)
|
|
38
|
+
and [4.0](https://www.first.org/cvss/v4.0/user-guide), including 4.0 macro-vector scoring.
|
|
39
|
+
`CvssSuite.parse` picks the version off the vector string.
|
|
40
|
+
- **Metrics stay readable.** Every metric exposes its name, its permitted options, and which option
|
|
41
|
+
the vector selected. `CvssSuite.metrics` returns the same schema without a vector, for building
|
|
42
|
+
input forms.
|
|
43
|
+
- **One runtime dependency.** `bigdecimal`, to keep arithmetic off binary floats.
|
|
44
|
+
- **Over 80,000 examples** in the test suite, run against the oldest supported Ruby, current
|
|
45
|
+
stable, and head.
|
|
46
|
+
- **Ruby 3.3+**, MIT licensed.
|
|
38
47
|
|
|
39
|
-
##
|
|
48
|
+
## Install
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
## Version 2.x
|
|
44
|
-
|
|
45
|
-
If you are still using CvssSuite 2.x please refer to the [specific branch](https://github.com/0llirocks/cvss-suite/tree/2.x) for documentation and changelog.
|
|
46
|
-
|
|
47
|
-
## Version 1.x
|
|
48
|
-
|
|
49
|
-
If you are still using CvssSuite 1.x please refer to the [specific branch](https://github.com/0llirocks/cvss-suite/tree/1.x) for documentation and changelog.
|
|
50
|
-
|
|
51
|
-
## Usage
|
|
50
|
+
Add the gem to your Gemfile. The gem is named `cvss-suite` but loads as `cvss_suite`, so tell
|
|
51
|
+
Bundler which file to load:
|
|
52
52
|
|
|
53
53
|
```ruby
|
|
54
|
-
require 'cvss_suite'
|
|
55
|
-
|
|
56
|
-
cvss4 = CvssSuite.new('CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N')
|
|
57
|
-
|
|
58
|
-
vector = cvss4.vector # 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N'
|
|
59
|
-
version = cvss4.version # 4.0
|
|
60
|
-
valid = cvss4.valid? # true
|
|
61
|
-
severity = cvss4.severity # 'Critical'
|
|
62
|
-
|
|
63
|
-
cvss31 = CvssSuite.new('CVSS:3.1/AV:P/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:H/E:H/RL:U/RC:U')
|
|
64
|
-
|
|
65
|
-
vector = cvss31.vector # 'CVSS:3.1/AV:P/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:H/E:H/RL:U/RC:U'
|
|
66
|
-
version = cvss31.version # 3.1
|
|
67
|
-
valid = cvss31.valid? # true
|
|
68
|
-
severity = cvss31.severity # 'Medium'
|
|
69
|
-
|
|
70
|
-
cvss3 = CvssSuite.new('CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/CR:L/IR:M/AR:H/MAV:N/MAC:H/MPR:N/MUI:R/MS:U/MC:N/MI:L/MA:H')
|
|
71
|
-
|
|
72
|
-
vector = cvss3.vector # 'CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/CR:L/IR:M/AR:H/MAV:N/MAC:H/MPR:N/MUI:R/MS:U/MC:N/MI:L/MA:H'
|
|
73
|
-
version = cvss3.version # 3.0
|
|
74
|
-
valid = cvss3.valid? # true
|
|
75
|
-
severity = cvss3.severity # 'High'
|
|
54
|
+
gem 'cvss-suite', require: 'cvss_suite'
|
|
55
|
+
```
|
|
76
56
|
|
|
77
|
-
|
|
57
|
+
Then:
|
|
78
58
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
severity = cvss.severity # 'Low'
|
|
59
|
+
```console
|
|
60
|
+
$ bundle install
|
|
61
|
+
```
|
|
83
62
|
|
|
84
|
-
|
|
85
|
-
score = cvss4.overall_score # 9.3, cvss4 only has overall score
|
|
86
|
-
base_score = cvss.base_score # 4.9
|
|
87
|
-
temporal_score = cvss.temporal_score # 3.6
|
|
88
|
-
environmental_score = cvss.environmental_score # 3.2
|
|
89
|
-
overall_score = cvss.overall_score # 3.2
|
|
63
|
+
Or without Bundler:
|
|
90
64
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
65
|
+
```console
|
|
66
|
+
$ gem install cvss-suite
|
|
67
|
+
```
|
|
94
68
|
|
|
95
|
-
|
|
96
|
-
value[:name] # 'Local', 'Adjacent Network', 'Network'
|
|
97
|
-
value[:abbreviation] # 'L', 'A', 'N'
|
|
98
|
-
value[:selected] # false, true, false
|
|
99
|
-
end
|
|
69
|
+
## Documentation
|
|
100
70
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
71
|
+
- **[Usage](https://github.com/0llirocks/cvss-suite/blob/master/docs/usage.md)** covers every score,
|
|
72
|
+
reading and enumerating metrics, and how invalid vectors behave.
|
|
73
|
+
- **[Upgrading to 5.x](https://github.com/0llirocks/cvss-suite/blob/master/docs/upgrading-to-5.md)**
|
|
74
|
+
lists the breaking changes and the migration for each.
|
|
75
|
+
- **[API reference](https://www.rubydoc.info/gems/cvss-suite)** on RubyDoc.
|
|
104
76
|
|
|
105
|
-
|
|
77
|
+
Using an older major? Documentation and changelog live on the
|
|
78
|
+
[3.x](https://github.com/0llirocks/cvss-suite/tree/3.x),
|
|
79
|
+
[2.x](https://github.com/0llirocks/cvss-suite/tree/2.x) and
|
|
80
|
+
[1.x](https://github.com/0llirocks/cvss-suite/tree/1.x) branches.
|
|
106
81
|
|
|
107
|
-
|
|
108
|
-
valid = cvss.valid? # false
|
|
109
|
-
version = cvss.version # will throw CvssSuite::Errors::InvalidVector: Vector is not valid!
|
|
110
|
-
cvss.base_score # will throw CvssSuite::Errors::InvalidVector: Vector is not valid!
|
|
82
|
+
## Known issues
|
|
111
83
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
version = cvss.version # will throw CvssSuite::Errors::InvalidVector: Vector is not valid!
|
|
115
|
-
cvss.base_score # will throw CvssSuite::Errors::InvalidVector: Vector is not valid!
|
|
84
|
+
Other implementations can produce scores differing by +/- 0.1, because floating-point arithmetic
|
|
85
|
+
differs between languages and hardware platforms.
|
|
116
86
|
|
|
117
|
-
|
|
87
|
+
## Changelog
|
|
118
88
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
cvss.base_score # will throw CvssSuite::Errors::InvalidVector: Vector is not valid!
|
|
123
|
-
```
|
|
89
|
+
Releases are recorded in
|
|
90
|
+
[CHANGELOG.md](https://github.com/0llirocks/cvss-suite/blob/master/CHANGELOG.md), which ships inside
|
|
91
|
+
the gem. The [Releases page](https://github.com/0llirocks/cvss-suite/releases) mirrors it.
|
|
124
92
|
|
|
125
|
-
##
|
|
93
|
+
## Contributing
|
|
126
94
|
|
|
127
|
-
|
|
95
|
+
Bug reports and pull requests are welcome at
|
|
96
|
+
[github.com/0llirocks/cvss-suite](https://github.com/0llirocks/cvss-suite). This project is intended
|
|
97
|
+
to be a safe, welcoming space for collaboration.
|
|
128
98
|
|
|
129
|
-
|
|
99
|
+
Pull requests are squashed, so the title you write becomes the commit message and picks the next
|
|
100
|
+
version. Title it as a [conventional commit](https://www.conventionalcommits.org):
|
|
130
101
|
|
|
131
|
-
|
|
102
|
+
| Title starts with | Releases |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| any type with a `!`, such as `feat!:` or `refactor!:` | a major |
|
|
105
|
+
| `feat:` | a minor |
|
|
106
|
+
| any other accepted type: `fix:`, `docs:`, `test:`, `refactor:`, `style:`, `perf:`, `chore:`, `build:`, `ci:`, `revert:` | a patch |
|
|
107
|
+
| a title the check rejects | nothing at all |
|
|
132
108
|
|
|
133
|
-
|
|
109
|
+
Note there is no such thing as a merge that ships nothing quietly: every accepted type releases at
|
|
110
|
+
least a patch. A title the automation cannot parse is skipped silently instead, which is what the
|
|
111
|
+
check exists to catch, while the title is still easy to fix. Your change is credited in the changelog
|
|
112
|
+
whichever prefix it carries -- the prefix only decides the version.
|
|
134
113
|
|
|
135
|
-
|
|
114
|
+
The description becomes the body of that commit, so write it for someone reading `git log` in two
|
|
115
|
+
years. For a breaking change, add a `BREAKING CHANGE:` line saying what a user has to change.
|
|
136
116
|
|
|
137
117
|
## References
|
|
118
|
+
|
|
138
119
|
[CvssSuite for .NET](https://cvsssuite.0lli.rocks)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Upgrading to 5.x
|
|
2
|
+
|
|
3
|
+
Most callers need no change. `CvssSuite.new` and every score for a valid vector behave as they did
|
|
4
|
+
in 4.x. What follows is the exceptions.
|
|
5
|
+
|
|
6
|
+
## Ruby 3.3 is the minimum
|
|
7
|
+
|
|
8
|
+
5.x requires Ruby 3.3 or newer. 4.1.4 ran on Ruby 2.6 and newer.
|
|
9
|
+
|
|
10
|
+
## Invalid vectors no longer score
|
|
11
|
+
|
|
12
|
+
In 4.x, only `base_score` checked that the vector was valid before computing. `temporal_score` and
|
|
13
|
+
`environmental_score` did not, so a vector that `valid?` had already rejected could still hand back
|
|
14
|
+
a plausible-looking number:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
cvss = CvssSuite.new('AV:A/AC:H/Au:M/C:C/I:C/A:C/ZZ:Q') # ZZ is not a CVSS 2 metric
|
|
18
|
+
|
|
19
|
+
cvss.valid? # => false, in both 4.x and 5.x
|
|
20
|
+
cvss.environmental_score # 4.x: 6.2
|
|
21
|
+
# 5.x: raises CvssSuite::Errors::InvalidVector
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`overall_score` on the `InvalidCvss` returned for unrecognized input used to raise `NoMethodError`
|
|
25
|
+
rather than `InvalidVector`. It now raises `InvalidVector` like every other reader.
|
|
26
|
+
|
|
27
|
+
If you were rescuing `TypeError` or `NoMethodError` around these calls, rescue
|
|
28
|
+
`CvssSuite::Errors::InvalidVector` instead. If you were checking `valid?` first, nothing changes.
|
|
29
|
+
|
|
30
|
+
## Three module methods are now private
|
|
31
|
+
|
|
32
|
+
`CvssSuite.version`, `CvssSuite.prepare_vector` and `CvssSuite.prepare_cvss2_vector` were public,
|
|
33
|
+
but only because `CvssSuite.new` needed them. They read module-level state that `CvssSuite.new`
|
|
34
|
+
writes, so their return value depended on whichever vector was parsed last, anywhere in the
|
|
35
|
+
process. Calling them directly could not be relied on, and now raises `NoMethodError`.
|
|
36
|
+
|
|
37
|
+
To get the CVSS version of a vector, ask the vector:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# Before (raises NoMethodError as of 5.x)
|
|
41
|
+
CvssSuite.new('CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H')
|
|
42
|
+
CvssSuite.version # 3.1, but only until anything else parses a vector
|
|
43
|
+
|
|
44
|
+
# After
|
|
45
|
+
CvssSuite.new('CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H').version # => 3.1
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`CvssSuite.version` was never the gem version; that is `CvssSuite::VERSION`, unchanged.
|
|
49
|
+
|
|
50
|
+
There is no replacement for `prepare_vector` and `prepare_cvss2_vector`. They strip the `CVSS:x.x/`
|
|
51
|
+
prefix, or the surrounding parentheses of a CVSS 2 vector, before the vector reaches the parser, and
|
|
52
|
+
that is now an implementation detail of `CvssSuite.new`.
|
|
53
|
+
|
|
54
|
+
## `InvalidVector` messages now name the vector
|
|
55
|
+
|
|
56
|
+
The message was the bare `Vector is not valid!` regardless of input. It now says which vector:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
CvssSuite.parse('CVSS:3.0/')
|
|
60
|
+
# 4.x: CvssSuite::Errors::InvalidVector: Vector is not valid!
|
|
61
|
+
# 5.x: CvssSuite::Errors::InvalidVector: Vector is not valid: "CVSS:3.0/"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Nothing about the class, or about when it is raised, has changed. Only match on the message if you
|
|
65
|
+
have to, and match a prefix rather than the whole string: the vector is repeated back through
|
|
66
|
+
`inspect`, and input longer than 200 characters is trimmed with a trailing `...`.
|
|
67
|
+
|
|
68
|
+
The vector named is the one you passed, not the parsed form — so a CVSS 2 vector in parentheses is
|
|
69
|
+
reported with its parentheses, and a non-string reads as itself (`CvssSuite.new(1337)` reports
|
|
70
|
+
`1337`, not `""`).
|
|
71
|
+
|
|
72
|
+
## `Errors::CvssError` is gone, replaced by `CvssSuite::Error`
|
|
73
|
+
|
|
74
|
+
`CvssSuite::Errors::CvssError` was documented as "the base error class to be inherited by more
|
|
75
|
+
specific classes", but nothing inherited it and the gem never raised it, so `rescue
|
|
76
|
+
CvssSuite::Errors::CvssError` caught nothing. It has been removed. In its place, every error the gem
|
|
77
|
+
raises includes the module `CvssSuite::Error`:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
begin
|
|
81
|
+
CvssSuite.metrics(version) # raises UnsupportedVersion
|
|
82
|
+
CvssSuite.parse(vector) # raises InvalidVector
|
|
83
|
+
rescue CvssSuite::Error => e # one rescue for either
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The concrete classes and their ancestors are unchanged: `InvalidVector` is still a `RuntimeError`,
|
|
88
|
+
`InvalidParentClass` and `UnsupportedVersion` are still `ArgumentError`s, and every `rescue` naming
|
|
89
|
+
those keeps working. This matters more in 5.x than it did in 4.x, because `CvssSuite.parse` makes an
|
|
90
|
+
exception the expected way to hear about a bad vector.
|
|
91
|
+
|
|
92
|
+
You need to change something only if you referenced `CvssError` yourself — raising it, subclassing
|
|
93
|
+
it, or rescuing it. Rescuing it caught nothing, so deleting that `rescue` is the fix. Note
|
|
94
|
+
`CvssSuite::Error` is a module, so `raise CvssSuite::Error` is a `TypeError`; raise one of the
|
|
95
|
+
classes under `CvssSuite::Errors` instead.
|
|
96
|
+
|
|
97
|
+
## `CvssSuite.parse` is new
|
|
98
|
+
|
|
99
|
+
`CvssSuite.parse` raises `CvssSuite::Errors::InvalidVector` on input it cannot parse, where
|
|
100
|
+
`CvssSuite.new` returns an object that reports the problem only through `valid?` and the scores.
|
|
101
|
+
This is additive; `CvssSuite.new` is unchanged. See
|
|
102
|
+
[Parsing a vector](usage.md#parsing-a-vector).
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# 4.x, still works
|
|
106
|
+
cvss = CvssSuite.new(untrusted_input)
|
|
107
|
+
return render_error unless cvss.valid?
|
|
108
|
+
|
|
109
|
+
# 5.x, when a bad vector is a bug rather than an input
|
|
110
|
+
cvss = CvssSuite.parse(vector_from_our_own_database)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## What did not change
|
|
114
|
+
|
|
115
|
+
Scores for valid vectors are unchanged across all four CVSS versions. `CvssSuite.new` still never
|
|
116
|
+
raises, and still returns the same object for the same input. Metric names, permitted options and
|
|
117
|
+
selected options are unchanged.
|
data/docs/usage.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Usage
|
|
2
|
+
|
|
3
|
+
Every score, every metric, and what invalid vectors do.
|
|
4
|
+
|
|
5
|
+
## Parsing a vector
|
|
6
|
+
|
|
7
|
+
`CvssSuite.parse` and `CvssSuite.new` return the same object for a valid vector. They differ on
|
|
8
|
+
input they cannot parse; pick the failure mode you want.
|
|
9
|
+
|
|
10
|
+
**`CvssSuite.parse` raises immediately** on any bad input:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
CvssSuite.parse('CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H').base_score
|
|
14
|
+
# => 9.8
|
|
15
|
+
CvssSuite.parse('CVSS:3.0/')
|
|
16
|
+
# => raises CvssSuite::Errors::InvalidVector: Vector is not valid: "CVSS:3.0/"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**`CvssSuite.new` never raises.** It hands back an object whose `valid?` is `false` and defers the
|
|
20
|
+
error to whatever eventually reads a score. What that object is depends on whether the vector's
|
|
21
|
+
prefix was recognized:
|
|
22
|
+
|
|
23
|
+
| `CvssSuite.new(...)` | returns | `valid?` | `version` | `base_score` |
|
|
24
|
+
| --- | --- | --- | --- | --- |
|
|
25
|
+
| `'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H'` | `Cvss31` | `true` | `3.1` | `9.8` |
|
|
26
|
+
| `'CVSS:3.0/'` | `Cvss3` | `false` | `3.0` | raises `Errors::InvalidVector` |
|
|
27
|
+
| `'AV:N/AC:L'` | `Cvss2` | `false` | `2` | raises `Errors::InvalidVector` |
|
|
28
|
+
| `'random_string'` | `InvalidCvss` | `false` | raises `Errors::InvalidVector` | raises `Errors::InvalidVector` |
|
|
29
|
+
| `1337` | `InvalidCvss` | `false` | raises `Errors::InvalidVector` | raises `Errors::InvalidVector` |
|
|
30
|
+
|
|
31
|
+
Note rows two and three: a recognized prefix with an unusable body still comes back as a real
|
|
32
|
+
version class.
|
|
33
|
+
|
|
34
|
+
Use `CvssSuite.new` when an invalid vector is an expected input you intend to branch on:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
cvss = CvssSuite.new(untrusted_input)
|
|
38
|
+
return render_error unless cvss.valid?
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use `CvssSuite.parse` when an invalid vector is a bug. The object `CvssSuite.new` returns stays
|
|
42
|
+
quiet until something asks it for a number, so a caller who forgets `valid?` meets the exception far
|
|
43
|
+
from the input that caused it. Or never, if that read sits behind a conditional.
|
|
44
|
+
|
|
45
|
+
Both raise `ArgumentError` when called with no argument.
|
|
46
|
+
|
|
47
|
+
CVSS 2 vectors are accepted with or without surrounding parentheses. Otherwise the string must begin
|
|
48
|
+
with the vector: anything before the `CVSS:x.x/` prefix, or before a CVSS 2 vector's first metric,
|
|
49
|
+
makes it unparseable.
|
|
50
|
+
|
|
51
|
+
## Scores
|
|
52
|
+
|
|
53
|
+
CVSS 2, 3.0 and 3.1 expose the three sub-scores plus an overall score:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
cvss = CvssSuite.parse('AV:A/AC:M/Au:S/C:P/I:P/A:P/E:POC/RL:TF/RC:UC/CDP:L/TD:M/CR:M/IR:M/AR:M')
|
|
57
|
+
|
|
58
|
+
cvss.base_score # => 4.9
|
|
59
|
+
cvss.temporal_score # => 3.6
|
|
60
|
+
cvss.environmental_score # => 3.2
|
|
61
|
+
cvss.overall_score # => 3.2
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`overall_score` is the most specific score the vector provides:
|
|
65
|
+
|
|
66
|
+
- the environmental score, if environmental metrics were given
|
|
67
|
+
- otherwise the temporal score, if temporal metrics were given
|
|
68
|
+
- otherwise the base score
|
|
69
|
+
|
|
70
|
+
CVSS 4.0 defines a single score. `overall_score` is that score, and `base_score` is exposed
|
|
71
|
+
alongside it so a 4.0 vector can be compared against the base score that NVD and GHSA publish:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
cvss = CvssSuite.parse('CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N')
|
|
75
|
+
|
|
76
|
+
cvss.overall_score # => 9.3
|
|
77
|
+
cvss.base_score # => 9.3
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Severity
|
|
81
|
+
|
|
82
|
+
`severity` maps a score onto the qualitative rating from the specification. The two rating scales
|
|
83
|
+
differ, and so does the score each one reads:
|
|
84
|
+
|
|
85
|
+
| | scale | reads |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| CVSS 3.0, 3.1, 4.0 | None, Low, Medium, High, Critical | `overall_score` |
|
|
88
|
+
| CVSS 2 | Low, Medium, High | `base_score` |
|
|
89
|
+
|
|
90
|
+
CVSS 2 predates the Critical band and rates on the base score, so a CVSS 2 vector can report a severity
|
|
91
|
+
that looks inconsistent with its own `overall_score`:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
cvss = CvssSuite.parse('AV:A/AC:M/Au:S/C:P/I:P/A:P/E:POC/RL:TF/RC:UC/CDP:L/TD:M/CR:M/IR:M/AR:M')
|
|
95
|
+
|
|
96
|
+
cvss.base_score # => 4.9
|
|
97
|
+
cvss.overall_score # => 3.2
|
|
98
|
+
cvss.severity # => 'Medium', rated on the 4.9 base score
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Reading metrics
|
|
102
|
+
|
|
103
|
+
A parsed vector exposes its metric groups. Every version has `base`. CVSS 2 and 3.x add `temporal`
|
|
104
|
+
and `environmental`. CVSS 4.0 has `threat`, `environmental`, `environmental_security` and
|
|
105
|
+
`supplemental`. Each metric answers its human-readable name, all permitted options, and the option
|
|
106
|
+
this vector selected.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
cvss = CvssSuite.parse('AV:A/AC:M/Au:S/C:P/I:P/A:P/E:POC/RL:TF/RC:UC/CDP:L/TD:M/CR:M/IR:M/AR:M')
|
|
110
|
+
|
|
111
|
+
access_vector = cvss.base.access_vector
|
|
112
|
+
|
|
113
|
+
access_vector.name # => 'Access Vector'
|
|
114
|
+
access_vector.selected_value[:name] # => 'Adjacent Network'
|
|
115
|
+
|
|
116
|
+
access_vector.values
|
|
117
|
+
# => [{ name: 'Network', abbreviation: 'N', weight: 1.0, selected: false },
|
|
118
|
+
# { name: 'Adjacent Network', abbreviation: 'A', weight: 0.646, selected: true },
|
|
119
|
+
# { name: 'Local', abbreviation: 'L', weight: 0.395, selected: false }]
|
|
120
|
+
|
|
121
|
+
cvss.temporal.remediation_level.name # => 'Remediation Level'
|
|
122
|
+
cvss.temporal.remediation_level.selected_value[:name] # => 'Temporary Fix'
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
On CVSS 2 and 3.x, `weight` is the numeric coefficient the specification assigns to that option. The
|
|
126
|
+
score is computed from it; it is exposed so you can show your working. CVSS 4.0 options carry no
|
|
127
|
+
`weight`, because 4.0 scores through a macro-vector lookup rather than per-option coefficients.
|
|
128
|
+
|
|
129
|
+
The original vector string is available too:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
CvssSuite.parse('CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N').vector
|
|
133
|
+
# => 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
One asymmetry: a CVSS 2 vector given in parentheses comes back without them.
|
|
137
|
+
`CvssSuite.parse('(AV:N/AC:L/Au:N/C:N/I:N/A:C)').vector` returns
|
|
138
|
+
`'AV:N/AC:L/Au:N/C:N/I:N/A:C'`.
|
|
139
|
+
|
|
140
|
+
To enumerate every metric in a group rather than naming one, use `properties`:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
cvss.base.properties.map(&:abbreviation)
|
|
144
|
+
# => ['AV', 'AC', 'Au', 'C', 'I', 'A']
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Metrics without a vector
|
|
148
|
+
|
|
149
|
+
`CvssSuite.metrics` returns the schema for a CVSS version without parsing anything, which is what
|
|
150
|
+
you want when building an input form or validating against the specification rather than against a
|
|
151
|
+
particular vector:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
schema = CvssSuite.metrics(3.1)
|
|
155
|
+
|
|
156
|
+
schema.map { |group| group[:group] }
|
|
157
|
+
# => ['Base', 'Temporal', 'Environmental']
|
|
158
|
+
|
|
159
|
+
schema.first[:metrics].first
|
|
160
|
+
# => { name: 'Attack Vector',
|
|
161
|
+
# abbreviation: 'AV',
|
|
162
|
+
# options: [{ name: 'Network', abbreviation: 'N', default: false },
|
|
163
|
+
# { name: 'Adjacent', abbreviation: 'A', default: false },
|
|
164
|
+
# { name: 'Local', abbreviation: 'L', default: false },
|
|
165
|
+
# { name: 'Physical', abbreviation: 'P', default: false }] }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Optional metrics flag their Not Defined option as the default, so a form can preselect it:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
temporal = CvssSuite.metrics(3.1).find { |group| group[:group] == 'Temporal' }
|
|
172
|
+
temporal[:metrics].first[:name] # => 'Exploit Code Maturity'
|
|
173
|
+
temporal[:metrics].first[:options].select { |o| o[:default] }.map { |o| o[:abbreviation] }
|
|
174
|
+
# => ['X']
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
It accepts `2`, `3.0`, `3.1` and `4.0`, the string equivalents of each, and the value a parsed
|
|
178
|
+
vector reports from `version`. Anything else raises `CvssSuite::Errors::UnsupportedVersion`, which
|
|
179
|
+
descends from `ArgumentError`.
|
|
180
|
+
|
|
181
|
+
## Exceptions
|
|
182
|
+
|
|
183
|
+
Reading a score or a version off an invalid vector raises `CvssSuite::Errors::InvalidVector`:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
cvss = CvssSuite.new('random_string')
|
|
187
|
+
|
|
188
|
+
cvss.valid? # => false
|
|
189
|
+
cvss.version # => raises CvssSuite::Errors::InvalidVector: Vector is not valid: "random_string"
|
|
190
|
+
cvss.base_score # => raises CvssSuite::Errors::InvalidVector: Vector is not valid: "random_string"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The message names the vector, so an exception surfacing well away from wherever the vector came from
|
|
194
|
+
still says which one it was. It is the vector you passed, not the parsed form, so a CVSS 2 vector in
|
|
195
|
+
parentheses keeps them. Input longer than 200 characters is trimmed with a trailing `...`; a CVSS 4.0
|
|
196
|
+
vector carrying every metric the specification defines, each at its longest option, is 178, so real
|
|
197
|
+
vectors are repeated back whole.
|
|
198
|
+
|
|
199
|
+
A vector whose prefix was recognized but whose body is unusable still answers `version`, and only
|
|
200
|
+
reports the problem through `valid?` and the scores:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
cvss = CvssSuite.new('AV:N/AC:P/C:P/AV:U/RL:OF/RC:C') # authentication is missing
|
|
204
|
+
|
|
205
|
+
cvss.version # => 2
|
|
206
|
+
cvss.valid? # => false
|
|
207
|
+
cvss.base_score # => raises CvssSuite::Errors::InvalidVector: Vector is not valid: "AV:N/AC:P/C:P/AV:U/RL:OF/RC:C"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Every score reader behaves the same way. On CVSS 2 and 3.x that is `base_score`, `temporal_score`,
|
|
211
|
+
`environmental_score`, `overall_score` and `severity`. CVSS 4.0 folds threat and environmental
|
|
212
|
+
metrics into its single score, so it defines only `base_score`, `overall_score` and `severity`;
|
|
213
|
+
calling `temporal_score` or `environmental_score` on a 4.0 vector raises `NoMethodError` whether or
|
|
214
|
+
not the vector is valid.
|
|
215
|
+
|
|
216
|
+
The metric groups themselves are not guarded. `cvss.base`, `cvss.temporal` and `cvss.environmental`
|
|
217
|
+
hand back live objects on an invalid vector, and their `score` will compute from it. Read scores
|
|
218
|
+
through the methods above rather than through the metric groups.
|
|
219
|
+
|
|
220
|
+
Every error class lives under `CvssSuite::Errors`, and every one of them includes `CvssSuite::Error`.
|
|
221
|
+
Rescue that to catch anything this gem raises without naming the classes one by one:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
begin
|
|
225
|
+
schema = CvssSuite.metrics(params[:version]) # raises UnsupportedVersion
|
|
226
|
+
cvss = CvssSuite.parse(params[:vector]) # raises InvalidVector
|
|
227
|
+
rescue CvssSuite::Error => e
|
|
228
|
+
render_error(e.message)
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`CvssSuite::Error` is a module, not a base class, so the concrete errors keep the ancestors you may
|
|
233
|
+
already be rescuing: `InvalidVector` descends from `RuntimeError`, so a bare `rescue => e` still
|
|
234
|
+
catches it, and `UnsupportedVersion` and `InvalidParentClass` descend from `ArgumentError`.
|