blob: 3854c0fcafd07644ee4c07161eb0e1be452bad92 [file] [log] [blame]
Andrew Geissler1fe918a2020-05-15 14:16:47 -05001# Security scanning class
2#
3# Based in part on buildhistory.bbclass which was in turn based on
4# testlab.bbclass and packagehistory.bbclass
5#
6# Copyright (C) 2011-2015 Intel Corporation
7# Copyright (C) 2007-2011 Koen Kooi <koen@openembedded.org>
8#
9
10LICENSE = "MIT"
11
12require conf/distro/include/distro_alias.inc
13
14ISAFW_WORKDIR = "${WORKDIR}/isafw"
15ISAFW_REPORTDIR ?= "${LOG_DIR}/isafw-report"
16ISAFW_LOGDIR ?= "${LOG_DIR}/isafw-logs"
17
18ISAFW_PLUGINS_WHITELIST ?= ""
19ISAFW_PLUGINS_BLACKLIST ?= ""
20
21ISAFW_LA_PLUGIN_IMAGE_WHITELIST ?= ""
22ISAFW_LA_PLUGIN_IMAGE_BLACKLIST ?= ""
23
24# First, code to handle scanning each recipe that goes into the build
25
26do_analysesource[nostamp] = "1"
27do_analysesource[cleandirs] = "${ISAFW_WORKDIR}"
28
29python do_analysesource() {
30 from isafw import isafw
31
32 imageSecurityAnalyser = isafw_init(isafw, d)
33
34 if not d.getVar('SRC_URI', True):
35 # Recipe didn't fetch any sources, nothing to do here I assume?
36 return
37
38 recipe = isafw.ISA_package()
39 recipe.name = d.getVar('BPN', True)
40 recipe.version = d.getVar('PV', True)
41 recipe.version = recipe.version.split('+git', 1)[0]
42
43 for p in d.getVar('PACKAGES', True).split():
Patrick Williams213cb262021-08-07 19:21:33 -050044 license = str(d.getVar('LICENSE:' + p, True))
Andrew Geissler1fe918a2020-05-15 14:16:47 -050045 if license == "None":
46 license = d.getVar('LICENSE', True)
47 license = license.replace("(", "")
48 license = license.replace(")", "")
49 licenses = license.split()
50 while '|' in licenses:
51 licenses.remove('|')
52 while '&' in licenses:
53 licenses.remove('&')
54 for l in licenses:
55 recipe.licenses.append(p + ":" + canonical_license(d, l))
56
57 aliases = d.getVar('DISTRO_PN_ALIAS', True)
58 if aliases:
59 recipe.aliases = aliases.split()
60 faliases = []
61 for a in recipe.aliases:
62 if (a != "OSPDT") and (not (a.startswith("upstream="))):
63 faliases.append(a.split('=', 1)[-1])
64 # remove possible duplicates in pkg names
65 faliases = list(set(faliases))
66 recipe.aliases = faliases
67
68 for patch in src_patches(d):
69 _,_,local,_,_,_=bb.fetch.decodeurl(patch)
70 recipe.patch_files.append(os.path.basename(local))
71 if (not recipe.patch_files) :
72 recipe.patch_files.append("None")
73
74 # Pass the recipe object to the security framework
75 bb.debug(1, '%s: analyse sources' % (d.getVar('PN', True)))
76 imageSecurityAnalyser.process_package(recipe)
77
78 return
79}
80
81addtask do_analysesource before do_build
82
83# This task intended to be called after default task to process reports
84
85PR_ORIG_TASK := "${BB_DEFAULT_TASK}"
86addhandler process_reports_handler
87process_reports_handler[eventmask] = "bb.event.BuildCompleted"
88
89python process_reports_handler() {
90 from isafw import isafw
91
92 dd = d.createCopy()
93 target_sysroot = dd.expand("${STAGING_DIR}/${MACHINE}")
94 native_sysroot = dd.expand("${STAGING_DIR}/${BUILD_ARCH}")
95 staging_populate_sysroot_dir(target_sysroot, native_sysroot, True, dd)
96
97 dd.setVar("STAGING_DIR_NATIVE", native_sysroot)
98 savedenv = os.environ.copy()
99 os.environ["PATH"] = dd.getVar("PATH", True)
100
101 imageSecurityAnalyser = isafw_init(isafw, dd)
102 bb.debug(1, 'isafw: process reports')
103 imageSecurityAnalyser.process_report()
104
105 os.environ["PATH"] = savedenv["PATH"]
106}
107
Andrew Geissler9aee5002022-03-30 16:27:02 +0000108do_build[depends] += "cve-update-db-native:do_fetch ca-certificates-native:do_populate_sysroot"
Andrew Geissler1fe918a2020-05-15 14:16:47 -0500109do_build[depends] += "python3-lxml-native:do_populate_sysroot"
110
111# These tasks are intended to be called directly by the user (e.g. bitbake -c)
112
113addtask do_analyse_sources after do_analysesource
114do_analyse_sources[doc] = "Produce ISAFW reports based on given package without building it"
115do_analyse_sources[nostamp] = "1"
116do_analyse_sources() {
117 :
118}
119
120addtask do_analyse_sources_all after do_analysesource
121do_analyse_sources_all[doc] = "Produce ISAFW reports for all packages in given target without building them"
122do_analyse_sources_all[recrdeptask] = "do_analyse_sources_all do_analysesource"
123do_analyse_sources_all[recideptask] = "do_${PR_ORIG_TASK}"
124do_analyse_sources_all[nostamp] = "1"
125do_analyse_sources_all() {
126 :
127}
128
129python() {
130 # We probably don't need to scan these
131 if bb.data.inherits_class('native', d) or \
132 bb.data.inherits_class('nativesdk', d) or \
133 bb.data.inherits_class('cross', d) or \
134 bb.data.inherits_class('crosssdk', d) or \
135 bb.data.inherits_class('cross-canadian', d) or \
136 bb.data.inherits_class('packagegroup', d) or \
137 bb.data.inherits_class('image', d):
138 bb.build.deltask('do_analysesource', d)
139}
140
141fakeroot python do_analyse_image() {
142
143 from isafw import isafw
144
145 imageSecurityAnalyser = isafw_init(isafw, d)
146
147 # Directory where the image's entire contents can be examined
148 rootfsdir = d.getVar('IMAGE_ROOTFS', True)
149
150 imagebasename = d.getVar('IMAGE_BASENAME', True)
151
152 kernelconf = d.getVar('STAGING_KERNEL_BUILDDIR', True) + "/.config"
153 if os.path.exists(kernelconf):
154 kernel = isafw.ISA_kernel()
155 kernel.img_name = imagebasename
156 kernel.path_to_config = kernelconf
157 bb.debug(1, 'do kernel conf analysis on %s' % kernelconf)
158 imageSecurityAnalyser.process_kernel(kernel)
159 else:
160 bb.debug(1, 'Kernel configuration file is missing. Not performing analysis on %s' % kernelconf)
161
162 pkglist = manifest2pkglist(d)
163
164 imagebasename = d.getVar('IMAGE_BASENAME', True)
165
166 if (pkglist):
167 pkg_list = isafw.ISA_pkg_list()
168 pkg_list.img_name = imagebasename
169 pkg_list.path_to_list = pkglist
170 bb.debug(1, 'do pkg list analysis on %s' % pkglist)
171 imageSecurityAnalyser.process_pkg_list(pkg_list)
172
173 fs = isafw.ISA_filesystem()
174 fs.img_name = imagebasename
175 fs.path_to_fs = rootfsdir
176
177 bb.debug(1, 'do image analysis on %s' % rootfsdir)
178 imageSecurityAnalyser.process_filesystem(fs)
179}
180
181do_rootfs[depends] += "checksec-native:do_populate_sysroot ca-certificates-native:do_populate_sysroot"
Andrew Geissler1fe918a2020-05-15 14:16:47 -0500182do_rootfs[depends] += "python3-lxml-native:do_populate_sysroot"
183
184isafw_init[vardepsexclude] = "DATETIME"
185def isafw_init(isafw, d):
186 import re, errno
187
188 isafw_config = isafw.ISA_config()
189 # Override the builtin default in curl-native (used by cve-update-db-nativ)
190 # because that default is a path that may not be valid: when curl-native gets
191 # installed from sstate, we end up with the sysroot path as it was on the
192 # original build host, which is not necessarily the same path used now
193 # (see https://bugzilla.yoctoproject.org/show_bug.cgi?id=9883).
194 #
195 # Can't use ${sysconfdir} here, it already includes ${STAGING_DIR_NATIVE}
196 # when the current recipe is native.
197 isafw_config.cacert = d.expand('${STAGING_DIR_NATIVE}/etc/ssl/certs/ca-certificates.crt')
198
199 bb.utils.export_proxies(d)
200
201 isafw_config.machine = d.getVar('MACHINE', True)
202 isafw_config.timestamp = d.getVar('DATETIME', True)
203 isafw_config.reportdir = d.getVar('ISAFW_REPORTDIR', True) + "_" + isafw_config.timestamp
204 if not os.path.exists(os.path.dirname(isafw_config.reportdir + "/test")):
205 try:
206 os.makedirs(os.path.dirname(isafw_config.reportdir + "/test"))
207 except OSError as exc:
208 if exc.errno == errno.EEXIST and os.path.isdir(isafw_config.reportdir):
209 pass
210 else: raise
211 isafw_config.logdir = d.getVar('ISAFW_LOGDIR', True)
212 # Adding support for arm
213 # TODO: Add support for other platforms
214 isafw_config.arch = d.getVar('TARGET_ARCH', True)
215 if ( isafw_config.arch != "arm" ):
216 isafw_config.arch = "x86"
217
218 whitelist = d.getVar('ISAFW_PLUGINS_WHITELIST', True)
219 blacklist = d.getVar('ISAFW_PLUGINS_BLACKLIST', True)
220 if whitelist:
221 isafw_config.plugin_whitelist = re.split(r'[,\s]*', whitelist)
222 if blacklist:
223 isafw_config.plugin_blacklist = re.split(r'[,\s]*', blacklist)
224
225 la_image_whitelist = d.getVar('ISAFW_LA_PLUGIN_IMAGE_WHITELIST', True)
226 la_image_blacklist = d.getVar('ISAFW_LA_PLUGIN_IMAGE_BLACKLIST', True)
227 if la_image_whitelist:
228 isafw_config.la_plugin_image_whitelist = re.split(r'[,\s]*', la_image_whitelist)
229 if la_image_blacklist:
230 isafw_config.la_plugin_image_blacklist = re.split(r'[,\s]*', la_image_blacklist)
231
232 return isafw.ISA(isafw_config)
233
234# based on toaster.bbclass _toaster_load_pkgdatafile function
235def binary2source(dirpath, filepath):
236 import re
237 originPkg = ""
238 with open(os.path.join(dirpath, filepath), "r") as fin:
239 for line in fin:
240 try:
241 kn, kv = line.strip().split(": ", 1)
242 m = re.match(r"^PKG_([^A-Z:]*)", kn)
243 if m:
244 originPkg = str(m.group(1))
245 except ValueError:
246 pass # ignore lines without valid key: value pairs:
247 if not originPkg:
248 originPkg = "UNKNOWN"
249 return originPkg
250
251manifest2pkglist[vardepsexclude] = "DATETIME"
252def manifest2pkglist(d):
253 import glob
254
255 manifest_file = d.getVar('IMAGE_MANIFEST', True)
256 imagebasename = d.getVar('IMAGE_BASENAME', True)
257 reportdir = d.getVar('ISAFW_REPORTDIR', True) + "_" + d.getVar('DATETIME', True)
258 pkgdata_dir = d.getVar("PKGDATA_DIR", True)
259 rr_dir = "%s/runtime-reverse/" % pkgdata_dir
260 pkglist = reportdir + "/pkglist"
261
262 with open(pkglist, 'a') as foutput:
263 foutput.write("Packages for image " + imagebasename + "\n")
264 try:
265 with open(manifest_file, 'r') as finput:
266 for line in finput:
267 items = line.split()
268 if items and (len(items) >= 3):
269 pkgnames = map(os.path.basename, glob.glob(os.path.join(rr_dir, items[0])))
270 for pkgname in pkgnames:
271 originPkg = binary2source(rr_dir, pkgname)
272 version = items[2]
273 if not version:
274 version = "undetermined"
275 foutput.write(pkgname + " " + version + " " + originPkg + "\n")
276 except IOError:
277 bb.debug(1, 'isafw: manifest file not found. Skip pkg list analysis')
278 return "";
279
280
281 return pkglist
282
283# NOTE: by the time IMAGE_POSTPROCESS_COMMAND items are called, the image
284# has been stripped of the package manager database (if runtime package management
285# is not enabled, i.e. 'package-management' is not in IMAGE_FEATURES). If you
286# do want to be using the package manager to operate on the image contents, you'll
287# need to call your function from ROOTFS_POSTINSTALL_COMMAND or
288# ROOTFS_POSTUNINSTALL_COMMAND instead - however if you do that you should then be
289# aware that what you'll be looking at isn't exactly what you will see in the image
290# at runtime (there will be other postprocessing functions called after yours).
291#
292# do_analyse_image does not need the package manager database. Making it
293# a separate task instead of a IMAGE_POSTPROCESS_COMMAND has several
294# advantages:
295# - all other image commands are guaranteed to have completed
296# - it can run in parallel to other tasks which depend on the complete
297# image, instead of blocking those other tasks
298# - meta-swupd helper images do not need to be analysed and won't be
299# because nothing depends on their "do_build" task, only on
300# do_image_complete
301python () {
302 if bb.data.inherits_class('image', d):
303 bb.build.addtask('do_analyse_image', 'do_build', 'do_image_complete', d)
304}
305
306python isafwreport_handler () {
307
308 import shutil
309
310 logdir = e.data.getVar('ISAFW_LOGDIR', True)
311 if os.path.exists(os.path.dirname(logdir+"/test")):
312 shutil.rmtree(logdir)
313 os.makedirs(os.path.dirname(logdir+"/test"))
314
315}
316addhandler isafwreport_handler
317isafwreport_handler[eventmask] = "bb.event.BuildStarted"