guix package: Inherit the transformed version number.
[jackhill/guix/guix.git] / guix / cve.scm
CommitLineData
0eef7551 1;;; GNU Guix --- Functional package management for GNU
cf557afa 2;;; Copyright © 2015, 2016 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)
5cdd21c7 22 #:use-module ((guix build utils) #:select (mkdir-p))
0eef7551
LC
23 #:use-module (sxml ssax)
24 #:use-module (web uri)
25 #:use-module (srfi srfi-1)
26 #:use-module (srfi srfi-9)
27 #:use-module (srfi srfi-19)
3af7a7a8 28 #:use-module (srfi srfi-26)
0eef7551
LC
29 #:use-module (ice-9 match)
30 #:use-module (ice-9 regex)
31 #:use-module (ice-9 vlist)
32 #:export (vulnerability?
33 vulnerability-id
34 vulnerability-packages
35
36 xml->vulnerabilities
37 current-vulnerabilities
38 vulnerabilities->lookup-proc))
39
40;;; Commentary:
41;;;
42;;; This modules provides the tools to fetch, parse, and digest part of the
43;;; Common Vulnerabilities and Exposures (CVE) feeds provided by the US NIST
44;;; at <https://nvd.nist.gov/download.cfm#CVE_FEED>.
45;;;
46;;; Code:
47
48(define-record-type <vulnerability>
49 (vulnerability id packages)
50 vulnerability?
51 (id vulnerability-id)
52 (packages vulnerability-packages))
53
6a25e595
LC
54(define %now
55 (current-date))
56(define %current-year
57 (date-year %now))
58(define %past-year
59 (- %current-year 1))
60
61(define (yearly-feed-uri year)
62 "Return the URI for the CVE feed for YEAR."
0eef7551 63 (string->uri
6a25e595
LC
64 (string-append "https://static.nvd.nist.gov/feeds/xml/cve/nvdcve-2.0-"
65 (number->string year) ".xml.gz")))
0eef7551 66
6a25e595 67(define %current-year-ttl
0eef7551
LC
68 ;; According to <https://nvd.nist.gov/download.cfm#CVE_FEED>, feeds are
69 ;; updated "approximately every two hours."
70 (* 3600 3))
71
6a25e595
LC
72(define %past-year-ttl
73 ;; Update the previous year's database more and more infrequently.
74 (* 3600 24 2 (date-month %now)))
75
76(define (call-with-cve-port uri ttl proc)
0eef7551 77 "Pass PROC an input port from which to read the CVE stream."
86cf1303 78 (let ((port (http-fetch uri)))
0eef7551
LC
79 (dynamic-wind
80 (const #t)
81 (lambda ()
82 (call-with-decompressed-port 'gzip port
6a25e595
LC
83 (lambda (port)
84 (setvbuf port _IOFBF 65536)
85 (proc port))))
0eef7551
LC
86 (lambda ()
87 (close-port port)))))
88
89(define %cpe-package-rx
cf557afa
LC
90 ;; For applications: "cpe:/a:VENDOR:PACKAGE:VERSION", or sometimes
91 ;; "cpe/a:VENDOR:PACKAGE:VERSION:PATCH-LEVEL".
92 (make-regexp "^cpe:/a:([^:]+):([^:]+):([^:]+)((:.+)?)"))
0eef7551
LC
93
94(define (cpe->package-name cpe)
95 "Converts the Common Platform Enumeration (CPE) string CPE to a package
96name, in a very naive way. Return #f if CPE does not look like an application
97CPE string."
98 (and=> (regexp-exec %cpe-package-rx (string-trim-both cpe))
99 (lambda (matches)
100 (cons (match:substring matches 2)
cf557afa
LC
101 (string-append (match:substring matches 3)
102 (match (match:substring matches 4)
103 ("" "")
104 (patch-level
105 ;; Drop the colon from things like
106 ;; "cpe:/a:openbsd:openssh:6.8:p1".
107 (string-drop patch-level 1))))))))
0eef7551
LC
108
109(define %parse-vulnerability-feed
110 ;; Parse the XML vulnerability feed from
111 ;; <https://nvd.nist.gov/download.cfm#CVE_FEED> and return a list of
112 ;; vulnerability objects.
113 (ssax:make-parser NEW-LEVEL-SEED
114 (lambda (elem-gi attributes namespaces expected-content
115 seed)
116 (match elem-gi
117 ((name-space . 'entry)
118 (cons (assoc-ref attributes 'id) seed))
119 ((name-space . 'vulnerable-software-list)
120 (cons '() seed))
121 ((name-space . 'product)
122 (cons 'product seed))
123 (x seed)))
124
125 FINISH-ELEMENT
126 (lambda (elem-gi attributes namespaces parent-seed
127 seed)
128 (match elem-gi
129 ((name-space . 'entry)
130 (match seed
131 (((? string? id) . rest)
132 ;; Some entries have no vulnerable-software-list.
133 rest)
134 ((products id . rest)
135 (match (filter-map cpe->package-name products)
136 (()
137 ;; No application among PRODUCTS.
138 rest)
139 (packages
140 (cons (vulnerability id (reverse packages))
141 rest))))))
142 (x
143 seed)))
144
145 CHAR-DATA-HANDLER
146 (lambda (str _ seed)
147 (match seed
148 (('product software-list . rest)
149 ;; Add STR to the vulnerable software list this
150 ;; <product> tag is part of.
151 (cons (cons str software-list) rest))
152 (x x)))))
153
154(define (xml->vulnerabilities port)
155 "Read from PORT an XML feed of vulnerabilities and return a list of
156vulnerability objects."
157 (reverse (%parse-vulnerability-feed port '())))
158
5cdd21c7
LC
159(define vulnerability->sexp
160 (match-lambda
161 (($ <vulnerability> id packages)
162 `(v ,id ,packages))))
163
164(define sexp->vulnerability
165 (match-lambda
166 (('v id (packages ...))
167 (vulnerability id packages))))
168
169(define (fetch-vulnerabilities year ttl)
170 "Return the list of <vulnerability> for YEAR, assuming the on-disk cache has
171the given TTL (fetch from the NIST web site when TTL has expired)."
172 ;; Note: We used to keep the original XML files in cache but parsing it
173 ;; would take typically ~15s for a year of data. Thus, we instead store a
174 ;; summarized version thereof as an sexp, which can be parsed in 1s or so.
175 (define cache
176 (string-append (cache-directory) "/cve/" (number->string year)))
177
178 (define (do-fetch)
179 (call-with-cve-port (yearly-feed-uri year) ttl
6a25e595
LC
180 (lambda (port)
181 ;; XXX: The SSAX "error port" is used to send pointless warnings such as
182 ;; "warning: Skipping PI". Turn that off.
3af7a7a8 183 (format (current-error-port) "fetching CVE database for ~a...~%" year)
6a25e595
LC
184 (parameterize ((current-ssax-error-port (%make-void-port "w")))
185 (xml->vulnerabilities port)))))
186
5cdd21c7
LC
187 (define (update-cache)
188 (mkdir-p (dirname cache))
189 (let ((vulns (do-fetch)))
190 (with-atomic-file-output cache
191 (lambda (port)
192 (write `(vulnerabilities
193 0 ;format version
194 ,(map vulnerability->sexp vulns))
195 port)))
196 vulns))
197
198 (define (old? file)
199 ;; Return true if PORT has passed TTL.
200 (let* ((s (stat file))
201 (now (current-time time-utc)))
202 (< (+ (stat:mtime s) ttl) (time-second now))))
203
204 (catch 'system-error
205 (lambda ()
206 (if (old? cache)
207 (update-cache)
208 (match (call-with-input-file cache read)
209 (('vulnerabilities 0 vulns)
210 (map sexp->vulnerability vulns))
211 (x
212 (update-cache)))))
213 (lambda args
214 (update-cache))))
215
216(define (current-vulnerabilities)
217 "Return the current list of Common Vulnerabilities and Exposures (CVE) as
218published by the US NIST."
3af7a7a8
LC
219 (let ((past-years (unfold (cut > <> 3)
220 (lambda (n)
221 (- %current-year n))
222 1+
223 1))
224 (past-ttls (unfold (cut > <> 3)
225 (lambda (n)
226 (* n %past-year-ttl))
227 1+
228 1)))
229 (append-map fetch-vulnerabilities
230 (cons %current-year past-years)
231 (cons %current-year-ttl past-ttls))))
0eef7551
LC
232
233(define (vulnerabilities->lookup-proc vulnerabilities)
234 "Return a lookup procedure built from VULNERABILITIES that takes a package
235name and optionally a version number. When the version is omitted, the lookup
236procedure returns a list of version/vulnerability pairs; otherwise, it returns
237a list of vulnerabilities affection the given package version."
238 (define table
239 ;; Map package names to lists of version/vulnerability pairs.
240 (fold (lambda (vuln table)
241 (match vuln
242 (($ <vulnerability> id packages)
243 (fold (lambda (package table)
244 (match package
245 ((name . version)
246 (vhash-cons name (cons version vuln)
247 table))))
248 table
249 packages))))
250 vlist-null
251 vulnerabilities))
252
253 (lambda* (package #:optional version)
254 (vhash-fold* (if version
255 (lambda (pair result)
256 (match pair
257 ((v . vuln)
258 (if (string=? v version)
259 (cons vuln result)
260 result))))
261 cons)
262 '()
263 package table)))
264
6a25e595
LC
265
266;;; Local Variables:
267;;; eval: (put 'call-with-cve-port 'scheme-indent-function 2)
268;;; End:
269
0eef7551 270;;; cve.scm ends here