cvss-suite 3.1.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +4 -4
  3. data/.github/workflows/rubocop.yml +3 -4
  4. data/.rubocop.yml +20 -0
  5. data/.rubocop_todo.yml +2 -2
  6. data/CHANGES.md +8 -0
  7. data/CODE_OF_CONDUCT.md +9 -2
  8. data/Gemfile +0 -6
  9. data/LICENSE.md +10 -1
  10. data/README.md +14 -5
  11. data/cvss_suite.gemspec +7 -10
  12. data/lib/cvss_suite/cvss.rb +1 -31
  13. data/lib/cvss_suite/cvss2/cvss2.rb +2 -8
  14. data/lib/cvss_suite/cvss2/cvss2_base.rb +0 -6
  15. data/lib/cvss_suite/cvss2/cvss2_environmental.rb +0 -6
  16. data/lib/cvss_suite/cvss2/cvss2_temporal.rb +0 -6
  17. data/lib/cvss_suite/cvss3/cvss3.rb +2 -8
  18. data/lib/cvss_suite/cvss3/cvss3_base.rb +0 -6
  19. data/lib/cvss_suite/cvss3/cvss3_environmental.rb +0 -6
  20. data/lib/cvss_suite/cvss3/cvss3_temporal.rb +0 -6
  21. data/lib/cvss_suite/cvss31/cvss31.rb +2 -8
  22. data/lib/cvss_suite/cvss31/cvss31_base.rb +0 -6
  23. data/lib/cvss_suite/cvss31/cvss31_environmental.rb +0 -6
  24. data/lib/cvss_suite/cvss31/cvss31_temporal.rb +0 -6
  25. data/lib/cvss_suite/cvss40/cvss40.rb +43 -0
  26. data/lib/cvss_suite/cvss40/cvss40_all_up.rb +40 -0
  27. data/lib/cvss_suite/cvss40/cvss40_base.rb +86 -0
  28. data/lib/cvss_suite/cvss40/cvss40_calc_helper.rb +389 -0
  29. data/lib/cvss_suite/cvss40/cvss40_constants_levels.rb +26 -0
  30. data/lib/cvss_suite/cvss40/cvss40_constants_macro_vector_lookup.rb +278 -0
  31. data/lib/cvss_suite/cvss40/cvss40_constants_max_composed.rb +41 -0
  32. data/lib/cvss_suite/cvss40/cvss40_constants_max_severity.rb +31 -0
  33. data/lib/cvss_suite/cvss40/cvss40_environmental.rb +105 -0
  34. data/lib/cvss_suite/cvss40/cvss40_environmental_security.rb +47 -0
  35. data/lib/cvss_suite/cvss40/cvss40_supplemental.rb +66 -0
  36. data/lib/cvss_suite/cvss40/cvss40_threat.rb +34 -0
  37. data/lib/cvss_suite/cvss_31_and_before.rb +50 -0
  38. data/lib/cvss_suite/cvss_40_and_later.rb +45 -0
  39. data/lib/cvss_suite/cvss_metric.rb +4 -6
  40. data/lib/cvss_suite/cvss_property.rb +0 -6
  41. data/lib/cvss_suite/errors.rb +0 -6
  42. data/lib/cvss_suite/extensions/string.rb +8 -0
  43. data/lib/cvss_suite/helpers/cvss31_helper.rb +0 -6
  44. data/lib/cvss_suite/helpers/cvss3_helper.rb +0 -6
  45. data/lib/cvss_suite/invalid_cvss.rb +0 -6
  46. data/lib/cvss_suite/version.rb +1 -7
  47. data/lib/cvss_suite.rb +6 -7
  48. metadata +41 -12
