From 58fc30b3a3652ddc6c3e3fc571133b96e0b3b037 Mon Sep 17 00:00:00 2001 From: Arjen van Bochoven Date: Thu, 3 Oct 2019 11:04:10 +0200 Subject: [PATCH 1/6] Update Munkireport-runner Add markers for start and end of run --- .../payload/usr/local/munkireport/munkireport-runner | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkireport-runner b/public/assets/client_installer/payload/usr/local/munkireport/munkireport-runner index 54399c493..f4b336338 100755 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkireport-runner +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkireport-runner @@ -11,6 +11,8 @@ import os def main(): '''Main''' + reportcommon.display_detail("## Starting MunkiReport run") + # set runtype runtype = 'auto' @@ -61,6 +63,8 @@ def main(): touchfile = '/Users/Shared/.com.github.munkireport.run' if os.path.exists(touchfile): os.remove(touchfile) + + reportcommon.display_detail("## Finished run") if __name__ == '__main__': main() From f9c556cb123f653b664077c62501fad16f6d0aab Mon Sep 17 00:00:00 2001 From: Arjen van Bochoven Date: Wed, 9 Oct 2019 22:40:20 +0200 Subject: [PATCH 2/6] Prevent XSS on login form --- app/controllers/auth.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/auth.php b/app/controllers/auth.php index fcca9e583..0fb4d24c2 100644 --- a/app/controllers/auth.php +++ b/app/controllers/auth.php @@ -96,7 +96,11 @@ public function login($return = '') } } - $data = array('login' => $login, 'url' => url("auth/login/$return")); + $data = [ + // Prevent XSS + 'login' => htmlspecialchars($login, ENT_QUOTES, 'UTF-8'), + 'url' => url("auth/login/$return") + ]; $obj = new View(); $obj->view('auth/login', $data); From 0e64b470a8f185327ad59c061b6b947450569e93 Mon Sep 17 00:00:00 2001 From: Arjen van Bochoven Date: Tue, 12 Nov 2019 20:02:40 +0800 Subject: [PATCH 3/6] Merge wip (#1298) * Release version 5.1.2. * Bumping to v5.1.3 for development. * Update install.php Discard paths containing @ fixes #1290 * Update reportcommon.py Increase connection_timeout to 60 seconds. * Update Database.php Fix port issue * Helper script for managing MunkiReport installs (#1293) * Update Munkireport-runner Add markers for start and end of run * Prevent XSS on login form * beginning resolution of APPLE-305. conversion to python3 and logging. * removing zip upgrade functionality. pushing in own commit in case we decide we want it in the future. resolving APPLE-306. * resolves APPLE-304. adds info parameter. general cleanup. regex for checking version name. error handling of all subprocess calls. * made *black*. also removed dependency on some global variables so we can appropriately write unit tests, if desired. * resolves APPLE-313, adds error handling to any subprocess calls or file handling. * adding requirements.txt * added verbose mode * resolves APPLE-316 * adding mr_upgrade * made more *black* * added actual error output when running shutil * switched to using subprocess.run for more accurate error handling. * added formatting changes * testing stash * testing switch for composer and migrations * testing * switched to fetch * switching from version number to commit * beginning outline for restore database functionality. * added more to restore functionality. * added some more documentation * Update mr_upgrade.py Will need to checkout the upgrade script from the master branch. * resolves APPLE-325. switches to branch tag if it exists, or commit if it doesnt. * had to reformat command for mysqldump * resolves APPLE-317. restoration compatibility complete for both mysql and sqlite. * added more debug output * added an extra quote * added checks to make sure sqlite db exists. * add example docker-compose file (#1295) * add example docker-compose file * Update com.github.munkireport.runner.plist Change launchd to use KeepAlive instead of WatchPaths, possible fix for #1297 * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 39 +- app/controllers/install.php | 4 + app/helpers/site_helper.php | 2 +- app/lib/munkireport/Database.php | 10 +- build/mr_upgrade.py | 425 ++++++++++++++++++ build/requirements.txt | 3 + docker-compose.yml.example | 40 ++ .../com.github.munkireport.runner.plist | 12 +- .../munkireport/munkilib/reportcommon.py | 1 + 9 files changed, 527 insertions(+), 9 deletions(-) create mode 100755 build/mr_upgrade.py create mode 100644 build/requirements.txt create mode 100644 docker-compose.yml.example diff --git a/CHANGELOG.md b/CHANGELOG.md index eb6a3f7c1..67df74ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,41 @@ -### [5.1.2](https://github.com/munkireport/munkireport-php/compare/v5.1.1...HEAD) (Unreleased) +### [5.1.3](https://github.com/munkireport/munkireport-php/compare/v5.1.2...HEAD) (Unreleased) + +FEATURES + - Added an upgrade script (@lcsees) + - Added a docker compose example (@b-reich) + +FIXES + - MySQL port is now correctly used in admin panel + - Server timeout is now 60 seconds for slow servers + - Filter scripts for @ sign (Synology issue) + - LaunchDaemon changed to use KeepAlive instead of WatchPaths - should prevent the runner from starting twice + +MODULE UPDATES + - munkireport/crashplan (v1.4 => V1.5) + - munkireport/managedinstalls (v2.2 => V2.3) + +DEPENDENCY UPDATES + - guzzlehttp/guzzle (6.3.3 => 6.4.1) + - doctrine/event-manager (v1.0.0 => 1.1.0) + - doctrine/cache (v1.8.0 => 1.9.0) + - doctrine/dbal (v2.9.2 => v2.10.0) + - symfony/yaml (v3.4.32 => v3.4.33) + - symfony/var-dumper (v4.3.5 => v4.3.6) + - tightenco/collect (v6.3.0 => v6.4.1) + - psr/log (1.1.0 => 1.1.2) + - adldap2/adldap2 (v10.1.1 => v10.2.0) + - robrichards/xmlseclibs (3.0.3 => 3.0.4) + - onelogin/php-saml (3.3.0 => 3.3.1) + - symfony/service-contracts (v1.1.7 => v1.1.8) + - symfony/console (v4.3.5 => v4.3.6) + - symfony/process (v4.3.5 => v4.3.6) + - doctrine/inflector (v1.3.0 => 1.3.1) + - symfony/translation (v4.3.5 => v4.3.6) + - nesbot/carbon (2.25.2 => 2.26.0) + - symfony/finder (v4.3.5 => v4.3.6) + - phpoption/phpoption (1.5.0 => 1.5.2) + +### [5.1.2](https://github.com/munkireport/munkireport-php/compare/v5.1.1...v5.1.2) (October 19, 2019) Increase default script timeout to 30 seconds diff --git a/app/controllers/install.php b/app/controllers/install.php index 6d5024668..3ce578fa2 100644 --- a/app/controllers/install.php +++ b/app/controllers/install.php @@ -143,6 +143,10 @@ private function is_regular_file($fileObj) if($fileObj['basename'][0] == '.'){ return false; } + // Don't accept @ in path - Synology I'm looking at you + if(strpos($fileObj['path'], '@') !== false){ + return false; + } return true; } } diff --git a/app/helpers/site_helper.php b/app/helpers/site_helper.php index 6fa57b35d..f7a7cb8a2 100644 --- a/app/helpers/site_helper.php +++ b/app/helpers/site_helper.php @@ -3,7 +3,7 @@ use munkireport\models\Machine_group, munkireport\lib\Modules, munkireport\lib\Dashboard; // Munkireport version (last number is number of commits) -$GLOBALS['version'] = '5.1.2.3918'; +$GLOBALS['version'] = '5.1.3.3919'; // Return version without commit count function get_version() diff --git a/app/lib/munkireport/Database.php b/app/lib/munkireport/Database.php index 9f4a13067..acd4cb5a9 100644 --- a/app/lib/munkireport/Database.php +++ b/app/lib/munkireport/Database.php @@ -43,11 +43,15 @@ public function connect() */ private function getDSN() { - switch ($this->connection['driver']) { + extract($this->connection, EXTR_SKIP); + + switch ($driver) { case 'sqlite': - return "sqlite:{$this->connection['database']}"; + return "sqlite:{$database}"; case 'mysql': - return "mysql:host={$this->connection['host']};dbname={$this->connection['database']}"; + return isset($port) + ? "mysql:host={$host};port={$port};dbname={$database}" + : "mysql:host={$host};dbname={$database}"; default: throw new \Exception("Unknown driver in connection", 1); } diff --git a/build/mr_upgrade.py b/build/mr_upgrade.py new file mode 100755 index 000000000..5b5f91ef1 --- /dev/null +++ b/build/mr_upgrade.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 + +""" +Script for upgrading MunkiReport. +""" + +import argparse +import datetime +import json +import logging +import operator +import os +import re +import shutil +import sqlite3 +import subprocess +import urllib.request +from distutils.dir_util import copy_tree + +import coloredlogs +from dotenv import load_dotenv + +# log output to console +log = logging.getLogger() +coloredlogs.install( + fmt="[%(asctime)s] [%(levelname)-8s] %(message)s", level="INFO", logger=log +) + +# load environment variables +load_dotenv() + + +def run_command(args: list, suppress_output: bool = False) -> bool: + """Run a given command.""" + log.debug( + f"Running command '{' '.join(args)}', suppress_output={suppress_output}...'" + ) + try: + subprocess.run(args, capture_output=True, check=True) + except subprocess.CalledProcessError as e: + if not suppress_output: + log.error( + f"Command '{' '.join(args)}' failed with the following output: '{e.stderr.decode('utf8')}'. Exiting..." + ) + return False + + log.debug(f"Command '{' '.join(args)}' completed successfully.") + return True + + +def get_current_version(install_path: str) -> str: + """Return current build version""" + helper = install_path + "app/helpers/site_helper.php" + if os.path.exists(helper): + try: + version = re.findall( + r"(?<=GLOBALS\['version'\])?[0-9].*(?=';)", open(helper).read() + )[0] + except: + log.error(f"Error encountered when parsing '{helper}'.") + return None + + return version + return None + + +def get_database_type() -> str: + """Return the database type.""" + return os.getenv("CONNECTION_DRIVER") or "sqlite" + + +def set_maintenance_mode(install_path: str, value: str) -> None: + """Set maintenance mode to enabled or disabled.""" + log.debug(f"Setting maintenance mode to '{value}'...") + maintenance_file = install_path + "storage/framework/down" + if value == "enabled": + try: + open(maintenance_file, "a").close() + return True + except: + log.error(f"Could not create '{maintenance_file}'.") + else: + try: + os.remove(maintenance_file) + except: + log.error(f"Could not remove '{maintenance_file}'.") + + +def backup_database(backup_dir: str, install_path: str, current_time: str) -> bool: + """Backup a MunkiReport database.""" + database_type = get_database_type() + + if database_type == "mysql": + database = os.getenv("CONNECTION_DATABASE") + backup_file = os.path.join(backup_dir, database + "-" + current_time + ".bak") + cmd = [ + "/usr/local/opt/mysql-client/bin/mysqldump", + f"--user={os.getenv('CONNECTION_USERNAME')}", + f"--password={os.getenv('CONNECTION_PASSWORD')}", + f"--host={os.getenv('CONNECTION_HOST')}", + "--databases", + os.getenv("CONNECTION_DATABASE"), + f"--result-file={backup_file}", + "--skip-comments", + ] + log.info("Backing up database to '{}'...".format(backup_file)) + if not run_command(cmd): + return False + + log.info("Backup completed successfully.") + + elif database_type == "sqlite": + database_path = os.getenv("CONNECTION_DATABASE") + + # ensure that the database path is defined and exists + if database_path: + if not os.path.isfile(database_path): + log.error(f"Could not find sqlite database at path '{database_path}'.") + return False + else: + log.error(f"'CONNECTION_DATABASE' is undefined in your environment.") + return False + + # backup the database to the backup directory with the current time + backup_file = backup_dir + "/db_" + current_time + ".sqlite.bak" + log.info(f"Backing up database to '{backup_file}'...") + + conn = sqlite3.connect(database_path) + try: + with open(backup_file, "w") as f: + for line in conn.iterdump(): + f.write("%s\n" % line) + + except OSError as e: + log.error(f"The following error encountered when backing up database: {e}.") + return False + + return True + + +def restore_database(backup_file: str, install_path: str) -> bool: + """Restore a MunkiReport database from a backup.""" + database_type = get_database_type() + + if not os.path.isfile(backup_file): + log.error(f"Backup file '{backup_file}' does not exist!'") + return False + + log.info(f"Restoring database from backup file '{backup_file}'...") + database = os.getenv("CONNECTION_DATABASE") + + if database_type == "mysql": + cmd = [ + "/usr/local/opt/mysql-client/bin/mysql", + f"--user={os.getenv('CONNECTION_USERNAME')}", + f"--password={os.getenv('CONNECTION_PASSWORD')}", + f"--host={os.getenv('CONNECTION_HOST')}", + f"--database={database}", + f"--execute=source {backup_file}", + ] + log.debug(f"Restoring database '{database}' from '{backup_file}'...'") + if not run_command(cmd): + return False + + elif database_type == "sqlite": + database_path = os.getenv("CONNECTION_DATABASE") + + # ensure that the database path is defined and exists + if database_path: + if not os.path.isfile(database_path): + log.error(f"Could not find sqlite database at path '{database_path}'.") + return False + else: + log.error(f"'CONNECTION_DATABASE' is undefined in your environment.") + return False + + # move the old database file to db.sqlite.old + log.debug( + f"Renaming current database from '{database_path}' to '{database_path}.old'..." + ) + try: + shutil.move(database_path, database_path + ".old") + except OSError as e: + log.error(f"The following error encountered when backing up database: {e}.") + return False + + # import from the backup + log.debug(f"Rename successful. Restoring database from '{backup_file}'...") + try: + conn = sqlite3.connect(database_path) + c = conn.cursor() + except: + log.error( + f"Unable to instantiate sqlite cursor with database {database_path}." + ) + return False + + try: + with open(backup_file, "r") as bf: + for line in bf: + c.execute(line) + except: + log.error("Errors encountered when reading backup file.") + return False + + log.info("Database restoration completed successfully.") + return True + + +def backup_files(backup_dir: str, install_path: str, current_time: str) -> bool: + """Create file backup of install.""" + backup_dir = os.path.join(backup_dir, "munkireport", current_time) + log.info(f"Backing up files to '{backup_dir}'...") + if not os.path.exists(backup_dir): + try: + os.makedirs(backup_dir) + except: + log.error(f"Could not make backup directory '{backup_dir}'.") + return False + else: + log.debug(f"Backup dir {backup_dir} already exists, continuing...") + try: + copy_tree(install_path, backup_dir) + except: + log.error( + f"Errors encountered when running copy_tree({install_path}, {backup_dir})." + ) + return False + + return True + + +def get_versions() -> dict: + """Return MR versions""" + mr_api = "https://api.github.com/repos/munkireport/munkireport-php/releases" + log.debug(f"Querying '{mr_api}' for latest release...") + versions = {} + try: + with urllib.request.urlopen(mr_api) as response: + data = json.loads(response.read()) + + for version in data: + versions[version["tag_name"].strip("v")] = version["target_commitish"] + log.debug(f"Found versions: {versions}.") + + except: + log.error("Errors encountered when grabbing latest version.") + + return versions + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Manage a MunkiReport install.") + group = parser.add_mutually_exclusive_group() + group.add_argument( + "-i", + "--info", + action="store_true", + help="Print info on the MunkiReport install.", + ) + parser.add_argument( + "--no-backup", + action="store_true", + default=False, + help="Do not take any backups before upgrading.", + ) + parser.add_argument( + "--backup-dir", type=str, help="Directory to back up to.", default="/tmp" + ) + parser.add_argument( + "--install-path", + type=str, + default=os.path.dirname(os.path.realpath(__file__)).strip("build"), + help="Install path for MunkiReport.", + ) + parser.add_argument( + "--upgrade", + action="store_true", + default=False, + help="Attempt to upgrade MunkiReport.", + ) + parser.add_argument("--restore", type=str, help="Restore database from backup.") + parser.add_argument( + "-v", + "--verbose", + action="store_true", + default=False, + help="Enable verbose logging.", + ) + parser.add_argument( + "--version", type=str, default="latest", help="Version to upgrade to." + ) + parser.add_argument( + "--no-composer", + action="store_true", + default=False, + help="Don't run composer after upgrade.", + ) + parser.add_argument( + "--no-migrations", + action="store_true", + default=False, + help="Don't run migrations after upgrade.", + ) + args = parser.parse_args() + + if args.verbose: + coloredlogs.install( + fmt="[%(asctime)s] - [%(levelname)-8s] - %(message)s", + level="DEBUG", + logger=log, + ) + + install_path = args.install_path + current_version = get_current_version(install_path) + desired_version = args.version + versions = get_versions() + latest_version = max(versions.items(), key=operator.itemgetter(0))[0] + if args.version == "latest": + desired_version = latest_version + + database_type = get_database_type() + + if not current_version: + log.error( + f"The directory '{install_path}' does not appear to be a valid MunkiReport install." + ) + exit() + + log.info(f"Current version: {current_version}") + log.info(f"Latest version: {latest_version}") + log.info(f"Install path: {install_path}") + log.info(f"Database type: {database_type}") + + if args.info: + exit() + + if args.upgrade: + if current_version >= latest_version and args.version == "latest": + log.info("No version upgrade available.") + + else: + if desired_version not in versions.keys(): + log.error(f"Version '{desired_version}' was not found. Exiting...") + exit() + + log.info(f"Installing version {desired_version}...") + + # enable maintenance mode + if not set_maintenance_mode(install_path, "enabled"): + exit() + + current_time = datetime.datetime.now().strftime("%Y%m%d%H%M") + + if not args.no_backup: + # backup database + if not backup_database(args.backup_dir, install_path, current_time): + exit() + + # backup files + if not backup_files(args.backup_dir, install_path, current_time): + exit() + + # if we go back to an old enough version, mr_upgrade wont exist so we wont + # be able to run an upgrade due to local changes, so stash the upgrade script. + run_command(["git", "stash", "save", "mr_upgrade.py"]) + + # attempt git fetch for update + log.info("Starting Git fetch...") + if not run_command(["git", "fetch", "origin", "master"]): + exit() + + log.info("Git fetch complete.") + + # switch to the specific commit for the version + log.info(f"Switching to commit for version {desired_version}...") + commit = versions[desired_version] + log.debug(f"Commit for version {desired_version} is {commit}.") + + # try to checkout to the specific version (v5.1.0, for example), + # which makes it wasier to see the version you are currently + # running when doing a git branch + if not run_command( + ["git", "checkout", f"v{desired_version}"], suppress_output=True + ): + if not run_command(["git", "checkout", commit]): + exit() + else: + log.info( + f"No tag was found for version {desired_version}, so commit was checked out instead of version tag." + ) + + log.info("Git checkout complete.") + + if not args.no_composer: + # run composer + os.chdir(install_path) + log.info("Running composer...") + if not run_command(["/usr/local/bin/composer", "update", "--no-dev"]): + exit() + + log.info("Composer complete.") + + if not args.no_migrations: + # run migrations + os.chdir(f"{install_path}/build/") + log.info("Running migrations...") + if not run_command( + ["/usr/bin/php", f"{install_path}database/migrate.php"] + ): + exit() + + log.info("Migrations complete.") + + # pull the latest version of the mr_upgrade script + run_command(["git", "checkout", "master", "mr_upgrade.py"]) + + # disable maintenance mode + set_maintenance_mode(install_path, "disabled") + log.info("Upgrade complete.") + + elif args.restore: + restore = restore_database(args.restore, install_path) + if not restore: + exit(1) diff --git a/build/requirements.txt b/build/requirements.txt new file mode 100644 index 000000000..c687a0680 --- /dev/null +++ b/build/requirements.txt @@ -0,0 +1,3 @@ +coloredlogs==10.0 +humanfriendly==4.18 +python-dotenv==0.10.3 diff --git a/docker-compose.yml.example b/docker-compose.yml.example new file mode 100644 index 000000000..6529bd4a8 --- /dev/null +++ b/docker-compose.yml.example @@ -0,0 +1,40 @@ +version: '3' +services: + munkireport: + image: munkireport/munkireport-php:latest + restart: always + environment: + - MODULES=applications, directory_service, disk_report, displays_info, extensions, filevault_status, omebrew, homebrew_info, ibridge, installhistory, inventory, localadmin, managedinstalls, mdm_status, munkiinfo, munkireport, munkireportinfo, network, power, printer, profile, security, softwareupdate, sophos, supported_os, timemachine, usage_stats, user_sessions, warranty, wifi + - SITENAME=Munkireport + - CONNECTION_DRIVER=mysql + - CONNECTION_HOST=db + - CONNECTION_PORT=3306 + - CONNECTION_DATABASE=munkireport + - CONNECTION_USERNAME=munkireport + - CONNECTION_PASSWORD= + - AUTH_METHODS=LOCAL + - PUID=1000 + - PGID=1000 + - CLIENT_PASSPHRASES= + - WEBHOST=https://munkireport-sda.domain.com + - TZ=Europe/Berlin + depends_on: + - db + ports: + - 80:80 + volumes: + - ./munkireport-db/:/var/munkireport/app/db + - ./user//:/var/munkireport/local/users + db: + image: mariadb:latest + restart: always + environment: + - MYSQL_ROOT_PASSWORD= + - MYSQL_DATABASE=munkireport + - MYSQL_USER=munkireport + - MYSQL_PASSWORD= + - PUID=1000 + - PGID=1000 + - TZ=Europe/Berlin + volumes: + - ./db/:/var/lib/mysql diff --git a/public/assets/client_installer/payload/Library/LaunchDaemons/com.github.munkireport.runner.plist b/public/assets/client_installer/payload/Library/LaunchDaemons/com.github.munkireport.runner.plist index 9913ef5b9..b465aa025 100644 --- a/public/assets/client_installer/payload/Library/LaunchDaemons/com.github.munkireport.runner.plist +++ b/public/assets/client_installer/payload/Library/LaunchDaemons/com.github.munkireport.runner.plist @@ -8,9 +8,13 @@ /usr/local/munkireport/munkireport-runner RunAtLoad - WatchPaths - - /Users/Shared/.com.github.munkireport.run - + KeepAlive + + PathState + + /Users/Shared/.com.github.munkireport.run + + + diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py index d6a62570f..6026212bc 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py @@ -73,6 +73,7 @@ def curl(url, values): options["content_type"] = "application/x-www-form-urlencoded" options["body"] = urlencode(values) options["logging_function"] = display_detail + options["connection_timeout"] = 60 if pref('UseMunkiAdditionalHttpHeaders'): custom_headers = prefs.pref( constants.ADDITIONAL_HTTP_HEADERS_KEY) From 0aa1559d0ed9bf323b1c9f84527db1293b144c80 Mon Sep 17 00:00:00 2001 From: Arjen van Bochoven Date: Thu, 16 Jan 2020 10:52:46 +0100 Subject: [PATCH 4/6] Update runner and common and make curl exit clean --- .../usr/local/munkireport/munkilib/reportcommon.py | 12 +++++++++++- .../payload/usr/local/munkireport/munkireport-runner | 7 +------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py index 6026212bc..82a57c3e8 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py @@ -39,7 +39,7 @@ class CurlError(Exception): def __init__(self, status, message): display_error(message) - exit(0) + finish_run() def set_verbosity(level): """ @@ -65,6 +65,16 @@ def display_detail(msg, *args): """ display.display_detail('%s' % msg, *args) +def finish_run(): + remove_run_file() + display_detail("## Finished run") + exit(0) + +def remove_run_file(): + touchfile = '/Users/Shared/.com.github.munkireport.run' + if os.path.exists(touchfile): + os.remove(touchfile) + def curl(url, values): options = dict() diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkireport-runner b/public/assets/client_installer/payload/usr/local/munkireport/munkireport-runner index f4b336338..b2bf95ce8 100755 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkireport-runner +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkireport-runner @@ -59,12 +59,7 @@ def main(): items[key] = {'path':val} reportcommon.process(serial, items) - - touchfile = '/Users/Shared/.com.github.munkireport.run' - if os.path.exists(touchfile): - os.remove(touchfile) - - reportcommon.display_detail("## Finished run") + reportcommon.finish_run() if __name__ == '__main__': main() From 40cf8186caf8025f2c541bf80116bc4ebbfdc9af Mon Sep 17 00:00:00 2001 From: Arjen van Bochoven Date: Sun, 2 Feb 2020 18:55:32 +0100 Subject: [PATCH 5/6] Release 5.1.5 (#1309) --- CHANGELOG.md | 26 +++++++++++++++++++++++++- app/helpers/site_helper.php | 2 +- public/assets/locales/de.json | 2 +- public/assets/locales/en.json | 2 +- public/assets/locales/fr.json | 2 +- public/assets/locales/nl.json | 2 +- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed7ece205..17ce3af54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,28 @@ -### [5.1.4](https://github.com/munkireport/munkireport-php/compare/v5.1.3...HEAD) (Unreleased) +### [5.1.5](https://github.com/munkireport/munkireport-php/compare/v5.1.4...HEAD) (Unreleased) + +Mostly module updates, and a small fix for apache servers. +Security module now reports on Secure Boot and External Boot thanks to @poundbangbash Note that this change needs a database migration. + +MODULE UPDATES + - munkireport/reportdata (v2.3 => v2.4) + - munkireport/location (v1.1 => V1.2) + - munkireport/mdm_status (v1.7 => v1.9) + - munkireport/security (V1.5 => v1.6) + - munkireport/supported_os (v1.7 => V1.8) + - munkireport/user_sessions (v1.3 => V1.4) + +DEPENDENCY UPDATES + - symfony/yaml (v3.4.36 => v3.4.37) + - symfony/var-dumper (v4.4.2 => v4.4.4) + - tightenco/collect (v6.11.0 => v6.13.0) + - symfony/console (v4.4.2 => v4.4.4) + - symfony/process (v4.4.2 => v4.4.4) + - symfony/translation (v4.4.2 => v4.4.4) + - nesbot/carbon (2.28.0 => 2.29.1) + - symfony/finder (v4.4.2 => v4.4.4) + + +### [5.1.4](https://github.com/munkireport/munkireport-php/compare/v5.1.3...v5.1.4) (January 20, 2020) This is a bug fix release. Most notable change: the model lookup now happens in the machine module and not in the warranty module anymore. diff --git a/app/helpers/site_helper.php b/app/helpers/site_helper.php index e66804a79..97ea2b11c 100644 --- a/app/helpers/site_helper.php +++ b/app/helpers/site_helper.php @@ -3,7 +3,7 @@ use munkireport\models\Machine_group, munkireport\lib\Modules, munkireport\lib\Dashboard; // Munkireport version (last number is number of commits) -$GLOBALS['version'] = '5.1.4.3920'; +$GLOBALS['version'] = '5.1.5.3921'; // Return version without commit count function get_version() diff --git a/public/assets/locales/de.json b/public/assets/locales/de.json index b59c53877..c15a1b871 100644 --- a/public/assets/locales/de.json +++ b/public/assets/locales/de.json @@ -104,7 +104,7 @@ "clients": "Clients", "comment": "Kommentar", "hour": "Clients pro Stunde", - "panel_title": "Clientaktivität im letzten Monat", + "panel_title": "Clientaktivität im letzten __number__ Tage", "report": "Client Report", "tab": { "summary": "Zusammenfassung" diff --git a/public/assets/locales/en.json b/public/assets/locales/en.json index 917abe283..33c89ec2f 100644 --- a/public/assets/locales/en.json +++ b/public/assets/locales/en.json @@ -107,7 +107,7 @@ "clients": "Clients", "comment": "Comments", "hour": "Clients per hour", - "panel_title": "Client activity in the last month", + "panel_title": "Client activity in the last __number__ days", "report": "Client Report", "tab": { "summary": "Summary" diff --git a/public/assets/locales/fr.json b/public/assets/locales/fr.json index 3d153241d..55e6a8b27 100644 --- a/public/assets/locales/fr.json +++ b/public/assets/locales/fr.json @@ -104,7 +104,7 @@ "clients": "Clients", "comment": "Commentaires", "hour": "Clients par heure", - "panel_title": "Activité du client dans le dernier mois", + "panel_title": "Activité du client dans le dernier __number__ jours", "report": "Client Report", "tab": { "summary": "Résumé" diff --git a/public/assets/locales/nl.json b/public/assets/locales/nl.json index ddd75a449..d534d0d21 100644 --- a/public/assets/locales/nl.json +++ b/public/assets/locales/nl.json @@ -107,7 +107,7 @@ "installed_third_party_software": "Geïnstalleerde Software van Derden", "install_date": "Installatie datum", "no_install_history": "Geen installatie historie", - "panel_title": "Client activity in the last month", + "panel_title": "Actieve machines in de laatste __number__ dagen", "process_name": "Procesnaam", "total": "Total Clients" }, From 8106cf6282b38863552513d33c627edd9a06c485 Mon Sep 17 00:00:00 2001 From: Elliot Jordan Date: Sun, 2 Feb 2020 11:07:34 -0800 Subject: [PATCH 6/6] Add pre-commit hooks for PHP linting, Python Black, and other basics (conflict resolved) (#1299) * Apply Black autoformatting to Python files Re-application of 6ff8e5a7f9285730a625d68b822fd0a9ce4bab0c from #1253. * Create .pre-commit-config.yaml Hooks that may be useful in the future but will require some initial work to implement are commented out for now. * Add JSON linting to pre-commit hooks * Add XML linting to pre-commit hooks * Use docformatter for docstings For alignment with PEP 257: https://www.python.org/dev/peps/pep-0257/ Re-application of ca231181163fe6f31aefcfe148e3753a2ab7e267 from #1253. --- .pre-commit-config.yaml | 27 ++ build/mr_upgrade.py | 8 +- build/release/make_munkireport_release.py | 21 +- .../munkireport/munkilib/FoundationPlist.py | 87 ++-- .../local/munkireport/munkilib/constants.py | 18 +- .../usr/local/munkireport/munkilib/display.py | 106 +++-- .../local/munkireport/munkilib/munkilog.py | 43 +- .../munkireport/munkilib/phpserialize.py | 128 +++--- .../usr/local/munkireport/munkilib/prefs.py | 108 ++--- .../usr/local/munkireport/munkilib/purl.py | 390 ++++++++++-------- .../munkireport/munkilib/reportcommon.py | 371 ++++++++++------- .../usr/local/munkireport/munkilib/reports.py | 94 +++-- .../usr/local/munkireport/munkilib/utils.py | 106 +++-- 13 files changed, 843 insertions(+), 664 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..d23e31df2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.2.3 + hooks: + - id: check-added-large-files + args: [--maxkb=100] + - id: check-ast + - id: check-byte-order-marker + - id: check-case-conflict + # - id: check-docstring-first + # - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-xml + # - id: check-yaml + # - id: end-of-file-fixer + - id: mixed-line-ending + # - id: trailing-whitespace + # args: [--markdown-linebreak-ext=md] +- repo: https://github.com/digitalpulp/pre-commit-php + rev: 1.3.0 + hooks: + - id: php-lint-all +- repo: https://github.com/python/black + rev: 19.3b0 + hooks: + - id: black diff --git a/build/mr_upgrade.py b/build/mr_upgrade.py index 5b5f91ef1..006b4059a 100755 --- a/build/mr_upgrade.py +++ b/build/mr_upgrade.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 -""" -Script for upgrading MunkiReport. -""" +"""Script for upgrading MunkiReport.""" import argparse import datetime @@ -49,7 +47,7 @@ def run_command(args: list, suppress_output: bool = False) -> bool: def get_current_version(install_path: str) -> str: - """Return current build version""" + """Return current build version.""" helper = install_path + "app/helpers/site_helper.php" if os.path.exists(helper): try: @@ -231,7 +229,7 @@ def backup_files(backup_dir: str, install_path: str, current_time: str) -> bool: def get_versions() -> dict: - """Return MR versions""" + """Return MR versions.""" mr_api = "https://api.github.com/repos/munkireport/munkireport-php/releases" log.debug(f"Querying '{mr_api}' for latest release...") versions = {} diff --git a/build/release/make_munkireport_release.py b/build/release/make_munkireport_release.py index d5ac447f0..80d0cd7a9 100755 --- a/build/release/make_munkireport_release.py +++ b/build/release/make_munkireport_release.py @@ -13,7 +13,7 @@ # Requires an OAuth token with push access to the repo. Currently the GitHub # Releases API is in a 'preview' status, and this script does very little error # handling. -'''See docstring for main() function''' +"""See docstring for main() function.""" import json import optparse @@ -31,7 +31,7 @@ from time import strftime class GitHubAPIError(BaseException): - '''Base error for GitHub API interactions''' + """Base error for GitHub API interactions.""" pass @@ -111,17 +111,16 @@ def run_command(cmd): subprocess.check_call(cmd) def main(): - """Builds and pushes a new munkireport-php release from an existing Git clone -of munkireport-php. + """Builds and pushes a new munkireport-php release from an existing Git + clone of munkireport-php. -Requirements: + Requirements: -API token: -You'll need an API OAuth token with push access to the repo. You can create a -Personal Access Token in your user's Account Settings: -https://github.com/settings/applications - -""" + API token: + You'll need an API OAuth token with push access to the repo. You can create a + Personal Access Token in your user's Account Settings: + https://github.com/settings/applications + """ usage = __doc__ parser = optparse.OptionParser(usage=usage) parser.add_option('-t', '--token', diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/FoundationPlist.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/FoundationPlist.py index 6a5c44f2c..1a05528ef 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/FoundationPlist.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/FoundationPlist.py @@ -47,6 +47,7 @@ from Foundation import NSPropertyListSerialization from Foundation import NSPropertyListMutableContainers from Foundation import NSPropertyListXMLFormat_v1_0 + # pylint: enable=E0611 # Disable PyLint complaining about 'invalid' camelCase names @@ -54,30 +55,39 @@ class FoundationPlistException(Exception): - """Basic exception for plist errors""" + """Basic exception for plist errors.""" + pass + class NSPropertyListSerializationException(FoundationPlistException): - """Read/parse error for plists""" + """Read/parse error for plists.""" + pass + class NSPropertyListWriteException(FoundationPlistException): - """Write error for plists""" + """Write error for plists.""" + pass + def readPlist(filepath): - """ - Read a .plist file from filepath. Return the unpacked root object - (which is usually a dictionary). + """Read a .plist file from filepath. + + Return the unpacked root object (which is usually a dictionary). """ plistData = NSData.dataWithContentsOfFile_(filepath) - dataObject, dummy_plistFormat, error = ( - NSPropertyListSerialization. - propertyListFromData_mutabilityOption_format_errorDescription_( - plistData, NSPropertyListMutableContainers, None, None)) + ( + dataObject, + dummy_plistFormat, + error, + ) = NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_( + plistData, NSPropertyListMutableContainers, None, None + ) if dataObject is None: if error: - error = error.encode('ascii', 'ignore') + error = error.encode("ascii", "ignore") else: error = "Unknown error" errmsg = "%s in file %s" % (error, filepath) @@ -87,18 +97,24 @@ def readPlist(filepath): def readPlistFromString(data): - '''Read a plist data from a string. Return the root object.''' + """Read a plist data from a string. + + Return the root object. + """ try: plistData = buffer(data) except TypeError, err: raise NSPropertyListSerializationException(err) - dataObject, dummy_plistFormat, error = ( - NSPropertyListSerialization. - propertyListFromData_mutabilityOption_format_errorDescription_( - plistData, NSPropertyListMutableContainers, None, None)) + ( + dataObject, + dummy_plistFormat, + error, + ) = NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_( + plistData, NSPropertyListMutableContainers, None, None + ) if dataObject is None: if error: - error = error.encode('ascii', 'ignore') + error = error.encode("ascii", "ignore") else: error = "Unknown error" raise NSPropertyListSerializationException(error) @@ -107,16 +123,16 @@ def readPlistFromString(data): def writePlist(dataObject, filepath): - ''' - Write 'rootObject' as a plist to filepath. - ''' - plistData, error = ( - NSPropertyListSerialization. - dataFromPropertyList_format_errorDescription_( - dataObject, NSPropertyListXMLFormat_v1_0, None)) + """Write 'rootObject' as a plist to filepath.""" + ( + plistData, + error, + ) = NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_( + dataObject, NSPropertyListXMLFormat_v1_0, None + ) if plistData is None: if error: - error = error.encode('ascii', 'ignore') + error = error.encode("ascii", "ignore") else: error = "Unknown error" raise NSPropertyListSerializationException(error) @@ -125,18 +141,21 @@ def writePlist(dataObject, filepath): return else: raise NSPropertyListWriteException( - "Failed to write plist data to %s" % filepath) + "Failed to write plist data to %s" % filepath + ) def writePlistToString(rootObject): - '''Return 'rootObject' as a plist-formatted string.''' - plistData, error = ( - NSPropertyListSerialization. - dataFromPropertyList_format_errorDescription_( - rootObject, NSPropertyListXMLFormat_v1_0, None)) + """Return 'rootObject' as a plist-formatted string.""" + ( + plistData, + error, + ) = NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_( + rootObject, NSPropertyListXMLFormat_v1_0, None + ) if plistData is None: if error: - error = error.encode('ascii', 'ignore') + error = error.encode("ascii", "ignore") else: error = "Unknown error" raise NSPropertyListSerializationException(error) @@ -144,5 +163,5 @@ def writePlistToString(rootObject): return str(plistData) -if __name__ == '__main__': - print 'This is a library of support tools for the Munki Suite.' +if __name__ == "__main__": + print "This is a library of support tools for the Munki Suite." diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/constants.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/constants.py index 54029bd0a..041a431dd 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/constants.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/constants.py @@ -13,8 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" -constants.py +"""constants.py. Created by Greg Neagle on 2016-12-14. @@ -33,15 +32,16 @@ EXIT_STATUS_INVALID_PARAMETERS = 200 EXIT_STATUS_ROOT_REQUIRED = 201 -BUNDLE_ID = 'MunkiReport' +BUNDLE_ID = "MunkiReport" # the following two items are not used internally by Munki # any longer, but remain for backwards compatibility with # pre and postflight script that might access these files directly -MANAGED_INSTALLS_PLIST_PATH = '/Library/Preferences/' + BUNDLE_ID + '.plist' -SECURE_MANAGED_INSTALLS_PLIST_PATH = \ - '/private/var/root/Library/Preferences/' + BUNDLE_ID + '.plist' +MANAGED_INSTALLS_PLIST_PATH = "/Library/Preferences/" + BUNDLE_ID + ".plist" +SECURE_MANAGED_INSTALLS_PLIST_PATH = ( + "/private/var/root/Library/Preferences/" + BUNDLE_ID + ".plist" +) -ADDITIONAL_HTTP_HEADERS_KEY = 'AdditionalHttpHeaders' +ADDITIONAL_HTTP_HEADERS_KEY = "AdditionalHttpHeaders" -if __name__ == '__main__': - print 'This is a library of support tools for the Munki Suite.' +if __name__ == "__main__": + print "This is a library of support tools for the Munki Suite." diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/display.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/display.py index 66f0ceb72..580a821a5 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/display.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/display.py @@ -13,8 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" -display.py +"""display.py. Created by Greg Neagle on 2016-12-13. @@ -29,19 +28,16 @@ from . import reports - def _getsteps(num_of_steps, limit): - """ - Helper function for display_percent_done - """ + """Helper function for display_percent_done.""" steps = [] current = 0.0 for i in range(0, num_of_steps): - if i == num_of_steps-1: + if i == num_of_steps - 1: steps.append(int(round(limit))) else: steps.append(int(round(current))) - current += float(limit)/float(num_of_steps-1) + current += float(limit) / float(num_of_steps - 1) return steps @@ -54,13 +50,13 @@ def str_to_ascii(a_string): str, ascii form, no >7bit chars """ try: - return unicode(a_string).encode('ascii', 'ignore') + return unicode(a_string).encode("ascii", "ignore") except UnicodeDecodeError: - return a_string.decode('ascii', 'ignore') + return a_string.decode("ascii", "ignore") -def _to_unicode(obj, encoding='UTF-8'): - """Coerces basestring obj to unicode""" +def _to_unicode(obj, encoding="UTF-8"): + """Coerces basestring obj to unicode.""" if isinstance(obj, basestring): if not isinstance(obj, unicode): obj = unicode(obj, encoding) @@ -68,8 +64,8 @@ def _to_unicode(obj, encoding='UTF-8'): def _concat_message(msg, *args): - """Concatenates a string with any additional arguments, - making sure everything is unicode""" + """Concatenates a string with any additional arguments, making sure + everything is unicode.""" # coerce msg to unicode if it's not already msg = _to_unicode(msg) if args: @@ -79,91 +75,81 @@ def _concat_message(msg, *args): msg = msg % tuple(args) except TypeError, dummy_err: warnings.warn( - 'String format does not match concat args: %s' - % (str(sys.exc_info()))) + "String format does not match concat args: %s" % (str(sys.exc_info())) + ) return msg.rstrip() def display_info(msg, *args): - """ - Displays info messages. - """ + """Displays info messages.""" msg = _concat_message(msg, *args) - munkilog.log(u' ' + msg) + munkilog.log(u" " + msg) if verbose > 0: - print ' %s' % msg.encode('UTF-8') + print " %s" % msg.encode("UTF-8") sys.stdout.flush() def display_detail(msg, *args): - """ - Displays minor info messages. - These are usually logged only, but can be printed to - stdout if verbose is set greater than 1 + """Displays minor info messages. + + These are usually logged only, but can be printed to stdout if + verbose is set greater than 1 """ msg = _concat_message(msg, *args) if verbose > 1: - print ' %s' % msg.encode('UTF-8') + print " %s" % msg.encode("UTF-8") sys.stdout.flush() - if prefs.pref('LoggingLevel') > 0: - munkilog.log(u' ' + msg) + if prefs.pref("LoggingLevel") > 0: + munkilog.log(u" " + msg) def display_debug1(msg, *args): - """ - Displays debug messages, formatting as needed. - """ + """Displays debug messages, formatting as needed.""" msg = _concat_message(msg, *args) if verbose > 2: - print ' %s' % msg.encode('UTF-8') + print " %s" % msg.encode("UTF-8") sys.stdout.flush() - if prefs.pref('LoggingLevel') > 1: - munkilog.log('DEBUG1: %s' % msg) + if prefs.pref("LoggingLevel") > 1: + munkilog.log("DEBUG1: %s" % msg) def display_debug2(msg, *args): - """ - Displays debug messages, formatting as needed. - """ + """Displays debug messages, formatting as needed.""" msg = _concat_message(msg, *args) if verbose > 3: - print ' %s' % msg.encode('UTF-8') - if prefs.pref('LoggingLevel') > 2: - munkilog.log('DEBUG2: %s' % msg) + print " %s" % msg.encode("UTF-8") + if prefs.pref("LoggingLevel") > 2: + munkilog.log("DEBUG2: %s" % msg) def display_warning(msg, *args): - """ - Prints warning msgs to stderr and the log - """ + """Prints warning msgs to stderr and the log.""" msg = _concat_message(msg, *args) - warning = 'WARNING: %s' % msg + warning = "WARNING: %s" % msg if verbose > 0: - print >> sys.stderr, warning.encode('UTF-8') + print >> sys.stderr, warning.encode("UTF-8") munkilog.log(warning) # append this warning to our warnings log - munkilog.log(warning, 'warnings.log') + munkilog.log(warning, "warnings.log") # collect the warning for later reporting - if 'Warnings' not in reports.report: - reports.report['Warnings'] = [] - reports.report['Warnings'].append('%s' % msg) + if "Warnings" not in reports.report: + reports.report["Warnings"] = [] + reports.report["Warnings"].append("%s" % msg) def display_error(msg, *args): - """ - Prints msg to stderr and the log - """ + """Prints msg to stderr and the log.""" msg = _concat_message(msg, *args) - errmsg = 'ERROR: %s' % msg + errmsg = "ERROR: %s" % msg if verbose > 0: - print >> sys.stderr, errmsg.encode('UTF-8') + print >> sys.stderr, errmsg.encode("UTF-8") munkilog.log(errmsg) # append this error to our errors log - munkilog.log(errmsg, 'errors.log') + munkilog.log(errmsg, "errors.log") # collect the errors for later reporting - if 'Errors' not in reports.report: - reports.report['Errors'] = [] - reports.report['Errors'].append('%s' % msg) + if "Errors" not in reports.report: + reports.report["Errors"] = [] + reports.report["Errors"].append("%s" % msg) # module globals @@ -172,5 +158,5 @@ def display_error(msg, *args): # pylint: enable=invalid-name -if __name__ == '__main__': - print 'This is a library of support tools for the Munki Suite.' +if __name__ == "__main__": + print "This is a library of support tools for the Munki Suite." diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/munkilog.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/munkilog.py index 17b50592f..cb2a1a10c 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/munkilog.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/munkilog.py @@ -13,8 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" -munkilog.py +"""munkilog.py. Created by Greg Neagle on 2016-12-14. @@ -30,7 +29,7 @@ from . import prefs -def log(msg, logname=''): +def log(msg, logname=""): """Generic logging function.""" if len(msg) > 1000: # See http://bugs.python.org/issue11907 and RFC-3164 @@ -43,16 +42,16 @@ def log(msg, logname=''): logging.info(msg) # noop unless configure_syslog() is called first. # date/time format string - formatstr = '%b %d %Y %H:%M:%S %z' + formatstr = "%b %d %Y %H:%M:%S %z" if not logname: # use our regular logfile - logpath = prefs.pref('LogFile') + logpath = prefs.pref("LogFile") else: - logpath = os.path.join(os.path.dirname(prefs.pref('LogFile')), logname) + logpath = os.path.join(os.path.dirname(prefs.pref("LogFile")), logname) try: - fileobj = open(logpath, mode='a', buffering=1) + fileobj = open(logpath, mode="a", buffering=1) try: - print >> fileobj, time.strftime(formatstr), msg.encode('UTF-8') + print >> fileobj, time.strftime(formatstr), msg.encode("UTF-8") except (OSError, IOError): pass fileobj.close() @@ -72,46 +71,46 @@ def configure_syslog(): # then /var/run/syslog stops listening. If we fail to catch this then # Munki completely errors. try: - syslog = logging.handlers.SysLogHandler('/var/run/syslog') + syslog = logging.handlers.SysLogHandler("/var/run/syslog") except BaseException: - log('LogToSyslog is enabled but socket connection failed.') + log("LogToSyslog is enabled but socket connection failed.") return - syslog.setFormatter(logging.Formatter('munkireport: %(message)s')) + syslog.setFormatter(logging.Formatter("munkireport: %(message)s")) syslog.setLevel(logging.INFO) logger.addHandler(syslog) -def rotatelog(logname=''): - """Rotate a log""" +def rotatelog(logname=""): + """Rotate a log.""" if not logname: # use our regular logfile - logpath = prefs.pref('LogFile') + logpath = prefs.pref("LogFile") else: - logpath = os.path.join(os.path.dirname(prefs.pref('LogFile')), logname) + logpath = os.path.join(os.path.dirname(prefs.pref("LogFile")), logname) if os.path.exists(logpath): for i in range(3, -1, -1): try: - os.unlink(logpath + '.' + str(i + 1)) + os.unlink(logpath + "." + str(i + 1)) except (OSError, IOError): pass try: - os.rename(logpath + '.' + str(i), logpath + '.' + str(i + 1)) + os.rename(logpath + "." + str(i), logpath + "." + str(i + 1)) except (OSError, IOError): pass try: - os.rename(logpath, logpath + '.0') + os.rename(logpath, logpath + ".0") except (OSError, IOError): pass def rotate_main_log(): - """Rotate our main log""" - main_log = prefs.pref('LogFile') + """Rotate our main log.""" + main_log = prefs.pref("LogFile") if os.path.exists(main_log): if os.path.getsize(main_log) > 1000000: rotatelog(main_log) -if __name__ == '__main__': - print 'This is a library of support tools for the Munki Suite.' +if __name__ == "__main__": + print "This is a library of support tools for the Munki Suite." diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/phpserialize.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/phpserialize.py index 8e74a49d8..f611f074e 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/phpserialize.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/phpserialize.py @@ -13,17 +13,21 @@ import string import objc + class PhpSerializationError(ValueError): pass + class PhpUnserializationError(ValueError): pass + class _PhpUnserializationError(ValueError): def __init__(self, msg, rest): self.message = msg self.rest = rest + class PHP_Class(object): def __init__(self, name, properties=()): self.name = name @@ -54,14 +58,15 @@ def __eq__(self, other): class PHP_Property(object): - SEPARATOR = '\x00' + SEPARATOR = "\x00" + def __init__(self, php_name, value): self.php_name = php_name self.name = php_name.split(self.SEPARATOR)[-1] self.value = value def __repr__(self): - return 'PHP_Property(%s, %s)' % (repr(self.php_name), repr(self.value)) + return "PHP_Property(%s, %s)" % (repr(self.php_name), repr(self.value)) __str__ = __unicode__ = __repr__ @@ -71,7 +76,7 @@ def print_php_class(cls, lvl=0): raise ValueError(type(cls)) def _print(s, l=None): - print string.ljust('', l or lvl, '\t') + s + print string.ljust("", l or lvl, "\t") + s def print_list(lst): for item in lst: @@ -86,30 +91,28 @@ def print_list(lst): def print_property(prt): if isinstance(prt.value, PHP_Class): - _print('property ' + prt.name + ':') + _print("property " + prt.name + ":") print_php_class(prt.value, lvl + 1) elif isinstance(prt.value, list): if len(prt.value): - _print('property ' + prt.name + ': [') + _print("property " + prt.name + ": [") print_list(prt.value) - _print(']') + _print("]") else: - _print('property ' + prt.name + ': []') + _print("property " + prt.name + ": []") else: - _print('property ' + prt.name + ': ' + repr(prt.value)) + _print("property " + prt.name + ": " + repr(prt.value)) - _print('class ' + cls.name) + _print("class " + cls.name) lvl += 1 for prt in cls: print_property(prt) def unserialize(s): - """ - Unserialize python struct from php serialization format - """ - if not isinstance(s, basestring) or s == '': - raise ValueError('Unserialize argument must be non-empty string') + """Unserialize python struct from php serialization format.""" + if not isinstance(s, basestring) or s == "": + raise ValueError("Unserialize argument must be non-empty string") try: return Unserializator(s).unserialize() @@ -117,56 +120,56 @@ def unserialize(s): char = len(str(s)) - len(e.rest) delta = 50 try: - sample = u'...%s --> %s <-- %s...' % ( - s[(char > delta and char - delta or 0):char], + sample = u"...%s --> %s <-- %s..." % ( + s[(char > delta and char - delta or 0) : char], s[char], - s[char + 1:char + delta] + s[char + 1 : char + delta], ) - message = u'%s in %s' % (e.message, sample) + message = u"%s in %s" % (e.message, sample) except Exception, e: raise raise PhpUnserializationError(message) def serialize(struct, typecast=None): - """ - Serialize python struct into php serialization format - """ + """Serialize python struct into php serialization format.""" if typecast: struct = typecast(struct) # N; if struct is None: - return 'N;' + return "N;" struct_type = type(struct) # d:; if struct_type is float: - return 'd:%.20f;' % struct # 20 digits after comma + return "d:%.20f;" % struct # 20 digits after comma # d:; if struct_type is Decimal: - return 'd:%.20f;' % struct # 20 digits after comma + return "d:%.20f;" % struct # 20 digits after comma # b:<0 or 1>; if struct_type is bool: - return 'b:%d;' % int(struct) + return "b:%d;" % int(struct) # i:; if struct_type is int or struct_type is long: - return 'i:%d;' % struct + return "i:%d;" % struct # s::""; if struct_type is str: return 's:%d:"%s";' % (len(struct), struct) if struct_type is unicode: - return serialize(struct.encode('utf-8'), typecast) + return serialize(struct.encode("utf-8"), typecast) # a::{...} if struct_type is dict: - core = ''.join([serialize(k, typecast) + serialize(v, typecast) for k, v in struct.items()]) - return 'a:%d:{%s}' % (len(struct), core) + core = "".join( + [serialize(k, typecast) + serialize(v, typecast) for k, v in struct.items()] + ) + return "a:%d:{%s}" % (len(struct), core) if struct_type is tuple or struct_type is list: return serialize(dict(enumerate(struct)), typecast) @@ -176,13 +179,18 @@ def serialize(struct, typecast=None): len(struct.name), struct.name, len(struct), - ''.join([serialize(x.php_name, typecast) + serialize(x.value, typecast) for x in struct]), + "".join( + [ + serialize(x.php_name, typecast) + serialize(x.value, typecast) + for x in struct + ] + ), ) if struct_type is objc.pyobjc_unicode: - return serialize(struct.encode('utf-8'), typecast) + return serialize(struct.encode("utf-8"), typecast) - raise PhpSerializationError('PHP serialize: cannot encode %r' % struct) + raise PhpSerializationError("PHP serialize: cannot encode %r" % struct) class Unserializator(object): @@ -191,14 +199,16 @@ def __init__(self, s): self._str = s def await(self, symbol, n=1): - #result = self.take(len(symbol)) - result = self._str[self._position:self._position + n] + # result = self.take(len(symbol)) + result = self._str[self._position : self._position + n] self._position += n if result != symbol: - raise _PhpUnserializationError('Next is `%s` not `%s`' % (result, symbol), self.get_rest()) + raise _PhpUnserializationError( + "Next is `%s` not `%s`" % (result, symbol), self.get_rest() + ) def take(self, n=1): - result = self._str[self._position:self._position + n] + result = self._str[self._position : self._position + n] self._position += n return result @@ -206,8 +216,8 @@ def take_while_not(self, stopsymbol, typecast=None): try: stopsymbol_position = self._str.index(stopsymbol, self._position) except ValueError: - raise _PhpUnserializationError('No `%s`' % stopsymbol, self.get_rest()) - result = self._str[self._position:stopsymbol_position] + raise _PhpUnserializationError("No `%s`" % stopsymbol, self.get_rest()) + result = self._str[self._position : stopsymbol_position] self._position = stopsymbol_position + 1 if typecast is None: return result @@ -215,43 +225,43 @@ def take_while_not(self, stopsymbol, typecast=None): return typecast(result) def get_rest(self): - return self._str[self._position:] + return self._str[self._position :] def unserialize(self): t = self.take() - if t == 'N': - self.await(';') + if t == "N": + self.await(";") return None - self.await(':') + self.await(":") - if t == 'i': - return self.take_while_not(';', int) + if t == "i": + return self.take_while_not(";", int) - if t == 'd': - return self.take_while_not(';', float) + if t == "d": + return self.take_while_not(";", float) - if t == 'b': - return bool(self.take_while_not(';', int)) + if t == "b": + return bool(self.take_while_not(";", int)) - if t == 's': - size = self.take_while_not(':', int) + if t == "s": + size = self.take_while_not(":", int) self.await('"') result = self.take(size) self.await('";', 2) return result - if t == 'a': - size = self.take_while_not(':', int) + if t == "a": + size = self.take_while_not(":", int) return self.parse_hash_core(size) - if t == 'O': - object_name_size = self.take_while_not(':', int) + if t == "O": + object_name_size = self.take_while_not(":", int) self.await('"') object_name = self.take(object_name_size) self.await('":', 2) - object_length = self.take_while_not(':', int) + object_length = self.take_while_not(":", int) php_class = PHP_Class(object_name) members = self.parse_hash_core(object_length) if members: @@ -259,11 +269,11 @@ def unserialize(self): php_class.set_item(php_name, value) return php_class - raise _PhpUnserializationError('Unknown type `%s`' % t, self.get_rest()) + raise _PhpUnserializationError("Unknown type `%s`" % t, self.get_rest()) def parse_hash_core(self, size): result = {} - self.await('{') + self.await("{") is_array = True for i in range(size): k = self.unserialize() @@ -273,5 +283,5 @@ def parse_hash_core(self, size): is_array = False if is_array: result = result.values() - self.await('}') - return result \ No newline at end of file + self.await("}") + return result diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/prefs.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/prefs.py index 53543c059..3e8f7cc94 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/prefs.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/prefs.py @@ -13,8 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" -prefs.py +"""prefs.py. Created by Greg Neagle on 2016-12-13. @@ -34,6 +33,7 @@ from Foundation import kCFPreferencesAnyHost from Foundation import kCFPreferencesCurrentUser from Foundation import kCFPreferencesCurrentHost + # pylint: enable=E0611 from .constants import BUNDLE_ID @@ -43,15 +43,15 @@ ##################################################### DEFAULT_PREFS = { - 'AdditionalHttpHeaders': None, - 'ClientCertificatePath': None, - 'FollowHTTPRedirects': 'none', - 'IgnoreSystemProxies': False, - 'LogFile': '/Library/MunkiReport/Logs/MunkiReport.log', - 'LoggingLevel': 1, - 'LogToSyslog': False, - 'UseClientCertificate': False, - 'UseClientCertificateCNAsClientIdentifier': False, + "AdditionalHttpHeaders": None, + "ClientCertificatePath": None, + "FollowHTTPRedirects": "none", + "IgnoreSystemProxies": False, + "LogFile": "/Library/MunkiReport/Logs/MunkiReport.log", + "LoggingLevel": 1, + "LogToSyslog": False, + "UseClientCertificate": False, + "UseClientCertificateCNAsClientIdentifier": False, } @@ -64,50 +64,57 @@ def __init__(self, bundle_id, user=kCFPreferencesAnyUser): Args: bundle_id: str, like 'MunkiReport' """ - if bundle_id.endswith('.plist'): + if bundle_id.endswith(".plist"): bundle_id = bundle_id[:-6] self.bundle_id = bundle_id self.user = user def __iter__(self): - """Iterator for keys in the specific 'level' of preferences; this - will fail to iterate all available keys for the preferences domain - since OS X reads from multiple 'levels' and composites them.""" + """Iterator for keys in the specific 'level' of preferences; this will + fail to iterate all available keys for the preferences domain since OS + X reads from multiple 'levels' and composites them.""" keys = CFPreferencesCopyKeyList( - self.bundle_id, self.user, kCFPreferencesCurrentHost) + self.bundle_id, self.user, kCFPreferencesCurrentHost + ) if keys is not None: for i in keys: yield i def __contains__(self, pref_name): """Since this uses CFPreferencesCopyAppValue, it will find a preference - regardless of the 'level' at which it is stored""" + regardless of the 'level' at which it is stored.""" pref_value = CFPreferencesCopyAppValue(pref_name, self.bundle_id) return pref_value is not None def __getitem__(self, pref_name): - """Get a preference value. Normal OS X preference search path applies""" + """Get a preference value. + + Normal OS X preference search path applies + """ return CFPreferencesCopyAppValue(pref_name, self.bundle_id) def __setitem__(self, pref_name, pref_value): - """Sets a preference. if the user is kCFPreferencesCurrentUser, the - preference actually gets written at the 'ByHost' level due to the use - of kCFPreferencesCurrentHost""" + """Sets a preference. + + if the user is kCFPreferencesCurrentUser, the preference + actually gets written at the 'ByHost' level due to the use of + kCFPreferencesCurrentHost + """ CFPreferencesSetValue( - pref_name, pref_value, self.bundle_id, self.user, - kCFPreferencesCurrentHost) + pref_name, pref_value, self.bundle_id, self.user, kCFPreferencesCurrentHost + ) CFPreferencesAppSynchronize(self.bundle_id) def __delitem__(self, pref_name): - """Delete a preference""" + """Delete a preference.""" self.__setitem__(pref_name, None) def __repr__(self): - """Return a text representation of the class""" - return '<%s %s>' % (self.__class__.__name__, self.bundle_id) + """Return a text representation of the class.""" + return "<%s %s>" % (self.__class__.__name__, self.bundle_id) def get(self, pref_name, default=None): - """Return a preference or the default value""" + """Return a preference or the default value.""" if not pref_name in self: return default else: @@ -115,23 +122,31 @@ def get(self, pref_name, default=None): def reload_prefs(): - """Uses CFPreferencesAppSynchronize(BUNDLE_ID) - to make sure we have the latest prefs. Call this - if you have modified /Library/Preferences/MunkiReport.plist - or /var/root/Library/Preferences/MunkiReport.plist directly""" + """Uses CFPreferencesAppSynchronize(BUNDLE_ID) to make sure we have the + latest prefs. + + Call this if you have modified + /Library/Preferences/MunkiReport.plist or + /var/root/Library/Preferences/MunkiReport.plist directly + """ CFPreferencesAppSynchronize(BUNDLE_ID) def set_pref(pref_name, pref_value): - """Sets a preference, writing it to - /Library/Preferences/MunkiReport.plist. - This should normally be used only for 'bookkeeping' values; - values that control the behavior of munki may be overridden - elsewhere (by MCX, for example)""" + """Sets a preference, writing it to. + + /Library/Preferences/MunkiReport.plist. This should normally be used + only for 'bookkeeping' values; values that control the behavior of + munki may be overridden elsewhere (by MCX, for example) + """ try: CFPreferencesSetValue( - pref_name, pref_value, BUNDLE_ID, - kCFPreferencesAnyUser, kCFPreferencesCurrentHost) + pref_name, + pref_value, + BUNDLE_ID, + kCFPreferencesAnyUser, + kCFPreferencesCurrentHost, + ) CFPreferencesAppSynchronize(BUNDLE_ID) except BaseException: pass @@ -140,12 +155,13 @@ def set_pref(pref_name, pref_value): def pref(pref_name): """Return a preference. Since this uses CFPreferencesCopyAppValue, Preferences can be defined several places. Precedence is: - - MCX/configuration profile - - /var/root/Library/Preferences/ByHost/MunkiReport.XXXXXX.plist - - /var/root/Library/Preferences/MunkiReport.plist - - /Library/Preferences/MunkiReport.plist - - .GlobalPreferences defined at various levels (ByHost, user, system) - - default_prefs defined here. + + - MCX/configuration profile + - /var/root/Library/Preferences/ByHost/MunkiReport.XXXXXX.plist + - /var/root/Library/Preferences/MunkiReport.plist + - /Library/Preferences/MunkiReport.plist + - .GlobalPreferences defined at various levels (ByHost, user, system) + - default_prefs defined here. """ pref_value = CFPreferencesCopyAppValue(pref_name, BUNDLE_ID) if pref_value is None: @@ -161,5 +177,5 @@ def pref(pref_name): return pref_value -if __name__ == '__main__': - print 'This is a library of support tools for the Munki Suite.' +if __name__ == "__main__": + print "This is a library of support tools for the Munki Suite." diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/purl.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/purl.py index d4097de41..5251adb9e 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/purl.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/purl.py @@ -17,13 +17,12 @@ # # Adaptation of purl.py by Michael Lynn # https://gist.github.com/pudquick/a73d0ce7cd8730c97491 -""" -purl.py +"""purl.py. Created by Greg Neagle on 2013-11-21. Modified by Arjen van Bochoven on 2015-09-23 -curl replacement using NSURLConnection and friends +curl replacement using NSURLConnection and friends """ # builtin super doesn't work with Cocoa classes in recent PyObjC releases. @@ -40,6 +39,7 @@ from Foundation import NSLog from Foundation import NSURLCredential, NSURLCredentialPersistenceNone from Foundation import NSData, NSString + # pylint: enable=E0611 # Disable PyLint complaining about 'invalid' names @@ -50,65 +50,66 @@ # this works around an issue with App Transport Security on 10.11 bundle = NSBundle.mainBundle() info = bundle.localizedInfoDictionary() or bundle.infoDictionary() -info['NSAppTransportSecurity'] = {'NSAllowsArbitraryLoads': True} +info["NSAppTransportSecurity"] = {"NSAllowsArbitraryLoads": True} ssl_error_codes = { - -9800: u'SSL protocol error', - -9801: u'Cipher Suite negotiation failure', - -9802: u'Fatal alert', - -9803: u'I/O would block (not fatal)', - -9804: u'Attempt to restore an unknown session', - -9805: u'Connection closed gracefully', - -9806: u'Connection closed via error', - -9807: u'Invalid certificate chain', - -9808: u'Bad certificate format', - -9809: u'Underlying cryptographic error', - -9810: u'Internal error', - -9811: u'Module attach failure', - -9812: u'Valid cert chain, untrusted root', - -9813: u'Cert chain not verified by root', - -9814: u'Chain had an expired cert', - -9815: u'Chain had a cert not yet valid', - -9816: u'Server closed session with no notification', - -9817: u'Insufficient buffer provided', - -9818: u'Bad SSLCipherSuite', - -9819: u'Unexpected message received', - -9820: u'Bad MAC', - -9821: u'Decryption failed', - -9822: u'Record overflow', - -9823: u'Decompression failure', - -9824: u'Handshake failure', - -9825: u'Misc. bad certificate', - -9826: u'Bad unsupported cert format', - -9827: u'Certificate revoked', - -9828: u'Certificate expired', - -9829: u'Unknown certificate', - -9830: u'Illegal parameter', - -9831: u'Unknown Cert Authority', - -9832: u'Access denied', - -9833: u'Decoding error', - -9834: u'Decryption error', - -9835: u'Export restriction', - -9836: u'Bad protocol version', - -9837: u'Insufficient security', - -9838: u'Internal error', - -9839: u'User canceled', - -9840: u'No renegotiation allowed', - -9841: u'Peer cert is valid, or was ignored if verification disabled', - -9842: u'Server has requested a client cert', - -9843: u'Peer host name mismatch', - -9844: u'Peer dropped connection before responding', - -9845: u'Decryption failure', - -9846: u'Bad MAC', - -9847: u'Record overflow', - -9848: u'Configuration error', - -9849: u'Unexpected (skipped) record in DTLS'} + -9800: u"SSL protocol error", + -9801: u"Cipher Suite negotiation failure", + -9802: u"Fatal alert", + -9803: u"I/O would block (not fatal)", + -9804: u"Attempt to restore an unknown session", + -9805: u"Connection closed gracefully", + -9806: u"Connection closed via error", + -9807: u"Invalid certificate chain", + -9808: u"Bad certificate format", + -9809: u"Underlying cryptographic error", + -9810: u"Internal error", + -9811: u"Module attach failure", + -9812: u"Valid cert chain, untrusted root", + -9813: u"Cert chain not verified by root", + -9814: u"Chain had an expired cert", + -9815: u"Chain had a cert not yet valid", + -9816: u"Server closed session with no notification", + -9817: u"Insufficient buffer provided", + -9818: u"Bad SSLCipherSuite", + -9819: u"Unexpected message received", + -9820: u"Bad MAC", + -9821: u"Decryption failed", + -9822: u"Record overflow", + -9823: u"Decompression failure", + -9824: u"Handshake failure", + -9825: u"Misc. bad certificate", + -9826: u"Bad unsupported cert format", + -9827: u"Certificate revoked", + -9828: u"Certificate expired", + -9829: u"Unknown certificate", + -9830: u"Illegal parameter", + -9831: u"Unknown Cert Authority", + -9832: u"Access denied", + -9833: u"Decoding error", + -9834: u"Decryption error", + -9835: u"Export restriction", + -9836: u"Bad protocol version", + -9837: u"Insufficient security", + -9838: u"Internal error", + -9839: u"User canceled", + -9840: u"No renegotiation allowed", + -9841: u"Peer cert is valid, or was ignored if verification disabled", + -9842: u"Server has requested a client cert", + -9843: u"Peer host name mismatch", + -9844: u"Peer dropped connection before responding", + -9845: u"Decryption failure", + -9846: u"Bad MAC", + -9847: u"Record overflow", + -9848: u"Configuration error", + -9849: u"Unexpected (skipped) record in DTLS", +} class Purl(NSObject): - '''A class for getting content from a URL - using NSURLConnection and friends''' + """A class for getting content from a URL using NSURLConnection and + friends.""" # since we inherit from NSObject, PyLint issues a few bogus warnings # pylint: disable=W0232,E1002 @@ -118,25 +119,24 @@ class Purl(NSObject): # pylint: disable=E1101,W0201 def initWithOptions_(self, options): - '''Set up our Purl object''' + """Set up our Purl object.""" self = super(Purl, self).init() if not self: return - self.follow_redirects = options.get('follow_redirects', False) - self.url = options.get('url') - self.method = options.get('method', 'GET') - self.additional_headers = options.get('additional_headers', {}) - self.username = options.get('username') - self.password = options.get('password') - self.connection_timeout = options.get('connection_timeout', 10) - if options.get('content_type') is not None: - self.additional_headers['Content-Type'] = options.get( - 'content_type') - self.body = options.get('body') - self.response_data = '' - - self.log = options.get('logging_function', NSLog) + self.follow_redirects = options.get("follow_redirects", False) + self.url = options.get("url") + self.method = options.get("method", "GET") + self.additional_headers = options.get("additional_headers", {}) + self.username = options.get("username") + self.password = options.get("password") + self.connection_timeout = options.get("connection_timeout", 10) + if options.get("content_type") is not None: + self.additional_headers["Content-Type"] = options.get("content_type") + self.body = options.get("body") + self.response_data = "" + + self.log = options.get("logging_function", NSLog) self.response = None self.headers = None @@ -149,95 +149,104 @@ def initWithOptions_(self, options): return self def start(self): - '''Start the connection''' + """Start the connection.""" url = NSURL.URLWithString_(self.url) - request = ( - NSMutableURLRequest.requestWithURL_cachePolicy_timeoutInterval_( - url, NSURLRequestReloadIgnoringLocalCacheData, - self.connection_timeout)) + request = NSMutableURLRequest.requestWithURL_cachePolicy_timeoutInterval_( + url, NSURLRequestReloadIgnoringLocalCacheData, self.connection_timeout + ) if self.additional_headers: for header, value in self.additional_headers.items(): request.setValue_forHTTPHeaderField_(value, header) request.setHTTPMethod_(self.method) - - if self.method == 'POST': + + if self.method == "POST": body_unicode = unicode(self.body) - body_data = NSData.dataWithBytes_length_(NSString.stringWithString_( - body_unicode).UTF8String(), len(body_unicode.encode('utf-8'))) + body_data = NSData.dataWithBytes_length_( + NSString.stringWithString_(body_unicode).UTF8String(), + len(body_unicode.encode("utf-8")), + ) request.setHTTPBody_(body_data) self.connection = NSURLConnection.alloc().initWithRequest_delegate_( - request, self) + request, self + ) def cancel(self): - '''Cancel the connection''' + """Cancel the connection.""" if self.connection: self.connection.cancel() self.done = True def isDone(self): - '''Check if the connection request is complete. As a side effect, - allow the delegates to work my letting the run loop run for a bit''' + """Check if the connection request is complete. + + As a side effect, allow the delegates to work my letting the run + loop run for a bit + """ if self.done: return self.done # let the delegates do their thing NSRunLoop.currentRunLoop().runUntilDate_( - NSDate.dateWithTimeIntervalSinceNow_(.1)) + NSDate.dateWithTimeIntervalSinceNow_(0.1) + ) return self.done def get_response_data(self): - '''Return response data''' + """Return response data.""" return self.response_data def connection_didFailWithError_(self, connection, error): - '''NSURLConnection delegate method - Sent when a connection fails to load its request successfully.''' + """NSURLConnection delegate method Sent when a connection fails to load + its request successfully.""" # we don't actually use the connection argument, so # pylint: disable=W0613 self.error = error # If this was an SSL error, try to extract the SSL error code. - if 'NSUnderlyingError' in error.userInfo(): - ssl_code = error.userInfo()['NSUnderlyingError'].userInfo().get( - '_kCFNetworkCFStreamSSLErrorOriginalValue', None) + if "NSUnderlyingError" in error.userInfo(): + ssl_code = ( + error.userInfo()["NSUnderlyingError"] + .userInfo() + .get("_kCFNetworkCFStreamSSLErrorOriginalValue", None) + ) if ssl_code: - self.SSLerror = (ssl_code, ssl_error_codes.get( - ssl_code, 'Unknown SSL error')) + self.SSLerror = ( + ssl_code, + ssl_error_codes.get(ssl_code, "Unknown SSL error"), + ) self.done = True - def connectionDidFinishLoading_(self, connection): - '''NSURLConnectionDataDelegat delegate method - Sent when a connection has finished loading successfully.''' + """NSURLConnectionDataDelegat delegate method Sent when a connection + has finished loading successfully.""" # we don't actually use the connection argument, so # pylint: disable=W0613 self.done = True - def connection_didReceiveResponse_(self, connection, response): - '''NSURLConnectionDataDelegate delegate method - Sent when the connection has received sufficient data to construct the - URL response for its request.''' + """NSURLConnectionDataDelegate delegate method Sent when the connection + has received sufficient data to construct the URL response for its + request.""" # we don't actually use the connection argument, so # pylint: disable=W0613 self.response = response - if response.className() == u'NSHTTPURLResponse': + if response.className() == u"NSHTTPURLResponse": # Headers and status code only available for HTTP/S transfers self.status = response.statusCode() self.headers = dict(response.allHeaderFields()) - def connection_willSendRequest_redirectResponse_( - self, connection, request, response): - '''NSURLConnectionDataDelegate delegate method - Sent when the connection determines that it must change URLs in order to - continue loading a request.''' + self, connection, request, response + ): + """NSURLConnectionDataDelegate delegate method Sent when the connection + determines that it must change URLs in order to continue loading a + request.""" # we don't actually use the connection argument, so # pylint: disable=W0613 @@ -254,129 +263,160 @@ def connection_willSendRequest_redirectResponse_( self.redirection.append([newURL, dict(response.allHeaderFields())]) if self.follow_redirects: # Allow the redirect - self.log('Allowing redirect to: %s' % newURL) + self.log("Allowing redirect to: %s" % newURL) return request else: # Deny the redirect - self.log('Denying redirect to: %s' % newURL) + self.log("Denying redirect to: %s" % newURL) return None def connection_willSendRequestForAuthenticationChallenge_( - self, connection, challenge): - '''NSURLConnection delegate method - Tells the delegate that the connection will send a request for an - authentication challenge. - New in 10.7.''' + self, connection, challenge + ): + """NSURLConnection delegate method Tells the delegate that the + connection will send a request for an authentication challenge. + + New in 10.7. + """ # we don't actually use the connection argument, so # pylint: disable=W0613 - self.log('connection_willSendRequestForAuthenticationChallenge_') + self.log("connection_willSendRequestForAuthenticationChallenge_") protectionSpace = challenge.protectionSpace() host = protectionSpace.host() realm = protectionSpace.realm() authenticationMethod = protectionSpace.authenticationMethod() self.log( - 'Authentication challenge for Host: %s Realm: %s AuthMethod: %s' - % (host, realm, authenticationMethod)) + "Authentication challenge for Host: %s Realm: %s AuthMethod: %s" + % (host, realm, authenticationMethod) + ) if challenge.previousFailureCount() > 0: # we have the wrong credentials. just fail - self.log('Previous authentication attempt failed.') + self.log("Previous authentication attempt failed.") challenge.sender().cancelAuthenticationChallenge_(challenge) - if self.username and self.password and authenticationMethod in [ - 'NSURLAuthenticationMethodDefault', - 'NSURLAuthenticationMethodHTTPBasic', - 'NSURLAuthenticationMethodHTTPDigest']: - self.log('Will attempt to authenticate.') - self.log('Username: %s Password: %s' - % (self.username, ('*' * len(self.password or '')))) - credential = ( - NSURLCredential.credentialWithUser_password_persistence_( - self.username, self.password, - NSURLCredentialPersistenceNone)) + if ( + self.username + and self.password + and authenticationMethod + in [ + "NSURLAuthenticationMethodDefault", + "NSURLAuthenticationMethodHTTPBasic", + "NSURLAuthenticationMethodHTTPDigest", + ] + ): + self.log("Will attempt to authenticate.") + self.log( + "Username: %s Password: %s" + % (self.username, ("*" * len(self.password or ""))) + ) + credential = NSURLCredential.credentialWithUser_password_persistence_( + self.username, self.password, NSURLCredentialPersistenceNone + ) challenge.sender().useCredential_forAuthenticationChallenge_( - credential, challenge) + credential, challenge + ) else: # fall back to system-provided default behavior - self.log('Allowing OS to handle authentication request') - challenge.sender( - ).performDefaultHandlingForAuthenticationChallenge_( - challenge) + self.log("Allowing OS to handle authentication request") + challenge.sender().performDefaultHandlingForAuthenticationChallenge_( + challenge + ) def connection_canAuthenticateAgainstProtectionSpace_( - self, connection, protectionSpace): - '''NSURLConnection delegate method - Sent to determine whether the delegate is able to respond to a - protection space’s form of authentication. - Deprecated in 10.10''' + self, connection, protectionSpace + ): + """NSURLConnection delegate method Sent to determine whether the + delegate is able to respond to a protection space’s form of + authentication. + + Deprecated in 10.10 + """ # we don't actually use the connection argument, so # pylint: disable=W0613 # this is not called in 10.5.x. - self.log('connection_canAuthenticateAgainstProtectionSpace_') + self.log("connection_canAuthenticateAgainstProtectionSpace_") if protectionSpace: host = protectionSpace.host() realm = protectionSpace.realm() authenticationMethod = protectionSpace.authenticationMethod() - self.log('Protection space found. Host: %s Realm: %s AuthMethod: %s' - % (host, realm, authenticationMethod)) - if self.username and self.password and authenticationMethod in [ - 'NSURLAuthenticationMethodDefault', - 'NSURLAuthenticationMethodHTTPBasic', - 'NSURLAuthenticationMethodHTTPDigest']: + self.log( + "Protection space found. Host: %s Realm: %s AuthMethod: %s" + % (host, realm, authenticationMethod) + ) + if ( + self.username + and self.password + and authenticationMethod + in [ + "NSURLAuthenticationMethodDefault", + "NSURLAuthenticationMethodHTTPBasic", + "NSURLAuthenticationMethodHTTPDigest", + ] + ): # we know how to handle this - self.log('Can handle this authentication request') + self.log("Can handle this authentication request") return True # we don't know how to handle this; let the OS try - self.log('Allowing OS to handle authentication request') + self.log("Allowing OS to handle authentication request") return False - def connection_didReceiveAuthenticationChallenge_( - self, connection, challenge): - '''NSURLConnection delegate method - Sent when a connection must authenticate a challenge in order to - download its request. - Deprecated in 10.10''' + def connection_didReceiveAuthenticationChallenge_(self, connection, challenge): + """NSURLConnection delegate method Sent when a connection must + authenticate a challenge in order to download its request. + + Deprecated in 10.10 + """ # we don't actually use the connection argument, so # pylint: disable=W0613 - self.log('connection_didReceiveAuthenticationChallenge_') + self.log("connection_didReceiveAuthenticationChallenge_") protectionSpace = challenge.protectionSpace() host = protectionSpace.host() realm = protectionSpace.realm() authenticationMethod = protectionSpace.authenticationMethod() self.log( - 'Authentication challenge for Host: %s Realm: %s AuthMethod: %s' - % (host, realm, authenticationMethod)) + "Authentication challenge for Host: %s Realm: %s AuthMethod: %s" + % (host, realm, authenticationMethod) + ) if challenge.previousFailureCount() > 0: # we have the wrong credentials. just fail - self.log('Previous authentication attempt failed.') + self.log("Previous authentication attempt failed.") challenge.sender().cancelAuthenticationChallenge_(challenge) - if self.username and self.password and authenticationMethod in [ - 'NSURLAuthenticationMethodDefault', - 'NSURLAuthenticationMethodHTTPBasic', - 'NSURLAuthenticationMethodHTTPDigest']: - self.log('Will attempt to authenticate.') - self.log('Username: %s Password: %s' - % (self.username, ('*' * len(self.password or '')))) - credential = ( - NSURLCredential.credentialWithUser_password_persistence_( - self.username, self.password, - NSURLCredentialPersistenceNone)) + if ( + self.username + and self.password + and authenticationMethod + in [ + "NSURLAuthenticationMethodDefault", + "NSURLAuthenticationMethodHTTPBasic", + "NSURLAuthenticationMethodHTTPDigest", + ] + ): + self.log("Will attempt to authenticate.") + self.log( + "Username: %s Password: %s" + % (self.username, ("*" * len(self.password or ""))) + ) + credential = NSURLCredential.credentialWithUser_password_persistence_( + self.username, self.password, NSURLCredentialPersistenceNone + ) challenge.sender().useCredential_forAuthenticationChallenge_( - credential, challenge) + credential, challenge + ) else: # fall back to system-provided default behavior - self.log('Continuing without credential.') - challenge.sender( - ).continueWithoutCredentialForAuthenticationChallenge_( - challenge) + self.log("Continuing without credential.") + challenge.sender().continueWithoutCredentialForAuthenticationChallenge_( + challenge + ) def connection_didReceiveData_(self, connection, data): - '''NSURLConnectionDataDelegate method - Sent as a connection loads data incrementally''' + """NSURLConnectionDataDelegate method Sent as a connection loads data + incrementally.""" # we don't actually use the connection argument, so # pylint: disable=W0613 diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py index 82a57c3e8..2f9a9a859 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reportcommon.py @@ -31,39 +31,38 @@ from Foundation import kCFPreferencesCurrentHost from Foundation import NSHTTPURLResponse from SystemConfiguration import SCDynamicStoreCopyConsoleUser + # pylint: enable=E0611 # our preferences "bundle_id" -BUNDLE_ID = 'MunkiReport' +BUNDLE_ID = "MunkiReport" + class CurlError(Exception): def __init__(self, status, message): display_error(message) finish_run() + def set_verbosity(level): - """ - Set verbosity level - """ + """Set verbosity level.""" display.verbose = int(level) + def display_error(msg, *args): - """ - Call display error msg handler - """ - display.display_error('%s' % msg, *args) + """Call display error msg handler.""" + display.display_error("%s" % msg, *args) + def display_warning(msg, *args): - """ - Call display warning msg handler - """ - display.display_warning('%s' % msg, *args) + """Call display warning msg handler.""" + display.display_warning("%s" % msg, *args) + def display_detail(msg, *args): - """ - Call display detail msg handler - """ - display.display_detail('%s' % msg, *args) + """Call display detail msg handler.""" + display.display_detail("%s" % msg, *args) + def finish_run(): remove_run_file() @@ -84,20 +83,22 @@ def curl(url, values): options["body"] = urlencode(values) options["logging_function"] = display_detail options["connection_timeout"] = 60 - if pref('UseMunkiAdditionalHttpHeaders'): - custom_headers = prefs.pref( - constants.ADDITIONAL_HTTP_HEADERS_KEY) + if pref("UseMunkiAdditionalHttpHeaders"): + custom_headers = prefs.pref(constants.ADDITIONAL_HTTP_HEADERS_KEY) if custom_headers: options["additional_headers"] = dict() for header in custom_headers: - m = re.search(r'^(?P.*?): (?P.*?)$', - header) + m = re.search(r"^(?P.*?): (?P.*?)$", header) if m: - options["additional_headers"][ - m.group('header_name')] = m.group('header_value') + options["additional_headers"][m.group("header_name")] = m.group( + "header_value" + ) else: - raise CurlError(-1, 'UseMunkiAdditionalHttpHeaders defined, ' - 'but not found in Munki preferences') + raise CurlError( + -1, + "UseMunkiAdditionalHttpHeaders defined, " + "but not found in Munki preferences", + ) # Build Purl with initial settings connection = Purl.alloc().initWithOptions_(options) @@ -113,7 +114,7 @@ def curl(url, values): # safely kill the connection then re-raise connection.cancel() raise - except Exception, err: # too general, I know + except Exception, err: # too general, I know # Let us out! ... Safely! Unexpectedly quit dialogs are annoying... connection.cancel() # Re-raise the error as a GurlError @@ -122,159 +123,204 @@ def curl(url, values): if connection.error != None: # Gurl returned an error display.display_detail( - 'Download error %s: %s', connection.error.code(), - connection.error.localizedDescription()) + "Download error %s: %s", + connection.error.code(), + connection.error.localizedDescription(), + ) if connection.SSLerror: - display_detail( - 'SSL error detail: %s', str(connection.SSLerror)) - display_detail('Headers: %s', connection.headers) - raise CurlError(connection.error.code(), - connection.error.localizedDescription()) + display_detail("SSL error detail: %s", str(connection.SSLerror)) + display_detail("Headers: %s", connection.headers) + raise CurlError( + connection.error.code(), connection.error.localizedDescription() + ) if connection.response != None and connection.status != 200: - display.display_detail('Status: %s', connection.status) - display.display_detail('Headers: %s', connection.headers) + display.display_detail("Status: %s", connection.status) + display.display_detail("Headers: %s", connection.headers) if connection.redirection != []: - display.display_detail('Redirection: %s', connection.redirection) + display.display_detail("Redirection: %s", connection.redirection) - connection.headers['http_result_code'] = str(connection.status) - description = NSHTTPURLResponse.localizedStringForStatusCode_( - connection.status) - connection.headers['http_result_description'] = description + connection.headers["http_result_code"] = str(connection.status) + description = NSHTTPURLResponse.localizedStringForStatusCode_(connection.status) + connection.headers["http_result_description"] = description - if str(connection.status).startswith('2'): + if str(connection.status).startswith("2"): return connection.get_response_data() else: # there was an HTTP error of some sort. - raise CurlError(connection.status, - '%s failed, HTTP returncode %s (%s)' % ( + raise CurlError( + connection.status, + "%s failed, HTTP returncode %s (%s)" + % ( url, connection.status, - connection.headers.get('http_result_description', 'Failed'))) + connection.headers.get("http_result_description", "Failed"), + ), + ) + def get_hardware_info(): - '''Uses system profiler to get hardware info for this machine''' - cmd = ['/usr/sbin/system_profiler', 'SPHardwareDataType', '-xml'] - proc = subprocess.Popen(cmd, shell=False, bufsize=-1, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + """Uses system profiler to get hardware info for this machine.""" + cmd = ["/usr/sbin/system_profiler", "SPHardwareDataType", "-xml"] + proc = subprocess.Popen( + cmd, + shell=False, + bufsize=-1, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) (output, dummy_error) = proc.communicate() try: plist = FoundationPlist.readPlistFromString(output) # system_profiler xml is an array sp_dict = plist[0] - items = sp_dict['_items'] + items = sp_dict["_items"] sp_hardware_dict = items[0] return sp_hardware_dict except BaseException: return {} + def get_long_username(username): try: long_name = pwd.getpwnam(username)[4] except: - long_name = '' - return long_name.decode('utf-8') + long_name = "" + return long_name.decode("utf-8") + def get_uid(username): try: uid = pwd.getpwnam(username)[2] except: - uid = '' + uid = "" return uid + def get_computername(): - cmd = ['/usr/sbin/scutil', '--get', 'ComputerName'] - proc = subprocess.Popen(cmd, shell=False, bufsize=-1, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = ["/usr/sbin/scutil", "--get", "ComputerName"] + proc = subprocess.Popen( + cmd, + shell=False, + bufsize=-1, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) (output, unused_error) = proc.communicate() output = output.strip() - return output.decode('utf-8') + return output.decode("utf-8") + def get_cpuinfo(): - cmd = ['/usr/sbin/sysctl', '-n', 'machdep.cpu.brand_string'] - proc = subprocess.Popen(cmd, shell=False, bufsize=-1, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = ["/usr/sbin/sysctl", "-n", "machdep.cpu.brand_string"] + proc = subprocess.Popen( + cmd, + shell=False, + bufsize=-1, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) (output, unused_error) = proc.communicate() output = output.strip() - return output.decode('utf-8') + return output.decode("utf-8") + def get_buildversion(): - cmd = ['/usr/bin/sw_vers', '-buildVersion'] - proc = subprocess.Popen(cmd, shell=False, bufsize=-1, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = ["/usr/bin/sw_vers", "-buildVersion"] + proc = subprocess.Popen( + cmd, + shell=False, + bufsize=-1, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) (output, unused_error) = proc.communicate() output = output.strip() - return output.decode('utf-8') + return output.decode("utf-8") + def get_uptime(): - cmd = ['/usr/sbin/sysctl', '-n', 'kern.boottime'] - proc = subprocess.Popen(cmd, shell=False, bufsize=-1, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = ["/usr/sbin/sysctl", "-n", "kern.boottime"] + proc = subprocess.Popen( + cmd, + shell=False, + bufsize=-1, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) (output, unused_error) = proc.communicate() - sec = int(re.sub('.*sec = (\d+),.*', '\\1', output)) + sec = int(re.sub(".*sec = (\d+),.*", "\\1", output)) up = int(time.time() - sec) return up if up > 0 else -1 def set_pref(pref_name, pref_value): - """Sets a preference, See prefs.py for details""" + """Sets a preference, See prefs.py for details.""" CFPreferencesSetValue( - pref_name, pref_value, BUNDLE_ID, - kCFPreferencesAnyUser, kCFPreferencesCurrentHost) + pref_name, + pref_value, + BUNDLE_ID, + kCFPreferencesAnyUser, + kCFPreferencesCurrentHost, + ) CFPreferencesAppSynchronize(BUNDLE_ID) print "set pref" try: CFPreferencesSetValue( - pref_name, pref_value, BUNDLE_ID, - kCFPreferencesAnyUser, kCFPreferencesCurrentHost) + pref_name, + pref_value, + BUNDLE_ID, + kCFPreferencesAnyUser, + kCFPreferencesCurrentHost, + ) CFPreferencesAppSynchronize(BUNDLE_ID) except Exception: pass def pref(pref_name): - """Return a preference. See prefs.py for details + """Return a preference. + + See prefs.py for details """ pref_value = CFPreferencesCopyAppValue(pref_name, BUNDLE_ID) return pref_value + def process(serial, items): - """Process receives a list of items, checks if they need updating - and updates them if necessary""" + """Process receives a list of items, checks if they need updating and + updates them if necessary.""" # Sanitize serial - serial = ''.join([c for c in serial if c.isalnum()]) + serial = "".join([c for c in serial if c.isalnum()]) # Get prefs - baseurl = pref('BaseUrl') or \ - prefs.pref('SoftwareRepoURL') + '/report/' + baseurl = pref("BaseUrl") or prefs.pref("SoftwareRepoURL") + "/report/" hashurl = baseurl + "index.php?/report/hash_check" checkurl = baseurl + "index.php?/report/check_in" # Get passphrase - passphrase = pref('Passphrase') + passphrase = pref("Passphrase") # Get hashes for all scripts for key, i in items.items(): - if i.get('path'): - i['hash'] = getmd5hash(i.get('path')) + if i.get("path"): + i["hash"] = getmd5hash(i.get("path")) # Check dict check = {} for key, i in items.items(): - if i.get('hash'): - check[key] = {'hash': i.get('hash')} + if i.get("hash"): + check[key] = {"hash": i.get("hash")} # Send hashes to server - values = {'serial': serial,\ - 'items': serialize(check),\ - 'passphrase' : passphrase} + values = {"serial": serial, "items": serialize(check), "passphrase": passphrase} server_data = curl(hashurl, values) # = response.read() @@ -282,48 +328,51 @@ def process(serial, items): try: result = unserialize(server_data) except Exception, e: - display_error('Could not unserialize server data: %s' % str(e)) - display_error('Request: %s' % str(values)) - display_error('Response: %s' % str(server_data)) + display_error("Could not unserialize server data: %s" % str(e)) + display_error("Request: %s" % str(values)) + display_error("Response: %s" % str(server_data)) return -1 - if result.get('error') != '': - display_error('Server error: %s' % result['error']) + if result.get("error") != "": + display_error("Server error: %s" % result["error"]) return -1 - if result.get('info') != '': - display_detail('Server info: %s' % result['info']) + if result.get("info") != "": + display_detail("Server info: %s" % result["info"]) # Retrieve hashes that need updating total_size = 0 for i in items.keys(): if i in result: - if items[i].get('path'): + if items[i].get("path"): try: - f = open(items[i]['path'], "r") - items[i]['data'] = f.read() + f = open(items[i]["path"], "r") + items[i]["data"] = f.read() except: - display_warning("Can't open %s" % items[i]['path']) + display_warning("Can't open %s" % items[i]["path"]) del items[i] continue - size = len(items[i]['data']) - display_detail('Need to update %s (%s)' % (i, sizeof_fmt(size))) + size = len(items[i]["data"]) + display_detail("Need to update %s (%s)" % (i, sizeof_fmt(size))) total_size = total_size + size - else: # delete items that don't have to be uploaded + else: # delete items that don't have to be uploaded del items[i] # Send new files with hashes if len(items): - display_detail('Sending items (%s)' % sizeof_fmt(total_size)) - response = curl(checkurl, {'serial': serial,\ - 'items': serialize(items),\ - 'passphrase': passphrase}) + display_detail("Sending items (%s)" % sizeof_fmt(total_size)) + response = curl( + checkurl, + {"serial": serial, "items": serialize(items), "passphrase": passphrase}, + ) display_detail(response) else: - display_detail('No changes') + display_detail("No changes") -def runExternalScriptWithTimeout(script, allow_insecure=False,\ - script_args=(), timeout=30): + +def runExternalScriptWithTimeout( + script, allow_insecure=False, script_args=(), timeout=30 +): """Run a script (e.g. preflight/postflight) and return its exit status. Args: @@ -339,29 +388,37 @@ def runExternalScriptWithTimeout(script, allow_insecure=False,\ from munkilib import utils if not os.path.exists(script): - raise ScriptNotFoundError('script does not exist: %s' % script) + raise ScriptNotFoundError("script does not exist: %s" % script) if not allow_insecure: try: utils.verifyFileOnlyWritableByMunkiAndRoot(script) except utils.VerifyFilePermissionsError, e: - msg = ('Skipping execution due to failed file permissions ' - 'verification: %s\n%s' % (script, str(e))) + msg = ( + "Skipping execution due to failed file permissions " + "verification: %s\n%s" % (script, str(e)) + ) raise utils.RunExternalScriptError(msg) if os.access(script, os.X_OK): cmd = [script] if script_args: cmd.extend(script_args) - proc = subprocess.Popen(cmd, shell=False, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + proc = subprocess.Popen( + cmd, + shell=False, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) while timeout > 0: if proc.poll() is not None: (stdout, stderr) = proc.communicate() - return proc.returncode, stdout.decode('UTF-8', 'replace'), \ - stderr.decode('UTF-8', 'replace') + return ( + proc.returncode, + stdout.decode("UTF-8", "replace"), + stderr.decode("UTF-8", "replace"), + ) time.sleep(0.1) timeout -= 0.1 else: @@ -370,32 +427,30 @@ def runExternalScriptWithTimeout(script, allow_insecure=False,\ except OSError, e: if e.errno != 3: raise - raise utils.RunExternalScriptError('%s timed out' % script) + raise utils.RunExternalScriptError("%s timed out" % script) return (0, None, None) else: - raise utils.RunExternalScriptError('%s not executable' % script) + raise utils.RunExternalScriptError("%s not executable" % script) -def rundir(scriptdir, runtype, abort=False, submitscript=''): - """ - Run scripts in directory scriptdir - runtype is passed to the script - if abort is True, a non-zero exit status will abort munki - submitscript is put at the end of the scriptlist - """ + +def rundir(scriptdir, runtype, abort=False, submitscript=""): + """Run scripts in directory scriptdir runtype is passed to the script if + abort is True, a non-zero exit status will abort munki submitscript is put + at the end of the scriptlist.""" if os.path.exists(scriptdir): from munkilib import utils # Get timeout for scripts scriptTimeOut = 30 - if pref('scriptTimeOut'): - scriptTimeOut = int(pref('scriptTimeOut')) - display_detail('# Set custom script timeout to %s seconds' % scriptTimeOut) + if pref("scriptTimeOut"): + scriptTimeOut = int(pref("scriptTimeOut")) + display_detail("# Set custom script timeout to %s seconds" % scriptTimeOut) # Directory containing the scripts parentdir = os.path.basename(scriptdir) - display_detail('# Executing scripts in %s' % parentdir) + display_detail("# Executing scripts in %s" % parentdir) # Get all files in scriptdir files = os.listdir(scriptdir) @@ -409,12 +464,12 @@ def rundir(scriptdir, runtype, abort=False, submitscript=''): sub = files.pop(files.index(submitscript)) files.append(sub) except Exception, e: - display_error('%s not found in %s' % (submitscript, parentdir)) + display_error("%s not found in %s" % (submitscript, parentdir)) for script in files: # Skip files that start with a period - if script.startswith('.'): + if script.startswith("."): continue # Concatenate dir and filename @@ -426,40 +481,40 @@ def rundir(scriptdir, runtype, abort=False, submitscript=''): try: # Attempt to execute script - display_detail('Running %s' % script) + display_detail("Running %s" % script) result, stdout, stderr = runExternalScriptWithTimeout( - scriptpath, \ - allow_insecure=False, \ - script_args=[runtype], \ - timeout=scriptTimeOut) + scriptpath, + allow_insecure=False, + script_args=[runtype], + timeout=scriptTimeOut, + ) if stdout: display_detail(stdout) if stderr: - display_detail('%s Error: %s' % (script, stderr)) + display_detail("%s Error: %s" % (script, stderr)) if result: if abort: - display_detail('Aborted by %s' % script) + display_detail("Aborted by %s" % script) exit(1) else: - display_warning('%s return code: %d'\ - % (script, result)) + display_warning("%s return code: %d" % (script, result)) except utils.ScriptNotFoundError: pass # Script has disappeared - pass. except Exception, e: display_warning("%s: %s" % (script, str(e))) + def sizeof_fmt(num): - for unit in ['B','KB','MB','GB','TB','PB','EB','ZB']: + for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]: if abs(num) < 1000.0: return "%.0f%s" % (num, unit) num /= 1000.0 - return "%.1f%s" % (num, 'YB') + return "%.1f%s" % (num, "YB") def gethash(filename, hash_function): - """ - Calculates the hashvalue of the given file with the given hash_function. + """Calculates the hashvalue of the given file with the given hash_function. Args: filename: The file name to calculate the hash value of. @@ -470,11 +525,11 @@ def gethash(filename, hash_function): The hashvalue of the given file as hex string. """ if not os.path.isfile(filename): - return 'NOT A FILE' + return "NOT A FILE" - fileref = open(filename, 'rb') + fileref = open(filename, "rb") while 1: - chunk = fileref.read(2**16) + chunk = fileref.read(2 ** 16) if not chunk: break hash_function.update(chunk) @@ -483,9 +538,7 @@ def gethash(filename, hash_function): def getmd5hash(filename): - """ - Returns hex of MD5 checksum of a file - """ + """Returns hex of MD5 checksum of a file.""" hash_function = hashlib.md5() return gethash(filename, hash_function) @@ -497,17 +550,17 @@ def getOsVersion(only_major_minor=True, as_tuple=False): only_major_minor: Boolean. If True, only include major/minor versions. as_tuple: Boolean. If True, return a tuple of ints, otherwise a string. """ - os_version_tuple = platform.mac_ver()[0].split('.') + os_version_tuple = platform.mac_ver()[0].split(".") if only_major_minor: os_version_tuple = os_version_tuple[0:2] if as_tuple: return tuple(map(int, os_version_tuple)) else: - return '.'.join(os_version_tuple) + return ".".join(os_version_tuple) def getconsoleuser(): - """Return console user""" + """Return console user.""" cfuser = SCDynamicStoreCopyConsoleUser(None, None, None) return cfuser[0] diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reports.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reports.py index 555d3aadd..5218f26a6 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reports.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/reports.py @@ -13,8 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" -reports.py +"""reports.py. Created by Greg Neagle on 2016-12-14. @@ -31,6 +30,7 @@ # No name 'Foo' in module 'Bar' warnings. Disable them. # pylint: disable=E0611 from Foundation import NSDate + # pylint: enable=E0611 from . import munkilog @@ -41,7 +41,9 @@ def format_time(timestamp=None): """Return timestamp as an ISO 8601 formatted string, in the current timezone. - If timestamp isn't given the current time is used.""" + + If timestamp isn't given the current time is used. + """ if timestamp is None: return str(NSDate.new()) else: @@ -49,44 +51,46 @@ def format_time(timestamp=None): def printreportitem(label, value, indent=0): - """Prints a report item in an 'attractive' way""" - indentspace = ' ' + """Prints a report item in an 'attractive' way.""" + indentspace = " " if type(value) == type(None): - print indentspace*indent, '%s: !NONE!' % label - elif type(value) == list or type(value).__name__ == 'NSCFArray': + print indentspace * indent, "%s: !NONE!" % label + elif type(value) == list or type(value).__name__ == "NSCFArray": if label: - print indentspace*indent, '%s:' % label + print indentspace * indent, "%s:" % label index = 0 for item in value: index += 1 - printreportitem(index, item, indent+1) - elif type(value) == dict or type(value).__name__ == 'NSCFDictionary': + printreportitem(index, item, indent + 1) + elif type(value) == dict or type(value).__name__ == "NSCFDictionary": if label: - print indentspace*indent, '%s:' % label + print indentspace * indent, "%s:" % label for subkey in value.keys(): - printreportitem(subkey, value[subkey], indent+1) + printreportitem(subkey, value[subkey], indent + 1) else: - print indentspace*indent, '%s: %s' % (label, value) + print indentspace * indent, "%s: %s" % (label, value) def printreport(reportdict): - """Prints the report dictionary in a pretty(?) way""" + """Prints the report dictionary in a pretty(?) way.""" for key in reportdict.keys(): printreportitem(key, reportdict[key]) def savereport(): - """Save our report""" + """Save our report.""" FoundationPlist.writePlist( - report, os.path.join( - prefs.pref('ManagedInstallDir'), 'ManagedInstallReport.plist')) + report, + os.path.join(prefs.pref("ManagedInstallDir"), "ManagedInstallReport.plist"), + ) def readreport(): - """Read report data from file""" + """Read report data from file.""" global report reportfile = os.path.join( - prefs.pref('ManagedInstallDir'), 'ManagedInstallReport.plist') + prefs.pref("ManagedInstallDir"), "ManagedInstallReport.plist" + ) try: report = FoundationPlist.readPlist(reportfile) except FoundationPlist.NSPropertyListSerializationException: @@ -95,42 +99,52 @@ def readreport(): def _warn(msg): """We can't use display module functions here because that would require - circular imports. So a partial reimplementation.""" - warning = 'WARNING: %s' % msg - print >> sys.stderr, warning.encode('UTF-8') + circular imports. + + So a partial reimplementation. + """ + warning = "WARNING: %s" % msg + print >> sys.stderr, warning.encode("UTF-8") munkilog.log(warning) # append this warning to our warnings log - munkilog.log(warning, 'warnings.log') + munkilog.log(warning, "warnings.log") def archive_report(): - """Archive a report""" + """Archive a report.""" reportfile = os.path.join( - prefs.pref('ManagedInstallDir'), 'ManagedInstallReport.plist') + prefs.pref("ManagedInstallDir"), "ManagedInstallReport.plist" + ) if os.path.exists(reportfile): modtime = os.stat(reportfile).st_mtime - formatstr = '%Y-%m-%d-%H%M%S' - archivename = ('ManagedInstallReport-%s.plist' - % time.strftime(formatstr, time.localtime(modtime))) - archivepath = os.path.join(prefs.pref('ManagedInstallDir'), 'Archives') + formatstr = "%Y-%m-%d-%H%M%S" + archivename = "ManagedInstallReport-%s.plist" % time.strftime( + formatstr, time.localtime(modtime) + ) + archivepath = os.path.join(prefs.pref("ManagedInstallDir"), "Archives") if not os.path.exists(archivepath): try: os.mkdir(archivepath) except (OSError, IOError): - _warn('Could not create report archive path.') + _warn("Could not create report archive path.") try: os.rename(reportfile, os.path.join(archivepath, archivename)) except (OSError, IOError): - _warn('Could not archive report.') + _warn("Could not archive report.") # now keep number of archived reports to 100 or fewer - proc = subprocess.Popen(['/bin/ls', '-t1', archivepath], - bufsize=1, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + proc = subprocess.Popen( + ["/bin/ls", "-t1", archivepath], + bufsize=1, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) (output, dummy_err) = proc.communicate() if output: - archiveitems = [item - for item in str(output).splitlines() - if item.startswith('ManagedInstallReport-')] + archiveitems = [ + item + for item in str(output).splitlines() + if item.startswith("ManagedInstallReport-") + ] if len(archiveitems) > 100: for item in archiveitems[100:]: itempath = os.path.join(archivepath, item) @@ -138,7 +152,7 @@ def archive_report(): try: os.unlink(itempath) except (OSError, IOError): - _warn('Could not remove archive item %s' % item) + _warn("Could not remove archive item %s" % item) # module globals @@ -147,5 +161,5 @@ def archive_report(): # pylint: enable=invalid-name -if __name__ == '__main__': - print 'This is a library of support tools for the Munki Suite.' +if __name__ == "__main__": + print "This is a library of support tools for the Munki Suite." diff --git a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/utils.py b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/utils.py index 23d940464..8bfe9564f 100644 --- a/public/assets/client_installer/payload/usr/local/munkireport/munkilib/utils.py +++ b/public/assets/client_installer/payload/usr/local/munkireport/munkilib/utils.py @@ -13,8 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" -utils +"""utils. Created by Justin McWilliams on 2010-10-26. @@ -31,8 +30,11 @@ class Memoize(dict): - '''Class to cache the return values of an expensive function. - This version supports only functions with non-keyword arguments''' + """Class to cache the return values of an expensive function. + + This version supports only functions with non-keyword arguments + """ + def __init__(self, func): self.func = func @@ -68,10 +70,10 @@ class InsecureFilePermissionsError(VerifyFilePermissionsError): # so disable PyLint warnings about invalid function names # pylint: disable=C0103 + def verifyFileOnlyWritableByMunkiAndRoot(file_path): - """ - Check the permissions on a given file path; fail if owner or group - does not match the munki process (default: root/admin) or the group is not + """Check the permissions on a given file path; fail if owner or group does + not match the munki process (default: root/admin) or the group is not 'wheel', or if other users are able to write to the file. This prevents escalated execution of arbitrary code. @@ -85,27 +87,27 @@ def verifyFileOnlyWritableByMunkiAndRoot(file_path): file_stat = os.stat(file_path) except OSError, err: raise VerifyFilePermissionsError( - '%s does not exist. \n %s' % (file_path, str(err))) + "%s does not exist. \n %s" % (file_path, str(err)) + ) try: - admin_gid = grp.getgrnam('admin').gr_gid - wheel_gid = grp.getgrnam('wheel').gr_gid + admin_gid = grp.getgrnam("admin").gr_gid + wheel_gid = grp.getgrnam("wheel").gr_gid user_gid = os.getegid() # verify the munki process uid matches the file owner uid. if os.geteuid() != file_stat.st_uid: - raise InsecureFilePermissionsError( - 'owner does not match munki process!') + raise InsecureFilePermissionsError("owner does not match munki process!") # verify the munki process gid matches the file owner gid, or the file # owner gid is wheel or admin gid. elif file_stat.st_gid not in [admin_gid, wheel_gid, user_gid]: - raise InsecureFilePermissionsError( - 'group does not match munki process!') + raise InsecureFilePermissionsError("group does not match munki process!") # verify other users cannot write to the file. elif file_stat.st_mode & stat.S_IWOTH != 0: - raise InsecureFilePermissionsError('world writable!') + raise InsecureFilePermissionsError("world writable!") except InsecureFilePermissionsError, err: raise InsecureFilePermissionsError( - '%s is not secure! %s' % (file_path, err.args[0])) + "%s is not secure! %s" % (file_path, err.args[0]) + ) def runExternalScript(script, allow_insecure=False, script_args=()): @@ -122,14 +124,16 @@ def runExternalScript(script, allow_insecure=False, script_args=()): RunExternalScriptError: there was an error running the script. """ if not os.path.exists(script): - raise ScriptNotFoundError('script does not exist: %s' % script) + raise ScriptNotFoundError("script does not exist: %s" % script) if not allow_insecure: try: verifyFileOnlyWritableByMunkiAndRoot(script) except VerifyFilePermissionsError, err: - msg = ('Skipping execution due to failed file permissions ' - 'verification: %s\n%s' % (script, str(err))) + msg = ( + "Skipping execution due to failed file permissions " + "verification: %s\n%s" % (script, str(err)) + ) raise RunExternalScriptError(msg) if os.access(script, os.X_OK): @@ -138,36 +142,48 @@ def runExternalScript(script, allow_insecure=False, script_args=()): cmd.extend(script_args) proc = None try: - proc = subprocess.Popen(cmd, shell=False, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + proc = subprocess.Popen( + cmd, + shell=False, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) except (OSError, IOError), err: raise RunExternalScriptError( - 'Error %s when attempting to run %s' % (unicode(err), script)) + "Error %s when attempting to run %s" % (unicode(err), script) + ) if proc: (stdout, stderr) = proc.communicate() - return proc.returncode, stdout.decode('UTF-8', 'replace'), \ - stderr.decode('UTF-8', 'replace') + return ( + proc.returncode, + stdout.decode("UTF-8", "replace"), + stderr.decode("UTF-8", "replace"), + ) else: - raise RunExternalScriptError('%s not executable' % script) + raise RunExternalScriptError("%s not executable" % script) def getPIDforProcessName(processname): - '''Returns a process ID for processname''' - cmd = ['/bin/ps', '-eo', 'pid=,command='] + """Returns a process ID for processname.""" + cmd = ["/bin/ps", "-eo", "pid=,command="] try: - proc = subprocess.Popen(cmd, shell=False, bufsize=-1, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + proc = subprocess.Popen( + cmd, + shell=False, + bufsize=-1, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) except OSError: return 0 while True: - line = proc.stdout.readline().decode('UTF-8') + line = proc.stdout.readline().decode("UTF-8") if not line and (proc.poll() != None): break - line = line.rstrip('\n') + line = line.rstrip("\n") if line: try: (pid, process) = line.split(None, 1) @@ -182,26 +198,28 @@ def getPIDforProcessName(processname): def getFirstPlist(textString): - """Gets the next plist from a text string that may contain one or - more text-style plists. + """Gets the next plist from a text string that may contain one or more + text-style plists. + Returns a tuple - the first plist (if any) and the remaining - string after the plist""" - plist_header = '' + string after the plist + """ + plist_header = "" plist_start_index = textString.find(plist_header) if plist_start_index == -1: # not found return ("", textString) plist_end_index = textString.find( - plist_footer, plist_start_index + len(plist_header)) + plist_footer, plist_start_index + len(plist_header) + ) if plist_end_index == -1: # not found return ("", textString) # adjust end value plist_end_index = plist_end_index + len(plist_footer) - return (textString[plist_start_index:plist_end_index], - textString[plist_end_index:]) + return (textString[plist_start_index:plist_end_index], textString[plist_end_index:]) -if __name__ == '__main__': - print 'This is a library of support tools for the Munki Suite.' +if __name__ == "__main__": + print "This is a library of support tools for the Munki Suite."