From 8fe7cdd10949c1ea9711c12369fe5b78d0bb4d23 Mon Sep 17 00:00:00 2001 From: "Rodrigo M. Duarte" Date: Fri, 7 Jun 2024 09:13:57 -0300 Subject: [PATCH] cve-filter: Add class to filter cve files This class is designed to filter CVEs (Common Vulnerabilities and Exposures) from CVE files. It should be used in conjunction with the cve-check class from the openembedded-core. Steps to Use This Class 1. Add the following lines to your distro configuration file: include conf/distro/include/cve-extra-exclusions.inc INHERIT += "cve-check" 2. Inherit the cve-filter class in the image recipe. -------- Configuration Variables ---------------------------- The cve-filter class provides several configurable variables: CVE_FILTER_PREVIOUS_FILE: Specifies the previous version of the CVE JSON file. If no file is provided, only the current file will be considered. Default: empty CVE_FILTER_PREVIOUS_VERSION: Specifies the distro version of the previous CVE JSON file. The CVE_FILTER_PREVIOUS_FILE must be defined, otherwise the value 0 will be used. Example: "1.0.0" Default: "0.0.0" CVE_FILTER_MARKDOWN_FILE_NAME: Specifies the name of the output Markdown file containing the list of detected CVEs. Default: "${IMAGE_NAME}.md" CVE_FILTER_IGNORED_CVES: Lists the CVEs that should be ignored by the filter. Example: "CVE-2017-6264 CVE-2023-1234" Default: empty The following files was added: - classes/cve-filter.bbclass - lib/ossystems/cve_filter.py - lib/ossystems/__init__.py Also, the following file was changed: - conf/layer.conf Signed-off-by: Rodrigo M. Duarte --- classes/cve-filter.bbclass | 91 ++++++++++ conf/layer.conf | 2 + lib/ossystems/__init__.py | 4 + lib/ossystems/cve_filter.py | 349 ++++++++++++++++++++++++++++++++++++ 4 files changed, 446 insertions(+) create mode 100644 classes/cve-filter.bbclass create mode 100644 lib/ossystems/__init__.py create mode 100644 lib/ossystems/cve_filter.py diff --git a/classes/cve-filter.bbclass b/classes/cve-filter.bbclass new file mode 100644 index 0000000..29901d3 --- /dev/null +++ b/classes/cve-filter.bbclass @@ -0,0 +1,91 @@ +# Copyright (c) 2024 O.S. Systems Software LTDA. +# Usage Instructions for the Yocto CVE Filter Class + +# This class is designed to filter CVEs (Common Vulnerabilities +# and Exposures) from CVE files. It should be used in conjunction +# with the cve-check class from the openembedded-core. + +# Steps to Use This Class + +# 1. Add the following lines to your distro configuration file: + +# include conf/distro/include/cve-extra-exclusions.inc +# INHERIT += "cve-check" + +# 2. Inherit the cve-filter class in the image recipe. +# +# -------- Configuration Variables ---------------------------- + +# The cve-filter class provides several configurable variables: + +# CVE_FILTER_PREVIOUS_FILE: Specifies the previous version of +# the CVE JSON file. If no file is provided, only the current +# file will be considered. +# Default: empty + +# CVE_FILTER_PREVIOUS_VERSION: Specifies the distro version of +# the previous CVE JSON file. The CVE_FILTER_PREVIOUS_FILE must +# be defined, otherwise the value 0 will be used. +# Example: "1.0.0" +# Default: "0.0.0" + +# CVE_FILTER_MARKDOWN_FILE_NAME: Specifies the name of the +# output Markdown file containing the list of detected CVEs. +# Default: "${IMAGE_NAME}.md" + +# CVE_FILTER_IGNORED_CVES: Lists the CVEs that should be ignored by the filter. +# Example: "CVE-2017-6264 CVE-2023-1234" +# Default: empty + +# Set the PATH to find the old CVE Json list +CVE_FILTER_PREVIOUS_FILE ??= "" +CVE_FILTER_PREVIOUS_VERSION ??= "0.0.0" + +CVE_FILTER_CURRENT_FILE = "${IMGDEPLOYDIR}/${IMAGE_NAME}.json" +CVE_FILTER_CURRENT_VERSION = "${DISTRO_VERSION}" + +# Set the name of markdown output file +CVE_FILTER_MARKDOWN_FILE_NAME ?= "${IMAGE_NAME}.md" +CVE_FILTER_MARKDOWN_FILE = "${IMGDEPLOYDIR}/${CVE_FILTER_MARKDOWN_FILE_NAME}" + +# List of CVE should be ignored Eg: CVE-2023-1234 +CVE_FILTER_IGNORED_CVES ??= "" + +inherit python3native + +python do_cve_filter (){ + from ossystems.cve_filter import Cve + + previousFile = d.getVar("CVE_FILTER_PREVIOUS_FILE") + previousVersion = d.getVar("CVE_FILTER_PREVIOUS_VERSION") + cveIgnoreList = d.getVar("CVE_FILTER_IGNORED_CVES").split() + + cve_prev = Cve() + cve_prev.setMarkdonFileName(d.getVar("CVE_FILTER_MARKDOWN_FILE")) + cve_curr = Cve() + + if previousFile: + cve_prev.loadCVEfile(previousFile) + cve_prev.setCVEVersion(previousVersion) + cve_prev.setIgnoreCVEList(cveIgnoreList) + cve_prev.loadCVEData() + else: + bb.warn("Previous CVE File Not Defined!!!") + + cve_curr.loadCVEfile(d.getVar("CVE_FILTER_CURRENT_FILE")) + cve_curr.setCVEVersion(d.getVar("CVE_FILTER_CURRENT_VERSION")) + cve_curr.setIgnoreCVEList(cveIgnoreList) + cve_curr.loadCVEData() + cve_prev.compareCVes(cve_curr) + bb.plain("DONE!!") +} + +addtask cve_filter after do_image before do_image_complete + +IMAGE_POSTPROCESS_COMMAND += "link_cvefilter_markdownfile;" + +link_cvefilter_markdownfile () { + if [ -e "${CVE_FILTER_MARKDOWN_FILE}" ]; then + ln -sf ${CVE_FILTER_MARKDOWN_FILE_NAME} ${IMGDEPLOYDIR}/${IMAGE_LINK_NAME}.md + fi +} diff --git a/conf/layer.conf b/conf/layer.conf index 4c2bc50..a3fdc64 100644 --- a/conf/layer.conf +++ b/conf/layer.conf @@ -9,6 +9,8 @@ BBFILE_COLLECTIONS += "ossystems-base" BBFILE_PATTERN_ossystems-base := "^${LAYERDIR}/" BBFILE_PRIORITY_ossystems-base = "8" +addpylib ${LAYERDIR}/lib ossystems + LAYERSERIES_COMPAT_ossystems-base = "scarthgap" LICENSE_PATH += "${LAYERDIR}/conf/licenses" diff --git a/lib/ossystems/__init__.py b/lib/ossystems/__init__.py new file mode 100644 index 0000000..8e5d5cb --- /dev/null +++ b/lib/ossystems/__init__.py @@ -0,0 +1,4 @@ +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) + +BBIMPORTS = ["cve_filter"] diff --git a/lib/ossystems/cve_filter.py b/lib/ossystems/cve_filter.py new file mode 100644 index 0000000..878f306 --- /dev/null +++ b/lib/ossystems/cve_filter.py @@ -0,0 +1,349 @@ +import argparse +import json +import sys + + +# Class to store the CVE data +class Issue: + def __init__(self, id, summary, scorev2, scorev3, vector, status, link): + self.id = id + self.summary = summary + self.scorev2 = scorev2 + self.scorev3 = scorev3 + self.vector = vector + self.status = status + self.link = link + + def __eq__(self, other): + return self.id == other.id + + def __hash__(self): + return hash(self.id) + + def __str__(self): + return self.id + + def toTuple(self): + return ( + self.id, + self.summary, + self.scorev2, + self.scorev3, + self.vector, + self.status, + self.link, + ) + + +# Class to store the filtered and ready to print CVes +class CVEFilter: + def __init__(self, name, status): + self.name = name + self.status = status + self.both = [] + self.patched = [] + self.old = [] + self.new = [] + + def setVersion(self, version1, version2=0): + self.version1 = version1 + self.version2 = version2 + + def addCVEBoth(self, issue): + self.both.append(issue) + + def addCVEPatched(self, issue): + self.patched.append(issue) + + def addCVEOld(self, issue): + self.old.append(issue) + + def addCVENew(self, issue): + self.new.append(issue) + + +# Class to store the Package with the list of Issues +class Package: + def __init__(self, name, version): + self.name = name + self.version = version + self.cve = [] + + def addCVE(self, cve): + self.cve.append(cve) + + +# Class to handle CVEs +class Cve: + def __init__(self): + self.__markdownFileName = "output.md" + self.__packages = [] + self.__printIssues = [] + self.__ignored_cves = [] + self.__version = 0 + + # def __del__ (self): + # self.__cveJsonFile.close() + + def loadCVEfile(self, fileName): + try: + self.__cveJsonFile = open(fileName, "r") + try: + self.__jsonData = json.load(self.__cveJsonFile) + except ValueError as e: + print("Fail on JSON file: " + fileName) + print(e) + self.__jsonPackages = self.__jsonData["package"] + + except FileNotFoundError: + print("File not found or wrong file name! " + fileName) + exit(1) + except: + print("Error to open file!") + exit(1) + + def setMarkdonFileName(self, name): + if name: + self.__markdownFileName = name + + def setIgnoreCVEList(self, listcve): + self.__ignored_cves = list(listcve) + + def setCVEVersion(self, version=0): + self.__version = version + + def getCVEPackages(self): + return self.__packages + + def getCVEVersion(self): + return self.__version + + def printCVEs(self): + print(self.__jsonData["package"]) + + def loadCVEData(self): + for pack in self.__jsonPackages: + p = Package(pack["name"], pack["version"]) + entry = False + for id in pack["issue"]: + if (float(id["scorev2"]) >= 9 or float(id["scorev3"]) >= 9) and id[ + "status" + ] != "Ignored": + if not (id["id"] in self.__ignored_cves): + p.addCVE( + Issue( + id["id"], + id["summary"], + id["scorev2"], + id["scorev3"], + id["vector"], + id["status"], + id["link"], + ) + ) + entry = True + if entry: + self.__packages.append(p) + + def __creatMarkdownFile(self): + try: + self.__markdownFile = open(self.__markdownFileName, "w") + except: + print("ERROR to create the Markdown output file") + exit(1) + + def printUnpatchedListCVEs(self): + for package in self.__packages: + print("--------------- PACKAGE -----------------") + print(package.name) + print(package.version) + print("=============== ISSUES ==================") + for id in package.cve: + if id.status != "Patched": + print(id.id) + print(id.scorev2) + print(id.scorev3) + print(id.status) + + def compareCVes(self, compCVE): + self.__creatMarkdownFile() + packagesNew = compCVE.getCVEPackages() + for packageOld in self.__packages: + contain = False + for packageNew in packagesNew: + if packageOld.name == packageNew.name: + # This case if the package has the same version + if packageOld.version == packageNew.version: + cveOutput = CVEFilter(packageOld.name, "Kept") + cveOutput.setVersion(packageOld.version) + self.__checkIssues(cveOutput, packageOld.cve, packageNew.cve) + self.__printIssues.append(cveOutput) + # remove package that has comparated + packagesNew.remove(packageNew) + contain = True + break + + # This case if the package was upgrated + elif packageOld.version < packageNew.version: + cveOutput = CVEFilter(packageOld.name, "Updated") + cveOutput.setVersion(packageOld.version, packageNew.version) + self.__checkIssues(cveOutput, packageOld.cve, packageNew.cve) + self.__printIssues.append(cveOutput) + # remove package that has comparated + packagesNew.remove(packageNew) + contain = True + break + + # This case if the package was Downgrade + else: + cveOutput = CVEFilter(packageOld.name, "Downgraded") + cveOutput.setVersion(packageOld.version, packageNew.version) + self.__checkIssues(cveOutput, packageOld.cve, packageNew.cve) + self.__printIssues.append(cveOutput) + # remove package that has comparated + packagesNew.remove(packageNew) + contain = True + break + + # This case is when the package was removed + if not contain: + cveOutput = CVEFilter(packageOld.name, "Removed") + cveOutput.setVersion(packageOld.version) + self.__checkIssues(cveOutput, packageOld.cve, []) + self.__printIssues.append(cveOutput) + # remove package that has comparated + # For all cases that packages was added + for newpackage in packagesNew: + cveOutput = CVEFilter(newpackage.name, "Added") + cveOutput.setVersion(newpackage.version) + self.__checkIssues(cveOutput, [], newpackage.cve) + self.__printIssues.append(cveOutput) + # remove package that has comparated + self.__printCVEs( + self.__printIssues, self.getCVEVersion(), compCVE.getCVEVersion() + ) + + def __checkIssues(self, cveOut, cveListOld, cveListNew): + cveOut.new = cveListNew.copy() + if cveListOld and cveListNew: + for cveold in cveListOld: + contain = False + for cvenew in cveOut.new: + if cveold.id == cvenew.id: + if (cveold.status == cvenew.status) and ( + cvenew.status == "Unpatched" + ): + cveOut.addCVEBoth(cveold) + cveOut.new.remove(cvenew) + contain = True + break + elif (cveold.status == cvenew.status) and ( + cvenew.status == "Patched" + ): + cveOut.new.remove(cvenew) + contain = True + break + elif (cveold.status != cvenew.status) and ( + cvenew.status == "Patched" + ): + cveOut.addCVEPatched(cvenew) + cveOut.new.remove(cvenew) + contain = True + break + elif (cveold.status != cvenew.status) and ( + cvenew.status == "Unpatched" + ): + cveOut.addCVENew(cvenew) + cveOut.new.remove(cvenew) + contain = True + break + if not contain: + cveOut.addCVEOld(cveold) + elif cveListNew: + for cveNew in cveListNew: + if cveNew.status == "Patched": + cveOut.new.remove(cveNew) + else: + for cveOld in cveListOld: + if cveOld.status != "Patched": + cveOut.addCVEOld(cveOld) + + def __printCVEs(self, cvePrint, v1, v2): + self.__markdownFile.write(f"# {v1} -> {v2}") + for cve in cvePrint: + if cve.old or cve.both or cve.patched or cve.new: + if cve.status == "Updated" or cve.status == "Downgraded": + self.__markdownFile.write("\n") + self.__markdownFile.write( + "## " + + cve.name + + ": " + + cve.version1 + + " -> " + + cve.version2 + + " : " + + cve.status + + "\n" + ) + else: + self.__markdownFile.write("\n") + self.__markdownFile.write( + "## " + + cve.name + + ": " + + cve.version1 + + " : " + + cve.status + + "\n" + ) + self.__printIs(cve) + + def __printIs(self, issue): + if issue.both: + if issue.status == "Updated" or issue.status == "Downgraded": + self.__markdownFile.write("\n### Affect both versions\n") + else: + self.__markdownFile.write("\n### Unsolved\n") + self.__markdownFile.write("| CVE | STATUS | SCORE V2 | SCORE V3 |\n") + self.__markdownFile.write("|:--|:--:|:--:|:---:|\n") + s = ' Vulnerable ' + for i in issue.both: + self.__markdownFile.write( + f"|[{i.id}]({i.link})|{s}|{i.scorev2}|{i.scorev3}|\n" + ) + + if issue.patched: + self.__markdownFile.write("\n### Fixed in new revision\n") + self.__markdownFile.write("| CVE | STATUS | SCORE V2 | SCORE V3 |\n") + self.__markdownFile.write("|:--|:--:|:--:|:---:|\n") + s = ' Patched ' + for i in issue.patched: + self.__markdownFile.write( + f"|[{i.id}]({i.link})|{s}|{i.scorev2}|{i.scorev3}|\n" + ) + + if issue.old: + if issue.status == "Removed": + self.__markdownFile.write("\n### Affected the removed version\n") + else: + self.__markdownFile.write("\n### Affect the old version\n") + self.__markdownFile.write("| CVE | STATUS | SCORE V2 | SCORE V3 |\n") + self.__markdownFile.write("|:--|:--:|:--:|:---:|\n") + s = ' Vulnerable ' + for i in issue.old: + self.__markdownFile.write( + f"|[{i.id}]({i.link})|{s}|{i.scorev2}|{i.scorev3}|\n" + ) + + if issue.new: + if issue.status == "Updated" or issue.status == "Downgraded": + self.__markdownFile.write("\n### New Issues added\n") + else: + self.__markdownFile.write("\n### Affect only new revision\n") + self.__markdownFile.write("| CVE | STATUS | SCORE V2 | SCORE V3 |\n") + self.__markdownFile.write("|:--|:--:|:--:|:---:|\n") + s = ' Vulnerable ' + for i in issue.new: + self.__markdownFile.write( + f"|[{i.id}]({i.link})|{s}|{i.scorev2}|{i.scorev3}|\n" + )