def check_description(self, pkg, lang, ignored_words): description = pkg.langtag(rpm.RPMTAG_DESCRIPTION, lang) if not Pkg.is_utf8_bytestr(description): self.output.add_info('E', pkg, 'tag-not-utf8', '%description', lang) description = byte_to_string(description) self._unexpanded_macros(pkg, '%%description -l %s' % lang, description) if self.spellcheck: pkgname = byte_to_string(pkg.header[rpm.RPMTAG_NAME]) typos = self.spellchecker.spell_check(description, '%description -l {}', lang, pkgname, ignored_words) for typo in typos.items(): self.output.add_info('E', pkg, 'spelling-error', typo) for l in description.splitlines(): if len(l) > self.max_line_len: self.output.add_info('E', pkg, 'description-line-too-long', lang, l) res = self.forbidden_words_regex.search(l) if res and self.config.configuration['ForbiddenWords']: self.output.add_info('W', pkg, 'description-use-invalid-word', lang, res.group(1)) res = tag_regex.search(l) if res: self.output.add_info('W', pkg, 'tag-in-description', lang, res.group(1))
def check_summary(self, pkg, lang, ignored_words): summary = pkg.langtag(rpm.RPMTAG_SUMMARY, lang) if not Pkg.is_utf8_bytestr(summary): self.output.add_info('E', pkg, 'tag-not-utf8', 'Summary', lang) summary = byte_to_string(summary) self._unexpanded_macros(pkg, 'Summary(%s)' % lang, summary) if self.spellcheck: pkgname = byte_to_string(pkg.header[rpm.RPMTAG_NAME]) typos = self.spellchecker.spell_check(summary, 'Summary({})', lang, pkgname, ignored_words) for typo in typos.items(): self.output.add_info('E', pkg, 'spelling-error', typo) if '\n' in summary: self.output.add_info('E', pkg, 'summary-on-multiple-lines', lang) if (summary[0] != summary[0].upper() and summary.partition(' ')[0] not in CAPITALIZED_IGNORE_LIST): self.output.add_info('W', pkg, 'summary-not-capitalized', lang, summary) if summary[-1] == '.': self.output.add_info('W', pkg, 'summary-ended-with-dot', lang, summary) if len(summary) > self.max_line_len: self.output.add_info('E', pkg, 'summary-too-long', lang, summary) if leading_space_regex.search(summary): self.output.add_info('E', pkg, 'summary-has-leading-spaces', lang, summary) res = self.forbidden_words_regex.search(summary) if res and self.config.configuration['ForbiddenWords']: self.output.add_info('W', pkg, 'summary-use-invalid-word', lang, res.group(1)) if pkg.name: sepchars = r'[\s%s]' % punct res = re.search( r'(?:^|\s)(%s)(?:%s|$)' % (re.escape(pkg.name), sepchars), summary, re.IGNORECASE | re.UNICODE) if res: self.output.add_info('W', pkg, 'name-repeated-in-summary', lang, res.group(1))
def check(self, pkg): packager = pkg[rpm.RPMTAG_PACKAGER] if packager: self._unexpanded_macros(pkg, 'Packager', packager) if self.config.configuration['Packager'] and \ not self.packager_regex.search(packager): self.output.add_info('W', pkg, 'invalid-packager', packager) else: self.output.add_info('E', pkg, 'no-packager-tag') version = pkg[rpm.RPMTAG_VERSION] if version: self._unexpanded_macros(pkg, 'Version', version) res = invalid_version_regex.search(version) if res: self.output.add_info('E', pkg, 'invalid-version', version) else: self.output.add_info('E', pkg, 'no-version-tag') release = pkg[rpm.RPMTAG_RELEASE] if release: self._unexpanded_macros(pkg, 'Release', release) if self.release_ext and not self.extension_regex.search(release): self.output.add_info('W', pkg, 'not-standard-release-extension', release) else: self.output.add_info('E', pkg, 'no-release-tag') epoch = pkg[rpm.RPMTAG_EPOCH] if epoch is None: if self.use_epoch: self.output.add_info('E', pkg, 'no-epoch-tag') else: if epoch > 99: self.output.add_info('W', pkg, 'unreasonable-epoch', epoch) epoch = str(epoch) if self.use_epoch: for tag in ('obsoletes', 'conflicts', 'provides', 'recommends', 'suggests', 'enhances', 'supplements'): for x in (x for x in getattr(pkg, tag)() if x[1] and x[2][0] is None): self.output.add_info('W', pkg, 'no-epoch-in-%s' % tag, Pkg.formatRequire(*x)) name = pkg.name deps = pkg.requires + pkg.prereq devel_depend = False is_devel = FilesCheck.devel_regex.search(name) is_source = pkg.is_source for d in deps: value = Pkg.formatRequire(*d) if self.use_epoch and d[1] and d[2][0] is None and \ not d[0].startswith('rpmlib('): self.output.add_info('W', pkg, 'no-epoch-in-dependency', value) for r in self.invalid_requires: if r.search(d[0]): self.output.add_info('E', pkg, 'invalid-dependency', d[0]) if d[0].startswith('/usr/local/'): self.output.add_info('E', pkg, 'invalid-dependency', d[0]) if is_source: if lib_devel_number_regex.search(d[0]): self.output.add_info('E', pkg, 'invalid-build-requires', d[0]) elif not is_devel: if not devel_depend and FilesCheck.devel_regex.search(d[0]): self.output.add_info('E', pkg, 'devel-dependency', d[0]) devel_depend = True if not d[1]: res = lib_package_regex.search(d[0]) if res and not res.group(1): self.output.add_info('E', pkg, 'explicit-lib-dependency', d[0]) if d[1] == rpm.RPMSENSE_EQUAL and d[2][2] is not None: self.output.add_info('W', pkg, 'requires-on-release', value) self._unexpanded_macros(pkg, 'dependency %s' % (value, ), value) self._unexpanded_macros(pkg, 'Name', name) if not name: self.output.add_info('E', pkg, 'no-name-tag') else: if is_devel and not is_source: base = is_devel.group(1) dep = None has_so = False has_pc = False for fname in pkg.files: if fname.endswith('.so'): has_so = True if pkg_config_regex.match(fname) and fname.endswith('.pc'): has_pc = True if has_so: base_or_libs = base + '*' + '/' + base + '-libs/lib' + base + '*' # try to match *%_isa as well (e.g. '(x86-64)', '(x86-32)') base_or_libs_re = re.compile( r'^(lib)?%s(-libs)?[\d_-]*(\(\w+-\d+\))?$' % re.escape(base)) for d in deps: if base_or_libs_re.match(d[0]): dep = d break if not dep: self.output.add_info('W', pkg, 'no-dependency-on', base_or_libs) elif version: exp = (epoch, version, None) sexp = Pkg.versionToString(exp) if not dep[1]: self.output.add_info('W', pkg, 'no-version-dependency-on', base_or_libs, sexp) elif dep[2][:2] != exp[:2]: self.output.add_info( 'W', pkg, 'incoherent-version-dependency-on', base_or_libs, Pkg.versionToString( (dep[2][0], dep[2][1], None)), sexp) res = devel_number_regex.search(name) if not res: self.output.add_info('W', pkg, 'no-major-in-name', name) else: if res.group(3): prov = res.group(1) + res.group(2) + '-devel' else: prov = res.group(1) + '-devel' if prov not in (x[0] for x in pkg.provides): self.output.add_info('W', pkg, 'no-provides', prov) if has_pc: found_pkg_config_dep = False for p in (x[0] for x in pkg.provides): if p.startswith('pkgconfig('): found_pkg_config_dep = True break if not found_pkg_config_dep: self.output.add_info('E', pkg, 'no-pkg-config-provides') # List of words to ignore in spell check ignored_words = set() for pf in pkg.files: ignored_words.update(pf.split('/')) ignored_words.update((x[0] for x in pkg.provides)) ignored_words.update((x[0] for x in pkg.requires)) ignored_words.update((x[0] for x in pkg.conflicts)) ignored_words.update((x[0] for x in pkg.obsoletes)) langs = pkg[rpm.RPMTAG_HEADERI18NTABLE] summary = byte_to_string(pkg[rpm.RPMTAG_SUMMARY]) if summary: if not langs: self._unexpanded_macros(pkg, 'Summary', summary) else: for lang in langs: self.check_summary(pkg, lang, ignored_words) else: self.output.add_info('E', pkg, 'no-summary-tag') description = byte_to_string(pkg[rpm.RPMTAG_DESCRIPTION]) if description: if not langs: self._unexpanded_macros(pkg, '%description', description) else: for lang in langs: self.check_description(pkg, lang, ignored_words) if len(description) < len(pkg[rpm.RPMTAG_SUMMARY]): self.output.add_info('W', pkg, 'description-shorter-than-summary') else: self.output.add_info('E', pkg, 'no-description-tag') group = pkg[rpm.RPMTAG_GROUP] self._unexpanded_macros(pkg, 'Group', group) if not group: self.output.add_info('E', pkg, 'no-group-tag') elif pkg.name.endswith( '-devel') and not group.startswith('Development/'): self.output.add_info('W', pkg, 'devel-package-with-non-devel-group', group) elif self.valid_groups and group not in self.valid_groups: self.output.add_info('W', pkg, 'non-standard-group', group) buildhost = pkg[rpm.RPMTAG_BUILDHOST] self._unexpanded_macros(pkg, 'BuildHost', buildhost) if not buildhost: self.output.add_info('E', pkg, 'no-buildhost-tag') elif self.config.configuration['ValidBuildHost'] and \ not self.valid_buildhost_regex.search(buildhost): self.output.add_info('W', pkg, 'invalid-buildhost', buildhost) changelog = pkg[rpm.RPMTAG_CHANGELOGNAME] if not changelog: self.output.add_info('E', pkg, 'no-changelogname-tag') else: clt = pkg[rpm.RPMTAG_CHANGELOGTEXT] if self.use_version_in_changelog: ret = changelog_version_regex.search( byte_to_string(changelog[0])) if not ret and clt: # we also allow the version specified as the first # thing on the first line of the text ret = changelog_text_version_regex.search( byte_to_string(clt[0])) if not ret: self.output.add_info('W', pkg, 'no-version-in-last-changelog') elif version and release: srpm = pkg[rpm.RPMTAG_SOURCERPM] or '' # only check when source name correspond to name if srpm[0:-8] == '%s-%s-%s' % (name, version, release): expected = [version + '-' + release] if epoch is not None: # regardless of use_epoch expected[0] = str(epoch) + ':' + expected[0] # Allow EVR in changelog without release extension, # the extension is often a macro or otherwise dynamic. if self.release_ext: expected.append( self.extension_regex.sub('', expected[0])) if ret.group(1) not in expected: if len(expected) == 1: expected = expected[0] self.output.add_info( 'W', pkg, 'incoherent-version-in-changelog', ret.group(1), expected) if clt: changelog = changelog + clt for s in changelog: if not Pkg.is_utf8_bytestr(s): self.output.add_info('E', pkg, 'tag-not-utf8', '%changelog') break e = Pkg.has_forbidden_controlchars(s) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%%changelog : %s' % e) break clt = pkg[rpm.RPMTAG_CHANGELOGTIME][0] if clt: clt -= clt % (24 * 3600) # roll back to 00:00:00, see #246 if clt < oldest_changelog_timestamp: self.output.add_info( 'W', pkg, 'changelog-time-overflow', time.strftime('%Y-%m-%d', time.gmtime(clt))) elif clt > time.time(): self.output.add_info( 'E', pkg, 'changelog-time-in-future', time.strftime('%Y-%m-%d', time.gmtime(clt))) def split_license(text): return (x.strip() for x in (l for l in license_regex.split(text) if l)) def split_license_exception(text): x, y = license_exception_regex.split(text)[1:3] or (text, '') return x.strip(), y.strip() rpm_license = pkg[rpm.RPMTAG_LICENSE] if not rpm_license: self.output.add_info('E', pkg, 'no-license') else: valid_license = True if rpm_license not in self.valid_licenses: license_string = rpm_license l1, lexception = split_license_exception(rpm_license) # SPDX allows "<license> WITH <license-exception>" if lexception: license_string = l1 if lexception not in self.valid_license_exceptions: self.output.add_info('W', pkg, 'invalid-license-exception', lexception) valid_license = False for l1 in split_license(license_string): if l1 in self.valid_licenses: continue for l2 in split_license(l1): if l2 not in self.valid_licenses: self.output.add_info('W', pkg, 'invalid-license', l2) valid_license = False if not valid_license: self._unexpanded_macros(pkg, 'License', rpm_license) for tag in ('URL', 'DistURL', 'BugURL'): if hasattr(rpm, 'RPMTAG_%s' % tag.upper()): url = byte_to_string(pkg[getattr(rpm, 'RPMTAG_%s' % tag.upper())]) self._unexpanded_macros(pkg, tag, url, is_url=True) if url: (scheme, netloc) = urlparse(url)[0:2] if not scheme or not netloc or '.' not in netloc or \ scheme not in ('http', 'https', 'ftp') or \ (self.config.configuration['InvalidURL'] and self.invalid_url_regex.search(url)): self.output.add_info('W', pkg, 'invalid-url', tag, url) elif tag == 'URL': self.output.add_info('W', pkg, 'no-url-tag') obs_names = [x[0] for x in pkg.obsoletes] prov_names = [x[0] for x in pkg.provides] for o in (x for x in obs_names if x not in prov_names): self.output.add_info('W', pkg, 'obsolete-not-provided', o) for o in pkg.obsoletes: value = Pkg.formatRequire(*o) self._unexpanded_macros(pkg, 'Obsoletes %s' % (value, ), value) # TODO: should take versions, <, <=, =, >=, > into account here # https://bugzilla.redhat.com/460872 useless_provides = set() for p in prov_names: if (prov_names.count(p) != 1 and not p.startswith('debuginfo(') and p not in useless_provides): useless_provides.add(p) for p in sorted(useless_provides): self.output.add_info('E', pkg, 'useless-provides', p) for tagname, items in (('Provides', pkg.provides), ('Conflicts', pkg.conflicts), ('Obsoletes', pkg.obsoletes), ('Supplements', pkg.supplements), ('Suggests', pkg.suggests), ('Enhances', pkg.enhances), ('Recommends', pkg.recommends)): for p in items: e = Pkg.has_forbidden_controlchars(p) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%s: %s' % (tagname, e)) value = Pkg.formatRequire(*p) self._unexpanded_macros(pkg, '%s %s' % (tagname, value), value) for p in (pkg.requires): e = Pkg.has_forbidden_controlchars(p) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Requires: %s' % e) obss = pkg.obsoletes if obss: provs = pkg.provides for prov in provs: for obs in obss: if Pkg.rangeCompare(obs, prov): self.output.add_info( 'W', pkg, 'self-obsoletion', '%s obsoletes %s' % (Pkg.formatRequire(*obs), Pkg.formatRequire(*prov))) expfmt = rpm.expandMacro('%{_build_name_fmt}') if pkg.is_source: # _build_name_fmt often (always?) ends up not outputting src/nosrc # as arch for source packages, do it ourselves expfmt = re.sub(r'(?i)%\{?ARCH\b\}?', pkg.arch, expfmt) expected = pkg.header.sprintf(expfmt).split('/')[-1] basename = Path(pkg.filename).parent if basename != expected: self.output.add_info('W', pkg, 'non-coherent-filename', basename, expected) for tag in ('Distribution', 'DistTag', 'ExcludeArch', 'ExcludeOS', 'Vendor'): if hasattr(rpm, 'RPMTAG_%s' % tag.upper()): res = byte_to_string(pkg[getattr(rpm, 'RPMTAG_%s' % tag.upper())]) self._unexpanded_macros(pkg, tag, res)
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)