Andrew Geissler | 1fe918a | 2020-05-15 14:16:47 -0500 | [diff] [blame] | 1 | # 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 | |
| 10 | LICENSE = "MIT" |
| 11 | |
| 12 | require conf/distro/include/distro_alias.inc |
| 13 | |
| 14 | ISAFW_WORKDIR = "${WORKDIR}/isafw" |
| 15 | ISAFW_REPORTDIR ?= "${LOG_DIR}/isafw-report" |
| 16 | ISAFW_LOGDIR ?= "${LOG_DIR}/isafw-logs" |
| 17 | |
| 18 | ISAFW_PLUGINS_WHITELIST ?= "" |
| 19 | ISAFW_PLUGINS_BLACKLIST ?= "" |
| 20 | |
| 21 | ISAFW_LA_PLUGIN_IMAGE_WHITELIST ?= "" |
| 22 | ISAFW_LA_PLUGIN_IMAGE_BLACKLIST ?= "" |
| 23 | |
| 24 | # First, code to handle scanning each recipe that goes into the build |
| 25 | |
| 26 | do_analysesource[nostamp] = "1" |
| 27 | do_analysesource[cleandirs] = "${ISAFW_WORKDIR}" |
| 28 | |
| 29 | python 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 Williams | 213cb26 | 2021-08-07 19:21:33 -0500 | [diff] [blame] | 44 | license = str(d.getVar('LICENSE:' + p, True)) |
Andrew Geissler | 1fe918a | 2020-05-15 14:16:47 -0500 | [diff] [blame] | 45 | 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 | |
| 81 | addtask do_analysesource before do_build |
| 82 | |
| 83 | # This task intended to be called after default task to process reports |
| 84 | |
| 85 | PR_ORIG_TASK := "${BB_DEFAULT_TASK}" |
| 86 | addhandler process_reports_handler |
| 87 | process_reports_handler[eventmask] = "bb.event.BuildCompleted" |
| 88 | |
| 89 | python 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 Geissler | 9aee500 | 2022-03-30 16:27:02 +0000 | [diff] [blame^] | 108 | do_build[depends] += "cve-update-db-native:do_fetch ca-certificates-native:do_populate_sysroot" |
Andrew Geissler | 1fe918a | 2020-05-15 14:16:47 -0500 | [diff] [blame] | 109 | do_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 | |
| 113 | addtask do_analyse_sources after do_analysesource |
| 114 | do_analyse_sources[doc] = "Produce ISAFW reports based on given package without building it" |
| 115 | do_analyse_sources[nostamp] = "1" |
| 116 | do_analyse_sources() { |
| 117 | : |
| 118 | } |
| 119 | |
| 120 | addtask do_analyse_sources_all after do_analysesource |
| 121 | do_analyse_sources_all[doc] = "Produce ISAFW reports for all packages in given target without building them" |
| 122 | do_analyse_sources_all[recrdeptask] = "do_analyse_sources_all do_analysesource" |
| 123 | do_analyse_sources_all[recideptask] = "do_${PR_ORIG_TASK}" |
| 124 | do_analyse_sources_all[nostamp] = "1" |
| 125 | do_analyse_sources_all() { |
| 126 | : |
| 127 | } |
| 128 | |
| 129 | python() { |
| 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 | |
| 141 | fakeroot 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 | |
| 181 | do_rootfs[depends] += "checksec-native:do_populate_sysroot ca-certificates-native:do_populate_sysroot" |
Andrew Geissler | 1fe918a | 2020-05-15 14:16:47 -0500 | [diff] [blame] | 182 | do_rootfs[depends] += "python3-lxml-native:do_populate_sysroot" |
| 183 | |
| 184 | isafw_init[vardepsexclude] = "DATETIME" |
| 185 | def 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 |
| 235 | def 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 | |
| 251 | manifest2pkglist[vardepsexclude] = "DATETIME" |
| 252 | def 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 |
| 301 | python () { |
| 302 | if bb.data.inherits_class('image', d): |
| 303 | bb.build.addtask('do_analyse_image', 'do_build', 'do_image_complete', d) |
| 304 | } |
| 305 | |
| 306 | python 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 | } |
| 316 | addhandler isafwreport_handler |
| 317 | isafwreport_handler[eventmask] = "bb.event.BuildStarted" |