cve.scm 15 KB

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