%% SPDX-License-Identifier: Apache-2.0 %% SPDX-FileCopyrightText: 2026 Erlang Ecosystem Foundation -module(cvss_v2). -moduledoc """ CVSS 2.0 parsing, composition, validation, and scoring. Use this module when working with CVSS 2.0 vectors directly. If the version is not known ahead of time, use `m:cvss` instead. Vector format: `AV:N/AC:L/Au:N/C:P/I:P/A:C` See: https://www.first.org/cvss/v2/guide """. -export([ base_score/1, compose/1, environmental_score/1, parse/1, score/1, temporal_score/1, valid/1 ]). -export_type([ cvss/0, av/0, ac/0, au/0, impact/0, exploitability/0, remediation_level/0, report_confidence/0, cdp/0, td/0, requirement/0 ]). -include("cvss_v2.hrl"). -type av() :: local | adjacent_network | network. -type ac() :: high | medium | low. -type au() :: multiple | single | none. -type impact() :: none | partial | complete. -type exploitability() :: unproven | proof_of_concept | functional | high | not_defined. -type remediation_level() :: official_fix | temporary_fix | workaround | unavailable | not_defined. -type report_confidence() :: unconfirmed | uncorroborated | confirmed | not_defined. -type cdp() :: none | low | low_medium | medium_high | high | not_defined. -type td() :: none | low | medium | high | not_defined. -type requirement() :: low | medium | high | not_defined. -type cvss() :: #cvss_v2{}. %%==================================================================== %% API %%==================================================================== -doc """ Parse a CVSS 2.0 vector string. Format: AV:N/AC:L/Au:N/C:P/I:P/A:C (parentheses optional) ```erlang > cvss_v2:parse(<<"AV:N/AC:L/Au:N/C:C/I:C/A:C">>). {ok, #cvss_v2{av = network, ac = low, au = none, c = complete, i = complete, a = complete}} > cvss_v2:parse(<<"(AV:N/AC:L/Au:N/C:P/I:P/A:C)">>). {ok, #cvss_v2{av = network, ac = low, au = none, c = partial, i = partial, a = complete}} > cvss_v2:parse(<<"AV:N/AC:L/Au:N/C:C/I:C/A:C/E:F/RL:W/RC:C">>). {ok, #cvss_v2{av = network, ac = low, au = none, c = complete, i = complete, a = complete, e = functional, rl = workaround, rc = confirmed}} > cvss_v2:parse(<<"AV:X/AC:L/Au:N/C:C/I:C/A:C">>). {error, {invalid_metric, <<"AV">>, <<"X">>}} ``` """. -spec parse(binary()) -> {ok, cvss_v2:cvss()} | {error, cvss:parse_error()}. parse(Vector) when is_binary(Vector) -> %% Strip optional parentheses Stripped = cvss_common:strip_parens(Vector), case cvss_common:parse_metrics(binary:split(Stripped, <<"/">>, [global]), fun parse_metric/2) of {ok, Metrics} -> build_record(Metrics); {error, _} = Error -> Error end. -doc """ Compose a CVSS 2.0 record into a vector string. ```erlang > iolist_to_binary(cvss_v2:compose(#cvss_v2{av = network, ac = low, au = none, c = complete, i = complete, a = complete})). <<"AV:N/AC:L/Au:N/C:C/I:C/A:C">> ``` """. -spec compose(cvss_v2:cvss()) -> iolist(). compose(#cvss_v2{} = Cvss) -> Base = [ <<"AV:">>, encode_av(Cvss#cvss_v2.av), <<"/AC:">>, encode_ac(Cvss#cvss_v2.ac), <<"/Au:">>, encode_au(Cvss#cvss_v2.au), <<"/C:">>, encode_impact(Cvss#cvss_v2.c), <<"/I:">>, encode_impact(Cvss#cvss_v2.i), <<"/A:">>, encode_impact(Cvss#cvss_v2.a) ], Temporal = cvss_common:encode_optional( [ {<<"E">>, Cvss#cvss_v2.e, fun encode_exploitability/1}, {<<"RL">>, Cvss#cvss_v2.rl, fun encode_remediation_level/1}, {<<"RC">>, Cvss#cvss_v2.rc, fun encode_report_confidence/1} ], [undefined] ), Environmental = cvss_common:encode_optional( [ {<<"CDP">>, Cvss#cvss_v2.cdp, fun encode_cdp/1}, {<<"TD">>, Cvss#cvss_v2.td, fun encode_td/1}, {<<"CR">>, Cvss#cvss_v2.cr, fun encode_requirement/1}, {<<"IR">>, Cvss#cvss_v2.ir, fun encode_requirement/1}, {<<"AR">>, Cvss#cvss_v2.ar, fun encode_requirement/1} ], [undefined] ), [Base, Temporal, Environmental]. -doc """ Check whether a CVSS 2.0 value is valid. Accepts either a vector string or a parsed record. ```erlang > cvss_v2:valid(<<"AV:N/AC:L/Au:N/C:C/I:C/A:C">>). true > cvss_v2:valid(#cvss_v2{av = network, ac = low, au = none, c = complete, i = complete, a = complete}). true > cvss_v2:valid(<<"AV:X/AC:L/Au:N/C:C/I:C/A:C">>). false ``` """. -spec valid(iodata() | cvss_v2:cvss()) -> boolean(). valid(#cvss_v2{av = AV, ac = AC, au = Au, c = C, i = I, a = A} = Cvss) -> ValidBase = lists:member(AV, [local, adjacent_network, network]) andalso lists:member(AC, [high, medium, low]) andalso lists:member(Au, [multiple, single, none]) andalso lists:member(C, [none, partial, complete]) andalso lists:member(I, [none, partial, complete]) andalso lists:member(A, [none, partial, complete]), ValidTemporal = cvss_common:valid_optional(Cvss#cvss_v2.e, [ unproven, proof_of_concept, functional, high, not_defined ]) andalso cvss_common:valid_optional(Cvss#cvss_v2.rl, [ official_fix, temporary_fix, workaround, unavailable, not_defined ]) andalso cvss_common:valid_optional(Cvss#cvss_v2.rc, [ unconfirmed, uncorroborated, confirmed, not_defined ]), ValidEnvironmental = cvss_common:valid_optional(Cvss#cvss_v2.cdp, [ none, low, low_medium, medium_high, high, not_defined ]) andalso cvss_common:valid_optional(Cvss#cvss_v2.td, [none, low, medium, high, not_defined]) andalso cvss_common:valid_optional(Cvss#cvss_v2.cr, [low, medium, high, not_defined]) andalso cvss_common:valid_optional(Cvss#cvss_v2.ir, [low, medium, high, not_defined]) andalso cvss_common:valid_optional(Cvss#cvss_v2.ar, [low, medium, high, not_defined]), ValidBase andalso ValidTemporal andalso ValidEnvironmental; valid(Vector) -> case parse(iolist_to_binary(Vector)) of {ok, Cvss} -> valid(Cvss); {error, _} -> false end. -doc """ Calculate the CVSS 2.0 score. Returns the most relevant score: Environmental > Temporal > Base. ```erlang > {ok, Cvss} = cvss_v2:parse(<<"AV:N/AC:L/Au:N/C:C/I:C/A:C">>). > cvss_v2:score(Cvss). 10.0 > {ok, Cvss2} = cvss_v2:parse(<<"AV:N/AC:L/Au:N/C:N/I:N/A:N">>). > cvss_v2:score(Cvss2). 0.0 > {ok, Cvss3} = cvss_v2:parse(<<"AV:N/AC:L/Au:N/C:N/I:N/A:C">>). > cvss_v2:score(Cvss3). 7.8 ``` """. -spec score(cvss_v2:cvss()) -> cvss:score(). score(#cvss_v2{} = Cvss) -> BaseScore = calculate_base_score(Cvss), TemporalScore = calculate_temporal_score(Cvss, BaseScore), cvss_common:to_float(calculate_environmental_score(Cvss, TemporalScore)). -doc """ Calculate the CVSS 2.0 Base Score. ```erlang > {ok, Cvss} = cvss_v2:parse(<<"AV:N/AC:L/Au:N/C:C/I:C/A:C">>). > cvss_v2:base_score(Cvss). 10.0 ``` """. -spec base_score(cvss_v2:cvss()) -> cvss:score(). base_score(#cvss_v2{} = Cvss) -> cvss_common:to_float(calculate_base_score(Cvss)). -doc """ Calculate the CVSS 2.0 Temporal Score. Returns the Base Score if no temporal metrics are present. ```erlang > {ok, Cvss} = cvss_v2:parse(<<"AV:N/AC:L/Au:N/C:C/I:C/A:C/E:U/RL:OF/RC:UC">>). > cvss_v2:temporal_score(Cvss). 6.7 ``` """. -spec temporal_score(cvss_v2:cvss()) -> cvss:score(). temporal_score(#cvss_v2{} = Cvss) -> cvss_common:to_float(calculate_temporal_score(Cvss, calculate_base_score(Cvss))). -doc """ Calculate the CVSS 2.0 Environmental Score. Returns the Temporal Score if no environmental metrics are present. ```erlang > {ok, Cvss} = cvss_v2:parse(<<"AV:N/AC:L/Au:N/C:C/I:C/A:C/CDP:H/TD:H/CR:H/IR:H/AR:H">>). > cvss_v2:environmental_score(Cvss). 10.0 ``` """. -spec environmental_score(cvss_v2:cvss()) -> cvss:score(). environmental_score(#cvss_v2{} = Cvss) -> BaseScore = calculate_base_score(Cvss), TemporalScore = calculate_temporal_score(Cvss, BaseScore), cvss_common:to_float(calculate_environmental_score(Cvss, TemporalScore)). %%==================================================================== %% Internal - Parsing %%==================================================================== parse_metric(<<"AV">>, <<"L">>) -> {ok, {av, local}}; parse_metric(<<"AV">>, <<"A">>) -> {ok, {av, adjacent_network}}; parse_metric(<<"AV">>, <<"N">>) -> {ok, {av, network}}; parse_metric(<<"AC">>, <<"H">>) -> {ok, {ac, high}}; parse_metric(<<"AC">>, <<"M">>) -> {ok, {ac, medium}}; parse_metric(<<"AC">>, <<"L">>) -> {ok, {ac, low}}; parse_metric(<<"Au">>, <<"M">>) -> {ok, {au, multiple}}; parse_metric(<<"Au">>, <<"S">>) -> {ok, {au, single}}; parse_metric(<<"Au">>, <<"N">>) -> {ok, {au, none}}; parse_metric(<<"C">>, V) -> parse_impact(c, V); parse_metric(<<"I">>, V) -> parse_impact(i, V); parse_metric(<<"A">>, V) -> parse_impact(a, V); parse_metric(<<"E">>, <<"U">>) -> {ok, {e, unproven}}; parse_metric(<<"E">>, <<"POC">>) -> {ok, {e, proof_of_concept}}; parse_metric(<<"E">>, <<"F">>) -> {ok, {e, functional}}; parse_metric(<<"E">>, <<"H">>) -> {ok, {e, high}}; parse_metric(<<"E">>, <<"ND">>) -> {ok, {e, not_defined}}; parse_metric(<<"RL">>, <<"OF">>) -> {ok, {rl, official_fix}}; parse_metric(<<"RL">>, <<"TF">>) -> {ok, {rl, temporary_fix}}; parse_metric(<<"RL">>, <<"W">>) -> {ok, {rl, workaround}}; parse_metric(<<"RL">>, <<"U">>) -> {ok, {rl, unavailable}}; parse_metric(<<"RL">>, <<"ND">>) -> {ok, {rl, not_defined}}; parse_metric(<<"RC">>, <<"UC">>) -> {ok, {rc, unconfirmed}}; parse_metric(<<"RC">>, <<"UR">>) -> {ok, {rc, uncorroborated}}; parse_metric(<<"RC">>, <<"C">>) -> {ok, {rc, confirmed}}; parse_metric(<<"RC">>, <<"ND">>) -> {ok, {rc, not_defined}}; parse_metric(<<"CDP">>, <<"N">>) -> {ok, {cdp, none}}; parse_metric(<<"CDP">>, <<"L">>) -> {ok, {cdp, low}}; parse_metric(<<"CDP">>, <<"LM">>) -> {ok, {cdp, low_medium}}; parse_metric(<<"CDP">>, <<"MH">>) -> {ok, {cdp, medium_high}}; parse_metric(<<"CDP">>, <<"H">>) -> {ok, {cdp, high}}; parse_metric(<<"CDP">>, <<"ND">>) -> {ok, {cdp, not_defined}}; parse_metric(<<"TD">>, <<"N">>) -> {ok, {td, none}}; parse_metric(<<"TD">>, <<"L">>) -> {ok, {td, low}}; parse_metric(<<"TD">>, <<"M">>) -> {ok, {td, medium}}; parse_metric(<<"TD">>, <<"H">>) -> {ok, {td, high}}; parse_metric(<<"TD">>, <<"ND">>) -> {ok, {td, not_defined}}; parse_metric(<<"CR">>, V) -> parse_requirement(cr, V); parse_metric(<<"IR">>, V) -> parse_requirement(ir, V); parse_metric(<<"AR">>, V) -> parse_requirement(ar, V); parse_metric(Key, Value) -> {error, {invalid_metric, Key, Value}}. parse_impact(Field, <<"N">>) -> {ok, {Field, none}}; parse_impact(Field, <<"P">>) -> {ok, {Field, partial}}; parse_impact(Field, <<"C">>) -> {ok, {Field, complete}}; parse_impact(Field, Value) -> {error, {invalid_metric, atom_to_binary(Field), Value}}. parse_requirement(Field, <<"L">>) -> {ok, {Field, low}}; parse_requirement(Field, <<"M">>) -> {ok, {Field, medium}}; parse_requirement(Field, <<"H">>) -> {ok, {Field, high}}; parse_requirement(Field, <<"ND">>) -> {ok, {Field, not_defined}}; parse_requirement(Field, Value) -> {error, {invalid_metric, atom_to_binary(Field), Value}}. build_record(Metrics) -> Required = [av, ac, au, c, i, a], case cvss_common:check_required(Required, Metrics) of ok -> {ok, #cvss_v2{ av = maps:get(av, Metrics), ac = maps:get(ac, Metrics), au = maps:get(au, Metrics), c = maps:get(c, Metrics), i = maps:get(i, Metrics), a = maps:get(a, Metrics), e = maps:get(e, Metrics, undefined), rl = maps:get(rl, Metrics, undefined), rc = maps:get(rc, Metrics, undefined), cdp = maps:get(cdp, Metrics, undefined), td = maps:get(td, Metrics, undefined), cr = maps:get(cr, Metrics, undefined), ir = maps:get(ir, Metrics, undefined), ar = maps:get(ar, Metrics, undefined) }}; {error, _} = Error -> Error end. %%==================================================================== %% Internal - Encoding %%==================================================================== encode_av(local) -> <<"L">>; encode_av(adjacent_network) -> <<"A">>; encode_av(network) -> <<"N">>. encode_ac(high) -> <<"H">>; encode_ac(medium) -> <<"M">>; encode_ac(low) -> <<"L">>. encode_au(multiple) -> <<"M">>; encode_au(single) -> <<"S">>; encode_au(none) -> <<"N">>. encode_impact(none) -> <<"N">>; encode_impact(partial) -> <<"P">>; encode_impact(complete) -> <<"C">>. encode_exploitability(unproven) -> <<"U">>; encode_exploitability(proof_of_concept) -> <<"POC">>; encode_exploitability(functional) -> <<"F">>; encode_exploitability(high) -> <<"H">>; encode_exploitability(not_defined) -> <<"ND">>. encode_remediation_level(official_fix) -> <<"OF">>; encode_remediation_level(temporary_fix) -> <<"TF">>; encode_remediation_level(workaround) -> <<"W">>; encode_remediation_level(unavailable) -> <<"U">>; encode_remediation_level(not_defined) -> <<"ND">>. encode_report_confidence(unconfirmed) -> <<"UC">>; encode_report_confidence(uncorroborated) -> <<"UR">>; encode_report_confidence(confirmed) -> <<"C">>; encode_report_confidence(not_defined) -> <<"ND">>. encode_cdp(none) -> <<"N">>; encode_cdp(low) -> <<"L">>; encode_cdp(low_medium) -> <<"LM">>; encode_cdp(medium_high) -> <<"MH">>; encode_cdp(high) -> <<"H">>; encode_cdp(not_defined) -> <<"ND">>. encode_td(none) -> <<"N">>; encode_td(low) -> <<"L">>; encode_td(medium) -> <<"M">>; encode_td(high) -> <<"H">>; encode_td(not_defined) -> <<"ND">>. encode_requirement(low) -> <<"L">>; encode_requirement(medium) -> <<"M">>; encode_requirement(high) -> <<"H">>; encode_requirement(not_defined) -> <<"ND">>. %%==================================================================== %% Internal - Scoring %%==================================================================== -include("internal/decimal.hrl"). %% Coefficients from CVSS 2.0 spec (decimal values) av_coeff(local) -> ?D(<<"0.395">>); av_coeff(adjacent_network) -> ?D(<<"0.646">>); av_coeff(network) -> ?ONE. ac_coeff(high) -> ?D(<<"0.35">>); ac_coeff(medium) -> ?D(<<"0.61">>); ac_coeff(low) -> ?D(<<"0.71">>). au_coeff(multiple) -> ?D(<<"0.45">>); au_coeff(single) -> ?D(<<"0.56">>); au_coeff(none) -> ?D(<<"0.704">>). impact_coeff(none) -> ?ZERO; impact_coeff(partial) -> ?D(<<"0.275">>); impact_coeff(complete) -> ?D(<<"0.660">>). exploitability_coeff(undefined) -> ?ONE; exploitability_coeff(unproven) -> ?D(<<"0.85">>); exploitability_coeff(proof_of_concept) -> ?D(<<"0.9">>); exploitability_coeff(functional) -> ?D(<<"0.95">>); exploitability_coeff(high) -> ?ONE; exploitability_coeff(not_defined) -> ?ONE. remediation_level_coeff(undefined) -> ?ONE; remediation_level_coeff(official_fix) -> ?D(<<"0.87">>); remediation_level_coeff(temporary_fix) -> ?D(<<"0.9">>); remediation_level_coeff(workaround) -> ?D(<<"0.95">>); remediation_level_coeff(unavailable) -> ?ONE; remediation_level_coeff(not_defined) -> ?ONE. report_confidence_coeff(undefined) -> ?ONE; report_confidence_coeff(unconfirmed) -> ?D(<<"0.9">>); report_confidence_coeff(uncorroborated) -> ?D(<<"0.95">>); report_confidence_coeff(confirmed) -> ?ONE; report_confidence_coeff(not_defined) -> ?ONE. cdp_coeff(undefined) -> ?ZERO; cdp_coeff(none) -> ?ZERO; cdp_coeff(low) -> ?D(<<"0.1">>); cdp_coeff(low_medium) -> ?D(<<"0.3">>); cdp_coeff(medium_high) -> ?D(<<"0.4">>); cdp_coeff(high) -> ?D(<<"0.5">>); cdp_coeff(not_defined) -> ?ZERO. td_coeff(undefined) -> ?ONE; td_coeff(none) -> ?ZERO; td_coeff(low) -> ?D(<<"0.25">>); td_coeff(medium) -> ?D(<<"0.75">>); td_coeff(high) -> ?ONE; td_coeff(not_defined) -> ?ONE. requirement_coeff(undefined) -> ?ONE; requirement_coeff(low) -> ?D(<<"0.5">>); requirement_coeff(medium) -> ?ONE; requirement_coeff(high) -> ?D(<<"1.51">>); requirement_coeff(not_defined) -> ?ONE. exploitability_sub_score(AV, AC, Au) -> decimal:mult( decimal:mult( decimal:mult(?D(20), av_coeff(AV)), ac_coeff(AC) ), au_coeff(Au) ). base_score_from_components(Impact, Exploitability) -> FImpact = case decimal:is_zero(Impact) of true -> ?ZERO; false -> ?D(<<"1.176">>) end, Score = decimal:mult( decimal:sub( decimal:add( decimal:mult(?D(<<"0.6">>), Impact), decimal:mult(?D(<<"0.4">>), Exploitability) ), ?D(<<"1.5">>) ), FImpact ), cvss_common:round_v1_v2(Score). calculate_base_score(#cvss_v2{av = AV, ac = AC, au = Au, c = C, i = I, a = A}) -> Impact = decimal:mult( ?D(<<"10.41">>), decimal:sub( ?ONE, decimal:mult( decimal:mult( decimal:sub(?ONE, impact_coeff(C)), decimal:sub(?ONE, impact_coeff(I)) ), decimal:sub(?ONE, impact_coeff(A)) ) ) ), Exploitability = exploitability_sub_score(AV, AC, Au), cvss_common:dmax(?ZERO, base_score_from_components(Impact, Exploitability)). calculate_temporal_score(#cvss_v2{e = E, rl = RL, rc = RC}, BaseScore) -> case has_temporal_metrics(E, RL, RC) of false -> BaseScore; true -> Score = decimal:mult( decimal:mult( decimal:mult(BaseScore, exploitability_coeff(E)), remediation_level_coeff(RL) ), report_confidence_coeff(RC) ), cvss_common:dmax(?ZERO, cvss_common:round_v1_v2(Score)) end. has_temporal_metrics(undefined, undefined, undefined) -> false; has_temporal_metrics(_, _, _) -> true. calculate_environmental_score( #cvss_v2{cdp = CDP, td = TD, cr = CR, ir = IR, ar = AR} = Cvss, TemporalScore ) -> case has_environmental_metrics(CDP, TD, CR, IR, AR) of false -> TemporalScore; true -> %% Calculate adjusted impact #cvss_v2{c = C, i = I, a = A} = Cvss, AdjustedImpact = cvss_common:dmin( ?D(10), decimal:mult( ?D(<<"10.41">>), decimal:sub( ?ONE, decimal:mult( decimal:mult( decimal:sub( ?ONE, decimal:mult(impact_coeff(C), requirement_coeff(CR)) ), decimal:sub( ?ONE, decimal:mult(impact_coeff(I), requirement_coeff(IR)) ) ), decimal:sub( ?ONE, decimal:mult(impact_coeff(A), requirement_coeff(AR)) ) ) ) ) ), %% Recalculate base with adjusted impact #cvss_v2{av = AV, ac = AC, au = Au, e = E, rl = RL, rc = RC} = Cvss, Exploitability = exploitability_sub_score(AV, AC, Au), AdjustedBase = base_score_from_components(AdjustedImpact, Exploitability), AdjustedTemporal = cvss_common:round_v1_v2( decimal:mult( decimal:mult( decimal:mult(AdjustedBase, exploitability_coeff(E)), remediation_level_coeff(RL) ), report_confidence_coeff(RC) ) ), Score = decimal:mult( decimal:add( AdjustedTemporal, decimal:mult( decimal:sub(?D(10), AdjustedTemporal), cdp_coeff(CDP) ) ), td_coeff(TD) ), cvss_common:dmax(?ZERO, cvss_common:round_v1_v2(Score)) end. has_environmental_metrics(undefined, undefined, undefined, undefined, undefined) -> false; has_environmental_metrics(_, _, _, _, _) -> true.