# sudo python2 get_jar_data_list.py --ScriptName get_jar_data_list.py --jar-search-path "/usr/sap/*/*/j2ee/cluster/apps/sap.com/devserver_metadataupload_ear/servlet_jsp/developmentserver/root/WEB-INF/lib/devserver_metadataupload_war.jar" --collect-manifest-keys "implementation-version","implementation-vendor" # sudo python3 get_jar_data_list.py --ScriptName get_jar_data_list.py --jar-search-path "/usr/sap/*/*/j2ee/cluster/apps/sap.com/devserver_metadataupload_ear/servlet_jsp/developmentserver/root/WEB-INF/lib/devserver_metadataupload_war.jar" --collect-manifest-keys "implementation-version","implementation-vendor" import os import re import sys import json import zipfile import argparse import traceback import functools import glob MAX_FILE_SIZE = 1024 * 1024 # 1MB MANIFEST_DEFAULT_PATH = "META-INF/MANIFEST.MF" class Jar: def __init__(self, path, manifest_path=None, manifest_keys=None): self.path = path self.manifest_path = manifest_path self.manifest_keys = manifest_keys or [] self._manifest = {} def _parse_manifest(self, lines): for line in lines: if not line.strip() or ':' not in line: continue try: key, value = line.split(':', 1) key, value = key.strip(), value.strip() if any(manifest_key.match(key.lower()) for manifest_key in self.manifest_keys): yield key, value except ValueError: raise ValueError("Invalid manifest line: {}".format(line)) def _open(self): if not os.path.exists(self.path): raise ValueError("path does not exist: {}".format(self.path)) if not zipfile.is_zipfile(self.path): raise ValueError("path is not a zip file: {}".format(self.path)) if sys.version_info >= (3, 8): return zipfile.ZipFile(self.path, strict_timestamps=True) else: return zipfile.ZipFile(self.path) def _get_manifest_path(self, zf): if self.manifest_path and self.manifest_path in zf.namelist(): return self.manifest_path return None def _read_manifest(self, throw_on_error=False): try: with self._open() as zf: manifest_path = self._get_manifest_path(zf) if not manifest_path: return {} manifest_info = zf.getinfo(manifest_path) if manifest_info.file_size > MAX_FILE_SIZE: raise IOError("manifest file is too big") with zf.open(manifest_path) as f: readline_f = functools.partial(f.readline, MAX_FILE_SIZE) manifest_lines = list(x.decode().strip() for x in iter(readline_f, b'')) manifest = self._parse_manifest(manifest_lines) return {k: v for k, v in manifest} except Exception: sys.stderr.write("error while reading manifest of '{}': {}\n".format(self.path, traceback.format_exc())) if throw_on_error: raise return {} def manifest(self, throw_on_error=False): if not self._manifest: self._manifest = self._read_manifest(throw_on_error) return self._manifest class FatalArgumentError(Exception): pass def split_by_comma(dest): return [x.strip() for x in dest.split(",")] def split_and_compile(dest): return [re.compile(r) for r in split_by_comma(dest)] def validate_limit(dest): limit = int(dest) if limit < 1: raise ValueError("limit is too small") return limit def validate_manifest_path(dest): normalized_path = os.path.normpath(dest) if normalized_path.startswith("..") or normalized_path.startswith("/"): raise ValueError("invalid manifest path: {}".format(dest)) return normalized_path def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--ScriptName", dest='script_name', help='name of the script') parser.add_argument("--jar-search-path", help='path to search for jar files') parser.add_argument("--manifest-path", type=validate_manifest_path, help='manifest path to read', default=MANIFEST_DEFAULT_PATH) parser.add_argument("--collect-manifest-keys", dest='manifest_keys', type=split_and_compile, default=[], help='manifest keys to be extracted') parser.add_argument("--payload-size-limit", type=validate_limit, default=60 * 1024, help='limit for payload size in bytes') parser.add_argument("--jar-file-limit", type=validate_limit, default=250, help='limit for number of jar files to process') args = parser.parse_args() if not all([args.jar_search_path, args.manifest_keys]): parser.error("you must pass --jar-search-path and --collect-manifest-keys") return args def main(args): if os.geteuid() != 0: raise ValueError("script needs to be run as root") manifest_data_list = [] try: if sys.version_info >= (3, 5): jar_files = glob.glob(args.jar_search_path, recursive=True) else: jar_files = glob.glob(args.jar_search_path) except Exception as e: raise ValueError("error while searching for jar files with search path {}: {}".format(args.jar_search_path, e)) limit = args.jar_file_limit for index, jar_file in enumerate(jar_files): if index >= limit: sys.stderr.write("Limit reached: {}/{} files\n".format(limit, len(jar_files))) break if os.path.isfile(jar_file) and os.access(jar_file, os.R_OK): jar_object = Jar(jar_file, manifest_path=args.manifest_path, manifest_keys=args.manifest_keys) fields_data = jar_object.manifest() else: sys.stderr.write("File not found or not readable: {}\n".format(jar_file)) fields_data = {} manifest_data = { "path": jar_file, "fields": fields_data } manifest_data_list.append(manifest_data) payload = dict(scriptVersion=1, cmdline=" ".join(sys.argv[1:]), count=len(jar_files), data=manifest_data_list) payload_json = json.dumps(payload) if len(payload_json) > args.payload_size_limit: raise ValueError("payload size is too big (limit: {} bytes, actual: {} bytes)".format(args.payload_size_limit, len(payload_json))) print(payload_json) return 0 if __name__ == "__main__": try: args = parse_args() sys.exit(main(args)) except FatalArgumentError: sys.exit(2) except SystemExit as e: # preserve exit codes from argparse and explicit sys.exit sys.exit(e.code) except Exception: # runtime errors sys.stderr.write(traceback.format_exc()) sys.exit(3)