diff options
| -rw-r--r-- | classes/cve-report.bbclass | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/classes/cve-report.bbclass b/classes/cve-report.bbclass new file mode 100644 index 0000000..35d58d0 --- /dev/null +++ b/classes/cve-report.bbclass | |||
| @@ -0,0 +1,216 @@ | |||
| 1 | # Class to inherit when you want to generate a CVE reports. | ||
| 2 | # | ||
| 3 | # Generates package list file and package CVE report. | ||
| 4 | # | ||
| 5 | # Example: | ||
| 6 | # echo 'INHERIT += "cve-report"' >> conf/local.conf | ||
| 7 | # bitbake -c report_cve core-image-minimal | ||
| 8 | # | ||
| 9 | # Variables to be passed to "cvert-*" scripts: | ||
| 10 | # | ||
| 11 | # CVE_REPORT_MODE[foss] | ||
| 12 | # Path to the CVE FOSS report to be generated. | ||
| 13 | # | ||
| 14 | # CVE_REPORT_MODE[restore] | ||
| 15 | # Path to the CVE dump data file. | ||
| 16 | # | ||
| 17 | # E.g. for multiple MACHINEs: | ||
| 18 | # (1) generate CVE dump: | ||
| 19 | # cvert-update --store /path/to/cvedump $TEMP/nvdfeed | ||
| 20 | # (2) for mach in $(get_machine_list); do | ||
| 21 | # (source oe-init-build-env "build-$mach"; | ||
| 22 | # echo 'CVE_REPORT_MODE[restore] = "/path/to/cvedump"' >> conf/local.conf; | ||
| 23 | # echo 'CVE_REPORT_MODE[foss] = "/path/to/report-foss-'${mach}'"' >> conf/local.conf; | ||
| 24 | # MACHINE=$mach bitbake -c report_cve core-image-minimal) | ||
| 25 | # done | ||
| 26 | # | ||
| 27 | # CVE_REPORT_MODE[offline] | ||
| 28 | # Either "0" or "1". Offline mode ("--offline" parameter for cvert-* scripts). | ||
| 29 | # | ||
| 30 | # CVE_REPORT_MODE[feeddir] | ||
| 31 | # Path to the NVD feed directory. | ||
| 32 | # | ||
| 33 | # CVE_REPORT_MODE[packagelist] | ||
| 34 | # Path to the package list file to be generated. | ||
| 35 | # | ||
| 36 | # CVE_REPORT_MODE[packageonly] | ||
| 37 | # Either "0" or "1". Generate package list file, then stop. | ||
| 38 | # | ||
| 39 | # CVE_REPORT_MODE[blacklist] | ||
| 40 | # Ignore specific class. | ||
| 41 | # | ||
| 42 | |||
| 43 | CVE_REPORT_MODE[foss] ?= "${LOG_DIR}/cvert/report-foss.txt" | ||
| 44 | CVE_REPORT_MODE[offline] ?= "0" | ||
| 45 | CVE_REPORT_MODE[feeddir] ?= "${LOG_DIR}/nvdfeeds" | ||
| 46 | CVE_REPORT_MODE[packagelist] ?= "${LOG_DIR}/cvert/package.lst" | ||
| 47 | CVE_REPORT_MODE[packageonly] ?= "0" | ||
| 48 | CVE_REPORT_MODE[blacklist] ?= "native,nativesdk,cross,crosssdk,cross-canadian,packagegroup,image" | ||
| 49 | |||
| 50 | CVE_PRODUCT ??= "${BPN}" | ||
| 51 | CVE_VERSION ??= "${PV}" | ||
| 52 | |||
| 53 | addhandler generate_report_handler | ||
| 54 | generate_report_handler[eventmask] = "bb.event.BuildCompleted" | ||
| 55 | |||
| 56 | def cvert_update(d): | ||
| 57 | """Update NVD storage and prepare CVE dump""" | ||
| 58 | |||
| 59 | import tempfile | ||
| 60 | import subprocess | ||
| 61 | |||
| 62 | bb.utils.export_proxies(d) | ||
| 63 | |||
| 64 | dump = os.path.join(d.getVar("LOG_DIR"), "cvedump") | ||
| 65 | |||
| 66 | bb.note("Updating CVE database: %s" % dump) | ||
| 67 | |||
| 68 | cmd = [ | ||
| 69 | "cvert-update", | ||
| 70 | "--store", dump, | ||
| 71 | "--debug", | ||
| 72 | d.getVarFlag("CVE_REPORT_MODE", "feeddir") | ||
| 73 | ] | ||
| 74 | |||
| 75 | if d.getVarFlag("CVE_REPORT_MODE", "offline") != "0": | ||
| 76 | cmd.append("--offline") | ||
| 77 | |||
| 78 | try: | ||
| 79 | bb.debug(2, "Call '%s'" % " ".join(cmd)) | ||
| 80 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode() | ||
| 81 | bb.debug(2, "Output: %s" % output) | ||
| 82 | except subprocess.CalledProcessError as e: | ||
| 83 | bb.error("Failed to run cvert-update: '%s'\n%s: %s" % (" ".join(cmd), e, e.output)) | ||
| 84 | |||
| 85 | return dump | ||
| 86 | |||
| 87 | # copied from cve-check.bbclass | ||
| 88 | def get_patches_cves(d): | ||
| 89 | """Get patches that solve CVEs using the "CVE: " tag""" | ||
| 90 | |||
| 91 | import re | ||
| 92 | |||
| 93 | pn = d.getVar("PN") | ||
| 94 | cve_match = re.compile("CVE:( CVE\-\d{4}\-\d+)+") | ||
| 95 | |||
| 96 | # Matches last CVE-1234-211432 in the file name, also if written | ||
| 97 | # with small letters. Not supporting multiple CVE id's in a single | ||
| 98 | # file name. | ||
| 99 | cve_file_name_match = re.compile(".*([Cc][Vv][Ee]\-\d{4}\-\d+)") | ||
| 100 | |||
| 101 | patched_cves = set() | ||
| 102 | bb.debug(2, "Looking for patches that solves CVEs for %s" % pn) | ||
| 103 | for url in src_patches(d): | ||
| 104 | patch_file = bb.fetch.decodeurl(url)[2] | ||
| 105 | |||
| 106 | # Check patch file name for CVE ID | ||
| 107 | fname_match = cve_file_name_match.search(patch_file) | ||
| 108 | if fname_match: | ||
| 109 | cve = fname_match.group(1).upper() | ||
| 110 | patched_cves.add(cve) | ||
| 111 | bb.debug(2, "Found CVE %s from patch file name %s" % (cve, patch_file)) | ||
| 112 | |||
| 113 | with open(patch_file, "r", encoding="utf-8") as f: | ||
| 114 | try: | ||
| 115 | patch_text = f.read() | ||
| 116 | except UnicodeDecodeError: | ||
| 117 | bb.debug(1, "Failed to read patch %s using UTF-8 encoding" | ||
| 118 | " trying with iso8859-1" % patch_file) | ||
| 119 | f.close() | ||
| 120 | with open(patch_file, "r", encoding="iso8859-1") as f: | ||
| 121 | patch_text = f.read() | ||
| 122 | |||
| 123 | # Search for one or more "CVE: " lines | ||
| 124 | text_match = False | ||
| 125 | for match in cve_match.finditer(patch_text): | ||
| 126 | # Get only the CVEs without the "CVE: " tag | ||
| 127 | cves = patch_text[match.start()+5:match.end()] | ||
| 128 | for cve in cves.split(): | ||
| 129 | bb.debug(2, "Patch %s solves %s" % (patch_file, cve)) | ||
| 130 | patched_cves.add(cve) | ||
| 131 | text_match = True | ||
| 132 | |||
| 133 | if not fname_match and not text_match: | ||
| 134 | bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file) | ||
| 135 | |||
| 136 | return patched_cves | ||
| 137 | |||
| 138 | |||
| 139 | python generate_report_handler() { | ||
| 140 | if d.getVarFlag("CVE_REPORT_MODE", "packageonly") != "0": | ||
| 141 | return | ||
| 142 | |||
| 143 | import subprocess | ||
| 144 | |||
| 145 | restore = d.getVarFlag("CVE_REPORT_MODE", "restore") | ||
| 146 | |||
| 147 | if not restore: | ||
| 148 | restore = cvert_update(d) | ||
| 149 | |||
| 150 | if os.path.exists(d.getVarFlag("CVE_REPORT_MODE", "packagelist")): | ||
| 151 | report_foss = d.getVarFlag("CVE_REPORT_MODE", "foss") | ||
| 152 | |||
| 153 | bb.note("Generating CVE FOSS report: %s" % report_foss) | ||
| 154 | |||
| 155 | cmd = [ | ||
| 156 | "cvert-foss", | ||
| 157 | "--restore", restore, | ||
| 158 | "--output", report_foss, | ||
| 159 | d.getVarFlag("CVE_REPORT_MODE", "packagelist") | ||
| 160 | ] | ||
| 161 | |||
| 162 | try: | ||
| 163 | bb.debug(2, "Call '%s'" % " ".join(cmd)) | ||
| 164 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode() | ||
| 165 | bb.debug(2, "Output: %s" % output) | ||
| 166 | except subprocess.CalledProcessError as e: | ||
| 167 | bb.error("Failed to run cvert-foss: '%s'\n%s: %s" % (" ".join(cmd), e, e.output)) | ||
| 168 | } | ||
| 169 | |||
| 170 | addhandler build_started | ||
| 171 | build_started[eventmask] = "bb.event.BuildStarted" | ||
| 172 | |||
| 173 | python build_started() { | ||
| 174 | packagelist = d.getVarFlag("CVE_REPORT_MODE", "packagelist") | ||
| 175 | bb.utils.remove(packagelist) | ||
| 176 | bb.utils.mkdirhier(os.path.dirname(packagelist)) | ||
| 177 | bb.note("Package list: ", packagelist) | ||
| 178 | } | ||
| 179 | |||
| 180 | addtask do_report_cve after do_report_patched | ||
| 181 | |||
| 182 | do_report_cve[recrdeptask] = "do_report_cve do_report_patched" | ||
| 183 | do_report_cve[recideptask] = "do_${BB_DEFAULT_TASK}" | ||
| 184 | do_report_cve[nostamp] = "1" | ||
| 185 | |||
| 186 | do_report_cve() { | ||
| 187 | : | ||
| 188 | } | ||
| 189 | |||
| 190 | python do_report_patched() { | ||
| 191 | if not d.getVar("SRC_URI"): | ||
| 192 | return | ||
| 193 | |||
| 194 | cve_product = d.getVar("CVE_PRODUCT") | ||
| 195 | |||
| 196 | if not cve_product: | ||
| 197 | return | ||
| 198 | |||
| 199 | cve_version = d.getVar("CVE_VERSION") | ||
| 200 | patched_cves = get_patches_cves(d) | ||
| 201 | |||
| 202 | with open(d.getVarFlag("CVE_REPORT_MODE", "packagelist"), "a") as fil: | ||
| 203 | fil.write("%s,%s,%s\n" % (cve_product, cve_version, " ".join(patched_cves))) | ||
| 204 | bb.debug(2, "Append to package-list: '%s,%s,%s'" % (cve_product, cve_version, " ".join(patched_cves))) | ||
| 205 | } | ||
| 206 | |||
| 207 | addtask do_report_patched after do_unpack before do_build | ||
| 208 | |||
| 209 | do_report_patched[nostamp] = "1" | ||
| 210 | |||
| 211 | python() { | ||
| 212 | for b in d.getVarFlag("CVE_REPORT_MODE", "blacklist").split(","): | ||
| 213 | if bb.data.inherits_class(b, d): | ||
| 214 | bb.build.deltask("do_report_patched", d) | ||
| 215 | break | ||
| 216 | } | ||
