pkg-stats 37 KB

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