aboutsummaryrefslogtreecommitdiffstats
path: root/meta-security-isfafw/classes/isafw.bbclass
blob: 146acdfbfd46ab7eb71cc5dcf6a93c37d5545e15 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# Security scanning class
#
# Based in part on buildhistory.bbclass which was in turn based on
# testlab.bbclass and packagehistory.bbclass
#
# Copyright (C) 2011-2015 Intel Corporation
# Copyright (C) 2007-2011 Koen Kooi <koen@openembedded.org>
#

LICENSE = "MIT"

require conf/distro/include/distro_alias.inc

ISAFW_WORKDIR = "${WORKDIR}/isafw"
ISAFW_REPORTDIR ?= "${LOG_DIR}/isafw-report"
ISAFW_LOGDIR ?= "${LOG_DIR}/isafw-logs"

ISAFW_PLUGINS_WHITELIST ?= ""
ISAFW_PLUGINS_BLACKLIST ?= ""

ISAFW_LA_PLUGIN_IMAGE_WHITELIST ?= ""
ISAFW_LA_PLUGIN_IMAGE_BLACKLIST ?= ""

# First, code to handle scanning each recipe that goes into the build

do_analysesource[nostamp] = "1"
do_analysesource[cleandirs] = "${ISAFW_WORKDIR}"

python do_analysesource() {
    from isafw import isafw

    imageSecurityAnalyser = isafw_init(isafw, d)

    if not d.getVar('SRC_URI', True):
        # Recipe didn't fetch any sources, nothing to do here I assume?
        return

    recipe = isafw.ISA_package()
    recipe.name = d.getVar('BPN', True)
    recipe.version = d.getVar('PV', True)
    recipe.version = recipe.version.split('+git', 1)[0]

    for p in d.getVar('PACKAGES', True).split():
        license = str(d.getVar('LICENSE_' + p, True))
        if license == "None":
            license = d.getVar('LICENSE', True)
        license = license.replace("(", "")
        license = license.replace(")", "")
        licenses = license.split()
        while '|' in licenses:
            licenses.remove('|')
        while '&' in licenses:
            licenses.remove('&')
        for l in licenses:
            recipe.licenses.append(p + ":" + canonical_license(d, l))

    aliases = d.getVar('DISTRO_PN_ALIAS', True)
    if aliases:
        recipe.aliases = aliases.split()
        faliases = []
        for a in recipe.aliases:
            if (a != "OSPDT") and (not (a.startswith("upstream="))):
                faliases.append(a.split('=', 1)[-1])
        # remove possible duplicates in pkg names
        faliases = list(set(faliases))
        recipe.aliases = faliases

    for patch in src_patches(d):
        _,_,local,_,_,_=bb.fetch.decodeurl(patch)
        recipe.patch_files.append(os.path.basename(local))
    if (not recipe.patch_files) :
        recipe.patch_files.append("None")

    # Pass the recipe object to the security framework
    bb.debug(1, '%s: analyse sources' % (d.getVar('PN', True)))
    imageSecurityAnalyser.process_package(recipe)

    return
}

addtask do_analysesource before do_build

# This task intended to be called after default task to process reports

PR_ORIG_TASK := "${BB_DEFAULT_TASK}"
addhandler process_reports_handler
process_reports_handler[eventmask] = "bb.event.BuildCompleted"

python process_reports_handler() {
    from isafw import isafw

    dd = d.createCopy()
    target_sysroot = dd.expand("${STAGING_DIR}/${MACHINE}")
    native_sysroot = dd.expand("${STAGING_DIR}/${BUILD_ARCH}")
    staging_populate_sysroot_dir(target_sysroot, native_sysroot, True, dd)
 
    dd.setVar("STAGING_DIR_NATIVE", native_sysroot)
    savedenv = os.environ.copy()
    os.environ["PATH"] = dd.getVar("PATH", True)

    imageSecurityAnalyser = isafw_init(isafw, dd)
    bb.debug(1, 'isafw: process reports')
    imageSecurityAnalyser.process_report()

    os.environ["PATH"] = savedenv["PATH"]
}

do_build[depends] += "cve-update-db-native:do_populate_cve_db ca-certificates-native:do_populate_sysroot"
do_build[depends] += "python3-lxml-native:do_populate_sysroot"

# These tasks are intended to be called directly by the user (e.g. bitbake -c)

addtask do_analyse_sources after do_analysesource
do_analyse_sources[doc] = "Produce ISAFW reports based on given package without building it"
do_analyse_sources[nostamp] = "1"
do_analyse_sources() {
	:
}

