Last active
February 8, 2024 08:30
-
-
Save AdrianVollmer/a00bc9c06aa007c0bf54c7375130b63e to your computer and use it in GitHub Desktop.
CVSSv4 Lua implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env lua | |
-- This is a translation of https://github.com/RedHatProductSecurity/cvss/blob/master/cvss/cvss4.py | |
-- Translated by Adrian Vollmer, SySS GmbH, 2024 | |
-- Copyright (c) 2023 FIRST.ORG, Inc., Red Hat, and contributors | |
-- | |
-- Redistribution and use in source and binary forms, with or without | |
-- modification, are permitted provided that the following conditions are met: | |
-- | |
-- 1. Redistributions of source code must retain the above copyright notice, this | |
-- list of conditions and the following disclaimer. | |
-- | |
-- 2. Redistributions in binary form must reproduce the above copyright notice, | |
-- this list of conditions and the following disclaimer in the documentation | |
-- and/or other materials provided with the distribution. | |
-- | |
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
-- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
-- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
-- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
-- The following bash script checks this code against some pre defined test cases: | |
-- #!/bin/bash | |
-- | |
-- # Define an array of URLs | |
-- base_url="https://raw.githubusercontent.com/RedHatProductSecurity/cvss/master/tests" | |
-- files=("vectors_base4" "vectors_modified4" "vectors_random4" "vectors_threat4" "vectors_supplemental4") | |
-- | |
-- # Loop through each URL | |
-- for file in "${files[@]}"; do | |
-- # Download the file and store it in a variable | |
-- file_content=$(curl -s "$base_url/$file") | |
-- | |
-- # Loop through each line in the file_content | |
-- while IFS= read -r line; do | |
-- # Extract CVSS VECTOR and CVSS SCORE using awk | |
-- cvss_vector=$(echo "$line" | awk -F ' - ' '{print $1}') | |
-- cvss_score=$(echo "$line" | awk -F ' - ' '{print $2}' | tr -dc '[[:print:]]') | |
-- | |
-- # Run lua cvss4.lua <CVSS VECTOR> and compare the output to CVSS SCORE | |
-- lua_output="$(lua cvss4.lua "$cvss_vector")" | |
-- | |
-- if [ "$lua_output" != "$cvss_score" ]; then | |
-- echo "Mismatch: $cvss_vector - Expected: $cvss_score - Lua output: $lua_output" | |
-- fi | |
-- done <<< "$file_content" | |
-- done | |
local vector = arg[1] | |
local metrics = {} | |
local missing_metrics = {} | |
local original_metrics = {} | |
local base_score = nil | |
local severity = nil | |
METRICS = { | |
"AV", | |
"AC", | |
"AT", | |
"PR", | |
"UI", | |
"VC", | |
"VI", | |
"VA", | |
"SC", | |
"SI", | |
"SA", | |
"S", | |
"AU", | |
"R", | |
"V", | |
"RE", | |
"U", | |
"MAV", | |
"MAC", | |
"MAT", | |
"MPR", | |
"MUI", | |
"MVC", | |
"MVI", | |
"MVA", | |
"MSC", | |
"MSI", | |
"MSA", | |
"CR", | |
"IR", | |
"AR", | |
"E" | |
} | |
METRICS_MANDATORY = { | |
"AV", | |
"AC", | |
"AT", | |
"PR", | |
"UI", | |
"VC", | |
"VI", | |
"VA", | |
"SC", | |
"SI", | |
"SA" | |
} | |
METRICS_ABBREVIATIONS = { | |
["AV"] = "Attack Vector", | |
["AC"] = "Attack Complexity", | |
["AT"] = "Attack Requirement", | |
["PR"] = "Privileges Required", | |
["UI"] = "User Interaction", | |
["VC"] = "Vulnerable System Impact Confidentiality", | |
["VI"] = "Vulnerable System Impact Integrity", | |
["VA"] = "Vulnerable System Impact Availability", | |
["SC"] = "Subsequent System Impact Confidentiality", | |
["SI"] = "Subsequent System Impact Integrity", | |
["SA"] = "Subsequent System Impact Availability", | |
["S"] = "Safety", | |
["AU"] = "Automatable", | |
["R"] = "Recovery", | |
["V"] = "Value Density", | |
["RE"] = "Vulnerability Response Effort", | |
["U"] = "Provider Urgency", | |
["MAV"] = "Modified Attack Vector", | |
["MAC"] = "Modified Attack Complexity", | |
["MAT"] = "Modified Attack Requirement", | |
["MPR"] = "Modified Privileges Required", | |
["MUI"] = "Modified User Interaction", | |
["MVC"] = "Modified Vulnerable System Impact Confidentiality", | |
["MVI"] = "Modified Vulnerable System Impact Integrity", | |
["MVA"] = "Modified Vulnerable System Impact Availability", | |
["MSC"] = "Modified Subsequent System Impact Confidentiality", | |
["MSI"] = "Modified Subsequent System Impact Integrity", | |
["MSA"] = "Modified Subsequent System Impact Availability", | |
["CR"] = "Confidentiality Req.", | |
["IR"] = "Integrity Req.", | |
["AR"] = "Availability Req.", | |
["E"] = "Exploit Maturity" | |
} | |
METRICS_VALUE_NAMES = { | |
["AV"] = { | |
["N"] = "Network", | |
["A"] = "Adjacent", | |
["L"] = "Local", | |
["P"] = "Physical" | |
}, | |
["AC"] = { | |
["L"] = "Low", | |
["H"] = "High" | |
}, | |
["AT"] = { | |
["N"] = "None", | |
["P"] = "Present" | |
}, | |
["PR"] = { | |
["N"] = "None", | |
["L"] = "Low", | |
["H"] = "High" | |
}, | |
["UI"] = { | |
["N"] = "None", | |
["P"] = "Passive", | |
["A"] = "Active" | |
}, | |
["VC"] = { | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["VI"] = { | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["VA"] = { | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["SC"] = { | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["SI"] = { | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["SA"] = { | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["S"] = { | |
["X"] = "Not Defined", | |
["N"] = "Negligible", | |
["P"] = "Present" | |
}, | |
["AU"] = { | |
["X"] = "Not Defined", | |
["N"] = "No", | |
["Y"] = "Yes" | |
}, | |
["R"] = { | |
["X"] = "Not Defined", | |
["A"] = "Automatic", | |
["U"] = "User", | |
["I"] = "Inrecoverable" | |
}, | |
["V"] = { | |
["X"] = "Not Defined", | |
["D"] = "Diffuse", | |
["C"] = "Concentrated" | |
}, | |
["RE"] = { | |
["X"] = "Not Defined", | |
["L"] = "Low", | |
["M"] = "Moderate", | |
["H"] = "High" | |
}, | |
["U"] = { | |
["X"] = "Not Defined", | |
["Clear"] = "Clear", | |
["Green"] = "Green", | |
["Amber"] = "Amber", | |
["Red"] = "Red" | |
}, | |
["MAV"] = { | |
["X"] = "Not Defined", | |
["N"] = "Network", | |
["A"] = "Adjacent", | |
["L"] = "Local", | |
["P"] = "Physical" | |
}, | |
["MAC"] = { | |
["X"] = "Not Defined", | |
["L"] = "Low", | |
["H"] = "High" | |
}, | |
["MAT"] = { | |
["X"] = "Not Defined", | |
["N"] = "None", | |
["P"] = "Present" | |
}, | |
["MPR"] = { | |
["X"] = "Not Defined", | |
["N"] = "None", | |
["L"] = "Low", | |
["H"] = "High" | |
}, | |
["MUI"] = { | |
["X"] = "Not Defined", | |
["N"] = "None", | |
["P"] = "Passive", | |
["A"] = "Active" | |
}, | |
["MVC"] = { | |
["X"] = "Not Defined", | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["MVI"] = { | |
["X"] = "Not Defined", | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["MVA"] = { | |
["X"] = "Not Defined", | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "None" | |
}, | |
["MSC"] = { | |
["X"] = "Not Defined", | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "Negligible" | |
}, | |
["MSI"] = { | |
["X"] = "Not Defined", | |
["S"] = "Safety", | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "Negligible" | |
}, | |
["MSA"] = { | |
["X"] = "Not Defined", | |
["S"] = "Safety", | |
["H"] = "High", | |
["L"] = "Low", | |
["N"] = "Negligible" | |
}, | |
["CR"] = { | |
["X"] = "Not Defined", | |
["H"] = "High", | |
["M"] = "Medium", | |
["L"] = "Low" | |
}, | |
["IR"] = { | |
["X"] = "Not Defined", | |
["H"] = "High", | |
["M"] = "Medium", | |
["L"] = "Low" | |
}, | |
["AR"] = { | |
["X"] = "Not Defined", | |
["H"] = "High", | |
["M"] = "Medium", | |
["L"] = "Low" | |
}, | |
["E"] = { | |
["X"] = "Not Defined", | |
["A"] = "Attacked", | |
["P"] = "POC", | |
["U"] = "Unreported" | |
} | |
} | |
MAX_COMPOSED = { | |
["eq1"] = { | |
["0"] = { | |
"AV:N/PR:N/UI:N/" | |
}, | |
["1"] = { | |
"AV:A/PR:N/UI:N/", | |
"AV:N/PR:L/UI:N/", | |
"AV:N/PR:N/UI:P/" | |
}, | |
["2"] = { | |
"AV:P/PR:N/UI:N/", | |
"AV:A/PR:L/UI:P/" | |
} | |
}, | |
["eq2"] = { | |
["0"] = { | |
"AC:L/AT:N/" | |
}, | |
["1"] = { | |
"AC:H/AT:N/", | |
"AC:L/AT:P/" | |
} | |
}, | |
["eq3"] = { | |
["0"] = { | |
["0"] = { | |
"VC:H/VI:H/VA:H/CR:H/IR:H/AR:H/" | |
}, | |
["1"] = { | |
"VC:H/VI:H/VA:L/CR:M/IR:M/AR:H/", | |
"VC:H/VI:H/VA:H/CR:M/IR:M/AR:M/" | |
} | |
}, | |
["1"] = { | |
["0"] = { | |
"VC:L/VI:H/VA:H/CR:H/IR:H/AR:H/", | |
"VC:H/VI:L/VA:H/CR:H/IR:H/AR:H/" | |
}, | |
["1"] = { | |
"VC:L/VI:H/VA:L/CR:H/IR:M/AR:H/", | |
"VC:L/VI:H/VA:H/CR:H/IR:M/AR:M/", | |
"VC:H/VI:L/VA:H/CR:M/IR:H/AR:M/", | |
"VC:H/VI:L/VA:L/CR:M/IR:H/AR:H/", | |
"VC:L/VI:L/VA:H/CR:H/IR:H/AR:M/" | |
} | |
}, | |
["2"] = { | |
["1"] = { | |
"VC:L/VI:L/VA:L/CR:H/IR:H/AR:H/" | |
} | |
} | |
}, | |
["eq4"] = { | |
["0"] = { | |
"SC:H/SI:S/SA:S/" | |
}, | |
["1"] = { | |
"SC:H/SI:H/SA:H/" | |
}, | |
["2"] = { | |
"SC:L/SI:L/SA:L/" | |
} | |
}, | |
["eq5"] = { | |
["0"] = { | |
"E:A/" | |
}, | |
["1"] = { | |
"E:P/" | |
}, | |
["2"] = { | |
"E:U/" | |
} | |
} | |
} | |
MAX_SEVERITY = { | |
["eq1"] = { | |
["0"] = 1, | |
["1"] = 4, | |
["2"] = 5 | |
}, | |
["eq2"] = { | |
["0"] = 1, | |
["1"] = 2 | |
}, | |
["eq3eq6"] = { | |
["0"] = { | |
["0"] = 7, | |
["1"] = 6 | |
}, | |
["1"] = { | |
["0"] = 8, | |
["1"] = 8 | |
}, | |
["2"] = { | |
["1"] = 10 | |
} | |
}, | |
["eq4"] = { | |
["0"] = 6, | |
["1"] = 5, | |
["2"] = 4 | |
}, | |
["eq5"] = { | |
["0"] = 1, | |
["1"] = 1, | |
["2"] = 1 | |
} | |
} | |
CVSS_LOOKUP_GLOBAL = { | |
["000000"] = 10, | |
["000001"] = 9.9, | |
["000010"] = 9.8, | |
["000011"] = 9.5, | |
["000020"] = 9.5, | |
["000021"] = 9.2, | |
["000100"] = 10, | |
["000101"] = 9.6, | |
["000110"] = 9.3, | |
["000111"] = 8.7, | |
["000120"] = 9.1, | |
["000121"] = 8.1, | |
["000200"] = 9.3, | |
["000201"] = 9, | |
["000210"] = 8.9, | |
["000211"] = 8, | |
["000220"] = 8.1, | |
["000221"] = 6.8, | |
["001000"] = 9.8, | |
["001001"] = 9.5, | |
["001010"] = 9.5, | |
["001011"] = 9.2, | |
["001020"] = 9, | |
["001021"] = 8.4, | |
["001100"] = 9.3, | |
["001101"] = 9.2, | |
["001110"] = 8.9, | |
["001111"] = 8.1, | |
["001120"] = 8.1, | |
["001121"] = 6.5, | |
["001200"] = 8.8, | |
["001201"] = 8, | |
["001210"] = 7.8, | |
["001211"] = 7, | |
["001220"] = 6.9, | |
["001221"] = 4.8, | |
["002001"] = 9.2, | |
["002011"] = 8.2, | |
["002021"] = 7.2, | |
["002101"] = 7.9, | |
["002111"] = 6.9, | |
["002121"] = 5, | |
["002201"] = 6.9, | |
["002211"] = 5.5, | |
["002221"] = 2.7, | |
["010000"] = 9.9, | |
["010001"] = 9.7, | |
["010010"] = 9.5, | |
["010011"] = 9.2, | |
["010020"] = 9.2, | |
["010021"] = 8.5, | |
["010100"] = 9.5, | |
["010101"] = 9.1, | |
["010110"] = 9, | |
["010111"] = 8.3, | |
["010120"] = 8.4, | |
["010121"] = 7.1, | |
["010200"] = 9.2, | |
["010201"] = 8.1, | |
["010210"] = 8.2, | |
["010211"] = 7.1, | |
["010220"] = 7.2, | |
["010221"] = 5.3, | |
["011000"] = 9.5, | |
["011001"] = 9.3, | |
["011010"] = 9.2, | |
["011011"] = 8.5, | |
["011020"] = 8.5, | |
["011021"] = 7.3, | |
["011100"] = 9.2, | |
["011101"] = 8.2, | |
["011110"] = 8, | |
["011111"] = 7.2, | |
["011120"] = 7, | |
["011121"] = 5.9, | |
["011200"] = 8.4, | |
["011201"] = 7, | |
["011210"] = 7.1, | |
["011211"] = 5.2, | |
["011220"] = 5, | |
["011221"] = 3, | |
["012001"] = 8.6, | |
["012011"] = 7.5, | |
["012021"] = 5.2, | |
["012101"] = 7.1, | |
["012111"] = 5.2, | |
["012121"] = 2.9, | |
["012201"] = 6.3, | |
["012211"] = 2.9, | |
["012221"] = 1.7, | |
["100000"] = 9.8, | |
["100001"] = 9.5, | |
["100010"] = 9.4, | |
["100011"] = 8.7, | |
["100020"] = 9.1, | |
["100021"] = 8.1, | |
["100100"] = 9.4, | |
["100101"] = 8.9, | |
["100110"] = 8.6, | |
["100111"] = 7.4, | |
["100120"] = 7.7, | |
["100121"] = 6.4, | |
["100200"] = 8.7, | |
["100201"] = 7.5, | |
["100210"] = 7.4, | |
["100211"] = 6.3, | |
["100220"] = 6.3, | |
["100221"] = 4.9, | |
["101000"] = 9.4, | |
["101001"] = 8.9, | |
["101010"] = 8.8, | |
["101011"] = 7.7, | |
["101020"] = 7.6, | |
["101021"] = 6.7, | |
["101100"] = 8.6, | |
["101101"] = 7.6, | |
["101110"] = 7.4, | |
["101111"] = 5.8, | |
["101120"] = 5.9, | |
["101121"] = 5, | |
["101200"] = 7.2, | |
["101201"] = 5.7, | |
["101210"] = 5.7, | |
["101211"] = 5.2, | |
["101220"] = 5.2, | |
["101221"] = 2.5, | |
["102001"] = 8.3, | |
["102011"] = 7, | |
["102021"] = 5.4, | |
["102101"] = 6.5, | |
["102111"] = 5.8, | |
["102121"] = 2.6, | |
["102201"] = 5.3, | |
["102211"] = 2.1, | |
["102221"] = 1.3, | |
["110000"] = 9.5, | |
["110001"] = 9, | |
["110010"] = 8.8, | |
["110011"] = 7.6, | |
["110020"] = 7.6, | |
["110021"] = 7, | |
["110100"] = 9, | |
["110101"] = 7.7, | |
["110110"] = 7.5, | |
["110111"] = 6.2, | |
["110120"] = 6.1, | |
["110121"] = 5.3, | |
["110200"] = 7.7, | |
["110201"] = 6.6, | |
["110210"] = 6.8, | |
["110211"] = 5.9, | |
["110220"] = 5.2, | |
["110221"] = 3, | |
["111000"] = 8.9, | |
["111001"] = 7.8, | |
["111010"] = 7.6, | |
["111011"] = 6.7, | |
["111020"] = 6.2, | |
["111021"] = 5.8, | |
["111100"] = 7.4, | |
["111101"] = 5.9, | |
["111110"] = 5.7, | |
["111111"] = 5.7, | |
["111120"] = 4.7, | |
["111121"] = 2.3, | |
["111200"] = 6.1, | |
["111201"] = 5.2, | |
["111210"] = 5.7, | |
["111211"] = 2.9, | |
["111220"] = 2.4, | |
["111221"] = 1.6, | |
["112001"] = 7.1, | |
["112011"] = 5.9, | |
["112021"] = 3, | |
["112101"] = 5.8, | |
["112111"] = 2.6, | |
["112121"] = 1.5, | |
["112201"] = 2.3, | |
["112211"] = 1.3, | |
["112221"] = 0.6, | |
["200000"] = 9.3, | |
["200001"] = 8.7, | |
["200010"] = 8.6, | |
["200011"] = 7.2, | |
["200020"] = 7.5, | |
["200021"] = 5.8, | |
["200100"] = 8.6, | |
["200101"] = 7.4, | |
["200110"] = 7.4, | |
["200111"] = 6.1, | |
["200120"] = 5.6, | |
["200121"] = 3.4, | |
["200200"] = 7, | |
["200201"] = 5.4, | |
["200210"] = 5.2, | |
["200211"] = 4, | |
["200220"] = 4, | |
["200221"] = 2.2, | |
["201000"] = 8.5, | |
["201001"] = 7.5, | |
["201010"] = 7.4, | |
["201011"] = 5.5, | |
["201020"] = 6.2, | |
["201021"] = 5.1, | |
["201100"] = 7.2, | |
["201101"] = 5.7, | |
["201110"] = 5.5, | |
["201111"] = 4.1, | |
["201120"] = 4.6, | |
["201121"] = 1.9, | |
["201200"] = 5.3, | |
["201201"] = 3.6, | |
["201210"] = 3.4, | |
["201211"] = 1.9, | |
["201220"] = 1.9, | |
["201221"] = 0.8, | |
["202001"] = 6.4, | |
["202011"] = 5.1, | |
["202021"] = 2, | |
["202101"] = 4.7, | |
["202111"] = 2.1, | |
["202121"] = 1.1, | |
["202201"] = 2.4, | |
["202211"] = 0.9, | |
["202221"] = 0.4, | |
["210000"] = 8.8, | |
["210001"] = 7.5, | |
["210010"] = 7.3, | |
["210011"] = 5.3, | |
["210020"] = 6, | |
["210021"] = 5, | |
["210100"] = 7.3, | |
["210101"] = 5.5, | |
["210110"] = 5.9, | |
["210111"] = 4, | |
["210120"] = 4.1, | |
["210121"] = 2, | |
["210200"] = 5.4, | |
["210201"] = 4.3, | |
["210210"] = 4.5, | |
["210211"] = 2.2, | |
["210220"] = 2, | |
["210221"] = 1.1, | |
["211000"] = 7.5, | |
["211001"] = 5.5, | |
["211010"] = 5.8, | |
["211011"] = 4.5, | |
["211020"] = 4, | |
["211021"] = 2.1, | |
["211100"] = 6.1, | |
["211101"] = 5.1, | |
["211110"] = 4.8, | |
["211111"] = 1.8, | |
["211120"] = 2, | |
["211121"] = 0.9, | |
["211200"] = 4.6, | |
["211201"] = 1.8, | |
["211210"] = 1.7, | |
["211211"] = 0.7, | |
["211220"] = 0.8, | |
["211221"] = 0.2, | |
["212001"] = 5.3, | |
["212011"] = 2.4, | |
["212021"] = 1.4, | |
["212101"] = 2.4, | |
["212111"] = 1.2, | |
["212121"] = 0.5, | |
["212201"] = 1, | |
["212211"] = 0.3, | |
["212221"] = 0.1 | |
} | |
local function round_away_from_zero(x) | |
return string.format("%.1f", math.floor(x * 10 + 0.5) / 10) | |
end | |
local function check_mandatory() | |
local missing = {} | |
for _, mandatory_metric in ipairs(METRICS_MANDATORY) do | |
if not metrics[mandatory_metric] then | |
table.insert(missing, mandatory_metric) | |
end | |
end | |
if #missing > 0 then | |
error('Missing mandatory metrics "' .. table.concat(missing, ", ") .. '"') | |
end | |
end | |
local function add_missing_optional() | |
original_metrics = {} | |
for k, v in pairs(metrics) do | |
original_metrics[k] = v | |
end | |
for _, abbreviation in ipairs({ | |
"MAV", "MAC", "MAT", "MPR", "MUI", "MVC", "MVI", "MVA", "MSC", "MSI", "MSA" | |
}) do | |
if not (metrics[abbreviation] or metrics[abbreviation] == "X") then | |
metrics[abbreviation] = metrics[string.sub(abbreviation, 2)] | |
end | |
end | |
for _, abbreviation in ipairs({ | |
"S", "AU", "R", "V", "RE", "U", "CR", "IR", "AR", "E" | |
}) do | |
if not metrics[abbreviation] then | |
metrics[abbreviation] = "X" | |
end | |
end | |
end | |
local function parse_vector() | |
if vector == "" then | |
error("Malformed CVSS4 vector, vector is empty") | |
end | |
if string.sub(vector, -1) == "/" then | |
error('Malformed CVSS4 vector, trailing "/"') | |
end | |
if not string.find(vector, "^CVSS:4.0/") then | |
error('Malformed CVSS4 vector "' .. vector .. '" is missing mandatory prefix or uses unsupported CVSS version') | |
end | |
local fields = {} | |
for field in string.gmatch(string.sub(vector, 10), "[^/]+") do | |
table.insert(fields, field) | |
end | |
for _, field in ipairs(fields) do | |
if field == "" then | |
error('Empty field in CVSS4 vector "' .. vector .. '"') | |
end | |
local metric, value = string.match(field, "([^:]+):([^:]+)") | |
if not metric or not value then | |
error('Malformed CVSS4 field "' .. field .. '"') | |
end | |
if metrics[metric] then | |
error('Duplicate metric "' .. metric .. '"') | |
end | |
if not METRICS_VALUE_NAMES[metric] or not METRICS_VALUE_NAMES[metric][value] then | |
error('Invalid metric in CVSS4 vector "' .. field .. '"') | |
end | |
metrics[metric] = value | |
end | |
end | |
local function get_eq_maxes(lookup, eq) | |
return MAX_COMPOSED["eq" .. tostring(eq)][string.sub(lookup, eq, eq)] | |
end | |
local function extract_value_metric(metric, string) | |
local startPos, endPos = string:find(metric .. ":[^/]+") | |
if startPos then | |
local extracted = string:sub(startPos, endPos) | |
local value = extracted:match(":[^/]+"):sub(2) | |
return value | |
end | |
return nil | |
end | |
local function m(metric) | |
local selected = metrics[metric] or "X" | |
if metric == "E" and selected == "X" then | |
return "A" | |
elseif (metric == "CR" or metric == "IR" or metric == "AR") and selected == "X" then | |
return "H" | |
elseif metrics["M" .. metric] then | |
local modified_selected = metrics["M" .. metric] | |
if modified_selected ~= "X" then | |
return modified_selected | |
end | |
end | |
return selected | |
end | |
local function fmacroVector() | |
local eq1, eq2, eq3, eq4, eq5, eq6 = "None", "None", "None", "None", "None", "None" | |
-- Logic for eq1 | |
if m("AV") == "N" and m("PR") == "N" and m("UI") == "N" then | |
eq1 = "0" | |
elseif (m("AV") == "N" or m("PR") == "N" or m("UI") == "N") and not (m("AV") == "N" and m("PR") == "N" and m("UI") == "N") and not (m("AV") == "P") then | |
eq1 = "1" | |
elseif m("AV") == "P" or not (m("AV") == "N" or m("PR") == "N" or m("UI") == "N") then | |
eq1 = "2" | |
end | |
-- Logic for eq2 | |
if m("AC") == "L" and m("AT") == "N" then | |
eq2 = "0" | |
elseif not (m("AC") == "L" and m("AT") == "N") then | |
eq2 = "1" | |
end | |
-- Logic for eq3 | |
if m("VC") == "H" and m("VI") == "H" then | |
eq3 = "0" | |
elseif not (m("VC") == "H" and m("VI") == "H") and (m("VC") == "H" or m("VI") == "H" or m("VA") == "H") then | |
eq3 = "1" | |
elseif not (m("VC") == "H" or m("VI") == "H" or m("VA") == "H") then | |
eq3 = "2" | |
end | |
-- Logic for eq4 | |
if m("MSI") == "S" or m("MSA") == "S" then | |
eq4 = "0" | |
elseif not (m("MSI") == "S" or m("MSA") == "S") and (m("SC") == "H" or m("SI") == "H" or m("SA") == "H") then | |
eq4 = "1" | |
elseif not (m("MSI") == "S" or m("MSA") == "S") and not (m("SC") == "H" or m("SI") == "H" or m("SA") == "H") then | |
eq4 = "2" | |
end | |
-- Logic for eq5 | |
if m("E") == "A" then | |
eq5 = "0" | |
elseif m("E") == "P" then | |
eq5 = "1" | |
elseif m("E") == "U" then | |
eq5 = "2" | |
end | |
-- Logic for eq6 | |
if (m("CR") == "H" and m("VC") == "H") or (m("IR") == "H" and m("VI") == "H") or (m("AR") == "H" and m("VA") == "H") then | |
eq6 = "0" | |
elseif not ((m("CR") == "H" and m("VC") == "H") or (m("IR") == "H" and m("VI") == "H") or (m("AR") == "H" and m("VA") == "H")) then | |
eq6 = "1" | |
end | |
return eq1 .. eq2 .. eq3 .. eq4 .. eq5 .. eq6 | |
end | |
local function compute_base_score() | |
local AV_levels = {N = 0.0, A = 0.1, L = 0.2, P = 0.3} | |
local PR_levels = {N = 0.0, L = 0.1, H = 0.2} | |
local UI_levels = {N = 0.0, P = 0.1, A = 0.2} | |
local AC_levels = {L = 0.0, H = 0.1} | |
local AT_levels = {N = 0.0, P = 0.1} | |
local VC_levels = {H = 0.0, L = 0.1, N = 0.2} | |
local VI_levels = {H = 0.0, L = 0.1, N = 0.2} | |
local VA_levels = {H = 0.0, L = 0.1, N = 0.2} | |
local SC_levels = {H = 0.1, L = 0.2, N = 0.3} | |
local SI_levels = {S = 0.0, H = 0.1, L = 0.2, N = 0.3} | |
local SA_levels = {S = 0.0, H = 0.1, L = 0.2, N = 0.3} | |
local CR_levels = {H = 0.0, M = 0.1, L = 0.2} | |
local IR_levels = {H = 0.0, M = 0.1, L = 0.2} | |
local AR_levels = {H = 0.0, M = 0.1, L = 0.2} | |
-- Reconstruct the macro vector logic to Lua equivalent | |
local macroVector = fmacroVector() | |
-- Initial base score logic, replaced Python's all() with Lua equivalent | |
if m("VC") == "N" and m("VI") == "N" and m("VA") == "N" and m("SC") == "N" and m("SI") == "N" and m("SA") == "N" then | |
base_score = "0.0" | |
return | |
end | |
local value = CVSS_LOOKUP_GLOBAL[macroVector] | |
-- Extract and convert individual values from the macroVector | |
local eq1_val = tonumber(string.sub(macroVector, 1, 1)) | |
local eq2_val = tonumber(string.sub(macroVector, 2, 2)) | |
local eq3_val = tonumber(string.sub(macroVector, 3, 3)) | |
local eq4_val = tonumber(string.sub(macroVector, 4, 4)) | |
local eq5_val = tonumber(string.sub(macroVector, 5, 5)) | |
local eq6_val = tonumber(string.sub(macroVector, 6, 6)) | |
local function tableToString(tbl) | |
local str = "" | |
for _, val in ipairs(tbl) do | |
str = str .. tostring(val) | |
end | |
return str | |
end | |
local eq1_next_lower_macro = tableToString({eq1_val + 1, eq2_val, eq3_val, eq4_val, eq5_val, eq6_val}) | |
local eq2_next_lower_macro = tableToString({eq1_val, eq2_val + 1, eq3_val, eq4_val, eq5_val, eq6_val}) | |
local eq3eq6_next_lower_macro | |
local eq3eq6_next_lower_macro_left | |
local eq3eq6_next_lower_macro_right | |
if eq3_val == 1 and eq6_val == 1 then | |
eq3eq6_next_lower_macro = tableToString({eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val}) | |
elseif eq3_val == 0 and eq6_val == 1 then | |
eq3eq6_next_lower_macro = tableToString({eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val}) | |
elseif eq3_val == 1 and eq6_val == 0 then | |
eq3eq6_next_lower_macro = tableToString({eq1_val, eq2_val, eq3_val, eq4_val, eq5_val, eq6_val + 1}) | |
elseif eq3_val == 0 and eq6_val == 0 then | |
eq3eq6_next_lower_macro_left = tableToString({eq1_val, eq2_val, eq3_val, eq4_val, eq5_val, eq6_val + 1}) | |
eq3eq6_next_lower_macro_right = tableToString({eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val}) | |
else | |
eq3eq6_next_lower_macro = tableToString({eq1_val, eq2_val, eq3_val + 1, eq4_val, eq5_val, eq6_val + 1}) | |
end | |
local eq4_next_lower_macro = tableToString({eq1_val, eq2_val, eq3_val, eq4_val + 1, eq5_val, eq6_val}) | |
local eq5_next_lower_macro = tableToString({eq1_val, eq2_val, eq3_val, eq4_val, eq5_val + 1, eq6_val}) | |
local function getScore(key) | |
local score = CVSS_LOOKUP_GLOBAL[key] | |
if score == nil then | |
return 0/0 -- Generates NaN in Lua | |
else | |
return score | |
end | |
end | |
local score_eq1_next_lower_macro = getScore(eq1_next_lower_macro) | |
local score_eq2_next_lower_macro = getScore(eq2_next_lower_macro) | |
local score_eq3eq6_next_lower_macro | |
if eq3_val == 0 and eq6_val == 0 then | |
local score_eq3eq6_next_lower_macro_left = getScore(eq3eq6_next_lower_macro_left) | |
local score_eq3eq6_next_lower_macro_right = getScore(eq3eq6_next_lower_macro_right) | |
-- Use math.max to find the maximum; NaN handling might be needed if scores are NaN | |
score_eq3eq6_next_lower_macro = math.max(score_eq3eq6_next_lower_macro_left, score_eq3eq6_next_lower_macro_right) | |
else | |
score_eq3eq6_next_lower_macro = getScore(eq3eq6_next_lower_macro) | |
end | |
local score_eq4_next_lower_macro = getScore(eq4_next_lower_macro) | |
local score_eq5_next_lower_macro = getScore(eq5_next_lower_macro) | |
local eq1_maxes = get_eq_maxes(macroVector, 1) | |
local eq2_maxes = get_eq_maxes(macroVector, 2) | |
local eq3_eq6_maxes = get_eq_maxes(macroVector, 3)[string.sub(macroVector, 6, 6)] -- lua indices start at 1 | |
local eq4_maxes = get_eq_maxes(macroVector, 4) | |
local eq5_maxes = get_eq_maxes(macroVector, 5) | |
local max_vectors = {} | |
for _, eq1_max in ipairs(eq1_maxes) do | |
for _, eq2_max in ipairs(eq2_maxes) do | |
for _, eq3_eq6_max in ipairs(eq3_eq6_maxes) do | |
for _, eq4_max in ipairs(eq4_maxes) do | |
for _, eq5max in ipairs(eq5_maxes) do | |
table.insert(max_vectors, eq1_max .. eq2_max .. eq3_eq6_max .. eq4_max .. eq5max) | |
end | |
end | |
end | |
end | |
end | |
local severity_distance_AV = 0 | |
local severity_distance_PR = 0 | |
local severity_distance_UI = 0 | |
local severity_distance_AC = 0 | |
local severity_distance_AT = 0 | |
local severity_distance_VC = 0 | |
local severity_distance_VI = 0 | |
local severity_distance_VA = 0 | |
local severity_distance_SC = 0 | |
local severity_distance_SI = 0 | |
local severity_distance_SA = 0 | |
local severity_distance_CR = 0 | |
local severity_distance_IR = 0 | |
local severity_distance_AR = 0 | |
for _, max_vector in ipairs(max_vectors) do | |
severity_distance_AV = AV_levels[m("AV")] - AV_levels[extract_value_metric("AV", max_vector)] | |
severity_distance_PR = PR_levels[m("PR")] - PR_levels[extract_value_metric("PR", max_vector)] | |
severity_distance_UI = UI_levels[m("UI")] - UI_levels[extract_value_metric("UI", max_vector)] | |
severity_distance_AC = AC_levels[m("AC")] - AC_levels[extract_value_metric("AC", max_vector)] | |
severity_distance_AT = AT_levels[m("AT")] - AT_levels[extract_value_metric("AT", max_vector)] | |
severity_distance_VC = VC_levels[m("VC")] - VC_levels[extract_value_metric("VC", max_vector)] | |
severity_distance_VI = VI_levels[m("VI")] - VI_levels[extract_value_metric("VI", max_vector)] | |
severity_distance_VA = VA_levels[m("VA")] - VA_levels[extract_value_metric("VA", max_vector)] | |
severity_distance_SC = SC_levels[m("SC")] - SC_levels[extract_value_metric("SC", max_vector)] | |
severity_distance_SI = SI_levels[m("SI")] - SI_levels[extract_value_metric("SI", max_vector)] | |
severity_distance_SA = SA_levels[m("SA")] - SA_levels[extract_value_metric("SA", max_vector)] | |
severity_distance_CR = CR_levels[m("CR")] - CR_levels[extract_value_metric("CR", max_vector)] | |
severity_distance_IR = IR_levels[m("IR")] - IR_levels[extract_value_metric("IR", max_vector)] | |
severity_distance_AR = AR_levels[m("AR")] - AR_levels[extract_value_metric("AR", max_vector)] | |
-- List of all severity distance variables | |
local severity_distances = { | |
severity_distance_AV, | |
severity_distance_PR, | |
severity_distance_UI, | |
severity_distance_AC, | |
severity_distance_AT, | |
severity_distance_VC, | |
severity_distance_VI, | |
severity_distance_VA, | |
severity_distance_SC, | |
severity_distance_SI, | |
severity_distance_SA, | |
severity_distance_CR, | |
severity_distance_IR, | |
severity_distance_AR, | |
} | |
-- Define a flag to indicate whether to break the loop | |
local shouldBreak = true | |
-- Check if any severity distance is less than 0 | |
for _, distance in ipairs(severity_distances) do | |
if distance < 0 then | |
shouldBreak = false | |
break -- Exit the loop early if a negative distance is found | |
end | |
end | |
-- Break out of the outer loop if shouldBreak is still true (no negative distances found) | |
if shouldBreak then | |
break | |
end | |
end | |
local current_severity_distance_eq1 = severity_distance_AV + severity_distance_PR + severity_distance_UI | |
local current_severity_distance_eq2 = severity_distance_AC + severity_distance_AT | |
local current_severity_distance_eq3eq6 = severity_distance_VC + severity_distance_VI + severity_distance_VA + severity_distance_CR + severity_distance_IR + severity_distance_AR | |
local current_severity_distance_eq4 = severity_distance_SC + severity_distance_SI + severity_distance_SA | |
-- current_severity_distance_eq5 is implicitly 0, so it's not needed unless used later | |
local step = 0.1 | |
local available_distance_eq1 = value - score_eq1_next_lower_macro | |
local available_distance_eq2 = value - score_eq2_next_lower_macro | |
local available_distance_eq3eq6 = value - score_eq3eq6_next_lower_macro | |
local available_distance_eq4 = value - score_eq4_next_lower_macro | |
local available_distance_eq5 = value - score_eq5_next_lower_macro | |
local percent_to_next_eq1_severity = 0 | |
local percent_to_next_eq2_severity = 0 | |
local percent_to_next_eq3eq6_severity = 0 | |
local percent_to_next_eq4_severity = 0 | |
local percent_to_next_eq5_severity = 0 | |
local n_existing_lower = 0 | |
local normalized_severity_eq1 = 0 | |
local normalized_severity_eq2 = 0 | |
local normalized_severity_eq3eq6 = 0 | |
local normalized_severity_eq4 = 0 | |
local normalized_severity_eq5 = 0 | |
local max_severity_eq1 = MAX_SEVERITY["eq1"][tostring(eq1_val)] * step | |
local max_severity_eq2 = MAX_SEVERITY["eq2"][tostring(eq2_val)] * step | |
local max_severity_eq3eq6 = MAX_SEVERITY["eq3eq6"][tostring(eq3_val)][tostring(eq6_val)] * step | |
local max_severity_eq4 = MAX_SEVERITY["eq4"][tostring(eq4_val)] * step | |
if (type(available_distance_eq1) == "number" or type(available_distance_eq1) == "integer") and available_distance_eq1 >= 0 then | |
n_existing_lower = n_existing_lower + 1 | |
percent_to_next_eq1_severity = current_severity_distance_eq1 / max_severity_eq1 | |
normalized_severity_eq1 = available_distance_eq1 * percent_to_next_eq1_severity | |
end | |
if (type(available_distance_eq2) == "number" or type(available_distance_eq2) == "integer") and available_distance_eq2 >= 0 then | |
n_existing_lower = n_existing_lower + 1 | |
percent_to_next_eq2_severity = current_severity_distance_eq2 / max_severity_eq2 | |
normalized_severity_eq2 = available_distance_eq2 * percent_to_next_eq2_severity | |
end | |
if (type(available_distance_eq3eq6) == "number" or type(available_distance_eq3eq6) == "integer") and available_distance_eq3eq6 >= 0 then | |
n_existing_lower = n_existing_lower + 1 | |
percent_to_next_eq3eq6_severity = current_severity_distance_eq3eq6 / max_severity_eq3eq6 | |
normalized_severity_eq3eq6 = available_distance_eq3eq6 * percent_to_next_eq3eq6_severity | |
end | |
if (type(available_distance_eq4) == "number" or type(available_distance_eq4) == "integer") and available_distance_eq4 >= 0 then | |
n_existing_lower = n_existing_lower + 1 | |
percent_to_next_eq4_severity = current_severity_distance_eq4 / max_severity_eq4 | |
normalized_severity_eq4 = available_distance_eq4 * percent_to_next_eq4_severity | |
end | |
if (type(available_distance_eq5) == "number" or type(available_distance_eq5) == "integer") and available_distance_eq5 >= 0 then | |
n_existing_lower = n_existing_lower + 1 | |
percent_to_next_eq5_severity = 0 | |
normalized_severity_eq5 = available_distance_eq5 * percent_to_next_eq5_severity | |
end | |
local mean_distance = 0 | |
if n_existing_lower == 0 then | |
mean_distance = 0 | |
else | |
mean_distance = (normalized_severity_eq1 + normalized_severity_eq2 + normalized_severity_eq3eq6 + normalized_severity_eq4 + normalized_severity_eq5) / n_existing_lower | |
end | |
value = value - mean_distance | |
value = math.max(0.0, value) -- Ensure value is not less than 0.0 | |
value = math.min(10.0, value) -- Ensure value is not greater than 10.0 | |
base_score = round_away_from_zero(value) | |
end | |
-- Clean vector | |
local function clean_vector(output_prefix) | |
output_prefix = output_prefix or true | |
local vector_parts = {} | |
for metric, _ in pairs(METRICS_ABBREVIATIONS) do | |
if original_metrics[metric] and original_metrics[metric] ~= "X" then | |
table.insert(vector_parts, metric .. ":" .. original_metrics[metric]) | |
end | |
end | |
local prefix = output_prefix and "CVSS:4.0/" or "" | |
return prefix .. table.concat(vector_parts, "/") | |
end | |
local function get_value_description(abbreviation) | |
local string_value = metrics[abbreviation] or "X" | |
return METRICS_VALUE_NAMES[abbreviation][string_value] | |
end | |
local function compute_severity() | |
local base_score = tonumber(base_score) | |
if base_score == 0.0 then | |
severity = "None" | |
elseif base_score <= 3.9 then | |
severity = "Low" | |
elseif base_score <= 6.9 then | |
severity = "Medium" | |
elseif base_score <= 8.9 then | |
severity = "High" | |
else | |
severity = "Critical" | |
end | |
end | |
local function compute_cvssv4() | |
parse_vector() | |
check_mandatory() | |
add_missing_optional() | |
compute_base_score() | |
compute_severity() | |
end | |
compute_cvssv4() | |
-- print(clean_vector()) | |
-- print(severity) | |
print(base_score) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment