Packages

CVSS (Common Vulnerability Scoring System) library for Erlang

Current section

Files

Jump to
cvss src cvss_v1.erl
Raw

src/cvss_v1.erl

%% SPDX-License-Identifier: Apache-2.0
%% SPDX-FileCopyrightText: 2026 Erlang Ecosystem Foundation
-module(cvss_v1).
-moduledoc """
CVSS 1.0 parsing, composition, validation, and scoring.
Use this module when working with CVSS 1.0 vectors directly. If the
version is not known ahead of time, use `m:cvss` instead.
Vector format: `AV:R/AC:L/Au:NR/C:C/I:C/A:C`
See: https://www.first.org/cvss/v1/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,
impact_bias/0,
exploitability/0,
remediation_level/0,
report_confidence/0,
cdp/0,
td/0
]).
-include("cvss_v1.hrl").
-type av() :: local | remote.
-type ac() :: high | low.
-type au() :: required | not_required.
-type impact() :: none | partial | complete.
-type impact_bias() :: normal | confidentiality | integrity | availability.
-type exploitability() :: unproven | proof_of_concept | functional | high.
-type remediation_level() :: official_fix | temporary_fix | workaround | unavailable.
-type report_confidence() :: unconfirmed | uncorroborated | confirmed.
-type cdp() :: none | low | medium | high.
-type td() :: none | low | medium | high.
-type cvss() :: #cvss_v1{}.
%%====================================================================
%% API
%%====================================================================
-doc """
Parse a CVSS 1.0 vector string.
Format: AV:R/AC:L/Au:NR/C:C/I:C/A:C
```erlang
> cvss_v1:parse(<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C">>).
{ok, #cvss_v1{av = remote, ac = low, au = not_required,
c = complete, i = complete, a = complete}}
> cvss_v1:parse(<<"AV:L/AC:H/Au:R/C:P/I:P/A:N">>).
{ok, #cvss_v1{av = local, ac = high, au = required,
c = partial, i = partial, a = none}}
> cvss_v1:parse(<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C/E:H/RL:U/RC:C">>).
{ok, #cvss_v1{av = remote, ac = low, au = not_required,
c = complete, i = complete, a = complete,
e = high, rl = unavailable, rc = confirmed}}
> cvss_v1:parse(<<"AV:X/AC:L/Au:NR/C:C/I:C/A:C">>).
{error, {invalid_metric, <<"AV">>, <<"X">>}}
> cvss_v1:parse(<<"AC:L/Au:NR/C:C/I:C/A:C">>).
{error, {missing_required_metric, av}}
```
""".
-spec parse(binary()) -> {ok, cvss_v1:cvss()} | {error, cvss:parse_error()}.
parse(Vector) when is_binary(Vector) ->
case cvss_common:parse_metrics(binary:split(Vector, <<"/">>, [global]), fun parse_metric/2) of
{ok, Metrics} ->
build_record(Metrics);
{error, _} = Error ->
Error
end.
-doc """
Compose a CVSS 1.0 record into a vector string.
```erlang
> iolist_to_binary(cvss_v1:compose(#cvss_v1{av = remote, ac = low, au = not_required,
c = complete, i = complete, a = complete,
ib = normal})).
<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C">>
> iolist_to_binary(cvss_v1:compose(#cvss_v1{av = remote, ac = low, au = not_required,
c = complete, i = complete, a = complete,
ib = normal,
e = high, rl = unavailable, rc = confirmed})).
<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C/E:H/RL:U/RC:C">>
```
""".
-spec compose(cvss_v1:cvss()) -> iolist().
compose(#cvss_v1{} = Cvss) ->
Base = [
<<"AV:">>,
encode_av(Cvss#cvss_v1.av),
<<"/AC:">>,
encode_ac(Cvss#cvss_v1.ac),
<<"/Au:">>,
encode_au(Cvss#cvss_v1.au),
<<"/C:">>,
encode_impact(Cvss#cvss_v1.c),
<<"/I:">>,
encode_impact(Cvss#cvss_v1.i),
<<"/A:">>,
encode_impact(Cvss#cvss_v1.a)
],
ImpactBias =
case Cvss#cvss_v1.ib of
normal -> [];
_ -> [<<"/IB:">>, encode_impact_bias(Cvss#cvss_v1.ib)]
end,
Temporal = cvss_common:encode_optional(
[
{<<"E">>, Cvss#cvss_v1.e, fun encode_exploitability/1},
{<<"RL">>, Cvss#cvss_v1.rl, fun encode_remediation_level/1},
{<<"RC">>, Cvss#cvss_v1.rc, fun encode_report_confidence/1}
],
[undefined]
),
Environmental = cvss_common:encode_optional(
[
{<<"CDP">>, Cvss#cvss_v1.cdp, fun encode_cdp/1},
{<<"TD">>, Cvss#cvss_v1.td, fun encode_td/1}
],
[undefined]
),
[Base, ImpactBias, Temporal, Environmental].
-doc """
Check whether a CVSS 1.0 value is valid.
Accepts either a vector string or a parsed record.
```erlang
> cvss_v1:valid(<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C">>).
true
> cvss_v1:valid(#cvss_v1{av = remote, ac = low, au = not_required,
c = complete, i = complete, a = complete,
ib = normal}).
true
> cvss_v1:valid(<<"AV:X/AC:L/Au:NR/C:C/I:C/A:C">>).
false
```
""".
-spec valid(iodata() | cvss_v1:cvss()) -> boolean().
valid(#cvss_v1{av = AV, ac = AC, au = Au, c = C, i = I, a = A, ib = IB} = Cvss) ->
ValidBase =
lists:member(AV, [local, remote]) andalso
lists:member(AC, [high, low]) andalso
lists:member(Au, [required, not_required]) andalso
lists:member(C, [none, partial, complete]) andalso
lists:member(I, [none, partial, complete]) andalso
lists:member(A, [none, partial, complete]) andalso
lists:member(IB, [normal, confidentiality, integrity, availability]),
ValidTemporal =
cvss_common:valid_optional(Cvss#cvss_v1.e, [unproven, proof_of_concept, functional, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v1.rl, [
official_fix, temporary_fix, workaround, unavailable
]) andalso
cvss_common:valid_optional(Cvss#cvss_v1.rc, [unconfirmed, uncorroborated, confirmed]),
ValidEnvironmental =
cvss_common:valid_optional(Cvss#cvss_v1.cdp, [none, low, medium, high]) andalso
cvss_common:valid_optional(Cvss#cvss_v1.td, [none, low, medium, high]),
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 1.0 score.
Returns the most relevant score: Environmental > Temporal > Base.
```erlang
> {ok, Cvss} = cvss_v1:parse(<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C">>).
> cvss_v1:score(Cvss).
10.0
> {ok, Cvss2} = cvss_v1:parse(<<"AV:L/AC:H/Au:NR/C:C/I:C/A:C">>).
> cvss_v1:score(Cvss2).
5.6
> {ok, Cvss3} = cvss_v1:parse(<<"AV:L/AC:H/Au:R/C:N/I:N/A:N">>).
> cvss_v1:score(Cvss3).
0.0
```
""".
-spec score(cvss_v1:cvss()) -> cvss:score().
score(#cvss_v1{} = 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 1.0 Base Score.
```erlang
> {ok, Cvss} = cvss_v1:parse(<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C">>).
> cvss_v1:base_score(Cvss).
10.0
```
""".
-spec base_score(cvss_v1:cvss()) -> cvss:score().
base_score(#cvss_v1{} = Cvss) ->
cvss_common:to_float(calculate_base_score(Cvss)).
-doc """
Calculate the CVSS 1.0 Temporal Score. Returns the Base Score if no temporal metrics are present.
```erlang
> {ok, Cvss} = cvss_v1:parse(<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C/E:U/RL:OF/RC:UC">>).
> cvss_v1:temporal_score(Cvss).
6.7
```
""".
-spec temporal_score(cvss_v1:cvss()) -> cvss:score().
temporal_score(#cvss_v1{} = Cvss) ->
cvss_common:to_float(calculate_temporal_score(Cvss, calculate_base_score(Cvss))).
-doc """
Calculate the CVSS 1.0 Environmental Score. Returns the Temporal Score if no environmental metrics are present.
```erlang
> {ok, Cvss} = cvss_v1:parse(<<"AV:R/AC:L/Au:NR/C:C/I:C/A:C/CDP:L/TD:L">>).
> cvss_v1:environmental_score(Cvss).
2.5
```
""".
-spec environmental_score(cvss_v1:cvss()) -> cvss:score().
environmental_score(#cvss_v1{} = 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">>, <<"R">>) -> {ok, {av, remote}};
parse_metric(<<"AC">>, <<"H">>) -> {ok, {ac, high}};
parse_metric(<<"AC">>, <<"L">>) -> {ok, {ac, low}};
parse_metric(<<"Au">>, <<"R">>) -> {ok, {au, required}};
parse_metric(<<"Au">>, <<"NR">>) -> {ok, {au, not_required}};
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(<<"IB">>, <<"N">>) -> {ok, {ib, normal}};
parse_metric(<<"IB">>, <<"C">>) -> {ok, {ib, confidentiality}};
parse_metric(<<"IB">>, <<"I">>) -> {ok, {ib, integrity}};
parse_metric(<<"IB">>, <<"A">>) -> {ok, {ib, availability}};
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(<<"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(<<"RC">>, <<"UC">>) -> {ok, {rc, unconfirmed}};
parse_metric(<<"RC">>, <<"UR">>) -> {ok, {rc, uncorroborated}};
parse_metric(<<"RC">>, <<"C">>) -> {ok, {rc, confirmed}};
parse_metric(<<"CDP">>, <<"N">>) -> {ok, {cdp, none}};
parse_metric(<<"CDP">>, <<"L">>) -> {ok, {cdp, low}};
parse_metric(<<"CDP">>, <<"M">>) -> {ok, {cdp, medium}};
parse_metric(<<"CDP">>, <<"H">>) -> {ok, {cdp, high}};
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(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}}.
build_record(Metrics) ->
Required = [av, ac, au, c, i, a],
case cvss_common:check_required(Required, Metrics) of
ok ->
{ok, #cvss_v1{
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),
ib = maps:get(ib, Metrics, normal),
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)
}};
{error, _} = Error ->
Error
end.
%%====================================================================
%% Internal - Encoding
%%====================================================================
encode_av(local) -> <<"L">>;
encode_av(remote) -> <<"R">>.
encode_ac(high) -> <<"H">>;
encode_ac(low) -> <<"L">>.
encode_au(required) -> <<"R">>;
encode_au(not_required) -> <<"NR">>.
encode_impact(none) -> <<"N">>;
encode_impact(partial) -> <<"P">>;
encode_impact(complete) -> <<"C">>.
encode_impact_bias(normal) -> <<"N">>;
encode_impact_bias(confidentiality) -> <<"C">>;
encode_impact_bias(integrity) -> <<"I">>;
encode_impact_bias(availability) -> <<"A">>.
encode_exploitability(unproven) -> <<"U">>;
encode_exploitability(proof_of_concept) -> <<"POC">>;
encode_exploitability(functional) -> <<"F">>;
encode_exploitability(high) -> <<"H">>.
encode_remediation_level(official_fix) -> <<"OF">>;
encode_remediation_level(temporary_fix) -> <<"TF">>;
encode_remediation_level(workaround) -> <<"W">>;
encode_remediation_level(unavailable) -> <<"U">>.
encode_report_confidence(unconfirmed) -> <<"UC">>;
encode_report_confidence(uncorroborated) -> <<"UR">>;
encode_report_confidence(confirmed) -> <<"C">>.
encode_cdp(none) -> <<"N">>;
encode_cdp(low) -> <<"L">>;
encode_cdp(medium) -> <<"M">>;
encode_cdp(high) -> <<"H">>.
encode_td(none) -> <<"N">>;
encode_td(low) -> <<"L">>;
encode_td(medium) -> <<"M">>;
encode_td(high) -> <<"H">>.
%%====================================================================
%% Internal - Scoring
%%====================================================================
-include("internal/decimal.hrl").
%% Coefficients from CVSS 1.0 spec (decimal values)
av_coeff(local) -> ?D(<<"0.7">>);
av_coeff(remote) -> ?D(1).
ac_coeff(high) -> ?D(<<"0.8">>);
ac_coeff(low) -> ?D(1).
au_coeff(required) -> ?D(<<"0.6">>);
au_coeff(not_required) -> ?D(1).
impact_coeff(none) -> ?D(0);
impact_coeff(partial) -> ?D(<<"0.7">>);
impact_coeff(complete) -> ?D(1).
%% Impact bias coefficients
impact_bias(normal) -> {?D(<<"0.333">>), ?D(<<"0.333">>), ?D(<<"0.333">>)};
impact_bias(confidentiality) -> {?D(<<"0.5">>), ?D(<<"0.25">>), ?D(<<"0.25">>)};
impact_bias(integrity) -> {?D(<<"0.25">>), ?D(<<"0.5">>), ?D(<<"0.25">>)};
impact_bias(availability) -> {?D(<<"0.25">>), ?D(<<"0.25">>), ?D(<<"0.5">>)}.
exploitability_coeff(undefined) -> ?D(1);
exploitability_coeff(unproven) -> ?D(<<"0.85">>);
exploitability_coeff(proof_of_concept) -> ?D(<<"0.9">>);
exploitability_coeff(functional) -> ?D(<<"0.95">>);
exploitability_coeff(high) -> ?D(1).
remediation_level_coeff(undefined) -> ?D(1);
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) -> ?D(1).
report_confidence_coeff(undefined) -> ?D(1);
report_confidence_coeff(unconfirmed) -> ?D(<<"0.9">>);
report_confidence_coeff(uncorroborated) -> ?D(<<"0.95">>);
report_confidence_coeff(confirmed) -> ?D(1).
cdp_coeff(undefined) -> ?D(0);
cdp_coeff(none) -> ?D(0);
cdp_coeff(low) -> ?D(<<"0.1">>);
cdp_coeff(medium) -> ?D(<<"0.3">>);
cdp_coeff(high) -> ?D(<<"0.5">>).
td_coeff(undefined) -> ?D(1);
td_coeff(none) -> ?D(0);
td_coeff(low) -> ?D(<<"0.25">>);
td_coeff(medium) -> ?D(<<"0.75">>);
td_coeff(high) -> ?D(1).
calculate_base_score(#cvss_v1{av = AV, ac = AC, au = Au, c = C, i = I, a = A, ib = IB}) ->
{ConfBias, IntegBias, AvailBias} = impact_bias(IB),
ImpactScore =
decimal:add(
decimal:add(
decimal:mult(impact_coeff(C), ConfBias),
decimal:mult(impact_coeff(I), IntegBias)
),
decimal:mult(impact_coeff(A), AvailBias)
),
Score = decimal:mult(
decimal:mult(
decimal:mult(
decimal:mult(?D(10), av_coeff(AV)),
ac_coeff(AC)
),
au_coeff(Au)
),
ImpactScore
),
cvss_common:round_v1_v2(Score).
calculate_temporal_score(#cvss_v1{e = E, rl = RL, rc = RC}, BaseScore) ->
case {E, RL, RC} of
{undefined, undefined, undefined} ->
BaseScore;
_ ->
Score = decimal:mult(
decimal:mult(
decimal:mult(BaseScore, exploitability_coeff(E)),
remediation_level_coeff(RL)
),
report_confidence_coeff(RC)
),
cvss_common:round_v1_v2(Score)
end.
calculate_environmental_score(#cvss_v1{cdp = CDP, td = TD}, TemporalScore) ->
case {CDP, TD} of
{undefined, undefined} ->
TemporalScore;
_ ->
Score = decimal:mult(
decimal:add(
TemporalScore,
decimal:mult(
decimal:sub(?D(10), TemporalScore),
cdp_coeff(CDP)
)
),
td_coeff(TD)
),
cvss_common:round_v1_v2(Score)
end.