addtask do_analyse_sources_all after do_analysesource
do_analyse_sources_all[doc] = "Produce ISAFW reports for all packages in given target without building them"
do_analyse_sources_all[recrdeptask] = "do_analyse_sources_all do_analysesource"
do_analyse_sources_all[recideptask] = "do_${PR_ORIG_TASK}"
do_analyse_sources_all[nostamp] = "1"
do_analyse_sources_all() {
	:
}

python() {
    # We probably don't need to scan these
    if bb.data.inherits_class('native', d) or \
       bb.data.inherits_class('nativesdk', d) or \
       bb.data.inherits_class('cross', d) or \
       bb.data.inherits_class('crosssdk', d) or \
       bb.data.inherits_class('cross-canadian', d) or \
       bb.data.inherits_class('packagegroup', d) or \
       bb.data.inherits_class('image', d):
        bb.build.deltask('do_analysesource', d)
}

fakeroot python do_analyse_image() {

    from isafw import isafw

    imageSecurityAnalyser = isafw_init(isafw, d)

    # Directory where the image's entire contents can be examined
    rootfsdir = d.getVar('IMAGE_ROOTFS', True)

    imagebasename = d.getVar('IMAGE_BASENAME', True)

    kernelconf = d.getVar('STAGING_KERNEL_BUILDDIR', True) + "/.config"
    if os.path.exists(kernelconf):
        kernel = isafw.ISA_kernel()
        kernel.img_name = imagebasename
        kernel.path_to_config = kernelconf
        bb.debug(1, 'do kernel conf analysis on %s' % kernelconf)
        imageSecurityAnalyser.process_kernel(kernel)
    else:
        bb.debug(1, 'Kernel configuration file is missing. Not performing analysis on %s' % kernelconf)

    pkglist = manifest2pkglist(d)

    imagebasename = d.getVar('IMAGE_BASENAME', True)

    if (pkglist):
        pkg_list = isafw.ISA_pkg_list()
        pkg_list.img_name = imagebasename
        pkg_list.path_to_list = pkglist
        bb.debug(1, 'do pkg list analysis on %s' % pkglist)
        imageSecurityAnalyser.process_pkg_list(pkg_list)

    fs = isafw.ISA_filesystem()
    fs.img_name = imagebasename
    fs.path_to_fs = rootfsdir

    bb.debug(1, 'do image analysis on %s' % rootfsdir)
    imageSecurityAnalyser.process_filesystem(fs)
}

do_rootfs[depends] += "checksec-native:do_populate_sysroot ca-certificates-native:do_populate_sysroot"
do_rootfs[depends] += "prelink-native:do_populate_sysroot"
do_rootfs[depends] += "python3-lxml-native:do_populate_sysroot"

isafw_init[vardepsexclude] = "DATETIME"
def isafw_init(isafw, d):
    import re, errno

    isafw_config = isafw.ISA_config()
    # Override the builtin default in curl-native (used by cve-update-db-nativ)
    # because that default is a path that may not be valid: when curl-native gets
    # installed from sstate, we end up with the sysroot path as it was on the
    # original build host, which is not necessarily the same path used now
    # (see https://bugzilla.yoctoproject.org/show_bug.cgi?id=9883).
    #
    # Can't use ${sysconfdir} here, it already includes ${STAGING_DIR_NATIVE}
    # when the current recipe is native.
    isafw_config.cacert = d.expand('${STAGING_DIR_NATIVE}/etc/ssl/certs/ca-certificates.crt')

    bb.utils.export_proxies(d)

    isafw_config.machine = d.getVar('MACHINE', True)
    isafw_config.timestamp = d.getVar('DATETIME', True)
    isafw_config.reportdir = d.getVar('ISAFW_REPORTDIR', True) + "_" + isafw_config.timestamp
    if not os.path.exists(os.path.dirname(isafw_config.reportdir + "/test")):
        try:
            os.makedirs(os.path.dirname(isafw_config.reportdir + "/test"))
        except OSError as exc:
            if exc.errno == errno.EEXIST and os.path.isdir(isafw_config.reportdir):
                pass
            else: raise
    isafw_config.logdir = d.getVar('ISAFW_LOGDIR', True)
    # Adding support for arm
    # TODO: Add support for other platforms
    isafw_config.arch =  d.getVar('TARGET_ARCH', True)
    if ( isafw_config.arch != "arm" ):
        isafw_config.arch = "x86"

    whitelist = d.getVar('ISAFW_PLUGINS_WHITELIST', True)
    blacklist = d.getVar('ISAFW_PLUGINS_BLACKLIST', True)
    if whitelist:
        isafw_config.plugin_whitelist = re.split(r'[,\s]*', whitelist)
    if blacklist:
        isafw_config.plugin_blacklist = re.split(r'[,\s]*', blacklist)

    la_image_whitelist = d.getVar('ISAFW_LA_PLUGIN_IMAGE_WHITELIST', True)
    la_image_blacklist = d.getVar('ISAFW_LA_PLUGIN_IMAGE_BLACKLIST', True)
    if la_image_whitelist:
        isafw_config.la_plugin_image_whitelist = re.split(r'[,\s]*', la_image_whitelist)
    if la_image_blacklist:
        isafw_config.la_plugin_image_blacklist = re.split(r'[,\s]*', la_image_blacklist)

    return isafw.ISA(isafw_config)

