diff options
author | Marta Rybczynska <rybczynska@gmail.com> | 2022-03-29 14:54:32 +0200 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2022-03-30 13:07:41 +0100 |
commit | b3f96e6feab7ef0b2797214631122efefe29dbe8 (patch) | |
tree | f76223fe113c38fed28cf8dbe06d3c0632018ea3 /meta | |
parent | 777f1d42b62ab482efa5a24600f4aeba1b156c64 (diff) | |
download | poky-b3f96e6feab7ef0b2797214631122efefe29dbe8.tar.gz |
cve-check: add coverage statistics on recipes with/without CVEs
Until now the CVE checker was giving information about CVEs found for
a product (or more products) contained in a recipe. However, there was
no easy way to find out which products or recipes have no CVEs. Having
no reported CVEs might mean there are simply none, but can also mean
a product name (CPE) mismatch.
This patch adds CVE_CHECK_COVERAGE option enabling a new type of
statistics. Then we use the new JSON format to report the information.
The legacy text mode report does not contain it.
This option is expected to help with an identification of recipes with
mismatched CPEs, issues in the database and more.
This work is based on [1], but adding the JSON format makes it easier
to implement, without additional result files.
[1] https://lists.openembedded.org/g/openembedded-core/message/159873
(From OE-Core rev: d1849a1facd64fa0bcf8336a0ed5fbf71b2e3cb5)
Signed-off-by: Marta Rybczynska <marta.rybczynska@huawei.com>
Signed-off-by: Alexandre Belloni <alexandre.belloni@bootlin.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta')
-rw-r--r-- | meta/classes/cve-check.bbclass | 51 |
1 files changed, 40 insertions, 11 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass index f574f5daa4..78516d0bb6 100644 --- a/meta/classes/cve-check.bbclass +++ b/meta/classes/cve-check.bbclass | |||
@@ -55,6 +55,9 @@ CVE_CHECK_FORMAT_TEXT ??= "1" | |||
55 | # Provide JSON output | 55 | # Provide JSON output |
56 | CVE_CHECK_FORMAT_JSON ??= "1" | 56 | CVE_CHECK_FORMAT_JSON ??= "1" |
57 | 57 | ||
58 | # Check for packages without CVEs (no issues or missing product name) | ||
59 | CVE_CHECK_COVERAGE ??= "1" | ||
60 | |||
58 | # Skip CVE Check for packages (PN) | 61 | # Skip CVE Check for packages (PN) |
59 | CVE_CHECK_SKIP_RECIPE ?= "" | 62 | CVE_CHECK_SKIP_RECIPE ?= "" |
60 | 63 | ||
@@ -114,10 +117,10 @@ python do_cve_check () { | |||
114 | patched_cves = get_patched_cves(d) | 117 | patched_cves = get_patched_cves(d) |
115 | except FileNotFoundError: | 118 | except FileNotFoundError: |
116 | bb.fatal("Failure in searching patches") | 119 | bb.fatal("Failure in searching patches") |
117 | ignored, patched, unpatched = check_cves(d, patched_cves) | 120 | ignored, patched, unpatched, status = check_cves(d, patched_cves) |
118 | if patched or unpatched: | 121 | if patched or unpatched or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status): |
119 | cve_data = get_cve_info(d, patched + unpatched) | 122 | cve_data = get_cve_info(d, patched + unpatched) |
120 | cve_write_data(d, patched, unpatched, ignored, cve_data) | 123 | cve_write_data(d, patched, unpatched, ignored, cve_data, status) |
121 | else: | 124 | else: |
122 | bb.note("No CVE database found, skipping CVE check") | 125 | bb.note("No CVE database found, skipping CVE check") |
123 | 126 | ||
@@ -207,17 +210,19 @@ def check_cves(d, patched_cves): | |||
207 | suffix = d.getVar("CVE_VERSION_SUFFIX") | 210 | suffix = d.getVar("CVE_VERSION_SUFFIX") |
208 | 211 | ||
209 | cves_unpatched = [] | 212 | cves_unpatched = [] |
213 | cves_status = [] | ||
214 | cves_in_recipe = False | ||
210 | # CVE_PRODUCT can contain more than one product (eg. curl/libcurl) | 215 | # CVE_PRODUCT can contain more than one product (eg. curl/libcurl) |
211 | products = d.getVar("CVE_PRODUCT").split() | 216 | products = d.getVar("CVE_PRODUCT").split() |
212 | # If this has been unset then we're not scanning for CVEs here (for example, image recipes) | 217 | # If this has been unset then we're not scanning for CVEs here (for example, image recipes) |
213 | if not products: | 218 | if not products: |
214 | return ([], [], []) | 219 | return ([], [], [], {}) |
215 | pv = d.getVar("CVE_VERSION").split("+git")[0] | 220 | pv = d.getVar("CVE_VERSION").split("+git")[0] |
216 | 221 | ||
217 | # If the recipe has been skipped/ignored we return empty lists | 222 | # If the recipe has been skipped/ignored we return empty lists |
218 | if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split(): | 223 | if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split(): |
219 | bb.note("Recipe has been skipped by cve-check") | 224 | bb.note("Recipe has been skipped by cve-check") |
220 | return ([], [], []) | 225 | return ([], [], [], []) |
221 | 226 | ||
222 | cve_ignore = d.getVar("CVE_CHECK_IGNORE").split() | 227 | cve_ignore = d.getVar("CVE_CHECK_IGNORE").split() |
223 | 228 | ||
@@ -227,6 +232,7 @@ def check_cves(d, patched_cves): | |||
227 | 232 | ||
228 | # For each of the known product names (e.g. curl has CPEs using curl and libcurl)... | 233 | # For each of the known product names (e.g. curl has CPEs using curl and libcurl)... |
229 | for product in products: | 234 | for product in products: |
235 | cves_in_product = False | ||
230 | if ":" in product: | 236 | if ":" in product: |
231 | vendor, product = product.split(":", 1) | 237 | vendor, product = product.split(":", 1) |
232 | else: | 238 | else: |
@@ -244,6 +250,11 @@ def check_cves(d, patched_cves): | |||
244 | elif cve in patched_cves: | 250 | elif cve in patched_cves: |
245 | bb.note("%s has been patched" % (cve)) | 251 | bb.note("%s has been patched" % (cve)) |
246 | continue | 252 | continue |
253 | # Write status once only for each product | ||
254 | if not cves_in_product: | ||
255 | cves_status.append([product, True]) | ||
256 | cves_in_product = True | ||
257 | cves_in_recipe = True | ||
247 | 258 | ||
248 | vulnerable = False | 259 | vulnerable = False |
249 | for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)): | 260 | for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)): |
@@ -290,9 +301,16 @@ def check_cves(d, patched_cves): | |||
290 | # TODO: not patched but not vulnerable | 301 | # TODO: not patched but not vulnerable |
291 | patched_cves.add(cve) | 302 | patched_cves.add(cve) |
292 | 303 | ||
304 | if not cves_in_product: | ||
305 | bb.note("No CVE records found for product %s, pn %s" % (product, pn)) | ||
306 | cves_status.append([product, False]) | ||
307 | |||
293 | conn.close() | 308 | conn.close() |
294 | 309 | ||
295 | return (list(cve_ignore), list(patched_cves), cves_unpatched) | 310 | if not cves_in_recipe: |
311 | bb.note("No CVE records for products in recipe %s" % (pn)) | ||
312 | |||
313 | return (list(cve_ignore), list(patched_cves), cves_unpatched, cves_status) | ||
296 | 314 | ||
297 | def get_cve_info(d, cves): | 315 | def get_cve_info(d, cves): |
298 | """ | 316 | """ |
@@ -323,7 +341,6 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data): | |||
323 | CVE manifest if enabled. | 341 | CVE manifest if enabled. |
324 | """ | 342 | """ |
325 | 343 | ||
326 | |||
327 | cve_file = d.getVar("CVE_CHECK_LOG") | 344 | cve_file = d.getVar("CVE_CHECK_LOG") |
328 | fdir_name = d.getVar("FILE_DIRNAME") | 345 | fdir_name = d.getVar("FILE_DIRNAME") |
329 | layer = fdir_name.split("/")[-3] | 346 | layer = fdir_name.split("/")[-3] |
@@ -337,6 +354,10 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data): | |||
337 | if include_layers and layer not in include_layers: | 354 | if include_layers and layer not in include_layers: |
338 | return | 355 | return |
339 | 356 | ||
357 | # Early exit, the text format does not report packages without CVEs | ||
358 | if not patched+unpatched: | ||
359 | return | ||
360 | |||
340 | nvd_link = "https://nvd.nist.gov/vuln/detail/" | 361 | nvd_link = "https://nvd.nist.gov/vuln/detail/" |
341 | write_string = "" | 362 | write_string = "" |
342 | unpatched_cves = [] | 363 | unpatched_cves = [] |
@@ -414,7 +435,7 @@ def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_fi | |||
414 | with open(index_path, "a+") as f: | 435 | with open(index_path, "a+") as f: |
415 | f.write("%s\n" % fragment_path) | 436 | f.write("%s\n" % fragment_path) |
416 | 437 | ||
417 | def cve_write_data_json(d, patched, unpatched, ignored, cve_data): | 438 | def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status): |
418 | """ | 439 | """ |
419 | Prepare CVE data for the JSON format, then write it. | 440 | Prepare CVE data for the JSON format, then write it. |
420 | """ | 441 | """ |
@@ -436,11 +457,19 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data): | |||
436 | 457 | ||
437 | unpatched_cves = [] | 458 | unpatched_cves = [] |
438 | 459 | ||
460 | product_data = [] | ||
461 | for s in cve_status: | ||
462 | p = {"product": s[0], "cvesInRecord": "Yes"} | ||
463 | if s[1] == False: | ||
464 | p["cvesInRecord"] = "No" | ||
465 | product_data.append(p) | ||
466 | |||
439 | package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV")) | 467 | package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV")) |
440 | package_data = { | 468 | package_data = { |
441 | "name" : d.getVar("PN"), | 469 | "name" : d.getVar("PN"), |
442 | "layer" : layer, | 470 | "layer" : layer, |
443 | "version" : package_version | 471 | "version" : package_version, |
472 | "products": product_data | ||
444 | } | 473 | } |
445 | cve_list = [] | 474 | cve_list = [] |
446 | 475 | ||
@@ -479,7 +508,7 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data): | |||
479 | 508 | ||
480 | cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file) | 509 | cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file) |
481 | 510 | ||
482 | def cve_write_data(d, patched, unpatched, ignored, cve_data): | 511 | def cve_write_data(d, patched, unpatched, ignored, cve_data, status): |
483 | """ | 512 | """ |
484 | Write CVE data in each enabled format. | 513 | Write CVE data in each enabled format. |
485 | """ | 514 | """ |
@@ -487,4 +516,4 @@ def cve_write_data(d, patched, unpatched, ignored, cve_data): | |||
487 | if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1": | 516 | if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1": |
488 | cve_write_data_text(d, patched, unpatched, ignored, cve_data) | 517 | cve_write_data_text(d, patched, unpatched, ignored, cve_data) |
489 | if d.getVar("CVE_CHECK_FORMAT_JSON") == "1": | 518 | if d.getVar("CVE_CHECK_FORMAT_JSON") == "1": |
490 | cve_write_data_json(d, patched, unpatched, ignored, cve_data) | 519 | cve_write_data_json(d, patched, unpatched, ignored, cve_data, status) |