diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b984c1a..82b835c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [#410](https://github.com/greenbone/gvm-libs/pull/410) - Add multiple severities for nvti [#317](https://github.com/greenbone/gvm-libs/pull/317) - Add support for new OSP element for defining alive test methods via separate subelements. [#409](https://github.com/greenbone/gvm-libs/pull/409) +- Add v3 handling to get_cvss_score_from_base_metrics [#411](https://github.com/greenbone/gvm-libs/pull/411) - Add severity_date tag in epoch time format. [#412](https://github.com/greenbone/gvm-libs/pull/412) ### Changed diff --git a/CMakeLists.txt b/CMakeLists.txt index e66ac72b8..eaafa5418 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -224,7 +224,7 @@ if (BUILD_TESTS AND NOT SKIP_SRC) add_custom_target (tests DEPENDS array-test alivedetection-test boreas_error-test boreas_io-test - cli-test ping-test sniffer-test util-test networking-test + cli-test cvss-test ping-test sniffer-test util-test networking-test xmlutils-test version-test osp-test nvti-test hosts-test) endif (BUILD_TESTS AND NOT SKIP_SRC) diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index 972acd2ae..9bbb64137 100644 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -83,6 +83,16 @@ if (BUILD_TESTS) target_link_libraries (array-test ${CGREEN_LIBRARIES} ${GLIB_LDFLAGS} ${LINKER_HARDENING_FLAGS}) + add_executable (cvss-test + EXCLUDE_FROM_ALL + cvss_tests.c) + + add_test (cvss-test cvss-test) + + target_include_directories (cvss-test PRIVATE ${CGREEN_INCLUDE_DIRS}) + + target_link_libraries (cvss-test ${CGREEN_LIBRARIES} -lm ${GLIB_LDFLAGS} ${LINKER_HARDENING_FLAGS}) + add_executable (networking-test EXCLUDE_FROM_ALL networking_tests.c) diff --git a/base/cvss.c b/base/cvss.c index cf245871c..08bf3952e 100644 --- a/base/cvss.c +++ b/base/cvss.c @@ -21,9 +21,23 @@ * @file * @brief CVSS utility functions * - * This file contains utility functions for handling CVSS. - * Namels a calculator for the CVSS base score from a CVSS base - * vector. + * This file contains utility functions for handling CVSS v2 and v3. + * get_cvss_score_from_base_metrics calculates the CVSS base score from a CVSS + * base vector. + * + * CVSS v3.1: + * + * See equations at https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator and + * constants at https://www.first.org/cvss/v3.1/specification-document (section + * 7.4. Metric Values). + * + * CVSS v3.0: + * + * See equations at https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator and + * constants at https://www.first.org/cvss/v3.0/specification-document (section + * 8.4. Metric Levels). + * + * CVSS v2: * * The base equation is the foundation of CVSS scoring. The base equation is: * BaseScore6 @@ -63,8 +77,16 @@ */ #include +#include #include +/* Static Headers. */ + +static double +get_cvss_score_from_base_metrics_v3 (const char *); + +/* CVSS v2. */ + // clang-format off /** * @brief AccessVector (AV) Constants. @@ -344,11 +366,16 @@ get_cvss_score_from_base_metrics (const char *cvss_str) struct cvss cvss; char *token, *base_str, *base_metrics; - memset (&cvss, 0x00, sizeof (struct cvss)); - if (cvss_str == NULL) return -1.0; + if (g_str_has_prefix (cvss_str, "CVSS:3.1/") + || g_str_has_prefix (cvss_str, "CVSS:3.0/")) + return get_cvss_score_from_base_metrics_v3 (cvss_str + + strlen ("CVSS:3.X/")); + + memset (&cvss, 0x00, sizeof (struct cvss)); + base_str = base_metrics = g_strdup_printf ("%s/", cvss_str); while ((token = strchr (base_metrics, '/')) != NULL) @@ -386,3 +413,197 @@ get_cvss_score_from_base_metrics (const char *cvss_str) g_free (base_str); return (double) -1; } + +/* CVSS v3. */ + +/** + * @brief Round final score as in spec. + * + * @param cvss CVSS score. + * + * @return Rounded score. + */ +static double +roundup (double cvss) +{ + int trim; + + /* "Roundup returns the smallest number, specified to 1 decimal place, + * that is equal to or higher than its input. For example, Roundup (4.02) + * returns 4.1; and Roundup (4.00) returns 4.0." */ + + /* 3.020000001 => 3.1 */ + /* 3.000000001 => 3.0 */ + + trim = round (cvss * 100000); + if ((trim % 10000) == 0) + return trim / 100000; + return (floor (trim / 10000) + 1) / 10.0; +} + +/** + * @brief Get impact. + * + * @param value Metric value. + * + * @return Impact. + */ +static double +v3_impact (const char *value) +{ + if (strcasecmp (value, "N") == 0) + return 0.0; + if (strcasecmp (value, "L") == 0) + return 0.22; + if (strcasecmp (value, "H") == 0) + return 0.56; + return -1.0; +} + +/** + * @brief Calculate CVSS Score. + * + * @param cvss_str Vector from which to compute score, without prefix. + * + * @return CVSS score, or -1 on error. + */ +static double +get_cvss_score_from_base_metrics_v3 (const char *cvss_str) +{ + gchar **split, **point; + int scope_changed; + double impact_conf, impact_integ, impact_avail; + double vector, complexity, privilege, user; + double isc_base, impact, exploitability, base; + + /* https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator + * https://www.first.org/cvss/v3.1/specification-document + * https://www.first.org/cvss/v3.0/specification-document */ + + scope_changed = -1; + impact_conf = -1.0; + impact_integ = -1.0; + impact_avail = -1.0; + vector = -1.0; + complexity = -1.0; + privilege = -1.0; + user = -1.0; + + /* AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N */ + + split = g_strsplit (cvss_str, "/", 0); + point = split; + while (*point) + { + /* Scope. */ + if (strncasecmp ("S:", *point, 2) == 0) + { + if (strcasecmp (*point + 2, "U") == 0) + scope_changed = 0; + else if (strcasecmp (*point + 2, "C") == 0) + scope_changed = 1; + } + + /* Confidentiality. */ + if (strncasecmp ("C:", *point, 2) == 0) + impact_conf = v3_impact (*point + 2); + + /* Integrity. */ + if (strncasecmp ("I:", *point, 2) == 0) + impact_integ = v3_impact (*point + 2); + + /* Availability. */ + if (strncasecmp ("A:", *point, 2) == 0) + impact_avail = v3_impact (*point + 2); + + /* Attack Vector. */ + if (strncasecmp ("AV:", *point, 3) == 0) + { + if (strcasecmp (*point + 3, "N") == 0) + vector = 0.85; + else if (strcasecmp (*point + 3, "A") == 0) + vector = 0.62; + else if (strcasecmp (*point + 3, "L") == 0) + vector = 0.55; + else if (strcasecmp (*point + 3, "P") == 0) + vector = 0.2; + } + + /* Attack Complexity. */ + if (strncasecmp ("AC:", *point, 3) == 0) + { + if (strcasecmp (*point + 3, "L") == 0) + complexity = 0.77; + else if (strcasecmp (*point + 3, "H") == 0) + complexity = 0.44; + } + + /* Privileges Required. */ + if (strncasecmp ("PR:", *point, 3) == 0) + { + if (strcasecmp (*point + 3, "N") == 0) + privilege = 0.85; + else if (strcasecmp (*point + 3, "L") == 0) + privilege = 0.62; + else if (strcasecmp (*point + 3, "H") == 0) + privilege = 0.27; + else + privilege = -1.0; + } + + /* User Interaction. */ + if (strncasecmp ("UI:", *point, 3) == 0) + { + if (strcasecmp (*point + 3, "N") == 0) + user = 0.85; + else if (strcasecmp (*point + 3, "R") == 0) + user = 0.62; + } + + point++; + } + + g_strfreev (split); + + /* All of the base metrics are required. */ + + if (scope_changed == -1 || impact_conf == -1.0 || impact_integ == -1.0 + || impact_avail == -1.0 || vector == -1.0 || complexity == -1.0 + || privilege == -1.0 || user == -1.0) + return -1.0; + + /* Privileges Required has a special case for S:C. */ + + if (scope_changed && privilege == 0.62) + privilege = 0.68; + else if (scope_changed && privilege == 0.27) + privilege = 0.5; + + /* Impact. */ + + isc_base = 1 - ((1 - impact_conf) * (1 - impact_integ) * (1 - impact_avail)); + + if (scope_changed) + impact = 7.52 * (isc_base - 0.029) - 3.25 * pow ((isc_base - 0.02), 15); + else + impact = 6.42 * isc_base; + + if (impact <= 0) + return 0.0; + + /* Exploitability. */ + + exploitability = 8.22 * vector * complexity * privilege * user; + + /* Final. */ + + if (scope_changed) + base = 1.08 * (impact + exploitability); + else + base = impact + exploitability; + + if (base > 10.0) + return 10.0; + + return roundup (base); +} diff --git a/base/cvss_tests.c b/base/cvss_tests.c new file mode 100644 index 000000000..35fcf0541 --- /dev/null +++ b/base/cvss_tests.c @@ -0,0 +1,151 @@ +/* Copyright (C) 2009-2020 Greenbone Networks GmbH + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "cvss.c" + +#include +#include +#include + +Describe (cvss); +BeforeEach (cvss) +{ +} +AfterEach (cvss) +{ +} + +/* roundup */ + +Ensure (cvss, roundup_succeeds) +{ + assert_that_double (roundup (0.0), is_equal_to_double (0.0)); + assert_that_double (roundup (1.0), is_equal_to_double (1.0)); + + assert_that_double (roundup (1.01), is_equal_to_double (1.1)); + assert_that_double (roundup (0.99), is_equal_to_double (1.0)); + + assert_that_double (roundup (1.000001), is_equal_to_double (1.0)); +} + +/* get_cvss_score_from_base_metrics */ + +#define CHECK(vector, score) \ + assert_that_double (nearest (get_cvss_score_from_base_metrics (vector)), \ + is_equal_to_double (score)) + +Ensure (cvss, get_cvss_score_from_base_metrics_null) +{ + assert_that (get_cvss_score_from_base_metrics (NULL), is_equal_to (-1.0)); +} + +double +nearest (double cvss) +{ + return round (cvss * 10) / 10; +} + +Ensure (cvss, get_cvss_score_from_base_metrics_succeeds) +{ + CHECK ("AV:N/AC:L/Au:N/C:N/I:N/A:C", 7.8); + CHECK ("AV:N/AC:L/Au:N/C:N/I:N/A:P", 5.0); + CHECK ("AV:N/AC:M/Au:N/C:N/I:N/A:P", 4.3); + CHECK ("AV:N/AC:L/Au:N/C:N/I:N/A:N", 0.0); +} + +Ensure (cvss, get_cvss_score_from_base_metrics_succeeds_v3) +{ + CHECK ("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N", 10.0); + CHECK ("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N", 8.2); + CHECK ("CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N", 5.4); + CHECK ("CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:L", 3.5); + CHECK ("CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:N/I:L/A:N", 2.4); + CHECK ("CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N", 0.0); + + CHECK ("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", 7.5); + CHECK ("CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H", 5.5); + CHECK ("CVSS:3.0/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N", 2.5); + CHECK ("CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N", 0.0); + + /* Trailing separator. */ + CHECK ("CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N/", 0.0); + + /* We support any case in metrics. */ + CHECK ("CVSS:3.1/av:n/ac:l/pr:n/ui:n/s:u/c:h/i:l/a:n", 8.2); +} + +Ensure (cvss, get_cvss_score_from_base_metrics_fails) +{ + CHECK ("", -1.0); + CHECK ("xxx", -1.0); + CHECK ("//////", -1.0); + + /* Unsupported version. */ + CHECK ("CVSS:3.2/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N", -1.0); + + /* Metric name errors. */ + CHECK ("CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/X:N", -1.0); + CHECK ("CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/X:L/A:N", -1.0); + CHECK ("CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/X:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.1/AV:L/AC:H/PR:L/UI:N/X:U/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.1/AV:L/AC:H/PR:L/UX:N/S:U/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.1/AV:L/AC:H/PX:L/UI:N/S:U/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.1/AV:L/XC:H/PR:L/UI:N/S:U/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.1/AXV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N", -1.0); + + /* Leading separator. */ + CHECK ("/CVSS:3.1/AXV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N", -1.0); + + /* Garbage at end of metric value. */ + CHECK ("CVSS:3.0/AV:LX/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.0/AV:L/AC:HX/PR:L/UI:N/S:U/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.0/AV:L/AC:H/PR:LX/UI:N/S:U/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.0/AV:L/AC:H/PR:L/UI:NX/S:U/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.0/AV:L/AC:H/PR:L/UI:N/S:UX/C:N/I:L/A:N", -1.0); + CHECK ("CVSS:3.0/AV:L/AC:H/PR:L/UI:N/S:U/C:NX/I:L/A:N", -1.0); + CHECK ("CVSS:3.0/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:LX/A:N", -1.0); + CHECK ("CVSS:3.0/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:NX", -1.0); + + /* Version must be uppercase. */ + CHECK ("cvss:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H", -1.0); +} + +/* Test suite. */ + +int +main (int argc, char **argv) +{ + TestSuite *suite; + + suite = create_test_suite (); + + add_test_with_context (suite, cvss, roundup_succeeds); + + add_test_with_context (suite, cvss, get_cvss_score_from_base_metrics_null); + add_test_with_context (suite, cvss, + get_cvss_score_from_base_metrics_succeeds); + add_test_with_context (suite, cvss, get_cvss_score_from_base_metrics_fails); + add_test_with_context (suite, cvss, + get_cvss_score_from_base_metrics_succeeds_v3); + + if (argc > 1) + return run_single_test (suite, argv[1], create_text_reporter ()); + + return run_test_suite (suite, create_text_reporter ()); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9bcbf90d3..2bc9d2f0d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,7 +23,7 @@ include_directories (${GLIB_INCLUDE_DIRS}) if (BUILD_SHARED) add_executable (test-hosts test-hosts.c) set_target_properties (test-hosts PROPERTIES LINKER_LANGUAGE C) - target_link_libraries (test-hosts ${LIBGVM_BASE_NAME} ${GLIB_LDFLAGS}) + target_link_libraries (test-hosts ${LIBGVM_BASE_NAME} -lm ${GLIB_LDFLAGS}) endif (BUILD_SHARED) ## End