1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun 6*4882a593Smuzhiyunimport sys, os, subprocess, re, shutil 7*4882a593Smuzhiyun 8*4882a593Smuzhiyunallowed = ( 9*4882a593Smuzhiyun # type is supported by dash 10*4882a593Smuzhiyun 'if type systemctl >/dev/null 2>/dev/null; then', 11*4882a593Smuzhiyun 'if type systemd-tmpfiles >/dev/null 2>/dev/null; then', 12*4882a593Smuzhiyun 'type update-rc.d >/dev/null 2>/dev/null; then', 13*4882a593Smuzhiyun 'command -v', 14*4882a593Smuzhiyun # HOSTNAME is set locally 15*4882a593Smuzhiyun 'buildhistory_single_commit "$CMDLINE" "$HOSTNAME"', 16*4882a593Smuzhiyun # False-positive, match is a grep not shell expression 17*4882a593Smuzhiyun 'grep "^$groupname:[^:]*:[^:]*:\\([^,]*,\\)*$username\\(,[^,]*\\)*"', 18*4882a593Smuzhiyun # TODO verify dash's '. script args' behaviour 19*4882a593Smuzhiyun '. $target_sdk_dir/${oe_init_build_env_path} $target_sdk_dir >> $LOGFILE' 20*4882a593Smuzhiyun ) 21*4882a593Smuzhiyun 22*4882a593Smuzhiyundef is_allowed(s): 23*4882a593Smuzhiyun for w in allowed: 24*4882a593Smuzhiyun if w in s: 25*4882a593Smuzhiyun return True 26*4882a593Smuzhiyun return False 27*4882a593Smuzhiyun 28*4882a593SmuzhiyunSCRIPT_LINENO_RE = re.compile(r' line (\d+) ') 29*4882a593SmuzhiyunBASHISM_WARNING = re.compile(r'^(possible bashism in.*)$', re.MULTILINE) 30*4882a593Smuzhiyun 31*4882a593Smuzhiyundef process(filename, function, lineno, script): 32*4882a593Smuzhiyun import tempfile 33*4882a593Smuzhiyun 34*4882a593Smuzhiyun if not script.startswith("#!"): 35*4882a593Smuzhiyun script = "#! /bin/sh\n" + script 36*4882a593Smuzhiyun 37*4882a593Smuzhiyun fn = tempfile.NamedTemporaryFile(mode="w+t") 38*4882a593Smuzhiyun fn.write(script) 39*4882a593Smuzhiyun fn.flush() 40*4882a593Smuzhiyun 41*4882a593Smuzhiyun try: 42*4882a593Smuzhiyun subprocess.check_output(("checkbashisms.pl", fn.name), universal_newlines=True, stderr=subprocess.STDOUT) 43*4882a593Smuzhiyun # No bashisms, so just return 44*4882a593Smuzhiyun return 45*4882a593Smuzhiyun except subprocess.CalledProcessError as e: 46*4882a593Smuzhiyun # TODO check exit code is 1 47*4882a593Smuzhiyun 48*4882a593Smuzhiyun # Replace the temporary filename with the function and split it 49*4882a593Smuzhiyun output = e.output.replace(fn.name, function) 50*4882a593Smuzhiyun if not output or not output.startswith('possible bashism'): 51*4882a593Smuzhiyun # Probably starts with or contains only warnings. Dump verbatim 52*4882a593Smuzhiyun # with one space indention. Can't do the splitting and allowed 53*4882a593Smuzhiyun # checking below. 54*4882a593Smuzhiyun return '\n'.join([filename, 55*4882a593Smuzhiyun ' Unexpected output from checkbashisms.pl'] + 56*4882a593Smuzhiyun [' ' + x for x in output.splitlines()]) 57*4882a593Smuzhiyun 58*4882a593Smuzhiyun # We know that the first line matches and that therefore the first 59*4882a593Smuzhiyun # list entry will be empty - skip it. 60*4882a593Smuzhiyun output = BASHISM_WARNING.split(output)[1:] 61*4882a593Smuzhiyun # Turn the output into a single string like this: 62*4882a593Smuzhiyun # /.../foobar.bb 63*4882a593Smuzhiyun # possible bashism in updatercd_postrm line 2 (type): 64*4882a593Smuzhiyun # if ${@use_updatercd(d)} && type update-rc.d >/dev/null 2>/dev/null; then 65*4882a593Smuzhiyun # ... 66*4882a593Smuzhiyun # ... 67*4882a593Smuzhiyun result = [] 68*4882a593Smuzhiyun # Check the results against the allowed list 69*4882a593Smuzhiyun for message, source in zip(output[0::2], output[1::2]): 70*4882a593Smuzhiyun if not is_whitelisted(source): 71*4882a593Smuzhiyun if lineno is not None: 72*4882a593Smuzhiyun message = SCRIPT_LINENO_RE.sub(lambda m: ' line %d ' % (int(m.group(1)) + int(lineno) - 1), 73*4882a593Smuzhiyun message) 74*4882a593Smuzhiyun result.append(' ' + message.strip()) 75*4882a593Smuzhiyun result.extend([' %s' % x for x in source.splitlines()]) 76*4882a593Smuzhiyun if result: 77*4882a593Smuzhiyun result.insert(0, filename) 78*4882a593Smuzhiyun return '\n'.join(result) 79*4882a593Smuzhiyun else: 80*4882a593Smuzhiyun return None 81*4882a593Smuzhiyun 82*4882a593Smuzhiyundef get_tinfoil(): 83*4882a593Smuzhiyun scripts_path = os.path.dirname(os.path.realpath(__file__)) 84*4882a593Smuzhiyun lib_path = scripts_path + '/lib' 85*4882a593Smuzhiyun sys.path = sys.path + [lib_path] 86*4882a593Smuzhiyun import scriptpath 87*4882a593Smuzhiyun scriptpath.add_bitbake_lib_path() 88*4882a593Smuzhiyun import bb.tinfoil 89*4882a593Smuzhiyun tinfoil = bb.tinfoil.Tinfoil() 90*4882a593Smuzhiyun tinfoil.prepare() 91*4882a593Smuzhiyun # tinfoil.logger.setLevel(logging.WARNING) 92*4882a593Smuzhiyun return tinfoil 93*4882a593Smuzhiyun 94*4882a593Smuzhiyunif __name__=='__main__': 95*4882a593Smuzhiyun import argparse, shutil 96*4882a593Smuzhiyun 97*4882a593Smuzhiyun parser = argparse.ArgumentParser(description='Bashim detector for shell fragments in recipes.') 98*4882a593Smuzhiyun parser.add_argument("recipes", metavar="RECIPE", nargs="*", help="recipes to check (if not specified, all will be checked)") 99*4882a593Smuzhiyun parser.add_argument("--verbose", default=False, action="store_true") 100*4882a593Smuzhiyun args = parser.parse_args() 101*4882a593Smuzhiyun 102*4882a593Smuzhiyun if shutil.which("checkbashisms.pl") is None: 103*4882a593Smuzhiyun print("Cannot find checkbashisms.pl on $PATH, get it from https://salsa.debian.org/debian/devscripts/raw/master/scripts/checkbashisms.pl") 104*4882a593Smuzhiyun sys.exit(1) 105*4882a593Smuzhiyun 106*4882a593Smuzhiyun # The order of defining the worker function, 107*4882a593Smuzhiyun # initializing the pool and connecting to the 108*4882a593Smuzhiyun # bitbake server is crucial, don't change it. 109*4882a593Smuzhiyun def func(item): 110*4882a593Smuzhiyun (filename, key, lineno), script = item 111*4882a593Smuzhiyun if args.verbose: 112*4882a593Smuzhiyun print("Scanning %s:%s" % (filename, key)) 113*4882a593Smuzhiyun return process(filename, key, lineno, script) 114*4882a593Smuzhiyun 115*4882a593Smuzhiyun import multiprocessing 116*4882a593Smuzhiyun pool = multiprocessing.Pool() 117*4882a593Smuzhiyun 118*4882a593Smuzhiyun tinfoil = get_tinfoil() 119*4882a593Smuzhiyun 120*4882a593Smuzhiyun # This is only the default configuration and should iterate over 121*4882a593Smuzhiyun # recipecaches to handle multiconfig environments 122*4882a593Smuzhiyun pkg_pn = tinfoil.cooker.recipecaches[""].pkg_pn 123*4882a593Smuzhiyun 124*4882a593Smuzhiyun if args.recipes: 125*4882a593Smuzhiyun initial_pns = args.recipes 126*4882a593Smuzhiyun else: 127*4882a593Smuzhiyun initial_pns = sorted(pkg_pn) 128*4882a593Smuzhiyun 129*4882a593Smuzhiyun pns = set() 130*4882a593Smuzhiyun scripts = {} 131*4882a593Smuzhiyun print("Generating scripts...") 132*4882a593Smuzhiyun for pn in initial_pns: 133*4882a593Smuzhiyun for fn in pkg_pn[pn]: 134*4882a593Smuzhiyun # There's no point checking multiple BBCLASSEXTENDed variants of the same recipe 135*4882a593Smuzhiyun # (at least in general - there is some risk that the variants contain different scripts) 136*4882a593Smuzhiyun realfn, _, _ = bb.cache.virtualfn2realfn(fn) 137*4882a593Smuzhiyun if realfn not in pns: 138*4882a593Smuzhiyun pns.add(realfn) 139*4882a593Smuzhiyun data = tinfoil.parse_recipe_file(realfn) 140*4882a593Smuzhiyun for key in data.keys(): 141*4882a593Smuzhiyun if data.getVarFlag(key, "func") and not data.getVarFlag(key, "python"): 142*4882a593Smuzhiyun script = data.getVar(key, False) 143*4882a593Smuzhiyun if script: 144*4882a593Smuzhiyun filename = data.getVarFlag(key, "filename") 145*4882a593Smuzhiyun lineno = data.getVarFlag(key, "lineno") 146*4882a593Smuzhiyun # There's no point in checking a function multiple 147*4882a593Smuzhiyun # times just because different recipes include it. 148*4882a593Smuzhiyun # We identify unique scripts by file, name, and (just in case) 149*4882a593Smuzhiyun # line number. 150*4882a593Smuzhiyun attributes = (filename or realfn, key, lineno) 151*4882a593Smuzhiyun scripts.setdefault(attributes, script) 152*4882a593Smuzhiyun 153*4882a593Smuzhiyun 154*4882a593Smuzhiyun print("Scanning scripts...\n") 155*4882a593Smuzhiyun for result in pool.imap(func, scripts.items()): 156*4882a593Smuzhiyun if result: 157*4882a593Smuzhiyun print(result) 158*4882a593Smuzhiyun tinfoil.shutdown() 159