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."