Hello community, here is the log from the commit of package rpmlint for openSUSE:Factory checked in at 2019-03-13 09:10:13 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rpmlint (Old) and /work/SRC/openSUSE:Factory/.rpmlint.new.28833 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rpmlint" Wed Mar 13 09:10:13 2019 rev:315 rq:682091 version:1.11 Changes: -------- --- /work/SRC/openSUSE:Factory/rpmlint/rpmlint.changes 2019-02-25 17:45:20.958907705 +0100 +++ /work/SRC/openSUSE:Factory/.rpmlint.new.28833/rpmlint.changes 2019-03-13 09:10:35.703415441 +0100 @@ -1,0 +2,22 @@ +Tue Mar 5 12:11:50 UTC 2019 - mvet...@suse.com + +- Add user/group 'minetest' for Minetest 5.0.0 (bsc#1127911) + +------------------------------------------------------------------- +Mon Mar 04 14:33:43 UTC 2019 - opensuse-packag...@opensuse.org + +- Update to version master: + * CheckPolkitPrivs: fix new rules.d check to use extracted rpm path + +------------------------------------------------------------------- +Wed Feb 27 11:33:42 UTC 2019 - opensuse-packag...@opensuse.org + +- Update rpmlint-checks to version master (bsc#1125314): + * coding style: fix indentation to satisfy flake8 travis-ci test + * CheckPolkitPrivs: implement new check for files put into rules.d dirs + * CheckPolkitPrivs: separate and refactor check for actions + * CheckPolkitPrivs: separate and refactor check of polkit-default-privs.d + * CheckPolkitPrivs: remove oudated PolicyKit path + * CheckPolkitPrivs: clearer error message for files in /etc/polkit-default-privs.d + +------------------------------------------------------------------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ rpmlint.spec: same change ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.hghIwU/_old 2019-03-13 09:10:37.619415243 +0100 +++ /var/tmp/diff_new_pack.hghIwU/_new 2019-03-13 09:10:37.619415243 +0100 @@ -3,4 +3,4 @@ <param name="url">https://github.com/openSUSE/rpmlint-tests.git</param> <param name="changesrevision">8914352f9a703602fbe68224878d4b540bff72c4</param></service><service name="tar_scm"> <param name="url">https://github.com/openSUSE/rpmlint-checks.git</param> - <param name="changesrevision">2fa6dd19efd033b406b56ecbd664497fba602a2d</param></service></servicedata> \ No newline at end of file + <param name="changesrevision">f6e3387e381e719deaf753423332881603b51b5e</param></service></servicedata> \ No newline at end of file ++++++ config ++++++ --- /var/tmp/diff_new_pack.hghIwU/_old 2019-03-13 09:10:37.635415242 +0100 +++ /var/tmp/diff_new_pack.hghIwU/_new 2019-03-13 09:10:37.635415242 +0100 @@ -405,6 +405,7 @@ 'mdom', 'memcached', 'messagebus', + 'minetest', 'mktex', 'modem', 'mumble-server', @@ -598,6 +599,7 @@ 'mednafen', 'memcached', 'messagebus', + 'minetest', 'mpd', 'mumble-server', 'mysql', ++++++ rpmlint-checks-master.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-checks-master/CheckDBusPolicy.py new/rpmlint-checks-master/CheckDBusPolicy.py --- old/rpmlint-checks-master/CheckDBusPolicy.py 2019-01-09 20:18:29.000000000 +0100 +++ new/rpmlint-checks-master/CheckDBusPolicy.py 2019-03-04 14:11:03.000000000 +0100 @@ -42,7 +42,7 @@ send_policy_seen = True printError(pkg, 'dbus-policy-allow-without-destination', "%(file)s: %(xml)s" % {'file': f, 'xml': allow.toxml()}) elif allow.hasAttribute('send_destination'): - send_policy_seen = True + send_policy_seen = True if (allow.hasAttribute('receive_sender') or allow.hasAttribute('receive_interface')): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-checks-master/CheckLogrotate.py new/rpmlint-checks-master/CheckLogrotate.py --- old/rpmlint-checks-master/CheckLogrotate.py 2019-01-09 20:18:29.000000000 +0100 +++ new/rpmlint-checks-master/CheckLogrotate.py 2019-03-04 14:11:03.000000000 +0100 @@ -47,9 +47,9 @@ pkg, 'suse-logrotate-user-writable-log-dir', "%s %s:%s %04o" % (d, files[d].user, files[d].group, mode)) elif files[d].group != 'root' and mode & 0o20 and (dirs[d] is None or dirs[d][1] != files[d].group): - printError( - pkg, 'suse-logrotate-user-writable-log-dir', - "%s %s:%s %04o" % (d, files[d].user, files[d].group, mode)) + printError( + pkg, 'suse-logrotate-user-writable-log-dir', + "%s %s:%s %04o" % (d, files[d].user, files[d].group, mode)) # extremely primitive logrotate parser def parselogrotateconf(self, root, f): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-checks-master/CheckPolkitPrivs.py new/rpmlint-checks-master/CheckPolkitPrivs.py --- old/rpmlint-checks-master/CheckPolkitPrivs.py 2019-01-09 20:18:29.000000000 +0100 +++ new/rpmlint-checks-master/CheckPolkitPrivs.py 2019-03-04 14:11:03.000000000 +0100 @@ -11,23 +11,51 @@ import Config import re import os +import sys +import json +import hashlib from xml.dom.minidom import parse - POLKIT_PRIVS_WHITELIST = Config.getOption('PolkitPrivsWhiteList', ()) # set of file names POLKIT_PRIVS_FILES = Config.getOption('PolkitPrivsFiles', ["/etc/polkit-default-privs.standard"]) +# path to JSON files containing whitelistings for files in rules.d directories +POLKIT_RULES_WHITELIST = Config.getOption('PolkitRulesWhitelist', ()) class PolkitCheck(AbstractCheck.AbstractCheck): def __init__(self): AbstractCheck.AbstractCheck.__init__(self, "CheckPolkitPrivs") self.privs = {} + self._collect_privs() + + # a structure like this: + # { + # "<package>": { + # "skip-digest-check": bool + # "<path>": { + # "audits": [ + # { + # "bug": "bsc#4711", + # "comment": "note about whitelisting", + # "digest": "<alg>:<digest>" + # } + # ] + # } + # } + # } + self.rules = {} + self._collect_rules_whitelist() + + def _get_err_prefix(self): + """error prefix label to be used for early error printing.""" + return self.__class__.__name__ + ":" + def _collect_privs(self): for filename in POLKIT_PRIVS_FILES: if os.path.exists(filename): - self._parsefile(filename) + self._parse_privs_file(filename) - def _parsefile(self, filename): + def _parse_privs_file(self, filename): with open(filename) as inputfile: for line in inputfile: line = line.split('#')[0].split('\n')[0] @@ -38,42 +66,122 @@ self.privs[priv] = value - def check(self, pkg): + def _collect_rules_whitelist(self): + for filename in POLKIT_RULES_WHITELIST: + if os.path.exists(filename): + self._parse_rules_whitelist(filename) - if pkg.isSource(): + def _parse_rules_whitelist(self, filename): + """ + The JSON data is structured like this: + + [ + { + "package": "polkit-default-privs", + "path": "/etc/polkit-1/rules.d/90-default-privs.rules", + # can be left out, default is false + # if set then the content will not + # be checked (only to be used for special cases) + "skip-digest-check": true, + "audits": [ + { + "bug": "bsc#1125314", + "comment": "rules dynamically generated by our own polkit profile tooling", + "digest": "sha256:aea3041de2c15db8683620de8533206e50241c309eb27893605d5ead17e5e75f" + }, + { + "bug": "bsc#4711", + "comment": "no-op changes in comments", + "digest": "<alg>:<digest>" + } + ] + }, + { + ... + } + ] + """ + + try: + with open(filename, 'r') as fd: + data = json.load(fd) + + for entry in data: + self._parse_rules_whitelist_entry(entry) + + except Exception as e: + print(self._get_err_prefix(), "failed to parse json file {}: {}".format( + filename, str(e)), + file=sys.stderr + ) + + def _parse_rules_whitelist_entry(self, entry): + path = entry["path"] + package = entry["package"] + skip_digest_check = entry.get("skip-digest-check", False) + + audits = entry.get("audits") + + # it is thinkable that the same rules file is shipped by a + # different conflicting package, therefore support + # multiple packages claiming the same path + pkg_dict = self.rules.setdefault(path, {}) + + if package in pkg_dict: + print(self._get_err_prefix(), "duplicate entry for path {} and package {}".format( + path, package), + file=sys.stderr + ) return + pkg_dict[package] = { + "skip-digest-check": skip_digest_check, + "audits": audits + } + + def check_perm_files(self, pkg): + """Checks files in polkit-default-privs.d.""" + files = pkg.files() + prefix = "/etc/polkit-default-privs.d/" + profiles = ("restrictive", "standard", "relaxed") - permfiles = {} + permfiles = [] # first pass, find additional files for f in files: if f in pkg.ghostFiles(): continue - if f.startswith("/etc/polkit-default-privs.d/"): + if f.startswith(prefix): - bn = f[28:] + bn = f[len(prefix):] if bn not in POLKIT_PRIVS_WHITELIST: printError(pkg, "polkit-unauthorized-file", f) - if bn.endswith(".restrictive") or bn.endswith(".standard") or bn.endswith(".relaxed"): - bn = bn.split('.')[0] + parts = bn.rsplit('.', 1) + + if len(parts) == 2 and parts[-1] in profiles: + bn = parts[0] if bn not in permfiles: - permfiles[bn] = 1 + permfiles.append(bn) - for f in permfiles: - f = pkg.dirName() + "/etc/polkit-default-privs.d/" + f + for f in sorted(permfiles): + f = pkg.dirName() + prefix + f - if os.path.exists(f + ".restrictive"): - self._parsefile(f + ".restrictive") - elif os.path.exists(f + ".standard"): - self._parsefile(f + ".standard") - elif os.path.exists(f + ".relaxed"): - self._parsefile(f + ".relaxed") + for profile in profiles: + path = '.'.join(f, profile) + if os.path.exists(path): + self._parse_privs_file(path) + break else: - self._parsefile(f) + self._parse_privs_file(f) + + def check_actions(self, pkg): + """Checks files in the actions directory.""" + + files = pkg.files() + prefix = "/usr/share/polkit-1/actions/" for f in files: if f in pkg.ghostFiles(): @@ -81,68 +189,153 @@ # catch xml exceptions try: - if f.startswith("/usr/share/PolicyKit/policy/")\ - or f.startswith("/usr/share/polkit-1/actions/"): + if f.startswith(prefix): xml = parse(pkg.dirName() + f) for a in xml.getElementsByTagName("action"): - action = a.getAttribute('id') - if action not in self.privs: - iserr = 0 - foundno = 0 - foundundef = 0 - settings = {} - try: - defaults = a.getElementsByTagName("defaults")[0] - for i in defaults.childNodes: - if not i.nodeType == i.ELEMENT_NODE: - continue - - if i.nodeName in ('allow_any', 'allow_inactive', 'allow_active'): - settings[i.nodeName] = i.firstChild.data - - except KeyError: - iserr = 1 - - for i in ('allow_any', 'allow_inactive', 'allow_active'): - if i not in settings: - foundundef = 1 - settings[i] = '??' - elif settings[i].find("auth_admin") != 0: - if settings[i] == 'no': - foundno = 1 - else: - iserr = 1 - - if iserr: - printError( - pkg, 'polkit-unauthorized-privilege', - '%s (%s:%s:%s)' % ( - action, - settings['allow_any'], - settings['allow_inactive'], - settings['allow_active'])) - else: - printError( - pkg, 'polkit-untracked-privilege', - '%s (%s:%s:%s)' % ( - action, - settings['allow_any'], - settings['allow_inactive'], - settings['allow_active'])) - - if foundno or foundundef: - printInfo( - pkg, 'polkit-cant-acquire-privilege', - '%s (%s:%s:%s)' % ( - action, - settings['allow_any'], - settings['allow_inactive'], - settings['allow_active'])) - + self.check_action(pkg, a) except Exception as x: printError(pkg, 'rpmlint-exception', "%(file)s raised an exception: %(x)s" % {'file': f, 'x': x}) continue + def check_action(self, pkg, action): + """Inspect a single polkit action used by an application.""" + action_id = action.getAttribute('id') + + if action_id in self.privs: + # the action is explicitly whitelisted, nothing else to do + return + + allow_types = ('allow_any', 'allow_inactive', 'allow_active') + foundunauthorized = False + foundno = False + foundundef = False + settings = {} + try: + defaults = action.getElementsByTagName("defaults")[0] + for i in defaults.childNodes: + if not i.nodeType == i.ELEMENT_NODE: + continue + + if i.nodeName in allow_types: + settings[i.nodeName] = i.firstChild.data + except KeyError: + foundunauthorized = True + + for i in allow_types: + if i not in settings: + foundundef = True + settings[i] = '??' + elif settings[i].find("auth_admin") != 0: + if settings[i] == 'no': + foundno = True + else: + foundunauthorized = True + + action_settings = "{} ({}:{}:{})".format( + action_id, + *(settings[type] for type in allow_types) + ) + + if foundunauthorized: + printError( + pkg, 'polkit-unauthorized-privilege', action_settings) + else: + printError( + pkg, 'polkit-untracked-privilege', action_settings) + + if foundno or foundundef: + printInfo( + pkg, 'polkit-cant-acquire-privilege', action_settings) + + def check_rules(self, pkg): + """Process files and whitelist for entries in rules.d dirs.""" + + files = pkg.files() + rule_dirs = ("/etc/polkit-1/rules.d/", "/usr/share/polkit-1/rules.d/") + + for f in files: + if f in pkg.ghostFiles(): + continue + + for rule_dir in rule_dirs: + if f.startswith(rule_dir): + break + else: + # no match + continue + + pkgs = self.rules.get(f, None) + wl_entry = pkgs.get(pkg.name, None) if pkgs else None + + # TODO: at the moment these are only warnings while we're newly + # implementing this feature. + # We should turn these into errors with badness - or change our + # enforcement procedure in OBS to not require this any more. + + if not pkgs or not wl_entry: + # no whitelist entry exists for this file + printWarning(pkg, 'polkit-unauthorized-rules', f) + continue + + if wl_entry["skip-digest-check"]: + # for this package/file combination no file content digest + # verification needs to be performed, so we're already fine + continue + + # check the newest entry first it is more likely to match what we + # have + for audit in reversed(wl_entry["audits"]): + digest_matches = self._checkDigest(pkg, f, audit["digest"]) + + if digest_matches: + break + else: + # none of the digest entries matched + printWarning(pkg, 'polkit-changed-rules', f) + continue + + def _checkDigest(self, pkg, path, digest_spec): + if not digest_spec: + return False + + parts = digest_spec.split(':', 1) + if len(parts) != 2: + print(self._get_err_prefix(), "bad digest specification for package {} file {}".format( + pkg.name, path), + file=sys.stderr + ) + return False + + alg, digest = parts + + try: + h = hashlib.new(alg) + except ValueError: + print(self._get_err_prefix(), "bad digest algorithm '{}' for package {} file {}".format( + alg, pkg.name, path), + file=sys.stderr + ) + return False + + with open(pkg.dirName() + path, 'rb') as fd: + while True: + data = fd.read(4096) + if not data: + break + + h.update(data) + + return h.hexdigest() == digest + + def check(self, pkg): + + if pkg.isSource(): + return + + self.check_perm_files(pkg) + self.check_actions(pkg) + self.check_rules(pkg) + check = PolkitCheck() @@ -150,9 +343,10 @@ addDetails( 'polkit-unauthorized-file', -"""If the package is intended for inclusion in any SUSE product -please open a bug report to request review of the package by the -security team. Please refer to {} for more information""".format(AUDIT_BUG_URL), +"""A custom polkit rule file is installed by this package. If the package is +intended for inclusion in any SUSE product please open a bug report to request +review of the package by the security team. Please refer to {} for more +information""".format(AUDIT_BUG_URL), 'polkit-unauthorized-privilege', """The package allows unprivileged users to carry out privileged @@ -174,4 +368,16 @@ """Usability can be improved by allowing users to acquire privileges via authentication. Use e.g. 'auth_admin' instead of 'no' and make sure to define 'allow_any'. This is an issue only if the privilege -is not listed in /etc/polkit-default-privs.*""") +is not listed in /etc/polkit-default-privs.*""", + +'polkit-unauthorized-rules', +"""A polkit rules file installed by this package is not whitelisted in the +polkit-whitelisting package. If the package is intended for inclusion in any +SUSE product please open a bug report to request review of the package by the +security team. Please refer to {} for more information.""".format(AUDIT_BUG_URL), + +'polkit-changed-rules', +"""A polkit rules file installed by this package changed in content. Please +open a bug report to request follow-up review of the introduced changes by +the security team. Please refer to {} for more information.""".format(AUDIT_BUG_URL), +)