pkg-stats 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276
  1. #!/usr/bin/env python3
  2. # Copyright (C) 2009 by Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. # General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  17. import aiohttp
  18. import argparse
  19. import asyncio
  20. import datetime
  21. import fnmatch
  22. import os
  23. from collections import defaultdict
  24. import re
  25. import subprocess
  26. import json
  27. import sys
  28. import time
  29. import gzip
  30. import xml.etree.ElementTree
  31. import requests
  32. brpath = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
  33. sys.path.append(os.path.join(brpath, "utils"))
  34. from getdeveloperlib import parse_developers # noqa: E402
  35. from cpedb import CPEDB_URL # noqa: E402
  36. INFRA_RE = re.compile(r"\$\(eval \$\(([a-z-]*)-package\)\)")
  37. URL_RE = re.compile(r"\s*https?://\S*\s*$")
  38. RM_API_STATUS_ERROR = 1
  39. RM_API_STATUS_FOUND_BY_DISTRO = 2
  40. RM_API_STATUS_FOUND_BY_PATTERN = 3
  41. RM_API_STATUS_NOT_FOUND = 4
  42. class Defconfig:
  43. def __init__(self, name, path):
  44. self.name = name
  45. self.path = path
  46. self.developers = None
  47. def set_developers(self, developers):
  48. """
  49. Fills in the .developers field
  50. """
  51. self.developers = [
  52. developer.name
  53. for developer in developers
  54. if developer.hasfile(self.path)
  55. ]
  56. def get_defconfig_list():
  57. """
  58. Builds the list of Buildroot defconfigs, returning a list of Defconfig
  59. objects.
  60. """
  61. return [
  62. Defconfig(name[:-len('_defconfig')], os.path.join('configs', name))
  63. for name in os.listdir(os.path.join(brpath, 'configs'))
  64. if name.endswith('_defconfig')
  65. ]
  66. class Package:
  67. all_licenses = dict()
  68. all_license_files = list()
  69. all_versions = dict()
  70. all_ignored_cves = dict()
  71. all_cpeids = dict()
  72. # This is the list of all possible checks. Add new checks to this list so
  73. # a tool that post-processeds the json output knows the checks before
  74. # iterating over the packages.
  75. status_checks = ['cve', 'developers', 'hash', 'license',
  76. 'license-files', 'patches', 'pkg-check', 'url', 'version']
  77. def __init__(self, name, path):
  78. self.name = name
  79. self.path = path
  80. self.pkg_path = os.path.dirname(path)
  81. self.infras = None
  82. self.license = None
  83. self.has_license = False
  84. self.has_license_files = False
  85. self.has_hash = False
  86. self.patch_files = []
  87. self.warnings = 0
  88. self.current_version = None
  89. self.url = None
  90. self.url_worker = None
  91. self.cpeid = None
  92. self.cves = list()
  93. self.ignored_cves = list()
  94. self.unsure_cves = list()
  95. self.latest_version = {'status': RM_API_STATUS_ERROR, 'version': None, 'id': None}
  96. self.status = {}
  97. def pkgvar(self):
  98. return self.name.upper().replace("-", "_")
  99. def set_url(self):
  100. """
  101. Fills in the .url field
  102. """
  103. self.status['url'] = ("warning", "no Config.in")
  104. pkgdir = os.path.dirname(os.path.join(brpath, self.path))
  105. for filename in os.listdir(pkgdir):
  106. if fnmatch.fnmatch(filename, 'Config.*'):
  107. fp = open(os.path.join(pkgdir, filename), "r")
  108. for config_line in fp:
  109. if URL_RE.match(config_line):
  110. self.url = config_line.strip()
  111. self.status['url'] = ("ok", "found")
  112. fp.close()
  113. return
  114. self.status['url'] = ("error", "missing")
  115. fp.close()
  116. @property
  117. def patch_count(self):
  118. return len(self.patch_files)
  119. @property
  120. def has_valid_infra(self):
  121. if self.infras is None:
  122. return False
  123. return len(self.infras) > 0
  124. @property
  125. def is_actual_package(self):
  126. try:
  127. if not self.has_valid_infra:
  128. return False
  129. if self.infras[0][1] == 'virtual':
  130. return False
  131. except IndexError:
  132. return False
  133. return True
  134. def set_infra(self):
  135. """
  136. Fills in the .infras field
  137. """
  138. self.infras = list()
  139. with open(os.path.join(brpath, self.path), 'r') as f:
  140. lines = f.readlines()
  141. for line in lines:
  142. match = INFRA_RE.match(line)
  143. if not match:
  144. continue
  145. infra = match.group(1)
  146. if infra.startswith("host-"):
  147. self.infras.append(("host", infra[5:]))
  148. else:
  149. self.infras.append(("target", infra))
  150. def set_license(self):
  151. """
  152. Fills in the .status['license'] and .status['license-files'] fields
  153. """
  154. if not self.is_actual_package:
  155. self.status['license'] = ("na", "no valid package infra")
  156. self.status['license-files'] = ("na", "no valid package infra")
  157. return
  158. var = self.pkgvar()
  159. self.status['license'] = ("error", "missing")
  160. self.status['license-files'] = ("error", "missing")
  161. if var in self.all_licenses:
  162. self.license = self.all_licenses[var]
  163. self.status['license'] = ("ok", "found")
  164. if var in self.all_license_files:
  165. self.status['license-files'] = ("ok", "found")
  166. def set_hash_info(self):
  167. """
  168. Fills in the .status['hash'] field
  169. """
  170. if not self.is_actual_package:
  171. self.status['hash'] = ("na", "no valid package infra")
  172. self.status['hash-license'] = ("na", "no valid package infra")
  173. return
  174. hashpath = self.path.replace(".mk", ".hash")
  175. if os.path.exists(os.path.join(brpath, hashpath)):
  176. self.status['hash'] = ("ok", "found")
  177. else:
  178. self.status['hash'] = ("error", "missing")
  179. def set_patch_count(self):
  180. """
  181. Fills in the .patch_count, .patch_files and .status['patches'] fields
  182. """
  183. if not self.is_actual_package:
  184. self.status['patches'] = ("na", "no valid package infra")
  185. return
  186. pkgdir = os.path.dirname(os.path.join(brpath, self.path))
  187. for subdir, _, _ in os.walk(pkgdir):
  188. self.patch_files = fnmatch.filter(os.listdir(subdir), '*.patch')
  189. if self.patch_count == 0:
  190. self.status['patches'] = ("ok", "no patches")
  191. elif self.patch_count < 5:
  192. self.status['patches'] = ("warning", "some patches")
  193. else:
  194. self.status['patches'] = ("error", "lots of patches")
  195. def set_current_version(self):
  196. """
  197. Fills in the .current_version field
  198. """
  199. var = self.pkgvar()
  200. if var in self.all_versions:
  201. self.current_version = self.all_versions[var]
  202. def set_cpeid(self):
  203. """
  204. Fills in the .cpeid field
  205. """
  206. var = self.pkgvar()
  207. if not self.is_actual_package:
  208. self.status['cpe'] = ("na", "N/A - virtual pkg")
  209. return
  210. if not self.current_version:
  211. self.status['cpe'] = ("na", "no version information available")
  212. return
  213. if var in self.all_cpeids:
  214. self.cpeid = self.all_cpeids[var]
  215. # Set a preliminary status, it might be overridden by check_package_cpes()
  216. self.status['cpe'] = ("warning", "not checked against CPE dictionnary")
  217. else:
  218. self.status['cpe'] = ("error", "no verified CPE identifier")
  219. def set_check_package_warnings(self):
  220. """
  221. Fills in the .warnings and .status['pkg-check'] fields
  222. """
  223. cmd = [os.path.join(brpath, "utils/check-package")]
  224. pkgdir = os.path.dirname(os.path.join(brpath, self.path))
  225. self.status['pkg-check'] = ("error", "Missing")
  226. for root, dirs, files in os.walk(pkgdir):
  227. for f in files:
  228. if f.endswith(".mk") or f.endswith(".hash") or f == "Config.in" or f == "Config.in.host":
  229. cmd.append(os.path.join(root, f))
  230. o = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[1]
  231. lines = o.splitlines()
  232. for line in lines:
  233. m = re.match("^([0-9]*) warnings generated", line.decode())
  234. if m:
  235. self.warnings = int(m.group(1))
  236. if self.warnings == 0:
  237. self.status['pkg-check'] = ("ok", "no warnings")
  238. else:
  239. self.status['pkg-check'] = ("error", "{} warnings".format(self.warnings))
  240. return
  241. def set_ignored_cves(self):
  242. """
  243. Give the list of CVEs ignored by the package
  244. """
  245. self.ignored_cves = list(self.all_ignored_cves.get(self.pkgvar(), []))
  246. def set_developers(self, developers):
  247. """
  248. Fills in the .developers and .status['developers'] field
  249. """
  250. self.developers = [
  251. dev.name
  252. for dev in developers
  253. if dev.hasfile(self.path)
  254. ]
  255. if self.developers:
  256. self.status['developers'] = ("ok", "{} developers".format(len(self.developers)))
  257. else:
  258. self.status['developers'] = ("warning", "no developers")
  259. def is_status_ok(self, name):
  260. return name in self.status and self.status[name][0] == 'ok'
  261. def is_status_error(self, name):
  262. return name in self.status and self.status[name][0] == 'error'
  263. def is_status_na(self, name):
  264. return name in self.status and self.status[name][0] == 'na'
  265. def __eq__(self, other):
  266. return self.path == other.path
  267. def __lt__(self, other):
  268. return self.path < other.path
  269. def __str__(self):
  270. return "%s (path='%s', license='%s', license_files='%s', hash='%s', patches=%d)" % \
  271. (self.name, self.path, self.is_status_ok('license'),
  272. self.is_status_ok('license-files'), self.status['hash'], self.patch_count)
  273. def get_pkglist(npackages, package_list):
  274. """
  275. Builds the list of Buildroot packages, returning a list of Package
  276. objects. Only the .name and .path fields of the Package object are
  277. initialized.
  278. npackages: limit to N packages
  279. package_list: limit to those packages in this list
  280. """
  281. WALK_USEFUL_SUBDIRS = ["boot", "linux", "package", "toolchain"]
  282. WALK_EXCLUDES = ["boot/common.mk",
  283. "linux/linux-ext-.*.mk",
  284. "package/freescale-imx/freescale-imx.mk",
  285. "package/gcc/gcc.mk",
  286. "package/gstreamer/gstreamer.mk",
  287. "package/gstreamer1/gstreamer1.mk",
  288. "package/gtk2-themes/gtk2-themes.mk",
  289. "package/matchbox/matchbox.mk",
  290. "package/opengl/opengl.mk",
  291. "package/qt5/qt5.mk",
  292. "package/x11r7/x11r7.mk",
  293. "package/doc-asciidoc.mk",
  294. "package/pkg-.*.mk",
  295. "toolchain/toolchain-external/pkg-toolchain-external.mk",
  296. "toolchain/toolchain-external/toolchain-external.mk",
  297. "toolchain/toolchain.mk",
  298. "toolchain/helpers.mk",
  299. "toolchain/toolchain-wrapper.mk"]
  300. packages = list()
  301. count = 0
  302. for root, dirs, files in os.walk(brpath):
  303. root = os.path.relpath(root, brpath)
  304. rootdir = root.split("/")
  305. if len(rootdir) < 1:
  306. continue
  307. if rootdir[0] not in WALK_USEFUL_SUBDIRS:
  308. continue
  309. for f in files:
  310. if not f.endswith(".mk"):
  311. continue
  312. # Strip ending ".mk"
  313. pkgname = f[:-3]
  314. if package_list and pkgname not in package_list:
  315. continue
  316. pkgpath = os.path.join(root, f)
  317. skip = False
  318. for exclude in WALK_EXCLUDES:
  319. if re.match(exclude, pkgpath):
  320. skip = True
  321. continue
  322. if skip:
  323. continue
  324. p = Package(pkgname, pkgpath)
  325. packages.append(p)
  326. count += 1
  327. if npackages and count == npackages:
  328. return packages
  329. return packages
  330. def get_config_packages():
  331. cmd = ["make", "--no-print-directory", "show-info"]
  332. js = json.loads(subprocess.check_output(cmd))
  333. return set([v["name"] for v in js.values() if 'name' in v])
  334. def package_init_make_info():
  335. # Fetch all variables at once
  336. variables = subprocess.check_output(["make", "--no-print-directory", "-s",
  337. "BR2_HAVE_DOT_CONFIG=y", "printvars",
  338. "VARS=%_LICENSE %_LICENSE_FILES %_VERSION %_IGNORE_CVES %_CPE_ID"])
  339. variable_list = variables.decode().splitlines()
  340. # We process first the host package VERSION, and then the target
  341. # package VERSION. This means that if a package exists in both
  342. # target and host variants, with different values (eg. version
  343. # numbers (unlikely)), we'll report the target one.
  344. variable_list = [x[5:] for x in variable_list if x.startswith("HOST_")] + \
  345. [x for x in variable_list if not x.startswith("HOST_")]
  346. for item in variable_list:
  347. # Get variable name and value
  348. pkgvar, value = item.split("=", maxsplit=1)
  349. # Strip the suffix according to the variable
  350. if pkgvar.endswith("_LICENSE"):
  351. # If value is "unknown", no license details available
  352. if value == "unknown":
  353. continue
  354. pkgvar = pkgvar[:-8]
  355. Package.all_licenses[pkgvar] = value
  356. elif pkgvar.endswith("_LICENSE_FILES"):
  357. if pkgvar.endswith("_MANIFEST_LICENSE_FILES"):
  358. continue
  359. pkgvar = pkgvar[:-14]
  360. Package.all_license_files.append(pkgvar)
  361. elif pkgvar.endswith("_VERSION"):
  362. if pkgvar.endswith("_DL_VERSION"):
  363. continue
  364. pkgvar = pkgvar[:-8]
  365. Package.all_versions[pkgvar] = value
  366. elif pkgvar.endswith("_IGNORE_CVES"):
  367. pkgvar = pkgvar[:-12]
  368. Package.all_ignored_cves[pkgvar] = value.split()
  369. elif pkgvar.endswith("_CPE_ID"):
  370. pkgvar = pkgvar[:-7]
  371. Package.all_cpeids[pkgvar] = value
  372. check_url_count = 0
  373. async def check_url_status(session, pkg, npkgs, retry=True):
  374. global check_url_count
  375. try:
  376. async with session.get(pkg.url) as resp:
  377. if resp.status >= 400:
  378. pkg.status['url'] = ("error", "invalid {}".format(resp.status))
  379. check_url_count += 1
  380. print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name))
  381. return
  382. except (aiohttp.ClientError, asyncio.TimeoutError):
  383. if retry:
  384. return await check_url_status(session, pkg, npkgs, retry=False)
  385. else:
  386. pkg.status['url'] = ("error", "invalid (err)")
  387. check_url_count += 1
  388. print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name))
  389. return
  390. pkg.status['url'] = ("ok", "valid")
  391. check_url_count += 1
  392. print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name))
  393. async def check_package_urls(packages):
  394. tasks = []
  395. connector = aiohttp.TCPConnector(limit_per_host=5)
  396. async with aiohttp.ClientSession(connector=connector, trust_env=True,
  397. timeout=aiohttp.ClientTimeout(total=15)) as sess:
  398. packages = [p for p in packages if p.status['url'][0] == 'ok']
  399. for pkg in packages:
  400. tasks.append(asyncio.ensure_future(check_url_status(sess, pkg, len(packages))))
  401. await asyncio.wait(tasks)
  402. def check_package_latest_version_set_status(pkg, status, version, identifier):
  403. pkg.latest_version = {
  404. "status": status,
  405. "version": version,
  406. "id": identifier,
  407. }
  408. if pkg.latest_version['status'] == RM_API_STATUS_ERROR:
  409. pkg.status['version'] = ('warning', "Release Monitoring API error")
  410. elif pkg.latest_version['status'] == RM_API_STATUS_NOT_FOUND:
  411. pkg.status['version'] = ('warning', "Package not found on Release Monitoring")
  412. if pkg.latest_version['version'] is None:
  413. pkg.status['version'] = ('warning', "No upstream version available on Release Monitoring")
  414. elif pkg.latest_version['version'] != pkg.current_version:
  415. pkg.status['version'] = ('error', "The newer version {} is available upstream".format(pkg.latest_version['version']))
  416. else:
  417. pkg.status['version'] = ('ok', 'up-to-date')
  418. async def check_package_get_latest_version_by_distro(session, pkg, retry=True):
  419. url = "https://release-monitoring.org//api/project/Buildroot/%s" % pkg.name
  420. try:
  421. async with session.get(url) as resp:
  422. if resp.status != 200:
  423. return False
  424. data = await resp.json()
  425. if 'stable_versions' in data and data['stable_versions']:
  426. version = data['stable_versions'][0]
  427. elif 'version' in data:
  428. version = data['version']
  429. else:
  430. version = None
  431. check_package_latest_version_set_status(pkg,
  432. RM_API_STATUS_FOUND_BY_DISTRO,
  433. version,
  434. data['id'])
  435. return True
  436. except (aiohttp.ClientError, asyncio.TimeoutError):
  437. if retry:
  438. return await check_package_get_latest_version_by_distro(session, pkg, retry=False)
  439. else:
  440. return False
  441. async def check_package_get_latest_version_by_guess(session, pkg, retry=True):
  442. url = "https://release-monitoring.org/api/projects/?pattern=%s" % pkg.name
  443. try:
  444. async with session.get(url) as resp:
  445. if resp.status != 200:
  446. return False
  447. data = await resp.json()
  448. # filter projects that have the right name and a version defined
  449. projects = [p for p in data['projects'] if p['name'] == pkg.name and 'stable_versions' in p]
  450. projects.sort(key=lambda x: x['id'])
  451. if len(projects) > 0:
  452. check_package_latest_version_set_status(pkg,
  453. RM_API_STATUS_FOUND_BY_PATTERN,
  454. projects[0]['stable_versions'][0],
  455. projects[0]['id'])
  456. return True
  457. except (aiohttp.ClientError, asyncio.TimeoutError):
  458. if retry:
  459. return await check_package_get_latest_version_by_guess(session, pkg, retry=False)
  460. else:
  461. return False
  462. check_latest_count = 0
  463. async def check_package_latest_version_get(session, pkg, npkgs):
  464. global check_latest_count
  465. if await check_package_get_latest_version_by_distro(session, pkg):
  466. check_latest_count += 1
  467. print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name))
  468. return
  469. if await check_package_get_latest_version_by_guess(session, pkg):
  470. check_latest_count += 1
  471. print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name))
  472. return
  473. check_package_latest_version_set_status(pkg,
  474. RM_API_STATUS_NOT_FOUND,
  475. None, None)
  476. check_latest_count += 1
  477. print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name))
  478. async def check_package_latest_version(packages):
  479. """
  480. Fills in the .latest_version field of all Package objects
  481. This field is a dict and has the following keys:
  482. - status: one of RM_API_STATUS_ERROR,
  483. RM_API_STATUS_FOUND_BY_DISTRO, RM_API_STATUS_FOUND_BY_PATTERN,
  484. RM_API_STATUS_NOT_FOUND
  485. - version: string containing the latest version known by
  486. release-monitoring.org for this package
  487. - id: string containing the id of the project corresponding to this
  488. package, as known by release-monitoring.org
  489. """
  490. for pkg in [p for p in packages if not p.is_actual_package]:
  491. pkg.status['version'] = ("na", "no valid package infra")
  492. tasks = []
  493. connector = aiohttp.TCPConnector(limit_per_host=5)
  494. async with aiohttp.ClientSession(connector=connector, trust_env=True) as sess:
  495. packages = [p for p in packages if p.is_actual_package]
  496. for pkg in packages:
  497. tasks.append(asyncio.ensure_future(check_package_latest_version_get(sess, pkg, len(packages))))
  498. await asyncio.wait(tasks)
  499. def check_package_cve_affects(cve, cpe_product_pkgs):
  500. for product in cve.affected_products:
  501. if product not in cpe_product_pkgs:
  502. continue
  503. for pkg in cpe_product_pkgs[product]:
  504. cve_status = cve.affects(pkg.name, pkg.current_version, pkg.ignored_cves, pkg.cpeid)
  505. if cve_status == cve.CVE_AFFECTS:
  506. pkg.cves.append(cve.identifier)
  507. elif cve_status == cve.CVE_UNKNOWN:
  508. pkg.unsure_cves.append(cve.identifier)
  509. def check_package_cves(nvd_path, packages):
  510. if not os.path.isdir(nvd_path):
  511. os.makedirs(nvd_path)
  512. cpe_product_pkgs = defaultdict(list)
  513. for pkg in packages:
  514. if not pkg.is_actual_package:
  515. pkg.status['cve'] = ("na", "N/A")
  516. continue
  517. if not pkg.current_version:
  518. pkg.status['cve'] = ("na", "no version information available")
  519. continue
  520. if pkg.cpeid:
  521. cpe_product = cvecheck.cpe_product(pkg.cpeid)
  522. cpe_product_pkgs[cpe_product].append(pkg)
  523. else:
  524. cpe_product_pkgs[pkg.name].append(pkg)
  525. for cve in cvecheck.CVE.read_nvd_dir(nvd_path):
  526. check_package_cve_affects(cve, cpe_product_pkgs)
  527. for pkg in packages:
  528. if 'cve' not in pkg.status:
  529. if pkg.cves or pkg.unsure_cves:
  530. pkg.status['cve'] = ("error", "affected by CVEs")
  531. else:
  532. pkg.status['cve'] = ("ok", "not affected by CVEs")
  533. def check_package_cpes(nvd_path, packages):
  534. class CpeXmlParser:
  535. cpes = []
  536. def start(self, tag, attrib):
  537. if tag == "{http://scap.nist.gov/schema/cpe-extension/2.3}cpe23-item":
  538. self.cpes.append(attrib['name'])
  539. def close(self):
  540. return self.cpes
  541. print("CPE: Setting up NIST dictionary")
  542. if not os.path.exists(os.path.join(nvd_path, "cpe")):
  543. os.makedirs(os.path.join(nvd_path, "cpe"))
  544. cpe_dict_local = os.path.join(nvd_path, "cpe", os.path.basename(CPEDB_URL))
  545. if not os.path.exists(cpe_dict_local) or os.stat(cpe_dict_local).st_mtime < time.time() - 86400:
  546. print("CPE: Fetching xml manifest from [" + CPEDB_URL + "]")
  547. cpe_dict = requests.get(CPEDB_URL)
  548. open(cpe_dict_local, "wb").write(cpe_dict.content)
  549. print("CPE: Unzipping xml manifest...")
  550. nist_cpe_file = gzip.GzipFile(fileobj=open(cpe_dict_local, 'rb'))
  551. parser = xml.etree.ElementTree.XMLParser(target=CpeXmlParser())
  552. while True:
  553. c = nist_cpe_file.read(1024*1024)
  554. if not c:
  555. break
  556. parser.feed(c)
  557. cpes = parser.close()
  558. for p in packages:
  559. if not p.cpeid:
  560. continue
  561. if p.cpeid in cpes:
  562. p.status['cpe'] = ("ok", "verified CPE identifier")
  563. else:
  564. p.status['cpe'] = ("error", "CPE version unknown in CPE database")
  565. def calculate_stats(packages):
  566. stats = defaultdict(int)
  567. stats['packages'] = len(packages)
  568. for pkg in packages:
  569. # If packages have multiple infra, take the first one. For the
  570. # vast majority of packages, the target and host infra are the
  571. # same. There are very few packages that use a different infra
  572. # for the host and target variants.
  573. if len(pkg.infras) > 0:
  574. infra = pkg.infras[0][1]
  575. stats["infra-%s" % infra] += 1
  576. else:
  577. stats["infra-unknown"] += 1
  578. if pkg.is_status_ok('license'):
  579. stats["license"] += 1
  580. else:
  581. stats["no-license"] += 1
  582. if pkg.is_status_ok('license-files'):
  583. stats["license-files"] += 1
  584. else:
  585. stats["no-license-files"] += 1
  586. if pkg.is_status_ok('hash'):
  587. stats["hash"] += 1
  588. else:
  589. stats["no-hash"] += 1
  590. if pkg.latest_version['status'] == RM_API_STATUS_FOUND_BY_DISTRO:
  591. stats["rmo-mapping"] += 1
  592. else:
  593. stats["rmo-no-mapping"] += 1
  594. if not pkg.latest_version['version']:
  595. stats["version-unknown"] += 1
  596. elif pkg.latest_version['version'] == pkg.current_version:
  597. stats["version-uptodate"] += 1
  598. else:
  599. stats["version-not-uptodate"] += 1
  600. stats["patches"] += pkg.patch_count
  601. stats["total-cves"] += len(pkg.cves)
  602. stats["total-unsure-cves"] += len(pkg.unsure_cves)
  603. if len(pkg.cves) != 0:
  604. stats["pkg-cves"] += 1
  605. if len(pkg.unsure_cves) != 0:
  606. stats["pkg-unsure-cves"] += 1
  607. if pkg.cpeid:
  608. stats["cpe-id"] += 1
  609. else:
  610. stats["no-cpe-id"] += 1
  611. return stats
  612. html_header = """
  613. <!DOCTYPE html>
  614. <html lang="en">
  615. <head>
  616. <meta charset="UTF-8">
  617. <meta name="viewport" content="width=device-width, initial-scale=1">
  618. <script>
  619. function sortGrid(sortLabel){
  620. let pkgSortArray = [], sortedPkgArray = [], pkgStringSortArray = [], pkgNumSortArray = [];
  621. let columnValues = Array.from(document.getElementsByClassName(sortLabel));
  622. columnValues.shift();
  623. columnValues.forEach((listing) => {
  624. let sortArr = [];
  625. sortArr[0] = listing.id.replace(sortLabel+"_", "");
  626. if (!listing.innerText){
  627. sortArr[1] = -1;
  628. } else {
  629. sortArr[1] = listing.innerText;
  630. };
  631. pkgSortArray.push(sortArr);
  632. })
  633. pkgSortArray.forEach((listing) => {
  634. if ( isNaN(parseInt(listing[1], 10)) ){
  635. pkgStringSortArray.push(listing);
  636. } else {
  637. listing[1] = parseFloat(listing[1]);
  638. pkgNumSortArray.push(listing);
  639. };
  640. })
  641. sortedStringPkgArray = pkgStringSortArray.sort(function(a, b) {
  642. const nameA = a[1].toUpperCase(); // ignore upper and lowercase
  643. const nameB = b[1].toUpperCase(); // ignore upper and lowercase
  644. if (nameA < nameB) { return -1; };
  645. if (nameA > nameB) { return 1; };
  646. return 0; // names must be equal
  647. });
  648. sortedNumPkgArray = pkgNumSortArray.sort(function(a, b) {
  649. return a[1] - b[1];
  650. });
  651. let triangleUp = String.fromCodePoint(32, 9652);
  652. let triangleDown = String.fromCodePoint(32, 9662);
  653. let columnName = document.getElementById(sortLabel);
  654. if (columnName.lastElementChild.innerText == triangleDown) {
  655. columnName.lastElementChild.innerText = triangleUp;
  656. sortedStringPkgArray.reverse();
  657. sortedNumPkgArray.reverse();
  658. sortedPkgArray = sortedNumPkgArray.concat(sortedStringPkgArray)
  659. } else {
  660. columnName.lastElementChild.innerText = triangleDown;
  661. sortedPkgArray = sortedStringPkgArray.concat(sortedNumPkgArray)
  662. }
  663. sortedPkgArray.forEach((listing) => {
  664. let row = Array.from(document.getElementsByClassName(listing[0]));
  665. let packageGrid = document.getElementById("package-grid");
  666. row.forEach((element) => { packageGrid.append(element)});
  667. })
  668. }
  669. </script>
  670. <style>
  671. .label {
  672. position: sticky;
  673. top: 1px;
  674. }
  675. .label{
  676. background: white;
  677. padding: 10px 2px 10px 2px;
  678. }
  679. #package-grid, #results-grid {
  680. display: grid;
  681. grid-gap: 2px;
  682. grid-template-columns: 1fr repeat(12, min-content);
  683. }
  684. #results-grid {
  685. grid-template-columns: 3fr 1fr;
  686. }
  687. .data {
  688. border: solid 1px gray;
  689. }
  690. .centered {
  691. text-align: center;
  692. }
  693. .wrong, .lotsofpatches, .invalid_url, .version-needs-update, .cpe-nok, .cve-nok {
  694. background: #ff9a69;
  695. }
  696. .correct, .nopatches, .good_url, .version-good, .cpe-ok, .cve-ok {
  697. background: #d2ffc4;
  698. }
  699. .somepatches, .missing_url, .version-unknown, .cpe-unknown, .cve-unknown {
  700. background: #ffd870;
  701. }
  702. .cve_ignored, .version-error {
  703. background: #ccc;
  704. }
  705. </style>
  706. <title>Statistics of Buildroot packages</title>
  707. </head>
  708. <body>
  709. <a href=\"#results\">Results</a><br/>
  710. <p id=\"sortable_hint\"></p>
  711. """
  712. html_footer = """
  713. <script>
  714. if (typeof sortGrid === "function") {
  715. document.getElementById("sortable_hint").innerHTML =
  716. "hint: the table can be sorted by clicking the column headers"
  717. }
  718. </script>
  719. </body>
  720. </html>
  721. """
  722. def infra_str(infra_list):
  723. if not infra_list:
  724. return "Unknown"
  725. elif len(infra_list) == 1:
  726. return "<b>%s</b><br/>%s" % (infra_list[0][1], infra_list[0][0])
  727. elif infra_list[0][1] == infra_list[1][1]:
  728. return "<b>%s</b><br/>%s + %s" % \
  729. (infra_list[0][1], infra_list[0][0], infra_list[1][0])
  730. else:
  731. return "<b>%s</b> (%s)<br/><b>%s</b> (%s)" % \
  732. (infra_list[0][1], infra_list[0][0],
  733. infra_list[1][1], infra_list[1][0])
  734. def boolean_str(b):
  735. if b:
  736. return "Yes"
  737. else:
  738. return "No"
  739. def dump_html_pkg(f, pkg):
  740. f.write( f'<div id=\"package_{pkg.name}\" \
  741. class=\"package data {pkg.name}\">{pkg.path}</div>\n')
  742. # Patch count
  743. data_field_id = f'patch_count_{pkg.name}'
  744. div_class = ["centered patch_count data"]
  745. div_class.append(pkg.name)
  746. if pkg.patch_count == 0:
  747. div_class.append("nopatches")
  748. elif pkg.patch_count < 5:
  749. div_class.append("somepatches")
  750. else:
  751. div_class.append("lotsofpatches")
  752. f.write( f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
  753. \">{str(pkg.patch_count)}</div>\n')
  754. # Infrastructure
  755. data_field_id = f'infrastructure_{pkg.name}'
  756. infra = infra_str(pkg.infras)
  757. div_class = ["centered infrastructure data"]
  758. div_class.append(pkg.name)
  759. if infra == "Unknown":
  760. div_class.append("wrong")
  761. else:
  762. div_class.append("correct")
  763. f.write( f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
  764. \">{infra_str(pkg.infras)}</div>\n')
  765. # License
  766. data_field_id = f'license_{pkg.name}'
  767. div_class = ["centered license data"]
  768. div_class.append(pkg.name)
  769. if pkg.is_status_ok('license'):
  770. div_class.append("correct")
  771. else:
  772. div_class.append("wrong")
  773. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
  774. \">{boolean_str(pkg.is_status_ok("license"))}</div>\n')
  775. # License files
  776. data_field_id = f'license_files_{pkg.name}'
  777. div_class = ["centered license_files data"]
  778. div_class.append(pkg.name)
  779. if pkg.is_status_ok('license-files'):
  780. div_class.append("correct")
  781. else:
  782. div_class.append("wrong")
  783. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
  784. \">{boolean_str(pkg.is_status_ok("license-files"))}</div>\n')
  785. # Hash
  786. data_field_id = f'hash_file_{pkg.name}'
  787. div_class = ["centered hash_file data"]
  788. div_class.append(pkg.name)
  789. if pkg.is_status_ok('hash'):
  790. div_class.append("correct")
  791. else:
  792. div_class.append("wrong")
  793. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
  794. \">{boolean_str(pkg.is_status_ok("hash"))}</div>\n')
  795. # Current version
  796. data_field_id = f'current_version_{pkg.name}'
  797. if len(pkg.current_version) > 20:
  798. current_version = pkg.current_version[:20] + "..."
  799. else:
  800. current_version = pkg.current_version
  801. f.write(f' <div id=\"{data_field_id}\" \
  802. class=\"centered current_version data {pkg.name}\">{current_version}</div>\n')
  803. # Latest version
  804. data_field_id = f'latest_version_{pkg.name}'
  805. div_class.append(pkg.name)
  806. div_class.append("latest_version data")
  807. if pkg.latest_version['status'] == RM_API_STATUS_ERROR:
  808. div_class.append("version-error")
  809. if pkg.latest_version['version'] is None:
  810. div_class.append("version-unknown")
  811. elif pkg.latest_version['version'] != pkg.current_version:
  812. div_class.append("version-needs-update")
  813. else:
  814. div_class.append("version-good")
  815. if pkg.latest_version['status'] == RM_API_STATUS_ERROR:
  816. latest_version_text = "<b>Error</b>"
  817. elif pkg.latest_version['status'] == RM_API_STATUS_NOT_FOUND:
  818. latest_version_text = "<b>Not found</b>"
  819. else:
  820. if pkg.latest_version['version'] is None:
  821. latest_version_text = "<b>Found, but no version</b>"
  822. else:
  823. latest_version_text = "<a href=\"https://release-monitoring.org/project/%s\"><b>%s</b></a>" % \
  824. (pkg.latest_version['id'], str(pkg.latest_version['version']))
  825. latest_version_text += "<br/>"
  826. if pkg.latest_version['status'] == RM_API_STATUS_FOUND_BY_DISTRO:
  827. latest_version_text += "found by <a href=\"https://release-monitoring.org/distro/Buildroot/\">distro</a>"
  828. else:
  829. latest_version_text += "found by guess"
  830. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">{latest_version_text}</div>\n')
  831. # Warnings
  832. data_field_id = f'warnings_{pkg.name}'
  833. div_class = ["centered warnings data"]
  834. div_class.append(pkg.name)
  835. if pkg.warnings == 0:
  836. div_class.append("correct")
  837. else:
  838. div_class.append("wrong")
  839. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">{pkg.warnings}</div>\n')
  840. # URL status
  841. data_field_id = f'upstream_url_{pkg.name}'
  842. div_class = ["centered upstream_url data"]
  843. div_class.append(pkg.name)
  844. url_str = pkg.status['url'][1]
  845. if pkg.status['url'][0] in ("error", "warning"):
  846. div_class.append("missing_url")
  847. if pkg.status['url'][0] == "error":
  848. div_class.append("invalid_url")
  849. url_str = "<a href=\"%s\">%s</a>" % (pkg.url, pkg.status['url'][1])
  850. else:
  851. div_class.append("good_url")
  852. url_str = "<a href=\"%s\">Link</a>" % pkg.url
  853. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">{url_str}</div>\n')
  854. # CVEs
  855. data_field_id = f'cves_{pkg.name}'
  856. div_class = ["centered cves data"]
  857. div_class.append(pkg.name)
  858. if pkg.is_status_ok("cve"):
  859. div_class.append("cve-ok")
  860. elif pkg.is_status_error("cve"):
  861. div_class.append("cve-nok")
  862. elif pkg.is_status_na("cve") and not pkg.is_actual_package:
  863. div_class.append("cve-ok")
  864. else:
  865. div_class.append("cve-unknown")
  866. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">\n')
  867. if pkg.is_status_error("cve"):
  868. for cve in pkg.cves:
  869. f.write(" <a href=\"https://security-tracker.debian.org/tracker/%s\">%s</a><br/>\n" % (cve, cve))
  870. for cve in pkg.unsure_cves:
  871. f.write(" <a href=\"https://security-tracker.debian.org/tracker/%s\">%s <i>(unsure)</i></a><br/>\n" % (cve, cve))
  872. elif pkg.is_status_na("cve"):
  873. f.write(" %s" % pkg.status['cve'][1])
  874. else:
  875. f.write(" N/A\n")
  876. f.write(" </div>\n")
  877. # CVEs Ignored
  878. data_field_id = f'ignored_cves_{pkg.name}'
  879. div_class = ["centered data ignored_cves"]
  880. div_class.append(pkg.name)
  881. if pkg.ignored_cves:
  882. div_class.append("cve_ignored")
  883. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">\n')
  884. for ignored_cve in pkg.ignored_cves:
  885. f.write(" <a href=\"https://security-tracker.debian.org/tracker/%s\">%s</a><br/>\n" % (ignored_cve, ignored_cve))
  886. f.write(" </div>\n")
  887. # CPE ID
  888. data_field_id = f'cpe_id_{pkg.name}'
  889. div_class = ["left cpe_id data"]
  890. div_class.append(pkg.name)
  891. if pkg.is_status_ok("cpe"):
  892. div_class.append("cpe-ok")
  893. elif pkg.is_status_error("cpe"):
  894. div_class.append("cpe-nok")
  895. elif pkg.is_status_na("cpe") and not pkg.is_actual_package:
  896. div_class.append("cpe-ok")
  897. else:
  898. div_class.append("cpe-unknown")
  899. f.write(f' <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">\n')
  900. if pkg.cpeid:
  901. f.write(" <code>%s</code>\n" % pkg.cpeid)
  902. if not pkg.is_status_ok("cpe"):
  903. if pkg.is_actual_package and pkg.current_version:
  904. if pkg.cpeid:
  905. f.write(" <br/>%s <a href=\"https://nvd.nist.gov/products/cpe/search/results?namingFormat=2.3&keyword=%s\">(Search)</a>\n" % # noqa: E501
  906. (pkg.status['cpe'][1], ":".join(pkg.cpeid.split(":")[0:5])))
  907. else:
  908. f.write(" %s <a href=\"https://nvd.nist.gov/products/cpe/search/results?namingFormat=2.3&keyword=%s\">(Search)</a>\n" % # noqa: E501
  909. (pkg.status['cpe'][1], pkg.name))
  910. else:
  911. f.write(" %s\n" % pkg.status['cpe'][1])
  912. f.write(" </div>\n")
  913. def dump_html_all_pkgs(f, packages):
  914. f.write("""
  915. <div id=\"package-grid\">
  916. <div style="grid-column: 1;" onclick="sortGrid(this.id)" id=\"package\" class=\"package data label\"><span>Package</span><span></span></div>
  917. <div style="grid-column: 2;" onclick="sortGrid(this.id)" id=\"patch_count\" class=\"centered patch_count data label\"><span>Patch count</span><span></span></div>
  918. <div style="grid-column: 3;" onclick="sortGrid(this.id)" id=\"infrastructure\" class=\"centered infrastructure data label\">Infrastructure<span></span></div>
  919. <div style="grid-column: 4;" onclick="sortGrid(this.id)" id=\"license\" class=\"centered license data label\"><span>License</span><span></span></div>
  920. <div style="grid-column: 5;" onclick="sortGrid(this.id)" id=\"license_files\" class=\"centered license_files data label\"><span>License files</span><span></span></div>
  921. <div style="grid-column: 6;" onclick="sortGrid(this.id)" id=\"hash_file\" class=\"centered hash_file data label\"><span>Hash file</span><span></span></div>
  922. <div style="grid-column: 7;" onclick="sortGrid(this.id)" id=\"current_version\" class=\"centered current_version data label\"><span>Current version</span><span></span></div>
  923. <div style="grid-column: 8;" onclick="sortGrid(this.id)" id=\"latest_version\" class=\"centered latest_version data label\"><span>Latest version</span><span></span></div>
  924. <div style="grid-column: 9;" onclick="sortGrid(this.id)" id=\"warnings\" class=\"centered warnings data label\"><span>Warnings</span><span></span></div>
  925. <div style="grid-column: 10;" onclick="sortGrid(this.id)" id=\"upstream_url\" class=\"centered upstream_url data label\"><span>Upstream URL</span><span></span></div>
  926. <div style="grid-column: 11;" onclick="sortGrid(this.id)" id=\"cves\" class=\"centered cves data label\"><span>CVEs</span><span></span></div>
  927. <div style="grid-column: 12;" onclick="sortGrid(this.id)" id=\"ignored_cves\" class=\"centered ignored_cves data label\"><span>CVEs Ignored</span><span></span></div>
  928. <div style="grid-column: 13;" onclick="sortGrid(this.id)" id=\"cpe_id\" class=\"centered cpe_id data label\"><span>CPE ID</span><span></span></div>
  929. """)
  930. for pkg in sorted(packages):
  931. dump_html_pkg(f, pkg)
  932. f.write("</div>")
  933. def dump_html_stats(f, stats):
  934. f.write("<a id=\"results\"></a>\n")
  935. f.write("<div class=\"data\" id=\"results-grid\">\n")
  936. infras = [infra[6:] for infra in stats.keys() if infra.startswith("infra-")]
  937. for infra in infras:
  938. f.write(" <div class=\"data\">Packages using the <i>%s</i> infrastructure</div><div class=\"data\">%s</div>\n" %
  939. (infra, stats["infra-%s" % infra]))
  940. f.write(" <div class=\"data\">Packages having license information</div><div class=\"data\">%s</div>\n" %
  941. stats["license"])
  942. f.write(" <div class=\"data\">Packages not having license information</div><div class=\"data\">%s</div>\n" %
  943. stats["no-license"])
  944. f.write(" <div class=\"data\">Packages having license files information</div><div class=\"data\">%s</div>\n" %
  945. stats["license-files"])
  946. f.write(" <div class=\"data\">Packages not having license files information</div><div class=\"data\">%s</div>\n" %
  947. stats["no-license-files"])
  948. f.write(" <div class=\"data\">Packages having a hash file</div><div class=\"data\">%s</div>\n" %
  949. stats["hash"])
  950. f.write(" <div class=\"data\">Packages not having a hash file</div><div class=\"data\">%s</div>\n" %
  951. stats["no-hash"])
  952. f.write(" <div class=\"data\">Total number of patches</div><div class=\"data\">%s</div>\n" %
  953. stats["patches"])
  954. f.write("<div class=\"data\">Packages having a mapping on <i>release-monitoring.org</i></div><div class=\"data\">%s</div>\n" %
  955. stats["rmo-mapping"])
  956. f.write("<div class=\"data\">Packages lacking a mapping on <i>release-monitoring.org</i></div><div class=\"data\">%s</div>\n" %
  957. stats["rmo-no-mapping"])
  958. f.write("<div class=\"data\">Packages that are up-to-date</div><div class=\"data\">%s</div>\n" %
  959. stats["version-uptodate"])
  960. f.write("<div class=\"data\">Packages that are not up-to-date</div><div class=\"data\">%s</div>\n" %
  961. stats["version-not-uptodate"])
  962. f.write("<div class=\"data\">Packages with no known upstream version</div><div class=\"data\">%s</div>\n" %
  963. stats["version-unknown"])
  964. f.write("<div class=\"data\">Packages affected by CVEs</div><div class=\"data\">%s</div>\n" %
  965. stats["pkg-cves"])
  966. f.write("<div class=\"data\">Total number of CVEs affecting all packages</div><div class=\"data\">%s</div>\n" %
  967. stats["total-cves"])
  968. f.write("<div class=\"data\">Packages affected by unsure CVEs</div><div class=\"data\">%s</div>\n" %
  969. stats["pkg-unsure-cves"])
  970. f.write("<div class=\"data\">Total number of unsure CVEs affecting all packages</div><div class=\"data\">%s</div>\n" %
  971. stats["total-unsure-cves"])
  972. f.write("<div class=\"data\">Packages with CPE ID</div><div class=\"data\">%s</div>\n" %
  973. stats["cpe-id"])
  974. f.write("<div class=\"data\">Packages without CPE ID</div><div class=\"data\">%s</div>\n" %
  975. stats["no-cpe-id"])
  976. f.write("</div>\n")
  977. def dump_html_gen_info(f, date, commit):
  978. # Updated on Mon Feb 19 08:12:08 CET 2018, Git commit aa77030b8f5e41f1c53eb1c1ad664b8c814ba032
  979. f.write("<p><i>Updated on %s, git commit %s</i></p>\n" % (str(date), commit))
  980. def dump_html(packages, stats, date, commit, output):
  981. with open(output, 'w') as f:
  982. f.write(html_header)
  983. dump_html_all_pkgs(f, packages)
  984. dump_html_stats(f, stats)
  985. dump_html_gen_info(f, date, commit)
  986. f.write(html_footer)
  987. def dump_json(packages, defconfigs, stats, date, commit, output):
  988. # Format packages as a dictionnary instead of a list
  989. # Exclude local field that does not contains real date
  990. excluded_fields = ['url_worker', 'name']
  991. pkgs = {
  992. pkg.name: {
  993. k: v
  994. for k, v in pkg.__dict__.items()
  995. if k not in excluded_fields
  996. } for pkg in packages
  997. }
  998. defconfigs = {
  999. d.name: {
  1000. k: v
  1001. for k, v in d.__dict__.items()
  1002. } for d in defconfigs
  1003. }
  1004. # Aggregate infrastructures into a single dict entry
  1005. statistics = {
  1006. k: v
  1007. for k, v in stats.items()
  1008. if not k.startswith('infra-')
  1009. }
  1010. statistics['infra'] = {k[6:]: v for k, v in stats.items() if k.startswith('infra-')}
  1011. # The actual structure to dump, add commit and date to it
  1012. final = {'packages': pkgs,
  1013. 'stats': statistics,
  1014. 'defconfigs': defconfigs,
  1015. 'package_status_checks': Package.status_checks,
  1016. 'commit': commit,
  1017. 'date': str(date)}
  1018. with open(output, 'w') as f:
  1019. json.dump(final, f, indent=2, separators=(',', ': '))
  1020. f.write('\n')
  1021. def resolvepath(path):
  1022. return os.path.abspath(os.path.expanduser(path))
  1023. def list_str(values):
  1024. return values.split(',')
  1025. def parse_args():
  1026. parser = argparse.ArgumentParser()
  1027. output = parser.add_argument_group('output', 'Output file(s)')
  1028. output.add_argument('--html', dest='html', type=resolvepath,
  1029. help='HTML output file')
  1030. output.add_argument('--json', dest='json', type=resolvepath,
  1031. help='JSON output file')
  1032. packages = parser.add_mutually_exclusive_group()
  1033. packages.add_argument('-c', dest='configpackages', action='store_true',
  1034. help='Apply to packages enabled in current configuration')
  1035. packages.add_argument('-n', dest='npackages', type=int, action='store',
  1036. help='Number of packages')
  1037. packages.add_argument('-p', dest='packages', action='store',
  1038. help='List of packages (comma separated)')
  1039. parser.add_argument('--nvd-path', dest='nvd_path',
  1040. help='Path to the local NVD database', type=resolvepath)
  1041. parser.add_argument('--disable', type=list_str,
  1042. help='Features to disable, comma-separated (cve, upstream, url, cpe, warning)',
  1043. default=[])
  1044. args = parser.parse_args()
  1045. if not args.html and not args.json:
  1046. parser.error('at least one of --html or --json (or both) is required')
  1047. return args
  1048. def __main__():
  1049. global cvecheck
  1050. args = parse_args()
  1051. if args.nvd_path:
  1052. import cve as cvecheck
  1053. if args.packages:
  1054. package_list = args.packages.split(",")
  1055. elif args.configpackages:
  1056. package_list = get_config_packages()
  1057. else:
  1058. package_list = None
  1059. date = datetime.datetime.utcnow()
  1060. commit = subprocess.check_output(['git', '-C', brpath,
  1061. 'rev-parse',
  1062. 'HEAD']).splitlines()[0].decode()
  1063. print("Build package list ...")
  1064. packages = get_pkglist(args.npackages, package_list)
  1065. print("Getting developers ...")
  1066. developers = parse_developers()
  1067. print("Build defconfig list ...")
  1068. defconfigs = get_defconfig_list()
  1069. for d in defconfigs:
  1070. d.set_developers(developers)
  1071. print("Getting package make info ...")
  1072. package_init_make_info()
  1073. print("Getting package details ...")
  1074. for pkg in packages:
  1075. pkg.set_infra()
  1076. pkg.set_license()
  1077. pkg.set_hash_info()
  1078. pkg.set_patch_count()
  1079. if "warnings" not in args.disable:
  1080. pkg.set_check_package_warnings()
  1081. pkg.set_current_version()
  1082. pkg.set_cpeid()
  1083. pkg.set_url()
  1084. pkg.set_ignored_cves()
  1085. pkg.set_developers(developers)
  1086. if "url" not in args.disable:
  1087. print("Checking URL status")
  1088. loop = asyncio.get_event_loop()
  1089. loop.run_until_complete(check_package_urls(packages))
  1090. if "upstream" not in args.disable:
  1091. print("Getting latest versions ...")
  1092. loop = asyncio.get_event_loop()
  1093. loop.run_until_complete(check_package_latest_version(packages))
  1094. if "cve" not in args.disable and args.nvd_path:
  1095. print("Checking packages CVEs")
  1096. check_package_cves(args.nvd_path, packages)
  1097. if "cpe" not in args.disable and args.nvd_path:
  1098. print("Checking packages CPEs")
  1099. check_package_cpes(args.nvd_path, packages)
  1100. print("Calculate stats")
  1101. stats = calculate_stats(packages)
  1102. if args.html:
  1103. print("Write HTML")
  1104. dump_html(packages, stats, date, commit, args.html)
  1105. if args.json:
  1106. print("Write JSON")
  1107. dump_json(packages, defconfigs, stats, date, commit, args.json)
  1108. __main__()