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
@@ -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 = if score_eq3eq6_next_lower_macro_left > score_eq3eq6_next_lower_macro_right
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
- String.new.concat(first.to_s, second.to_s, third.to_s, fourth.to_s, fifth.to_s, sixth.to_s)
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
- (string_to_truncate[0, truncate_to + 1]).to_s
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.
@@ -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.
@@ -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.
@@ -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.
@@ -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
- check_metrics_validity
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 check_metrics_validity
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&.set_selected_value selected_property[:selected]
58
+ property&.mark_selected selected_property[:selected]
57
59
  end
58
- @properties.select(&:non_selected?).each(&:set_default_value)
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
- # Sets the selected value by a +value+.
74
+ # Marks the value with the given +abbreviation+ as selected.
73
75
 
74
- def set_selected_value(selected_value)
76
+ def mark_selected(abbreviation)
75
77
  values.each do |value|
76
- value[:selected] = selected_value.eql?(value[:abbreviation])
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: selected_value }
83
+ @selected_value = { abbreviation: abbreviation }
82
84
  end
83
85
 
84
86
  ##
85
- # Sets the default value.
87
+ # Marks the default value (X / ND) as selected.
86
88
 
87
- def set_default_value
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')
@@ -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
- # This will define classed errors to be expected
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
- # The base error class to be inherited by more specific classes
12
- class CvssError < StandardError
13
- attr_accessor :message
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
- def initialize(message)
16
- @message = message
17
- super
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
- class InvalidVector < RuntimeError; end
79
+ ##
80
+ # Raised when an abstract class is instantiated directly.
81
+ class InvalidParentClass < ArgumentError
82
+ include Error
83
+ end
22
84
 
23
- class InvalidParentClass < ArgumentError; end
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
- def initialize; end
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
@@ -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 = '4.1.4'.freeze
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
- @vector_string = if vector.frozen?
31
- vector.dup
32
- else
33
- vector
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
- private
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 } [:string]
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