pkg-stats 32 KB

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