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 | } | ||