2
1

pkg-stats 52 KB

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