1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
4 ;;; This file is part of GNU Guix.
6 ;;; GNU Guix is free software; you can redistribute it and/or modify it
7 ;;; under the terms of the GNU General Public License as published by
8 ;;; the Free Software Foundation; either version 3 of the License, or (at
9 ;;; your option) any later version.
11 ;;; GNU Guix is distributed in the hope that it will be useful, but
12 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ;;; GNU General Public License for more details.
16 ;;; You should have received a copy of the GNU General Public License
17 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19 (define-module (guix cve)
20 #:use-module (guix utils)
21 #:use-module (guix http-client)
22 #:use-module (guix json)
23 #:use-module (guix i18n)
24 #:use-module ((guix diagnostics) #:select (formatted-message))
26 #:use-module (web uri)
27 #:use-module (srfi srfi-1)
28 #:use-module (srfi srfi-9)
29 #:use-module (srfi srfi-11)
30 #:use-module (srfi srfi-19)
31 #:use-module (srfi srfi-26)
32 #:use-module (srfi srfi-34)
33 #:use-module (srfi srfi-35)
34 #:use-module (ice-9 match)
35 #:use-module (ice-9 regex)
36 #:use-module (ice-9 vlist)
37 #:export (json->cve-items
41 cve-item-configurations
42 cve-item-published-date
43 cve-item-last-modified-date
57 vulnerability-packages
60 current-vulnerabilities
61 vulnerabilities->lookup-proc))
65 ;;; This modules provides the tools to fetch, parse, and digest part of the
66 ;;; Common Vulnerabilities and Exposures (CVE) feeds provided by the US NIST
67 ;;; at <https://nvd.nist.gov/vuln/data-feeds>.
71 (define (string->date* str)
72 (string->date str "~Y-~m-~dT~H:~M~z"))
74 (define-json-mapping <cve-item> cve-item cve-item?
76 (cve cve-item-cve "cve" json->cve) ;<cve>
77 (configurations cve-item-configurations ;list of sexps
78 "configurations" configuration-data->cve-configurations)
79 (published-date cve-item-published-date
80 "publishedDate" string->date*)
81 (last-modified-date cve-item-last-modified-date
82 "lastModifiedDate" string->date*))
84 (define-json-mapping <cve> cve cve?
86 (id cve-id "CVE_data_meta" ;string
87 (cut assoc-ref <> "ID"))
88 (data-type cve-data-type ;'CVE
89 "data_type" string->symbol)
90 (data-format cve-data-format ;'MITRE
91 "data_format" string->symbol)
92 (references cve-references ;list of <cve-reference>
93 "references" reference-data->cve-references))
95 (define-json-mapping <cve-reference> cve-reference cve-reference?
97 (url cve-reference-url) ;string
98 (tags cve-reference-tags ;list of strings
101 (define (reference-data->cve-references alist)
102 (map json->cve-reference
103 (vector->list (assoc-ref alist "reference_data"))))
105 (define %cpe-package-rx
106 ;; For applications: "cpe:2.3:a:VENDOR:PACKAGE:VERSION", or sometimes
107 ;; "cpe:2.3:a:VENDOR:PACKAGE:VERSION:PATCH-LEVEL".
108 (make-regexp "^cpe:2\\.3:a:([^:]+):([^:]+):([^:]+):([^:]+):"))
110 (define (cpe->package-name cpe)
111 "Converts the Common Platform Enumeration (CPE) string CPE to a package
112 name, in a very naive way. Return two values: the package name, and its
113 version string. Return #f and #f if CPE does not look like an application CPE
115 (cond ((regexp-exec %cpe-package-rx cpe)
118 (values (match:substring matches 2)
119 (match (match:substring matches 3)
122 (string-append version
123 (match (match:substring matches 4)
126 ;; Drop the colon from things like
127 ;; "cpe:2.3:a:openbsd:openssh:6.8:p1".
128 (string-drop patch-level 1)))))))))
132 (define (cpe-match->cve-configuration alist)
133 "Convert ALIST, a \"cpe_match\" alist, into an sexp representing the package
134 and versions matched. Return #f if ALIST doesn't correspond to an application
136 (let ((cpe (assoc-ref alist "cpe23Uri"))
137 (starti (assoc-ref alist "versionStartIncluding"))
138 (starte (assoc-ref alist "versionStartExcluding"))
139 (endi (assoc-ref alist "versionEndIncluding"))
140 (ende (assoc-ref alist "versionEndExcluding")))
141 (let-values (((package version) (cpe->package-name cpe)))
144 ,(cond ((and (or starti starte) (or endi ende))
145 `(and ,(if starti `(>= ,starti) `(> ,starte))
146 ,(if endi `(<= ,endi) `(< ,ende))))
147 (starti `(>= ,starti))
148 (starte `(> ,starte))
153 (define (configuration-data->cve-configurations alist)
154 "Given ALIST, a JSON dictionary for the baroque \"configurations\"
155 element found in CVEs, return an sexp such as (\"binutils\" (<
156 \"2.31\")) that represents matching configurations."
157 (define string->operator
162 (define (node->configuration node)
163 (let ((operator (string->operator (assoc-ref node "operator"))))
165 ((assoc-ref node "cpe_match")
168 (let ((matches (vector->list matches)))
169 (match (filter-map cpe-match->cve-configuration
173 (lst (cons operator lst))))))
174 ((assoc-ref node "children") ;typically for 'and'
177 (match (filter-map node->configuration (vector->list children))
180 (lst (cons operator lst)))))
184 (let ((nodes (vector->list (assoc-ref alist "nodes"))))
185 (filter-map node->configuration nodes)))
187 (define (json->cve-items json)
188 "Parse JSON, an input port or a string, and return a list of <cve-item>
190 (let* ((alist (json->scm json))
191 (type (assoc-ref alist "CVE_data_type"))
192 (format (assoc-ref alist "CVE_data_format"))
193 (version (assoc-ref alist "CVE_data_version")))
194 (unless (equal? type "CVE")
195 (raise (condition (&message
196 (message "invalid CVE feed")))))
197 (unless (equal? format "MITRE")
198 (raise (formatted-message (G_ "unsupported CVE format: '~a'")
200 (unless (equal? version "4.0")
201 (raise (formatted-message (G_ "unsupported CVE data version: '~a'")
205 (vector->list (assoc-ref alist "CVE_Items")))))
207 (define (version-matches? version sexp)
208 "Return true if VERSION, a string, matches SEXP."
212 ((? string? expected)
213 (version-prefix? expected version))
215 (any (cut version-matches? version <>) sexps))
217 (every (cut version-matches? version <>) sexps))
219 (version>? max version))
221 (version>=? max version))
223 (version>? version min))
225 (version>=? version min))))
229 ;;; High-level interface.
234 (define %current-year
239 (define (yearly-feed-uri year)
240 "Return the URI for the CVE feed for YEAR."
242 (string-append "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-"
243 (number->string year) ".json.gz")))
245 (define %current-year-ttl
246 ;; According to <https://nvd.nist.gov/download.cfm#CVE_FEED>, feeds are
247 ;; updated "approximately every two hours."
250 (define %past-year-ttl
251 ;; Update the previous year's database more and more infrequently.
252 (* 3600 24 (date-month %now)))
254 (define-record-type <vulnerability>
255 (vulnerability id packages)
257 (id vulnerability-id) ;string
258 (packages vulnerability-packages)) ;((p1 sexp1) (p2 sexp2) ...)
260 (define vulnerability->sexp
262 (($ <vulnerability> id packages)
263 `(v ,id ,packages))))
265 (define sexp->vulnerability
267 (('v id (packages ...))
268 (vulnerability id packages))))
270 (define (cve-configuration->package-list config)
271 "Parse CONFIG, a config sexp, and return a list of the form (P SEXP)
272 where P is a package name and SEXP expresses constraints on the matching
274 (let loop ((config config)
278 (fold loop packages configs))
279 (('and config _ ...) ;XXX
280 (loop config packages))
281 (((? string? package) '_) ;any version
283 (alist-delete package packages)))
284 (((? string? package) sexp)
285 (let ((previous (assoc-ref packages package)))
287 (cons `(,package (or ,sexp ,@previous))
288 (alist-delete package packages))
289 (cons `(,package ,sexp) packages)))))))
291 (define (merge-package-lists lst)
292 "Merge the list in LST, each of which has the form (p sexp), where P
293 is the name of a package and SEXP is an sexp that constrains matching
295 (fold (lambda (plist result) ;XXX: quadratic
297 (((package version) result)
298 (match (assoc-ref result package)
300 (cons `(,package ,version) result))
302 (cons `(,package (or ,version ,previous))
303 (alist-delete package result))))))
309 (define (cve-item->vulnerability item)
310 "Return a <vulnerability> corresponding to ITEM, a <cve-item> record;
311 return #f if ITEM does not list any configuration or if it does not list
312 any \"a\" (application) configuration."
313 (let ((id (cve-id (cve-item-cve item))))
314 (match (cve-item-configurations item)
315 (() ;no configurations
320 (map cve-configuration->package-list configs)))))))
322 (define (json->vulnerabilities json)
323 "Parse JSON, an input port or a string, and return the list of
324 vulnerabilities found therein."
325 (filter-map cve-item->vulnerability (json->cve-items json)))
327 (define (write-cache input cache)
328 "Read vulnerabilities as gzipped JSON from INPUT, and write it as a compact
330 (call-with-decompressed-port 'gzip input
333 (json->vulnerabilities input))
335 (write `(vulnerabilities
337 ,(map vulnerability->sexp vulns))
340 (define (fetch-vulnerabilities year ttl)
341 "Return the list of <vulnerability> for YEAR, assuming the on-disk cache has
342 the given TTL (fetch from the NIST web site when TTL has expired)."
343 (define (cache-miss uri)
344 (format (current-error-port) "fetching CVE database for ~a...~%" year))
347 ;; Disable read options to avoid populating the source property weak
348 ;; table, which speeds things up, saves memory, and works around
349 ;; <https://lists.gnu.org/archive/html/guile-devel/2017-09/msg00031.html>.
350 (let ((options (read-options)))
353 (read-disable 'positions))
357 (read-options options)))))
359 ;; Note: We used to keep the original JSON files in cache but parsing it
360 ;; would take typically ~15s for a year of data. Thus, we instead store a
361 ;; summarized version thereof as an sexp, which can be parsed in 1s or so.
362 (let* ((port (http-fetch/cached (yearly-feed-uri year)
364 #:write-cache write-cache
365 #:cache-miss cache-miss))
369 (('vulnerabilities 1 vulns)
370 (map sexp->vulnerability vulns)))))
372 (define (current-vulnerabilities)
373 "Return the current list of Common Vulnerabilities and Exposures (CVE) as
374 published by the US NIST."
375 (let ((past-years (unfold (cut > <> 3)
380 (past-ttls (unfold (cut > <> 3)
382 (* n %past-year-ttl))
385 (append-map fetch-vulnerabilities
386 (cons %current-year past-years)
387 (cons %current-year-ttl past-ttls))))
389 (define (vulnerabilities->lookup-proc vulnerabilities)
390 "Return a lookup procedure built from VULNERABILITIES that takes a package
391 name and optionally a version number. When the version is omitted, the lookup
392 procedure returns a list of vulnerabilities; otherwise, it returns a list of
393 vulnerabilities affecting the given package version."
395 ;; Map package names to lists of version/vulnerability pairs.
396 (fold (lambda (vuln table)
398 (($ <vulnerability> id packages)
399 (fold (lambda (package table)
402 (vhash-cons name (cons vuln versions)
409 (lambda* (package #:optional version)
410 (vhash-fold* (if version
411 (lambda (pair result)
414 (if (version-matches? version sexp)
417 (lambda (pair result)
420 (cons vuln result)))))
425 ;;; cve.scm ends here