Skip to content

Instantly share code, notes, and snippets.

@AdrianVollmer
Last active February 8, 2024 08:30
Show Gist options
  • Save AdrianVollmer/a00bc9c06aa007c0bf54c7375130b63e to your computer and use it in GitHub Desktop.
Save AdrianVollmer/a00bc9c06aa007c0bf54c7375130b63e to your computer and use it in GitHub Desktop.
CVSSv4 Lua implementation
#!/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