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 ;;; 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. ;;; This file is part of GNU Guix.
;;; ;;;
@ -36,17 +37,11 @@
#:export (json->cve-items #:export (json->cve-items
cve-item? cve-item?
cve-item-cve cve-item-id
cve-item-configurations cve-item-configurations
cve-item-published-date cve-item-published-date
cve-item-last-modified-date cve-item-last-modified-date
cve?
cve-id
cve-data-type
cve-data-format
cve-references
cve-reference? cve-reference?
cve-reference-url cve-reference-url
cve-reference-tags cve-reference-tags
@ -68,28 +63,17 @@
;;; Code: ;;; Code:
(define (string->date* str) (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? (define-json-mapping <cve-item> cve-item cve-item?
json->cve-item json->cve-item
(cve cve-item-cve "cve" json->cve) ;<cve> (id cve-item-id "id") ;string
(configurations cve-item-configurations ;list of sexps (configurations cve-item-configurations ;list of sexps
"configurations" configuration-data->cve-configurations) "configurations" configuration-data->cve-configurations)
(published-date cve-item-published-date (published-date cve-item-published-date
"publishedDate" string->date*) "published" string->date*)
(last-modified-date cve-item-last-modified-date (last-modified-date cve-item-last-modified-date
"lastModifiedDate" string->date*)) "lastModified" 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))
(define-json-mapping <cve-reference> cve-reference cve-reference? (define-json-mapping <cve-reference> cve-reference cve-reference?
json->cve-reference json->cve-reference
@ -97,12 +81,6 @@
(tags cve-reference-tags ;list of strings (tags cve-reference-tags ;list of strings
"tags" vector->list)) "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 (define %cpe-package-rx
;; For applications: "cpe:2.3:a:VENDOR:PACKAGE:VERSION", or sometimes ;; For applications: "cpe:2.3:a:VENDOR:PACKAGE:VERSION", or sometimes
;; "cpe:2.3:a:VENDOR:PACKAGE:VERSION:PATCH-LEVEL". ;; "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)))) (values #f #f #f))))
(define (cpe-match->cve-configuration alist) (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 and versions matched. Return #f if ALIST doesn't correspond to an application
package." package."
(let ((cpe (assoc-ref alist "cpe23Uri")) (let ((cpe (assoc-ref alist "criteria"))
(starti (assoc-ref alist "versionStartIncluding")) (starti (assoc-ref alist "versionStartIncluding"))
(starte (assoc-ref alist "versionStartExcluding")) (starte (assoc-ref alist "versionStartExcluding"))
(endi (assoc-ref alist "versionEndIncluding")) (endi (assoc-ref alist "versionEndIncluding"))
(ende (assoc-ref alist "versionEndExcluding"))) (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. ;; has a configuration that lacks it.
(and cpe (and cpe
(let ((vendor package version (cpe->package-identifier cpe))) (let ((vendor package version (cpe->package-identifier cpe)))
@ -156,7 +134,7 @@ package."
(ende `(< ,ende)) (ende `(< ,ende))
(else version)))))))) (else version))))))))
(define (configuration-data->cve-configurations alist) (define (configuration-data->cve-configurations vector)
"Given ALIST, a JSON dictionary for the baroque \"configurations\" "Given ALIST, a JSON dictionary for the baroque \"configurations\"
element found in CVEs, return an sexp such as (\"binutils\" (< element found in CVEs, return an sexp such as (\"binutils\" (<
\"2.31\")) that represents matching configurations." \"2.31\")) that represents matching configurations."
@ -165,10 +143,13 @@ element found in CVEs, return an sexp such as (\"binutils\" (<
("OR" 'or) ("OR" 'or)
("AND" 'and))) ("AND" 'and)))
(define (maybe-vector->alist vector)
(vector->list (or (and (unspecified? vector) #()) vector #())))
(define (node->configuration node) (define (node->configuration node)
(let ((operator (string->operator (assoc-ref node "operator")))) (let ((operator (string->operator (assoc-ref node "operator"))))
(cond (cond
((assoc-ref node "cpe_match") ((assoc-ref node "cpeMatch")
=> =>
(lambda (matches) (lambda (matches)
(let ((matches (vector->list matches))) (let ((matches (vector->list matches)))
@ -187,28 +168,31 @@ element found in CVEs, return an sexp such as (\"binutils\" (<
(else (else
#f)))) #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))) (filter-map node->configuration nodes)))
(define (json->cve-items json) (define (json->cve-items json)
"Parse JSON, an input port or a string, and return a list of <cve-item> "Parse JSON, an input port or a string, and return a list of <cve-item>
records." records."
(let* ((alist (json->scm json)) (let ((alist (json->scm json)))
(type (assoc-ref alist "CVE_data_type")) (match (assoc-ref alist "format")
(format (assoc-ref alist "CVE_data_format")) ("NVD_CVE"
(version (assoc-ref alist "CVE_data_version"))) #t)
(unless (equal? type "CVE") (format
(raise (condition (&message (raise (formatted-message (G_ "unsupported CVE format: '~a'")
(message "invalid CVE feed"))))) format))))
(unless (equal? format "MITRE") (match (assoc-ref alist "version")
(raise (formatted-message (G_ "unsupported CVE format: '~a'") ("2.0"
format))) #t)
(unless (equal? version "4.0") (version
(raise (formatted-message (G_ "unsupported CVE data version: '~a'") (raise (formatted-message (G_ "unsupported CVE data version: '~a'")
version))) version))))
(map json->cve-item (map (compose json->cve-item (cut assoc-ref <> "cve"))
(vector->list (assoc-ref alist "CVE_Items"))))) (vector->list (assoc-ref alist "vulnerabilities")))))
(define (version-matches? version sexp) (define (version-matches? version sexp)
"Return true if VERSION, a string, matches SEXP." "Return true if VERSION, a string, matches SEXP."
@ -269,7 +253,7 @@ HIDDEN-VENDORS."
(define (yearly-feed-uri year) (define (yearly-feed-uri year)
"Return the URI for the CVE feed for YEAR." "Return the URI for the CVE feed for YEAR."
(string->uri (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"))) (number->string year) ".json.gz")))
(define %current-year-ttl (define %current-year-ttl
@ -352,14 +336,13 @@ matching versions."
"Return a <vulnerability> corresponding to ITEM, a <cve-item> record; "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 return #f if ITEM does not list any configuration or if it does not list
any \"a\" (application) configuration." any \"a\" (application) configuration."
(let ((id (cve-id (cve-item-cve item)))) (match (cve-item-configurations item)
(match (cve-item-configurations item) (() ;no configurations
(() ;no configurations #f)
#f) ((configs ...)
((configs ...) (vulnerability (cve-item-id item)
(vulnerability id (merge-package-lists
(merge-package-lists (map cve-configuration->package-list configs))))))
(map cve-configuration->package-list configs)))))))
(define (json->vulnerabilities json) (define (json->vulnerabilities json)
"Parse JSON, an input port or a string, and return the list of "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-19)
#:use-module (srfi srfi-64)) #: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 (define %sample
(search-path %load-path "tests/cve-sample.json")) (search-path %load-path "tests/cve-sample.json"))
@ -31,23 +33,19 @@
(define %expected-vulnerabilities (define %expected-vulnerabilities
;; What we should get when reading %SAMPLE. ;; What we should get when reading %SAMPLE.
(list (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" (vulnerability "CVE-2019-1010204"
'(("gnu" "binutils" (and (>= "2.21") (<= "2.31.1"))) '(("gnu" "binutils" (and (>= "2.21") (<= "2.31.1")))
("gnu" "binutils_gold" (and (>= "1.11") (<= "1.16"))))) ("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" (test-equal "json->cve-items"
'("CVE-2019-0001" '("CVE-2019-0001"
"CVE-2019-0005" "CVE-2019-1010204"
"CVE-2019-1010180"
"CVE-2019-14811" "CVE-2019-14811"
"CVE-2019-17365" "CVE-2019-17365"
"CVE-2019-1010180"
"CVE-2019-1010204"
"CVE-2019-18192") "CVE-2019-18192")
(map (compose cve-id cve-item-cve) (map cve-item-id
(call-with-input-file %sample json->cve-items))) (call-with-input-file %sample json->cve-items)))
(test-equal "cve-item-published-date" (test-equal "cve-item-published-date"
@ -75,32 +72,32 @@
(call-with-input-file %sample json->vulnerabilities)) (call-with-input-file %sample json->vulnerabilities))
(test-equal "vulnerabilities->lookup-proc" (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 (third %expected-vulnerabilities))
'() '()
(list (fifth %expected-vulnerabilities)) ;gdb
(list (fifth %expected-vulnerabilities))
(list (fourth %expected-vulnerabilities)) ;nix (list (fourth %expected-vulnerabilities)) ;nix
'()
(list (sixth %expected-vulnerabilities)) ;binutils
'()
(list (sixth %expected-vulnerabilities))
'()) '())
(let* ((vulns (call-with-input-file %sample json->vulnerabilities)) (let* ((vulns (call-with-input-file %sample json->vulnerabilities))
(lookup (vulnerabilities->lookup-proc vulns))) (lookup (vulnerabilities->lookup-proc vulns)))
(list (lookup "ghostscript") (list (lookup "binutils" "2.31.1")
(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")
(lookup "binutils" "2.10") (lookup "binutils" "2.10")
(lookup "binutils_gold" "1.11") (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") (test-end "cve")