cve: Upgrade to JSON 2.0 feeds.

Fixes guix/guix#2213.  The 1.1-formatted-data is no longer available
from NIST.

* guix/cve.scm (string->date*, <cve-item>,
reference-data->cve-configuration, cpe-match->cve-configuration,
configuration-data->cve-configurations, json->cve-items,
yearly-feed-uri, cve-item->vulnerability): Upgrade to JSON 2.0 feeds
schema.
(<cve>): Remove uneeded record.
* tests/cve-sample.json: Update them. Remove CVE-2019-0005 (no value
added, lots of lines).
* tests/cve.scm (%expected-vulnerabilities): Upgrade accordingly.
(json->cve-items, vulnerabilities->lookup-proc tests): Update accordingly.

Signed-off-by: Ludovic Courtès <ludo@gnu.org>
This commit is contained in:
Nicolas Graves 2025-08-26 13:17:16 +02:00 committed by Ludovic Courtès
parent ad5e0fc720
commit d431f4620a
No known key found for this signature in database
GPG key ID: 090B11993D9AEBB5
3 changed files with 1773 additions and 1362 deletions

View file

@ -1,5 +1,6 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2015-2021 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2025 Nicolas Graves <ngraves@ngraves.fr>
;;;
;;; This file is part of GNU Guix.
;;;
@ -36,17 +37,11 @@
#:export (json->cve-items
cve-item?
cve-item-cve
cve-item-id
cve-item-configurations
cve-item-published-date
cve-item-last-modified-date
cve?
cve-id
cve-data-type
cve-data-format
cve-references
cve-reference?
cve-reference-url
cve-reference-tags
@ -68,28 +63,17 @@
;;; Code:
(define (string->date* str)
(string->date str "~Y-~m-~dT~H:~M~z"))
(string->date str "~Y-~m-~dT~H:~M:~S"))
(define-json-mapping <cve-item> cve-item cve-item?
json->cve-item
(cve cve-item-cve "cve" json->cve) ;<cve>
(configurations cve-item-configurations ;list of sexps
(id cve-item-id "id") ;string
(configurations cve-item-configurations ;list of sexps
"configurations" configuration-data->cve-configurations)
(published-date cve-item-published-date
"publishedDate" string->date*)
"published" string->date*)
(last-modified-date cve-item-last-modified-date
"lastModifiedDate" string->date*))
(define-json-mapping <cve> cve cve?
json->cve
(id cve-id "CVE_data_meta" ;string
(cut assoc-ref <> "ID"))
(data-type cve-data-type ;'CVE
"data_type" string->symbol)
(data-format cve-data-format ;'MITRE
"data_format" string->symbol)
(references cve-references ;list of <cve-reference>
"references" reference-data->cve-references))
"lastModified" string->date*))
(define-json-mapping <cve-reference> cve-reference cve-reference?
json->cve-reference
@ -97,12 +81,6 @@
(tags cve-reference-tags ;list of strings
"tags" vector->list))
(define (reference-data->cve-references alist)
(map json->cve-reference
;; Normally "reference_data" is always present but rejected CVEs such
;; as CVE-2020-10020 can lack it.
(vector->list (or (assoc-ref alist "reference_data") '#()))))
(define %cpe-package-rx
;; For applications: "cpe:2.3:a:VENDOR:PACKAGE:VERSION", or sometimes
;; "cpe:2.3:a:VENDOR:PACKAGE:VERSION:PATCH-LEVEL".
@ -132,15 +110,15 @@ Return three #f values if CPE does not look like an application CPE string."
(values #f #f #f))))
(define (cpe-match->cve-configuration alist)
"Convert ALIST, a \"cpe_match\" alist, into an sexp representing the package
"Convert ALIST, a \"cpeMatch\" alist, into an sexp representing the package
and versions matched. Return #f if ALIST doesn't correspond to an application
package."
(let ((cpe (assoc-ref alist "cpe23Uri"))
(let ((cpe (assoc-ref alist "criteria"))
(starti (assoc-ref alist "versionStartIncluding"))
(starte (assoc-ref alist "versionStartExcluding"))
(endi (assoc-ref alist "versionEndIncluding"))
(ende (assoc-ref alist "versionEndExcluding")))
;; Normally "cpe23Uri" is here in each "cpe_match" item, but CVE-2020-0534
;; Normally "criteria" is here in each "cpeMatch" item, but CVE-2020-0534
;; has a configuration that lacks it.
(and cpe
(let ((vendor package version (cpe->package-identifier cpe)))
@ -156,7 +134,7 @@ package."
(ende `(< ,ende))
(else version))))))))
(define (configuration-data->cve-configurations alist)
(define (configuration-data->cve-configurations vector)
"Given ALIST, a JSON dictionary for the baroque \"configurations\"
element found in CVEs, return an sexp such as (\"binutils\" (<
\"2.31\")) that represents matching configurations."
@ -165,10 +143,13 @@ element found in CVEs, return an sexp such as (\"binutils\" (<
("OR" 'or)
("AND" 'and)))
(define (maybe-vector->alist vector)
(vector->list (or (and (unspecified? vector) #()) vector #())))
(define (node->configuration node)
(let ((operator (string->operator (assoc-ref node "operator"))))
(cond
((assoc-ref node "cpe_match")
((assoc-ref node "cpeMatch")
=>
(lambda (matches)
(let ((matches (vector->list matches)))
@ -187,28 +168,31 @@ element found in CVEs, return an sexp such as (\"binutils\" (<
(else
#f))))
(let ((nodes (vector->list (assoc-ref alist "nodes"))))
(let* ((alist (maybe-vector->alist vector))
(nodes (if (null? alist)
'()
(maybe-vector->alist (assoc-ref (car alist) "nodes")))))
(filter-map node->configuration nodes)))
(define (json->cve-items json)
"Parse JSON, an input port or a string, and return a list of <cve-item>
records."
(let* ((alist (json->scm json))
(type (assoc-ref alist "CVE_data_type"))
(format (assoc-ref alist "CVE_data_format"))
(version (assoc-ref alist "CVE_data_version")))
(unless (equal? type "CVE")
(raise (condition (&message
(message "invalid CVE feed")))))
(unless (equal? format "MITRE")
(raise (formatted-message (G_ "unsupported CVE format: '~a'")
format)))
(unless (equal? version "4.0")
(raise (formatted-message (G_ "unsupported CVE data version: '~a'")
version)))
(let ((alist (json->scm json)))
(match (assoc-ref alist "format")
("NVD_CVE"
#t)
(format
(raise (formatted-message (G_ "unsupported CVE format: '~a'")
format))))
(match (assoc-ref alist "version")
("2.0"
#t)
(version
(raise (formatted-message (G_ "unsupported CVE data version: '~a'")
version))))
(map json->cve-item
(vector->list (assoc-ref alist "CVE_Items")))))
(map (compose json->cve-item (cut assoc-ref <> "cve"))
(vector->list (assoc-ref alist "vulnerabilities")))))
(define (version-matches? version sexp)
"Return true if VERSION, a string, matches SEXP."
@ -269,7 +253,7 @@ HIDDEN-VENDORS."
(define (yearly-feed-uri year)
"Return the URI for the CVE feed for YEAR."
(string->uri
(string-append "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-"
(string-append "https://nvd.nist.gov/feeds/json/cve/2.0/nvdcve-2.0-"
(number->string year) ".json.gz")))
(define %current-year-ttl
@ -352,14 +336,13 @@ matching versions."
"Return a <vulnerability> corresponding to ITEM, a <cve-item> record;
return #f if ITEM does not list any configuration or if it does not list
any \"a\" (application) configuration."
(let ((id (cve-id (cve-item-cve item))))
(match (cve-item-configurations item)
(() ;no configurations
#f)
((configs ...)
(vulnerability id
(merge-package-lists
(map cve-configuration->package-list configs)))))))
(match (cve-item-configurations item)
(() ;no configurations
#f)
((configs ...)
(vulnerability (cve-item-id item)
(merge-package-lists
(map cve-configuration->package-list configs))))))
(define (json->vulnerabilities json)
"Parse JSON, an input port or a string, and return the list of

File diff suppressed because it is too large Load diff

View file

@ -22,6 +22,8 @@
#:use-module (srfi srfi-19)
#:use-module (srfi srfi-64))
;; Generated from the 2019 database :
;; jq -M '.vulnerabilities |= map(select(.cve.id | IN("CVE-2019-14811", "CVE-2019-17365", "CVE-2019-1010180", "CVE-2019-1010204", "CVE-2019-18192", "CVE-2019-0001"))) | .totalResults = (.vulnerabilities | length) | .resultsPerPage = (.vulnerabilities | length)'
(define %sample
(search-path %load-path "tests/cve-sample.json"))
@ -31,23 +33,19 @@
(define %expected-vulnerabilities
;; What we should get when reading %SAMPLE.
(list
(vulnerability "CVE-2019-0001"
;; Only the "a" CPE configurations are kept; the "o"
;; configurations are discarded.
'(("juniper" "junos" (or "18.2" (or "18.21-s3" "18.21-s4")))))
(vulnerability "CVE-2019-0005"
'(("juniper" "junos" (or "18.1" "18.11"))))
;; CVE-2019-0005 has no "a" configurations.
(vulnerability "CVE-2019-14811"
'(("artifex" "ghostscript" (< "9.28"))))
(vulnerability "CVE-2019-17365"
'(("nixos" "nix" (<= "2.3"))))
(vulnerability "CVE-2019-1010180"
'(("gnu" "gdb" _))) ;any version
(vulnerability "CVE-2019-1010204"
'(("gnu" "binutils" (and (>= "2.21") (<= "2.31.1")))
("gnu" "binutils_gold" (and (>= "1.11") (<= "1.16")))))
;; CVE-2019-18192 has no associated configurations.
(vulnerability "CVE-2019-1010180"
'(("gnu" "gdb" (< "9.1"))))
(vulnerability "CVE-2019-14811"
'(("artifex" "ghostscript" (< "9.50"))))
(vulnerability "CVE-2019-17365"
'(("nixos" "nix" (<= "2.3"))))
(vulnerability "CVE-2019-18192"
'(("gnu" "guix" "1.0.1")))
;; Only the "a" CPE configurations are kept; the "o" configurations are discarded.
;; This is why CVE-2019-0001 doesn't appear here.
))
@ -55,13 +53,12 @@
(test-equal "json->cve-items"
'("CVE-2019-0001"
"CVE-2019-0005"
"CVE-2019-1010204"
"CVE-2019-1010180"
"CVE-2019-14811"
"CVE-2019-17365"
"CVE-2019-1010180"
"CVE-2019-1010204"
"CVE-2019-18192")
(map (compose cve-id cve-item-cve)
(map cve-item-id
(call-with-input-file %sample json->cve-items)))
(test-equal "cve-item-published-date"
@ -75,32 +72,32 @@
(call-with-input-file %sample json->vulnerabilities))
(test-equal "vulnerabilities->lookup-proc"
(list (list (third %expected-vulnerabilities)) ;ghostscript
(list (list (first %expected-vulnerabilities)) ;binutils
'()
(list (first %expected-vulnerabilities))
'()
(list (second %expected-vulnerabilities)) ;gdb
(list (second %expected-vulnerabilities))
(list (third %expected-vulnerabilities)) ;ghostscript
(list (third %expected-vulnerabilities))
'()
(list (fifth %expected-vulnerabilities)) ;gdb
(list (fifth %expected-vulnerabilities))
(list (fourth %expected-vulnerabilities)) ;nix
'()
(list (sixth %expected-vulnerabilities)) ;binutils
'()
(list (sixth %expected-vulnerabilities))
'())
(let* ((vulns (call-with-input-file %sample json->vulnerabilities))
(lookup (vulnerabilities->lookup-proc vulns)))
(list (lookup "ghostscript")
(lookup "ghostscript" "9.27")
(lookup "ghostscript" "9.28")
(lookup "gdb")
(lookup "gdb" "42.0")
(lookup "nix")
(lookup "nix" "2.4")
(lookup "binutils" "2.31.1")
(list (lookup "binutils" "2.31.1")
(lookup "binutils" "2.10")
(lookup "binutils_gold" "1.11")
(lookup "binutils" "2.32"))))
(lookup "binutils" "2.32")
(lookup "gdb")
(lookup "gdb" "9.0")
(lookup "ghostscript")
(lookup "ghostscript" "9.27")
(lookup "ghostscript" "9.51")
(lookup "nix")
(lookup "nix" "2.4"))))
(test-end "cve")