#!/usr/bin/env python3 import os import sys import glob import time import atexit import difflib import pathlib import argparse import tempfile from io import StringIO from typing import List from pathlib import Path import config from explain import ExplainOpts, FileInfo, ElfFileInfo, NginxInfo from expect import ExpectChain, glob_match_ignore_slash def parse_args(): parser = argparse.ArgumentParser() parser.add_argument( "--path", "-p", help="Path to the directory, binary package or docker image tag to compare") parser.add_argument( "--image", help="Docker image tag to compare") parser.add_argument( "--output", "-o", help="Path to output manifest, use - to write to stdout") parser.add_argument( "--suite", "-s", help="Expect suite name to test, defined in config.py") parser.add_argument( "--file_list", "-f", help="Path to the files list to explain for manifest; " + "each line in the file should be a glob pattern of full path") parser.add_argument( "--owners", help="Display owner and group", action="store_true") parser.add_argument( "--mode", help="Display mode", action="store_true") parser.add_argument( "--size", help="Display size", action="store_true") parser.add_argument("--arch", help="Display ELF architecture", action="store_true") parser.add_argument("--merge_rpaths_runpaths", help="Treate RPATH and RUNPATH as same", action="store_true") parser.add_argument( "--imported_symbols", help="Display imported symbols", action="store_true") parser.add_argument( "--exported_symbols", help="Display exported symbols", action="store_true") parser.add_argument("--version_requirement", help="Display exported symbols", action="store_true") return parser.parse_args() def read_glob(path: str): if not path: return ["**"] with open(path, "r") as f: return f.read().splitlines() def gather_files(path: str, image: str): if image: t = tempfile.TemporaryDirectory() atexit.register(t.cleanup) code = os.system("docker pull {img} && docker create --name={name} {img} && docker export {name} | tar xf - -C {tmp} && docker rm -f {name}".format( img=image, name="explain_manifest_%d" % time.time(), tmp=t.name )) if code != 0: raise Exception("Failed to extract image %s" % image) return t.name ext = os.path.splitext(path)[1] if ext in (".deb", ".rpm") or path.endswith(".apk.tar.gz"): t = tempfile.TemporaryDirectory() atexit.register(t.cleanup) if ext == ".deb": code = os.system( "ar p %s data.tar.gz | tar -C %s -xz" % (path, t.name)) elif ext == ".rpm": # rpm2cpio is needed # rpm2archive ships with rpm2cpio on debians # https://github.com/rpm-software-management/rpm/commit/37b963fa51d6ad31086a6e345ce6701afda5afff # rpm2archive has changed the behaviour to extract to stdout if stdout is not tty code = os.system( """ rpm2archive %s | tar -C %s -xz """ % (path, t.name)) elif ext == ".gz": code = os.system("tar -C %s -xf %s" % (t.name, path)) if code != 0: raise Exception("Failed to extract %s" % path) return t.name elif not Path(path).is_dir(): raise Exception("Don't know how to process \"%s\"" % path) return path def walk_files(path: str, globs: List[str]): results = [] # use pathlib instead of glob.glob to avoid recurse into symlink dir for file in sorted(pathlib.Path(path).rglob("*")): full_path = str(file) file = str(file.relative_to(path)) if globs and not glob_match_ignore_slash(file, globs): continue if not file.startswith("/") and not file.startswith("./"): file = '/' + file # prettifier if file.endswith("sbin/nginx"): f = NginxInfo(full_path, file) elif os.path.splitext(file)[1] == ".so" or os.path.basename(os.path.dirname(file)) in ("bin", "lib", "lib64", "sbin"): p = Path(full_path) if p.is_symlink(): continue f = ElfFileInfo(full_path, file) else: f = FileInfo(full_path, file) config.transform(f) results.append(f) return results def write_manifest(title: str, results: List[FileInfo], globs: List[str], opts: ExplainOpts): f = StringIO() for result in results: if not glob_match_ignore_slash(result.relpath, globs): continue entries = result.explain(opts) ident = 2 first = True for k, v in entries: if isinstance(v, list): v = ("\n" + " " * ident + "- ").join([""] + v) else: v = " %s" % v if first: f.write("-" + (" " * (ident-1))) first = False else: f.write(" " * ident) f.write("%-10s:%s\n" % (k, v)) f.write("\n") f.flush() return f.getvalue().encode("utf-8") if __name__ == "__main__": args = parse_args() if not args.suite and not args.output: raise Exception("At least one of --suite or --output is required") if not args.path and not args.image: raise Exception("At least one of --path or --image is required") if args.image and os.getuid() != 0: raise Exception("Running as root is required to explain an image") if args.path and Path(args.path).is_dir(): raise Exception( "suite mode only works with archive files (deb, rpm, apk.tar.gz, etc.") directory = gather_files(args.path, args.image) globs = read_glob(args.file_list) # filter by filelist only when explaining an image to reduce time infos = walk_files(directory, globs=globs if args.image else None) if args.image: title = "contents in image %s" % args.image elif Path(args.path).is_file(): title = "contents in archive %s" % args.path else: title = "contents in directory %s" % args.path manifest = write_manifest(title, infos, globs, ExplainOpts.from_args(args)) if args.suite: if args.suite not in config.targets: closest = difflib.get_close_matches( config.targets.keys(), args.suite, 1) maybe = "" if closest: maybe = ", maybe you meant %s" % closest[0] raise Exception("Unknown suite %s%s" % (args.suite, maybe)) E = ExpectChain(infos) E.compare_manifest(config.targets[args.suite], manifest) E.run(config.targets[args.suite]) if args.output: if args.output == "-": f = sys.stdout manifest = manifest.decode("utf-8") else: f = open(args.output, "wb") f.write(manifest) if args.output != "-": f.close()