processes: Allow 'less' to properly estimate line length.
[jackhill/guix/guix.git] / guix / cve.scm
CommitLineData
0eef7551 1;;; GNU Guix --- Functional package management for GNU
d51bfe24 2;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
0eef7551
LC
3;;;
4;;; This file is part of GNU Guix.
5;;;
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.
10;;;
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.
15;;;
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/>.
18
19(define-module (guix cve)
20 #:use-module (guix utils)
21 #:use-module (guix http-client)
74afaa37
LC
22 #:use-module (guix json)
23 #:use-module (guix i18n)
d51bfe24 24 #:use-module ((guix diagnostics) #:select (formatted-message))
74afaa37 25 #:use-module (json)
0eef7551
LC
26 #:use-module (web uri)
27 #:use-module (srfi srfi-1)
28 #:use-module (srfi srfi-9)
870bf71e 29 #:use-module (srfi srfi-11)
0eef7551 30 #:use-module (srfi srfi-19)
3af7a7a8 31 #:use-module (srfi srfi-26)
74afaa37
LC
32 #:use-module (srfi srfi-34)
33 #:use-module (srfi srfi-35)
0eef7551
LC
34 #:use-module (ice-9 match)
35 #:use-module (ice-9 regex)
36 #:use-module (ice-9 vlist)
74afaa37
LC
37 #:export (json->cve-items
38
39 cve-item?
40 cve-item-cve
41 cve-item-configurations
42 cve-item-published-date
43 cve-item-last-modified-date
44
45 cve?
46 cve-id
47 cve-data-type
48 cve-data-format
a055604e 49 cve-references
74afaa37
LC
50
51 cve-reference?
52 cve-reference-url
53 cve-reference-tags
54
55 vulnerability?
0eef7551
LC
56 vulnerability-id
57 vulnerability-packages
58
74afaa37 59 json->vulnerabilities
0eef7551
LC
60 current-vulnerabilities
61 vulnerabilities->lookup-proc))
62
63;;; Commentary:
64;;;
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
74afaa37 67;;; at <https://nvd.nist.gov/vuln/data-feeds>.
0eef7551
LC
68;;;
69;;; Code:
70
74afaa37
LC
71(define (string->date* str)
72 (string->date str "~Y-~m-~dT~H:~M~z"))
73
74(define-json-mapping <cve-item> cve-item cve-item?
75 json->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*))
83
84(define-json-mapping <cve> cve cve?
85 json->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)
a055604e 92 (references cve-references ;list of <cve-reference>
74afaa37
LC
93 "references" reference-data->cve-references))
94
95(define-json-mapping <cve-reference> cve-reference cve-reference?
96 json->cve-reference
97 (url cve-reference-url) ;string
98 (tags cve-reference-tags ;list of strings
99 "tags" vector->list))
100
101(define (reference-data->cve-references alist)
102 (map json->cve-reference
103 (vector->list (assoc-ref alist "reference_data"))))
104
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:([^:]+):([^:]+):([^:]+):([^:]+):"))
109
110(define (cpe->package-name cpe)
111 "Converts the Common Platform Enumeration (CPE) string CPE to a package
112name, in a very naive way. Return two values: the package name, and its
113version string. Return #f and #f if CPE does not look like an application CPE
114string."
115 (cond ((regexp-exec %cpe-package-rx cpe)
116 =>
117 (lambda (matches)
118 (values (match:substring matches 2)
119 (match (match:substring matches 3)
120 ("*" '_)
121 (version
122 (string-append version
123 (match (match:substring matches 4)
124 ("" "")
125 (patch-level
126 ;; Drop the colon from things like
127 ;; "cpe:2.3:a:openbsd:openssh:6.8:p1".
128 (string-drop patch-level 1)))))))))
129 (else
130 (values #f #f))))
131
132(define (cpe-match->cve-configuration alist)
133 "Convert ALIST, a \"cpe_match\" alist, into an sexp representing the package
134and versions matched. Return #f if ALIST doesn't correspond to an application
135package."
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)))
142 (and package
143 `(,package
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))
149 (endi `(<= ,endi))
150 (ende `(< ,ende))
151 (else version)))))))
152
153(define (configuration-data->cve-configurations alist)
154 "Given ALIST, a JSON dictionary for the baroque \"configurations\"
155element found in CVEs, return an sexp such as (\"binutils\" (<
156\"2.31\")) that represents matching configurations."
157 (define string->operator
158 (match-lambda
159 ("OR" 'or)
160 ("AND" 'and)))
161
162 (define (node->configuration node)
163 (let ((operator (string->operator (assoc-ref node "operator"))))
164 (cond
165 ((assoc-ref node "cpe_match")
166 =>
167 (lambda (matches)
168 (let ((matches (vector->list matches)))
169 (match (filter-map cpe-match->cve-configuration
170 matches)
171 (() #f)
172 ((one) one)
173 (lst (cons operator lst))))))
174 ((assoc-ref node "children") ;typically for 'and'
175 =>
176 (lambda (children)
177 (match (filter-map node->configuration (vector->list children))
178 (() #f)
179 ((one) one)
180 (lst (cons operator lst)))))
181 (else
182 #f))))
183
184 (let ((nodes (vector->list (assoc-ref alist "nodes"))))
185 (filter-map node->configuration nodes)))
186
187(define (json->cve-items json)
188 "Parse JSON, an input port or a string, and return a list of <cve-item>
189records."
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")
d51bfe24
LC
198 (raise (formatted-message (G_ "unsupported CVE format: '~a'")
199 format)))
74afaa37 200 (unless (equal? version "4.0")
d51bfe24
LC
201 (raise (formatted-message (G_ "unsupported CVE data version: '~a'")
202 version)))
74afaa37
LC
203
204 (map json->cve-item
205 (vector->list (assoc-ref alist "CVE_Items")))))
206
207(define (version-matches? version sexp)
208 "Return true if VERSION, a string, matches SEXP."
209 (match sexp
210 ('_
211 #t)
212 ((? string? expected)
213 (version-prefix? expected version))
214 (('or sexps ...)
215 (any (cut version-matches? version <>) sexps))
216 (('and sexps ...)
217 (every (cut version-matches? version <>) sexps))
218 (('< max)
219 (version>? max version))
220 (('<= max)
221 (version>=? max version))
222 (('> min)
223 (version>? version min))
224 (('>= min)
225 (version>=? version min))))
226
227\f
228;;;
229;;; High-level interface.
230;;;
0eef7551 231
6a25e595
LC
232(define %now
233 (current-date))
234(define %current-year
235 (date-year %now))
236(define %past-year
237 (- %current-year 1))
238
239(define (yearly-feed-uri year)
240 "Return the URI for the CVE feed for YEAR."
0eef7551 241 (string->uri
74afaa37
LC
242 (string-append "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-"
243 (number->string year) ".json.gz")))
0eef7551 244
6a25e595 245(define %current-year-ttl
0eef7551
LC
246 ;; According to <https://nvd.nist.gov/download.cfm#CVE_FEED>, feeds are
247 ;; updated "approximately every two hours."
7482b981 248 (* 60 30))
0eef7551 249
6a25e595
LC
250(define %past-year-ttl
251 ;; Update the previous year's database more and more infrequently.
7482b981 252 (* 3600 24 (date-month %now)))
0eef7551 253
74afaa37
LC
254(define-record-type <vulnerability>
255 (vulnerability id packages)
256 vulnerability?
257 (id vulnerability-id) ;string
258 (packages vulnerability-packages)) ;((p1 sexp1) (p2 sexp2) ...)
0eef7551 259
5cdd21c7
LC
260(define vulnerability->sexp
261 (match-lambda
262 (($ <vulnerability> id packages)
263 `(v ,id ,packages))))
264
265(define sexp->vulnerability
266 (match-lambda
267 (('v id (packages ...))
268 (vulnerability id packages))))
269
74afaa37
LC
270(define (cve-configuration->package-list config)
271 "Parse CONFIG, a config sexp, and return a list of the form (P SEXP)
272where P is a package name and SEXP expresses constraints on the matching
273versions."
274 (let loop ((config config)
275 (packages '()))
276 (match config
277 (('or configs ...)
278 (fold loop packages configs))
279 (('and config _ ...) ;XXX
280 (loop config packages))
281 (((? string? package) '_) ;any version
282 (cons `(,package _)
283 (alist-delete package packages)))
284 (((? string? package) sexp)
285 (let ((previous (assoc-ref packages package)))
286 (if previous
287 (cons `(,package (or ,sexp ,@previous))
288 (alist-delete package packages))
289 (cons `(,package ,sexp) packages)))))))
290
291(define (merge-package-lists lst)
292 "Merge the list in LST, each of which has the form (p sexp), where P
293is the name of a package and SEXP is an sexp that constrains matching
294versions."
295 (fold (lambda (plist result) ;XXX: quadratic
296 (fold (match-lambda*
297 (((package version) result)
298 (match (assoc-ref result package)
299 (#f
300 (cons `(,package ,version) result))
301 ((previous)
302 (cons `(,package (or ,version ,previous))
303 (alist-delete package result))))))
304 result
305 plist))
306 '()
307 lst))
308
309(define (cve-item->vulnerability item)
310 "Return a <vulnerability> corresponding to ITEM, a <cve-item> record;
311return #f if ITEM does not list any configuration or if it does not list
312any \"a\" (application) configuration."
313 (let ((id (cve-id (cve-item-cve item))))
314 (match (cve-item-configurations item)
315 (() ;no configurations
316 #f)
317 ((configs ...)
318 (vulnerability id
319 (merge-package-lists
320 (map cve-configuration->package-list configs)))))))
321
322(define (json->vulnerabilities json)
323 "Parse JSON, an input port or a string, and return the list of
324vulnerabilities found therein."
325 (filter-map cve-item->vulnerability (json->cve-items json)))
326
7482b981 327(define (write-cache input cache)
74afaa37 328 "Read vulnerabilities as gzipped JSON from INPUT, and write it as a compact
7482b981
LC
329sexp to CACHE."
330 (call-with-decompressed-port 'gzip input
331 (lambda (input)
7482b981 332 (define vulns
74afaa37 333 (json->vulnerabilities input))
6a25e595 334
7482b981
LC
335 (write `(vulnerabilities
336 1 ;format version
337 ,(map vulnerability->sexp vulns))
338 cache))))
5cdd21c7 339
7482b981
LC
340(define (fetch-vulnerabilities year ttl)
341 "Return the list of <vulnerability> for YEAR, assuming the on-disk cache has
342the 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))
5cdd21c7 345
f1b65d0d
LC
346 (define (read* port)
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)))
351 (dynamic-wind
352 (lambda ()
353 (read-disable 'positions))
354 (lambda ()
355 (read port))
356 (lambda ()
357 (read-options options)))))
358
74afaa37 359 ;; Note: We used to keep the original JSON files in cache but parsing it
7482b981
LC
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)
363 #:ttl ttl
364 #:write-cache write-cache
365 #:cache-miss cache-miss))
366 (sexp (read* port)))
367 (close-port port)
368 (match sexp
369 (('vulnerabilities 1 vulns)
370 (map sexp->vulnerability vulns)))))
5cdd21c7
LC
371
372(define (current-vulnerabilities)
373 "Return the current list of Common Vulnerabilities and Exposures (CVE) as
374published by the US NIST."
3af7a7a8
LC
375 (let ((past-years (unfold (cut > <> 3)
376 (lambda (n)
377 (- %current-year n))
378 1+
379 1))
380 (past-ttls (unfold (cut > <> 3)
381 (lambda (n)
382 (* n %past-year-ttl))
383 1+
384 1)))
385 (append-map fetch-vulnerabilities
386 (cons %current-year past-years)
387 (cons %current-year-ttl past-ttls))))
0eef7551
LC
388
389(define (vulnerabilities->lookup-proc vulnerabilities)
390 "Return a lookup procedure built from VULNERABILITIES that takes a package
391name and optionally a version number. When the version is omitted, the lookup
870bf71e
LC
392procedure returns a list of vulnerabilities; otherwise, it returns a list of
393vulnerabilities affecting the given package version."
0eef7551
LC
394 (define table
395 ;; Map package names to lists of version/vulnerability pairs.
396 (fold (lambda (vuln table)
397 (match vuln
398 (($ <vulnerability> id packages)
399 (fold (lambda (package table)
400 (match package
870bf71e
LC
401 ((name . versions)
402 (vhash-cons name (cons vuln versions)
0eef7551
LC
403 table))))
404 table
405 packages))))
406 vlist-null
407 vulnerabilities))
408
409 (lambda* (package #:optional version)
410 (vhash-fold* (if version
411 (lambda (pair result)
412 (match pair
74afaa37
LC
413 ((vuln sexp)
414 (if (version-matches? version sexp)
0eef7551
LC
415 (cons vuln result)
416 result))))
870bf71e
LC
417 (lambda (pair result)
418 (match pair
419 ((vuln . _)
420 (cons vuln result)))))
0eef7551
LC
421 '()
422 package table)))
423
6a25e595 424
0eef7551 425;;; cve.scm ends here