@@ -0,0 +1,86 @@
1
+ # CVSS-Suite, a Ruby gem to manage the CVSS vector
2
+ #
3
+ # This work is licensed under the terms of the MIT license.
4
+ # See the LICENSE.md file in the top-level directory.
5
+
6
+ require_relative '../cvss_property'
7
+ require_relative '../cvss_metric'
8
+ require_relative 'cvss40_calc_helper'
9
+
10
+ module CvssSuite
11
+ ##
12
+ # This class represents a CVSS Base metric in version 4.0.
13
+ class Cvss40Base < CvssMetric
14
+ ##
15
+ # Property of this metric
16
+
17
+ attr_reader :attack_vector, :attack_complexity, :attack_requirements, :privileges_required, :user_interaction,
18
+ :vulnerable_system_confidentiality, :vulnerable_system_integrity, :vulnerable_system_availability,
19
+ :subsequent_system_confidentiality, :subsequent_system_integrity, :subsequent_system_availability
20
+
21
+ ##
22
+ # Returns score of this metric
23
+ def score
24
+ Cvss40CalcHelper.new(@properties.map { |p| [p.abbreviation, p.selected_value[:abbreviation]] }.to_h).score
25
+ end
26
+
27
+ private
28
+
29
+ def init_properties
30
+ @properties.push(@attack_vector =
31
+ CvssProperty.new(name: 'Attack Vector', abbreviation: 'AV',
32
+ values: [{ name: 'Network', abbreviation: 'N' },
33
+ { name: 'Adjacent', abbreviation: 'A' },
34
+ { name: 'Local', abbreviation: 'L' },
35
+ { name: 'Physical', abbreviation: 'P' }]))
36
+ @properties.push(@attack_complexity =
37
+ CvssProperty.new(name: 'Attack Complexity', abbreviation: 'AC',
38
+ values: [{ name: 'Low', abbreviation: 'L' },
39
+ { name: 'High', abbreviation: 'H' }]))
40
+ @properties.push(@attack_requirements =
41
+ CvssProperty.new(name: 'Attack Requirements', abbreviation: 'AT',
42
+ values: [{ name: 'None', abbreviation: 'N' },
43
+ { name: 'Present', abbreviation: 'P' }]))
44
+ @properties.push(@privileges_required =
45
+ CvssProperty.new(name: 'Privileges Required', abbreviation: 'PR',
46
+ values: [{ name: 'None', abbreviation: 'N' },
47
+ { name: 'Low', abbreviation: 'L' },
48
+ { name: 'High', abbreviation: 'H' }]))
49
+ @properties.push(@user_interaction =
50
+ CvssProperty.new(name: 'User Interaction', abbreviation: 'UI',
51
+ values: [{ name: 'None', abbreviation: 'N' },
52
+ { name: 'Passive', abbreviation: 'P' },
53
+ { name: 'Active', abbreviation: 'A' }]))
54
+ @properties.push(@vulnerable_system_confidentiality =
55
+ CvssProperty.new(name: 'Vulnerable System Confidentiality Impact', abbreviation: 'VC',
56
+ values: [{ name: 'None', abbreviation: 'N' },
57
+ { name: 'Low', abbreviation: 'L' },
58
+ { name: 'High', abbreviation: 'H' }]))
59
+ @properties.push(@vulnerable_system_integrity =
60
+ CvssProperty.new(name: 'Vulnerable System Integrity Impact', abbreviation: 'VI',
61
+ values: [{ name: 'None', abbreviation: 'N' },
62
+ { name: 'Low', abbreviation: 'L' },
63
+ { name: 'High', abbreviation: 'H' }]))
64
+ @properties.push(@vulnerable_system_availability =
65
+ CvssProperty.new(name: 'Vulnerable System Availability Impact', abbreviation: 'VA',
66
+ values: [{ name: 'None', abbreviation: 'N' },
67
+ { name: 'Low', abbreviation: 'L' },
68
+ { name: 'High', abbreviation: 'H' }]))
69
+ @properties.push(@subsequent_system_confidentiality =
70
+ CvssProperty.new(name: 'Subsequent System Confidentiality Impact', abbreviation: 'SC',
71
+ values: [{ name: 'None', abbreviation: 'N' },
72
+ { name: 'Low', abbreviation: 'L' },
73
+ { name: 'High', abbreviation: 'H' }]))
74
+ @properties.push(@subsequent_system_integrity =
75
+ CvssProperty.new(name: 'Subsequent System Integrity Impact', abbreviation: 'SI',
76
+ values: [{ name: 'None', abbreviation: 'N' },
77
+ { name: 'Low', abbreviation: 'L' },
78
+ { name: 'High', abbreviation: 'H' }]))
79
+ @properties.push(@subsequent_system_availability =
80
+ CvssProperty.new(name: 'Subsequent System Availability Impact', abbreviation: 'SA',
81
+ values: [{ name: 'None', abbreviation: 'N' },
82
+ { name: 'Low', abbreviation: 'L' },
83
+ { name: 'High', abbreviation: 'H' }]))
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,389 @@
1
+ require_relative 'cvss40_constants_macro_vector_lookup'
2
+ require_relative 'cvss40_constants_max_composed'
3
+ require_relative 'cvss40_constants_max_severity'
4
+ require_relative 'cvss40_constants_levels'
5
+
6
+ module CvssSuite
7
+ # This class performs much of the score calculation logic for CVSS 4.0.
8
+ # It is heavily ported from the m and scoring methods in https://github.com/FIRSTdotorg/cvss-v4-calculator/blob/ac71416d935ad2ac87cd107ff87024561ea954a7/app.js#L121
9
+ # This class has a few rubocop exclusions but maintaining parity with the ported
10
+ # code seems more valuable than trying to follow the cops in this case.
11
+ class Cvss40CalcHelper
12
+ include Cvss40Constants
13
+
14
+ def initialize(cvss_property_bag)
15
+ @cvss_property_bag = cvss_property_bag
16
+ end
17
+
18
+ def m(metric)
19
+ selected = @cvss_property_bag[metric]
20
+
21
+ # If E=X it will default to the worst case i.e. E=A
22
+ return 'A' if metric == 'E' && (selected == 'X' || selected.nil?)
23
+ # If CR=X, IR=X or AR=X they will default to the worst case i.e. CR=H, IR=H and AR=H
24
+ return 'H' if metric == 'CR' && (selected == 'X' || selected.nil?)
25
+ # IR:X is the same as IR:H
26
+ return 'H' if metric == 'IR' && (selected == 'X' || selected.nil?)
27
+ # AR:X is the same as AR:H
28
+ return 'H' if metric == 'AR' && (selected == 'X' || selected.nil?)
29
+
30
+ # All other environmental metrics just overwrite base score values,
31
+ # so if they’re not defined just use the base score value.
32
+ if @cvss_property_bag.include?("M#{metric}")
33
+ modified_selected = @cvss_property_bag["M#{metric}"]
34
+ return modified_selected if modified_selected != 'X'
35
+ end
36
+
37
+ selected
38
+ end
39
+
40
+ def retrieve_macro_vector
41
+ # EQ1: 0-AV:N and PR:N and UI:N
42
+ # 1-(AV:N or PR:N or UI:N) and not (AV:N and PR:N and UI:N) and not AV:P
43
+ # 2-AV:P or not(AV:N or PR:N or UI:N)
44
+
45
+ if m('AV') == 'N' && m('PR') == 'N' && m('UI') == 'N'
46
+ eq1 = '0'
47
+ elsif (m('AV') == 'N' || m('PR') == 'N' || m('UI') == 'N') &&
48
+ !(m('AV') == 'N' && m('PR') == 'N' && m('UI') == 'N') &&
49
+ (m('AV') != 'P')
50
+ eq1 = '1'
51
+ elsif m('AV') == 'P' ||
52
+ !(m('AV') == 'N' ||
53
+ m('PR') == 'N' ||
54
+ m('UI') == 'N')
55
+ eq1 = '2'
56
+ end
57
+
58
+ # EQ2: 0-(AC:L and AT:N)
59
+ # 1-(not(AC:L and AT:N))
60
+
61
+ if m('AC') == 'L' && m('AT') == 'N'
62
+ eq2 = '0'
63
+ elsif !(m('AC') == 'L' && m('AT') == 'N')
64
+ eq2 = '1'
65
+ end
66
+
67
+ # EQ3: 0-(VC:H and VI:H)
68
+ # 1-(not(VC:H and VI:H) and (VC:H or VI:H or VA:H))
69
+ # 2-not (VC:H or VI:H or VA:H)
70
+ if m('VC') == 'H' && m('VI') == 'H'
71
+ eq3 = '0'
72
+ elsif !(m('VC') == 'H' && m('VI') == 'H') &&
73
+ (m('VC') == 'H' || m('VI') == 'H' || m('VA') == 'H')
74
+ eq3 = '1'
75
+ elsif !(m('VC') == 'H' || m('VI') == 'H' || m('VA') == 'H')
76
+ eq3 = '2'
77
+ end
78
+
79
+ # EQ4: 0-(MSI:S or MSA:S)
80
+ # 1-not (MSI:S or MSA:S) and (SC:H or SI:H or SA:H)
81
+ # 2-not (MSI:S or MSA:S) and not (SC:H or SI:H or SA:H)
82
+
83
+ if m('MSI') == 'S' || m('MSA') == 'S'
84
+ eq4 = '0'
85
+ elsif !(m('MSI') == 'S' || m('MSA') == 'S') &&
86
+ (m('SC') == 'H' || m('SI') == 'H' || m('SA') == 'H')
87
+ eq4 = '1'
88
+ elsif !(m('MSI') == 'S' || m('MSA') == 'S') &&
89
+ !(m('SC') == 'H' || m('SI') == 'H' || m('SA') == 'H')
90
+ eq4 = '2'
91
+ end
92
+
93
+ # EQ5: 0-E:A
94
+ # 1-E:P
95
+ # 2-E:U
96
+
97
+ eq5 = case m('E')
98
+ when 'A'
99
+ '0'
100
+ when 'P'
101
+ '1'
102
+ when 'U'
103
+ '2'
104
+ else
105
+ # brphelps TODO added figure it out
106
+ '0'
107
+ end
108
+
109
+ # EQ6: 0-(CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H)
110
+ # 1-not[(CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H)]
111
+
112
+ if (m('CR') == 'H' && m('VC') == 'H') ||
113
+ (m('IR') == 'H' && m('VI') == 'H') ||
114
+ (m('AR') == 'H' && m('VA') == 'H')
115
+ eq6 = '0'
116
+ elsif !((m('CR') == 'H' && m('VC') == 'H') ||
117
+ (m('IR') == 'H' && m('VI') == 'H') ||
118
+ (m('AR') == 'H' && m('VA') == 'H'))
119
+ eq6 = '1'
120
+ end
121
+
122
+ eq1 + eq2 + eq3 + eq4 + eq5 + eq6
123
+ end
124
+
125
+ def score
126
+ # The following defines the index of each metric's values.
127
+ # It is used when looking for the highest vector part of the
128
+ # combinations produced by the MacroVector respective highest vectors.
129
+
130
+ macro_vector = retrieve_macro_vector
131
+
132
+ # Exception for no impact on system (shortcut)
133
+ return 0.0 if %w[VC VI VA SC SI SA].all? { |metric| m(metric) == 'N' }
134
+
135
+ value = LOOKUP[macro_vector]
136
+
137
+ # 1. For each of the EQs:
138
+ # a. The maximal scoring difference is determined as the difference
139
+ # between the current MacroVector and the lower MacroVector.
140
+ # i. If there is no lower MacroVector the available distance is
141
+ # set to nil and then ignored in the further calculations.
142
+ eq1_val = parse_int(macro_vector[0])
143
+ eq2_val = parse_int(macro_vector[1])
144
+ eq3_val = parse_int(macro_vector[2])
145
+ eq4_val = parse_int(macro_vector[3])
146
+ eq5_val = parse_int(macro_vector[4])
147
+ eq6_val = parse_int(macro_vector[5])
148
+
149
+ # compute next lower macro, it can also not exist
150
+ eq1_next_lower_macro = concat_and_stringify(eq1_val + 1, eq2_val, eq3_val, eq4_val, eq5_val, eq6_val)
151
+ eq2_next_lower_macro = concat_and_stringify(eq1_val, eq2_val + 1, eq3_val, eq4_val, eq5_val, eq6_val)
152
+
153
+ # eq3 and eq6 are related
154
+ if eq3_val == 1 && eq6_val == 1
155
+ # 11 --> 21
156
+ eq3eq6_next_lower_macro = concat_and_stringify(eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val)
157
+ elsif eq3_val.zero? && eq6_val == 1
158
+ # 01 --> 11
159
+ eq3eq6_next_lower_macro = concat_and_stringify(eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val)
160
+ elsif eq3_val == 1 && eq6_val.zero?
161
+ # 10 --> 11
162
+ eq3eq6_next_lower_macro = concat_and_stringify(eq1_val, eq2_val, eq3_val, eq4_val, eq5_val, eq6_val + 1)
163
+ elsif eq3_val.zero? && eq6_val.zero?
164
+ # 00 --> 01
165
+ # 00 --> 10
166
+ eq3eq6_next_lower_macro_left = concat_and_stringify(eq1_val, eq2_val, eq3_val, eq4_val, eq5_val, eq6_val + 1)
167
+ eq3eq6_next_lower_macro_right = concat_and_stringify(eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val)
168
+ else
169
+ # 21 --> 32 (do not exist)
170
+ eq3eq6_next_lower_macro = concat_and_stringify(eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val + 1)
171
+ end
172
+
173
+ eq4_next_lower_macro = concat_and_stringify(eq1_val, eq2_val, eq3_val, eq4_val + 1, eq5_val, eq6_val)
174
+ eq5_next_lower_macro = concat_and_stringify(eq1_val, eq2_val, eq3_val, eq4_val, eq5_val + 1, eq6_val)
175
+
176
+ # get their score, if the next lower macro score do not exist the result is NaN
177
+ score_eq1_next_lower_macro = LOOKUP[eq1_next_lower_macro]
178
+ score_eq2_next_lower_macro = LOOKUP[eq2_next_lower_macro]
179
+
180
+ if eq3_val.zero? && eq6_val.zero?
181
+ # multiple path take the one with higher score
182
+ score_eq3eq6_next_lower_macro_left = LOOKUP[eq3eq6_next_lower_macro_left]
183
+ score_eq3eq6_next_lower_macro_right = LOOKUP[eq3eq6_next_lower_macro_right]
184
+
185
+ score_eq3eq6_next_lower_macro = if score_eq3eq6_next_lower_macro_left > score_eq3eq6_next_lower_macro_right
186
+ score_eq3eq6_next_lower_macro_left
187
+ else
188
+ score_eq3eq6_next_lower_macro_right
189
+ end
190
+ else
191
+ score_eq3eq6_next_lower_macro = LOOKUP[eq3eq6_next_lower_macro]
192
+ end
193
+
194
+ score_eq4_next_lower_macro = LOOKUP[eq4_next_lower_macro]
195
+ score_eq5_next_lower_macro = LOOKUP[eq5_next_lower_macro]
196
+
197
+ # b. The severity distance of the to-be scored vector from a
198
+ # highest severity vector in the same MacroVector is determined.
199
+ eq1_maxes = get_eq_maxes(macro_vector, 1)
200
+ eq2_maxes = get_eq_maxes(macro_vector, 2)
201
+ eq3_eq6_maxes = get_eq_maxes(macro_vector, 3)[macro_vector[5]]
202
+ eq4_maxes = get_eq_maxes(macro_vector, 4)
203
+ eq5_maxes = get_eq_maxes(macro_vector, 5)
204
+
205
+ # compose them
206
+ max_vectors = []
207
+ eq1_maxes.each do |eq1_max|
208
+ eq2_maxes.each do |eq2_max|
209
+ eq3_eq6_maxes.each do |eq3_eq6_max|
210
+ eq4_maxes.each do |eq4_max|
211
+ eq5_maxes.each do |eq5max|
212
+ max_vectors.push(eq1_max + eq2_max + eq3_eq6_max + eq4_max + eq5max)
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ severity_distance_av = severity_distance_pr = severity_distance_ui = 0
220
+ severity_distance_ac = severity_distance_at = severity_distance_vc = 0
221
+ severity_distance_vi = severity_distance_va = severity_distance_sc = 0
222
+ severity_distance_si = severity_distance_sa = severity_distance_cr = 0
223
+ severity_distance_ir = severity_distance_ar = 0
224
+
225
+ # Find the max vector to use i.e. one in the combination of all the highests
226
+ # that is greater or equal (severity distance) than the to-be scored vector.
227
+ max_vectors.each do |max_vector|
228
+ severity_distance_av = AV_LEVELS[m('AV')] - AV_LEVELS[extract_value_metric('AV', max_vector)]
229
+ severity_distance_pr = PR_LEVELS[m('PR')] - PR_LEVELS[extract_value_metric('PR', max_vector)]
230
+ severity_distance_ui = UI_LEVELS[m('UI')] - UI_LEVELS[extract_value_metric('UI', max_vector)]
231
+
232
+ severity_distance_ac = AC_LEVELS[m('AC')] - AC_LEVELS[extract_value_metric('AC', max_vector)]
233
+ severity_distance_at = AT_LEVELS[m('AT')] - AT_LEVELS[extract_value_metric('AT', max_vector)]
234
+
235
+ severity_distance_vc = VC_LEVELS[m('VC')] - VC_LEVELS[extract_value_metric('VC', max_vector)]
236
+ severity_distance_vi = VI_LEVELS[m('VI')] - VI_LEVELS[extract_value_metric('VI', max_vector)]
237
+ severity_distance_va = VA_LEVELS[m('VA')] - VA_LEVELS[extract_value_metric('VA', max_vector)]
238
+
239
+ severity_distance_sc = SC_LEVELS[m('SC')] - SC_LEVELS[extract_value_metric('SC', max_vector)]
240
+ severity_distance_si = SI_LEVELS[m('SI')] - SI_LEVELS[extract_value_metric('SI', max_vector)]
241
+ severity_distance_sa = SA_LEVELS[m('SA')] - SA_LEVELS[extract_value_metric('SA', max_vector)]
242
+
243
+ severity_distance_cr = subtract_or_nil(CR_LEVELS[m('CR')], CR_LEVELS[extract_value_metric('CR', max_vector)])
244
+ severity_distance_ir = subtract_or_nil(IR_LEVELS[m('IR')], IR_LEVELS[extract_value_metric('IR', max_vector)])
245
+ severity_distance_ar = subtract_or_nil(AR_LEVELS[m('AR')], AR_LEVELS[extract_value_metric('AR', max_vector)])
246
+
247
+ # if any is less than zero this is not the right max
248
+ if [severity_distance_av, severity_distance_pr, severity_distance_ui, severity_distance_ac,
249
+ severity_distance_at, severity_distance_vc, severity_distance_vi, severity_distance_va,
250
+ severity_distance_sc, severity_distance_si, severity_distance_sa,
251
+ severity_distance_cr,
252
+ severity_distance_ir, severity_distance_ar].compact.any?(&:negative?)
253
+ next
254
+ end
255
+
256
+ # if multiple maxes exist to reach it it is enough the first one
257
+ break
258
+ end
259
+
260
+ current_severity_distance_eq1 = severity_distance_av + severity_distance_pr + severity_distance_ui
261
+ current_severity_distance_eq2 = severity_distance_ac + severity_distance_at
262
+ current_severity_distance_eq3eq6 = sum_or_nil([severity_distance_vc, severity_distance_vi, severity_distance_va,
263
+ severity_distance_cr, severity_distance_ir, severity_distance_ar])
264
+ current_severity_distance_eq4 = severity_distance_sc + severity_distance_si + severity_distance_sa
265
+
266
+ step = 0.1
267
+
268
+ # if the next lower macro score do not exist the result is Nan
269
+ # Rename to maximal scoring difference (aka MSD)
270
+ available_distance_eq1 = score_eq1_next_lower_macro ? value - score_eq1_next_lower_macro : nil
271
+ available_distance_eq2 = score_eq2_next_lower_macro ? value - score_eq2_next_lower_macro : nil
272
+ available_distance_eq3eq6 = score_eq3eq6_next_lower_macro ? value - score_eq3eq6_next_lower_macro : nil
273
+ available_distance_eq4 = score_eq4_next_lower_macro ? value - score_eq4_next_lower_macro : nil
274
+ available_distance_eq5 = score_eq5_next_lower_macro ? value - score_eq5_next_lower_macro : nil
275
+
276
+ # some of them do not exist, we will find them by retrieving the score. If score null then do not exist
277
+ n_existing_lower = 0
278
+
279
+ normalized_severity_eq1 = 0
280
+ normalized_severity_eq2 = 0
281
+ normalized_severity_eq3eq6 = 0
282
+ normalized_severity_eq4 = 0
283
+ normalized_severity_eq5 = 0
284
+
285
+ # multiply by step because distance is pure
286
+ max_severity_eq1 = MAX_SEVERITY['eq1'][eq1_val] * step
287
+ max_severity_eq2 = MAX_SEVERITY['eq2'][eq2_val] * step
288
+ max_severity_eq3eq6 = MAX_SEVERITY['eq3eq6'][eq3_val][eq6_val] * step
289
+ max_severity_eq4 = MAX_SEVERITY['eq4'][eq4_val] * step
290
+
291
+ # c. The proportion of the distance is determined by dividing
292
+ # the severity distance of the to-be-scored vector by the depth
293
+ # of the MacroVector.
294
+ # d. The maximal scoring difference is multiplied by the proportion of
295
+ # distance.
296
+ unless nil?(available_distance_eq1)
297
+ n_existing_lower += 1
298
+ percent_to_next_eq1_severity = current_severity_distance_eq1 / max_severity_eq1
299
+ normalized_severity_eq1 = available_distance_eq1 * percent_to_next_eq1_severity
300
+ end
301
+
302
+ unless nil?(available_distance_eq2)
303
+ n_existing_lower += 1
304
+ percent_to_next_eq2_severity = current_severity_distance_eq2 / max_severity_eq2
305
+ normalized_severity_eq2 = available_distance_eq2 * percent_to_next_eq2_severity
306
+ end
307
+
308
+ unless nil?(available_distance_eq3eq6)
309
+ n_existing_lower += 1
310
+ percent_to_next_eq3eq6_severity = current_severity_distance_eq3eq6 / max_severity_eq3eq6
311
+ normalized_severity_eq3eq6 = available_distance_eq3eq6 * percent_to_next_eq3eq6_severity
312
+ end
313
+
314
+ unless nil?(available_distance_eq4)
315
+ n_existing_lower += 1
316
+ percent_to_next_eq4_severity = current_severity_distance_eq4 / max_severity_eq4
317
+ normalized_severity_eq4 = available_distance_eq4 * percent_to_next_eq4_severity
318
+ end
319
+
320
+ unless nil?(available_distance_eq5)
321
+ # for eq5 is always 0 the percentage
322
+ n_existing_lower += 1
323
+ percent_to_next_eq5_severity = 0
324
+ normalized_severity_eq5 = available_distance_eq5 * percent_to_next_eq5_severity
325
+ end
326
+
327
+ # 2. The mean of the above computed proportional distances is computed.
328
+ mean_distance = if n_existing_lower.zero?
329
+ 0
330
+ else # sometimes we need to go up but there is nothing there, or down
331
+ # but there is nothing there so it's a change of 0.
332
+ (normalized_severity_eq1 + normalized_severity_eq2 + normalized_severity_eq3eq6 +
333
+ normalized_severity_eq4 + normalized_severity_eq5) / n_existing_lower
334
+ end
335
+
336
+ # 3. The score of the vector is the score of the MacroVector
337
+ # (i.e. the score of the highest severity vector) minus the mean
338
+ # distance so computed. This score is rounded to one decimal place.
339
+ value -= mean_distance
340
+ value = 0.0 if value.negative?
341
+ value = 10.0 if value > 10
342
+ value.round(1)
343
+ end
344
+
345
+ def get_eq_maxes(lookup, eq_value)
346
+ MAX_COMPOSED["eq#{eq_value}"][lookup[eq_value - 1]]
347
+ end
348
+
349
+ def nil?(value)
350
+ value.nil?
351
+ end
352
+
353
+ def concat_and_stringify(first, second, third, fourth, fifth, sixth)
354
+ ''.concat(first.to_s, second.to_s, third.to_s, fourth.to_s, fifth.to_s, sixth.to_s)
355
+ end
356
+
357
+ def sum_or_nil(values)
358
+ return nil if values.any?(&:nil?)
359
+
360
+ values.sum
361
+ end
362
+
363
+ def subtract_or_nil(left, right)
364
+ return nil if left.nil? || right.nil?
365
+
366
+ left - right
367
+ end
368
+
369
+ def parse_int(string_to_parse)
370
+ Integer(string_to_parse)
371
+ end
372
+
373
+ def extract_value_metric(metric, str)
374
+ # indexOf gives first index of the metric, we then need to go over its size
375
+ index = str.index(metric) + metric.length + 1
376
+ extracted = str.slice(index..)
377
+ # remove what follow
378
+ if extracted.index('/').positive?
379
+ index_to_drop_after = extracted.index('/') - 1
380
+ metric_val = extracted.truncate(index_to_drop_after)
381
+ elsif extracted
382
+ metric_val = extracted
383
+ # case where it is the last metric so no ending /
384
+ end
385
+
386
+ metric_val
387
+ end
388
+ end
389
+ end
@@ -0,0 +1,26 @@
1
+ module CvssSuite
2
+ module Cvss40Constants
3
+ # 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
4
+
5
+ AV_LEVELS = { 'N' => 0.0, 'A' => 0.1, 'L' => 0.2, 'P' => 0.3 }.freeze
6
+ PR_LEVELS = { 'N' => 0.0, 'L' => 0.1, 'H' => 0.2 }.freeze
7
+ UI_LEVELS = { 'N' => 0.0, 'P' => 0.1, 'A' => 0.2 }.freeze
8
+
9
+ AC_LEVELS = { 'L' => 0.0, 'H' => 0.1 }.freeze
10
+ AT_LEVELS = { 'N' => 0.0, 'P' => 0.1 }.freeze
11
+
12
+ VC_LEVELS = { 'H' => 0.0, 'L' => 0.1, 'N' => 0.2 }.freeze
13
+ VI_LEVELS = { 'H' => 0.0, 'L' => 0.1, 'N' => 0.2 }.freeze
14
+ VA_LEVELS = { 'H' => 0.0, 'L' => 0.1, 'N' => 0.2 }.freeze
15
+
16
+ SC_LEVELS = { 'H' => 0.1, 'L' => 0.2, 'N' => 0.3 }.freeze
17
+ SI_LEVELS = { 'S' => 0.0, 'H' => 0.1, 'L' => 0.2, 'N' => 0.3 }.freeze
18
+ SA_LEVELS = { 'S' => 0.0, 'H' => 0.1, 'L' => 0.2, 'N' => 0.3 }.freeze
19
+
20
+ CR_LEVELS = { 'H' => 0.0, 'M' => 0.1, 'L' => 0.2 }.freeze
21
+ IR_LEVELS = { 'H' => 0.0, 'M' => 0.1, 'L' => 0.2 }.freeze
22
+ AR_LEVELS = { 'H' => 0.0, 'M' => 0.1, 'L' => 0.2 }.freeze
23
+
24
+ E_LEVELS = { 'U' => 0.2, 'P' => 0.1, 'A' => 0 }.freeze
25
+ end
26
+ end