cvss-suite 3.0.1 → 3.3.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 (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/.rspec +1 -0
  5. data/.rubocop.yml +20 -0
  6. data/.rubocop_todo.yml +2 -2
  7. data/CHANGES.md +34 -0
  8. data/CODE_OF_CONDUCT.md +0 -5
  9. data/Gemfile +0 -6
  10. data/LICENSE.md +12 -2
  11. data/README.md +20 -11
  12. data/cvss_suite.gemspec +12 -14
  13. data/lib/cvss_suite/cvss.rb +7 -55
  14. data/lib/cvss_suite/cvss2/cvss2.rb +2 -8
  15. data/lib/cvss_suite/cvss2/cvss2_base.rb +0 -6
  16. data/lib/cvss_suite/cvss2/cvss2_environmental.rb +0 -6
  17. data/lib/cvss_suite/cvss2/cvss2_temporal.rb +0 -6
  18. data/lib/cvss_suite/cvss3/cvss3.rb +8 -8
  19. data/lib/cvss_suite/cvss3/cvss3_base.rb +8 -14
  20. data/lib/cvss_suite/cvss3/cvss3_environmental.rb +11 -17
  21. data/lib/cvss_suite/cvss3/cvss3_temporal.rb +3 -9
  22. data/lib/cvss_suite/cvss31/cvss31.rb +8 -8
  23. data/lib/cvss_suite/cvss31/cvss31_base.rb +8 -14
  24. data/lib/cvss_suite/cvss31/cvss31_environmental.rb +11 -17
  25. data/lib/cvss_suite/cvss31/cvss31_temporal.rb +3 -9
  26. data/lib/cvss_suite/cvss40/cvss40.rb +43 -0
  27. data/lib/cvss_suite/cvss40/cvss40_all_up.rb +40 -0
  28. data/lib/cvss_suite/cvss40/cvss40_base.rb +86 -0
  29. data/lib/cvss_suite/cvss40/cvss40_calc_helper.rb +397 -0
  30. data/lib/cvss_suite/cvss40/cvss40_constants_levels.rb +26 -0
  31. data/lib/cvss_suite/cvss40/cvss40_constants_macro_vector_lookup.rb +278 -0
  32. data/lib/cvss_suite/cvss40/cvss40_constants_max_composed.rb +41 -0
  33. data/lib/cvss_suite/cvss40/cvss40_constants_max_severity.rb +31 -0
  34. data/lib/cvss_suite/cvss40/cvss40_environmental.rb +105 -0
  35. data/lib/cvss_suite/cvss40/cvss40_environmental_security.rb +47 -0
  36. data/lib/cvss_suite/cvss40/cvss40_supplemental.rb +66 -0
  37. data/lib/cvss_suite/cvss40/cvss40_threat.rb +34 -0
  38. data/lib/cvss_suite/cvss_31_and_before.rb +61 -0
  39. data/lib/cvss_suite/cvss_40_and_later.rb +51 -0
  40. data/lib/cvss_suite/cvss_metric.rb +7 -7
  41. data/lib/cvss_suite/cvss_property.rb +23 -8
  42. data/lib/cvss_suite/errors.rb +0 -6
  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 +48 -11
  48. metadata +40 -11
@@ -0,0 +1,397 @@
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
+ String.new.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 = truncate(extracted, 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
+
389
+ # rails defines this method on String, so we need to avoid polluting the
390
+ # String class to preserve Rails behavior.
391
+ def truncate(string_to_truncate, truncate_to)
392
+ return string_to_truncate.dup unless string_to_truncate.length > truncate_to
393
+
394
+ (string_to_truncate[0, truncate_to + 1]).to_s
395
+ end
396
+ end
397
+ 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