cvss-suite 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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
         |