diff options
author | Anas Nashif <anas.nashif@intel.com> | 2012-11-21 15:28:00 -0800 |
---|---|---|
committer | Anas Nashif <anas.nashif@intel.com> | 2012-11-21 15:28:00 -0800 |
commit | ec6268183d43997c7fe124ca40a877edb0d7555b (patch) | |
tree | eb871af5b189b33b9bdbb18fc7fd77d40b0e8c8f /FilesCheck.py | |
download | rpmlint-ec6268183d43997c7fe124ca40a877edb0d7555b.tar.gz rpmlint-ec6268183d43997c7fe124ca40a877edb0d7555b.tar.bz2 rpmlint-ec6268183d43997c7fe124ca40a877edb0d7555b.zip |
Imported Upstream version 1.4upstream/1.4
Diffstat (limited to 'FilesCheck.py')
-rw-r--r-- | FilesCheck.py | 1341 |
1 files changed, 1341 insertions, 0 deletions
diff --git a/FilesCheck.py b/FilesCheck.py new file mode 100644 index 0000000..eb314e0 --- /dev/null +++ b/FilesCheck.py @@ -0,0 +1,1341 @@ +# -*- coding: utf-8 -*- +############################################################################# +# File : FilesCheck.py +# Package : rpmlint +# Author : Frederic Lepied +# Created on : Mon Oct 4 19:32:49 1999 +# Version : $Id: FilesCheck.py 1894 2011-11-24 21:56:18Z scop $ +# Purpose : test various aspects on files: locations, owner, groups, +# permission, setuid, setgid... +############################################################################# + +from datetime import datetime +import commands +import os +import re +import stat +import string + +import rpm + +from Filter import addDetails, printError, printWarning +from Pkg import catcmd, is_utf8, is_utf8_str +import AbstractCheck +import Config + + +# must be kept in sync with the filesystem package +STANDARD_DIRS = ( + '/', + '/bin', + '/boot', + '/etc', + '/etc/X11', + '/etc/opt', + '/etc/profile.d', + '/etc/skel', + '/etc/xinetd.d', + '/home', + '/lib', + '/lib/modules', + '/lib64', + '/media', + '/mnt', + '/mnt/cdrom', + '/mnt/disk', + '/mnt/floppy', + '/opt', + '/proc', + '/root', + '/sbin', + '/selinux', + '/srv', + '/sys', + '/tmp', + '/usr', + '/usr/X11R6', + '/usr/X11R6/bin', + '/usr/X11R6/doc', + '/usr/X11R6/include', + '/usr/X11R6/lib', + '/usr/X11R6/lib64', + '/usr/X11R6/man', + '/usr/X11R6/man/man1', + '/usr/X11R6/man/man2', + '/usr/X11R6/man/man3', + '/usr/X11R6/man/man4', + '/usr/X11R6/man/man5', + '/usr/X11R6/man/man6', + '/usr/X11R6/man/man7', + '/usr/X11R6/man/man8', + '/usr/X11R6/man/man9', + '/usr/X11R6/man/mann', + '/usr/bin', + '/usr/bin/X11', + '/usr/etc', + '/usr/games', + '/usr/include', + '/usr/lib', + '/usr/lib/X11', + '/usr/lib/games', + '/usr/lib/gcc-lib', + '/usr/lib/menu', + '/usr/lib64', + '/usr/lib64/gcc-lib', + '/usr/local', + '/usr/local/bin', + '/usr/local/doc', + '/usr/local/etc', + '/usr/local/games', + '/usr/local/info', + '/usr/local/lib', + '/usr/local/lib64', + '/usr/local/man', + '/usr/local/man/man1', + '/usr/local/man/man2', + '/usr/local/man/man3', + '/usr/local/man/man4', + '/usr/local/man/man5', + '/usr/local/man/man6', + '/usr/local/man/man7', + '/usr/local/man/man8', + '/usr/local/man/man9', + '/usr/local/man/mann', + '/usr/local/sbin', + '/usr/local/share', + '/usr/local/share/man', + '/usr/local/share/man/man1', + '/usr/local/share/man/man2', + '/usr/local/share/man/man3', + '/usr/local/share/man/man4', + '/usr/local/share/man/man5', + '/usr/local/share/man/man6', + '/usr/local/share/man/man7', + '/usr/local/share/man/man8', + '/usr/local/share/man/man9', + '/usr/local/share/man/mann', + '/usr/local/src', + '/usr/sbin', + '/usr/share', + '/usr/share/dict', + '/usr/share/doc', + '/usr/share/icons', + '/usr/share/info', + '/usr/share/man', + '/usr/share/man/man1', + '/usr/share/man/man2', + '/usr/share/man/man3', + '/usr/share/man/man4', + '/usr/share/man/man5', + '/usr/share/man/man6', + '/usr/share/man/man7', + '/usr/share/man/man8', + '/usr/share/man/man9', + '/usr/share/man/mann', + '/usr/share/misc', + '/usr/src', + '/usr/tmp', + '/var', + '/var/cache', + '/var/db', + '/var/lib', + '/var/lib/games', + '/var/lib/misc', + '/var/lib/rpm', + '/var/local', + '/var/lock', + '/var/lock/subsys', + '/var/log', + '/var/mail', + '/var/nis', + '/var/opt', + '/var/preserve', + '/var/run', + '/var/spool', + '/var/spool/mail', + '/var/tmp', + ) + +DEFAULT_GAMES_GROUPS = 'Games' + +DEFAULT_DANGLING_EXCEPTIONS = (['consolehelper$', 'usermode-consoleonly'], + ) + +# Standard users and groups from LSB Core 4.0.0: 21.2 User & Group Names +DEFAULT_STANDARD_USERS = ('root', 'bin', 'daemon', 'adm', 'lp', 'sync', + 'shutdown', 'halt', 'mail', 'news', 'uucp', + 'operator', 'man', 'nobody',) +DEFAULT_STANDARD_GROUPS = ('root', 'bin', 'daemon', 'adm', 'lp', 'sync', + 'shutdown', 'halt', 'mail', 'news', 'uucp', + 'man', 'nobody',) + +tmp_regex = re.compile('^/tmp/|^(/var|/usr)/tmp/') +sub_bin_regex = re.compile('^(/usr)?/s?bin/\S+/') +backup_regex = re.compile('(~|\#[^/]+\#|\.orig|\.rej)$') +compr_regex = re.compile('\.(gz|z|Z|zip|bz2|lzma|xz)$') +absolute_regex = re.compile('^/([^/]+)') +absolute2_regex = re.compile('^/?([^/]+)') +points_regex = re.compile('^\.\./(.*)') +doc_regex = re.compile('^/usr(/share|/X11R6)?/(doc|man|info)/') +bin_regex = re.compile('^/(?:usr/(?:s?bin|games)|s?bin)/(.*)') +includefile_regex = re.compile('\.(c|h)(pp|xx)?$', re.IGNORECASE) +develfile_regex = re.compile('\.(a|cmxa?|mli?)$') +buildconfigfile_regex = re.compile('(\.pc|/bin/.+-config)$') +# room for improvement with catching more -R, but also for false positives... +buildconfig_rpath_regex = re.compile('(?:-rpath|Wl,-R)\\b') +sofile_regex = re.compile('/lib(64)?/(.+/)?lib[^/]+\.so$') +devel_regex = re.compile('(.*)-(debug(info)?|devel|headers|source|static)$') +debuginfo_package_regex = re.compile('-debug(info)?$') +lib_regex = re.compile('lib(64)?/lib[^/]*\.so\..*') +ldconfig_regex = re.compile('^[^#]*ldconfig', re.MULTILINE) +depmod_regex = re.compile('^[^#]*depmod', re.MULTILINE) +install_info_regex = re.compile('^[^#]*install-info', re.MULTILINE) +perl_temp_file_regex = re.compile('.*perl.*/(\.packlist|perllocal\.pod)$') +scm_regex = re.compile('/CVS/[^/]+$|/\.(bzr|cvs|git|hg)ignore$|/\.hgtags$|/\.(bzr|git|hg|svn)/|/(\.arch-ids|{arch})/') +games_path_regex = re.compile('^/usr(/lib(64)?)?/games/') +games_group_regex = re.compile(Config.getOption('RpmGamesGroups', DEFAULT_GAMES_GROUPS)) +dangling_exceptions = Config.getOption('DanglingSymlinkExceptions', DEFAULT_DANGLING_EXCEPTIONS) +logrotate_regex = re.compile('^/etc/logrotate\.d/(.*)') +module_rpms_ok = Config.getOption('KernelModuleRPMsOK', True) +kernel_modules_regex = re.compile('^/lib/modules/(2\.[23456]\.[0-9]+[^/]*?)/') +kernel_package_regex = re.compile('^kernel(22)?(-)?(smp|enterprise|bigmem|secure|BOOT|i686-up-4GB|p3-smp-64GB)?') +normal_zero_length_regex = re.compile('^/etc/security/console\.apps/|/\.nosearch$|/__init__\.py$') +perl_regex = re.compile('^/usr/lib/perl5/(?:vendor_perl/)?([0-9]+\.[0-9]+)\.([0-9]+)/') +python_regex = re.compile('^/usr/lib(?:64)?/python([.0-9]+)/') +python_bytecode_regex_pep3147 = re.compile('^(.*)/__pycache__/(.*)\.(.*)(\.py[oc])$') +python_bytecode_regex = re.compile('^(.*)(\.py[oc])$') +python_default_version = Config.getOption('PythonDefaultVersion', None) +perl_version_trick = Config.getOption('PerlVersionTrick', True) +log_regex = re.compile('^/var/log/[^/]+$') +lib_path_regex = re.compile('^(/usr(/X11R6)?)?/lib(64)?') +lib_package_regex = re.compile('^(lib|.+-libs)') +hidden_file_regex = re.compile('/\.[^/]*$') +manifest_perl_regex = re.compile('^/usr/share/doc/perl-.*/MANIFEST(\.SKIP)?$') +shebang_regex = re.compile('^#!\s*(\S+)') +interpreter_regex = re.compile('^/(usr/)?(s?bin|games|libexec(/.+)?|(lib(64)?|share)/.+)/[^/]+$') +script_regex = re.compile('^/((usr/)?s?bin|etc/(rc\.d/init\.d|X11/xinit\.d|cron\.(hourly|daily|monthly|weekly)))/') +sourced_script_regex = re.compile('^/etc/(bash_completion\.d|profile\.d)/') +use_utf8 = Config.getOption('UseUTF8', Config.USEUTF8_DEFAULT) +skipdocs_regex = re.compile(Config.getOption('SkipDocsRegexp', '\.(?:rtf|x?html?|svg|ml[ily]?)$'), re.IGNORECASE) +meta_package_regex = re.compile(Config.getOption('MetaPackageRegexp', '^(bundle|task)-')) +filesys_packages = ['filesystem'] # TODO: make configurable? +quotes_regex = re.compile('[\'"]+') + +for idx in range(0, len(dangling_exceptions)): + dangling_exceptions[idx][0] = re.compile(dangling_exceptions[idx][0]) +del idx + +use_relative_symlinks = Config.getOption("UseRelativeSymlinks", True) + +standard_groups = Config.getOption('StandardGroups', DEFAULT_STANDARD_GROUPS) +standard_users = Config.getOption('StandardUsers', DEFAULT_STANDARD_USERS) + +non_readable_regexs = (re.compile('^/var/log/'), + re.compile('^/etc/(g?shadow-?|securetty)$')) + +man_base_regex = re.compile(r'^/usr(?:/share)?/man/man[^/]+/(.+)\.[1-9n]') +man_warn_regex = re.compile(r'^([^:]+:)\d+:\s*') +man_nowarn_regex = re.compile( + # From Lintian: ignore common undefined macros from pod2man << Perl 5.10 + r'\`(Tr|IX)\' not defined|' + # .so entries won't resolve as we're dealing with stdin + r'No such file or directory|' + # TODO, better handling for these (see e.g. Lintian) + r'(can\'t break|cannot adjust) line') +man_warn_category = Config.getOption('ManWarningCategory', 'mac') + +fsf_license_regex = re.compile('(GNU((\s+(Library|Lesser|Affero))?(\s+General)?\s+Public|\s+Free\s+Documentation)\s+Licen[cs]e|(GP|FD)L)', re.IGNORECASE) +fsf_wrong_address_regex = re.compile('(675\s+Mass\s+Ave|59\s+Temple\s+Place|Franklin\s+Steet|02139|02111-1307)', re.IGNORECASE) + +# loosely inspired from Python Cookbook +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/173220 +text_characters = "".join(map(chr, range(32, 127)) + list("\n\r\t\b")) +_null_trans = string.maketrans("", "") + +def peek(filename, pkg, length=1024): + """Peek into a file, return a chunk from its beginning and a flag if it + seems to be a text file.""" + fobj = None + chunk = None + try: + fobj = open(filename, 'rb') + chunk = fobj.read(length) + fobj.close() + except Exception, e: # eg. https://bugzilla.redhat.com/209876 + printWarning(pkg, 'read-error', e) + if fobj: + fobj.close() + return (chunk, False) + + if "\0" in chunk: + return (chunk, False) + + if not chunk: # Empty files are considered text + return (chunk, True) + + # PDF's are binary but often detected as text by the algorithm below + if filename.lower().endswith('.pdf') and chunk.startswith('%PDF-'): + return (chunk, False) + + # Get the non-text characters (maps a character to itself then + # use the 'remove' option to get rid of the text characters.) + t = chunk.translate(_null_trans, text_characters) + + # If more than 30% non-text characters, then consider it a binary file + istext = float(len(t))/len(chunk) <= 0.30 + return (chunk, istext) + +# See Python/import.c (in the trunk and py3k branches) for a full list of +# the values here. +_python_magic_values = { + '2.2': 60717, + '2.3': 62011, + '2.4': 62061, + '2.5': 62131, + '2.6': 62161, + '2.7': 62211, + '3.0': 3130, + '3.1': 3150, + '3.2': 3180, + '3.3': 3190, + } + +def get_expected_pyc_magic(path): + """.pyc/.pyo files embed a 4-byte magic value identifying which version of + the python bytecode ABI they are for. Given a path to a .pyc/.pyo file, + return a (magic ABI value, python version) tuple. For example, + '/usr/lib/python3.1/foo.pyc' should return (3151, '3.1'). + The first value will be None if the python version was not resolved + from the given pathname and the PythonDefaultVersion configuration + variable is not set, or if we don't know the magic ABI value for the + python version (no matter from which source the version came from). + The second value will be None if a python version could not be resolved + from the given pathname.""" + + ver_from_path = None + m = python_regex.search(path) + if m: + ver_from_path = m.group(1) + + expected_version = ver_from_path or python_default_version + expected_magic_value = _python_magic_values.get(expected_version) + + if not expected_magic_value: + return (None, ver_from_path) + + # In Python 2, if Py_UnicodeFlag is set, Python's import code uses a value + # one higher, but this is off by default. In Python 3.0 and 3.1 (but no + # longer in 3.2), it always uses the value one higher: + if expected_version[:3] in ('3.0', '3.1'): + expected_magic_value += 1 + + return (expected_magic_value, ver_from_path) + +def py_demarshal_long(b): + """Counterpart to Python's PyMarshal_ReadLongFromFile, operating on the + bytes in a string.""" + return (ord(b[0]) + + (ord(b[1]) << 8) + + (ord(b[2]) << 16) + + (ord(b[3]) << 24)) + +def python_bytecode_to_script(path): + """Given a python bytecode path, give the path of the .py file + (or None if not python bytecode).""" + + res = python_bytecode_regex_pep3147.search(path) + if res: + return res.group(1) + '/' + res.group(2) + '.py' + + res = python_bytecode_regex.search(path) + if res: + return res.group(1) + '.py' + + return None + +class FilesCheck(AbstractCheck.AbstractCheck): + + def __init__(self): + AbstractCheck.AbstractCheck.__init__(self, 'FilesCheck') + + def check(self, pkg): + + files = pkg.files() + + if use_utf8: + for filename in files: + if not is_utf8_str(filename): + printError(pkg, 'filename-not-utf8', filename) + + # Rest of the checks are for binary packages only + if pkg.isSource(): + return + + # Check if the package is a development package + devel_pkg = devel_regex.search(pkg.name) + + config_files = pkg.configFiles() + ghost_files = pkg.ghostFiles() + doc_files = pkg.docFiles() + req_names = pkg.req_names() + lib_package = lib_package_regex.search(pkg.name) + is_kernel_package = kernel_package_regex.search(pkg.name) + debuginfo_package = debuginfo_package_regex.search(pkg.name) + + # report these errors only once + perl_dep_error = False + python_dep_error = False + lib_file = False + non_lib_file = None + log_file = None + logrotate_file = False + debuginfo_srcs = False + debuginfo_debugs = False + + if not doc_files: + printWarning(pkg, 'no-documentation') + + if files: + if meta_package_regex.search(pkg.name): + printWarning(pkg, 'file-in-meta-package') + elif debuginfo_package: + printError(pkg, 'empty-debuginfo-package') + + # Unique (rdev, inode) combinations + hardlinks = {} + + # All executable files from standard bin dirs (basename => [paths]) + # Hack: basenames with empty paths links are symlinks (not subject + # to duplicate binary check, but yes for man page existence check) + bindir_exes = {} + + # All man page "base" names (without section etc extensions) + man_basenames = set() + + for f, pkgfile in files.items(): + mode = pkgfile.mode + user = pkgfile.user + group = pkgfile.group + link = pkgfile.linkto + size = pkgfile.size + rdev = pkgfile.rdev + inode = pkgfile.inode + is_doc = f in doc_files + nonexec_file = False + + for match in AbstractCheck.macro_regex.findall(f): + printWarning(pkg, 'unexpanded-macro', f, match) + if standard_users and user not in standard_users: + printWarning(pkg, 'non-standard-uid', f, user) + if standard_groups and group not in standard_groups: + printWarning(pkg, 'non-standard-gid', f, group) + + if not module_rpms_ok and kernel_modules_regex.search(f) and not \ + is_kernel_package: + printError(pkg, "kernel-modules-not-in-kernel-packages", f) + + if tmp_regex.search(f): + printError(pkg, 'dir-or-file-in-tmp', f) + elif f.startswith('/mnt/'): + printError(pkg, 'dir-or-file-in-mnt', f) + elif f.startswith('/opt/'): + printError(pkg, 'dir-or-file-in-opt', f) + elif f.startswith('/usr/local/'): + printError(pkg, 'dir-or-file-in-usr-local', f) + elif f.startswith('/var/local/'): + printError(pkg, 'dir-or-file-in-var-local', f) + elif f.startswith('/var/run/'): + if f not in ghost_files: + printWarning(pkg, 'non-ghost-in-var-run', f) + elif f.startswith('/var/lock/'): + if f not in ghost_files: + printWarning(pkg, 'non-ghost-in-var-lock', f) + elif sub_bin_regex.search(f): + printError(pkg, 'subdir-in-bin', f) + elif f.startswith('/home/'): + printError(pkg, 'dir-or-file-in-home', f) + elif '/site_perl/' in f: + printWarning(pkg, 'siteperl-in-perl-module', f) + + if backup_regex.search(f): + printError(pkg, 'backup-file-in-package', f) + elif scm_regex.search(f): + printError(pkg, 'version-control-internal-file', f) + elif f.endswith('/.htaccess'): + printError(pkg, 'htaccess-file', f) + elif hidden_file_regex.search(f) and not f.startswith("/etc/skel/"): + printWarning(pkg, 'hidden-file-or-dir', f) + elif manifest_perl_regex.search(f): + printWarning(pkg, 'manifest-in-perl-module', f) + elif f == '/usr/info/dir' or f == '/usr/share/info/dir': + printError(pkg, 'info-dir-file', f) + + res = logrotate_regex.search(f) + if res: + logrotate_file = True + if res.group(1) != pkg.name: + printError(pkg, 'incoherent-logrotate-file', f) + + if link != '': + ext = compr_regex.search(link) + if ext: + if not re.compile('\.' + ext.group(1) + '$').search(f): + printError(pkg, 'compressed-symlink-with-wrong-ext', + f, link) + + perm = mode & 07777 + + if log_regex.search(f): + log_file = f + + # Hardlink check + hardlink = hardlinks.get((rdev, inode)) + if hardlink and os.path.dirname(hardlink) != os.path.dirname(f): + printWarning(pkg, 'cross-directory-hard-link', f, hardlink) + hardlinks[(rdev, inode)] = f + + # normal file check + if stat.S_ISREG(mode): + + # set[ug]id bit check + if stat.S_ISGID & mode or stat.S_ISUID & mode: + if stat.S_ISUID & mode: + printError(pkg, 'setuid-binary', f, user, oct(perm)) + if stat.S_ISGID & mode: + if not (group == 'games' and + (games_path_regex.search(f) or + games_group_regex.search( + pkg[rpm.RPMTAG_GROUP]))): + printError(pkg, 'setgid-binary', f, group, + oct(perm)) + if mode & 0777 != 0755: + printError(pkg, 'non-standard-executable-perm', f, + oct(perm)) + + # Prefetch scriptlets, strip quotes from them (#169) + postin = pkg[rpm.RPMTAG_POSTIN] or \ + pkg.scriptprog(rpm.RPMTAG_POSTINPROG) + if postin: + postin = quotes_regex.sub('', postin) + postun = pkg[rpm.RPMTAG_POSTUN] or \ + pkg.scriptprog(rpm.RPMTAG_POSTUNPROG) + if postun: + postun = quotes_regex.sub('', postun) + + if not devel_pkg: + if lib_path_regex.search(f): + lib_file = True + elif not is_doc: + non_lib_file = f + + if log_regex.search(f): + nonexec_file = True + if user != 'root': + printError(pkg, 'non-root-user-log-file', f, user) + if group != 'root': + printError(pkg, 'non-root-group-log-file', f, group) + if f not in ghost_files: + printError(pkg, 'non-ghost-file', f) + + chunk = None + istext = False + if os.access(pkgfile.path, os.R_OK): + (chunk, istext) = peek(pkgfile.path, pkg) + + interpreter = None + if chunk: + res = shebang_regex.search(chunk) + if res: + interpreter = res.group(1) + + if doc_regex.search(f): + if not interpreter: + nonexec_file = True + if not is_doc: + printError(pkg, 'not-listed-as-documentation', f) + + # check ldconfig call in %post and %postun + if lib_regex.search(f): + if not postin: + printError(pkg, 'library-without-ldconfig-postin', f) + else: + if not ldconfig_regex.search(postin): + printError(pkg, 'postin-without-ldconfig', f) + + if not postun: + printError(pkg, 'library-without-ldconfig-postun', f) + else: + if not ldconfig_regex.search(postun): + printError(pkg, 'postun-without-ldconfig', f) + + # check depmod call in %post and %postun + res = not is_kernel_package and kernel_modules_regex.search(f) + if res: + kernel_version = res.group(1) + kernel_version_regex = re.compile( + '\\bdepmod\s+-a.*F\s+/boot/System\.map-' + + re.escape(kernel_version) + '\\b.*\\b' + + re.escape(kernel_version) + '\\b', + re.MULTILINE | re.DOTALL) + + if not postin or not depmod_regex.search(postin): + printError(pkg, 'module-without-depmod-postin', f) + # check that we run depmod on the right kernel + elif not kernel_version_regex.search(postin): + printError(pkg, 'postin-with-wrong-depmod', f) + + if not postun or not depmod_regex.search(postun): + printError(pkg, 'module-without-depmod-postun', f) + # check that we run depmod on the right kernel + elif not kernel_version_regex.search(postun): + printError(pkg, 'postun-with-wrong-depmod', f) + + # check install-info call in %post and %postun + if f.startswith('/usr/share/info/'): + if not postin: + printError(pkg, + 'info-files-without-install-info-postin', f) + elif not install_info_regex.search(postin): + printError(pkg, 'postin-without-install-info', f) + + preun = pkg[rpm.RPMTAG_PREUN] or \ + pkg.scriptprog(rpm.RPMTAG_PREUNPROG) + if not postun and not preun: + printError(pkg, + 'info-files-without-install-info-postun', f) + elif (not postun or + not install_info_regex.search(postun)) and \ + (not preun or not install_info_regex.search(preun)): + printError(pkg, 'postin-without-install-info', f) + + # check perl temp file + if perl_temp_file_regex.search(f): + printWarning(pkg, 'perl-temp-file', f) + + is_buildconfig = buildconfigfile_regex.search(f) and True + + # check rpaths in buildconfig files + if is_buildconfig: + ln = pkg.grep(buildconfig_rpath_regex, f) + if ln: + printError(pkg, 'rpath-in-buildconfig', f, 'lines', ln) + + res = bin_regex.search(f) + if res: + if mode & 0111 == 0: + printWarning(pkg, 'non-executable-in-bin', f, oct(perm)) + else: + exe = res.group(1) + if "/" not in exe: + bindir_exes.setdefault(exe, []).append(f) + + if not devel_pkg and not is_doc and \ + (includefile_regex.search(f) or \ + develfile_regex.search(f) or is_buildconfig): + printWarning(pkg, 'devel-file-in-non-devel-package', f) + if mode & 0444 != 0444 and perm & 07000 == 0: + ok_nonreadable = False + for regex in non_readable_regexs: + if regex.search(f): + ok_nonreadable = True + break + if not ok_nonreadable: + printError(pkg, 'non-readable', f, oct(perm)) + if size == 0 and not normal_zero_length_regex.search(f) and \ + f not in ghost_files: + printError(pkg, 'zero-length', f) + + if mode & 0002 != 0: + printError(pkg, 'world-writable', f, oct(perm)) + + if not perl_dep_error: + res = perl_regex.search(f) + if res: + if perl_version_trick: + vers = res.group(1) + '.' + res.group(2) + else: + vers = res.group(1) + res.group(2) + if not (pkg.check_versioned_dep('perl-base', vers) or + pkg.check_versioned_dep('perl', vers)): + printError(pkg, 'no-dependency-on', + 'perl-base', vers) + perl_dep_error = True + + if not python_dep_error: + res = python_regex.search(f) + if res and not (pkg.check_versioned_dep('python-base', + res.group(1)) or + pkg.check_versioned_dep('python', + res.group(1))): + printError(pkg, 'no-dependency-on', 'python-base', + res.group(1)) + python_dep_error = True + + source_file = python_bytecode_to_script(f) + if source_file: + if source_file in files: + if chunk: + # Verify that the magic ABI value embedded in the + # .pyc header is correct + found_magic = py_demarshal_long(chunk[:4]) & 0xffff + exp_magic, exp_version = get_expected_pyc_magic(f) + if exp_magic and found_magic != exp_magic: + found_version = 'unknown' + for (pv, pm) in _python_magic_values.items(): + if pm == found_magic: + found_version = pv + break + # If expected version was from the file path, + # issue # an error, otherwise a warning. + msg = (pkg, 'python-bytecode-wrong-magic-value', + f, "expected %d (%s), found %d (%s)" % + (exp_magic, + exp_version or python_default_version, + found_magic, found_version)) + if exp_version is not None: + printError(*msg) + else: + printWarning(*msg) + + # Verify that the timestamp embedded in the .pyc + # header matches the mtime of the .py file: + pyc_timestamp = py_demarshal_long(chunk[4:8]) + # If it's a symlink, check target file mtime. + srcfile = pkg.readlink(files[source_file]) + if not srcfile: + printWarning( + pkg, 'python-bytecode-without-source', f) + elif pyc_timestamp != srcfile.mtime: + cts = datetime.fromtimestamp( + pyc_timestamp).isoformat() + sts = datetime.fromtimestamp( + srcfile.mtime).isoformat() + printError(pkg, + 'python-bytecode-inconsistent-mtime', + f, cts, srcfile.name, sts) + else: + printWarning(pkg, 'python-bytecode-without-source', f) + + # normal executable check + if mode & stat.S_IXUSR and perm != 0755: + printError(pkg, 'non-standard-executable-perm', + f, oct(perm)) + if mode & 0111 != 0: + if f in config_files: + printError(pkg, 'executable-marked-as-config-file', f) + if not nonexec_file: + # doc_regex and log_regex checked earlier, no match, + # check rest of usual cases here. Sourced scripts have + # their own check, so disregard them here. + nonexec_file = f.endswith('.pc') or \ + compr_regex.search(f) or \ + includefile_regex.search(f) or \ + develfile_regex.search(f) or \ + logrotate_regex.search(f) + if nonexec_file: + printWarning(pkg, 'spurious-executable-perm', f) + elif f.startswith('/etc/') and f not in config_files and \ + f not in ghost_files: + printWarning(pkg, 'non-conffile-in-etc', f) + + if pkg.arch == 'noarch' and f.startswith('/usr/lib64/python'): + printError(pkg, 'noarch-python-in-64bit-path', f) + + if debuginfo_package: + if f.endswith('.debug'): + debuginfo_debugs = True + else: + debuginfo_srcs = True + + res = man_base_regex.search(f) + if res: + man_basenames.add(res.group(1)) + if use_utf8 and chunk: + # TODO: better shell escaping or seq based invocation + cmd = commands.getstatusoutput( + 'env LC_ALL=C %s "%s" | gtbl | ' + 'env LC_ALL=en_US.UTF-8 groff -mtty-char -Tutf8 ' + '-P-c -mandoc -w%s >/dev/null' % + (catcmd(f), pkgfile.path, man_warn_category)) + for line in cmd[1].split("\n"): + res = man_warn_regex.search(line) + if not res or man_nowarn_regex.search(line): + continue + printWarning(pkg, "manual-page-warning", f, + line[res.end(1):]) + + # text file checks + if istext: + # ignore perl module shebang -- TODO: disputed... + if f.endswith('.pm'): + interpreter = None + # sourced scripts should not be executable + if sourced_script_regex.search(f): + if interpreter: + printError(pkg, + 'sourced-script-with-shebang', f, + interpreter) + if mode & 0111 != 0: + printError(pkg, 'executable-sourced-script', + f, oct(perm)) + # ...but executed ones should + elif interpreter or mode & 0111 != 0 or \ + script_regex.search(f): + if interpreter: + if not interpreter_regex.search(interpreter): + printError(pkg, 'wrong-script-interpreter', + f, interpreter) + elif not nonexec_file and not \ + (lib_path_regex.search(f) and + f.endswith('.la')): + printError(pkg, 'script-without-shebang', f) + + if mode & 0111 == 0 and not is_doc: + printError(pkg, 'non-executable-script', f, + oct(perm), interpreter) + if '\r' in chunk: + printError( + pkg, 'wrong-script-end-of-line-encoding', f) + elif is_doc and not skipdocs_regex.search(f): + if '\r' in chunk: + printWarning( + pkg, 'wrong-file-end-of-line-encoding', f) + # We check only doc text files for UTF-8-ness; + # checking everything may be slow and can generate + # lots of unwanted noise. + if use_utf8 and not is_utf8(pkgfile.path): + printWarning(pkg, 'file-not-utf8', f) + if fsf_license_regex.search(chunk) and \ + fsf_wrong_address_regex.search(chunk): + printError(pkg, 'incorrect-fsf-address', f) + + elif is_doc and chunk and compr_regex.search(f): + ff = compr_regex.sub('', f) + if not skipdocs_regex.search(ff): + # compressed docs, eg. info and man files etc + if use_utf8 and not is_utf8(pkgfile.path): + printWarning(pkg, 'file-not-utf8', f) + + # normal dir check + elif stat.S_ISDIR(mode): + if mode & 01002 == 2: # world writable without sticky bit + printError(pkg, 'world-writable', f, oct(perm)) + if perm != 0755: + printError(pkg, 'non-standard-dir-perm', f, oct(perm)) + if pkg.name not in filesys_packages and f in STANDARD_DIRS: + printError(pkg, 'standard-dir-owned-by-package', f) + if hidden_file_regex.search(f): + printWarning(pkg, 'hidden-file-or-dir', f) + + + # symbolic link check + elif stat.S_ISLNK(mode): + + is_so = sofile_regex.search(f) + if not devel_pkg and is_so and not link.endswith('.so'): + printWarning(pkg, 'devel-file-in-non-devel-package', f) + + res = man_base_regex.search(f) + if res: + man_basenames.add(res.group(1)) + else: + res = bin_regex.search(f) + if res: + exe = res.group(1) + if "/" not in exe: + bindir_exes.setdefault(exe, []) + + # absolute link + r = absolute_regex.search(link) + if r: + if not is_so and link not in files and \ + link not in req_names: + is_exception = False + for e in dangling_exceptions: + if e[0].search(link): + is_exception = e[1] + break + if is_exception: + if is_exception not in req_names: + printWarning(pkg, 'no-dependency-on', + is_exception) + else: + printWarning(pkg, 'dangling-symlink', f, link) + linktop = r.group(1) + r = absolute_regex.search(f) + if r: + filetop = r.group(1) + if filetop == linktop or use_relative_symlinks: + printWarning(pkg, 'symlink-should-be-relative', + f, link) + # relative link + else: + if not is_so: + abslink = '%s/%s' % (os.path.dirname(f), link) + abslink = os.path.normpath(abslink) + if abslink not in files and abslink not in req_names: + is_exception = False + for e in dangling_exceptions: + if e[0].search(link): + is_exception = e[1] + break + if is_exception: + if is_exception not in req_names: + printWarning(pkg, 'no-dependency-on', + is_exception) + else: + printWarning(pkg, 'dangling-relative-symlink', + f, link) + pathcomponents = f.split('/')[1:] + r = points_regex.search(link) + lastpop = None + mylink = None + + while r: + mylink = r.group(1) + if len(pathcomponents) == 0: + printError(pkg, 'symlink-has-too-many-up-segments', + f, link) + break + else: + lastpop = pathcomponents[0] + pathcomponents = pathcomponents[1:] + r = points_regex.search(mylink) + + if mylink and lastpop: + r = absolute2_regex.search(mylink) + linktop = r.group(1) + + # does the link go up and then down into the same + # directory? + #if linktop == lastpop: + # printWarning(pkg, 'lengthy-symlink', f, link) + + # have we reached the root directory? + if len(pathcomponents) == 0 and linktop != lastpop \ + and not use_relative_symlinks: + # relative link into other toplevel directory + printWarning(pkg, 'symlink-should-be-absolute', f, + link) + # check additional segments for mistakes like + # `foo/../bar/' + for linksegment in mylink.split('/'): + if linksegment == '..': + printError( + pkg, + 'symlink-contains-up-and-down-segments', + f, link) + + if f.startswith('/etc/cron.d/'): + if stat.S_ISLNK(mode): + printError(pkg, 'symlink-crontab-file', f) + + if mode & 0111: + printError(pkg, 'executable-crontab-file', f) + + if stat.S_IWGRP & mode or stat.S_IWOTH & mode: + printError(pkg, 'non-owner-writeable-only-crontab-file', f) + + if log_file and not logrotate_file: + printWarning(pkg, 'log-files-without-logrotate', log_file) + + if lib_package and lib_file and non_lib_file: + printError(pkg, 'outside-libdir-files', non_lib_file) + + if debuginfo_package and debuginfo_debugs and not debuginfo_srcs: + printError(pkg, 'debuginfo-without-sources') + + for exe, paths in bindir_exes.items(): + if len(paths) > 1: + printWarning(pkg, "duplicate-executable", exe, paths) + if exe not in man_basenames: + printWarning(pkg, "no-manual-page-for-binary", exe) + +# Create an object to enable the auto registration of the test +check = FilesCheck() + +addDetails( +'no-documentation', +'''The package contains no documentation (README, doc, etc). +You have to include documentation files.''', + +'not-listed-as-documentation', +'''The documentation files of this package are not listed with +the standard %doc tag.''', + +'non-standard-uid', +'''A file in this package is owned by a non standard user. +Standard users are: +%s.''' % ", ".join(standard_users), + +'non-standard-gid', +'''A file in this package is owned by a non standard group. +Standard groups are: +%s.''' % ", ".join(standard_groups), + +'library-without-ldconfig-postin', +'''This package contains a library and provides no %post scriptlet containing +a call to ldconfig.''', + +'postin-without-ldconfig', +'''This package contains a library and its %post scriptlet doesn't call +ldconfig.''', + +'library-without-ldconfig-postun', +'''This package contains a library and provides no %postun scriptlet containing +a call to ldconfig.''', + +'postun-without-ldconfig', +'''This package contains a library and its %postun doesn't call ldconfig.''', + +'info-files-without-install-info-postin', +'''This package contains info files and provides no %post scriptlet containing +a call to install-info.''', + +'postin-without-install-info', +'''This package contains info files and its %post doesn't call install-info.''', + +'info-files-without-install-info-postun', +'''This package contains info files and provides no %postun scriptlet containing +a call to install-info.''', + +'postun-without-install-info', +'''This package contains info files and its %postun doesn't call +install-info.''', + +'perl-temp-file', +'''You have a perl temporary file in your package. Usually, this +file is beginning with a dot (.) and contain "perl" in its name.''', + +'dir-or-file-in-tmp', +'''A file in the package is located in /tmp. It's not permitted +for packages to install files in this directory.''', + +'dir-or-file-in-mnt', +'''A file in the package is located in /mnt. It's not permitted +for packages to install files in this directory.''', + +'dir-or-file-in-opt', +'''A file in the package is located in /opt. It's not permitted +for packages to install files in this directory.''', + +'dir-or-file-in-usr-local', +'''A file in the package is located in /usr/local. It's not permitted +for packages to install files in this directory.''', + +'dir-or-file-in-var-local', +'''A file in the package is located in /var/local. It's not permitted +for packages to install files in this directory.''', + +'non-ghost-in-var-run', +'''A file or directory in the package is located in /var/run. Files installed +in this directory should be marked as %ghost and created at runtime to work +properly in tmpfs /var/run setups.''', + +'non-ghost-in-var-lock', +'''A file or directory in the package is located in /var/lock. Files installed +in this directory should be marked as %ghost and created at runtime to work +properly in tmpfs /var/lock setups.''', + +'subdir-in-bin', +'''The package contains a subdirectory in /usr/bin. It's not permitted to +create a subdir there. Create it in /usr/lib/ instead.''', + +'backup-file-in-package', +'''You have a file whose name looks like one for backup files, usually created +by an editor or resulting from applying unclean (fuzzy, or ones with line +offsets) patches.''', + +'dir-or-file-in-home', +'''A file in the package is located in /home. It's not permitted +for packages to install files in this directory.''', + +'version-control-internal-file', +'''You have included file(s) internally used by a version control system +in the package. Move these files out of the package and rebuild it.''', + +'htaccess-file', +'''You have individual apache configuration .htaccess file(s) in your package. +Replace them by a central configuration file in /etc/, according to the web +application packaging policy for your distribution.''', + +'info-dir-file', +'''You have /usr/info/dir or /usr/share/info/dir in your package. It will cause +conflicts with other packages and thus is not allowed. Please remove it and +rebuild your package.''', + +'non-conffile-in-etc', +'''A non-executable file in your package is being installed in /etc, but is not +a configuration file. All non-executable files in /etc should be configuration +files. Mark the file as %config in the spec file.''', + +'compressed-symlink-with-wrong-ext', +'''The symlink points to a compressed file but doesn't use the same +extension.''', + +'setuid-binary', +'''The file is setuid; this may be dangerous, especially if this +file is setuid root. Sometimes file capabilities can be used instead of +setuid bits.''', + +'setgid-binary', +'''The file is setgid. Usually this is a packaging bug. If this is a game, +then, you should use the proper rpm group, or location.''', + +'non-standard-executable-perm', +'''A standard executable should have permission set to 0755. If you get this +message, it means that you have a wrong executable permissions in some files +included in your package.''', + +'non-executable-in-bin', +'''A file is being installed in /usr/bin, but is not an executable. Be sure +that the file is an executable or that it has executable permissions.''', + +'devel-file-in-non-devel-package', +'''A development file (usually source code) is located in a non-devel +package. If you want to include source code in your package, be sure to +create a development package.''', + +'non-standard-dir-perm', +'''A standard directory should have permission set to 0755. If you get this +message, it means that you have wrong directory permissions in some dirs +included in your package.''', + +'spurious-executable-perm', +'''The file is installed with executable permissions, but was identified as one +that probably should not be executable. Verify if the executable bits are +desired, and remove if not.''', + +'world-writable', +'''A file or directory in the package is installed with world writable +permissions, which is most likely a security issue.''', + +'standard-dir-owned-by-package', +'''This package owns a directory that is part of the standard hierarchy, which +can lead to default directory permissions or ownerships being changed to +something non-standard.''', + +'no-dependency-on', +''' +''', + +'cross-directory-hard-link', +'''File is hard linked across directories. This can cause problems in +installations where the directories are located on different devices.''', + +'dangling-symlink', +'''The target of the symbolic link does not exist within this package or its +file based dependencies. Verify spelling of the link target and that the +target is included in a package in this package's dependency chain.''', + +'symlink-should-be-relative', +'''Absolute symlinks are problematic eg. when working with chroot environments. +symlinks(8) is a tool that can be useful for creating/dealing with relative +symlinks at package build time.''', + +'dangling-relative-symlink', +'''The target of the symbolic link does not exist within this package or its +file based dependencies. Verify spelling of the link target and that the +target is included in a package in this package's dependency chain.''', + +'symlink-has-too-many-up-segments', +''' +''', + +'symlink-should-be-absolute', +''' +''', + +'symlink-contains-up-and-down-segments', +''' +''', + +'non-readable', +'''The file can't be read by everybody. If this is expected (for security +reasons), contact your rpmlint distributor to get it added to the list of +exceptions for your distro (or add it to your local configuration if you +installed rpmlint from the source tarball).''', + +'incoherent-logrotate-file', +'''Your logrotate file should be named /etc/logrotate.d/<package name>.''', + +'non-root-user-log-file', +'''If you need log files owned by a non-root user, just create a subdir in +/var/log and put your log files in it.''', + +'non-root-group-log-file', +'''If you need log files owned by a non-root group, just create a subdir in +/var/log and put your log files in it.''', + +'non-ghost-file', +'''File should be tagged %ghost.''', + +'outside-libdir-files', +'''This library package must not contain non library files to allow 64 +and 32 bits versions of the package to coexist.''', + +'hidden-file-or-dir', +'''The file or directory is hidden. You should see if this is normal, +and delete it from the package if not.''', + +'module-without-depmod-postin', +'''This package contains a kernel module but provides no call to depmod in the +%post scriptlet.''', + +'postin-with-wrong-depmod', +'''This package contains a kernel module but its %post scriptlet calls depmod +for the wrong kernel.''', + +'module-without-depmod-postun', +'''This package contains a kernel module but provides no call to depmod in the +%postun scriptlet.''', + +'postun-with-wrong-depmod', +'''This package contains a kernel module but its %postun scriptlet calls depmod +for the wrong kernel.''', + +'log-files-without-logrotate', +'''This package contains files in /var/log/ without adding logrotate +configuration for them.''', + +'unexpanded-macro', +'''This package contains a file whose path contains something that looks like +an unexpanded macro; this is often the sign of a misspelling. Please check your +specfile.''', + +'manifest-in-perl-module', +'''This perl module package contains a MANIFEST or a MANIFEST.SKIP file +in the documentation directory.''', + +'siteperl-in-perl-module', +'''This perl module package installs files under the subdirectory site_perl, +while they must appear under vendor_perl.''', + +'executable-marked-as-config-file', +'''Executables must not be marked as config files because that may +prevent upgrades from working correctly. If you need to be able to +customize an executable, make it for example read a config file in +/etc/sysconfig.''', + +'sourced-script-with-shebang', +'''This text file contains a shebang, but is meant to be sourced, not +executed.''', + +'executable-sourced-script', +'''This text file has executable bit set, but is meant to be sourced, not +executed.''', + +'wrong-script-interpreter', +'''This script uses an incorrect interpreter.''', + +'non-executable-script', +'''This text file contains a shebang or is located in a path dedicated for +executables, but lacks the executable bits and cannot thus be executed. If +the file is meant to be an executable script, add the executable bits, +otherwise remove the shebang or move the file elsewhere.''', + +'script-without-shebang', +'''This text file has executable bits set or is located in a path dedicated +for executables, but lacks a shebang and cannot thus be executed. If the file +is meant to be an executable script, add the shebang, otherwise remove the +executable bits or move the file elsewhere.''', + +'wrong-script-end-of-line-encoding', +'''This script has wrong end-of-line encoding, usually caused by creation or +modification on a non-Unix system. It will prevent its execution.''', + +'wrong-file-end-of-line-encoding', +'''This file has wrong end-of-line encoding, usually caused by creation or +modification on a non-Unix system. It could prevent it from being displayed +correctly in some circumstances.''', + +'file-not-utf8', +'''The character encoding of this file is not UTF-8. Consider converting it +in the specfile's %prep section for example using iconv(1).''', + +'filename-not-utf8', +'''The character encoding of the name of this file is not UTF-8. +Rename it.''', + +'file-in-meta-package', +'''This package seems to be a meta-package (an empty package used to require +other packages), but it is not empty. You should remove or rename it, see the +option MetaPackageRegexp.''', + +'empty-debuginfo-package', +'''This debuginfo package contains no files. This is often a sign of binaries +being unexpectedly stripped too early during the build, rpmbuild not being able +to strip the binaries, the package actually being a noarch one but erratically +packaged as arch dependent, or something else. Verify what the case is, and +if there's no way to produce useful debuginfo out of it, disable creation of +the debuginfo package.''', + +'debuginfo-without-sources', +'''This debuginfo package appears to contain debug symbols but no source files. +This is often a sign of binaries being unexpectedly stripped too early during +the build, or being compiled without compiler debug flags (which again often +is a sign of distro's default compiler flags ignored which might have security +consequences), or other compiler flags which result in rpmbuild's debuginfo +extraction not working as expected. Verify that the binaries are not +unexpectedly stripped and that the intended compiler flags are used.''', + +'read-error', +'''This file could not be read. A reason for this could be that the info about +it in the rpm header indicates that it is supposed to be a readable normal file +but it actually is not in the filesystem. Because of this, some checks will +be skipped.''', + +'executable-crontab-file', +'''This crontab file has executable bit set, which is refused by newer version +of cron''', + +'non-owner-writeable-only-crontab-file', +'''This crontab file is writeable by other users as its owner, which is refused +by newer version of cron and insecure''', + +'symlink-crontab-file', +'''This crontab file is a symbolic link, which is insecure and refused by newer +version of cron''', + +'rpath-in-buildconfig', +'''This build configuration file contains rpaths which will be introduced into +dependent packages.''', + +'python-bytecode-wrong-magic-value', +'''The "magic" ABI version embedded in this python bytecode file isn't equal +to that of the corresponding runtime, which will force the interpreter to +recompile the .py source every time, ignoring the saved bytecode.''', + +'python-bytecode-inconsistent-mtime', +'''The timestamp embedded in this python bytecode file isn't equal to the mtime +of the original source file, which will force the interpreter to recompile the +.py source every time, ignoring the saved bytecode.''', + +'python-bytecode-without-source', +'''This python bytecode file (.pyo/.pyc) is not accompanied by its original +source file (.py)''', + +'duplicate-executable', +'''This executable file exists in more than one standard binary directories. +It can cause problems when dirs in $PATH are reordered.''', + +'no-manual-page-for-binary', +'''Each executable in standard binary directories should have a man page.''', + +'manual-page-warning', +'''This man page may contain problems that can cause it not to be formatted +as intended.''', + +'incorrect-fsf-address', +'''The Free Software Foundation address in this file seems to be outdated or +misspelled. Ask upstream to update the address, or if this is a license file, +possibly the entire file with a new copy available from the FSF.''', +) + +# FilesCheck.py ends here + +# Local variables: +# indent-tabs-mode: nil +# py-indent-offset: 4 +# End: +# ex: ts=4 sw=4 et |