Packages

CVSS (Common Vulnerability Scoring System) library for Erlang

Current section

Files

Jump to
cvss src cvss_v4.erl
Raw

src/cvss_v4.erl

%% SPDX-License-Identifier: Apache-2.0
%% SPDX-FileCopyrightText: 2026 Erlang Ecosystem Foundation
-module(cvss_v4).
-moduledoc """
CVSS 4.0 parsing, composition, validation, and scoring.
Use this module when working with CVSS 4.0 vectors directly. If the
version is not known ahead of time, use `m:cvss` instead.
Vector format: `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H`
See: https://www.first.org/cvss/v4.0/specification-document
""".
-include("cvss_v4.hrl").
-export([
base_score/1, compose/1, environmental_score/1, parse/1, score/1, threat_score/1, valid/1
]).
-export_type([
cvss/0,
av/0,
ac/0,
at/0,
pr/0,
ui/0,
cia/0,
si_sa/0,
exploit_maturity/0,
requirement/0,
safety/0,
automatable/0,
recovery/0,
value_density/0,
response_effort/0,
urgency/0
]).
-type av() :: network | adjacent | local | physical.
-type ac() :: low | high.
-type at() :: none | present.
-type pr() :: none | low | high.
-type ui() :: none | passive | active.
-type cia() :: none | low | high.
-type si_sa() :: none | low | high | safety.
-type exploit_maturity() :: attacked | poc | unreported | not_defined.
-type requirement() :: low | medium | high | not_defined.
-type safety() :: negligible | present.
-type automatable() :: no | yes.
-type recovery() :: automatic | user | irrecoverable.
-type value_density() :: diffuse | concentrated.
-type response_effort() :: low | moderate | high.
-type urgency() :: clear | green | amber | red.
-type cvss() :: #cvss_v4{}.
%% Internal record for effective metric values
-record(eff, {
av,
ac,
at,
pr,
ui,
vc,
vi,
va,
sc,
si,
sa,
cr,
ir,
ar,
e
}).
%%====================================================================
%% Public API
%%====================================================================
-doc """
Parse a CVSS 4.0 vector string.
```erlang
> cvss_v4:parse(<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H">>).
{ok, #cvss_v4{av = network, ac = low, at = none, pr = none, ui = none,
vc = high, vi = high, va = high, sc = high, si = high, sa = high}}
> cvss_v4:parse(<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:A">>).
{ok, #cvss_v4{av = network, ac = low, at = none, pr = none, ui = none,
vc = high, vi = high, va = high, sc = high, si = high, sa = high,
e = attacked}}
> cvss_v4:parse(<<"CVSS:4.0/AV:P/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:S/SA:S">>).
{ok, #cvss_v4{av = physical, ac = low, at = none, pr = none, ui = none,
vc = high, vi = high, va = high, sc = high, si = safety, sa = safety}}
> cvss_v4:parse(<<"CVSS:4.0/AV:N">>).
{error, {missing_required_metric, ac}}
```
""".
-spec parse(binary()) -> {ok, cvss_v4:cvss()} | {error, cvss:parse_error()}.
parse(Vector) when is_binary(Vector) ->
maybe
{ok, Rest} ?= parse_prefix(Vector),
{ok, Metrics} ?= parse_metrics(Rest),
build_record(Metrics)
end.
-doc """
Compose a CVSS 4.0 record into a vector string.
```erlang
> iolist_to_binary(cvss_v4:compose(#cvss_v4{av = network, ac = low, at = none,
pr = none, ui = none,
vc = high, vi = high, va = high,
sc = high, si = high, sa = high})).
<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H">>
```
""".
-spec compose(cvss_v4:cvss()) -> iolist().
compose(#cvss_v4{} = Cvss) ->
Prefix = "CVSS:4.0/",
Base = [
"AV:",
av_to_string(Cvss#cvss_v4.av),
"/AC:",
ac_to_string(Cvss#cvss_v4.ac),
"/AT:",
at_to_string(Cvss#cvss_v4.at),
"/PR:",
pr_to_string(Cvss#cvss_v4.pr),
"/UI:",
ui_to_string(Cvss#cvss_v4.ui),
"/VC:",
cia_to_string(Cvss#cvss_v4.vc),
"/VI:",
cia_to_string(Cvss#cvss_v4.vi),
"/VA:",
cia_to_string(Cvss#cvss_v4.va),
"/SC:",
cia_to_string(Cvss#cvss_v4.sc),
"/SI:",
si_sa_to_string(Cvss#cvss_v4.si),
"/SA:",
si_sa_to_string(Cvss#cvss_v4.sa)
],
Optional = cvss_common:encode_optional(
[
{"E", Cvss#cvss_v4.e, fun e_to_string/1},
{"CR", Cvss#cvss_v4.cr, fun req_to_string/1},
{"IR", Cvss#cvss_v4.ir, fun req_to_string/1},
{"AR", Cvss#cvss_v4.ar, fun req_to_string/1},
{"MAV", Cvss#cvss_v4.mav, fun av_to_string/1},
{"MAC", Cvss#cvss_v4.mac, fun ac_to_string/1},
{"MAT", Cvss#cvss_v4.mat, fun at_to_string/1},
{"MPR", Cvss#cvss_v4.mpr, fun pr_to_string/1},
{"MUI", Cvss#cvss_v4.mui, fun ui_to_string/1},
{"MVC", Cvss#cvss_v4.mvc, fun cia_to_string/1},
{"MVI", Cvss#cvss_v4.mvi, fun cia_to_string/1},
{"MVA", Cvss#cvss_v4.mva, fun cia_to_string/1},
{"MSC", Cvss#cvss_v4.msc, fun cia_to_string/1},
{"MSI", Cvss#cvss_v4.msi, fun si_sa_to_string/1},
{"MSA", Cvss#cvss_v4.msa, fun si_sa_to_string/1},
{"S", Cvss#cvss_v4.safety, fun safety_to_string/1},
{"AU", Cvss#cvss_v4.automatable, fun automatable_to_string/1},
{"R", Cvss#cvss_v4.recovery, fun recovery_to_string/1},
{"V", Cvss#cvss_v4.value_density, fun value_density_to_string/1},
{"RE", Cvss#cvss_v4.response_effort, fun response_effort_to_string/1},
{"U", Cvss#cvss_v4.urgency, fun urgency_to_string/1}
],
[undefined]
),
[Prefix, Base, Optional].
-doc """
Check whether a CVSS 4.0 value is valid.
Accepts either a vector string or a parsed record.
```erlang
> cvss_v4:valid(<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H">>).
true
> cvss_v4:valid(#cvss_v4{av = network, ac = low, at = none, pr = none, ui = none,
vc = high, vi = high, va = high,
sc = high, si = high, sa = high}).
true
> cvss_v4:valid(<<"CVSS:4.0/AV:X">>).
false
```
""".
-spec valid(iodata() | cvss_v4:cvss()) -> boolean().
valid(#cvss_v4{} = Cvss) ->
ValidBase =
lists:member(Cvss#cvss_v4.av, [network, adjacent, local, physical]) andalso
lists:member(Cvss#cvss_v4.ac, [low, high]) andalso
lists:member(Cvss#cvss_v4.at, [none, present]) andalso
lists:member(Cvss#cvss_v4.pr, [none, low, high]) andalso
lists:member(Cvss#cvss_v4.ui, [none, passive, active]) andalso
lists:member(Cvss#cvss_v4.vc, [none, low, high]) andalso
lists:member(Cvss#cvss_v4.vi, [none, low, high]) andalso
lists:member(Cvss#cvss_v4.va, [none, low, high]) andalso
lists:member(Cvss#cvss_v4.sc, [none, low, high]) andalso
lists:member(Cvss#cvss_v4.si, [none, low, high, safety]) andalso
lists:member(Cvss#cvss_v4.sa, [none, low, high, safety]),
ValidThreat =
cvss_common:valid_optional(Cvss#cvss_v4.e, [attacked, poc, unreported, not_defined]),
ValidEnvironmental =
cvss_common:valid_optional(Cvss#cvss_v4.cr, [low, medium, high, not_defined]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.ir, [low, medium, high, not_defined]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.ar, [low, medium, high, not_defined]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.mav, [network, adjacent, local, physical]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.mac, [low, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.mat, [none, present]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.mpr, [none, low, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.mui, [none, passive, active]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.mvc, [none, low, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.mvi, [none, low, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.mva, [none, low, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.msc, [none, low, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.msi, [none, low, high, safety]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.msa, [none, low, high, safety]),
ValidSupplemental =
cvss_common:valid_optional(Cvss#cvss_v4.safety, [negligible, present]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.automatable, [no, yes]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.recovery, [automatic, user, irrecoverable]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.value_density, [diffuse, concentrated]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.response_effort, [low, moderate, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v4.urgency, [clear, green, amber, red]),
ValidBase andalso ValidThreat andalso ValidEnvironmental andalso ValidSupplemental;
valid(Vector) ->
case parse(iolist_to_binary(Vector)) of
{ok, Cvss} -> valid(Cvss);
{error, _} -> false
end.
-doc """
Calculate the CVSS 4.0 score (CVSS-BTE).
Uses all present metrics (Base + Threat + Environmental).
Metrics not explicitly set default per the CVSS 4.0 specification.
```erlang
> {ok, Cvss} = cvss_v4:parse(<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H">>).
> cvss_v4:score(Cvss).
10.0
> {ok, Cvss2} = cvss_v4:parse(<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N">>).
> cvss_v4:score(Cvss2).
0.0
```
""".
-spec score(cvss_v4:cvss()) -> cvss:score().
score(#cvss_v4{} = Cvss) ->
calculate_with_effective(effective_values(Cvss)).
-doc """
Calculate the CVSS 4.0 Base Score (CVSS-B).
Only considers base metrics; threat and environmental metrics are ignored.
```erlang
> {ok, Cvss} = cvss_v4:parse(<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H">>).
> cvss_v4:base_score(Cvss).
10.0
```
""".
-spec base_score(cvss_v4:cvss()) -> cvss:score().
base_score(#cvss_v4{} = Cvss) ->
calculate_with_effective(
effective_values(
Cvss#cvss_v4{
e = undefined,
cr = undefined,
ir = undefined,
ar = undefined,
mav = undefined,
mac = undefined,
mat = undefined,
mpr = undefined,
mui = undefined,
mvc = undefined,
mvi = undefined,
mva = undefined,
msc = undefined,
msi = undefined,
msa = undefined
}
)
).
-doc """
Calculate the CVSS 4.0 Threat Score (CVSS-BT).
Considers base and threat metrics; environmental metrics are ignored.
```erlang
> {ok, Cvss} = cvss_v4:parse(<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:A">>).
> cvss_v4:threat_score(Cvss).
10.0
```
""".
-spec threat_score(cvss_v4:cvss()) -> cvss:score().
threat_score(#cvss_v4{} = Cvss) ->
calculate_with_effective(
effective_values(
Cvss#cvss_v4{
cr = undefined,
ir = undefined,
ar = undefined,
mav = undefined,
mac = undefined,
mat = undefined,
mpr = undefined,
mui = undefined,
mvc = undefined,
mvi = undefined,
mva = undefined,
msc = undefined,
msi = undefined,
msa = undefined
}
)
).
-doc """
Calculate the CVSS 4.0 Environmental Score (CVSS-BE).
Considers base and environmental metrics; threat metrics are ignored.
```erlang
> {ok, Cvss} = cvss_v4:parse(<<"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/MVC:N/MVI:N/MVA:N/MSC:N/MSI:N/MSA:N">>).
> cvss_v4:environmental_score(Cvss).
0.0
```
""".
-spec environmental_score(cvss_v4:cvss()) -> cvss:score().
environmental_score(#cvss_v4{} = Cvss) ->
calculate_with_effective(
effective_values(
Cvss#cvss_v4{e = undefined}
)
).
calculate_with_effective(#eff{vc = VC, vi = VI, va = VA, sc = SC, si = SI, sa = SA} = Eff) ->
case {VC, VI, VA, SC, SI, SA} of
{none, none, none, none, none, none} -> 0.0;
_ -> cvss_common:to_float(calculate_score_with_eff(Eff))
end.
effective_values(#cvss_v4{} = C) ->
#eff{
av = effective(C#cvss_v4.mav, C#cvss_v4.av),
ac = effective(C#cvss_v4.mac, C#cvss_v4.ac),
at = effective(C#cvss_v4.mat, C#cvss_v4.at),
pr = effective(C#cvss_v4.mpr, C#cvss_v4.pr),
ui = effective(C#cvss_v4.mui, C#cvss_v4.ui),
vc = effective(C#cvss_v4.mvc, C#cvss_v4.vc),
vi = effective(C#cvss_v4.mvi, C#cvss_v4.vi),
va = effective(C#cvss_v4.mva, C#cvss_v4.va),
sc = effective(C#cvss_v4.msc, C#cvss_v4.sc),
si = effective_si_sa(C#cvss_v4.msi, C#cvss_v4.si),
sa = effective_si_sa(C#cvss_v4.msa, C#cvss_v4.sa),
cr = effective_req(C#cvss_v4.cr),
ir = effective_req(C#cvss_v4.ir),
ar = effective_req(C#cvss_v4.ar),
e = effective_e(C#cvss_v4.e)
}.
-include("internal/decimal.hrl").
calculate_score_with_eff(#eff{} = E) ->
EQ1 = calculate_eq1(E#eff.av, E#eff.pr, E#eff.ui),
EQ2 = calculate_eq2(E#eff.ac, E#eff.at),
EQ3 = calculate_eq3(E#eff.vc, E#eff.vi, E#eff.va),
EQ4 = calculate_eq4(E#eff.sc, E#eff.si, E#eff.sa),
EQ5 = calculate_eq5(E#eff.e),
EQ6 = calculate_eq6(E#eff.cr, E#eff.ir, E#eff.ar, E#eff.vc, E#eff.vi, E#eff.va),
case cvss_v4_lookup:lookup(EQ1, EQ2, EQ3, EQ4, EQ5, EQ6) of
undefined -> ?ZERO;
BaseScore -> interpolate_score(BaseScore, E, EQ1, EQ2, EQ3, EQ4, EQ5, EQ6)
end.
%%====================================================================
%% Internal: Parsing
%%====================================================================
-spec parse_prefix(binary()) -> {ok, binary()} | {error, cvss:parse_error()}.
parse_prefix(<<"CVSS:4.0/", Rest/binary>>) -> {ok, Rest};
parse_prefix(Vector) -> {error, {invalid_prefix, Vector}}.
-spec parse_metrics(binary()) -> {ok, [{binary(), binary()}]} | {error, cvss:parse_error()}.
parse_metrics(Vector) ->
Parts = binary:split(Vector, <<"/">>, [global]),
parse_metric_parts(Parts, #{}, []).
-spec parse_metric_parts([binary()], #{binary() => true}, [{binary(), binary()}]) ->
{ok, [{binary(), binary()}]} | {error, cvss:parse_error()}.
parse_metric_parts([], _Seen, Acc) ->
{ok, lists:reverse(Acc)};
parse_metric_parts([Part | Rest], Seen, Acc) ->
case binary:split(Part, <<":">>) of
[Key, Value] ->
case maps:is_key(Key, Seen) of
true -> {error, {duplicate_metric, Key}};
false -> parse_metric_parts(Rest, Seen#{Key => true}, [{Key, Value} | Acc])
end;
_ ->
{error, malformed_vector}
end.
-spec build_record([{binary(), binary()}]) -> {ok, cvss_v4:cvss()} | {error, cvss:parse_error()}.
build_record(Metrics) ->
case apply_metrics(Metrics, #{}) of
{ok, Map} -> validate_required(Map);
{error, _} = Error -> Error
end.
apply_metrics([], Map) ->
{ok, Map};
apply_metrics([{Key, Value} | Rest], Map) ->
case apply_metric(Key, Value) of
{ok, {K, V}} -> apply_metrics(Rest, Map#{K => V});
{error, _} = Error -> Error
end.
-spec apply_metric(binary(), binary()) -> {ok, {atom(), term()}} | {error, cvss:parse_error()}.
apply_metric(<<"AV">>, V) -> parse_into(av, V, fun parse_av/1);
apply_metric(<<"AC">>, V) -> parse_into(ac, V, fun parse_ac/1);
apply_metric(<<"AT">>, V) -> parse_into(at, V, fun parse_at/1);
apply_metric(<<"PR">>, V) -> parse_into(pr, V, fun parse_pr/1);
apply_metric(<<"UI">>, V) -> parse_into(ui, V, fun parse_ui/1);
apply_metric(<<"VC">>, V) -> parse_into(vc, V, fun parse_cia/1);
apply_metric(<<"VI">>, V) -> parse_into(vi, V, fun parse_cia/1);
apply_metric(<<"VA">>, V) -> parse_into(va, V, fun parse_cia/1);
apply_metric(<<"SC">>, V) -> parse_into(sc, V, fun parse_cia/1);
apply_metric(<<"SI">>, V) -> parse_into(si, V, fun parse_si_sa/1);
apply_metric(<<"SA">>, V) -> parse_into(sa, V, fun parse_si_sa/1);
apply_metric(<<"E">>, V) -> parse_into(e, V, fun parse_e/1);
apply_metric(<<"CR">>, V) -> parse_into(cr, V, fun parse_req/1);
apply_metric(<<"IR">>, V) -> parse_into(ir, V, fun parse_req/1);
apply_metric(<<"AR">>, V) -> parse_into(ar, V, fun parse_req/1);
apply_metric(<<"MAV">>, V) -> parse_into(mav, V, fun parse_av_x/1);
apply_metric(<<"MAC">>, V) -> parse_into(mac, V, fun parse_ac_x/1);
apply_metric(<<"MAT">>, V) -> parse_into(mat, V, fun parse_at_x/1);
apply_metric(<<"MPR">>, V) -> parse_into(mpr, V, fun parse_pr_x/1);
apply_metric(<<"MUI">>, V) -> parse_into(mui, V, fun parse_ui_x/1);
apply_metric(<<"MVC">>, V) -> parse_into(mvc, V, fun parse_cia_x/1);
apply_metric(<<"MVI">>, V) -> parse_into(mvi, V, fun parse_cia_x/1);
apply_metric(<<"MVA">>, V) -> parse_into(mva, V, fun parse_cia_x/1);
apply_metric(<<"MSC">>, V) -> parse_into(msc, V, fun parse_cia_x/1);
apply_metric(<<"MSI">>, V) -> parse_into(msi, V, fun parse_si_sa_x/1);
apply_metric(<<"MSA">>, V) -> parse_into(msa, V, fun parse_si_sa_x/1);
apply_metric(<<"S">>, V) -> parse_into(safety, V, fun parse_safety/1);
apply_metric(<<"AU">>, V) -> parse_into(automatable, V, fun parse_automatable/1);
apply_metric(<<"R">>, V) -> parse_into(recovery, V, fun parse_recovery/1);
apply_metric(<<"V">>, V) -> parse_into(value_density, V, fun parse_value_density/1);
apply_metric(<<"RE">>, V) -> parse_into(response_effort, V, fun parse_response_effort/1);
apply_metric(<<"U">>, V) -> parse_into(urgency, V, fun parse_urgency/1);
apply_metric(Key, Value) -> {error, {invalid_metric, Key, Value}}.
parse_into(Key, Value, Parser) ->
case Parser(Value) of
{ok, Parsed} -> {ok, {Key, Parsed}};
{error, _} = Error -> Error
end.
-spec validate_required(map()) -> {ok, cvss_v4:cvss()} | {error, cvss:parse_error()}.
validate_required(M) ->
RequiredKeys = [av, ac, at, pr, ui, vc, vi, va, sc, si, sa],
case cvss_common:check_required(RequiredKeys, M) of
{error, _} = Error ->
Error;
ok ->
{ok, #cvss_v4{
av = maps:get(av, M),
ac = maps:get(ac, M),
at = maps:get(at, M),
pr = maps:get(pr, M),
ui = maps:get(ui, M),
vc = maps:get(vc, M),
vi = maps:get(vi, M),
va = maps:get(va, M),
sc = maps:get(sc, M),
si = maps:get(si, M),
sa = maps:get(sa, M),
e = maps:get(e, M, undefined),
cr = maps:get(cr, M, undefined),
ir = maps:get(ir, M, undefined),
ar = maps:get(ar, M, undefined),
mav = maps:get(mav, M, undefined),
mac = maps:get(mac, M, undefined),
mat = maps:get(mat, M, undefined),
mpr = maps:get(mpr, M, undefined),
mui = maps:get(mui, M, undefined),
mvc = maps:get(mvc, M, undefined),
mvi = maps:get(mvi, M, undefined),
mva = maps:get(mva, M, undefined),
msc = maps:get(msc, M, undefined),
msi = maps:get(msi, M, undefined),
msa = maps:get(msa, M, undefined),
safety = maps:get(safety, M, undefined),
automatable = maps:get(automatable, M, undefined),
recovery = maps:get(recovery, M, undefined),
value_density = maps:get(value_density, M, undefined),
response_effort = maps:get(response_effort, M, undefined),
urgency = maps:get(urgency, M, undefined)
}}
end.
%%====================================================================
%% Internal: Metric Parsing
%%====================================================================
parse_av(<<"N">>) -> {ok, network};
parse_av(<<"A">>) -> {ok, adjacent};
parse_av(<<"L">>) -> {ok, local};
parse_av(<<"P">>) -> {ok, physical};
parse_av(V) -> {error, {invalid_metric, <<"AV">>, V}}.
parse_ac(<<"L">>) -> {ok, low};
parse_ac(<<"H">>) -> {ok, high};
parse_ac(V) -> {error, {invalid_metric, <<"AC">>, V}}.
parse_at(<<"N">>) -> {ok, none};
parse_at(<<"P">>) -> {ok, present};
parse_at(V) -> {error, {invalid_metric, <<"AT">>, V}}.
parse_pr(<<"N">>) -> {ok, none};
parse_pr(<<"L">>) -> {ok, low};
parse_pr(<<"H">>) -> {ok, high};
parse_pr(V) -> {error, {invalid_metric, <<"PR">>, V}}.
parse_ui(<<"N">>) -> {ok, none};
parse_ui(<<"P">>) -> {ok, passive};
parse_ui(<<"A">>) -> {ok, active};
parse_ui(V) -> {error, {invalid_metric, <<"UI">>, V}}.
parse_cia(<<"N">>) -> {ok, none};
parse_cia(<<"L">>) -> {ok, low};
parse_cia(<<"H">>) -> {ok, high};
parse_cia(V) -> {error, {invalid_metric, <<"VC/VI/VA/SC">>, V}}.
parse_si_sa(<<"N">>) -> {ok, none};
parse_si_sa(<<"L">>) -> {ok, low};
parse_si_sa(<<"H">>) -> {ok, high};
parse_si_sa(<<"S">>) -> {ok, safety};
parse_si_sa(V) -> {error, {invalid_metric, <<"SI/SA">>, V}}.
parse_e(<<"A">>) -> {ok, attacked};
parse_e(<<"P">>) -> {ok, poc};
parse_e(<<"U">>) -> {ok, unreported};
parse_e(<<"X">>) -> {ok, not_defined};
parse_e(V) -> {error, {invalid_metric, <<"E">>, V}}.
parse_req(<<"L">>) -> {ok, low};
parse_req(<<"M">>) -> {ok, medium};
parse_req(<<"H">>) -> {ok, high};
parse_req(<<"X">>) -> {ok, not_defined};
parse_req(V) -> {error, {invalid_metric, <<"CR/IR/AR">>, V}}.
parse_safety(<<"X">>) -> {ok, undefined};
parse_safety(<<"N">>) -> {ok, negligible};
parse_safety(<<"P">>) -> {ok, present};
parse_safety(V) -> {error, {invalid_metric, <<"S">>, V}}.
parse_automatable(<<"X">>) -> {ok, undefined};
parse_automatable(<<"N">>) -> {ok, no};
parse_automatable(<<"Y">>) -> {ok, yes};
parse_automatable(V) -> {error, {invalid_metric, <<"AU">>, V}}.
parse_recovery(<<"X">>) -> {ok, undefined};
parse_recovery(<<"A">>) -> {ok, automatic};
parse_recovery(<<"U">>) -> {ok, user};
parse_recovery(<<"I">>) -> {ok, irrecoverable};
parse_recovery(V) -> {error, {invalid_metric, <<"R">>, V}}.
parse_value_density(<<"X">>) -> {ok, undefined};
parse_value_density(<<"D">>) -> {ok, diffuse};
parse_value_density(<<"C">>) -> {ok, concentrated};
parse_value_density(V) -> {error, {invalid_metric, <<"V">>, V}}.
parse_response_effort(<<"X">>) -> {ok, undefined};
parse_response_effort(<<"L">>) -> {ok, low};
parse_response_effort(<<"M">>) -> {ok, moderate};
parse_response_effort(<<"H">>) -> {ok, high};
parse_response_effort(V) -> {error, {invalid_metric, <<"RE">>, V}}.
%% Modified metric parsers - accept "X" as undefined
parse_av_x(<<"X">>) -> {ok, undefined};
parse_av_x(V) -> parse_av(V).
parse_ac_x(<<"X">>) -> {ok, undefined};
parse_ac_x(V) -> parse_ac(V).
parse_at_x(<<"X">>) -> {ok, undefined};
parse_at_x(V) -> parse_at(V).
parse_pr_x(<<"X">>) -> {ok, undefined};
parse_pr_x(V) -> parse_pr(V).
parse_ui_x(<<"X">>) -> {ok, undefined};
parse_ui_x(V) -> parse_ui(V).
parse_cia_x(<<"X">>) -> {ok, undefined};
parse_cia_x(V) -> parse_cia(V).
parse_si_sa_x(<<"X">>) -> {ok, undefined};
parse_si_sa_x(V) -> parse_si_sa(V).
parse_urgency(<<"X">>) -> {ok, undefined};
parse_urgency(<<"Clear">>) -> {ok, clear};
parse_urgency(<<"Green">>) -> {ok, green};
parse_urgency(<<"Amber">>) -> {ok, amber};
parse_urgency(<<"Red">>) -> {ok, red};
parse_urgency(V) -> {error, {invalid_metric, <<"U">>, V}}.
%%====================================================================
%% Internal: Metric Composing
%%====================================================================
av_to_string(network) -> "N";
av_to_string(adjacent) -> "A";
av_to_string(local) -> "L";
av_to_string(physical) -> "P".
ac_to_string(low) -> "L";
ac_to_string(high) -> "H".
at_to_string(none) -> "N";
at_to_string(present) -> "P".
pr_to_string(none) -> "N";
pr_to_string(low) -> "L";
pr_to_string(high) -> "H".
ui_to_string(none) -> "N";
ui_to_string(passive) -> "P";
ui_to_string(active) -> "A".
cia_to_string(none) -> "N";
cia_to_string(low) -> "L";
cia_to_string(high) -> "H".
si_sa_to_string(none) -> "N";
si_sa_to_string(low) -> "L";
si_sa_to_string(high) -> "H";
si_sa_to_string(safety) -> "S".
e_to_string(attacked) -> "A";
e_to_string(poc) -> "P";
e_to_string(unreported) -> "U";
e_to_string(not_defined) -> "X".
req_to_string(low) -> "L";
req_to_string(medium) -> "M";
req_to_string(high) -> "H";
req_to_string(not_defined) -> "X".
safety_to_string(negligible) -> "N";
safety_to_string(present) -> "P".
automatable_to_string(no) -> "N";
automatable_to_string(yes) -> "Y".
recovery_to_string(automatic) -> "A";
recovery_to_string(user) -> "U";
recovery_to_string(irrecoverable) -> "I".
value_density_to_string(diffuse) -> "D";
value_density_to_string(concentrated) -> "C".
response_effort_to_string(low) -> "L";
response_effort_to_string(moderate) -> "M";
response_effort_to_string(high) -> "H".
urgency_to_string(clear) -> "Clear";
urgency_to_string(green) -> "Green";
urgency_to_string(amber) -> "Amber";
urgency_to_string(red) -> "Red".
%%====================================================================
%% Internal: Effective Values
%%====================================================================
effective(undefined, Base) -> Base;
effective(Modified, _Base) -> Modified.
effective_si_sa(undefined, Base) -> Base;
effective_si_sa(Modified, _Base) -> Modified.
%% CR/IR/AR: X defaults to H (high), not M (medium)
%% Per FIRST reference implementation
effective_req(undefined) -> high;
effective_req(not_defined) -> high;
effective_req(Value) -> Value.
effective_e(undefined) -> attacked;
effective_e(not_defined) -> attacked;
effective_e(Value) -> Value.
%%====================================================================
%% Internal: MacroVector Calculation (EQ1-EQ6)
%%====================================================================
%% EQ1: AV/PR/UI
%% 0: AV:N AND PR:N AND UI:N
%% 1: (AV:N OR PR:N OR UI:N) AND NOT level 0 AND AV != P
%% 2: AV:P OR none of AV/PR/UI is N/N/N
calculate_eq1(AV, PR, UI) ->
case {AV, PR, UI} of
{network, none, none} ->
0;
{physical, _, _} ->
2;
_ ->
HasNetwork = (AV =:= network) orelse (PR =:= none) orelse (UI =:= none),
case HasNetwork of
true -> 1;
false -> 2
end
end.
%% EQ2: AC/AT
calculate_eq2(AC, AT) ->
case {AC, AT} of
{low, none} -> 0;
{_, _} -> 1
end.
%% EQ3: VC/VI/VA
%% 0: VC:H and VI:H
%% 1: not (VC:H and VI:H) and (VC:H or VI:H or VA:H)
%% 2: not (VC:H or VI:H or VA:H)
calculate_eq3(VC, VI, VA) ->
HasHigh = (VC =:= high) orelse (VI =:= high) orelse (VA =:= high),
case {VC, VI, HasHigh} of
{high, high, _} -> 0;
{_, _, true} -> 1;
{_, _, false} -> 2
end.
%% EQ4: SC/SI/SA (with safety consideration)
%% 0: MSI:S or MSA:S
%% 1: not (MSI:S or MSA:S) and (SC:H or SI:H or SA:H)
%% 2: not (MSI:S or MSA:S) and not (SC:H or SI:H or SA:H)
calculate_eq4(SC, SI, SA) ->
HasSafety = (SI =:= safety) orelse (SA =:= safety),
HasHigh = (SC =:= high) orelse (SI =:= high) orelse (SA =:= high),
case {HasSafety, HasHigh} of
{true, _} -> 0;
{false, true} -> 1;
{false, false} -> 2
end.
%% EQ5: E
calculate_eq5(E) ->
case E of
attacked -> 0;
poc -> 1;
unreported -> 2
end.
%% EQ6: CR/IR/AR with VC/VI/VA
calculate_eq6(CR, IR, AR, VC, VI, VA) ->
%% Check if any high requirement is paired with high impact
HasHighReqHighImpact =
(CR =:= high andalso VC =:= high) orelse
(IR =:= high andalso VI =:= high) orelse
(AR =:= high andalso VA =:= high),
case HasHighReqHighImpact of
true -> 0;
false -> 1
end.
%%====================================================================
%% Internal: Score Interpolation
%%====================================================================
interpolate_score(BaseScore, #eff{} = E, EQ1, EQ2, EQ3, EQ4, EQ5, EQ6) ->
%% Get max composed vectors for each EQ level
MaxEQ1 = max_composed_eq1(EQ1),
MaxEQ2 = max_composed_eq2(EQ2),
MaxEQ3EQ6 = max_composed_eq3eq6(EQ3, EQ6),
MaxEQ4 = max_composed_eq4(EQ4),
%% EQ5 max composed doesn't affect distance (always 0)
%% Try each combination of max composed vectors
case find_valid_distances(E, MaxEQ1, MaxEQ2, MaxEQ3EQ6, MaxEQ4) of
none ->
BaseScore;
{D1, D2, D3EQ6, D4} ->
%% Max severity depths
Max1 = max_severity_eq1(EQ1),
Max2 = max_severity_eq2(EQ2),
Max3EQ6 = max_severity_eq3eq6(EQ3, EQ6),
Max4 = max_severity_eq4(EQ4),
%% Calculate MSD (Maximal Scoring Difference) for each EQ
MSD1 = msd(BaseScore, EQ1 + 1, EQ2, EQ3, EQ4, EQ5, EQ6),
MSD2 = msd(BaseScore, EQ1, EQ2 + 1, EQ3, EQ4, EQ5, EQ6),
MSD3EQ6 = msd_eq3eq6(BaseScore, EQ1, EQ2, EQ3, EQ4, EQ5, EQ6),
MSD4 = msd(BaseScore, EQ1, EQ2, EQ3, EQ4 + 1, EQ5, EQ6),
MSD5 = msd(BaseScore, EQ1, EQ2, EQ3, EQ4, EQ5 + 1, EQ6),
%% EQ5 distance is always 0 - each EQ5 level has only one possible E value
EQs = [
{MSD1, D1, Max1},
{MSD2, D2, Max2},
{MSD3EQ6, D3EQ6, Max3EQ6},
{MSD4, D4, Max4},
{MSD5, ?ZERO, ?D(<<"0.1">>)}
],
{NormSum, Count} = lists:foldl(
fun fold_eq_distance/2,
{?ZERO, 0},
EQs
),
case Count of
0 ->
BaseScore;
_ ->
MeanDistance = cvss_decimal:divide(NormSum, ?D(Count), ?DECIMAL_OPTS),
cvss_common:dmax(
?ZERO,
cvss_decimal:round(
round_half_up, cvss_decimal:sub(BaseScore, MeanDistance), 1
)
)
end
end.
fold_eq_distance({undefined, _D, _Max}, Acc) ->
Acc;
fold_eq_distance({MSD, D, Max}, {Sum, N}) ->
case cvss_decimal:is_zero(Max) of
true ->
{Sum, N};
false ->
Percent = cvss_decimal:divide(D, Max, ?DECIMAL_OPTS),
{cvss_decimal:add(Sum, cvss_decimal:mult(MSD, Percent)), N + 1}
end.
%% Try all combinations of max composed vectors to find one where
%% every individual metric distance is non-negative (matching JS reference).
find_valid_distances(E, MaxEQ1List, MaxEQ2List, MaxEQ3EQ6List, MaxEQ4List) ->
MaxEQ5List = max_composed_eq5(E#eff.e),
Combinations = [
{M1, M2, M3EQ6, M4, M5}
|| M1 <- MaxEQ1List,
M2 <- MaxEQ2List,
M3EQ6 <- MaxEQ3EQ6List,
M4 <- MaxEQ4List,
M5 <- MaxEQ5List
],
find_first_valid(E, Combinations).
find_first_valid(_E, []) ->
none;
find_first_valid(E, [{Max1, Max2, Max3EQ6, Max4, _Max5} | Rest]) ->
%% Compute individual metric distances
#eff{
av = AV,
ac = AC,
at = AT,
pr = PR,
ui = UI,
vc = VC,
vi = VI,
va = VA,
sc = SC,
si = SI,
sa = SA,
cr = CR,
ir = IR,
ar = AR
} = E,
{MaxAV, MaxPR, MaxUI} = Max1,
{MaxAC, MaxAT} = Max2,
{MaxVC, MaxVI, MaxVA, MaxCR, MaxIR, MaxAR} = Max3EQ6,
{MaxSC, MaxSI, MaxSA} = Max4,
Dists = [
cvss_decimal:sub(av_level(AV), av_level(MaxAV)),
cvss_decimal:sub(pr_level(PR), pr_level(MaxPR)),
cvss_decimal:sub(ui_level(UI), ui_level(MaxUI)),
cvss_decimal:sub(ac_level(AC), ac_level(MaxAC)),
cvss_decimal:sub(at_level(AT), at_level(MaxAT)),
cvss_decimal:sub(vc_level(VC), vc_level(MaxVC)),
cvss_decimal:sub(vi_level(VI), vi_level(MaxVI)),
cvss_decimal:sub(va_level(VA), va_level(MaxVA)),
cvss_decimal:sub(sc_level(SC), sc_level(MaxSC)),
cvss_decimal:sub(si_level(SI), si_level(MaxSI)),
cvss_decimal:sub(sa_level(SA), sa_level(MaxSA)),
cvss_decimal:sub(cr_level(CR), cr_level(MaxCR)),
cvss_decimal:sub(ir_level(IR), ir_level(MaxIR)),
cvss_decimal:sub(ar_level(AR), ar_level(MaxAR))
],
Opts = ?DECIMAL_OPTS,
case lists:all(fun(D) -> cvss_decimal:cmp(D, ?ZERO, Opts) >= 0 end, Dists) of
true ->
[DAV, DPR, DUI, DAC, DAT, DVC, DVI, DVA, DSC, DSI, DSA, DCR, DIR, DAR] = Dists,
D1 = cvss_decimal:add(cvss_decimal:add(DAV, DPR), DUI),
D2 = cvss_decimal:add(DAC, DAT),
D3EQ6 = cvss_decimal:add(
cvss_decimal:add(
cvss_decimal:add(cvss_decimal:add(cvss_decimal:add(DVC, DVI), DVA), DCR),
DIR
),
DAR
),
D4 = cvss_decimal:add(cvss_decimal:add(DSC, DSI), DSA),
{D1, D2, D3EQ6, D4};
false ->
find_first_valid(E, Rest)
end.
max_composed_eq5(attacked) -> [{attacked}];
max_composed_eq5(poc) -> [{poc}];
max_composed_eq5(unreported) -> [{unreported}].
%% Metric level values matching FIRST JS reference (0.1 step decimals)
av_level(network) -> ?ZERO;
av_level(adjacent) -> ?D(<<"0.1">>);
av_level(local) -> ?D(<<"0.2">>);
av_level(physical) -> ?D(<<"0.3">>).
pr_level(none) -> ?ZERO;
pr_level(low) -> ?D(<<"0.1">>);
pr_level(high) -> ?D(<<"0.2">>).
ui_level(none) -> ?ZERO;
ui_level(passive) -> ?D(<<"0.1">>);
ui_level(active) -> ?D(<<"0.2">>).
ac_level(low) -> ?ZERO;
ac_level(high) -> ?D(<<"0.1">>).
at_level(none) -> ?ZERO;
at_level(present) -> ?D(<<"0.1">>).
vc_level(high) -> ?ZERO;
vc_level(low) -> ?D(<<"0.1">>);
vc_level(none) -> ?D(<<"0.2">>).
vi_level(high) -> ?ZERO;
vi_level(low) -> ?D(<<"0.1">>);
vi_level(none) -> ?D(<<"0.2">>).
va_level(high) -> ?ZERO;
va_level(low) -> ?D(<<"0.1">>);
va_level(none) -> ?D(<<"0.2">>).
sc_level(high) -> ?D(<<"0.1">>);
sc_level(low) -> ?D(<<"0.2">>);
sc_level(none) -> ?D(<<"0.3">>).
si_level(safety) -> ?ZERO;
si_level(high) -> ?D(<<"0.1">>);
si_level(low) -> ?D(<<"0.2">>);
si_level(none) -> ?D(<<"0.3">>).
sa_level(safety) -> ?ZERO;
sa_level(high) -> ?D(<<"0.1">>);
sa_level(low) -> ?D(<<"0.2">>);
sa_level(none) -> ?D(<<"0.3">>).
cr_level(high) -> ?ZERO;
cr_level(medium) -> ?D(<<"0.1">>);
cr_level(low) -> ?D(<<"0.2">>).
ir_level(high) -> ?ZERO;
ir_level(medium) -> ?D(<<"0.1">>);
ir_level(low) -> ?D(<<"0.2">>).
ar_level(high) -> ?ZERO;
ar_level(medium) -> ?D(<<"0.1">>);
ar_level(low) -> ?D(<<"0.2">>).
%% MSD for combined EQ3+EQ6
msd_eq3eq6(BaseScore, EQ1, EQ2, EQ3, EQ4, EQ5, EQ6) ->
case {EQ3, EQ6} of
{0, 0} ->
%% Two paths: 00->01 or 00->10, take the one with higher score
Left = lookup_eq(EQ1, EQ2, EQ3, EQ4, EQ5, EQ6 + 1),
Right = lookup_eq(EQ1, EQ2, EQ3 + 1, EQ4, EQ5, EQ6),
case {Left, Right} of
{undefined, undefined} -> undefined;
{undefined, R} -> cvss_decimal:sub(BaseScore, R);
{L, undefined} -> cvss_decimal:sub(BaseScore, L);
{L, R} -> cvss_decimal:sub(BaseScore, cvss_common:dmax(L, R))
end;
{1, 1} ->
%% 11 -> 21
msd(BaseScore, EQ1, EQ2, EQ3 + 1, EQ4, EQ5, EQ6);
{0, 1} ->
%% 01 -> 11
msd(BaseScore, EQ1, EQ2, EQ3 + 1, EQ4, EQ5, EQ6);
{1, 0} ->
%% 10 -> 11
msd(BaseScore, EQ1, EQ2, EQ3, EQ4, EQ5, EQ6 + 1);
{2, 1} ->
%% 21 -> 32 (does not exist, so undefined)
undefined;
_ ->
undefined
end.
msd(BaseScore, EQ1, EQ2, EQ3, EQ4, EQ5, EQ6) ->
case lookup_eq(EQ1, EQ2, EQ3, EQ4, EQ5, EQ6) of
undefined -> undefined;
LowerScore -> cvss_decimal:sub(BaseScore, LowerScore)
end.
lookup_eq(EQ1, EQ2, EQ3, EQ4, EQ5, EQ6) when
EQ1 >= 0,
EQ1 =< 2,
EQ2 >= 0,
EQ2 =< 1,
EQ3 >= 0,
EQ3 =< 2,
EQ4 >= 0,
EQ4 =< 2,
EQ5 >= 0,
EQ5 =< 2,
EQ6 >= 0,
EQ6 =< 1
->
cvss_v4_lookup:lookup(EQ1, EQ2, EQ3, EQ4, EQ5, EQ6);
lookup_eq(_, _, _, _, _, _) ->
undefined.
%% Max severity depths per EQ (decimal values, step=0.1)
max_severity_eq1(0) -> ?D(<<"0.1">>);
max_severity_eq1(1) -> ?D(<<"0.4">>);
max_severity_eq1(2) -> ?D(<<"0.5">>).
max_severity_eq2(0) -> ?D(<<"0.1">>);
max_severity_eq2(1) -> ?D(<<"0.2">>).
max_severity_eq3eq6(0, 0) -> ?D(<<"0.7">>);
max_severity_eq3eq6(0, 1) -> ?D(<<"0.6">>);
max_severity_eq3eq6(1, 0) -> ?D(<<"0.8">>);
max_severity_eq3eq6(1, 1) -> ?D(<<"0.8">>);
max_severity_eq3eq6(2, 1) -> ?D(<<"1.0">>);
max_severity_eq3eq6(_, _) -> ?ZERO.
max_severity_eq4(0) -> ?D(<<"0.6">>);
max_severity_eq4(1) -> ?D(<<"0.5">>);
max_severity_eq4(2) -> ?D(<<"0.4">>).
%% Max composed vectors for each EQ level
%% Returns list of {MetricValue, ...} tuples representing max severity vectors
max_composed_eq1(0) -> [{network, none, none}];
max_composed_eq1(1) -> [{adjacent, none, none}, {network, low, none}, {network, none, passive}];
max_composed_eq1(2) -> [{physical, none, none}, {adjacent, low, passive}].
max_composed_eq2(0) -> [{low, none}];
max_composed_eq2(1) -> [{high, none}, {low, present}].
max_composed_eq3eq6(0, 0) ->
[{high, high, high, high, high, high}];
max_composed_eq3eq6(0, 1) ->
[{high, high, low, medium, medium, high}, {high, high, high, medium, medium, medium}];
max_composed_eq3eq6(1, 0) ->
[{low, high, high, high, high, high}, {high, low, high, high, high, high}];
max_composed_eq3eq6(1, 1) ->
[
{low, high, low, high, medium, high},
{low, high, high, high, medium, medium},
{high, low, high, medium, high, medium},
{high, low, low, medium, high, high},
{low, low, high, high, high, medium}
];
max_composed_eq3eq6(2, 1) ->
[{low, low, low, high, high, high}];
max_composed_eq3eq6(_, _) ->
[].
max_composed_eq4(0) -> [{high, safety, safety}];
max_composed_eq4(1) -> [{high, high, high}];
max_composed_eq4(2) -> [{low, low, low}].