From patchwork Fri Jul 24 15:43:50 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Gregory CLEMENT X-Patchwork-Id: 1335814 Return-Path: X-Original-To: incoming-buildroot@patchwork.ozlabs.org Delivered-To: patchwork-incoming-buildroot@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=busybox.net (client-ip=140.211.166.136; helo=silver.osuosl.org; envelope-from=buildroot-bounces@busybox.net; receiver=) Authentication-Results: ozlabs.org; dmarc=none (p=none dis=none) header.from=bootlin.com Received: from silver.osuosl.org (smtp3.osuosl.org [140.211.166.136]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 4BCtmZ4PfLz9sRN for ; Sat, 25 Jul 2020 01:44:30 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by silver.osuosl.org (Postfix) with ESMTP id 1A59123600; Fri, 24 Jul 2020 15:44:29 +0000 (UTC) X-Virus-Scanned: amavisd-new at osuosl.org Received: from silver.osuosl.org ([127.0.0.1]) by localhost (.osuosl.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id klCpHr+f7cmT; Fri, 24 Jul 2020 15:44:26 +0000 (UTC) Received: from ash.osuosl.org (ash.osuosl.org [140.211.166.34]) by silver.osuosl.org (Postfix) with ESMTP id E8B6F23509; Fri, 24 Jul 2020 15:44:25 +0000 (UTC) X-Original-To: buildroot@lists.busybox.net Delivered-To: buildroot@osuosl.org Received: from hemlock.osuosl.org (smtp2.osuosl.org [140.211.166.133]) by ash.osuosl.org (Postfix) with ESMTP id 298281BF39D for ; Fri, 24 Jul 2020 15:44:17 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by hemlock.osuosl.org (Postfix) with ESMTP id 26AE4889A0 for ; Fri, 24 Jul 2020 15:44:17 +0000 (UTC) X-Virus-Scanned: amavisd-new at osuosl.org Received: from hemlock.osuosl.org ([127.0.0.1]) by localhost (.osuosl.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id 8P9ueAaO0CbM for ; Fri, 24 Jul 2020 15:44:14 +0000 (UTC) X-Greylist: domain auto-whitelisted by SQLgrey-1.7.6 Received: from relay11.mail.gandi.net (relay11.mail.gandi.net [217.70.178.231]) by hemlock.osuosl.org (Postfix) with ESMTPS id 5FE8A88995 for ; Fri, 24 Jul 2020 15:44:14 +0000 (UTC) Received: from localhost (91-175-115-186.subs.proxad.net [91.175.115.186]) (Authenticated sender: gregory.clement@bootlin.com) by relay11.mail.gandi.net (Postfix) with ESMTPSA id 83E1110000C; Fri, 24 Jul 2020 15:44:10 +0000 (UTC) From: Gregory CLEMENT To: buildroot@buildroot.org Date: Fri, 24 Jul 2020 17:43:50 +0200 Message-Id: <20200724154356.2607639-3-gregory.clement@bootlin.com> X-Mailer: git-send-email 2.27.0 In-Reply-To: <20200724154356.2607639-1-gregory.clement@bootlin.com> References: <20200724154356.2607639-1-gregory.clement@bootlin.com> MIME-Version: 1.0 Subject: [Buildroot] [PATCH v3 2/8] support/scripts/cve.py: Switch to JSON 1.1 X-BeenThere: buildroot@busybox.net X-Mailman-Version: 2.1.29 Precedence: list List-Id: Discussion and development of buildroot List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Matt Weber , Thomas Petazzoni , Titouan Christophe Errors-To: buildroot-bounces@busybox.net Sender: "buildroot" In 2019, the JSON vulnerability feeds switched from version 1.0 to 1.1. The main difference is the removal of the affects element that was used to check if a package was affected by a CVE. This information is duplicated in the configuration element which contains in the end the cpeid as well as properties about the versions affected. Instead of having a list of the versions affected, with these properties, it is possible to have a range of versions. Signed-off-by: Gregory CLEMENT --- support/scripts/cve.py | 125 ++++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 27 deletions(-) diff --git a/support/scripts/cve.py b/support/scripts/cve.py index 8a4087ef8a..a8861d966c 100755 --- a/support/scripts/cve.py +++ b/support/scripts/cve.py @@ -34,9 +34,19 @@ except ImportError: sys.path.append('utils/') NVD_START_YEAR = 2002 -NVD_JSON_VERSION = "1.0" +NVD_JSON_VERSION = "1.1" NVD_BASE_URL = "https://nvd.nist.gov/feeds/json/cve/" + NVD_JSON_VERSION +import operator + +ops = { + '>=' : operator.ge, + '>' : operator.gt, + '<=' : operator.le, + '<' : operator.lt, + '=' : operator.eq +} + class CVE: """An accessor class for CVE Items in NVD files""" CVE_AFFECTS = 1 @@ -99,23 +109,81 @@ class CVE: print("ERROR: cannot read %s. Please remove the file then rerun this script" % filename) raise for cve in content: - yield cls(cve['cve']) + yield cls(cve) def each_product(self): """Iterate over each product section of this cve""" - for vendor in self.nvd_cve['affects']['vendor']['vendor_data']: + for vendor in self.nvd_cve['cve']['affects']['vendor']['vendor_data']: for product in vendor['product']['product_data']: yield product + def parse_node(self, node): + """ + Parse the node inside the configurations section to extract the + cpe information usefull to know if a product is affected by + the CVE. Actually only the product name and the version + descriptor are needed, but we also provide the vendor name. + """ + + # The node containing the cpe entries matching the CVE can also + # contain sub-nodes, so we need to manage it. + for child in node.get('children', ()): + for parsed_node in self.parse_node(child): + yield parsed_node + + for cpe in node.get('cpe_match', ()): + if not cpe['vulnerable']: + return + vendor, product, version = cpe['cpe23Uri'].split(':')[3:6] + op_start = '' + op_end = '' + v_start = '' + v_end = '' + + if version != '*' and version != '-': + # Version is defined, this is a '=' match + op_start = '=' + v_start = version + elif version == '-': + # no version information is available + op_start = '=' + v_start = version + else: + # Parse start version, end version and operators + if 'versionStartIncluding' in cpe: + op_start = '>=' + v_start = cpe['versionStartIncluding'] + + if 'versionStartExcluding' in cpe: + op_start = '>' + v_start = cpe['versionStartExcluding'] + + if 'versionEndIncluding' in cpe: + op_end = '<=' + v_end = cpe['versionEndIncluding'] + + if 'versionEndExcluding' in cpe: + op_end = '<' + v_end = cpe['versionEndExcluding'] + + key =['vendor', 'product', 'v_start', 'op_start', 'v_end', 'op_end'] + val = [vendor, product, v_start, op_start, v_end, op_end] + yield dict(zip(key, val)) + + def each_cpe(self): + for node in self.nvd_cve['configurations']['nodes']: + for cpe in self.parse_node(node): + yield cpe + @property def identifier(self): """The CVE unique identifier""" - return self.nvd_cve['CVE_data_meta']['ID'] + return self.nvd_cve['cve']['CVE_data_meta']['ID'] @property def pkg_names(self): """The set of package names referred by this CVE definition""" - return set(p['product_name'] for p in self.each_product()) + return set(p['product'] for p in self.each_cpe()) def affects(self, br_pkg): """ @@ -125,32 +193,35 @@ class CVE: if br_pkg.is_cve_ignored(self.identifier): return self.CVE_DOESNT_AFFECT - for product in self.each_product(): - if product['product_name'] != br_pkg.name: + for cpe in self.each_cpe(): + affected = True + if cpe['product'] != br_pkg.name: continue + if cpe['v_start'] == '-': + return self.CVE_AFFECTS + if not (cpe['v_start'] or cpe['v_end']): + print("No CVE affected version") + continue + pkg_version = distutils.version.LooseVersion(br_pkg.current_version) + if not hasattr(pkg_version, "version"): + print("Cannot parse package '%s' version '%s'" % (br_pkg.name, br_pkg.current_version)) + continue + + if cpe['v_start']: + try: + cve_affected_version = distutils.version.LooseVersion(cpe['v_start']) + affected = ops.get(cpe['op_start'])(pkg_version, cve_affected_version) + break + except: + return self.CVE_UNKNOWN - for v in product['version']['version_data']: - if v["version_affected"] == "=": - if br_pkg.current_version == v["version_value"]: - return self.CVE_AFFECTS - elif v["version_affected"] == "<=": - pkg_version = distutils.version.LooseVersion(br_pkg.current_version) - if not hasattr(pkg_version, "version"): - print("Cannot parse package '%s' version '%s'" % (br_pkg.name, br_pkg.current_version)) - continue - cve_affected_version = distutils.version.LooseVersion(v["version_value"]) - if not hasattr(cve_affected_version, "version"): - print("Cannot parse CVE affected version '%s'" % v["version_value"]) - continue + if (affected and cpe['v_end']): try: - affected = pkg_version <= cve_affected_version + cve_affected_version = distutils.version.LooseVersion(cpe['v_end']) + affected = ops.get(cpe['op_end'])(pkg_version, cve_affected_version) break except TypeError: return self.CVE_UNKNOWN - if affected: - return self.CVE_AFFECTS - else: - return self.CVE_DOESNT_AFFECT - else: - print("version_affected: %s" % v['version_affected']) + if (affected): + return self.CVE_AFFECTS return self.CVE_DOESNT_AFFECT