Hello community, here is the log from the commit of package rpmlint for openSUSE:Factory checked in at 2019-12-11 12:04:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rpmlint (Old) and /work/SRC/openSUSE:Factory/.rpmlint.new.4691 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rpmlint" Wed Dec 11 12:04:22 2019 rev:325 rq:755596 version:1.11 Changes: -------- --- /work/SRC/openSUSE:Factory/rpmlint/rpmlint.changes 2019-10-21 12:26:14.539813893 +0200 +++ /work/SRC/openSUSE:Factory/.rpmlint.new.4691/rpmlint.changes 2019-12-11 12:04:49.440747954 +0100 @@ -1,0 +2,12 @@ +Tue Dec 10 14:46:11 UTC 2019 - [email protected] + +- Update to version master: + * new common whitelisting code for CheckPolkitPrivs and CheckCronJobs + +------------------------------------------------------------------- +Thu Nov 28 11:58:21 UTC 2019 - Malte Kraus <[email protected]> + +- whitelist sssd infopipe (bsc#1157663) +- whitelist sysprof3 D-Bus services (bsc#1151418) + +------------------------------------------------------------------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rpmlint-tests.spec ++++++ --- /var/tmp/diff_new_pack.EyfTUT/_old 2019-12-11 12:04:51.316747164 +0100 +++ /var/tmp/diff_new_pack.EyfTUT/_new 2019-12-11 12:04:51.320747163 +0100 @@ -12,7 +12,7 @@ # license that conforms to the Open Source Definition (Version 1.9) # published by the Open Source Initiative. -# Please submit bugfixes or comments via http://bugs.opensuse.org/ +# Please submit bugfixes or comments via https://bugs.opensuse.org/ # # icecream 0 @@ -30,7 +30,7 @@ License: SUSE-Public-Domain Group: Development/Tools/Building BuildRoot: %{_tmppath}/%{name}-%{version}-build -Url: http://www.opensuse.org/ +URL: http://www.opensuse.org/ Source: rpmlint-tests-%version.tar.xz Patch0: rpmlint-tests-sle15.patch ++++++ rpmlint.spec ++++++ --- /var/tmp/diff_new_pack.EyfTUT/_old 2019-12-11 12:04:51.340747154 +0100 +++ /var/tmp/diff_new_pack.EyfTUT/_new 2019-12-11 12:04:51.344747153 +0100 @@ -12,7 +12,7 @@ # license that conforms to the Open Source Definition (Version 1.9) # published by the Open Source Initiative. -# Please submit bugfixes or comments via http://bugs.opensuse.org/ +# Please submit bugfixes or comments via https://bugs.opensuse.org/ # @@ -22,7 +22,7 @@ Summary: RPM file correctness checker License: GPL-2.0-or-later Group: System/Packages -Url: https://github.com/rpm-software-management/rpmlint +URL: https://github.com/rpm-software-management/rpmlint Source0: https://github.com/rpm-software-management/rpmlint/archive/rpmlint-%{version}.tar.gz Source1: rpmlint-checks-master.tar.xz Source2: config ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.EyfTUT/_old 2019-12-11 12:04:51.412747124 +0100 +++ /var/tmp/diff_new_pack.EyfTUT/_new 2019-12-11 12:04:51.412747124 +0100 @@ -3,4 +3,4 @@ <param name="url">https://github.com/openSUSE/rpmlint-tests.git</param> <param name="changesrevision">e27d43198d06699c9a705b71e2d511a94efab752</param></service><service name="tar_scm"> <param name="url">https://github.com/openSUSE/rpmlint-checks.git</param> - <param name="changesrevision">97ff0bdbab5a7039bd4a6551772cf9446ab01d70</param></service></servicedata> \ No newline at end of file + <param name="changesrevision">00e6393112de7c6da46780842cd787f693b05af3</param></service></servicedata> \ No newline at end of file ++++++ config ++++++ --- /var/tmp/diff_new_pack.EyfTUT/_old 2019-12-11 12:04:51.428747117 +0100 +++ /var/tmp/diff_new_pack.EyfTUT/_new 2019-12-11 12:04:51.428747117 +0100 @@ -697,6 +697,9 @@ # sysprof (bsc#996111) "org.gnome.Sysprof2.service", "org.gnome.Sysprof2.conf", + # sysprof (bsc#1151418) + "org.gnome.Sysprof3.service", + "org.gnome.Sysprof3.conf", # flatpak (bsc#984817) "org.freedesktop.Flatpak.SystemHelper.service", "org.freedesktop.Flatpak.SystemHelper.conf", @@ -776,6 +779,9 @@ # systemd-portabled (boo#1145639) "org.freedesktop.portable1.service", "org.freedesktop.portable1.conf", + # sssd (bsc#1157663, bsc#1106600) + "org.freedesktop.sssd.infopipe.service", + "org.freedesktop.sssd.infopipe.conf", )) setOption("PAMModules.WhiteList", ( ++++++ rpmlint-checks-master.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-checks-master/CheckCronJobs.py new/rpmlint-checks-master/CheckCronJobs.py --- old/rpmlint-checks-master/CheckCronJobs.py 1970-01-01 01:00:00.000000000 +0100 +++ new/rpmlint-checks-master/CheckCronJobs.py 2019-12-10 13:24:44.000000000 +0100 @@ -0,0 +1,100 @@ +# vim: sw=4 ts=4 sts=4 et : +############################################################################# +# Author : Matthias Gerstner +# Purpose : Enforce Whitelisting for cron jobs in /etc/cron.* directories +############################################################################# + +import os + +import AbstractCheck +import Config +import Whitelisting + +from Filter import addDetails + +# this option is found in config files in /opt/testing/share/rpmlint/mini, +# installed there by the rpmlint-mini package. +WHITELIST_DIR = Config.getOption('WhitelistDataDir', []) + + +class CronCheck(AbstractCheck.AbstractCheck): + + def __init__(self): + AbstractCheck.AbstractCheck.__init__(self, "CheckCronJobs") + + for wd in WHITELIST_DIR: + candidate = os.path.join(wd, "cron-whitelist.json") + if os.path.exists(candidate): + whitelist_path = candidate + break + else: + whitelist_path = None + + self.m_check_configured = whitelist_path is not None + + if not self.m_check_configured: + return + + parser = Whitelisting.WhitelistParser(whitelist_path) + whitelist_entries = parser.parse() + self.m_wl_checker = Whitelisting.WhitelistChecker( + whitelist_entries, + restricted_paths=( + "/etc/cron.d/", "/etc/cron.hourly/", "/etc/cron.daily/", + "/etc/cron.weekly/", "/etc/cron.monthly/" + ), + error_map={ + "unauthorized": "cronjob-unauthorized-file", + "changed": "cronjob-changed-file", + "ghost": "cronjob-ghost-file" + } + ) + + def _getPrintPrefix(self): + """Returns a prefix for error / warning output.""" + return self.__class__.__name__ + ":" + + def _getErrorPrefix(self): + return self._getPrintPrefix() + " ERROR: " + + def _getWarnPrefix(self): + return self._getPrintPrefix() + " WARN: " + + def check(self, pkg): + """This is called by rpmlint to perform the cron check on the given + pkg.""" + + if not self.m_check_configured: + # don't ruin the whole run if this check is not configured, this + # was hopefully intended by the user. + return + + self.m_wl_checker.check(pkg) + + +# needs to be instantiated for the check to be registered with rpmlint +check = CronCheck() + +for _id, desc in ( + ( + 'cronjob-unauthorized-file', + """A cron job 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 {url} for more + information""" + ), + ( + 'cronjob-changed-file', + """A cron job or cron job related 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 {url} for more + information.""" + ), + ( + 'cronjob-ghost-file', + """A cron job path has been marked as %ghost file by this package. + This is not allowed as it is impossible to review. Please refer to + {url} for more information.""" + ) +): + addDetails(_id, desc.format(url=Whitelisting.AUDIT_BUG_URL)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-checks-master/CheckDBUSServices.py new/rpmlint-checks-master/CheckDBUSServices.py --- old/rpmlint-checks-master/CheckDBUSServices.py 2019-09-03 14:03:37.000000000 +0200 +++ new/rpmlint-checks-master/CheckDBUSServices.py 2019-12-10 13:24:44.000000000 +0100 @@ -1,4 +1,4 @@ -# vim:sw=4:et +# vim: sw=4 et sts=4 ts=4 : ############################################################################# # File : CheckDBUSServices.py # Package : rpmlint @@ -10,6 +10,7 @@ from Filter import * import AbstractCheck +import Whitelisting SERVICES_WHITELIST = Config.getOption('DBUSServices.WhiteList', ()) # set of file names @@ -35,12 +36,13 @@ files = pkg.files() for f in files: - if f in pkg.ghostFiles(): - continue - for p in _dbus_system_paths: if f.startswith(p): + if f in pkg.ghostFiles(): + printError(pkig, "suse-dbus-ghost-service", f) + continue + bn = f[len(p):] if bn not in SERVICES_WHITELIST: printError(pkg, "suse-dbus-unauthorized-service", f) @@ -49,11 +51,19 @@ check = DBUSServiceCheck() if Config.info: - addDetails( -'suse-dbus-unauthorized-service', -"""The package installs a DBUS system service file. If the package -is intended for inclusion in any SUSE product please open a bug -report to request review of the service by the security team. Please -refer to https://en.opensuse.org/openSUSE:Package_security_guidelines#audit_bugs -for more information.""", -) + for _id, desc in ( + ( + 'suse-dbus-unauthorized-service', + """The package installs a DBUS system service file. If the package + is intended for inclusion in any SUSE product please open a bug + report to request review of the service by the security team. Please + refer to {url} for more information.""" + ), + ( + 'suse-dbus-ghost-service', + """This package installs a DBUS system service marked as %ghost. + This is not allowed, since it is impossible to review. Please + refer to {url} for more information.""" + ) + ): + addDetails(_id, desc.format(url=Whitelisting.AUDIT_BUG_URL)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-checks-master/CheckPAMModules.py new/rpmlint-checks-master/CheckPAMModules.py --- old/rpmlint-checks-master/CheckPAMModules.py 2019-09-03 14:03:37.000000000 +0200 +++ new/rpmlint-checks-master/CheckPAMModules.py 2019-12-10 13:24:44.000000000 +0100 @@ -1,4 +1,4 @@ -# vim:sw=4:et +# vim: sw=4 ts=4 sts=4 et : ############################################################################# # File : CheckPAMModules.py # Package : rpmlint @@ -9,6 +9,7 @@ from Filter import * import AbstractCheck import re +import Whitelisting PAM_WHITELIST = Config.getOption('PAMModules.WhiteList', ()) # set of file names @@ -28,11 +29,12 @@ files = pkg.files() for f in files: - if f in pkg.ghostFiles(): - continue - m = pam_module_re.match(f) if m: + if f in pkg.ghostFiles(): + printError(pkg, 'suse-pam-ghost-module', f) + continue + bn = m.groups()[0] if bn not in PAM_WHITELIST: printError(pkg, "suse-pam-unauthorized-module", bn) @@ -41,10 +43,20 @@ check = PAMModulesCheck() if Config.info: - addDetails( -'suse-pam-unauthorized-module', -"""The package installs a PAM module. If the package -is intended for inclusion in any SUSE product please open a bug -report to request review of the service by the security team. -Please refer to https://en.opensuse.org/openSUSE:Package_security_guidelines#audit_bugs""", -) + + for _id, desc in ( + ( + 'suse-pam-unauthorized-module', + """The package installs a PAM module. If the package + is intended for inclusion in any SUSE product please open a bug + report to request review of the service by the security team. + Please refer to {url}""" + ), + ( + 'suse-pam-ghost-module', + """The package installs a PAM module as %ghost file. This is not + allowed as it is impossible to review. For more information please + refer to {url} for more information.""" + ) + ): + addDetails(_id, desc.format(url=Whitelisting.AUDIT_BUG_URL)) 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-09-03 14:03:37.000000000 +0200 +++ new/rpmlint-checks-master/CheckPolkitPrivs.py 2019-12-10 13:24:44.000000000 +0100 @@ -1,4 +1,4 @@ -# vim:sw=4:et +# vim: sw=4 et sts=4 ts=4 : ############################################################################# # File : CheckPolkitPrivs.py # Package : rpmlint @@ -11,9 +11,7 @@ import Config import re import os -import sys -import json -import hashlib +import Whitelisting from xml.dom.minidom import parse POLKIT_PRIVS_WHITELIST = Config.getOption('PolkitPrivsWhiteList', ()) # set of file names @@ -27,23 +25,6 @@ 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): @@ -67,77 +48,25 @@ self.privs[priv] = value def _collect_rules_whitelist(self): + rules_entries = {} for filename in POLKIT_RULES_WHITELIST: - if os.path.exists(filename): - self._parse_rules_whitelist(filename) - - 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>" - } - ] - }, - { - ... + if not os.path.exists(filename): + continue + parser = Whitelisting.WhitelistParser(filename) + res = parser.parse() + rules_entries.update(res) + + self.m_rules_checker = Whitelisting.WhitelistChecker( + rules_entries, + restricted_paths=( + "/etc/polkit-1/rules.d/", "/usr/share/polkit-1/rules.d/" + ), + error_map={ + "unauthorized": "polkit-unauthorized-rules", + "changed": "polkit-changed-rules", + "ghost": "polkit-ghost-file" } - ] - """ - - 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.""" @@ -149,11 +78,13 @@ permfiles = [] # first pass, find additional files for f in files: - if f in pkg.ghostFiles(): - continue if f.startswith(prefix): + if f in pkg.ghostFiles(): + printError(pkg, 'polkit-ghost-file', f) + continue + bn = f[len(prefix):] if bn not in POLKIT_PRIVS_WHITELIST: printError(pkg, "polkit-unauthorized-file", f) @@ -170,7 +101,7 @@ f = pkg.dirName() + prefix + f for profile in profiles: - path = '.'.join(f, profile) + path = '.'.join((f, profile)) if os.path.exists(path): self._parse_privs_file(path) break @@ -184,12 +115,13 @@ prefix = "/usr/share/polkit-1/actions/" for f in files: - if f in pkg.ghostFiles(): - continue - # catch xml exceptions try: if f.startswith(prefix): + if f in pkg.ghostFiles(): + printError(pkg, 'polkit-ghost-file', f) + continue + xml = parse(pkg.dirName() + f) for a in xml.getElementsByTagName("action"): self.check_action(pkg, a) @@ -250,82 +182,7 @@ 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 + self.m_rules_checker.check(pkg) def check(self, pkg): @@ -339,45 +196,57 @@ check = PolkitCheck() -AUDIT_BUG_URL = "https://en.opensuse.org/openSUSE:Package_security_guidelines#audit_bugs" - -addDetails( -'polkit-unauthorized-file', -"""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 -operations without authentication. This could cause security -problems if not done carefully. 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-untracked-privilege', -"""The privilege is not listed in /etc/polkit-default-privs.* -which makes it harder for admins to find. Furthermore polkit -authorization checks can easily introduce security issues. 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-cant-acquire-privilege', -"""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.*""", - -'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), -) +for _id, desc in ( + ( + 'polkit-unauthorized-file', + """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 {url} for more + information""" + ), + ( + 'polkit-unauthorized-privilege', + """The package allows unprivileged users to carry out privileged + operations without authentication. This could cause security + problems if not done carefully. 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 {url} + for more information.""" + ), + ( + 'polkit-untracked-privilege', + """The privilege is not listed in /etc/polkit-default-privs.* + which makes it harder for admins to find. Furthermore polkit + authorization checks can easily introduce security issues. 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 {url} for more information.""" + ), + ( + 'polkit-cant-acquire-privilege', + """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.*""" + ), + ( + '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 {url} for more information.""" + ), + ( + '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 {url} for more information.""" + ), + ( + 'polkit-ghost-file', + """This package installs a polkit rule or policy as %ghost file. + This is not allowed as it is impossible to review. For more + information please refer to {url} for more information.""" + ) +): + addDetails(_id, desc.format(url=Whitelisting.AUDIT_BUG_URL)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-checks-master/CheckSUIDPermissions.py new/rpmlint-checks-master/CheckSUIDPermissions.py --- old/rpmlint-checks-master/CheckSUIDPermissions.py 2019-09-03 14:03:37.000000000 +0200 +++ new/rpmlint-checks-master/CheckSUIDPermissions.py 2019-12-10 13:24:44.000000000 +0100 @@ -1,4 +1,4 @@ -# vim:sw=4:et +# vim: sw=4 et sts=4 ts=4 : ############################################################################# # File : CheckSUIDPermissions.py # Package : rpmlint @@ -10,6 +10,7 @@ from Filter import printWarning, printError, printInfo, addDetails import AbstractCheck +import Whitelisting import os import re import rpm @@ -83,13 +84,15 @@ permfiles = {} # first pass, find and parse permissions.d files + prefix = "/etc/permissions.d/" for f in files: - if f in pkg.ghostFiles(): - continue + if f.startswith(prefix): - if f.startswith("/etc/permissions.d/"): + if f in pkg.ghostFiles(): + printError(pkg, 'polkit-ghost-file', f) + continue - bn = f[19:] + bn = f[len(prefix):] if bn not in _permissions_d_whitelist: printError(pkg, "permissions-unauthorized-file", f) @@ -240,57 +243,90 @@ check = SUIDCheck() -AUDIT_BUG_URL = "https://en.opensuse.org/openSUSE:Package_security_guidelines#audit_bugs" - -addDetails( -'permissions-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), -'permissions-symlink', -"""permissions handling for symlinks is useless. Please contact [email protected] to remove the entry. Please refer to {} for more -information.""".format(AUDIT_BUG_URL), -'permissions-dir-without-slash', -"""the entry in the permissions file refers to a directory. Please -contact [email protected] to append a slash to the entry in order to -avoid security problems. Please refer to {} for more information.""".format(AUDIT_BUG_URL), -'permissions-file-as-dir', -"""the entry in the permissions file refers to a directory but the -package actually contains a file. Please contact [email protected] to -remove the slash. Please refer to {} for more information.""".format(AUDIT_BUG_URL), -'permissions-incorrect', -"""please use the %attr macro to set the correct permissions.""", -'permissions-incorrect-owner', -"""please use the %attr macro to set the correct ownership.""", -'permissions-file-setuid-bit', -"""If the package is intended for inclusion in any SUSE product -please open a bug report to request review of the program by the -security team. Please refer to {} for more information.""".format(AUDIT_BUG_URL), -'permissions-directory-setuid-bit', -"""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), -'permissions-world-writable', -"""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), -'permissions-fscaps', -"""Packaging file capabilities is currently not supported. Please -use normal permissions instead. You may contact the security team to -request an entry that sets capabilities in /etc/permissions -instead.""", -'permissions-missing-postin', -"""Please add an appropriate %post section""", -'permissions-missing-requires', -"""Please add \"PreReq: permissions\"""", -'permissions-missing-verifyscript', -"""Please add a %verifyscript section""", -'permissions-suseconfig-obsolete', -"""The %run_permissions macro calls SuSEconfig which sets permissions for all -files in the system. Please use %set_permissions <filename> instead -to only set permissions for files contained in this package""", -) +for _id, desc in ( + ( + 'permissions-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 {url} for more + information.""" + ), + ( + 'permissions-symlink', + """permissions handling for symlinks is useless. Please contact + [email protected] to remove the entry. Please refer to {url} for more + information.""" + ), + ( + 'permissions-dir-without-slash', + """the entry in the permissions file refers to a directory. Please + contact [email protected] to append a slash to the entry in order to + avoid security problems. Please refer to {url} for more information.""" + ), + ( + 'permissions-file-as-dir', + """the entry in the permissions file refers to a directory but the + package actually contains a file. Please contact [email protected] to + remove the slash. Please refer to {url} for more information.""" + ), + ( + 'permissions-incorrect', + """please use the %attr macro to set the correct permissions.""" + ), + ( + 'permissions-incorrect-owner', + """please use the %attr macro to set the correct ownership.""" + ), + ( + 'permissions-file-setuid-bit', + """If the package is intended for inclusion in any SUSE product + please open a bug report to request review of the program by the + security team. Please refer to {url} for more information.""" + ), + ( + 'permissions-directory-setuid-bit', + """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 {url} for more + information.""" + ), + ( + 'permissions-world-writable', + """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 {url} for more + information.""" + ), + ( + 'permissions-fscaps', + """Packaging file capabilities is currently not supported. Please + use normal permissions instead. You may contact the security team to + request an entry that sets capabilities in /etc/permissions + instead.""", + ), + ( + 'permissions-missing-postin', + """Please add an appropriate %post section""" + ), + ( + 'permissions-missing-requires', + """Please add 'PreReq: permissions'""" + ), + ( + 'permissions-missing-verifyscript', + """Please add a %verifyscript section""" + ), + ( + 'permissions-suseconfig-obsolete', + """The %run_permissions macro calls SuSEconfig which sets permissions for all + files in the system. Please use %set_permissions <filename> instead + to only set permissions for files contained in this package""", + ), + ( + 'permissions-ghostfile', + """This package installs a permissions file as a %ghost file. This + is not allowed as it is impossible to review. Please refer to + {url} for more information.""" + ) +): + addDetails(_id, desc.format(url=Whitelisting.AUDIT_BUG_URL)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-checks-master/Whitelisting.py new/rpmlint-checks-master/Whitelisting.py --- old/rpmlint-checks-master/Whitelisting.py 1970-01-01 01:00:00.000000000 +0100 +++ new/rpmlint-checks-master/Whitelisting.py 2019-12-10 13:24:44.000000000 +0100 @@ -0,0 +1,400 @@ +# vim: sw=4 ts=4 sts=4 et : +############################################################################# +# Author : Matthias Gerstner +# Purpose : reusable code for dealing with security whitelistings +############################################################################# + +import os +import sys +import json +import hashlib +import traceback + +from Filter import printError + +AUDIT_BUG_URL = "https://en.opensuse.org/openSUSE:Package_security_guidelines#audit_bugs" + + +class DigestVerificationResult(object): + """This type represents the result of a digest verification as returned + from AuditEntry.compareDigests().""" + + def __init__(self, path, alg, expected, encountered): + + self.m_path = path + self.m_alg = alg + self.m_expected = expected + self.m_encountered = encountered + + def path(self): + return self.m_path + + def algorithm(self): + return self.m_alg + + def matches(self): + """Returns a boolean whether the encountered digest matches the + expected digest.""" + return self.m_expected == self.m_encountered + + def expected(self): + return self.m_expected + + def encountered(self): + return self.m_encountered + + +class AuditEntry(object): + """This object represents a single audit entry as found in a whitelisting + entry like: + + "bsc#1234": { + "comment": "some comment", + "digests": { + "/some/file": "<alg>:<digest>", + ... + } + } + + """ + + def __init__(self, bug): + + self.m_bug = bug + self._verifyBugNr() + self.m_comment = "" + self.m_digests = {} + + def bug(self): + return self.m_bug + + def setComment(self, comment): + self.m_comment = comment + + def comment(self): + return self.m_comment + + def setDigests(self, digests): + for path, digest in digests.items(): + self._verifyPath(path) + self._verifyDigestSyntax(digest) + + self.m_digests = digests + + def digests(self): + """Returns a dictionary specifying file paths and their whitelisted + digests. The digests are suitable for the + Python hashlib module. They're of the form '<alg>:<hexdigest>'. As a + special case the digest entry can be 'skip:<none>' which indicates + that no digest verification should be performed and the file is + acceptable regardless of its contents.""" + return self.m_digests + + def isSkipDigest(self, digest): + """Returns whether the given digest entry denotes the special "skip + digest" case which means not to check the file digest at all.""" + return digest == 'skip:<none>' + + def compareDigests(self, pkg): + """Compares the digests recorded in this AuditEntry against the actual + files coming from the given rpmlint @pkg. Returns a tuple of + (boolean, [DigestVerificationResult, ...]). The boolean indicates the + overall verification result, while the list of + DigestVerificationResult entries provides detailed information about + the encountered data. Any "skip digest" entries will be ignored and + not be included in the result list.""" + + results = [] + + # NOTE: syntax and algorithm validity of stored digests was already + # checked in setDigests() so we can skip the respective error handling + # here. + + for path, digest in self.digests().items(): + if self.isSkipDigest(digest): + continue + + alg, digest = digest.split(':', 1) + + try: + h = hashlib.new(alg) + + # NOTE: this path is dynamic and rpmlint unpacks the RPM + # contents into a temporary directory even when outside the + # build environment i.e. the file content should always be + # available to us. + with open(pkg.dirName() + path, 'rb') as fd: + while True: + chunk = fd.read(4096) + if not chunk: + break + + h.update(chunk) + + encountered = h.hexdigest() + except IOError as e: + encountered = "error:" + str(e) + + dig_res = DigestVerificationResult(path, alg, digest, encountered) + results.append(dig_res) + + return (all([res.matches() for res in results]), results) + + def _verifyBugNr(self): + """Perform some sanity checks on the bug nr associated with this audit + entry.""" + + parts = self.m_bug.split('#') + + if len(parts) != 2 or \ + parts[0] not in ("bsc", "boo", "bnc") or \ + not parts[1].isdigit(): + raise Exception("Bad bug nr# '{}'".format(self.m_bug)) + + def _verifyDigestSyntax(self, digest): + if self.isSkipDigest(digest): + return + + parts = digest.split(':') + if len(parts) != 2: + raise Exception("Bad digest specification " + digest) + + alg, hexdigest = parts + + try: + hashlib.new(alg) + except ValueError: + raise Exception("Bad digest algorithm in " + digest) + + def _verifyPath(self, path): + if not path.startswith(os.path.sep): + raise Exception("Bad whitelisting path " + path) + + +class WhitelistEntry(object): + """This object represents a single whitelisting entry like: + + "somepackage" { + "audits": { + ... + } + }, + """ + + def __init__(self, package): + self.m_package = package + # a list of AuditEntry objects associated with this whitelisting entry + self.m_audits = [] + + def package(self): + return self.m_package + + def addAudit(self, audit): + self.m_audits.append(audit) + + def audits(self): + return self.m_audits + + +class WhitelistParser(object): + """This type knows how to parse the JSON whitelisting format. The format + is documented in [1]. + + [1]: https://github.com/openSUSE/rpmlint-security-whitelistings/blob/master/README.md + """ + + def __init__(self, wl_path): + """Creates a new instance of WhitelistParser that operates on + @wl_path.""" + + self.m_path = wl_path + + def parse(self): + """Parses the whitelisting file and returns a dictionary of the + following structure: + + { + "path/to/file": [WhitelistEntry(), ...], + ... + } + + Since a single path might be claimed by more than one package the + values of the dictionary are lists, to cover for this possibility. + """ + + ret = {} + + try: + with open(self.m_path, 'r') as fd: + data = json.load(fd) + + for pkg, config in data.items(): + entry = self._parseWhitelistEntry(pkg, config) + if not entry: + # soft error, continue parsing + continue + for a in entry.audits(): + for path in a.digests(): + entries = ret.setdefault(path, []) + entries.append(entry) + except Exception as e: + _, _, tb = sys.exc_info() + fn, ln, _, _ = traceback.extract_tb(tb)[-1] + raise Exception(self._getErrorPrefix() + "Failed to parse JSON file: {}:{}: {}".format( + fn, ln, str(e) + )) + + return ret + + def _parseWhitelistEntry(self, package, config): + """Parses a single JSON whitelist entry and returns a WhitelistEntry() + object for it. On non-critical error conditions None is returned, + otherwise an exception is raised.""" + + ret = WhitelistEntry(package) + + audits = config.get("audits", {}) + + if not audits: + raise Exception(self._getErrorPrefix() + "no 'audits' entries for package {}".format(package)) + + for bug, data in audits.items(): + try: + audit = self._parseAuditEntry(bug, data) + except Exception as e: + raise Exception(self._getErrorPrefix() + "Failed to parse audit entries: " + str(e)) + + if not audit: + # soft error, continue parsing + continue + ret.addAudit(audit) + + return ret + + def _parseAuditEntry(self, bug, data): + """Parses a single JSON audit sub-entry returns an AuditEntry() object + for it. On non-critical error conditions None is returned, otherwise + an exception is raised""" + + ret = AuditEntry(bug) + + comment = data.get("comment", None) + if comment: + ret.setComment(comment) + + digests = data.get("digests", {}) + + if not digests: + raise Exception(self._getErrorPrefix() + "no 'digests' entry for '{}'".format(bug)) + + ret.setDigests(digests) + + return ret + + def _getErrorPrefix(self): + return self.m_path + ": ERROR: " + + def _getWarnPrefix(self): + return self.m_path + ": WARN: " + + +class WhitelistChecker(object): + """This type actually compares files found in an RPM against whitelist + entries.""" + + def __init__(self, whitelist_entries, restricted_paths, error_map): + """Instantiate a properly configured checker + + :param whitelist_entries: is a dictionary data structure as returned + from WhitelistParser.parse(). + :param restricted_paths: a sequence of path prefixes that will trigger + the whitelisting check. All other paths will + be ignored. + :param error_map: is a specification of rpmlint error labels for ghost + files, unauthorized files and changed files like: + { + "unauthorized": "polkit-unauthorized-rules", + "changed": "polkit-changed-rules", + "ghost": "polkit-ghost-file" + } + """ + + self.m_restricted_paths = restricted_paths + self.m_whitelist_entries = whitelist_entries + self.m_error_map = error_map + + req_error_keys = ("unauthorized", "changed", "ghost") + + for req_key in req_error_keys: + if req_key not in self.m_error_map: + raise Exception("Missing {} error mapping".format(req_key)) + + def check(self, pkg): + """Checks the given RPM pkg instance against the configured whitelist + restriction. + + Each whitelist violation will be printed with the according error tag. + Nothing is returned from this function. + """ + + if pkg.isSource(): + return + + files = pkg.files() + + for f in files: + for restricted in self.m_restricted_paths: + if f.startswith(restricted): + break + else: + # no match + continue + + if f in pkg.ghostFiles(): + printError(pkg, self.m_error_map['ghost'], f) + continue + + entries = self.m_whitelist_entries.get(f, []) + wl_match = None + for entry in entries: + if entry.package() == pkg.name: + wl_match = entry + break + else: + # no whitelist entry exists for this file + printError(pkg, self.m_error_map['unauthorized'], f) + continue + + # for the case that there's no match of digests, remember the most + # recent digest verification result for diagnosis output towards + # the user + diag_results = None + + # check the newest (bottom) entry first it is more likely to match + # what we have + for audit in reversed(wl_match.audits()): + digest_matches, results = audit.compareDigests(pkg) + + if digest_matches: + break + + if not diag_results: + diag_results = results + else: + # none of the digest entries matched + self._printVerificationResults(diag_results) + printError(pkg, self.m_error_map['changed'], f) + continue + + def _printVerificationResults(self, verification_results): + """For the case of changed file digests this function prints the + encountered and expected digests and paths for diagnostic purposes.""" + + for result in verification_results: + if result.matches(): + continue + + print("{path}: expected {alg} digest {expected} but encountered {encountered}".format( + path=result.path(), alg=result.algorithm(), + expected=result.expected(), encountered=result.encountered() + ), file=sys.stderr)
