pkg-stats 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119
  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. brpath = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
  29. sys.path.append(os.path.join(brpath, "utils"))
  30. from getdeveloperlib import parse_developers # noqa: E402
  31. INFRA_RE = re.compile(r"\$\(eval \$\(([a-z-]*)-package\)\)")
  32. URL_RE = re.compile(r"\s*https?://\S*\s*$")
  33. RM_API_STATUS_ERROR = 1
  34. RM_API_STATUS_FOUND_BY_DISTRO = 2
  35. RM_API_STATUS_FOUND_BY_PATTERN = 3
  36. RM_API_STATUS_NOT_FOUND = 4
  37. class Defconfig:
  38. def __init__(self, name, path):
  39. self.name = name
  40. self.path = path
  41. self.developers = None
  42. def set_developers(self, developers):
  43. """
  44. Fills in the .developers field
  45. """
  46. self.developers = [
  47. developer.name
  48. for developer in developers
  49. if developer.hasfile(self.path)
  50. ]
  51. def get_defconfig_list():
  52. """
  53. Builds the list of Buildroot defconfigs, returning a list of Defconfig
  54. objects.
  55. """
  56. return [
  57. Defconfig(name[:-len('_defconfig')], os.path.join('configs', name))
  58. for name in os.listdir(os.path.join(brpath, 'configs'))
  59. if name.endswith('_defconfig')
  60. ]
  61. class Package:
  62. all_licenses = dict()
  63. all_license_files = list()
  64. all_versions = dict()
  65. all_ignored_cves = dict()
  66. all_cpeids = dict()
  67. # This is the list of all possible checks. Add new checks to this list so
  68. # a tool that post-processeds the json output knows the checks before
  69. # iterating over the packages.
  70. status_checks = ['cve', 'developers', 'hash', 'license',
  71. 'license-files', 'patches', 'pkg-check', 'url', 'version']
  72. def __init__(self, name, path):
  73. self.name = name
  74. self.path = path
  75. self.pkg_path = os.path.dirname(path)
  76. self.infras = None
  77. self.license = None
  78. self.has_license = False
  79. self.has_license_files = False
  80. self.has_hash = False
  81. self.patch_files = []
  82. self.warnings = 0
  83. self.current_version = None
  84. self.url = None
  85. self.url_worker = None
  86. self.cpeid = None
  87. self.cves = list()
  88. self.latest_version = {'status': RM_API_STATUS_ERROR, 'version': None, 'id': None}
  89. self.status = {}
  90. def pkgvar(self):
  91. return self.name.upper().replace("-", "_")
  92. def set_url(self):
  93. """
  94. Fills in the .url field
  95. """
  96. self.status['url'] = ("warning", "no Config.in")
  97. pkgdir = os.path.dirname(os.path.join(brpath, self.path))
  98. for filename in os.listdir(pkgdir):
  99. if fnmatch.fnmatch(filename, 'Config.*'):
  100. fp = open(os.path.join(pkgdir, filename), "r")
  101. for config_line in fp:
  102. if URL_RE.match(config_line):
  103. self.url = config_line.strip()
  104. self.status['url'] = ("ok", "found")
  105. fp.close()
  106. return
  107. self.status['url'] = ("error", "missing")
  108. fp.close()
  109. @property
  110. def patch_count(self):
  111. return len(self.patch_files)
  112. @property
  113. def has_valid_infra(self):
  114. try:
  115. if self.infras[0][1] == 'virtual':
  116. return False
  117. except IndexError:
  118. return False
  119. return True
  120. def set_infra(self):
  121. """
  122. Fills in the .infras field
  123. """
  124. self.infras = list()
  125. with open(os.path.join(brpath, self.path), 'r') as f:
  126. lines = f.readlines()
  127. for l in lines:
  128. match = INFRA_RE.match(l)
  129. if not match:
  130. continue
  131. infra = match.group(1)
  132. if infra.startswith("host-"):
  133. self.infras.append(("host", infra[5:]))
  134. else:
  135. self.infras.append(("target", infra))
  136. def set_license(self):
  137. """
  138. Fills in the .status['license'] and .status['license-files'] fields
  139. """
  140. if not self.has_valid_infra:
  141. self.status['license'] = ("na", "no valid package infra")
  142. self.status['license-files'] = ("na", "no valid package infra")
  143. return
  144. var = self.pkgvar()
  145. self.status['license'] = ("error", "missing")
  146. self.status['license-files'] = ("error", "missing")
  147. if var in self.all_licenses:
  148. self.license = self.all_licenses[var]
  149. self.status['license'] = ("ok", "found")
  150. if var in self.all_license_files:
  151. self.status['license-files'] = ("ok", "found")
  152. def set_hash_info(self):
  153. """
  154. Fills in the .status['hash'] field
  155. """
  156. if not self.has_valid_infra:
  157. self.status['hash'] = ("na", "no valid package infra")
  158. self.status['hash-license'] = ("na", "no valid package infra")
  159. return
  160. hashpath = self.path.replace(".mk", ".hash")
  161. if os.path.exists(os.path.join(brpath, hashpath)):
  162. self.status['hash'] = ("ok", "found")
  163. else:
  164. self.status['hash'] = ("error", "missing")
  165. def set_patch_count(self):
  166. """
  167. Fills in the .patch_count, .patch_files and .status['patches'] fields
  168. """
  169. if not self.has_valid_infra:
  170. self.status['patches'] = ("na", "no valid package infra")
  171. return
  172. pkgdir = os.path.dirname(os.path.join(brpath, self.path))
  173. for subdir, _, _ in os.walk(pkgdir):
  174. self.patch_files = fnmatch.filter(os.listdir(subdir), '*.patch')
  175. if self.patch_count == 0:
  176. self.status['patches'] = ("ok", "no patches")
  177. elif self.patch_count < 5:
  178. self.status['patches'] = ("warning", "some patches")
  179. else:
  180. self.status['patches'] = ("error", "lots of patches")
  181. def set_current_version(self):
  182. """
  183. Fills in the .current_version field
  184. """
  185. var = self.pkgvar()
  186. if var in self.all_versions:
  187. self.current_version = self.all_versions[var]
  188. def set_cpeid(self):
  189. """
  190. Fills in the .cpeid field
  191. """
  192. var = self.pkgvar()
  193. if not self.has_valid_infra:
  194. self.status['cpe'] = ("na", "no valid package infra")
  195. return
  196. if var in self.all_cpeids:
  197. self.cpeid = self.all_cpeids[var]
  198. self.status['cpe'] = ("ok", "verified CPE identifier")
  199. else:
  200. self.status['cpe'] = ("error", "no verified CPE identifier")
  201. def set_check_package_warnings(self):
  202. """
  203. Fills in the .warnings and .status['pkg-check'] fields
  204. """
  205. cmd = [os.path.join(brpath, "utils/check-package")]
  206. pkgdir = os.path.dirname(os.path.join(brpath, self.path))
  207. self.status['pkg-check'] = ("error", "Missing")
  208. for root, dirs, files in os.walk(pkgdir):
  209. for f in files:
  210. if f.endswith(".mk") or f.endswith(".hash") or f == "Config.in" or f == "Config.in.host":
  211. cmd.append(os.path.join(root, f))
  212. o = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[1]
  213. lines = o.splitlines()
  214. for line in lines:
  215. m = re.match("^([0-9]*) warnings generated", line.decode())
  216. if m:
  217. self.warnings = int(m.group(1))
  218. if self.warnings == 0:
  219. self.status['pkg-check'] = ("ok", "no warnings")
  220. else:
  221. self.status['pkg-check'] = ("error", "{} warnings".format(self.warnings))
  222. return
  223. @property
  224. def ignored_cves(self):
  225. """
  226. Give the list of CVEs ignored by the package
  227. """
  228. return list(self.all_ignored_cves.get(self.pkgvar(), []))
  229. def set_developers(self, developers):
  230. """
  231. Fills in the .developers and .status['developers'] field
  232. """
  233. self.developers = [
  234. dev.name
  235. for dev in developers
  236. if dev.hasfile(self.path)
  237. ]
  238. if self.developers:
  239. self.status['developers'] = ("ok", "{} developers".format(len(self.developers)))
  240. else:
  241. self.status['developers'] = ("warning", "no developers")
  242. def is_status_ok(self, name):
  243. return self.status[name][0] == 'ok'
  244. def __eq__(self, other):
  245. return self.path == other.path
  246. def __lt__(self, other):
  247. return self.path < other.path
  248. def __str__(self):
  249. return "%s (path='%s', license='%s', license_files='%s', hash='%s', patches=%d)" % \
  250. (self.name, self.path, self.is_status_ok('license'),
  251. self.is_status_ok('license-files'), self.status['hash'], self.patch_count)
  252. def get_pkglist(npackages, package_list):
  253. """
  254. Builds the list of Buildroot packages, returning a list of Package
  255. objects. Only the .name and .path fields of the Package object are
  256. initialized.
  257. npackages: limit to N packages
  258. package_list: limit to those packages in this list
  259. """
  260. WALK_USEFUL_SUBDIRS = ["boot", "linux", "package", "toolchain"]
  261. WALK_EXCLUDES = ["boot/common.mk",
  262. "linux/linux-ext-.*.mk",
  263. "package/freescale-imx/freescale-imx.mk",
  264. "package/gcc/gcc.mk",
  265. "package/gstreamer/gstreamer.mk",
  266. "package/gstreamer1/gstreamer1.mk",
  267. "package/gtk2-themes/gtk2-themes.mk",
  268. "package/matchbox/matchbox.mk",
  269. "package/opengl/opengl.mk",
  270. "package/qt5/qt5.mk",
  271. "package/x11r7/x11r7.mk",
  272. "package/doc-asciidoc.mk",
  273. "package/pkg-.*.mk",
  274. "toolchain/toolchain-external/pkg-toolchain-external.mk",
  275. "toolchain/toolchain-external/toolchain-external.mk",
  276. "toolchain/toolchain.mk",
  277. "toolchain/helpers.mk",
  278. "toolchain/toolchain-wrapper.mk"]
  279. packages = list()
  280. count = 0
  281. for root, dirs, files in os.walk(brpath):
  282. root = os.path.relpath(root, brpath)
  283. rootdir = root.split("/")
  284. if len(rootdir) < 1:
  285. continue
  286. if rootdir[0] not in WALK_USEFUL_SUBDIRS:
  287. continue
  288. for f in files:
  289. if not f.endswith(".mk"):
  290. continue
  291. # Strip ending ".mk"
  292. pkgname = f[:-3]
  293. if package_list and pkgname not in package_list:
  294. continue
  295. pkgpath = os.path.join(root, f)
  296. skip = False
  297. for exclude in WALK_EXCLUDES:
  298. if re.match(exclude, pkgpath):
  299. skip = True
  300. continue
  301. if skip:
  302. continue
  303. p = Package(pkgname, pkgpath)
  304. packages.append(p)
  305. count += 1
  306. if npackages and count == npackages:
  307. return packages
  308. return packages
  309. def get_config_packages():
  310. cmd = ["make", "--no-print-directory", "show-info"]
  311. js = json.loads(subprocess.check_output(cmd))
  312. return js.keys()
  313. def package_init_make_info():
  314. # Fetch all variables at once
  315. variables = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y", "-s", "printvars",
  316. "VARS=%_LICENSE %_LICENSE_FILES %_VERSION %_IGNORE_CVES %_CPE_ID"])
  317. variable_list = variables.decode().splitlines()
  318. # We process first the host package VERSION, and then the target
  319. # package VERSION. This means that if a package exists in both
  320. # target and host variants, with different values (eg. version
  321. # numbers (unlikely)), we'll report the target one.
  322. variable_list = [x[5:] for x in variable_list if x.startswith("HOST_")] + \
  323. [x for x in variable_list if not x.startswith("HOST_")]
  324. for l in variable_list:
  325. # Get variable name and value
  326. pkgvar, value = l.split("=")
  327. # Strip the suffix according to the variable
  328. if pkgvar.endswith("_LICENSE"):
  329. # If value is "unknown", no license details available
  330. if value == "unknown":
  331. continue
  332. pkgvar = pkgvar[:-8]
  333. Package.all_licenses[pkgvar] = value
  334. elif pkgvar.endswith("_LICENSE_FILES"):
  335. if pkgvar.endswith("_MANIFEST_LICENSE_FILES"):
  336. continue
  337. pkgvar = pkgvar[:-14]
  338. Package.all_license_files.append(pkgvar)
  339. elif pkgvar.endswith("_VERSION"):
  340. if pkgvar.endswith("_DL_VERSION"):
  341. continue
  342. pkgvar = pkgvar[:-8]
  343. Package.all_versions[pkgvar] = value
  344. elif pkgvar.endswith("_IGNORE_CVES"):
  345. pkgvar = pkgvar[:-12]
  346. Package.all_ignored_cves[pkgvar] = value.split()
  347. elif pkgvar.endswith("_CPE_ID"):
  348. pkgvar = pkgvar[:-7]
  349. Package.all_cpeids[pkgvar] = value
  350. check_url_count = 0
  351. async def check_url_status(session, pkg, npkgs, retry=True):
  352. global check_url_count
  353. try:
  354. async with session.get(pkg.url) as resp:
  355. if resp.status >= 400:
  356. pkg.status['url'] = ("error", "invalid {}".format(resp.status))
  357. check_url_count += 1
  358. print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name))
  359. return
  360. except (aiohttp.ClientError, asyncio.TimeoutError):
  361. if retry:
  362. return await check_url_status(session, pkg, npkgs, retry=False)
  363. else:
  364. pkg.status['url'] = ("error", "invalid (err)")
  365. check_url_count += 1
  366. print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name))
  367. return
  368. pkg.status['url'] = ("ok", "valid")
  369. check_url_count += 1
  370. print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name))
  371. async def check_package_urls(packages):
  372. tasks = []
  373. connector = aiohttp.TCPConnector(limit_per_host=5)
  374. async with aiohttp.ClientSession(connector=connector, trust_env=True) as sess:
  375. packages = [p for p in packages if p.status['url'][0] == 'ok']
  376. for pkg in packages:
  377. tasks.append(asyncio.ensure_future(check_url_status(sess, pkg, len(packages))))
  378. await asyncio.wait(tasks)
  379. def check_package_latest_version_set_status(pkg, status, version, identifier):
  380. pkg.latest_version = {
  381. "status": status,
  382. "version": version,
  383. "id": identifier,
  384. }
  385. if pkg.latest_version['status'] == RM_API_STATUS_ERROR:
  386. pkg.status['version'] = ('warning', "Release Monitoring API error")
  387. elif pkg.latest_version['status'] == RM_API_STATUS_NOT_FOUND:
  388. pkg.status['version'] = ('warning', "Package not found on Release Monitoring")
  389. if pkg.latest_version['version'] is None:
  390. pkg.status['version'] = ('warning', "No upstream version available on Release Monitoring")
  391. elif pkg.latest_version['version'] != pkg.current_version:
  392. pkg.status['version'] = ('error', "The newer version {} is available upstream".format(pkg.latest_version['version']))
  393. else:
  394. pkg.status['version'] = ('ok', 'up-to-date')
  395. async def check_package_get_latest_version_by_distro(session, pkg, retry=True):
  396. url = "https://release-monitoring.org//api/project/Buildroot/%s" % pkg.name
  397. try:
  398. async with session.get(url) as resp:
  399. if resp.status != 200:
  400. return False
  401. data = await resp.json()
  402. version = data['version'] if 'version' in data else None
  403. check_package_latest_version_set_status(pkg,
  404. RM_API_STATUS_FOUND_BY_DISTRO,
  405. version,
  406. data['id'])
  407. return True
  408. except (aiohttp.ClientError, asyncio.TimeoutError):
  409. if retry:
  410. return await check_package_get_latest_version_by_distro(session, pkg, retry=False)
  411. else:
  412. return False
  413. async def check_package_get_latest_version_by_guess(session, pkg, retry=True):
  414. url = "https://release-monitoring.org/api/projects/?pattern=%s" % pkg.name
  415. try:
  416. async with session.get(url) as resp:
  417. if resp.status != 200:
  418. return False
  419. data = await resp.json()
  420. # filter projects that have the right name and a version defined
  421. projects = [p for p in data['projects'] if p['name'] == pkg.name and 'version' in p]
  422. projects.sort(key=lambda x: x['id'])
  423. if len(projects) > 0:
  424. check_package_latest_version_set_status(pkg,
  425. RM_API_STATUS_FOUND_BY_DISTRO,
  426. projects[0]['version'],
  427. projects[0]['id'])
  428. return True
  429. except (aiohttp.ClientError, asyncio.TimeoutError):
  430. if retry:
  431. return await check_package_get_latest_version_by_guess(session, pkg, retry=False)
  432. else:
  433. return False
  434. check_latest_count = 0
  435. async def check_package_latest_version_get(session, pkg, npkgs):
  436. global check_latest_count
  437. if await check_package_get_latest_version_by_distro(session, pkg):
  438. check_latest_count += 1
  439. print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name))
  440. return
  441. if await check_package_get_latest_version_by_guess(session, pkg):
  442. check_latest_count += 1
  443. print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name))
  444. return
  445. check_package_latest_version_set_status(pkg,
  446. RM_API_STATUS_NOT_FOUND,
  447. None, None)
  448. check_latest_count += 1
  449. print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name))
  450. async def check_package_latest_version(packages):
  451. """
  452. Fills in the .latest_version field of all Package objects
  453. This field is a dict and has the following keys:
  454. - status: one of RM_API_STATUS_ERROR,
  455. RM_API_STATUS_FOUND_BY_DISTRO, RM_API_STATUS_FOUND_BY_PATTERN,
  456. RM_API_STATUS_NOT_FOUND
  457. - version: string containing the latest version known by
  458. release-monitoring.org for this package
  459. - id: string containing the id of the project corresponding to this
  460. package, as known by release-monitoring.org
  461. """
  462. for pkg in [p for p in packages if not p.has_valid_infra]:
  463. pkg.status['version'] = ("na", "no valid package infra")
  464. tasks = []
  465. connector = aiohttp.TCPConnector(limit_per_host=5)
  466. async with aiohttp.ClientSession(connector=connector, trust_env=True) as sess:
  467. packages = [p for p in packages if p.has_valid_infra]
  468. for pkg in packages:
  469. tasks.append(asyncio.ensure_future(check_package_latest_version_get(sess, pkg, len(packages))))
  470. await asyncio.wait(tasks)
  471. def check_package_cve_affects(cve, cpe_product_pkgs):
  472. for product in cve.affected_products:
  473. if product not in cpe_product_pkgs:
  474. continue
  475. for pkg in cpe_product_pkgs[product]:
  476. if cve.affects(pkg.name, pkg.current_version, pkg.ignored_cves, pkg.cpeid) == cve.CVE_AFFECTS:
  477. pkg.cves.append(cve.identifier)
  478. def check_package_cves(nvd_path, packages):
  479. if not os.path.isdir(nvd_path):
  480. os.makedirs(nvd_path)
  481. cpe_product_pkgs = defaultdict(list)
  482. for pkg in packages:
  483. if not pkg.has_valid_infra:
  484. pkg.status['cve'] = ("na", "no valid package infra")
  485. continue
  486. if not pkg.current_version:
  487. pkg.status['cve'] = ("na", "no version information available")
  488. continue
  489. if pkg.cpeid:
  490. cpe_product = cvecheck.cpe_product(pkg.cpeid)
  491. cpe_product_pkgs[cpe_product].append(pkg)
  492. else:
  493. cpe_product_pkgs[pkg.name].append(pkg)
  494. for cve in cvecheck.CVE.read_nvd_dir(nvd_path):
  495. check_package_cve_affects(cve, cpe_product_pkgs)
  496. for pkg in packages:
  497. if 'cve' not in pkg.status:
  498. if pkg.cves:
  499. pkg.status['cve'] = ("error", "affected by CVEs")
  500. else:
  501. pkg.status['cve'] = ("ok", "not affected by CVEs")
  502. def calculate_stats(packages):
  503. stats = defaultdict(int)
  504. stats['packages'] = len(packages)
  505. for pkg in packages:
  506. # If packages have multiple infra, take the first one. For the
  507. # vast majority of packages, the target and host infra are the
  508. # same. There are very few packages that use a different infra
  509. # for the host and target variants.
  510. if len(pkg.infras) > 0:
  511. infra = pkg.infras[0][1]
  512. stats["infra-%s" % infra] += 1
  513. else:
  514. stats["infra-unknown"] += 1
  515. if pkg.is_status_ok('license'):
  516. stats["license"] += 1
  517. else:
  518. stats["no-license"] += 1
  519. if pkg.is_status_ok('license-files'):
  520. stats["license-files"] += 1
  521. else:
  522. stats["no-license-files"] += 1
  523. if pkg.is_status_ok('hash'):
  524. stats["hash"] += 1
  525. else:
  526. stats["no-hash"] += 1
  527. if pkg.latest_version['status'] == RM_API_STATUS_FOUND_BY_DISTRO:
  528. stats["rmo-mapping"] += 1
  529. else:
  530. stats["rmo-no-mapping"] += 1
  531. if not pkg.latest_version['version']:
  532. stats["version-unknown"] += 1
  533. elif pkg.latest_version['version'] == pkg.current_version:
  534. stats["version-uptodate"] += 1
  535. else:
  536. stats["version-not-uptodate"] += 1
  537. stats["patches"] += pkg.patch_count
  538. stats["total-cves"] += len(pkg.cves)
  539. if len(pkg.cves) != 0:
  540. stats["pkg-cves"] += 1
  541. if pkg.cpeid:
  542. stats["cpe-id"] += 1
  543. else:
  544. stats["no-cpe-id"] += 1
  545. return stats
  546. html_header = """
  547. <head>
  548. <script src=\"https://www.kryogenix.org/code/browser/sorttable/sorttable.js\"></script>
  549. <style type=\"text/css\">
  550. table {
  551. width: 100%;
  552. }
  553. td {
  554. border: 1px solid black;
  555. }
  556. td.centered {
  557. text-align: center;
  558. }
  559. td.wrong {
  560. background: #ff9a69;
  561. }
  562. td.correct {
  563. background: #d2ffc4;
  564. }
  565. td.nopatches {
  566. background: #d2ffc4;
  567. }
  568. td.somepatches {
  569. background: #ffd870;
  570. }
  571. td.lotsofpatches {
  572. background: #ff9a69;
  573. }
  574. td.good_url {
  575. background: #d2ffc4;
  576. }
  577. td.missing_url {
  578. background: #ffd870;
  579. }
  580. td.invalid_url {
  581. background: #ff9a69;
  582. }
  583. td.version-good {
  584. background: #d2ffc4;
  585. }
  586. td.version-needs-update {
  587. background: #ff9a69;
  588. }
  589. td.version-unknown {
  590. background: #ffd870;
  591. }
  592. td.version-error {
  593. background: #ccc;
  594. }
  595. td.cpe-ok {
  596. background: #d2ffc4;
  597. }
  598. td.cpe-nok {
  599. background: #ff9a69;
  600. }
  601. td.cpe-unknown {
  602. background: #ffd870;
  603. }
  604. td.cve-ok {
  605. background: #d2ffc4;
  606. }
  607. td.cve-nok {
  608. background: #ff9a69;
  609. }
  610. td.cve-unknown {
  611. background: #ffd870;
  612. }
  613. </style>
  614. <title>Statistics of Buildroot packages</title>
  615. </head>
  616. <a href=\"#results\">Results</a><br/>
  617. <p id=\"sortable_hint\"></p>
  618. """
  619. html_footer = """
  620. </body>
  621. <script>
  622. if (typeof sorttable === \"object\") {
  623. document.getElementById(\"sortable_hint\").innerHTML =
  624. \"hint: the table can be sorted by clicking the column headers\"
  625. }
  626. </script>
  627. </html>
  628. """
  629. def infra_str(infra_list):
  630. if not infra_list:
  631. return "Unknown"
  632. elif len(infra_list) == 1:
  633. return "<b>%s</b><br/>%s" % (infra_list[0][1], infra_list[0][0])
  634. elif infra_list[0][1] == infra_list[1][1]:
  635. return "<b>%s</b><br/>%s + %s" % \
  636. (infra_list[0][1], infra_list[0][0], infra_list[1][0])
  637. else:
  638. return "<b>%s</b> (%s)<br/><b>%s</b> (%s)" % \
  639. (infra_list[0][1], infra_list[0][0],
  640. infra_list[1][1], infra_list[1][0])
  641. def boolean_str(b):
  642. if b:
  643. return "Yes"
  644. else:
  645. return "No"
  646. def dump_html_pkg(f, pkg):
  647. f.write(" <tr>\n")
  648. f.write(" <td>%s</td>\n" % pkg.path)
  649. # Patch count
  650. td_class = ["centered"]
  651. if pkg.patch_count == 0:
  652. td_class.append("nopatches")
  653. elif pkg.patch_count < 5:
  654. td_class.append("somepatches")
  655. else:
  656. td_class.append("lotsofpatches")
  657. f.write(" <td class=\"%s\">%s</td>\n" %
  658. (" ".join(td_class), str(pkg.patch_count)))
  659. # Infrastructure
  660. infra = infra_str(pkg.infras)
  661. td_class = ["centered"]
  662. if infra == "Unknown":
  663. td_class.append("wrong")
  664. else:
  665. td_class.append("correct")
  666. f.write(" <td class=\"%s\">%s</td>\n" %
  667. (" ".join(td_class), infra_str(pkg.infras)))
  668. # License
  669. td_class = ["centered"]
  670. if pkg.is_status_ok('license'):
  671. td_class.append("correct")
  672. else:
  673. td_class.append("wrong")
  674. f.write(" <td class=\"%s\">%s</td>\n" %
  675. (" ".join(td_class), boolean_str(pkg.is_status_ok('license'))))
  676. # License files
  677. td_class = ["centered"]
  678. if pkg.is_status_ok('license-files'):
  679. td_class.append("correct")
  680. else:
  681. td_class.append("wrong")
  682. f.write(" <td class=\"%s\">%s</td>\n" %
  683. (" ".join(td_class), boolean_str(pkg.is_status_ok('license-files'))))
  684. # Hash
  685. td_class = ["centered"]
  686. if pkg.is_status_ok('hash'):
  687. td_class.append("correct")
  688. else:
  689. td_class.append("wrong")
  690. f.write(" <td class=\"%s\">%s</td>\n" %
  691. (" ".join(td_class), boolean_str(pkg.is_status_ok('hash'))))
  692. # Current version
  693. if len(pkg.current_version) > 20:
  694. current_version = pkg.current_version[:20] + "..."
  695. else:
  696. current_version = pkg.current_version
  697. f.write(" <td class=\"centered\">%s</td>\n" % current_version)
  698. # Latest version
  699. if pkg.latest_version['status'] == RM_API_STATUS_ERROR:
  700. td_class.append("version-error")
  701. if pkg.latest_version['version'] is None:
  702. td_class.append("version-unknown")
  703. elif pkg.latest_version['version'] != pkg.current_version:
  704. td_class.append("version-needs-update")
  705. else:
  706. td_class.append("version-good")
  707. if pkg.latest_version['status'] == RM_API_STATUS_ERROR:
  708. latest_version_text = "<b>Error</b>"
  709. elif pkg.latest_version['status'] == RM_API_STATUS_NOT_FOUND:
  710. latest_version_text = "<b>Not found</b>"
  711. else:
  712. if pkg.latest_version['version'] is None:
  713. latest_version_text = "<b>Found, but no version</b>"
  714. else:
  715. latest_version_text = "<a href=\"https://release-monitoring.org/project/%s\"><b>%s</b></a>" % \
  716. (pkg.latest_version['id'], str(pkg.latest_version['version']))
  717. latest_version_text += "<br/>"
  718. if pkg.latest_version['status'] == RM_API_STATUS_FOUND_BY_DISTRO:
  719. latest_version_text += "found by <a href=\"https://release-monitoring.org/distro/Buildroot/\">distro</a>"
  720. else:
  721. latest_version_text += "found by guess"
  722. f.write(" <td class=\"%s\">%s</td>\n" %
  723. (" ".join(td_class), latest_version_text))
  724. # Warnings
  725. td_class = ["centered"]
  726. if pkg.warnings == 0:
  727. td_class.append("correct")
  728. else:
  729. td_class.append("wrong")
  730. f.write(" <td class=\"%s\">%d</td>\n" %
  731. (" ".join(td_class), pkg.warnings))
  732. # URL status
  733. td_class = ["centered"]
  734. url_str = pkg.status['url'][1]
  735. if pkg.status['url'][0] in ("error", "warning"):
  736. td_class.append("missing_url")
  737. if pkg.status['url'][0] == "error":
  738. td_class.append("invalid_url")
  739. url_str = "<a href=%s>%s</a>" % (pkg.url, pkg.status['url'][1])
  740. else:
  741. td_class.append("good_url")
  742. url_str = "<a href=%s>Link</a>" % pkg.url
  743. f.write(" <td class=\"%s\">%s</td>\n" %
  744. (" ".join(td_class), url_str))
  745. # CVEs
  746. td_class = ["centered"]
  747. if pkg.status['cve'][0] == "ok":
  748. td_class.append("cve-ok")
  749. elif pkg.status['cve'][0] == "error":
  750. td_class.append("cve-nok")
  751. else:
  752. td_class.append("cve-unknown")
  753. f.write(" <td class=\"%s\">\n" % " ".join(td_class))
  754. if pkg.status['cve'][0] == "error":
  755. for cve in pkg.cves:
  756. f.write(" <a href=\"https://security-tracker.debian.org/tracker/%s\">%s<br/>\n" % (cve, cve))
  757. elif pkg.status['cve'][0] == "na":
  758. f.write(" %s" % pkg.status['cve'][1])
  759. f.write(" </td>\n")
  760. # CPE ID
  761. td_class = ["left"]
  762. if pkg.status['cpe'][0] == "ok":
  763. td_class.append("cpe-ok")
  764. elif pkg.status['cpe'][0] == "error":
  765. td_class.append("cpe-nok")
  766. else:
  767. td_class.append("cpe-unknown")
  768. f.write(" <td class=\"%s\">\n" % " ".join(td_class))
  769. if pkg.status['cpe'][0] == "ok":
  770. f.write(" <code>%s</code>\n" % pkg.cpeid)
  771. elif pkg.status['cpe'][0] == "error":
  772. f.write(" N/A\n")
  773. else:
  774. f.write(" %s\n" % pkg.status['cpe'][1])
  775. f.write(" </td>\n")
  776. f.write(" </tr>\n")
  777. def dump_html_all_pkgs(f, packages):
  778. f.write("""
  779. <table class=\"sortable\">
  780. <tr>
  781. <td>Package</td>
  782. <td class=\"centered\">Patch count</td>
  783. <td class=\"centered\">Infrastructure</td>
  784. <td class=\"centered\">License</td>
  785. <td class=\"centered\">License files</td>
  786. <td class=\"centered\">Hash file</td>
  787. <td class=\"centered\">Current version</td>
  788. <td class=\"centered\">Latest version</td>
  789. <td class=\"centered\">Warnings</td>
  790. <td class=\"centered\">Upstream URL</td>
  791. <td class=\"centered\">CVEs</td>
  792. <td class=\"centered\">CPE ID</td>
  793. </tr>
  794. """)
  795. for pkg in sorted(packages):
  796. dump_html_pkg(f, pkg)
  797. f.write("</table>")
  798. def dump_html_stats(f, stats):
  799. f.write("<a id=\"results\"></a>\n")
  800. f.write("<table>\n")
  801. infras = [infra[6:] for infra in stats.keys() if infra.startswith("infra-")]
  802. for infra in infras:
  803. f.write(" <tr><td>Packages using the <i>%s</i> infrastructure</td><td>%s</td></tr>\n" %
  804. (infra, stats["infra-%s" % infra]))
  805. f.write(" <tr><td>Packages having license information</td><td>%s</td></tr>\n" %
  806. stats["license"])
  807. f.write(" <tr><td>Packages not having license information</td><td>%s</td></tr>\n" %
  808. stats["no-license"])
  809. f.write(" <tr><td>Packages having license files information</td><td>%s</td></tr>\n" %
  810. stats["license-files"])
  811. f.write(" <tr><td>Packages not having license files information</td><td>%s</td></tr>\n" %
  812. stats["no-license-files"])
  813. f.write(" <tr><td>Packages having a hash file</td><td>%s</td></tr>\n" %
  814. stats["hash"])
  815. f.write(" <tr><td>Packages not having a hash file</td><td>%s</td></tr>\n" %
  816. stats["no-hash"])
  817. f.write(" <tr><td>Total number of patches</td><td>%s</td></tr>\n" %
  818. stats["patches"])
  819. f.write("<tr><td>Packages having a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" %
  820. stats["rmo-mapping"])
  821. f.write("<tr><td>Packages lacking a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" %
  822. stats["rmo-no-mapping"])
  823. f.write("<tr><td>Packages that are up-to-date</td><td>%s</td></tr>\n" %
  824. stats["version-uptodate"])
  825. f.write("<tr><td>Packages that are not up-to-date</td><td>%s</td></tr>\n" %
  826. stats["version-not-uptodate"])
  827. f.write("<tr><td>Packages with no known upstream version</td><td>%s</td></tr>\n" %
  828. stats["version-unknown"])
  829. f.write("<tr><td>Packages affected by CVEs</td><td>%s</td></tr>\n" %
  830. stats["pkg-cves"])
  831. f.write("<tr><td>Total number of CVEs affecting all packages</td><td>%s</td></tr>\n" %
  832. stats["total-cves"])
  833. f.write("<tr><td>Packages with CPE ID</td><td>%s</td></tr>\n" %
  834. stats["cpe-id"])
  835. f.write("<tr><td>Packages without CPE ID</td><td>%s</td></tr>\n" %
  836. stats["no-cpe-id"])
  837. f.write("</table>\n")
  838. def dump_html_gen_info(f, date, commit):
  839. # Updated on Mon Feb 19 08:12:08 CET 2018, Git commit aa77030b8f5e41f1c53eb1c1ad664b8c814ba032
  840. f.write("<p><i>Updated on %s, git commit %s</i></p>\n" % (str(date), commit))
  841. def dump_html(packages, stats, date, commit, output):
  842. with open(output, 'w') as f:
  843. f.write(html_header)
  844. dump_html_all_pkgs(f, packages)
  845. dump_html_stats(f, stats)
  846. dump_html_gen_info(f, date, commit)
  847. f.write(html_footer)
  848. def dump_json(packages, defconfigs, stats, date, commit, output):
  849. # Format packages as a dictionnary instead of a list
  850. # Exclude local field that does not contains real date
  851. excluded_fields = ['url_worker', 'name']
  852. pkgs = {
  853. pkg.name: {
  854. k: v
  855. for k, v in pkg.__dict__.items()
  856. if k not in excluded_fields
  857. } for pkg in packages
  858. }
  859. defconfigs = {
  860. d.name: {
  861. k: v
  862. for k, v in d.__dict__.items()
  863. } for d in defconfigs
  864. }
  865. # Aggregate infrastructures into a single dict entry
  866. statistics = {
  867. k: v
  868. for k, v in stats.items()
  869. if not k.startswith('infra-')
  870. }
  871. statistics['infra'] = {k[6:]: v for k, v in stats.items() if k.startswith('infra-')}
  872. # The actual structure to dump, add commit and date to it
  873. final = {'packages': pkgs,
  874. 'stats': statistics,
  875. 'defconfigs': defconfigs,
  876. 'package_status_checks': Package.status_checks,
  877. 'commit': commit,
  878. 'date': str(date)}
  879. with open(output, 'w') as f:
  880. json.dump(final, f, indent=2, separators=(',', ': '))
  881. f.write('\n')
  882. def resolvepath(path):
  883. return os.path.abspath(os.path.expanduser(path))
  884. def parse_args():
  885. parser = argparse.ArgumentParser()
  886. output = parser.add_argument_group('output', 'Output file(s)')
  887. output.add_argument('--html', dest='html', type=resolvepath,
  888. help='HTML output file')
  889. output.add_argument('--json', dest='json', type=resolvepath,
  890. help='JSON output file')
  891. packages = parser.add_mutually_exclusive_group()
  892. packages.add_argument('-c', dest='configpackages', action='store_true',
  893. help='Apply to packages enabled in current configuration')
  894. packages.add_argument('-n', dest='npackages', type=int, action='store',
  895. help='Number of packages')
  896. packages.add_argument('-p', dest='packages', action='store',
  897. help='List of packages (comma separated)')
  898. parser.add_argument('--nvd-path', dest='nvd_path',
  899. help='Path to the local NVD database', type=resolvepath)
  900. parser.add_argument("--cpeid", action='store_true')
  901. args = parser.parse_args()
  902. if not args.html and not args.json:
  903. parser.error('at least one of --html or --json (or both) is required')
  904. return args
  905. def cpeid_name(pkg):
  906. try:
  907. return pkg.cpeid.split(':')[1]
  908. except Exception: # cpeid may be None, or improperly formatted
  909. return ''
  910. def __main__():
  911. global cvecheck
  912. args = parse_args()
  913. if args.nvd_path:
  914. import cve as cvecheck
  915. if args.packages:
  916. package_list = args.packages.split(",")
  917. elif args.configpackages:
  918. package_list = get_config_packages()
  919. else:
  920. package_list = None
  921. date = datetime.datetime.utcnow()
  922. commit = subprocess.check_output(['git', '-C', brpath,
  923. 'rev-parse',
  924. 'HEAD']).splitlines()[0].decode()
  925. print("Build package list ...")
  926. packages = get_pkglist(args.npackages, package_list)
  927. print("Getting developers ...")
  928. developers = parse_developers()
  929. print("Build defconfig list ...")
  930. defconfigs = get_defconfig_list()
  931. for d in defconfigs:
  932. d.set_developers(developers)
  933. print("Getting package make info ...")
  934. package_init_make_info()
  935. print("Getting package details ...")
  936. for pkg in packages:
  937. pkg.set_infra()
  938. pkg.set_license()
  939. pkg.set_hash_info()
  940. pkg.set_patch_count()
  941. pkg.set_check_package_warnings()
  942. pkg.set_current_version()
  943. pkg.set_cpeid()
  944. pkg.set_url()
  945. pkg.set_developers(developers)
  946. print("Checking URL status")
  947. loop = asyncio.get_event_loop()
  948. loop.run_until_complete(check_package_urls(packages))
  949. print("Getting latest versions ...")
  950. loop = asyncio.get_event_loop()
  951. loop.run_until_complete(check_package_latest_version(packages))
  952. if args.nvd_path:
  953. print("Checking packages CVEs")
  954. check_package_cves(args.nvd_path, packages)
  955. print("Calculate stats")
  956. stats = calculate_stats(packages)
  957. if args.html:
  958. print("Write HTML")
  959. dump_html(packages, stats, date, commit, args.html)
  960. if args.json:
  961. print("Write JSON")
  962. dump_json(packages, defconfigs, stats, date, commit, args.json)
  963. __main__()