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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGES.md → CHANGELOG.md} +30 -3
  3. data/LICENSE.md +2 -1
  4. data/README.md +80 -99
  5. data/docs/upgrading-to-5.md +117 -0
  6. data/docs/usage.md +234 -0
  7. data/lib/cvss_suite/cvss.rb +20 -3
  8. data/lib/cvss_suite/cvss2/cvss2.rb +3 -0
  9. data/lib/cvss_suite/cvss2/cvss2_base.rb +15 -4
  10. data/lib/cvss_suite/cvss2/cvss2_environmental.rb +3 -1
  11. data/lib/cvss_suite/cvss2/cvss2_temporal.rb +2 -0
  12. data/lib/cvss_suite/cvss3/cvss3.rb +4 -0
  13. data/lib/cvss_suite/cvss3/cvss3_base.rb +11 -1
  14. data/lib/cvss_suite/cvss3/cvss3_environmental.rb +43 -60
  15. data/lib/cvss_suite/cvss3/cvss3_temporal.rb +2 -0
  16. data/lib/cvss_suite/cvss31/cvss31.rb +4 -0
  17. data/lib/cvss_suite/cvss31/cvss31_base.rb +15 -1
  18. data/lib/cvss_suite/cvss31/cvss31_environmental.rb +46 -60
  19. data/lib/cvss_suite/cvss31/cvss31_temporal.rb +2 -0
  20. data/lib/cvss_suite/cvss40/cvss40.rb +10 -0
  21. data/lib/cvss_suite/cvss40/cvss40_all_up.rb +3 -1
  22. data/lib/cvss_suite/cvss40/cvss40_base.rb +8 -0
  23. data/lib/cvss_suite/cvss40/cvss40_calc_helper.rb +12 -8
  24. data/lib/cvss_suite/cvss40/cvss40_constants_levels.rb +2 -0
  25. data/lib/cvss_suite/cvss40/cvss40_constants_macro_vector_lookup.rb +2 -0
  26. data/lib/cvss_suite/cvss40/cvss40_constants_max_composed.rb +2 -0
  27. data/lib/cvss_suite/cvss40/cvss40_constants_max_severity.rb +2 -0
  28. data/lib/cvss_suite/cvss40/cvss40_environmental.rb +2 -0
  29. data/lib/cvss_suite/cvss40/cvss40_environmental_security.rb +2 -0
  30. data/lib/cvss_suite/cvss40/cvss40_supplemental.rb +2 -0
  31. data/lib/cvss_suite/cvss40/cvss40_threat.rb +2 -0
  32. data/lib/cvss_suite/cvss_31_and_before.rb +5 -3
  33. data/lib/cvss_suite/cvss_40_and_later.rb +3 -1
  34. data/lib/cvss_suite/cvss_metric.rb +4 -2
  35. data/lib/cvss_suite/cvss_property.rb +8 -6
  36. data/lib/cvss_suite/errors.rb +75 -9
  37. data/lib/cvss_suite/helpers/cvss31_helper.rb +6 -0
  38. data/lib/cvss_suite/helpers/cvss3_helper.rb +4 -0
  39. data/lib/cvss_suite/invalid_cvss.rb +26 -2
  40. data/lib/cvss_suite/version.rb +3 -1
  41. data/lib/cvss_suite.rb +86 -13
  42. metadata +9 -129
  43. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -21
  44. data/.github/ISSUE_TEMPLATE/custom.md +0 -7
  45. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
  46. data/.github/workflows/push.yml +0 -32
  47. data/.github/workflows/rspec.yml +0 -23
  48. data/.github/workflows/rubocop.yml +0 -21
  49. data/.gitignore +0 -14
  50. data/.rspec +0 -3
  51. data/.rubocop.yml +0 -69
  52. data/.rubocop_todo.yml +0 -59
  53. data/CNAME +0 -1
  54. data/CODE_OF_CONDUCT.md +0 -79
  55. data/Gemfile +0 -9
  56. data/PULL_REQUEST_TEMPLATE.md +0 -24
  57. data/Rakefile +0 -2
  58. data/_config.yml +0 -1
  59. data/bin/console +0 -14
  60. data/bin/setup +0 -7
  61. data/cvss_suite.gemspec +0 -49
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3efb3fc75b7c4cc0428b409767c3abc83c2efa00b0dce57597c933fd2e233f3
4
- data.tar.gz: ec150b5056b70c831088d49028c69e846dff6194e4043dde7fe5d31235c18613
3
+ metadata.gz: 38b3fbdc1354c4d34347faa2978c7049ed17e29ee0ed918c0ef0005a10bd9122
4
+ data.tar.gz: 6891802700da8fb10e65a1cb9ed46c6715cad8358961c7b903d3d68087d6f1e1
5
5
  SHA512:
