Commit | Line | Data |
---|---|---|
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 | |
96 | name, in a very naive way. Return #f if CPE does not look like an application | |
97 | CPE 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 | |
156 | vulnerability 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 | |
171 | the 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 | |
218 | published 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 | |
235 | name and optionally a version number. When the version is omitted, the lookup | |
236 | procedure returns a list of version/vulnerability pairs; otherwise, it returns | |
237 | a 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 |