cvss-suite 3.1.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +4 -4
- data/.github/workflows/rubocop.yml +3 -4
- data/.rubocop.yml +20 -0
- data/.rubocop_todo.yml +2 -2
- data/CHANGES.md +13 -0
- data/CODE_OF_CONDUCT.md +9 -2
- data/Gemfile +0 -6
- data/LICENSE.md +10 -1
- data/README.md +20 -5
- data/cvss_suite.gemspec +7 -10
- data/lib/cvss_suite/cvss.rb +4 -32
- data/lib/cvss_suite/cvss2/cvss2.rb +2 -8
- data/lib/cvss_suite/cvss2/cvss2_base.rb +0 -6
- data/lib/cvss_suite/cvss2/cvss2_environmental.rb +0 -6
- data/lib/cvss_suite/cvss2/cvss2_temporal.rb +0 -6
- data/lib/cvss_suite/cvss3/cvss3.rb +8 -8
- data/lib/cvss_suite/cvss3/cvss3_base.rb +0 -6
- data/lib/cvss_suite/cvss3/cvss3_environmental.rb +0 -6
- data/lib/cvss_suite/cvss3/cvss3_temporal.rb +0 -6
- data/lib/cvss_suite/cvss31/cvss31.rb +8 -8
- data/lib/cvss_suite/cvss31/cvss31_base.rb +0 -6
- data/lib/cvss_suite/cvss31/cvss31_environmental.rb +0 -6
- data/lib/cvss_suite/cvss31/cvss31_temporal.rb +0 -6
- data/lib/cvss_suite/cvss40/cvss40.rb +43 -0
- data/lib/cvss_suite/cvss40/cvss40_all_up.rb +40 -0
- data/lib/cvss_suite/cvss40/cvss40_base.rb +86 -0
- data/lib/cvss_suite/cvss40/cvss40_calc_helper.rb +389 -0
- data/lib/cvss_suite/cvss40/cvss40_constants_levels.rb +26 -0
- data/lib/cvss_suite/cvss40/cvss40_constants_macro_vector_lookup.rb +278 -0
- data/lib/cvss_suite/cvss40/cvss40_constants_max_composed.rb +41 -0
- data/lib/cvss_suite/cvss40/cvss40_constants_max_severity.rb +31 -0
- data/lib/cvss_suite/cvss40/cvss40_environmental.rb +105 -0
- data/lib/cvss_suite/cvss40/cvss40_environmental_security.rb +47 -0
- data/lib/cvss_suite/cvss40/cvss40_supplemental.rb +66 -0
- data/lib/cvss_suite/cvss40/cvss40_threat.rb +34 -0
- data/lib/cvss_suite/cvss_31_and_before.rb +50 -0
- data/lib/cvss_suite/cvss_40_and_later.rb +45 -0
- data/lib/cvss_suite/cvss_metric.rb +4 -6
- data/lib/cvss_suite/cvss_property.rb +0 -6
- data/lib/cvss_suite/errors.rb +0 -6
- data/lib/cvss_suite/extensions/string.rb +8 -0
- data/lib/cvss_suite/helpers/cvss31_helper.rb +0 -6
- data/lib/cvss_suite/helpers/cvss3_helper.rb +0 -6
- data/lib/cvss_suite/invalid_cvss.rb +0 -6
- data/lib/cvss_suite/version.rb +1 -7
- data/lib/cvss_suite.rb +6 -7
- 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
|