cve.scm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
  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. (define-module (guix cve)
  19. #:use-module (guix utils)
  20. #:use-module (guix http-client)
  21. #:use-module (guix i18n)
  22. #:use-module ((guix diagnostics) #:select (formatted-message))
  23. #:use-module (json)
  24. #:use-module (web uri)
  25. #:use-module (srfi srfi-1)
  26. #:use-module (srfi srfi-9)
  27. #:use-module (srfi srfi-11)
  28. #:use-module (srfi srfi-19)
  29. #:use-module (srfi srfi-26)
  30. #:use-module (srfi srfi-34)
  31. #:use-module (srfi srfi-35)
  32. #:use-module (ice-9 match)
  33. #:use-module (ice-9 regex)
  34. #:use-module (ice-9 vlist)
  35. #:export (json->cve-items
  36. cve-item?
  37. cve-item-cve
  38. cve-item-configurations
  39. cve-item-published-date
  40. cve-item-last-modified-date
  41. cve?
  42. cve-id
  43. cve-data-type
  44. cve-data-format
  45. cve-references
  46. cve-reference?
  47. cve-reference-url
  48. cve-reference-tags
  49. vulnerability?
  50. vulnerability-id
  51. vulnerability-packages
  52. json->vulnerabilities
  53. current-vulnerabilities
  54. vulnerabilities->lookup-proc))
  55. ;;; Commentary:
  56. ;;;
  57. ;;; This modules provides the tools to fetch, parse, and digest part of the
  58. ;;; Common Vulnerabilities and Exposures (CVE) feeds provided by the US NIST
  59. ;;; at <https://nvd.nist.gov/vuln/data-feeds>.
  60. ;;;
  61. ;;; Code:
  62. (define (string->date* str)
  63. (string->date str "~Y-~m-~dT~H:~M~z"))
  64. (define-json-mapping <cve-item> cve-item cve-item?
  65. json->cve-item
  66. (cve cve-item-cve "cve" json->cve) ;<cve>
  67. (configurations cve-item-configurations ;list of sexps
  68. "configurations" configuration-data->cve-configurations)
  69. (published-date cve-item-published-date
  70. "publishedDate" string->date*)
  71. (last-modified-date cve-item-last-modified-date
  72. "lastModifiedDate" string->date*))
  73. (define-json-mapping <cve> cve cve?
  74. json->cve
  75. (id cve-id "CVE_data_meta" ;string
  76. (cut assoc-ref <> "ID"))
  77. (data-type cve-data-type ;'CVE
  78. "data_type" string->symbol)
  79. (data-format cve-data-format ;'MITRE
  80. "data_format" string->symbol)
  81. (references cve-references ;list of <cve-reference>
  82. "references" reference-data->cve-references))
  83. (define-json-mapping <cve-reference> cve-reference cve-reference?
  84. json->cve-reference
  85. (url cve-reference-url) ;string
  86. (tags cve-reference-tags ;list of strings
  87. "tags" vector->list))
  88. (define (reference-data->cve-references alist)
  89. (map json->cve-reference
  90. ;; Normally "reference_data" is always present but rejected CVEs such
  91. ;; as CVE-2020-10020 can lack it.
  92. (vector->list (or (assoc-ref alist "reference_data") '#()))))
  93. (define %cpe-package-rx
  94. ;; For applications: "cpe:2.3:a:VENDOR:PACKAGE:VERSION", or sometimes
  95. ;; "cpe:2.3:a:VENDOR:PACKAGE:VERSION:PATCH-LEVEL".
  96. (make-regexp "^cpe:2\\.3:a:([^:]+):([^:]+):([^:]+):([^:]+):"))
  97. (define (cpe->package-name cpe)
  98. "Converts the Common Platform Enumeration (CPE) string CPE to a package
  99. name, in a very naive way. Return two values: the package name, and its
  100. version string. Return #f and #f if CPE does not look like an application CPE
  101. string."
  102. (cond ((regexp-exec %cpe-package-rx cpe)
  103. =>
  104. (lambda (matches)
  105. (values (match:substring matches 2)
  106. (match (match:substring matches 3)
  107. ("*" '_)
  108. (version
  109. (string-append version
  110. (match (match:substring matches 4)
  111. ("" "")
  112. (patch-level
  113. ;; Drop the colon from things like
  114. ;; "cpe:2.3:a:openbsd:openssh:6.8:p1".
  115. (string-drop patch-level 1)))))))))
  116. (else
  117. (values #f #f))))
  118. (define (cpe-match->cve-configuration alist)
  119. "Convert ALIST, a \"cpe_match\" alist, into an sexp representing the package
  120. and versions matched. Return #f if ALIST doesn't correspond to an application
  121. package."
  122. (let ((cpe (assoc-ref alist "cpe23Uri"))
  123. (starti (assoc-ref alist "versionStartIncluding"))
  124. (starte (assoc-ref alist "versionStartExcluding"))
  125. (endi (assoc-ref alist "versionEndIncluding"))
  126. (ende (assoc-ref alist "versionEndExcluding")))
  127. ;; Normally "cpe23Uri" is here in each "cpe_match" item, but CVE-2020-0534
  128. ;; has a configuration that lacks it.
  129. (and cpe
  130. (let-values (((package version) (cpe->package-name cpe)))
  131. (and package
  132. `(,package
  133. ,(cond ((and (or starti starte) (or endi ende))
  134. `(and ,(if starti `(>= ,starti) `(> ,starte))
  135. ,(if endi `(<= ,endi) `(< ,ende))))
  136. (starti `(>= ,starti))
  137. (starte `(> ,starte))
  138. (endi `(<= ,endi))
  139. (ende `(< ,ende))
  140. (else version))))))))
  141. (define (configuration-data->cve-configurations alist)
  142. "Given ALIST, a JSON dictionary for the baroque \"configurations\"
  143. element found in CVEs, return an sexp such as (\"binutils\" (<
  144. \"2.31\")) that represents matching configurations."
  145. (define string->operator
  146. (match-lambda
  147. ("OR" 'or)
  148. ("AND" 'and)))
  149. (define (node->configuration node)
  150. (let ((operator (string->operator (assoc-ref node "operator"))))
  151. (cond
  152. ((assoc-ref node "cpe_match")
  153. =>
  154. (lambda (matches)
  155. (let ((matches (vector->list matches)))
  156. (match (filter-map cpe-match->cve-configuration
  157. matches)
  158. (() #f)
  159. ((one) one)
  160. (lst (cons operator lst))))))
  161. ((assoc-ref node "children") ;typically for 'and'
  162. =>
  163. (lambda (children)
  164. (match (filter-map node->configuration (vector->list children))
  165. (() #f)
  166. ((one) one)
  167. (lst (cons operator lst)))))
  168. (else
  169. #f))))
  170. (let ((nodes (vector->list (assoc-ref alist "nodes"))))
  171. (filter-map node->configuration nodes)))
  172. (define (json->cve-items json)
  173. "Parse JSON, an input port or a string, and return a list of <cve-item>
  174. records."
  175. (let* ((alist (json->scm json))
  176. (type (assoc-ref alist "CVE_data_type"))
  177. (format (assoc-ref alist "CVE_data_format"))
  178. (version (assoc-ref alist "CVE_data_version")))
  179. (unless (equal? type "CVE")
  180. (raise (condition (&message
  181. (message "invalid CVE feed")))))
  182. (unless (equal? format "MITRE")
  183. (raise (formatted-message (G_ "unsupported CVE format: '~a'")
  184. format)))
  185. (unless (equal? version "4.0")
  186. (raise (formatted-message (G_ "unsupported CVE data version: '~a'")
  187. version)))
  188. (map json->cve-item
  189. (vector->list (assoc-ref alist "CVE_Items")))))
  190. (define (version-matches? version sexp)
  191. "Return true if VERSION, a string, matches SEXP."
  192. (match sexp
  193. ('_
  194. #t)
  195. ((? string? expected)
  196. (version-prefix? expected version))
  197. (('or sexps ...)
  198. (any (cut version-matches? version <>) sexps))
  199. (('and sexps ...)
  200. (every (cut version-matches? version <>) sexps))
  201. (('< max)
  202. (version>? max version))
  203. (('<= max)
  204. (version>=? max version))
  205. (('> min)
  206. (version>? version min))
  207. (('>= min)
  208. (version>=? version min))))
  209. ;;;
  210. ;;; High-level interface.
  211. ;;;
  212. (define %now
  213. (current-date))
  214. (define %current-year
  215. (date-year %now))
  216. (define %past-year
  217. (- %current-year 1))
  218. (define (yearly-feed-uri year)
  219. "Return the URI for the CVE feed for YEAR."
  220. (string->uri
  221. (string-append "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-"
  222. (number->string year) ".json.gz")))
  223. (define %current-year-ttl
  224. ;; According to <https://nvd.nist.gov/download.cfm#CVE_FEED>, feeds are
  225. ;; updated "approximately every two hours."
  226. (* 60 30))
  227. (define %past-year-ttl
  228. ;; Update the previous year's database more and more infrequently.
  229. (* 3600 24 (date-month %now)))
  230. (define-record-type <vulnerability>
  231. (vulnerability id packages)
  232. vulnerability?
  233. (id vulnerability-id) ;string
  234. (packages vulnerability-packages)) ;((p1 sexp1) (p2 sexp2) ...)
  235. (define vulnerability->sexp
  236. (match-lambda
  237. (($ <vulnerability> id packages)
  238. `(v ,id ,packages))))
  239. (define sexp->vulnerability
  240. (match-lambda
  241. (('v id (packages ...))
  242. (vulnerability id packages))))
  243. (define (cve-configuration->package-list config)
  244. "Parse CONFIG, a config sexp, and return a list of the form (P SEXP)
  245. where P is a package name and SEXP expresses constraints on the matching
  246. versions."
  247. (let loop ((config config)
  248. (packages '()))
  249. (match config
  250. (('or configs ...)
  251. (fold loop packages configs))
  252. (('and config _ ...) ;XXX
  253. (loop config packages))
  254. (((? string? package) '_) ;any version
  255. (cons `(,package _)
  256. (alist-delete package packages)))
  257. (((? string? package) sexp)
  258. (let ((previous (assoc-ref packages package)))
  259. (if previous
  260. (cons `(,package (or ,sexp ,@previous))
  261. (alist-delete package packages))
  262. (cons `(,package ,sexp) packages)))))))
  263. (define (merge-package-lists lst)
  264. "Merge the list in LST, each of which has the form (p sexp), where P
  265. is the name of a package and SEXP is an sexp that constrains matching
  266. versions."
  267. (fold (lambda (plist result) ;XXX: quadratic
  268. (fold (match-lambda*
  269. (((package version) result)
  270. (match (assoc-ref result package)
  271. (#f
  272. (cons `(,package ,version) result))
  273. ((previous)
  274. (cons `(,package (or ,version ,previous))
  275. (alist-delete package result))))))
  276. result
  277. plist))
  278. '()
  279. lst))
  280. (define (cve-item->vulnerability item)
  281. "Return a <vulnerability> corresponding to ITEM, a <cve-item> record;
  282. return #f if ITEM does not list any configuration or if it does not list
  283. any \"a\" (application) configuration."
  284. (let ((id (cve-id (cve-item-cve item))))
  285. (match (cve-item-configurations item)
  286. (() ;no configurations
  287. #f)
  288. ((configs ...)
  289. (vulnerability id
  290. (merge-package-lists
  291. (map cve-configuration->package-list configs)))))))
  292. (define (json->vulnerabilities json)
  293. "Parse JSON, an input port or a string, and return the list of
  294. vulnerabilities found therein."
  295. (filter-map cve-item->vulnerability (json->cve-items json)))
  296. (define (write-cache input cache)
  297. "Read vulnerabilities as gzipped JSON from INPUT, and write it as a compact
  298. sexp to CACHE."
  299. (call-with-decompressed-port 'gzip input
  300. (lambda (input)
  301. (define vulns
  302. (json->vulnerabilities input))
  303. (write `(vulnerabilities
  304. 1 ;format version
  305. ,(map vulnerability->sexp vulns))
  306. cache))))
  307. (define* (fetch-vulnerabilities year ttl #:key (timeout 10))
  308. "Return the list of <vulnerability> for YEAR, assuming the on-disk cache has
  309. the given TTL (fetch from the NIST web site when TTL has expired)."
  310. (define (cache-miss uri)
  311. (format (current-error-port) "fetching CVE database for ~a...~%" year))
  312. (define (read* port)
  313. ;; Disable read options to avoid populating the source property weak
  314. ;; table, which speeds things up, saves memory, and works around
  315. ;; <https://lists.gnu.org/archive/html/guile-devel/2017-09/msg00031.html>.
  316. (let ((options (read-options)))
  317. (dynamic-wind
  318. (lambda ()
  319. (read-disable 'positions))
  320. (lambda ()
  321. (read port))
  322. (lambda ()
  323. (read-options options)))))
  324. ;; Note: We used to keep the original JSON files in cache but parsing it
  325. ;; would take typically ~15s for a year of data. Thus, we instead store a
  326. ;; summarized version thereof as an sexp, which can be parsed in 1s or so.
  327. (let* ((port (http-fetch/cached (yearly-feed-uri year)
  328. #:ttl ttl
  329. #:write-cache write-cache
  330. #:cache-miss cache-miss
  331. #:timeout timeout))
  332. (sexp (read* port)))
  333. (close-port port)
  334. (match sexp
  335. (('vulnerabilities 1 vulns)
  336. (map sexp->vulnerability vulns)))))
  337. (define* (current-vulnerabilities #:key (timeout 10))
  338. "Return the current list of Common Vulnerabilities and Exposures (CVE) as
  339. published by the US NIST. TIMEOUT specifies the timeout in seconds for
  340. connection establishment."
  341. (let ((past-years (unfold (cut > <> 3)
  342. (lambda (n)
  343. (- %current-year n))
  344. 1+
  345. 1))
  346. (past-ttls (unfold (cut > <> 3)
  347. (lambda (n)
  348. (* n %past-year-ttl))
  349. 1+
  350. 1)))
  351. (append-map (cut fetch-vulnerabilities <> <> #:timeout timeout)
  352. (cons %current-year past-years)
  353. (cons %current-year-ttl past-ttls))))
  354. (define (vulnerabilities->lookup-proc vulnerabilities)
  355. "Return a lookup procedure built from VULNERABILITIES that takes a package
  356. name and optionally a version number. When the version is omitted, the lookup
  357. procedure returns a list of vulnerabilities; otherwise, it returns a list of
  358. vulnerabilities affecting the given package version."
  359. (define table
  360. ;; Map package names to lists of version/vulnerability pairs.
  361. (fold (lambda (vuln table)
  362. (match vuln
  363. (($ <vulnerability> id packages)
  364. (fold (lambda (package table)
  365. (match package
  366. ((name . versions)
  367. (vhash-cons name (cons vuln versions)
  368. table))))
  369. table
  370. packages))))
  371. vlist-null
  372. vulnerabilities))
  373. (lambda* (package #:optional version)
  374. (vhash-fold* (if version
  375. (lambda (pair result)
  376. (match pair
  377. ((vuln sexp)
  378. (if (version-matches? version sexp)
  379. (cons vuln result)
  380. result))))
  381. (lambda (pair result)
  382. (match pair
  383. ((vuln . _)
  384. (cons vuln result)))))
  385. '()
  386. package table)))
  387. ;;; cve.scm ends here