Ejemplo n.º 1
0
    def check_file(self, pkg, filename):
        root = pkg.dirName()
        f = root + filename
        st = getstatusoutput(('desktop-file-validate', f), True)
        if st[0]:
            error_printed = False
            for line in st[1].splitlines():
                if 'error: ' in line:
                    self.output.add_info('E', pkg, 'invalid-desktopfile', filename,
                                         line.split('error: ')[1])
                    error_printed = True
            if not error_printed:
                self.output.add_info('E', pkg, 'invalid-desktopfile', filename)
        if not is_utf8(f):
            self.output.add_info('E', pkg, 'non-utf8-desktopfile', filename)

        self.parse_desktop_file(pkg, root, f, filename)
Ejemplo n.º 2
0
    def check(self, pkg):

        for filename in pkg.header[rpm.RPMTAG_FILENAMES] or ():
            if not is_utf8_bytestr(filename):
                self.output.add_info('E', pkg, 'filename-not-utf8', byte_to_string(filename))

        # Rest of the checks are for binary packages only
        if pkg.is_source:
            return

        files = pkg.files

        # Check if the package is a development package
        devel_pkg = devel_regex.search(pkg.name)

        if not devel_pkg:
            for p in pkg.provides:
                if devel_regex.search(p[0]):
                    devel_pkg = True
                    break

        config_files = pkg.config_files
        ghost_files = pkg.ghost_files
        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)
        debugsource_package = debugsource_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_files = []
        logrotate_file = False
        debuginfo_srcs = False
        debuginfo_debugs = False

        if not lib_package and not pkg.doc_files:
            self.output.add_info('W', pkg, 'no-documentation')

        if files:
            if self.meta_package_regex.search(pkg.name):
                self.output.add_info('W', pkg, 'file-in-meta-package')
        elif debuginfo_package or debugsource_package:
            self.output.add_info('E', pkg, 'empty-debuginfo-package')

        # 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)

        # 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 pkg.doc_files
            nonexec_file = False

            self._check_manpage_compressed(pkg, f)
            self._check_infopage_compressed(pkg, f)

            for match in self.macro_regex.findall(f):
                self.output.add_info('W', pkg, 'unexpanded-macro', f, match)
            if user not in self.standard_users:
                self.output.add_info('W', pkg, 'non-standard-uid', f, user)
            if group not in self.standard_groups:
                self.output.add_info('W', pkg, 'non-standard-gid', f, group)

            if not self.module_rpms_ok and kernel_modules_regex.search(f) and not \
                    is_kernel_package:
                self.output.add_info('E', pkg, 'kernel-modules-not-in-kernel-packages', f)

            for i in self.disallowed_dirs:
                if f.startswith(i):
                    self.output.add_info('E', pkg, 'dir-or-file-in-%s' %
                                         '-'.join(i.split('/')[1:]), f)

            if f.startswith('/run/'):
                if f not in ghost_files:
                    self.output.add_info('W', pkg, 'non-ghost-in-run', f)
            elif f.startswith('/etc/systemd/system/'):
                self.output.add_info('W', pkg, 'systemd-unit-in-etc', f)
            elif f.startswith('/etc/udev/rules.d/'):
                self.output.add_info('W', pkg, 'udev-rule-in-etc', f)
            elif f.startswith('/etc/tmpfiles.d/'):
                self.output.add_info('W', pkg, 'tmpfiles-conf-in-etc', f)
            elif sub_bin_regex.search(f):
                self.output.add_info('E', pkg, 'subdir-in-bin', f)
            elif '/site_perl/' in f:
                self.output.add_info('W', pkg, 'siteperl-in-perl-module', f)

            if backup_regex.search(f):
                self.output.add_info('E', pkg, 'backup-file-in-package', f)
            elif scm_regex.search(f):
                self.output.add_info('E', pkg, 'version-control-internal-file', f)
            elif f.endswith('/.htaccess'):
                self.output.add_info('E', pkg, 'htaccess-file', f)
            elif hidden_file_regex.search(f) and not f.startswith('/etc/skel/') and not f.endswith('/.build-id'):
                self.output.add_info('W', pkg, 'hidden-file-or-dir', f)
            elif manifest_perl_regex.search(f):
                self.output.add_info('W', pkg, 'manifest-in-perl-module', f)
            elif f == '/usr/info/dir' or f == '/usr/share/info/dir':
                self.output.add_info('E', pkg, 'info-dir-file', f)
            elif makefile_regex.search(f) and not f.startswith('/usr/share/selinux'):
                self.output.add_info('E', pkg, 'makefile-junk', f)

            res = logrotate_regex.search(f)
            if res:
                logrotate_file = True
                if res.group(1) != pkg.name:
                    self.output.add_info('E', pkg, 'incoherent-logrotate-file', f)

            deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests]
            if res and not ('logrotate' in deps) and pkg.name != 'logrotate':
                self.output.add_info('E', pkg, 'missing-dependency-to-logrotate', 'for logrotate script', f)
            if f.startswith('/etc/cron.') \
               and not ('cron' in deps) and pkg.name != 'cron':
                self.output.add_info('E', pkg, 'missing-dependency-to-cron', 'for cron script', f)
            if f.startswith('/etc/xinet.d/') \
               and not ('xinetd' in deps) and pkg.name != 'xinetd':
                self.output.add_info('E', pkg, 'missing-dependency-to-xinetd', 'for xinet.d script', f)

            if link != '':
                ext = compr_regex.search(link)
                if ext:
                    if not re.compile(r'\.%s$' % ext.group(1)).search(f):
                        self.output.add_info('E', pkg, 'compressed-symlink-with-wrong-ext',
                                             f, link)

            perm = mode & 0o7777
            mode_is_exec = mode & 0o111

            if log_regex.search(f):
                log_files.append(f)

            # Hardlink check
            for hardlink in hardlinks.get((rdev, inode), ()):
                if Path(hardlink).parent != Path(f).parent:
                    self.output.add_info('W', pkg, 'cross-directory-hard-link', f, hardlink)
            hardlinks.setdefault((rdev, inode), []).append(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:
                        self.output.add_info('E', pkg, 'setuid-binary', f, user, '%o' % perm)
                    if stat.S_ISGID & mode:
                        if not (group == 'games' and
                                (games_path_regex.search(f) or
                                 self.games_group_regex.search(
                                    pkg[rpm.RPMTAG_GROUP]))):
                            self.output.add_info('E', pkg, 'setgid-binary', f, group,
                                                 '%o' % perm)
                    if mode & 0o777 != 0o755:
                        self.output.add_info('E', pkg, 'non-standard-executable-perm', f,
                                             '%o' % perm)

                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':
                        self.output.add_info('E', pkg, 'non-root-user-log-file', f, user)
                    if group != 'root':
                        self.output.add_info('E', pkg, 'non-root-group-log-file', f, group)
                    if f not in ghost_files:
                        self.output.add_info('E', pkg, 'non-ghost-file', f)

                chunk = None
                istext = False
                res = None
                try:
                    res = os.access(pkgfile.path, os.R_OK)
                except UnicodeError as e:  # e.g. non-ASCII, C locale, python 3
                    self.output.add_info('W', pkg, 'inaccessible-filename', f, e)
                else:
                    if res:
                        (chunk, istext) = self.peek(pkgfile.path, pkg)

                (interpreter, interpreter_args) = script_interpreter(chunk)

                if doc_regex.search(f):
                    if not interpreter:
                        nonexec_file = True
                    if not is_doc:
                        self.output.add_info('E', pkg, 'not-listed-as-documentation', f)

                if devel_pkg and f.endswith('.typelib'):
                    self.output.add_info('E', pkg, 'non-devel-file-in-devel-package', f)

                # check ldconfig call in %post and %postun
                if lib_regex.search(f):
                    if devel_pkg and not (sofile_regex.search(f) and stat.S_ISLNK(mode)):
                        self.output.add_info('E', pkg, 'non-devel-file-in-devel-package', f)
                    if not postin:
                        self.output.add_info('E', pkg, 'library-without-ldconfig-postin', f)
                    else:
                        if not ldconfig_regex.search(postin):
                            self.output.add_info('E', pkg, 'postin-without-ldconfig', f)

                    if not postun:
                        self.output.add_info('E', pkg, 'library-without-ldconfig-postun', f)
                    else:
                        if not ldconfig_regex.search(postun):
                            self.output.add_info('E', 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(
                        r'\bdepmod\s+-a.*F\s+/boot/System\.map-' +
                        re.escape(kernel_version) + r'\b.*\b' +
                        re.escape(kernel_version) + r'\b',
                        re.MULTILINE | re.DOTALL)

                    if not postin or not depmod_regex.search(postin):
                        self.output.add_info('E', pkg, 'module-without-depmod-postin', f)
                    # check that we run depmod on the right kernel
                    elif not kernel_version_regex.search(postin):
                        self.output.add_info('E', pkg, 'postin-with-wrong-depmod', f)

                    if not postun or not depmod_regex.search(postun):
                        self.output.add_info('E', pkg, 'module-without-depmod-postun', f)
                    # check that we run depmod on the right kernel
                    elif not kernel_version_regex.search(postun):
                        self.output.add_info('E', pkg, 'postun-with-wrong-depmod', f)

                # check install-info call in %post and %postun
                if f.startswith('/usr/share/info/'):
                    if not postin:
                        self.output.add_info('E', pkg,
                                             'info-files-without-install-info-postin', f)
                    elif not install_info_regex.search(postin):
                        self.output.add_info('E', pkg, 'postin-without-install-info', f)

                    preun = pkg[rpm.RPMTAG_PREUN] or \
                        pkg.scriptprog(rpm.RPMTAG_PREUNPROG)
                    if not postun and not preun:
                        self.output.add_info('E', pkg,
                                             'info-files-without-install-info-postun', f)
                    elif not ((postun and install_info_regex.search(postun)) or
                              (preun and install_info_regex.search(preun))):
                        self.output.add_info('E', pkg, 'postin-without-install-info', f)

                # check perl temp file
                if perl_temp_file_regex.search(f):
                    self.output.add_info('W', pkg, 'perl-temp-file', f)

                is_buildconfig = istext and buildconfigfile_regex.search(f)

                # check rpaths in buildconfig files
                if is_buildconfig:
                    ln = pkg.grep(buildconfig_rpath_regex, f)
                    if ln:
                        self.output.add_info('E', pkg, 'rpath-in-buildconfig', f, 'lines', ln)

                res = bin_regex.search(f)
                if res:
                    if not mode_is_exec:
                        self.output.add_info('W', pkg, 'non-executable-in-bin', f,
                                             '%o' % 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
                    (is_buildconfig or includefile_regex.search(f) or
                     develfile_regex.search(f))):
                    self.output.add_info('W', pkg, 'devel-file-in-non-devel-package', f)
                if mode & 0o444 != 0o444 and perm & 0o7000 == 0:
                    ok_nonreadable = False
                    for regex in non_readable_regexs:
                        if regex.search(f):
                            ok_nonreadable = True
                            break
                    if not ok_nonreadable:
                        self.output.add_info('E', pkg, 'non-readable', f, '%o' % perm)
                if size == 0 and not normal_zero_length_regex.search(f) and \
                        f not in ghost_files:
                    self.output.add_info('E', pkg, 'zero-length', f)

                if mode & stat.S_IWOTH:
                    self.output.add_info('E', pkg, 'world-writable', f, '%o' % perm)

                if not perl_dep_error:
                    res = perl_regex.search(f)
                    if res:
                        if self.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)):
                            self.output.add_info('E', 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
                            any((pkg.check_versioned_dep(dep, res.group(1))
                                for dep in (
                                    'python', 'python-base', 'python(abi)')))):
                        self.output.add_info('E', 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 = pyc_magic_from_chunk(chunk)
                            exp_magic, exp_version = get_expected_pyc_magic(f, self.python_default_version)
                            if exp_magic and found_magic not in exp_magic:
                                found_version = 'unknown'
                                for (pv, pm) in _python_magic_values.items():
                                    if found_magic in pm:
                                        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 %s (%s), found %d (%s)' %
                                       (' or '.join(map(str, exp_magic)),
                                        exp_version or self.python_default_version,
                                        found_magic, found_version))
                                if exp_version is not None:
                                    self.output.add_info('E', *msg)
                                else:
                                    self.output.add_info('W', *msg)

                            # Verify that the timestamp embedded in the .pyc
                            # header matches the mtime of the .py file:
                            pyc_timestamp = pyc_mtime_from_chunk(chunk)
                            # If it's a symlink, check target file mtime.
                            srcfile = pkg.readlink(files[source_file])
                            if not srcfile:
                                self.output.add_info('W', pkg, 'python-bytecode-without-source', f)
                            elif (pyc_timestamp is not None and
                                  pyc_timestamp != srcfile.mtime):
                                cts = datetime.fromtimestamp(
                                    pyc_timestamp).isoformat()
                                sts = datetime.fromtimestamp(
                                    srcfile.mtime).isoformat()
                                self.output.add_info('E',
                                                     pkg, 'python-bytecode-inconsistent-mtime',
                                                     f, cts, srcfile.name, sts)
                    else:
                        self.output.add_info('W', pkg, 'python-bytecode-without-source', f)

                # normal executable check
                if mode & stat.S_IXUSR and perm != 0o755:
                    self.output.add_info('E', pkg, 'non-standard-executable-perm',
                                         f, '%o' % perm)
                if mode_is_exec:
                    if f in config_files:
                        self.output.add_info('E', 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:
                        self.output.add_info('W', pkg, 'spurious-executable-perm', f)
                elif f.startswith('/etc/') and f not in config_files and \
                        f not in ghost_files:
                    self.output.add_info('W', pkg, 'non-conffile-in-etc', f)

                if pkg.arch == 'noarch' and f.startswith('/usr/lib64/python'):
                    self.output.add_info('E', 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 chunk:
                        # TODO: sequence based invocation
                        command = subprocess.run(
                            '%s %s | gtbl | groff -mtty-char -Tutf8 '
                            '-P-c -mandoc -w%s >%s' %
                            (catcmd(f), quote(pkgfile.path),
                             quote(self.man_warn_category), os.devnull),
                            shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                            env=dict(os.environ, LC_ALL='en_US.UTF-8'))

                        for line in command.stdout.decode().split('\n'):
                            res = man_warn_regex.search(line)
                            if not res or man_nowarn_regex.search(line):
                                continue
                            self.output.add_info('W', pkg, 'manual-page-warning', f,
                                                 line[res.end(1):])

                if f.endswith('.svgz') and f[0:-1] not in files \
                        and scalable_icon_regex.search(f):
                    self.output.add_info('W', pkg, 'gzipped-svg-icon', f)

                if f.endswith('.pem') and f not in ghost_files:
                    if pkg.grep(start_certificate_regex, f):
                        self.output.add_info('W', pkg, 'pem-certificate', f)
                    if pkg.grep(start_private_key_regex, f):
                        self.output.add_info('E', pkg, 'pem-private-key', f)

                if tcl_regex.search(f):
                    self.output.add_info('E', pkg, 'tcl-extension-file', f)

                # 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:
                            self.output.add_info('E', pkg,
                                                 'sourced-script-with-shebang', f,
                                                 interpreter, interpreter_args)
                        if mode_is_exec:
                            self.output.add_info('E', pkg, 'executable-sourced-script',
                                                 f, '%o' % perm)
                    # ...but executed ones should
                    elif interpreter or mode_is_exec or script_regex.search(f):
                        if interpreter:
                            res = interpreter_regex.search(interpreter)
                            if (mode_is_exec or script_regex.search(f)):
                                if res and res.group(1) == 'env':
                                    self.output.add_info('E', pkg, 'env-script-interpreter',
                                                         f, interpreter,
                                                         interpreter_args)
                                elif not res:
                                    self.output.add_info('E', pkg, 'wrong-script-interpreter',
                                                         f, interpreter,
                                                         interpreter_args)
                        elif not nonexec_file and not \
                                (lib_path_regex.search(f) and
                                 f.endswith('.la')):
                            self.output.add_info('E', pkg, 'script-without-shebang', f)

                        if not mode_is_exec and not is_doc and \
                                interpreter and interpreter.startswith('/'):
                            self.output.add_info('E', pkg, 'non-executable-script', f,
                                                 '%o' % perm, interpreter,
                                                 interpreter_args)
                        if b'\r' in chunk:
                            self.output.add_info('E', pkg, 'wrong-script-end-of-line-encoding', f)
                    elif is_doc and not self.skipdocs_regex.search(f):
                        if b'\r' in chunk:
                            self.output.add_info('W', 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 not is_utf8(pkgfile.path):
                            self.output.add_info('W', pkg, 'file-not-utf8', f)
                    if fsf_license_regex.search(chunk) and \
                            fsf_wrong_address_regex.search(chunk):
                        self.output.add_info('E', pkg, 'incorrect-fsf-address', f)

                elif is_doc and chunk and compr_regex.search(f):
                    ff = compr_regex.sub('', f)
                    if not self.skipdocs_regex.search(ff):
                        # compressed docs, eg. info and man files etc
                        if not is_utf8(pkgfile.path):
                            self.output.add_info('W', pkg, 'file-not-utf8', f)

            # normal dir check
            elif stat.S_ISDIR(mode):
                if mode & 0o1002 == 2:  # world writable w/o sticky bit
                    self.output.add_info('E', pkg, 'world-writable', f, '%o' % perm)
                if perm != 0o755:
                    self.output.add_info('E', pkg, 'non-standard-dir-perm', f, '%o' % perm)
                if pkg.name not in filesys_packages and f in STANDARD_DIRS:
                    self.output.add_info('E', pkg, 'standard-dir-owned-by-package', f)
                if hidden_file_regex.search(f) and not f.endswith('/.build-id'):
                    self.output.add_info('W', 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'):
                    self.output.add_info('W', 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 self.dangling_exceptions.values():
                            if e['path'].search(link):
                                is_exception = e['name']
                                break
                        if is_exception:
                            if is_exception not in req_names:
                                self.output.add_info('W', pkg, 'no-dependency-on',
                                                     is_exception)
                        else:
                            self.output.add_info('W', pkg, 'dangling-symlink', f, link)
                    linktop = r.group(1)
                    r = absolute_regex.search(f)
                    if r:
                        filetop = r.group(1)
                        if filetop == linktop or self.use_relative_symlinks:
                            self.output.add_info('W', pkg, 'symlink-should-be-relative',
                                                 f, link)
                # relative link
                else:
                    if not is_so:
                        abslink = '%s/%s' % (Path(f).parent, link)
                        abslink = os.path.normpath(abslink)
                        if abslink not in files and abslink not in req_names:
                            is_exception = False
                            for e in self.dangling_exceptions.values():
                                if e['path'].search(link):
                                    is_exception = e['name']
                                    break
                            if is_exception:
                                if is_exception not in req_names:
                                    self.output.add_info('W', pkg, 'no-dependency-on',
                                                         is_exception)
                            else:
                                self.output.add_info('W', 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:
                            self.output.add_info('E', 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:
                        #    self.output.add_info('W', pkg, 'lengthy-symlink', f, link)

                        # have we reached the root directory?
                        if len(pathcomponents) == 0 and linktop != lastpop \
                                and not self.use_relative_symlinks:
                            # relative link into other toplevel directory
                            self.output.add_info('W', pkg, 'symlink-should-be-absolute', f,
                                                 link)
                        # check additional segments for mistakes like
                        # `foo/../bar/'
                        for linksegment in mylink.split('/'):
                            if linksegment == '..':
                                self.output.add_info('E',
                                                     pkg,
                                                     'symlink-contains-up-and-down-segments',
                                                     f, link)

            if f.startswith('/etc/cron.d/'):
                if stat.S_ISLNK(mode):
                    self.output.add_info('E', pkg, 'symlink-crontab-file', f)

                if mode_is_exec:
                    self.output.add_info('E', pkg, 'executable-crontab-file', f)

                if stat.S_IWGRP & mode or stat.S_IWOTH & mode:
                    self.output.add_info('E', pkg, 'non-owner-writeable-only-crontab-file', f)

        if len(log_files) and not logrotate_file:
            self.output.add_info('W', pkg, 'log-files-without-logrotate', sorted(log_files))

        if lib_package and lib_file and non_lib_file:
            self.output.add_info('E', pkg, 'outside-libdir-files', non_lib_file)

        if not self.use_debugsource and debuginfo_package and debuginfo_debugs and not debuginfo_srcs:
            self.output.add_info('E', pkg, 'debuginfo-without-sources')

        for exe, paths in bindir_exes.items():
            if len(paths) > 1:
                self.output.add_info('W', pkg, 'duplicate-executable', exe, paths)
            if exe not in man_basenames:
                self.output.add_info('W', pkg, 'no-manual-page-for-binary', exe)
Ejemplo n.º 3
0
    def check_spec(self, pkg):
        self._spec_file = pkg.name
        spec_only = isinstance(pkg, Pkg.FakePkg)
        spec_lines = Pkg.readlines(self._spec_file)
        patches = {}
        applied_patches = []
        applied_patches_ifarch = []
        patches_auto_applied = False
        source_dir = False
        buildroot = False
        configure_linenum = None
        configure_cmdline = ''
        mklibname = False
        is_lib_pkg = False
        if_depth = 0
        ifarch_depth = -1
        current_section = 'package'
        buildroot_clean = {'clean': False, 'install': False}
        depscript_override = False
        depgen_disabled = False
        patch_fuzz_override = False
        indent_spaces = 0
        indent_tabs = 0
        section = {}
        # None == main package
        current_package = None
        package_noarch = {}

        if self._spec_file:
            if not Pkg.is_utf8(self._spec_file):
                self.output.add_info('E', pkg, 'non-utf8-spec-file',
                                     self._spec_name or self._spec_file)

        # gather info from spec lines

        pkg.current_linenum = 0

        nbsp = UNICODE_NBSP

        for line in spec_lines:

            pkg.current_linenum += 1

            char = line.find(nbsp)
            if char != -1:
                self.output.add_info('W', pkg, 'non-break-space', 'line %s, char %d' %
                                     (pkg.current_linenum, char))

            section_marker = False
            for sec, regex in section_regexs.items():
                res = regex.search(line)
                if res:
                    current_section = sec
                    section_marker = True
                    section[sec] = section.get(sec, 0) + 1
                    if sec in ('package', 'files'):
                        rest = filelist_regex.sub('', line[res.end() - 1:])
                        res = pkgname_regex.search(rest)
                        if res:
                            current_package = res.group(1)
                        else:
                            current_package = None
                    break

            if section_marker:

                if not is_lib_pkg and lib_package_regex.search(line):
                    is_lib_pkg = True

                continue

            if (current_section in Pkg.RPM_SCRIPTLETS + ('prep', 'build') and
                    contains_buildroot(line)):
                self.output.add_info('W', pkg, 'rpm-buildroot-usage', '%' + current_section,
                                     line[:-1].strip())

            if make_check_regex.search(line) and current_section not in \
                    ('check', 'changelog', 'package', 'description'):
                self.output.add_info('W', pkg, 'make-check-outside-check-section',
                                     line[:-1])

            if current_section in buildroot_clean and \
                    not buildroot_clean[current_section] and \
                    contains_buildroot(line) and rm_regex.search(line):
                buildroot_clean[current_section] = True

            if ifarch_regex.search(line):
                if_depth = if_depth + 1
                ifarch_depth = if_depth

            if if_regex.search(line):
                if_depth = if_depth + 1

            if setup_regex.match(line):
                if not setup_q_regex.search(line):
                    # Don't warn if there's a -T without -a or -b
                    if setup_t_regex.search(line):
                        if setup_ab_regex.search(line):
                            self.output.add_info('W', pkg, 'setup-not-quiet')
                    else:
                        self.output.add_info('W', pkg, 'setup-not-quiet')
                if current_section != 'prep':
                    self.output.add_info('W', pkg, 'setup-not-in-prep')
            elif autopatch_regex.search(line):
                patches_auto_applied = True
                if current_section != 'prep':
                    self.output.add_info('W', pkg, '%autopatch-not-in-prep')
            else:
                res = autosetup_regex.search(line)
                if res:
                    if not autosetup_n_regex.search(res.group(1)):
                        patches_auto_applied = True
                    if current_section != 'prep':
                        self.output.add_info('W', pkg, '%autosetup-not-in-prep')

            if endif_regex.search(line):
                if ifarch_depth == if_depth:
                    ifarch_depth = -1
                if_depth = if_depth - 1

            res = applied_patch_regex.search(line)
            if res:
                pnum = res.group(1) or 0
                for tmp in applied_patch_p_regex.findall(line) or [pnum]:
                    pnum = int(tmp)
                    applied_patches.append(pnum)
                    if ifarch_depth > 0:
                        applied_patches_ifarch.append(pnum)
            else:
                res = applied_patch_pipe_regex.search(line)
                if res:
                    pnum = int(res.group(1))
                    applied_patches.append(pnum)
                    if ifarch_depth > 0:
                        applied_patches_ifarch.append(pnum)
                else:
                    res = applied_patch_i_regex.search(line)
                    if res:
                        pnum = int(res.group(1))
                        applied_patches.append(pnum)
                        if ifarch_depth > 0:
                            applied_patches_ifarch.append(pnum)
            if not res and not source_dir:
                res = source_dir_regex.search(line)
                if res:
                    source_dir = True
                    self.output.add_info('E', pkg, 'use-of-RPM_SOURCE_DIR')

            if configure_linenum:
                if configure_cmdline[-1] == '\\':
                    configure_cmdline = configure_cmdline[:-1] + line.strip()
                else:
                    res = configure_libdir_spec_regex.search(configure_cmdline)
                    if not res:
                        # Hack to get the correct (start of ./configure) line
                        # number displayed:
                        real_linenum = pkg.current_linenum
                        pkg.current_linenum = configure_linenum
                        self.output.add_info('W', pkg, 'configure-without-libdir-spec')
                        pkg.current_linenum = real_linenum
                    elif res.group(1):
                        res = re.match(hardcoded_library_paths, res.group(1))
                        if res:
                            self.output.add_info('E', pkg, 'hardcoded-library-path',
                                                 res.group(1), 'in configure options')
                    configure_linenum = None

            hashPos = line.find('#')

            if current_section != 'changelog':
                cfgPos = line.find('./configure')
                if cfgPos != -1 and (hashPos == -1 or hashPos > cfgPos):
                    # store line where it started
                    configure_linenum = pkg.current_linenum
                    configure_cmdline = line.strip()

            res = hardcoded_library_path_regex.search(line)
            if current_section != 'changelog' and res and not \
                    (biarch_package_regex.match(pkg.name) or
                     self.hardcoded_lib_path_exceptions_regex.search(
                         res.group(1).lstrip())):
                self.output.add_info('E', pkg, 'hardcoded-library-path', 'in',
                                     res.group(1).lstrip())

            if '%mklibname' in line:
                mklibname = True

            if current_section == 'package':

                # Would be cleaner to get sources and patches from the
                # specfile parsed in Python (see below), but we want to
                # catch %ifarch'd etc ones as well, and also catch these when
                # the specfile is not parseable.

                res = patch_regex.search(line)
                if res:
                    pnum = int(res.group(1) or 0)
                    patches[pnum] = res.group(2)

                res = obsolete_tags_regex.search(line)
                if res:
                    self.output.add_info('W', pkg, 'obsolete-tag', res.group(1))

                res = buildroot_regex.search(line)
                if res:
                    buildroot = True
                    if res.group(1).startswith('/'):
                        self.output.add_info('W', pkg, 'hardcoded-path-in-buildroot-tag',
                                             res.group(1))

                res = buildarch_regex.search(line)
                if res:
                    if res.group(1) != 'noarch':
                        self.output.add_info('E', pkg,
                                             'buildarch-instead-of-exclusivearch-tag',
                                             res.group(1))
                    else:
                        package_noarch[current_package] = True

                res = packager_regex.search(line)
                if res:
                    self.output.add_info('W', pkg, 'hardcoded-packager-tag', res.group(1))

                res = prefix_regex.search(line)
                if res:
                    if not res.group(1).startswith('%'):
                        self.output.add_info('W', pkg, 'hardcoded-prefix-tag', res.group(1))

                res = prereq_regex.search(line)
                if res:
                    self.output.add_info('E', pkg, 'prereq-use', res.group(2))

                res = buildprereq_regex.search(line)
                if res:
                    self.output.add_info('E', pkg, 'buildprereq-use', res.group(1))

                if scriptlet_requires_regex.search(line):
                    self.output.add_info('E', pkg, 'broken-syntax-in-scriptlet-requires',
                                         line.strip())

                res = requires_regex.search(line)
                if res:
                    reqs = Pkg.parse_deps(res.group(1))
                    e = Pkg.has_forbidden_controlchars(reqs)
                    if e:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             'Requires: %s' % e)
                    for req in unversioned(reqs):
                        if compop_regex.search(req):
                            self.output.add_info('W', pkg,
                                                 'comparison-operator-in-deptoken',
                                                 req)

                res = provides_regex.search(line)
                if res:
                    provs = Pkg.parse_deps(res.group(1))
                    e = Pkg.has_forbidden_controlchars(provs)
                    if e:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             'Provides: %s' % e)
                    for prov in unversioned(provs):
                        if not prov.startswith('/'):
                            self.output.add_info('W', pkg, 'unversioned-explicit-provides',
                                                 prov)
                        if compop_regex.search(prov):
                            self.output.add_info('W', pkg,
                                                 'comparison-operator-in-deptoken',
                                                 prov)

                res = obsoletes_regex.search(line)
                if res:
                    obses = Pkg.parse_deps(res.group(1))
                    e = Pkg.has_forbidden_controlchars(obses)
                    if e:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             'Obsoletes: %s' % e)
                    for obs in unversioned(obses):
                        if not obs.startswith('/'):
                            self.output.add_info('W', pkg, 'unversioned-explicit-obsoletes',
                                                 obs)
                        if compop_regex.search(obs):
                            self.output.add_info('W', pkg,
                                                 'comparison-operator-in-deptoken',
                                                 obs)

                res = conflicts_regex.search(line)
                if res:
                    confs = Pkg.parse_deps(res.group(1))
                    e = Pkg.has_forbidden_controlchars(confs)
                    if e:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             'Conflicts: %s' % e)
                    for conf in unversioned(confs):
                        if compop_regex.search(conf):
                            self.output.add_info('W', pkg,
                                                 'comparison-operator-in-deptoken',
                                                 conf)

            if current_section == 'changelog':
                e = Pkg.has_forbidden_controlchars(line)
                if e:
                    self.output.add_info('E', pkg,
                                         'forbidden-controlchar-found',
                                         '%%changelog: %s' % e)
                for match in self.macro_regex.findall(line):
                    res = re.match('%+', match)
                    if len(res.group(0)) % 2:
                        self.output.add_info('W', pkg, 'macro-in-%changelog', match)
            else:
                if not depscript_override:
                    depscript_override = \
                        depscript_override_regex.search(line) is not None
                if not depgen_disabled:
                    depgen_disabled = \
                        depgen_disable_regex.search(line) is not None
                if not patch_fuzz_override:
                    patch_fuzz_override = \
                        patch_fuzz_override_regex.search(line) is not None

            if current_section == 'files':
                # TODO: check scriptlets for these too?
                if package_noarch.get(current_package) or \
                        (current_package not in package_noarch and
                         package_noarch.get(None)):
                    res = libdir_regex.search(line)
                    if res:
                        pkgname = current_package
                        if pkgname is None:
                            pkgname = '(main package)'
                        self.output.add_info('W', pkg, 'libdir-macro-in-noarch-package',
                                             pkgname, line.rstrip())

            if not indent_tabs and '\t' in line:
                indent_tabs = pkg.current_linenum
            if not indent_spaces and indent_spaces_regex.search(line):
                indent_spaces = pkg.current_linenum

            # Check if egrep or fgrep is used
            if current_section not in \
                    ('package', 'changelog', 'description', 'files'):
                greps = deprecated_grep_regex.findall(line)
                if greps:
                    self.output.add_info('W', pkg, 'deprecated-grep', greps)

            # If not checking spec file only, we're checking one inside a
            # SRPM -> skip this check to avoid duplicate warnings (#167)
            if spec_only and self.valid_groups and \
               line.lower().startswith('group:'):
                group = line[6:].strip()
                if group not in self.valid_groups:
                    self.output.add_info('W', pkg, 'non-standard-group', group)

            # Test if there are macros in comments
            if hashPos != -1 and \
                    (hashPos == 0 or line[hashPos - 1] in (' ', '\t')):
                for match in self.macro_regex.findall(
                        line[hashPos + 1:]):
                    res = re.match('%+', match)
                    if len(res.group(0)) % 2:
                        self.output.add_info('W', pkg, 'macro-in-comment', match)

        # Last line read is not useful after this point
        pkg.current_linenum = None

        for sect in (x for x in buildroot_clean if not buildroot_clean[x]):
            self.output.add_info('W', pkg, 'no-cleaning-of-buildroot', '%' + sect)

        if not buildroot:
            self.output.add_info('W', pkg, 'no-buildroot-tag')

        for sec in ('prep', 'build', 'install', 'clean'):
            if not section.get(sec):
                self.output.add_info('W', pkg, 'no-%%%s-section' % sec)
        for sec in ('changelog',):
            # prep, build, install, clean, check prevented by rpmbuild 4.4
            if section.get(sec, 0) > 1:
                self.output.add_info('W', pkg, 'more-than-one-%%%s-section' % sec)

        if is_lib_pkg and not mklibname:
            self.output.add_info('E', pkg, 'lib-package-without-%mklibname')

        if depscript_override and not depgen_disabled:
            self.output.add_info('W', pkg, 'depscript-without-disabling-depgen')

        if patch_fuzz_override:
            self.output.add_info('W', pkg, 'patch-fuzz-is-changed')

        if indent_spaces and indent_tabs:
            pkg.current_linenum = max(indent_spaces, indent_tabs)
            self.output.add_info('W', pkg, 'mixed-use-of-spaces-and-tabs',
                                 '(spaces: line %d, tab: line %d)' %
                                 (indent_spaces, indent_tabs))
            pkg.current_linenum = None

        # process gathered info
        if not patches_auto_applied:
            for pnum, pfile in patches.items():
                if pnum in applied_patches_ifarch:
                    self.output.add_info('W', pkg, '%ifarch-applied-patch',
                                         'Patch%d:' % pnum, pfile)
                if pnum not in applied_patches:
                    self.output.add_info('W', pkg, 'patch-not-applied',
                                         'Patch%d:' % pnum, pfile)

        # Rest of the checks require a real spec file
        if not self._spec_file:
            return

        # We'd like to parse the specfile only once using python bindings,
        # but it seems errors from rpmlib get logged to stderr and we can't
        # capture and print them nicely, so we do it once each way :P

        outcmd = subprocess.run(
            ('rpm', '-q', '--qf=', '-D', '_sourcedir %s' % Path(self._spec_file).parent, '--specfile', self._spec_file), stdout=subprocess.PIPE)
        text = outcmd.stdout.decode()
        if text.endswith('\n'):
            text = text[:-1]

        parse_error = False
        for line in text.splitlines():
            # No such file or dir hack: https://bugzilla.redhat.com/487855
            if 'No such file or directory' not in line:
                parse_error = True
                self.output.add_info('E', pkg, 'specfile-error', line)

        if not parse_error:
            # grab sources and patches from parsed spec object to get
            # them with macros expanded for URL checking

            spec_obj = None
            rpm.addMacro('_sourcedir', pkg.dirName())
            try:
                ts = rpm.TransactionSet()
                spec_obj = ts.parseSpec(str(self._spec_file))
            except (ValueError, rpm.error):
                # errors logged above already
                pass
            rpm.delMacro('_sourcedir')
            if spec_obj:
                try:
                    # rpm < 4.8.0
                    sources = spec_obj.sources()
                except TypeError:
                    # rpm >= 4.8.0
                    sources = spec_obj.sources
                for src in sources:
                    (url, num, flags) = src
                    (scheme, netloc) = urlparse(url)[0:2]
                    if flags & 1:  # rpmspec.h, rpm.org ticket #123
                        srctype = 'Source'
                    else:
                        srctype = 'Patch'
                    tag = '%s%s' % (srctype, num)
                    if scheme and netloc:
                        continue
                    elif srctype == 'Source' and tarball_regex.search(url):
                        self.output.add_info('W', pkg, 'invalid-url', '%s:' % tag, url)
Ejemplo n.º 4
0
 def _check_non_utf8_spec_file(self, pkg):
     """Check if spec file has UTF-8 character encoding."""
     if self._spec_file:
         if not Pkg.is_utf8(self._spec_file):
             self.output.add_info('E', pkg, 'non-utf8-spec-file',
                                  self._spec_name or self._spec_file)