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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'cvss40_constants_macro_vector_lookup'
|
|
2
4
|
require_relative 'cvss40_constants_max_composed'
|
|
3
5
|
require_relative 'cvss40_constants_max_severity'
|
|
@@ -96,6 +98,9 @@ module CvssSuite
|
|
|
96
98
|
# 1-E:P
|
|
97
99
|
# 2-E:U
|
|
98
100
|
|
|
101
|
+
# The `else` fallback intentionally mirrors the 'A' branch pending upstream
|
|
102
|
+
# clarification (see TODO below).
|
|
103
|
+
# rubocop:disable-next Lint/DuplicateBranch
|
|
99
104
|
eq5 = case m('E')
|
|
100
105
|
when 'A'
|
|
101
106
|
'0'
|
|
@@ -152,7 +157,10 @@ module CvssSuite
|
|
|
152
157
|
eq1_next_lower_macro = concat_and_stringify(eq1_val + 1, eq2_val, eq3_val, eq4_val, eq5_val, eq6_val)
|
|
153
158
|
eq2_next_lower_macro = concat_and_stringify(eq1_val, eq2_val + 1, eq3_val, eq4_val, eq5_val, eq6_val)
|
|
154
159
|
|
|
155
|
-
# eq3 and eq6 are related
|
|
160
|
+
# eq3 and eq6 are related. Several branches share a body because distinct
|
|
161
|
+
# (eq3, eq6) states map to the same next-lower macro per the CVSS 4.0 spec;
|
|
162
|
+
# kept as separate branches to mirror the spec's case table.
|
|
163
|
+
# rubocop:disable-next Lint/DuplicateBranch
|
|
156
164
|
if eq3_val == 1 && eq6_val == 1
|
|
157
165
|
# 11 --> 21
|
|
158
166
|
eq3eq6_next_lower_macro = concat_and_stringify(eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val)
|
|
@@ -184,11 +192,7 @@ module CvssSuite
|
|
|
184
192
|
score_eq3eq6_next_lower_macro_left = LOOKUP[eq3eq6_next_lower_macro_left]
|
|
185
193
|
score_eq3eq6_next_lower_macro_right = LOOKUP[eq3eq6_next_lower_macro_right]
|
|
186
194
|
|
|
187
|
-
score_eq3eq6_next_lower_macro =
|
|
188
|
-
score_eq3eq6_next_lower_macro_left
|
|
189
|
-
else
|
|
190
|
-
score_eq3eq6_next_lower_macro_right
|
|
191
|
-
end
|
|
195
|
+
score_eq3eq6_next_lower_macro = [score_eq3eq6_next_lower_macro_left, score_eq3eq6_next_lower_macro_right].max
|
|
192
196
|
else
|
|
193
197
|
score_eq3eq6_next_lower_macro = LOOKUP[eq3eq6_next_lower_macro]
|
|
194
198
|
end
|
|
@@ -353,7 +357,7 @@ module CvssSuite
|
|
|
353
357
|
end
|
|
354
358
|
|
|
355
359
|
def concat_and_stringify(first, second, third, fourth, fifth, sixth)
|
|
356
|
-
|
|
360
|
+
"#{first}#{second}#{third}#{fourth}#{fifth}#{sixth}"
|
|
357
361
|
end
|
|
358
362
|
|
|
359
363
|
def sum_or_nil(values)
|
|
@@ -393,7 +397,7 @@ module CvssSuite
|
|
|
393
397
|
def truncate(string_to_truncate, truncate_to)
|
|
394
398
|
return string_to_truncate.dup unless string_to_truncate.length > truncate_to
|
|
395
399
|
|
|
396
|
-
|
|
400
|
+
string_to_truncate[0, truncate_to + 1].to_s
|
|
397
401
|
end
|
|
398
402
|
end
|
|
399
403
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CvssSuite
|
|
2
4
|
module Cvss40Constants
|
|
3
5
|
# These constants were almost directly ported from the CVSS 4.0 calculator code found at https://github.com/FIRSTdotorg/cvss-v4-calculator/blob/ac71416d935ad2ac87cd107ff87024561ea954a7/app.js#L278C17-L278C18
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CvssSuite
|
|
2
4
|
module Cvss40Constants
|
|
3
5
|
# These constants were almost directly ported from the CVSS 4.0 calculator code found at https://github.com/FIRSTdotorg/cvss-v4-calculator/blob/ac71416d935ad2ac87cd107ff87024561ea954a7/cvss_lookup.js#L1
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CvssSuite
|
|
2
4
|
module Cvss40Constants
|
|
3
5
|
# These constants were almost directly ported from the CVSS 4.0 calculator code found at https://github.com/FIRSTdotorg/cvss-v4-calculator/blob/ac71416d935ad2ac87cd107ff87024561ea954a7/max_composed.js#L4
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CvssSuite
|
|
2
4
|
module Cvss40Constants
|
|
3
5
|
# These constants were almost directly ported from the CVSS 4.0 calculator code found at https://github.com/FIRSTdotorg/cvss-v4-calculator/blob/ac71416d935ad2ac87cd107ff87024561ea954a7/max_severity.js#L1
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -17,7 +19,7 @@ module CvssSuite
|
|
|
17
19
|
# Creates a new CVSS vector by a +vector+, for all CVSS versions through 3.1.
|
|
18
20
|
#
|
|
19
21
|
# Raises an exception if it is called on Cvss31AndBefore class.
|
|
20
|
-
def initialize(vector)
|
|
22
|
+
def initialize(vector, original = vector)
|
|
21
23
|
raise CvssSuite::Errors::InvalidParentClass, 'Do not instantiate this class!' if instance_of? Cvss31AndBefore
|
|
22
24
|
|
|
23
25
|
super
|
|
@@ -30,7 +32,7 @@ module CvssSuite
|
|
|
30
32
|
entered_keys = @properties.collect { |p| p[:name] }
|
|
31
33
|
return false if (entered_keys - allowed_abbreviations).size.positive?
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
metrics_valid?
|
|
34
36
|
else
|
|
35
37
|
false
|
|
36
38
|
end
|
|
@@ -54,7 +56,7 @@ module CvssSuite
|
|
|
54
56
|
@environmental.properties.collect(&:abbreviation)
|
|
55
57
|
end
|
|
56
58
|
|
|
57
|
-
def
|
|
59
|
+
def metrics_valid?
|
|
58
60
|
@base.valid? && @temporal&.valid? && @environmental&.valid?
|
|
59
61
|
end
|
|
60
62
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -17,7 +19,7 @@ module CvssSuite
|
|
|
17
19
|
# Creates a new CVSS vector by a +vector+, for all CVSS versions from 4.0.
|
|
18
20
|
#
|
|
19
21
|
# Raises an exception if it is called on Cvss40AndLater class.
|
|
20
|
-
def initialize(vector)
|
|
22
|
+
def initialize(vector, original = vector)
|
|
21
23
|
raise CvssSuite::Errors::InvalidParentClass, 'Do not instantiate this class!' if instance_of? Cvss40AndLater
|
|
22
24
|
|
|
23
25
|
super
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -53,9 +55,9 @@ module CvssSuite
|
|
|
53
55
|
p.abbreviation == selected_property[:name] &&
|
|
54
56
|
(p.position&.include?(selected_property[:position]) || p.position.nil?)
|
|
55
57
|
end
|
|
56
|
-
property&.
|
|
58
|
+
property&.mark_selected selected_property[:selected]
|
|
57
59
|
end
|
|
58
|
-
@properties.select(&:non_selected?).each(&:
|
|
60
|
+
@properties.select(&:non_selected?).each(&:mark_default)
|
|
59
61
|
end
|
|
60
62
|
end
|
|
61
63
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -69,22 +71,22 @@ module CvssSuite
|
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
##
|
|
72
|
-
#
|
|
74
|
+
# Marks the value with the given +abbreviation+ as selected.
|
|
73
75
|
|
|
74
|
-
def
|
|
76
|
+
def mark_selected(abbreviation)
|
|
75
77
|
values.each do |value|
|
|
76
|
-
value[:selected] =
|
|
78
|
+
value[:selected] = abbreviation.eql?(value[:abbreviation])
|
|
77
79
|
end
|
|
78
80
|
@selected_value = values.detect { |value| value[:selected] }
|
|
79
81
|
return unless @selected_value.nil?
|
|
80
82
|
|
|
81
|
-
@selected_value = { abbreviation:
|
|
83
|
+
@selected_value = { abbreviation: abbreviation }
|
|
82
84
|
end
|
|
83
85
|
|
|
84
86
|
##
|
|
85
|
-
#
|
|
87
|
+
# Marks the default value (X / ND) as selected.
|
|
86
88
|
|
|
87
|
-
def
|
|
89
|
+
def mark_default
|
|
88
90
|
values.each do |value|
|
|
89
91
|
value[:selected] = value[:abbreviation].eql?('X')
|
|
90
92
|
value[:selected] ||= value[:abbreviation].eql?('ND')
|
data/lib/cvss_suite/errors.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -5,21 +7,85 @@
|
|
|
5
7
|
|
|
6
8
|
module CvssSuite
|
|
7
9
|
##
|
|
8
|
-
#
|
|
10
|
+
# Included by every error this gem raises, so one rescue catches them all:
|
|
11
|
+
#
|
|
12
|
+
# rescue CvssSuite::Error => e
|
|
13
|
+
#
|
|
14
|
+
# A module rather than a base class, so the concrete errors keep the ancestors
|
|
15
|
+
# callers already rescue: InvalidVector stays a RuntimeError, and the argument
|
|
16
|
+
# errors stay ArgumentErrors. f7e9866 moved them off a shared base class in
|
|
17
|
+
# 2018 for exactly that compatibility, and this does not take it back.
|
|
18
|
+
module Error; end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# The concrete errors this gem raises. Each one includes Error above.
|
|
9
22
|
module Errors
|
|
10
23
|
##
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
# Raised for a vector this gem cannot parse, and by every score reader on a
|
|
25
|
+
# vector +valid?+ has rejected.
|
|
26
|
+
class InvalidVector < RuntimeError
|
|
27
|
+
include Error
|
|
28
|
+
|
|
29
|
+
# Caps the vector echoed back, so arbitrary input cannot push arbitrary
|
|
30
|
+
# length into an exception message and from there into a log. It caps
|
|
31
|
+
# what is echoed, not the message: escaping expands it, and 200 NUL bytes
|
|
32
|
+
# inspect to roughly 1.2 KB. Bounded either way, just not at 200.
|
|
33
|
+
#
|
|
34
|
+
# A CVSS 4.0 vector carrying every metric the specification defines, each
|
|
35
|
+
# at its longest option, is 178 characters, so a real vector is always
|
|
36
|
+
# repeated back whole.
|
|
37
|
+
MAX_REPORTED_LENGTH = 200
|
|
38
|
+
|
|
39
|
+
UNINSPECTABLE = '(an object that could not be inspected)'
|
|
40
|
+
private_constant :MAX_REPORTED_LENGTH, :UNINSPECTABLE
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Builds the error for the +vector+ that was rejected, naming it in the
|
|
44
|
+
# message. Takes the raw input rather than a string, so a caller who
|
|
45
|
+
# passed nil or an Integer is told that, instead of being shown '""'.
|
|
46
|
+
def self.for(vector)
|
|
47
|
+
new("Vector is not valid: #{describe(vector)}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Strings are trimmed before inspect, not after, so the escaping is never
|
|
51
|
+
# cut mid-sequence and the closing quote survives -- and a huge input is
|
|
52
|
+
# not escaped in full only to be thrown away.
|
|
53
|
+
#
|
|
54
|
+
# CvssSuite.new accepts any object, so #inspect here runs on something the
|
|
55
|
+
# gem never validated. A broken one must not become the exception raised
|
|
56
|
+
# in place of the one it was helping to describe.
|
|
57
|
+
def self.describe(vector)
|
|
58
|
+
return trim(vector).inspect if vector.is_a?(String)
|
|
14
59
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
60
|
+
described = vector.inspect
|
|
61
|
+
# Tested for String rather than trimmed blind: Array and Hash answer
|
|
62
|
+
# #length and #[] too, so duck-typing here would let an #inspect that
|
|
63
|
+
# returns a collection walk straight past MAX_REPORTED_LENGTH.
|
|
64
|
+
described.is_a?(String) ? trim(described) : UNINSPECTABLE
|
|
65
|
+
# SystemStackError is not a StandardError, and a self-recursive #inspect
|
|
66
|
+
# raises exactly that.
|
|
67
|
+
rescue StandardError, SystemStackError
|
|
68
|
+
UNINSPECTABLE
|
|
18
69
|
end
|
|
70
|
+
|
|
71
|
+
def self.trim(text)
|
|
72
|
+
return text if text.length <= MAX_REPORTED_LENGTH
|
|
73
|
+
|
|
74
|
+
"#{text[0, MAX_REPORTED_LENGTH]}..."
|
|
75
|
+
end
|
|
76
|
+
private_class_method :describe, :trim
|
|
19
77
|
end
|
|
20
78
|
|
|
21
|
-
|
|
79
|
+
##
|
|
80
|
+
# Raised when an abstract class is instantiated directly.
|
|
81
|
+
class InvalidParentClass < ArgumentError
|
|
82
|
+
include Error
|
|
83
|
+
end
|
|
22
84
|
|
|
23
|
-
|
|
85
|
+
##
|
|
86
|
+
# Raised for a CVSS version this gem does not implement.
|
|
87
|
+
class UnsupportedVersion < ArgumentError
|
|
88
|
+
include Error
|
|
89
|
+
end
|
|
24
90
|
end
|
|
25
91
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -10,6 +12,10 @@ module CvssSuite
|
|
|
10
12
|
##
|
|
11
13
|
# Since CVSS 3 all float values are rounded up, therefore this method is used
|
|
12
14
|
# instead of the mathematically correct method round().
|
|
15
|
+
# This is the exact Roundup from CVSS v3.1 Appendix A, which works on integer
|
|
16
|
+
# arithmetic (x100000) to avoid the floating-point edge cases that plain
|
|
17
|
+
# ceil(1) hits -- the reason v3.1 replaced v3.0's rounding.
|
|
18
|
+
# https://www.first.org/cvss/v3.1/specification-document
|
|
13
19
|
def self.round_up(float)
|
|
14
20
|
output = (float * 100_000).round
|
|
15
21
|
if (output % 10_000).zero?
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -17,6 +19,8 @@ module CvssSuite
|
|
|
17
19
|
##
|
|
18
20
|
# Since CVSS 3 the Privilege Required score depends on the selected value of the Scope metric.
|
|
19
21
|
# This method takes a +Privilege+ +Required+ and a +Scope+ metric and returns the newly calculated score.
|
|
22
|
+
# Per the PrivilegesRequired metric table, when Scope is Changed the weights
|
|
23
|
+
# become PR:Low = 0.68 and PR:High = 0.50 (vs 0.62 / 0.27 when Unchanged).
|
|
20
24
|
def self.privileges_required_score(privileges_required, scope)
|
|
21
25
|
changed = scope.selected_value[:name] == 'Changed'
|
|
22
26
|
privilege_score = privileges_required.score
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -9,8 +11,16 @@ module CvssSuite
|
|
|
9
11
|
class InvalidCvss < Cvss
|
|
10
12
|
# rubocop:disable Lint/MissingSuper
|
|
11
13
|
##
|
|
12
|
-
# Creates a new invalid CVSS vector
|
|
13
|
-
|
|
14
|
+
# Creates a new invalid CVSS vector, remembering the +vector+ that was
|
|
15
|
+
# rejected so the error it raises can name it. Cannot call super: that
|
|
16
|
+
# parses the vector, which is the thing that already failed.
|
|
17
|
+
#
|
|
18
|
+
# Deliberately not stored as @vector: #vector reads that through to_s, and
|
|
19
|
+
# .new accepts any object, so an object with a hostile to_s would turn a
|
|
20
|
+
# total reader into one that raises.
|
|
21
|
+
def initialize(vector = nil)
|
|
22
|
+
@rejected = vector
|
|
23
|
+
end
|
|
14
24
|
# rubocop:enable Lint/MissingSuper
|
|
15
25
|
|
|
16
26
|
##
|
|
@@ -42,5 +52,19 @@ module CvssSuite
|
|
|
42
52
|
def environmental_score
|
|
43
53
|
check_validity
|
|
44
54
|
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Since this is an invalid CVSS vector, it always throws an exception.
|
|
58
|
+
def overall_score
|
|
59
|
+
check_validity
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# The raw input. .new accepts anything, so nil has to read as nil and 1337
|
|
65
|
+
# as 1337 rather than both arriving in the message as an empty string.
|
|
66
|
+
def rejected_vector
|
|
67
|
+
@rejected
|
|
68
|
+
end
|
|
45
69
|
end
|
|
46
70
|
end
|
data/lib/cvss_suite/version.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
4
6
|
# See the LICENSE.md file in the top-level directory.
|
|
5
7
|
|
|
6
8
|
module CvssSuite
|
|
7
|
-
VERSION = '
|
|
9
|
+
VERSION = '5.0.0'
|
|
8
10
|
end
|
data/lib/cvss_suite.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# CVSS-Suite, a Ruby gem to manage the CVSS vector
|
|
2
4
|
#
|
|
3
5
|
# This work is licensed under the terms of the MIT license.
|
|
@@ -14,6 +16,11 @@ require 'cvss_suite/invalid_cvss'
|
|
|
14
16
|
##
|
|
15
17
|
# Module of this gem.
|
|
16
18
|
module CvssSuite
|
|
19
|
+
# Exempt from the seal below, which covers only what has never shipped. Age is
|
|
20
|
+
# not the reason -- 5.0.0 privatizes three module methods that were public in
|
|
21
|
+
# 4.1.4 -- it stays because prefix-to-version lets a caller sniff a vector's
|
|
22
|
+
# version without constructing one. Sealing it means editing the three
|
|
23
|
+
# Cvss*#vector readers too: they use the qualified form, which a seal rejects.
|
|
17
24
|
CVSS_VECTOR_BEGINNINGS = [
|
|
18
25
|
{ string: 'AV:', version: 2 },
|
|
19
26
|
{ string: '(AV:', version: 2 },
|
|
@@ -22,32 +29,94 @@ module CvssSuite
|
|
|
22
29
|
{ string: 'CVSS:4.0/', version: 4.0 }
|
|
23
30
|
].freeze
|
|
24
31
|
|
|
32
|
+
# The metric groups that make up each CVSS version, in vector order. Keyed by
|
|
33
|
+
# the same version values as CVSS_VECTOR_BEGINNINGS and Cvss#version, so
|
|
34
|
+
# CvssSuite.metrics(instance.version) works for every version.
|
|
35
|
+
METRIC_GROUPS = {
|
|
36
|
+
2 => [['Base', Cvss2Base], ['Temporal', Cvss2Temporal], ['Environmental', Cvss2Environmental]],
|
|
37
|
+
3.0 => [['Base', Cvss3Base], ['Temporal', Cvss3Temporal], ['Environmental', Cvss3Environmental]],
|
|
38
|
+
3.1 => [['Base', Cvss31Base], ['Temporal', Cvss31Temporal], ['Environmental', Cvss31Environmental]],
|
|
39
|
+
4.0 => [['Base', Cvss40Base], ['Threat', Cvss40Threat], ['Environmental', Cvss40Environmental],
|
|
40
|
+
['Environmental Security Requirements', Cvss40EnvironmentalSecurity], ['Supplemental', Cvss40Supplemental]]
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Accepted version identifiers mapped to their canonical METRIC_GROUPS key, so
|
|
44
|
+
# both the canonical value (2, 3.0, ...) and the human-friendly string
|
|
45
|
+
# ('2', '2.0', '3.1', ...) resolve to the same schema.
|
|
46
|
+
VERSION_ALIASES = {
|
|
47
|
+
2 => 2, 2.0 => 2, '2' => 2, '2.0' => 2,
|
|
48
|
+
3.0 => 3.0, '3.0' => 3.0,
|
|
49
|
+
3.1 => 3.1, '3.1' => 3.1,
|
|
50
|
+
4.0 => 4.0, '4.0' => 4.0
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
# Both tables exist to answer .metrics, which is the supported way to reach
|
|
54
|
+
# them. Sealed before they ship in a release, while it still breaks nobody.
|
|
55
|
+
private_constant :METRIC_GROUPS, :VERSION_ALIASES
|
|
56
|
+
|
|
25
57
|
##
|
|
26
58
|
# Returns a CVSS class by a +vector+.
|
|
27
59
|
def self.new(vector)
|
|
28
|
-
return InvalidCvss.new unless vector.is_a? String
|
|
60
|
+
return InvalidCvss.new(vector) unless vector.is_a? String
|
|
29
61
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
62
|
+
# Always a copy, not only when frozen: the string is kept for the lifetime
|
|
63
|
+
# of the returned object to name it in errors, and a caller who mutates
|
|
64
|
+
# theirs afterwards would otherwise change what we report having rejected.
|
|
65
|
+
@vector_string = vector.dup
|
|
35
66
|
|
|
67
|
+
# version is a discrete value parsed from the vector and matched against exact
|
|
68
|
+
# literals, not the result of float arithmetic, so these comparisons are reliable.
|
|
69
|
+
# rubocop:disable-next Lint/FloatComparison
|
|
36
70
|
case version
|
|
37
71
|
when 2
|
|
38
|
-
Cvss2.new(prepare_vector(@vector_string))
|
|
72
|
+
Cvss2.new(prepare_vector(@vector_string), @vector_string)
|
|
39
73
|
when 3.0
|
|
40
|
-
Cvss3.new(prepare_vector(@vector_string))
|
|
74
|
+
Cvss3.new(prepare_vector(@vector_string), @vector_string)
|
|
41
75
|
when 3.1
|
|
42
|
-
Cvss31.new(prepare_vector(@vector_string))
|
|
76
|
+
Cvss31.new(prepare_vector(@vector_string), @vector_string)
|
|
43
77
|
when 4.0
|
|
44
|
-
Cvss40.new(prepare_vector(@vector_string))
|
|
78
|
+
Cvss40.new(prepare_vector(@vector_string), @vector_string)
|
|
45
79
|
else
|
|
46
|
-
InvalidCvss.new
|
|
80
|
+
InvalidCvss.new(@vector_string)
|
|
47
81
|
end
|
|
48
82
|
end
|
|
49
83
|
|
|
50
|
-
|
|
84
|
+
##
|
|
85
|
+
# Returns a CVSS class by a +vector+, raising CvssSuite::Errors::InvalidVector
|
|
86
|
+
# if the vector cannot be parsed.
|
|
87
|
+
#
|
|
88
|
+
# Prefer this over .new when a bad vector is a bug rather than an expected
|
|
89
|
+
# input: .new answers with an InvalidCvss sentinel that only reports the
|
|
90
|
+
# problem once a score is asked for, so a caller who forgets +valid?+ carries a
|
|
91
|
+
# broken vector until something far from the parse blows up.
|
|
92
|
+
def self.parse(vector)
|
|
93
|
+
cvss = new(vector)
|
|
94
|
+
raise Errors::InvalidVector.for(vector) unless cvss.valid?
|
|
95
|
+
|
|
96
|
+
cvss
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# Returns the static schema of metrics and their options for a CVSS +version+
|
|
101
|
+
# (2, 3.0, 3.1 or 4.0; the equivalent strings are accepted too) without
|
|
102
|
+
# constructing a vector. Each metric lists its options with the +default+
|
|
103
|
+
# option flagged, so a caller can build input forms directly. Closes #8.
|
|
104
|
+
def self.metrics(version)
|
|
105
|
+
groups = METRIC_GROUPS[VERSION_ALIASES[version]]
|
|
106
|
+
raise Errors::UnsupportedVersion, "Unsupported CVSS version: #{version.inspect}" if groups.nil?
|
|
107
|
+
|
|
108
|
+
groups.map { |label, metric_class| { group: label, metrics: metric_schema(metric_class) } }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.metric_schema(metric_class)
|
|
112
|
+
metric_class.new([]).properties.map do |property|
|
|
113
|
+
options = property.values.map do |value|
|
|
114
|
+
{ name: value[:name], abbreviation: value[:abbreviation], default: value.fetch(:selected, false) }
|
|
115
|
+
end
|
|
116
|
+
{ name: property.name, abbreviation: property.abbreviation, options: options }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
private_class_method :metric_schema
|
|
51
120
|
|
|
52
121
|
def self.version
|
|
53
122
|
CVSS_VECTOR_BEGINNINGS.each do |beginning|
|
|
@@ -60,7 +129,7 @@ module CvssSuite
|
|
|
60
129
|
|
|
61
130
|
return prepare_cvss2_vector(vector) if version == 2
|
|
62
131
|
|
|
63
|
-
version_string = CVSS_VECTOR_BEGINNINGS.detect { |v| v[:version] == version }
|
|
132
|
+
version_string = CVSS_VECTOR_BEGINNINGS.detect { |v| v[:version] == version }[:string]
|
|
64
133
|
start_of_vector = vector.index(version_string)
|
|
65
134
|
|
|
66
135
|
if start_of_vector.nil?
|
|
@@ -88,4 +157,8 @@ module CvssSuite
|
|
|
88
157
|
vector[start_of_vector..]
|
|
89
158
|
end
|
|
90
159
|
end
|
|
160
|
+
|
|
161
|
+
# Parsing internals. They read module state set by .new, so calling them
|
|
162
|
+
# directly was never meaningful; they were public only because .new needs them.
|
|
163
|
+
private_class_method :version, :prepare_vector, :prepare_cvss2_vector
|
|
91
164
|
end
|