| #!/usr/bin/env python3 |
| |
| # Copyright (C) 2009 by Thomas Petazzoni <thomas.petazzoni@free-electrons.com> |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| # General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| |
| import aiohttp |
| import argparse |
| import asyncio |
| import datetime |
| import fnmatch |
| import os |
| from collections import defaultdict |
| import re |
| import subprocess |
| import json |
| import sys |
| |
| brpath = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) |
| |
| sys.path.append(os.path.join(brpath, "utils")) |
| from getdeveloperlib import parse_developers # noqa: E402 |
| from cpedb import CPEDB # noqa: E402 |
| |
| INFRA_RE = re.compile(r"\$\(eval \$\(([a-z-]*)-package\)\)") |
| URL_RE = re.compile(r"\s*https?://\S*\s*$") |
| |
| RM_API_STATUS_ERROR = 1 |
| RM_API_STATUS_FOUND_BY_DISTRO = 2 |
| RM_API_STATUS_FOUND_BY_PATTERN = 3 |
| RM_API_STATUS_NOT_FOUND = 4 |
| |
| |
| class Defconfig: |
| def __init__(self, name, path): |
| self.name = name |
| self.path = path |
| self.developers = None |
| |
| def set_developers(self, developers): |
| """ |
| Fills in the .developers field |
| """ |
| self.developers = [ |
| developer.name |
| for developer in developers |
| if developer.hasfile(self.path) |
| ] |
| |
| |
| def get_defconfig_list(): |
| """ |
| Builds the list of Buildroot defconfigs, returning a list of Defconfig |
| objects. |
| """ |
| return [ |
| Defconfig(name[:-len('_defconfig')], os.path.join('configs', name)) |
| for name in os.listdir(os.path.join(brpath, 'configs')) |
| if name.endswith('_defconfig') |
| ] |
| |
| |
| class Package: |
| all_licenses = dict() |
| all_license_files = list() |
| all_versions = dict() |
| all_ignored_cves = dict() |
| all_cpeids = dict() |
| # This is the list of all possible checks. Add new checks to this list so |
| # a tool that post-processeds the json output knows the checks before |
| # iterating over the packages. |
| status_checks = ['cve', 'developers', 'hash', 'license', |
| 'license-files', 'patches', 'pkg-check', 'url', 'version'] |
| |
| def __init__(self, name, path): |
| self.name = name |
| self.path = path |
| self.pkg_path = os.path.dirname(path) |
| self.infras = None |
| self.license = None |
| self.has_license = False |
| self.has_license_files = False |
| self.has_hash = False |
| self.patch_files = [] |
| self.warnings = 0 |
| self.current_version = None |
| self.url = None |
| self.url_worker = None |
| self.cpeid = None |
| self.cves = list() |
| self.ignored_cves = list() |
| self.latest_version = {'status': RM_API_STATUS_ERROR, 'version': None, 'id': None} |
| self.status = {} |
| |
| def pkgvar(self): |
| return self.name.upper().replace("-", "_") |
| |
| def set_url(self): |
| """ |
| Fills in the .url field |
| """ |
| self.status['url'] = ("warning", "no Config.in") |
| pkgdir = os.path.dirname(os.path.join(brpath, self.path)) |
| for filename in os.listdir(pkgdir): |
| if fnmatch.fnmatch(filename, 'Config.*'): |
| fp = open(os.path.join(pkgdir, filename), "r") |
| for config_line in fp: |
| if URL_RE.match(config_line): |
| self.url = config_line.strip() |
| self.status['url'] = ("ok", "found") |
| fp.close() |
| return |
| self.status['url'] = ("error", "missing") |
| fp.close() |
| |
| @property |
| def patch_count(self): |
| return len(self.patch_files) |
| |
| @property |
| def has_valid_infra(self): |
| try: |
| if self.infras[0][1] == 'virtual': |
| return False |
| except IndexError: |
| return False |
| return True |
| |
| def set_infra(self): |
| """ |
| Fills in the .infras field |
| """ |
| self.infras = list() |
| with open(os.path.join(brpath, self.path), 'r') as f: |
| lines = f.readlines() |
| for line in lines: |
| match = INFRA_RE.match(line) |
| if not match: |
| continue |
| infra = match.group(1) |
| if infra.startswith("host-"): |
| self.infras.append(("host", infra[5:])) |
| else: |
| self.infras.append(("target", infra)) |
| |
| def set_license(self): |
| """ |
| Fills in the .status['license'] and .status['license-files'] fields |
| """ |
| if not self.has_valid_infra: |
| self.status['license'] = ("na", "no valid package infra") |
| self.status['license-files'] = ("na", "no valid package infra") |
| return |
| |
| var = self.pkgvar() |
| self.status['license'] = ("error", "missing") |
| self.status['license-files'] = ("error", "missing") |
| if var in self.all_licenses: |
| self.license = self.all_licenses[var] |
| self.status['license'] = ("ok", "found") |
| if var in self.all_license_files: |
| self.status['license-files'] = ("ok", "found") |
| |
| def set_hash_info(self): |
| """ |
| Fills in the .status['hash'] field |
| """ |
| if not self.has_valid_infra: |
| self.status['hash'] = ("na", "no valid package infra") |
| self.status['hash-license'] = ("na", "no valid package infra") |
| return |
| |
| hashpath = self.path.replace(".mk", ".hash") |
| if os.path.exists(os.path.join(brpath, hashpath)): |
| self.status['hash'] = ("ok", "found") |
| else: |
| self.status['hash'] = ("error", "missing") |
| |
| def set_patch_count(self): |
| """ |
| Fills in the .patch_count, .patch_files and .status['patches'] fields |
| """ |
| if not self.has_valid_infra: |
| self.status['patches'] = ("na", "no valid package infra") |
| return |
| |
| pkgdir = os.path.dirname(os.path.join(brpath, self.path)) |
| for subdir, _, _ in os.walk(pkgdir): |
| self.patch_files = fnmatch.filter(os.listdir(subdir), '*.patch') |
| |
| if self.patch_count == 0: |
| self.status['patches'] = ("ok", "no patches") |
| elif self.patch_count < 5: |
| self.status['patches'] = ("warning", "some patches") |
| else: |
| self.status['patches'] = ("error", "lots of patches") |
| |
| def set_current_version(self): |
| """ |
| Fills in the .current_version field |
| """ |
| var = self.pkgvar() |
| if var in self.all_versions: |
| self.current_version = self.all_versions[var] |
| |
| def set_cpeid(self): |
| """ |
| Fills in the .cpeid field |
| """ |
| var = self.pkgvar() |
| if not self.has_valid_infra: |
| self.status['cpe'] = ("na", "no valid package infra") |
| return |
| |
| if var in self.all_cpeids: |
| self.cpeid = self.all_cpeids[var] |
| # Set a preliminary status, it might be overridden by check_package_cpes() |
| self.status['cpe'] = ("warning", "not checked against CPE dictionnary") |
| else: |
| self.status['cpe'] = ("error", "no verified CPE identifier") |
| |
| def set_check_package_warnings(self): |
| """ |
| Fills in the .warnings and .status['pkg-check'] fields |
| """ |
| cmd = [os.path.join(brpath, "utils/check-package")] |
| pkgdir = os.path.dirname(os.path.join(brpath, self.path)) |
| self.status['pkg-check'] = ("error", "Missing") |
| for root, dirs, files in os.walk(pkgdir): |
| for f in files: |
| if f.endswith(".mk") or f.endswith(".hash") or f == "Config.in" or f == "Config.in.host": |
| cmd.append(os.path.join(root, f)) |
| o = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[1] |
| lines = o.splitlines() |
| for line in lines: |
| m = re.match("^([0-9]*) warnings generated", line.decode()) |
| if m: |
| self.warnings = int(m.group(1)) |
| if self.warnings == 0: |
| self.status['pkg-check'] = ("ok", "no warnings") |
| else: |
| self.status['pkg-check'] = ("error", "{} warnings".format(self.warnings)) |
| return |
| |
| def set_ignored_cves(self): |
| """ |
| Give the list of CVEs ignored by the package |
| """ |
| self.ignored_cves = list(self.all_ignored_cves.get(self.pkgvar(), [])) |
| |
| def set_developers(self, developers): |
| """ |
| Fills in the .developers and .status['developers'] field |
| """ |
| self.developers = [ |
| dev.name |
| for dev in developers |
| if dev.hasfile(self.path) |
| ] |
| |
| if self.developers: |
| self.status['developers'] = ("ok", "{} developers".format(len(self.developers))) |
| else: |
| self.status['developers'] = ("warning", "no developers") |
| |
| def is_status_ok(self, name): |
| return name in self.status and self.status[name][0] == 'ok' |
| |
| def is_status_error(self, name): |
| return name in self.status and self.status[name][0] == 'error' |
| |
| def is_status_na(self, name): |
| return name in self.status and self.status[name][0] == 'na' |
| |
| def __eq__(self, other): |
| return self.path == other.path |
| |
| def __lt__(self, other): |
| return self.path < other.path |
| |
| def __str__(self): |
| return "%s (path='%s', license='%s', license_files='%s', hash='%s', patches=%d)" % \ |
| (self.name, self.path, self.is_status_ok('license'), |
| self.is_status_ok('license-files'), self.status['hash'], self.patch_count) |
| |
| |
| def get_pkglist(npackages, package_list): |
| """ |
| Builds the list of Buildroot packages, returning a list of Package |
| objects. Only the .name and .path fields of the Package object are |
| initialized. |
| |
| npackages: limit to N packages |
| package_list: limit to those packages in this list |
| """ |
| WALK_USEFUL_SUBDIRS = ["boot", "linux", "package", "toolchain"] |
| WALK_EXCLUDES = ["boot/common.mk", |
| "linux/linux-ext-.*.mk", |
| "package/freescale-imx/freescale-imx.mk", |
| "package/gcc/gcc.mk", |
| "package/gstreamer/gstreamer.mk", |
| "package/gstreamer1/gstreamer1.mk", |
| "package/gtk2-themes/gtk2-themes.mk", |
| "package/matchbox/matchbox.mk", |
| "package/opengl/opengl.mk", |
| "package/qt5/qt5.mk", |
| "package/x11r7/x11r7.mk", |
| "package/doc-asciidoc.mk", |
| "package/pkg-.*.mk", |
| "toolchain/toolchain-external/pkg-toolchain-external.mk", |
| "toolchain/toolchain-external/toolchain-external.mk", |
| "toolchain/toolchain.mk", |
| "toolchain/helpers.mk", |
| "toolchain/toolchain-wrapper.mk"] |
| packages = list() |
| count = 0 |
| for root, dirs, files in os.walk(brpath): |
| root = os.path.relpath(root, brpath) |
| rootdir = root.split("/") |
| if len(rootdir) < 1: |
| continue |
| if rootdir[0] not in WALK_USEFUL_SUBDIRS: |
| continue |
| for f in files: |
| if not f.endswith(".mk"): |
| continue |
| # Strip ending ".mk" |
| pkgname = f[:-3] |
| if package_list and pkgname not in package_list: |
| continue |
| pkgpath = os.path.join(root, f) |
| skip = False |
| for exclude in WALK_EXCLUDES: |
| if re.match(exclude, pkgpath): |
| skip = True |
| continue |
| if skip: |
| continue |
| p = Package(pkgname, pkgpath) |
| packages.append(p) |
| count += 1 |
| if npackages and count == npackages: |
| return packages |
| return packages |
| |
| |
| def get_config_packages(): |
| cmd = ["make", "--no-print-directory", "show-info"] |
| js = json.loads(subprocess.check_output(cmd)) |
| return set([v["name"] for v in js.values()]) |
| |
| |
| def package_init_make_info(): |
| # Fetch all variables at once |
| variables = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y", "-s", "printvars", |
| "VARS=%_LICENSE %_LICENSE_FILES %_VERSION %_IGNORE_CVES %_CPE_ID"]) |
| variable_list = variables.decode().splitlines() |
| |
| # We process first the host package VERSION, and then the target |
| # package VERSION. This means that if a package exists in both |
| # target and host variants, with different values (eg. version |
| # numbers (unlikely)), we'll report the target one. |
| variable_list = [x[5:] for x in variable_list if x.startswith("HOST_")] + \ |
| [x for x in variable_list if not x.startswith("HOST_")] |
| |
| for l in variable_list: |
| # Get variable name and value |
| pkgvar, value = l.split("=") |
| |
| # Strip the suffix according to the variable |
| if pkgvar.endswith("_LICENSE"): |
| # If value is "unknown", no license details available |
| if value == "unknown": |
| continue |
| pkgvar = pkgvar[:-8] |
| Package.all_licenses[pkgvar] = value |
| |
| elif pkgvar.endswith("_LICENSE_FILES"): |
| if pkgvar.endswith("_MANIFEST_LICENSE_FILES"): |
| continue |
| pkgvar = pkgvar[:-14] |
| Package.all_license_files.append(pkgvar) |
| |
| elif pkgvar.endswith("_VERSION"): |
| if pkgvar.endswith("_DL_VERSION"): |
| continue |
| pkgvar = pkgvar[:-8] |
| Package.all_versions[pkgvar] = value |
| |
| elif pkgvar.endswith("_IGNORE_CVES"): |
| pkgvar = pkgvar[:-12] |
| Package.all_ignored_cves[pkgvar] = value.split() |
| |
| elif pkgvar.endswith("_CPE_ID"): |
| pkgvar = pkgvar[:-7] |
| Package.all_cpeids[pkgvar] = value |
| |
| |
| check_url_count = 0 |
| |
| |
| async def check_url_status(session, pkg, npkgs, retry=True): |
| global check_url_count |
| |
| try: |
| async with session.get(pkg.url) as resp: |
| if resp.status >= 400: |
| pkg.status['url'] = ("error", "invalid {}".format(resp.status)) |
| check_url_count += 1 |
| print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name)) |
| return |
| except (aiohttp.ClientError, asyncio.TimeoutError): |
| if retry: |
| return await check_url_status(session, pkg, npkgs, retry=False) |
| else: |
| pkg.status['url'] = ("error", "invalid (err)") |
| check_url_count += 1 |
| print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name)) |
| return |
| |
| pkg.status['url'] = ("ok", "valid") |
| check_url_count += 1 |
| print("[%04d/%04d] %s" % (check_url_count, npkgs, pkg.name)) |
| |
| |
| async def check_package_urls(packages): |
| tasks = [] |
| connector = aiohttp.TCPConnector(limit_per_host=5) |
| async with aiohttp.ClientSession(connector=connector, trust_env=True) as sess: |
| packages = [p for p in packages if p.status['url'][0] == 'ok'] |
| for pkg in packages: |
| tasks.append(asyncio.ensure_future(check_url_status(sess, pkg, len(packages)))) |
| await asyncio.wait(tasks) |
| |
| |
| def check_package_latest_version_set_status(pkg, status, version, identifier): |
| pkg.latest_version = { |
| "status": status, |
| "version": version, |
| "id": identifier, |
| } |
| |
| if pkg.latest_version['status'] == RM_API_STATUS_ERROR: |
| pkg.status['version'] = ('warning', "Release Monitoring API error") |
| elif pkg.latest_version['status'] == RM_API_STATUS_NOT_FOUND: |
| pkg.status['version'] = ('warning', "Package not found on Release Monitoring") |
| |
| if pkg.latest_version['version'] is None: |
| pkg.status['version'] = ('warning', "No upstream version available on Release Monitoring") |
| elif pkg.latest_version['version'] != pkg.current_version: |
| pkg.status['version'] = ('error', "The newer version {} is available upstream".format(pkg.latest_version['version'])) |
| else: |
| pkg.status['version'] = ('ok', 'up-to-date') |
| |
| |
| async def check_package_get_latest_version_by_distro(session, pkg, retry=True): |
| url = "https://release-monitoring.org//api/project/Buildroot/%s" % pkg.name |
| try: |
| async with session.get(url) as resp: |
| if resp.status != 200: |
| return False |
| |
| data = await resp.json() |
| version = data['version'] if 'version' in data else None |
| check_package_latest_version_set_status(pkg, |
| RM_API_STATUS_FOUND_BY_DISTRO, |
| version, |
| data['id']) |
| return True |
| |
| except (aiohttp.ClientError, asyncio.TimeoutError): |
| if retry: |
| return await check_package_get_latest_version_by_distro(session, pkg, retry=False) |
| else: |
| return False |
| |
| |
| async def check_package_get_latest_version_by_guess(session, pkg, retry=True): |
| url = "https://release-monitoring.org/api/projects/?pattern=%s" % pkg.name |
| try: |
| async with session.get(url) as resp: |
| if resp.status != 200: |
| return False |
| |
| data = await resp.json() |
| # filter projects that have the right name and a version defined |
| projects = [p for p in data['projects'] if p['name'] == pkg.name and 'version' in p] |
| projects.sort(key=lambda x: x['id']) |
| |
| if len(projects) > 0: |
| check_package_latest_version_set_status(pkg, |
| RM_API_STATUS_FOUND_BY_DISTRO, |
| projects[0]['version'], |
| projects[0]['id']) |
| return True |
| |
| except (aiohttp.ClientError, asyncio.TimeoutError): |
| if retry: |
| return await check_package_get_latest_version_by_guess(session, pkg, retry=False) |
| else: |
| return False |
| |
| |
| check_latest_count = 0 |
| |
| |
| async def check_package_latest_version_get(session, pkg, npkgs): |
| global check_latest_count |
| |
| if await check_package_get_latest_version_by_distro(session, pkg): |
| check_latest_count += 1 |
| print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name)) |
| return |
| |
| if await check_package_get_latest_version_by_guess(session, pkg): |
| check_latest_count += 1 |
| print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name)) |
| return |
| |
| check_package_latest_version_set_status(pkg, |
| RM_API_STATUS_NOT_FOUND, |
| None, None) |
| check_latest_count += 1 |
| print("[%04d/%04d] %s" % (check_latest_count, npkgs, pkg.name)) |
| |
| |
| async def check_package_latest_version(packages): |
| """ |
| Fills in the .latest_version field of all Package objects |
| |
| This field is a dict and has the following keys: |
| |
| - status: one of RM_API_STATUS_ERROR, |
| RM_API_STATUS_FOUND_BY_DISTRO, RM_API_STATUS_FOUND_BY_PATTERN, |
| RM_API_STATUS_NOT_FOUND |
| - version: string containing the latest version known by |
| release-monitoring.org for this package |
| - id: string containing the id of the project corresponding to this |
| package, as known by release-monitoring.org |
| """ |
| |
| for pkg in [p for p in packages if not p.has_valid_infra]: |
| pkg.status['version'] = ("na", "no valid package infra") |
| |
| tasks = [] |
| connector = aiohttp.TCPConnector(limit_per_host=5) |
| async with aiohttp.ClientSession(connector=connector, trust_env=True) as sess: |
| packages = [p for p in packages if p.has_valid_infra] |
| for pkg in packages: |
| tasks.append(asyncio.ensure_future(check_package_latest_version_get(sess, pkg, len(packages)))) |
| await asyncio.wait(tasks) |
| |
| |
| def check_package_cve_affects(cve, cpe_product_pkgs): |
| for product in cve.affected_products: |
| if product not in cpe_product_pkgs: |
| continue |
| for pkg in cpe_product_pkgs[product]: |
| if cve.affects(pkg.name, pkg.current_version, pkg.ignored_cves, pkg.cpeid) == cve.CVE_AFFECTS: |
| pkg.cves.append(cve.identifier) |
| |
| |
| def check_package_cves(nvd_path, packages): |
| if not os.path.isdir(nvd_path): |
| os.makedirs(nvd_path) |
| |
| cpe_product_pkgs = defaultdict(list) |
| for pkg in packages: |
| if not pkg.has_valid_infra: |
| pkg.status['cve'] = ("na", "no valid package infra") |
| continue |
| if not pkg.current_version: |
| pkg.status['cve'] = ("na", "no version information available") |
| continue |
| if pkg.cpeid: |
| cpe_product = cvecheck.cpe_product(pkg.cpeid) |
| cpe_product_pkgs[cpe_product].append(pkg) |
| else: |
| cpe_product_pkgs[pkg.name].append(pkg) |
| |
| for cve in cvecheck.CVE.read_nvd_dir(nvd_path): |
| check_package_cve_affects(cve, cpe_product_pkgs) |
| |
| for pkg in packages: |
| if 'cve' not in pkg.status: |
| if pkg.cves: |
| pkg.status['cve'] = ("error", "affected by CVEs") |
| else: |
| pkg.status['cve'] = ("ok", "not affected by CVEs") |
| |
| |
| def check_package_cpes(nvd_path, packages): |
| cpedb = CPEDB(nvd_path) |
| cpedb.get_xml_dict() |
| for p in packages: |
| if not p.cpeid: |
| continue |
| if cpedb.find(p.cpeid): |
| p.status['cpe'] = ("ok", "verified CPE identifier") |
| else: |
| p.status['cpe'] = ("error", "CPE identifier unknown in CPE database") |
| |
| |
| def calculate_stats(packages): |
| stats = defaultdict(int) |
| stats['packages'] = len(packages) |
| for pkg in packages: |
| # If packages have multiple infra, take the first one. For the |
| # vast majority of packages, the target and host infra are the |
| # same. There are very few packages that use a different infra |
| # for the host and target variants. |
| if len(pkg.infras) > 0: |
| infra = pkg.infras[0][1] |
| stats["infra-%s" % infra] += 1 |
| else: |
| stats["infra-unknown"] += 1 |
| if pkg.is_status_ok('license'): |
| stats["license"] += 1 |
| else: |
| stats["no-license"] += 1 |
| if pkg.is_status_ok('license-files'): |
| stats["license-files"] += 1 |
| else: |
| stats["no-license-files"] += 1 |
| if pkg.is_status_ok('hash'): |
| stats["hash"] += 1 |
| else: |
| stats["no-hash"] += 1 |
| if pkg.latest_version['status'] == RM_API_STATUS_FOUND_BY_DISTRO: |
| stats["rmo-mapping"] += 1 |
| else: |
| stats["rmo-no-mapping"] += 1 |
| if not pkg.latest_version['version']: |
| stats["version-unknown"] += 1 |
| elif pkg.latest_version['version'] == pkg.current_version: |
| stats["version-uptodate"] += 1 |
| else: |
| stats["version-not-uptodate"] += 1 |
| stats["patches"] += pkg.patch_count |
| stats["total-cves"] += len(pkg.cves) |
| if len(pkg.cves) != 0: |
| stats["pkg-cves"] += 1 |
| if pkg.cpeid: |
| stats["cpe-id"] += 1 |
| else: |
| stats["no-cpe-id"] += 1 |
| return stats |
| |
| |
| html_header = """ |
| <head> |
| <script src=\"https://www.kryogenix.org/code/browser/sorttable/sorttable.js\"></script> |
| <style type=\"text/css\"> |
| table { |
| width: 100%; |
| } |
| td { |
| border: 1px solid black; |
| } |
| td.centered { |
| text-align: center; |
| } |
| td.wrong { |
| background: #ff9a69; |
| } |
| td.correct { |
| background: #d2ffc4; |
| } |
| td.nopatches { |
| background: #d2ffc4; |
| } |
| td.somepatches { |
| background: #ffd870; |
| } |
| td.lotsofpatches { |
| background: #ff9a69; |
| } |
| |
| td.good_url { |
| background: #d2ffc4; |
| } |
| td.missing_url { |
| background: #ffd870; |
| } |
| td.invalid_url { |
| background: #ff9a69; |
| } |
| |
| td.version-good { |
| background: #d2ffc4; |
| } |
| td.version-needs-update { |
| background: #ff9a69; |
| } |
| td.version-unknown { |
| background: #ffd870; |
| } |
| td.version-error { |
| background: #ccc; |
| } |
| |
| td.cpe-ok { |
| background: #d2ffc4; |
| } |
| |
| td.cpe-nok { |
| background: #ff9a69; |
| } |
| |
| td.cpe-unknown { |
| background: #ffd870; |
| } |
| |
| td.cve-ok { |
| background: #d2ffc4; |
| } |
| |
| td.cve-nok { |
| background: #ff9a69; |
| } |
| |
| td.cve-unknown { |
| background: #ffd870; |
| } |
| |
| </style> |
| <title>Statistics of Buildroot packages</title> |
| </head> |
| |
| <a href=\"#results\">Results</a><br/> |
| |
| <p id=\"sortable_hint\"></p> |
| """ |
| |
| |
| html_footer = """ |
| </body> |
| <script> |
| if (typeof sorttable === \"object\") { |
| document.getElementById(\"sortable_hint\").innerHTML = |
| \"hint: the table can be sorted by clicking the column headers\" |
| } |
| </script> |
| </html> |
| """ |
| |
| |
| def infra_str(infra_list): |
| if not infra_list: |
| return "Unknown" |
| elif len(infra_list) == 1: |
| return "<b>%s</b><br/>%s" % (infra_list[0][1], infra_list[0][0]) |
| elif infra_list[0][1] == infra_list[1][1]: |
| return "<b>%s</b><br/>%s + %s" % \ |
| (infra_list[0][1], infra_list[0][0], infra_list[1][0]) |
| else: |
| return "<b>%s</b> (%s)<br/><b>%s</b> (%s)" % \ |
| (infra_list[0][1], infra_list[0][0], |
| infra_list[1][1], infra_list[1][0]) |
| |
| |
| def boolean_str(b): |
| if b: |
| return "Yes" |
| else: |
| return "No" |
| |
| |
| def dump_html_pkg(f, pkg): |
| f.write(" <tr>\n") |
| f.write(" <td>%s</td>\n" % pkg.path) |
| |
| # Patch count |
| td_class = ["centered"] |
| if pkg.patch_count == 0: |
| td_class.append("nopatches") |
| elif pkg.patch_count < 5: |
| td_class.append("somepatches") |
| else: |
| td_class.append("lotsofpatches") |
| f.write(" <td class=\"%s\">%s</td>\n" % |
| (" ".join(td_class), str(pkg.patch_count))) |
| |
| # Infrastructure |
| infra = infra_str(pkg.infras) |
| td_class = ["centered"] |
| if infra == "Unknown": |
| td_class.append("wrong") |
| else: |
| td_class.append("correct") |
| f.write(" <td class=\"%s\">%s</td>\n" % |
| (" ".join(td_class), infra_str(pkg.infras))) |
| |
| # License |
| td_class = ["centered"] |
| if pkg.is_status_ok('license'): |
| td_class.append("correct") |
| else: |
| td_class.append("wrong") |
| f.write(" <td class=\"%s\">%s</td>\n" % |
| (" ".join(td_class), boolean_str(pkg.is_status_ok('license')))) |
| |
| # License files |
| td_class = ["centered"] |
| if pkg.is_status_ok('license-files'): |
| td_class.append("correct") |
| else: |
| td_class.append("wrong") |
| f.write(" <td class=\"%s\">%s</td>\n" % |
| (" ".join(td_class), boolean_str(pkg.is_status_ok('license-files')))) |
| |
| # Hash |
| td_class = ["centered"] |
| if pkg.is_status_ok('hash'): |
| td_class.append("correct") |
| else: |
| td_class.append("wrong") |
| f.write(" <td class=\"%s\">%s</td>\n" % |
| (" ".join(td_class), boolean_str(pkg.is_status_ok('hash')))) |
| |
| # Current version |
| if len(pkg.current_version) > 20: |
| current_version = pkg.current_version[:20] + "..." |
| else: |
| current_version = pkg.current_version |
| f.write(" <td class=\"centered\">%s</td>\n" % current_version) |
| |
| # Latest version |
| if pkg.latest_version['status'] == RM_API_STATUS_ERROR: |
| td_class.append("version-error") |
| if pkg.latest_version['version'] is None: |
| td_class.append("version-unknown") |
| elif pkg.latest_version['version'] != pkg.current_version: |
| td_class.append("version-needs-update") |
| else: |
| td_class.append("version-good") |
| |
| if pkg.latest_version['status'] == RM_API_STATUS_ERROR: |
| latest_version_text = "<b>Error</b>" |
| elif pkg.latest_version['status'] == RM_API_STATUS_NOT_FOUND: |
| latest_version_text = "<b>Not found</b>" |
| else: |
| if pkg.latest_version['version'] is None: |
| latest_version_text = "<b>Found, but no version</b>" |
| else: |
| latest_version_text = "<a href=\"https://release-monitoring.org/project/%s\"><b>%s</b></a>" % \ |
| (pkg.latest_version['id'], str(pkg.latest_version['version'])) |
| |
| latest_version_text += "<br/>" |
| |
| if pkg.latest_version['status'] == RM_API_STATUS_FOUND_BY_DISTRO: |
| latest_version_text += "found by <a href=\"https://release-monitoring.org/distro/Buildroot/\">distro</a>" |
| else: |
| latest_version_text += "found by guess" |
| |
| f.write(" <td class=\"%s\">%s</td>\n" % |
| (" ".join(td_class), latest_version_text)) |
| |
| # Warnings |
| td_class = ["centered"] |
| if pkg.warnings == 0: |
| td_class.append("correct") |
| else: |
| td_class.append("wrong") |
| f.write(" <td class=\"%s\">%d</td>\n" % |
| (" ".join(td_class), pkg.warnings)) |
| |
| # URL status |
| td_class = ["centered"] |
| url_str = pkg.status['url'][1] |
| if pkg.status['url'][0] in ("error", "warning"): |
| td_class.append("missing_url") |
| if pkg.status['url'][0] == "error": |
| td_class.append("invalid_url") |
| url_str = "<a href=%s>%s</a>" % (pkg.url, pkg.status['url'][1]) |
| else: |
| td_class.append("good_url") |
| url_str = "<a href=%s>Link</a>" % pkg.url |
| f.write(" <td class=\"%s\">%s</td>\n" % |
| (" ".join(td_class), url_str)) |
| |
| # CVEs |
| td_class = ["centered"] |
| if pkg.is_status_ok("cve"): |
| td_class.append("cve-ok") |
| elif pkg.is_status_error("cve"): |
| td_class.append("cve-nok") |
| else: |
| td_class.append("cve-unknown") |
| f.write(" <td class=\"%s\">\n" % " ".join(td_class)) |
| if pkg.is_status_error("cve"): |
| for cve in pkg.cves: |
| f.write(" <a href=\"https://security-tracker.debian.org/tracker/%s\">%s<br/>\n" % (cve, cve)) |
| elif pkg.is_status_na("cve"): |
| f.write(" %s" % pkg.status['cve'][1]) |
| else: |
| f.write(" N/A\n") |
| f.write(" </td>\n") |
| |
| # CPE ID |
| td_class = ["left"] |
| if pkg.is_status_ok("cpe"): |
| td_class.append("cpe-ok") |
| elif pkg.is_status_error("cpe"): |
| td_class.append("cpe-nok") |
| else: |
| td_class.append("cpe-unknown") |
| f.write(" <td class=\"%s\">\n" % " ".join(td_class)) |
| if pkg.cpeid: |
| f.write(" <code>%s</code>\n" % pkg.cpeid) |
| if not pkg.is_status_ok("cpe"): |
| f.write(" %s%s\n" % ("<br/>" if pkg.cpeid else "", pkg.status['cpe'][1])) |
| f.write(" </td>\n") |
| |
| f.write(" </tr>\n") |
| |
| |
| def dump_html_all_pkgs(f, packages): |
| f.write(""" |
| <table class=\"sortable\"> |
| <tr> |
| <td>Package</td> |
| <td class=\"centered\">Patch count</td> |
| <td class=\"centered\">Infrastructure</td> |
| <td class=\"centered\">License</td> |
| <td class=\"centered\">License files</td> |
| <td class=\"centered\">Hash file</td> |
| <td class=\"centered\">Current version</td> |
| <td class=\"centered\">Latest version</td> |
| <td class=\"centered\">Warnings</td> |
| <td class=\"centered\">Upstream URL</td> |
| <td class=\"centered\">CVEs</td> |
| <td class=\"centered\">CPE ID</td> |
| </tr> |
| """) |
| for pkg in sorted(packages): |
| dump_html_pkg(f, pkg) |
| f.write("</table>") |
| |
| |
| def dump_html_stats(f, stats): |
| f.write("<a id=\"results\"></a>\n") |
| f.write("<table>\n") |
| infras = [infra[6:] for infra in stats.keys() if infra.startswith("infra-")] |
| for infra in infras: |
| f.write(" <tr><td>Packages using the <i>%s</i> infrastructure</td><td>%s</td></tr>\n" % |
| (infra, stats["infra-%s" % infra])) |
| f.write(" <tr><td>Packages having license information</td><td>%s</td></tr>\n" % |
| stats["license"]) |
| f.write(" <tr><td>Packages not having license information</td><td>%s</td></tr>\n" % |
| stats["no-license"]) |
| f.write(" <tr><td>Packages having license files information</td><td>%s</td></tr>\n" % |
| stats["license-files"]) |
| f.write(" <tr><td>Packages not having license files information</td><td>%s</td></tr>\n" % |
| stats["no-license-files"]) |
| f.write(" <tr><td>Packages having a hash file</td><td>%s</td></tr>\n" % |
| stats["hash"]) |
| f.write(" <tr><td>Packages not having a hash file</td><td>%s</td></tr>\n" % |
| stats["no-hash"]) |
| f.write(" <tr><td>Total number of patches</td><td>%s</td></tr>\n" % |
| stats["patches"]) |
| f.write("<tr><td>Packages having a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" % |
| stats["rmo-mapping"]) |
| f.write("<tr><td>Packages lacking a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" % |
| stats["rmo-no-mapping"]) |
| f.write("<tr><td>Packages that are up-to-date</td><td>%s</td></tr>\n" % |
| stats["version-uptodate"]) |
| f.write("<tr><td>Packages that are not up-to-date</td><td>%s</td></tr>\n" % |
| stats["version-not-uptodate"]) |
| f.write("<tr><td>Packages with no known upstream version</td><td>%s</td></tr>\n" % |
| stats["version-unknown"]) |
| f.write("<tr><td>Packages affected by CVEs</td><td>%s</td></tr>\n" % |
| stats["pkg-cves"]) |
| f.write("<tr><td>Total number of CVEs affecting all packages</td><td>%s</td></tr>\n" % |
| stats["total-cves"]) |
| f.write("<tr><td>Packages with CPE ID</td><td>%s</td></tr>\n" % |
| stats["cpe-id"]) |
| f.write("<tr><td>Packages without CPE ID</td><td>%s</td></tr>\n" % |
| stats["no-cpe-id"]) |
| f.write("</table>\n") |
| |
| |
| def dump_html_gen_info(f, date, commit): |
| # Updated on Mon Feb 19 08:12:08 CET 2018, Git commit aa77030b8f5e41f1c53eb1c1ad664b8c814ba032 |
| f.write("<p><i>Updated on %s, git commit %s</i></p>\n" % (str(date), commit)) |
| |
| |
| def dump_html(packages, stats, date, commit, output): |
| with open(output, 'w') as f: |
| f.write(html_header) |
| dump_html_all_pkgs(f, packages) |
| dump_html_stats(f, stats) |
| dump_html_gen_info(f, date, commit) |
| f.write(html_footer) |
| |
| |
| def dump_json(packages, defconfigs, stats, date, commit, output): |
| # Format packages as a dictionnary instead of a list |
| # Exclude local field that does not contains real date |
| excluded_fields = ['url_worker', 'name'] |
| pkgs = { |
| pkg.name: { |
| k: v |
| for k, v in pkg.__dict__.items() |
| if k not in excluded_fields |
| } for pkg in packages |
| } |
| defconfigs = { |
| d.name: { |
| k: v |
| for k, v in d.__dict__.items() |
| } for d in defconfigs |
| } |
| # Aggregate infrastructures into a single dict entry |
| statistics = { |
| k: v |
| for k, v in stats.items() |
| if not k.startswith('infra-') |
| } |
| statistics['infra'] = {k[6:]: v for k, v in stats.items() if k.startswith('infra-')} |
| # The actual structure to dump, add commit and date to it |
| final = {'packages': pkgs, |
| 'stats': statistics, |
| 'defconfigs': defconfigs, |
| 'package_status_checks': Package.status_checks, |
| 'commit': commit, |
| 'date': str(date)} |
| |
| with open(output, 'w') as f: |
| json.dump(final, f, indent=2, separators=(',', ': ')) |
| f.write('\n') |
| |
| |
| def resolvepath(path): |
| return os.path.abspath(os.path.expanduser(path)) |
| |
| |
| def parse_args(): |
| parser = argparse.ArgumentParser() |
| output = parser.add_argument_group('output', 'Output file(s)') |
| output.add_argument('--html', dest='html', type=resolvepath, |
| help='HTML output file') |
| output.add_argument('--json', dest='json', type=resolvepath, |
| help='JSON output file') |
| packages = parser.add_mutually_exclusive_group() |
| packages.add_argument('-c', dest='configpackages', action='store_true', |
| help='Apply to packages enabled in current configuration') |
| packages.add_argument('-n', dest='npackages', type=int, action='store', |
| help='Number of packages') |
| packages.add_argument('-p', dest='packages', action='store', |
| help='List of packages (comma separated)') |
| parser.add_argument('--nvd-path', dest='nvd_path', |
| help='Path to the local NVD database', type=resolvepath) |
| args = parser.parse_args() |
| if not args.html and not args.json: |
| parser.error('at least one of --html or --json (or both) is required') |
| return args |
| |
| |
| def __main__(): |
| global cvecheck |
| |
| args = parse_args() |
| |
| if args.nvd_path: |
| import cve as cvecheck |
| |
| if args.packages: |
| package_list = args.packages.split(",") |
| elif args.configpackages: |
| package_list = get_config_packages() |
| else: |
| package_list = None |
| date = datetime.datetime.utcnow() |
| commit = subprocess.check_output(['git', '-C', brpath, |
| 'rev-parse', |
| 'HEAD']).splitlines()[0].decode() |
| print("Build package list ...") |
| packages = get_pkglist(args.npackages, package_list) |
| print("Getting developers ...") |
| developers = parse_developers() |
| print("Build defconfig list ...") |
| defconfigs = get_defconfig_list() |
| for d in defconfigs: |
| d.set_developers(developers) |
| print("Getting package make info ...") |
| package_init_make_info() |
| print("Getting package details ...") |
| for pkg in packages: |
| pkg.set_infra() |
| pkg.set_license() |
| pkg.set_hash_info() |
| pkg.set_patch_count() |
| pkg.set_check_package_warnings() |
| pkg.set_current_version() |
| pkg.set_cpeid() |
| pkg.set_url() |
| pkg.set_ignored_cves() |
| pkg.set_developers(developers) |
| print("Checking URL status") |
| loop = asyncio.get_event_loop() |
| loop.run_until_complete(check_package_urls(packages)) |
| print("Getting latest versions ...") |
| loop = asyncio.get_event_loop() |
| loop.run_until_complete(check_package_latest_version(packages)) |
| if args.nvd_path: |
| print("Checking packages CVEs") |
| check_package_cves(args.nvd_path, packages) |
| check_package_cpes(args.nvd_path, packages) |
| print("Calculate stats") |
| stats = calculate_stats(packages) |
| if args.html: |
| print("Write HTML") |
| dump_html(packages, stats, date, commit, args.html) |
| if args.json: |
| print("Write JSON") |
| dump_json(packages, defconfigs, stats, date, commit, args.json) |
| |
| |
| __main__() |