6
- metadata.gz: 49532a6b6954c2b12734990897134beec7c8ccbadb2420111dc7c32a4858ed7a03a691226e879144f75fbdd1dc6f4e67351907d3e2f11606db8235d829394fbe
7
- data.tar.gz: 6ffd130cc5107b9aeec32eb37601ef50c8c55001fe20553e46c2b82f353e72b15f4ce089f2c75959d5f9e51df63db5ea3f8795410261549cedf4f3c5fd41b798
6
+ metadata.gz: 9efc55c3235da09d62c3e31853075961e48b8ce1fe25e2eef32da0f3858a618571e1dc3f66e5589825001c7a1b6e79db7e80276fb1575138efe71ded4360d689
7
+ data.tar.gz: 9ee51323adbef6afc920dd8c44327b8e91772721ebbc3e81aacc723c7634b7868782e9dac0ca098550be1094657f9adb14b881f7d9e8d7bf1d64c9d1c5f2b60f
@@ -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-2024 0llirocks
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
- [![Gem Version](http://img.shields.io/gem/v/cvss-suite.svg)](https://rubygems.org/gems/cvss-suite)
4
- [![Ruby Version](https://img.shields.io/badge/Ruby-2.6-brightgreen.svg)](https://rubygems.org/gems/cvss-suite)
5
- [![Cvss Support](https://img.shields.io/badge/CVSS-v2-brightgreen.svg)](https://www.first.org/cvss/v2/guide)
6
- [![Cvss Support](https://img.shields.io/badge/CVSS-v3.0-brightgreen.svg)](https://www.first.org/cvss/v3.0/user-guide)
7
- [![Cvss Support](https://img.shields.io/badge/CVSS-v3.1-brightgreen.svg)](https://www.first.org/cvss/v3.1/user-guide)
8
- [![Cvss Support](https://img.shields.io/badge/CVSS-v4.0-brightgreen.svg)](https://www.first.org/cvss/v4.0/user-guide)
9
- [![RSpec](https://github.com/0llirocks/cvss-suite/workflows/RSpec/badge.svg)](https://github.com/0llirocks/cvss-suite/actions)
3
+ Score a CVSS vector string, and read back every metric in it.
10
4
 
11
- This Ruby gem helps you to process the vector of the [**Common Vulnerability Scoring System**](https://www.first.org/cvss/specification-document).
12
- Besides calculating the Base, Temporal and Environmental Score, you are able to extract the selected option.
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
- ## Installation
18
-
19
- Add this line to your application's Gemfile:
15
+ ## Example
20
16
 
21
17
  ```ruby
22
- gem 'cvss-suite'
23
- ```
18
+ require 'cvss_suite'
24
19
 
25
- Since the naming of this gem is not following the naming convention you can also add the following line to automatically require the gem:
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
- ```ruby
28
- gem 'cvss-suite', require: 'cvss_suite'
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
- And then execute:
32
-
33
- $ bundle
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
- Or install it yourself as:
34
+ ## What you get
36
35
 
37
- $ gem install cvss-suite
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
- ## Version 3.x
48
+ ## Install
40
49
 
41
- If you are still using CvssSuite 3.x please refer to the [specific branch](https://github.com/0llirocks/cvss-suite/tree/3.x) for documentation and changelog.
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
- cvss = CvssSuite.new('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
+ Then:
78
58
 
79
- vector = cvss.vector # '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'
80
- version = cvss.version # 2
81
- valid = cvss.valid? # true
82
- severity = cvss.severity # 'Low'
59
+ ```console
60
+ $ bundle install
61
+ ```
83
62
 
84
- # Scores
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
- # Available options
92
- access_vector = cvss.base.access_vector.name # 'Access Vector'
93
- remediation_level = cvss.temporal.remediation_level.name # 'Remediation Level'
65
+ ```console
66
+ $ gem install cvss-suite
67
+ ```
94
68
 
95
- access_vector.values.each do |value|
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
- # Selected options
102
- cvss.base.access_vector.selected_value[:name] # Adjacent Network
103
- cvss.temporal.remediation_level.selected_value[:name] # Temporary Fix
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
- # Exceptions
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
- cvss = CvssSuite.new('random_string') # invalid vector
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
- cvss = CvssSuite.new(1337) # invalid vector
113
- valid = cvss.valid? # false
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
- CvssSuite.new() # will throw a ArgumentError
87
+ ## Changelog
118
88
 
119
- cvss = CvssSuite.new('AV:N/AC:P/C:P/AV:U/RL:OF/RC:C') # invalid vector, authentication is missing
120
- version = cvss.version # 2
121
- valid = cvss.valid? # false
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
- ## Known Issues
93
+ ## Contributing
126
94
 
127
- There is a possibility of implementations generating different scores (+/- 0,1) due to small floating-point inaccuracies. This can happen due to differences in floating point arithmetic between different languages and hardware platforms.
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
- ## Changelog
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
- [Click here to see all changes.](https://github.com/0llirocks/cvss-suite/blob/master/CHANGES.md)
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
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/0llirocks/cvss-suite. This project is intended to be a safe, welcoming space for collaboration.
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`.