#!/usr/bin/env python import os import json import subprocess from yunohost.diagnosis import Diagnoser from moulinette.utils.filesystem import read_json, write_to_json class SecurityDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 3600 dependencies = [] def run(self): "CVE-2017-5754" if self.is_vulnerable_to_meltdown(): yield dict(meta={"test": "meltdown"}, status="ERROR", summary="diagnosis_security_vulnerable_to_meltdown", details=["diagnosis_security_vulnerable_to_meltdown_details"] ) else: yield dict(meta={}, status="SUCCESS", summary="diagnosis_security_all_good" ) def is_vulnerable_to_meltdown(self): # meltdown CVE: https://security-tracker.debian.org/tracker/CVE-2017-5754 # We use a cache file to avoid re-running the script so many times, # which can be expensive (up to around 5 seconds on ARM) # and make the admin appear to be slow (c.f. the calls to diagnosis # from the webadmin) # # The cache is in /tmp and shall disappear upon reboot # *or* we compare it to dpkg.log modification time # such that it's re-ran if there was package upgrades # (e.g. from yunohost) cache_file = "/tmp/yunohost-meltdown-diagnosis" dpkg_log = "/var/log/dpkg.log" if os.path.exists(cache_file): if not os.path.exists(dpkg_log) or os.path.getmtime(cache_file) > os.path.getmtime(dpkg_log): self.logger_debug("Using cached results for meltdown checker, from %s" % cache_file) return read_json(cache_file)[0]["VULNERABLE"] # script taken from https://github.com/speed47/spectre-meltdown-checker # script commit id is store directly in the script SCRIPT_PATH = "/usr/lib/moulinette/yunohost/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh" # '--variant 3' corresponds to Meltdown # example output from the script: # [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}] try: self.logger_debug("Running meltdown vulnerability checker") call = subprocess.Popen("bash %s --batch json --variant 3" % SCRIPT_PATH, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # TODO / FIXME : here we are ignoring error messages ... # in particular on RPi2 and other hardware, the script complains about # "missing some kernel info (see -v), accuracy might be reduced" # Dunno what to do about that but we probably don't want to harass # users with this warning ... output, err = call.communicate() assert call.returncode in (0, 2, 3), "Return code: %s" % call.returncode # If there are multiple lines, sounds like there was some messages # in stdout that are not json >.> ... Try to get the actual json # stuff which should be the last line output = output.strip() if "\n" in output: self.logger_debug("Original meltdown checker output : %s" % output) output = output.split("\n")[-1] CVEs = json.loads(output) assert len(CVEs) == 1 assert CVEs[0]["NAME"] == "MELTDOWN" except Exception as e: import traceback traceback.print_exc() self.logger_warning("Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" % e) raise Exception("Command output for failed meltdown check: '%s'" % output) self.logger_debug("Writing results from meltdown checker to cache file, %s" % cache_file) write_to_json(cache_file, CVEs) return CVEs[0]["VULNERABLE"] def main(args, env, loggers): return SecurityDiagnoser(args, env, loggers).diagnose()