diff --git a/CMakeLists.txt b/CMakeLists.txt index c1b749cd3..772712ac4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,7 +103,7 @@ include (CPack) set (GVMD_DATABASE_VERSION 255) -set (GVMD_SCAP_DATABASE_VERSION 20) +set (GVMD_SCAP_DATABASE_VERSION 21) set (GVMD_CERT_DATABASE_VERSION 8) diff --git a/src/gmp.c b/src/gmp.c index 3d099c684..11dfcd8a6 100644 --- a/src/gmp.c +++ b/src/gmp.c @@ -13403,6 +13403,18 @@ handle_get_info (gmp_parser_t *gmp_parser, GError **error) cve_info_iterator_vector (&info), cve_info_iterator_description (&info), cve_info_iterator_products (&info)); + + if (cve_info_iterator_epss_score (&info) > 0.0) + { + xml_string_append (result, + "" + "%0.5f" + "%0.5f" + "", + cve_info_iterator_epss_score (&info), + cve_info_iterator_epss_percentile (&info)); + } + if (get_info_data->details == 1) { iterator_t nvts; diff --git a/src/manage.h b/src/manage.h index fecace472..4d5845e48 100644 --- a/src/manage.h +++ b/src/manage.h @@ -3331,6 +3331,12 @@ cve_info_iterator_description (iterator_t*); const char* cve_info_iterator_products (iterator_t*); +double +cve_info_iterator_epss_score (iterator_t*); + +double +cve_info_iterator_epss_percentile (iterator_t*); + int init_cve_info_iterator (iterator_t*, get_data_t*, const char*); diff --git a/src/manage_pg.c b/src/manage_pg.c index c261f4ca9..851769370 100644 --- a/src/manage_pg.c +++ b/src/manage_pg.c @@ -3422,6 +3422,12 @@ manage_db_init (const gchar *name) " (cve INTEGER," " cpe INTEGER);"); + sql ("CREATE TABLE scap2.epss_scores" + " (cve TEXT," + " epss DOUBLE PRECISION," + " percentile DOUBLE PRECISION);"); + + /* Init tables. */ sql ("INSERT INTO scap2.meta (name, value)" @@ -3466,6 +3472,10 @@ manage_db_add_constraints (const gchar *name) " ADD UNIQUE (cve, cpe)," " ADD FOREIGN KEY(cve) REFERENCES cves(id)," " ADD FOREIGN KEY(cpe) REFERENCES cpes(id);"); + + sql ("ALTER TABLE scap2.epss_scores" + " ALTER cve SET NOT NULL," + " ADD UNIQUE (cve);"); } else { @@ -3512,6 +3522,9 @@ manage_db_init_indexes (const gchar *name) " ON scap2.affected_products (cpe);"); sql ("CREATE INDEX afp_cve_idx" " ON scap2.affected_products (cve);"); + + sql ("CREATE INDEX epss_scores_by_cve" + " ON scap2.epss_scores (cve);"); } else { diff --git a/src/manage_sql_secinfo.c b/src/manage_sql_secinfo.c index 0f7543776..25f1a6794 100644 --- a/src/manage_sql_secinfo.c +++ b/src/manage_sql_secinfo.c @@ -48,6 +48,7 @@ #include #include +#include #include #include #include @@ -72,6 +73,11 @@ */ static int secinfo_commit_size = SECINFO_COMMIT_SIZE_DEFAULT; +/** + * @brief Maximum number of rows in a EPSS INSERT. + */ +#define EPSS_MAX_CHUNK_SIZE 10000 + /* Headers. */ @@ -650,7 +656,9 @@ cve_info_count (const get_data_t *get) { static const char *filter_columns[] = CVE_INFO_ITERATOR_FILTER_COLUMNS; static column_t columns[] = CVE_INFO_ITERATOR_COLUMNS; - return count ("cve", get, columns, NULL, filter_columns, 0, 0, 0, FALSE); + return count ("cve", get, columns, NULL, filter_columns, 0, + " LEFT JOIN epss_scores ON cve = cves.uuid", + 0, FALSE); } /** @@ -696,7 +704,7 @@ init_cve_info_iterator (iterator_t* iterator, get_data_t *get, const char *name) NULL, filter_columns, 0, - NULL, + " LEFT JOIN epss_scores ON cve = cves.uuid", clause, FALSE); g_free (clause); @@ -769,6 +777,38 @@ DEF_ACCESS (cve_info_iterator_severity, GET_ITERATOR_COLUMN_COUNT + 2); */ DEF_ACCESS (cve_info_iterator_description, GET_ITERATOR_COLUMN_COUNT + 3); +/** + * @brief Get the EPSS score for this CVE. + * + * @param[in] iterator Iterator. + * + * @return The EPSS score of this CVE, or 0.0 if iteration is + * complete. + */ +double +cve_info_iterator_epss_score (iterator_t *iterator) +{ + if (iterator->done) + return 0.0; + return iterator_double (iterator, GET_ITERATOR_COLUMN_COUNT + 5); +} + +/** + * @brief Get the EPSS percentile for this CVE. + * + * @param[in] iterator Iterator. + * + * @return The EPSS percentile of this CVE, or 0.0 if iteration is + * complete. + */ +double +cve_info_iterator_epss_percentile (iterator_t *iterator) +{ + if (iterator->done) + return 0.0; + return iterator_double (iterator, GET_ITERATOR_COLUMN_COUNT + 6); +} + /* CERT-Bund data. */ @@ -2762,6 +2802,175 @@ update_scap_cves () return 0; } +/** + * @brief Adds a EPSS score entry to an SQL inserts buffer. + * + * @param[in] inserts The SQL inserts buffer to add to. + * @param[in] cve The CVE the epss score and percentile apply to. + * @param[in] epss The EPSS score to add. + * @param[in] percentile The EPSS percentile to add. + */ +static void +insert_epss_score_entry (inserts_t *inserts, const char *cve, + double epss, double percentile) +{ + gchar *quoted_cve; + int first = inserts_check_size (inserts); + + quoted_cve = sql_quote (cve); + g_string_append_printf (inserts->statement, + "%s ('%s', %lf, %lf)", + first ? "" : ",", + quoted_cve, + epss, + percentile); + g_free (quoted_cve); + + inserts->current_chunk_size++; +} + +/** + * @brief Checks a failure condition for validating EPSS JSON. + */ +#define EPSS_JSON_FAIL_IF(failure_condition, error_message) \ +if (failure_condition) { \ + g_warning ("%s: %s", __func__, error_message); \ + goto fail_insert; \ +} + +/** + * @brief Updates the base EPSS scores table in the SCAP database. + * + * @return 0 success, -1 error. + */ +static int +update_epss_scores () +{ + GError *error = NULL; + gchar *latest_json_path; + gchar *file_contents = NULL; + cJSON *parsed, *list_item; + inserts_t inserts; + + latest_json_path = g_build_filename (GVM_SCAP_DATA_DIR, "epss-latest.json", + NULL); + + if (! g_file_get_contents (latest_json_path, &file_contents, NULL, &error)) + { + int ret; + if (error->code == G_FILE_ERROR_NOENT) + { + g_info ("%s: EPSS scores file '%s' not found", + __func__, latest_json_path); + ret = 0; + } + else + { + g_warning ("%s: Error loading EPSS scores file: %s", + __func__, error->message); + ret = -1; + } + g_error_free (error); + g_free (latest_json_path); + return ret; + } + + g_info ("Updating EPSS scores from %s", latest_json_path); + g_free (latest_json_path); + + parsed = cJSON_Parse (file_contents); + g_free (file_contents); + + if (parsed == NULL) + { + g_warning ("%s: EPSS scores file is not valid JSON", __func__); + return -1; + } + + if (! cJSON_IsArray (parsed)) + { + g_warning ("%s: EPSS scores file is not a JSON array", __func__); + cJSON_Delete (parsed); + return -1; + } + + sql_begin_immediate (); + inserts_init (&inserts, + EPSS_MAX_CHUNK_SIZE, + setting_secinfo_sql_buffer_threshold_bytes (), + "INSERT INTO scap2.epss_scores" + " (cve, epss, percentile)" + " VALUES ", + " ON CONFLICT (cve) DO NOTHING"); + + cJSON_ArrayForEach (list_item, parsed) + { + cJSON *cve_json, *epss_json, *percentile_json; + + EPSS_JSON_FAIL_IF (! cJSON_IsObject (list_item), + "Unexpected non-object item in EPSS scores file") + + cve_json = cJSON_GetObjectItem (list_item, "cve"); + epss_json = cJSON_GetObjectItem (list_item, "epss"); + percentile_json = cJSON_GetObjectItem (list_item, "percentile"); + + EPSS_JSON_FAIL_IF (cve_json == NULL, + "Item missing mandatory 'cve' field"); + + EPSS_JSON_FAIL_IF (epss_json == NULL, + "Item missing mandatory 'epss' field"); + + EPSS_JSON_FAIL_IF (percentile_json == NULL, + "Item missing mandatory 'percentile' field"); + + EPSS_JSON_FAIL_IF (! cJSON_IsString (cve_json), + "Field 'cve' in item is not a string"); + + EPSS_JSON_FAIL_IF (! cJSON_IsNumber(epss_json), + "Field 'epss' in item is not a number"); + + EPSS_JSON_FAIL_IF (! cJSON_IsNumber(percentile_json), + "Field 'percentile' in item is not a number"); + + insert_epss_score_entry (&inserts, + cve_json->valuestring, + epss_json->valuedouble, + percentile_json->valuedouble); + } + + inserts_run (&inserts, TRUE); + sql_commit (); + cJSON_Delete (parsed); + + return 0; + +fail_insert: + inserts_free (&inserts); + sql_rollback (); + char *printed_item = cJSON_Print (list_item); + g_message ("%s: invalid item: %s", __func__, printed_item); + free (printed_item); + cJSON_Delete (parsed); + return -1; +} + +/** + * @brief Update EPSS data as supplement to SCAP CVEs. + * + * Assume that the databases are attached. + * + * @return 0 success, -1 error. + */ +static int +update_scap_epss () +{ + if (update_epss_scores ()) + return -1; + + return 0; +} + + /* CERT and SCAP update. */ @@ -3672,6 +3881,14 @@ update_scap (gboolean reset_scap_db) g_debug ("%s: updating user defined data", __func__); + g_debug ("%s: update epss", __func__); + setproctitle ("Syncing SCAP: Updating EPSS scores"); + + if (update_scap_epss () == -1) + { + abort_scap_update (); + return -1; + } /* Do calculations that need all data. */ diff --git a/src/manage_sql_secinfo.h b/src/manage_sql_secinfo.h index bbeb1252b..d4ef087eb 100644 --- a/src/manage_sql_secinfo.h +++ b/src/manage_sql_secinfo.h @@ -76,7 +76,8 @@ */ #define CVE_INFO_ITERATOR_FILTER_COLUMNS \ { GET_ITERATOR_FILTER_COLUMNS, "cvss_vector", "products", \ - "description", "published", "severity", NULL } + "description", "published", "severity", "epss_score", \ + "epss_percentile", NULL } /** * @brief CVE iterator columns. @@ -91,6 +92,8 @@ { "severity", NULL, KEYWORD_TYPE_DOUBLE }, \ { "description", NULL, KEYWORD_TYPE_STRING }, \ { "creation_time", "published", KEYWORD_TYPE_INTEGER }, \ + { "coalesce (epss, 0.0)", "epss_score", KEYWORD_TYPE_DOUBLE }, \ + { "coalesce (percentile, 0.0)", "epss_percentile", KEYWORD_TYPE_DOUBLE }, \ { NULL, NULL, KEYWORD_TYPE_UNKNOWN } \ } diff --git a/src/schema_formats/XML/GMP.xml.in b/src/schema_formats/XML/GMP.xml.in index 99cdb1050..5dd097f66 100644 --- a/src/schema_formats/XML/GMP.xml.in +++ b/src/schema_formats/XML/GMP.xml.in @@ -12529,6 +12529,7 @@ END:VCALENDAR cvss_vector description products + epss nvts cert raw_data @@ -12562,6 +12563,28 @@ END:VCALENDAR text + + epss + Exploit Prediction Scoring System (EPSS) info if available + + score + percentile + + + score + EPSS score of the CVE + + decimal + + + + percentile + EPSS percentile of the CVE + + decimal + + + nvts NVTs addressing this CVE. Only when details were requested