# based on toaster.bbclass _toaster_load_pkgdatafile function
def binary2source(dirpath, filepath):
    import re
    originPkg = ""
    with open(os.path.join(dirpath, filepath), "r") as fin:
        for line in fin:
            try:
                kn, kv = line.strip().split(": ", 1)
                m = re.match(r"^PKG_([^A-Z:]*)", kn)
                if m:
                    originPkg = str(m.group(1))
            except ValueError:
                pass    # ignore lines without valid key: value pairs:
    if not originPkg:
        originPkg = "UNKNOWN"
    return originPkg

manifest2pkglist[vardepsexclude] = "DATETIME"
def manifest2pkglist(d):
    import glob

    manifest_file = d.getVar('IMAGE_MANIFEST', True)
    imagebasename = d.getVar('IMAGE_BASENAME', True)
    reportdir = d.getVar('ISAFW_REPORTDIR', True) + "_" + d.getVar('DATETIME', True)
    pkgdata_dir = d.getVar("PKGDATA_DIR", True)
    rr_dir = "%s/runtime-reverse/" % pkgdata_dir
    pkglist = reportdir + "/pkglist"

    with open(pkglist, 'a') as foutput:
        foutput.write("Packages for image " + imagebasename + "\n")
        try:
            with open(manifest_file, 'r') as finput:
                for line in finput:
                    items = line.split()
                    if items and (len(items) >= 3):
                        pkgnames = map(os.path.basename, glob.glob(os.path.join(rr_dir, items[0])))
                        for pkgname in pkgnames:
                            originPkg = binary2source(rr_dir, pkgname)
                            version = items[2]
                            if not version:
                                version = "undetermined"
                            foutput.write(pkgname + " " + version + " " + originPkg + "\n")
        except IOError:
            bb.debug(1, 'isafw: manifest file not found. Skip pkg list analysis')
            return "";


    return pkglist

# NOTE: by the time IMAGE_POSTPROCESS_COMMAND items are called, the image
# has been stripped of the package manager database (if runtime package management
# is not enabled, i.e. 'package-management' is not in IMAGE_FEATURES). If you
# do want to be using the package manager to operate on the image contents, you'll
# need to call your function from ROOTFS_POSTINSTALL_COMMAND or
# ROOTFS_POSTUNINSTALL_COMMAND instead - however if you do that you should then be
# aware that what you'll be looking at isn't exactly what you will see in the image
# at runtime (there will be other postprocessing functions called after yours).
#
# do_analyse_image does not need the package manager database. Making it
# a separate task instead of a IMAGE_POSTPROCESS_COMMAND has several
# advantages:
# - all other image commands are guaranteed to have completed
# - it can run in parallel to other tasks which depend on the complete
#   image, instead of blocking those other tasks
# - meta-swupd helper images do not need to be analysed and won't be
#   because nothing depends on their "do_build" task, only on
#   do_image_complete
python () {
    if bb.data.inherits_class('image', d):
        bb.build.addtask('do_analyse_image', 'do_build', 'do_image_complete', d)
}

python isafwreport_handler () {

    import shutil

    logdir = e.data.getVar('ISAFW_LOGDIR', True)
    if os.path.exists(os.path.dirname(logdir+"/test")):
        shutil.rmtree(logdir)
    os.makedirs(os.path.dirname(logdir+"/test"))

}
addhandler isafwreport_handler
isafwreport_handler[eventmask] = "bb.event.BuildStarted"