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 _gather_aux(self, header, list, nametag, flagstag, versiontag, prereq=None): names = header[nametag] flags = header[flagstag] versions = header[versiontag] if versions: for loop in range(len(versions)): name = byte_to_string(names[loop]) evr = stringToVersion(byte_to_string(versions[loop])) if prereq is not None and flags[loop] & PREREQ_FLAG: prereq.append((name, flags[loop] & (~PREREQ_FLAG), evr)) else: list.append((name, flags[loop], evr))
def __comparePRCOs(self, old, new, name): try: oldflags = old[name[:-1] + 'FLAGS'] except ValueError: # assume tag not supported, e.g. Recommends with older rpm return newflags = new[name[:-1] + 'FLAGS'] # fix buggy rpm binding not returning list for single entries if not isinstance(oldflags, list): oldflags = [oldflags] if not isinstance(newflags, list): newflags = [newflags] o = zip(old[name], oldflags, old[name[:-1] + 'VERSION']) if not isinstance(o, list): o = list(o) n = zip(new[name], newflags, new[name[:-1] + 'VERSION']) if not isinstance(n, list): n = list(n) # filter self provides, TODO: self %name(%_isa) as well if name == 'PROVIDES': oldE = old['epoch'] is not None and str(old['epoch']) + ':' or '' oldV = '%s%s' % (oldE, old.format('%{VERSION}-%{RELEASE}')) oldNV = (old['name'], rpm.RPMSENSE_EQUAL, oldV.encode()) newE = new['epoch'] is not None and str(new['epoch']) + ':' or '' newV = '%s%s' % (newE, new.format('%{VERSION}-%{RELEASE}')) newNV = (new['name'], rpm.RPMSENSE_EQUAL, newV.encode()) o = [entry for entry in o if entry != oldNV] n = [entry for entry in n if entry != newNV] for oldentry in o: if oldentry not in n: namestr = name if namestr == 'REQUIRES': namestr = self.req2str(oldentry[1]) self.__add( self.DEPFORMAT, (self.REMOVED, namestr, byte_to_string(oldentry[0]), self.sense2str(oldentry[1]), byte_to_string(oldentry[2]))) for newentry in n: if newentry not in o: namestr = name if namestr == 'REQUIRES': namestr = self.req2str(newentry[1]) self.__add( self.DEPFORMAT, (self.ADDED, namestr, byte_to_string(newentry[0]), self.sense2str(newentry[1]), byte_to_string(newentry[2])))
def check(self, pkg): if pkg.is_source: return # populate scriptlets self.post = byte_to_string(pkg.header[rpm.RPMTAG_POSTIN]) self.postun = byte_to_string(pkg.header[rpm.RPMTAG_POSTUN]) if not self._check_ua_presence(pkg): return self._check_requirements(pkg) self._check_post_phase(pkg, self.post) self._check_postun_phase(pkg, self.postun) self._check_filelist(pkg)
def getstatusoutput(cmd, stdoutonly=False, shell=False, raw=False, lc_all='C'): """ A version of commands.getstatusoutput() which can take cmd as a sequence, thus making it potentially more secure. """ env = dict(os.environ, LC_ALL=lc_all) if stdoutonly: proc = subprocess.Popen(cmd, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=True, env=env) else: proc = subprocess.Popen(cmd, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env, stderr=subprocess.STDOUT, close_fds=True) proc.stdin.close() with proc.stdout: text = proc.stdout.read() sts = proc.wait() if not raw: text = byte_to_string(text) if text.endswith('\n'): text = text[:-1] if sts is None: sts = 0 return sts, text
def test_bytetostr(): """ Test bytetostr function """ list_items = ( b'\xc5\xbe\xc3\xad\xc5\xbeala', 'texty', ) item = b'p\xc5\x99\xc3\xad\xc5\xa1ern\xc4\x9b \xc5\xbelu\xc5\xa5ou\xc4\x8dk\xc3\xbd k\xc5\xaf\xc5\x88' result = helpers.byte_to_string(item) assert isinstance(result, str) assert result == 'příšerně žluťoučký kůň' result = helpers.byte_to_string(list_items) assert isinstance(result, list) assert result[0] == 'žížala'
def get_magic(path): # file() method evaluates every file twice with python2, # use descriptor() method instead try: fd = os.open(path, os.O_RDONLY) magic = byte_to_string(_magic.descriptor(fd)) os.close(fd) return magic except OSError: return ''
def check_file(self, pkg, filename): beam = BeamFile(pkg.files[filename].path) compile_state = byte_to_string(beam.compileinfo['source'].value) if 'debug_info' not in beam.compileinfo['options']: self.output.add_info('E', pkg, 'beam-compiled-without-debuginfo', filename) # This can't be an error as builddir can be user specific and vary between users # it could be error in OBS where all the builds are done by user abuild, not in # general. if not self.source_re.match(compile_state): self.output.add_info('W', pkg, 'beam-was-not-recompiled', filename, compile_state)
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): if pkg.is_source: return if self._check_libalternatives_presence(pkg): self.output.add_info('I', pkg, 'package supports libalternatives') self._check_libalternatives_requirements(pkg) self._check_libalternatives_filelist(pkg) self.install_binaries = {} self.slave_binaries = [] # populate scriptlets self.post = byte_to_string(pkg.header[rpm.RPMTAG_POSTIN]) self.postun = byte_to_string(pkg.header[rpm.RPMTAG_POSTUN]) if not self._check_ua_presence(pkg): return self.output.add_info('I', pkg, 'package supports update-alternatives') self._check_requirements(pkg) self._check_post_phase(pkg, self.post) self._check_postun_phase(pkg, self.postun) self._check_filelist(pkg)
def _normalize_script(self, script): """ Remove "backslash+newline" to keep all commands as oneliners. Remove single and double quotes everywhere. Keep only the line that contains the update-alternatives call. Return the list of lines that contain update-alternatives calls """ # with old rpm we get wrong type script = byte_to_string(script) script.replace('\\\n', '') script.replace('"', '') script.replace("'", '') script.strip() return [i for i in script.splitlines() if self.command in i]
def _check_doc_file_dependencies(self, pkg): """ Check if docfiles create additional dependencies in the package and print a warning if so. """ files = pkg.files reqs = {} for fname, pkgfile in files.items(): reqs[fname] = [x[0] for x in pkgfile.requires] core_reqs = {} # dependencies of non-doc files doc_reqs = {} # dependencies of doc files for dep in pkg.header.dsFromHeader(): # skip deps which were found by find-requires if dep.Flags() & rpm.RPMSENSE_FIND_REQUIRES != 0: continue core_reqs[dep.N()] = [] # register things which are provided by the package for i in pkg.header[rpm.RPMTAG_PROVIDES]: core_reqs[byte_to_string(i)] = [] for i in files: core_reqs[i] = [] for i in files: if not reqs[i]: continue # skip empty dependencies if i in pkg.doc_files: target = doc_reqs else: target = core_reqs for r in reqs[i]: if r not in target: target[r] = [] target[r].append(i) # go through the calculated requirements of the %doc files for (dep, req_files) in doc_reqs.items(): if dep not in core_reqs: for f in req_files: self.output.add_info('W', pkg, 'doc-file-dependency', f, dep)
def __getitem__(self, key): try: val = self.header[key] except KeyError: val = [] if val == []: return None else: # Note that text tags we want to try decoding for real in TagsCheck # such as summary, description and changelog are not here. if key in (rpm.RPMTAG_NAME, rpm.RPMTAG_VERSION, rpm.RPMTAG_RELEASE, rpm.RPMTAG_ARCH, rpm.RPMTAG_GROUP, rpm.RPMTAG_BUILDHOST, rpm.RPMTAG_LICENSE, rpm.RPMTAG_HEADERI18NTABLE, rpm.RPMTAG_PACKAGER, rpm.RPMTAG_SOURCERPM, rpm.RPMTAG_DISTRIBUTION, rpm.RPMTAG_VENDOR) \ or key in (x[0] for x in SCRIPT_TAGS) \ or key in (x[1] for x in SCRIPT_TAGS): val = byte_to_string(val) return val
def _check_url(self, pkg): """Trigger check invalid-url, no-url-tag """ for tag in ('URL', 'DistURL', 'BugURL'): if hasattr(rpm, 'RPMTAG_{}'.format(tag.upper())): url = byte_to_string(pkg[getattr( rpm, 'RPMTAG_{}'.format(tag.upper()))]) self._unexpanded_macros(pkg, tag, url, is_url=True) if url: (scheme, netloc) = urlparse(url)[0:2] # Check if a package contains a unreasonable URL # [This check is also triggered with Source: tag value] 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) # Check if a package does not have a URL: tag in its spec file elif tag == 'URL': self.output.add_info('W', pkg, 'no-url-tag')
def __init__(self, config, output, pkg, path, fname, is_ar, is_shlib): self.readelf_error = False self.needed = [] self.rpath = [] self.undef = [] self.unused = [] self.config = config self.output = output self.comment = False self.soname = False self.non_pic = True self.stack = False self.exec_stack = False self.exit_calls = [] self.forbidden_calls = [] fork_called = False self.tail = '' self.lto_sections = False self.no_text_in_archive = False self.setgid = False self.setuid = False self.setgroups = False self.mktemp = False self.forbidden_functions = self.config.configuration['WarnOnFunction'] if self.forbidden_functions: for name, func in self.forbidden_functions.items(): # precompile regexps f_name = func['f_name'] func['f_regex'] = create_nonlibc_regexp_call(f_name) if 'good_param' in func and func['good_param']: func['waiver_regex'] = re.compile(func['good_param']) # register descriptions self.output.error_details.update({name: func['description']}) is_debug = path.endswith('.debug') is_archive = path.endswith('.a') res = Pkg.getstatusoutput( ('readelf', '-W', '-S', '-l', '-d', '-s', path)) if not res[0]: lines = res[1].splitlines() # For an archive, test if all .text sections are empty if is_archive: has_text_segment = False non_zero_text_segment = False for line in lines: r = self.text_section_regex.search(line) if r: has_text_segment = True size = int(r.group(1), 16) if size > 0: non_zero_text_segment = True if has_text_segment and not non_zero_text_segment: self.no_text_in_archive = True for line in lines: if self.lto_section_name_prefix in line: self.lto_sections = True r = self.needed_regex.search(line) if r: self.needed.append(r.group(1)) continue r = self.rpath_regex.search(line) if r: for p in r.group(1).split(':'): self.rpath.append(p) continue if self.comment_regex.search(line): self.comment = True continue if self.pic_regex.search(line): self.non_pic = False continue r = self.soname_regex.search(line) if r: self.soname = r.group(1) continue r = self.stack_regex.search(line) if r: self.stack = True flags = r.group(1) if flags and self.stack_exec_regex.search(flags): self.exec_stack = True continue if line.startswith('Symbol table'): break for line in lines: r = self.call_regex.search(line) if not r: continue line = r.group(1) if self.mktemp_call_regex.search(line): self.mktemp = True if self.setgid_call_regex.search(line): self.setgid = True if self.setuid_call_regex.search(line): self.setuid = True if self.setgroups_call_regex.search(line): self.setgroups = True if self.forbidden_functions: for r_name, func in self.forbidden_functions.items(): ret = func['f_regex'].search(line) if ret: self.forbidden_calls.append(r_name) if is_shlib: r = self.exit_call_regex.search(line) if r: self.exit_calls.append(r.group(1)) continue r = self.fork_call_regex.search(line) if r: fork_called = True continue # check if we don't have a string that will automatically # waive the presence of a forbidden call if self.forbidden_calls: res = Pkg.getstatusoutput(('strings', path)) if not res[0]: for line in res[1].splitlines(): # as we need to remove elements, iterate backwards for i in range(len(self.forbidden_calls) - 1, -1, -1): func = self.forbidden_calls[i] f = self.forbidden_functions[func] if 'waiver_regex' not in f: continue r = f['waiver_regex'].search(line) if r: del self.forbidden_calls[i] if self.non_pic: self.non_pic = 'TEXTREL' in res[1] # Ignore all exit() calls if fork() is being called. # Does not have any context at all but without this kludge, the # number of false positives would probably be intolerable. if fork_called: self.exit_calls = [] else: self.readelf_error = True # Go and others are producing ar archives that don't have ELF # headers, so don't complain about it if not is_ar: self.output.add_info('W', pkg, 'binaryinfo-readelf-failed', fname, re.sub('\n.*', '', res[1])) try: with open(path, 'rb') as fobj: fobj.seek(-12, os.SEEK_END) self.tail = byte_to_string(fobj.read()) except Exception as e: self.output.add_info('W', pkg, 'binaryinfo-tail-failed %s: %s' % (fname, e)) # Undefined symbol and unused direct dependency checks make sense only # for installed packages. # skip debuginfo: https://bugzilla.redhat.com/190599 if not is_ar and not is_debug and isinstance(pkg, Pkg.InstalledPkg): # We could do this with objdump, but it's _much_ simpler with ldd. res = Pkg.getstatusoutput(('ldd', '-d', '-r', path)) if not res[0]: for line in res[1].splitlines(): undef = self.undef_regex.search(line) if undef: self.undef.append(undef.group(1)) if self.undef: try: res = Pkg.getstatusoutput(['c++filt'] + self.undef) if not res[0]: self.undef = res[1].splitlines() except OSError: pass else: self.output.add_info('W', pkg, 'ldd-failed', fname) res = Pkg.getstatusoutput(('ldd', '-r', '-u', path)) if res[0]: # Either ldd doesn't grok -u (added in glibc 2.3.4) or we have # unused direct dependencies in_unused = False for line in res[1].splitlines(): if not line.rstrip(): pass elif line.startswith('Unused direct dependencies'): in_unused = True elif in_unused: unused = self.unused_regex.search(line) if unused: self.unused.append(unused.group(1)) else: in_unused = False
def _gatherFilesInfo(self): ret = {} flags = self.header[rpm.RPMTAG_FILEFLAGS] modes = self.header[rpm.RPMTAG_FILEMODES] users = self.header[rpm.RPMTAG_FILEUSERNAME] groups = self.header[rpm.RPMTAG_FILEGROUPNAME] links = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILELINKTOS]] sizes = self.header[rpm.RPMTAG_FILESIZES] md5s = self.header[rpm.RPMTAG_FILEMD5S] mtimes = self.header[rpm.RPMTAG_FILEMTIMES] rdevs = self.header[rpm.RPMTAG_FILERDEVS] langs = self.header[rpm.RPMTAG_FILELANGS] inodes = self.header[rpm.RPMTAG_FILEINODES] requires = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILEREQUIRE]] provides = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILEPROVIDE]] files = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILENAMES]] magics = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILECLASS]] try: # rpm >= 4.7.0 filecaps = self.header[rpm.RPMTAG_FILECAPS] except AttributeError: filecaps = None # rpm-python < 4.6 does not return a list for this (or FILEDEVICES, # FWIW) for packages containing exactly one file if not isinstance(inodes, list): inodes = [inodes] if files: for idx in range(0, len(files)): pkgfile = PkgFile(files[idx]) pkgfile.path = os.path.normpath(os.path.join( self.dirName() or '/', pkgfile.name.lstrip('/'))) pkgfile.flags = flags[idx] pkgfile.mode = modes[idx] pkgfile.user = byte_to_string(users[idx]) pkgfile.group = byte_to_string(groups[idx]) pkgfile.linkto = links[idx] and os.path.normpath(links[idx]) pkgfile.size = sizes[idx] pkgfile.md5 = md5s[idx] pkgfile.mtime = mtimes[idx] pkgfile.rdev = rdevs[idx] pkgfile.inode = inodes[idx] pkgfile.requires = parse_deps(requires[idx]) pkgfile.provides = parse_deps(provides[idx]) pkgfile.lang = byte_to_string(langs[idx]) pkgfile.magic = magics[idx] if not pkgfile.magic: if stat.S_ISDIR(pkgfile.mode): pkgfile.magic = 'directory' elif stat.S_ISLNK(pkgfile.mode): pkgfile.magic = "symbolic link to `%s'" % pkgfile.linkto elif not pkgfile.size: pkgfile.magic = 'empty' if (not pkgfile.magic and not pkgfile.is_ghost and _magic): # file() method evaluates every file twice with python2, # use descriptor() method instead try: fd = os.open(pkgfile.path, os.O_RDONLY) pkgfile.magic = byte_to_string(_magic.descriptor(fd)) os.close(fd) except OSError: pass if pkgfile.magic is None: pkgfile.magic = '' elif Pkg._magic_from_compressed_re.search(pkgfile.magic): # Discard magic from inside compressed files ('file -z') # until PkgFile gets decompression support. We may get # such magic strings from package headers already now; # for example Fedora's rpmbuild as of F-11's 4.7.1 is # patched so it generates them. pkgfile.magic = '' if filecaps: pkgfile.filecaps = filecaps[idx] ret[pkgfile.name] = pkgfile return ret
def check(self, pkg): """Contains methods that checks tags and values in a spec file of a package.""" version = pkg[rpm.RPMTAG_VERSION] release = pkg[rpm.RPMTAG_RELEASE] epoch = pkg[rpm.RPMTAG_EPOCH] group = pkg[rpm.RPMTAG_GROUP] buildhost = pkg[rpm.RPMTAG_BUILDHOST] langs = pkg[rpm.RPMTAG_HEADERI18NTABLE] summary = byte_to_string(pkg[rpm.RPMTAG_SUMMARY]) description = byte_to_string(pkg[rpm.RPMTAG_DESCRIPTION]) changelog = pkg[rpm.RPMTAG_CHANGELOGNAME] rpm_license = pkg[rpm.RPMTAG_LICENSE] name = pkg.name deps = pkg.requires + pkg.prereq is_devel = FilesCheck.devel_regex.search(name) is_source = pkg.is_source # List of words to ignore in spell check ignored_words = set() for pf in pkg.files: ignored_words.update(pf.split('/')) for tag in ('provides', 'requires', 'conflicts', 'obsoletes'): ignored_words.update((x[0] for x in 'pkg.' + str(tag))) # Run checks for whole package self._check_invalid_packager(pkg) self._check_invalid_version_and_no_version_tag(pkg, version) self._check_non_standard_release_extension(pkg, release) self._check_no_epoch_tag(pkg, epoch) self._check_no_epoch_in_tags(pkg) self._check_multiple_dependencies(pkg, deps, is_devel, is_source) self._unexpanded_macros(pkg, 'Name', name) self._check_multiple_tags(pkg, name, is_devel, is_source, deps, epoch, version) self._check_summary_tag(pkg, summary, langs, ignored_words) self._check_description_tag(pkg, description, langs, ignored_words) self._check_group_tag(pkg, group) self._check_buildhost_tag(pkg, buildhost) self._check_changelog_tag(pkg, changelog, version, release, name, epoch) self._check_license(pkg, rpm_license) self._check_url(pkg) prov_names = [x[0] for x in pkg.provides] self._check_obsolete_not_provided(pkg, prov_names) for dep_token in pkg.obsoletes: value = Pkg.formatRequire(*dep_token) self._unexpanded_macros(pkg, 'Obsoletes {}'.format(value, ), value) self._check_useless_provides(pkg, prov_names) self._check_forbidden_controlchar(pkg) self._check_self_obsoletion(pkg) self._check_non_coherent_filename(pkg) 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_changelog_tag(self, pkg, changelog, version, release, name, epoch): """Trigger multiple check of type *-changelog, *-changelogname-*, changelog-* and forbidden-controlchar Contains all the checks that cause an issue during build of the rpm in the %changelog of the specfile Args: changelog: Find the %changelog in the specfile Returns: Output info to STDOUT """ # Check if a package does not have a %changelog in its spec file 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])) # Check if a package does not have version in the %changelog in latest version 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])) # Check if a package does not have a version that is # compatible with epoch:vesrion-release tuple 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 deptoken in changelog: dep = Pkg.has_forbidden_controlchars(deptoken) # Check if a package contains a forbidden character in %changelog if dep: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%%changelog : %s' % dep) break clt = pkg[rpm.RPMTAG_CHANGELOGTIME][0] if clt: clt -= clt % (24 * 3600) # roll back to 00:00:00, see #246 # Check if a package contains a changelog entry that is suspiciously too far behind if clt < oldest_changelog_timestamp: self.output.add_info( 'W', pkg, 'changelog-time-overflow', time.strftime('%Y-%m-%d', time.gmtime(clt))) # Check if a package contians a entry in %changelog # with timestamp thats in the future of its writing elif clt > time.time(): self.output.add_info( 'E', pkg, 'changelog-time-in-future', time.strftime('%Y-%m-%d', time.gmtime(clt)))
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_aux(self, pkg, files, prog, script, tag, prereq): if script: script_str = byte_to_string(script) if prog: if prog not in self.valid_shells: self.output.add_info('E', pkg, 'invalid-shell-in-' + tag, prog) if prog in self.empty_shells: self.output.add_info('E', pkg, 'non-empty-' + tag, prog) if prog in syntaxcheck_shells or prog == '/usr/bin/perl': if percent_regex.search(script_str): self.output.add_info('W', pkg, 'percent-in-' + tag) if bracket_regex.search(script_str): self.output.add_info('W', pkg, 'spurious-bracket-in-' + tag) res = dangerous_command_regex.search(script_str) if res: self.output.add_info('W', pkg, 'dangerous-command-in-' + tag, res.group(2)) res = selinux_regex.search(script_str) if res: self.output.add_info('E', pkg, 'forbidden-selinux-command-in-' + tag, res.group(2)) if 'update-menus' in script_str: menu_error = True for f in files: if menu_regex.search(f): menu_error = False break if menu_error: self.output.add_info('E', pkg, 'update-menus-without-menu-file-in-' + tag) if tmp_regex.search(script_str): self.output.add_info('E', pkg, 'use-tmp-in-' + tag) for c in prereq_assoc: if c[0].search(script_str): found = False for p in c[1]: if p in prereq or p in files: found = True break if not found: self.output.add_info('E', pkg, 'no-prereq-on', c[1][0]) if prog in syntaxcheck_shells: if incorrect_shell_script(prog, script): self.output.add_info('E', pkg, 'shell-syntax-error-in-' + tag) if home_regex.search(script_str): self.output.add_info('E', pkg, 'use-of-home-in-' + tag) res = bogus_var_regex.search(script_str) if res: self.output.add_info('W', pkg, 'bogus-variable-use-in-' + tag, res.group(1)) if prog == '/usr/bin/perl': if incorrect_perl_script(prog, script): self.output.add_info('E', pkg, 'perl-syntax-error-in-' + tag) elif prog.endswith('sh'): res = single_command_regex.search(script_str) if res: self.output.add_info('W', pkg, 'one-line-command-in-' + tag, res.group(1)) elif prog not in self.empty_shells and prog in self.valid_shells: self.output.add_info('W', pkg, 'empty-' + tag)
def check(self, pkg): if pkg.is_source: return files = pkg.files for check in self.checks: if 'IgnorePkgIf' in check: if getattr(self, check['IgnorePkgIf'])(pkg): continue if 'Good' in check or 'Bad' in check: for f in files: ok = False if 'Good' in check: for g in check['Good']: if ((not isinstance(g, str) and g.match(f)) or g == f): ok = True break if ok: continue if 'Bad' in check: for b in check['Bad']: if 'IgnoreFileIf' in check: if getattr(self, check['IgnoreFileIf'])(pkg, f): continue if ((not isinstance(b, str) and b.match(f)) or b == f): self.output.add_info('E', pkg, check['Message'], f) invalidfhs = set() invalidopt = set() is_suse = (pkg.header[RPMTAG_VENDOR] and 'SUSE' in byte_to_string(pkg.header[RPMTAG_VENDOR])) # the checks here only warn about a directory once rather # than reporting potentially hundreds of files individually for f, pkgfile in files.items(): file_type = (pkgfile.mode >> 12) & 0o17 # append / to directories if file_type == 4: f += '/' if not f.startswith(self.goodprefixes): base = f.rpartition('/') pfx = None # find the first invalid path component # (/usr/foo/bar/baz -> /usr) while (base[0] and not base[0].startswith(self.goodprefixes) and not base[0] in self._restricteddirs): pfx = base[0] base = base[0].rpartition('/') if not pfx: invalidfhs.add(f) else: invalidfhs.add(pfx) if f.startswith('/opt'): try: provider = f.split('/')[2] except Exception: continue if is_suse and (provider == 'suse' or provider == 'novell'): continue d = '/opt/' + provider invalidopt.add(d) for f in invalidfhs: self.output.add_info('E', pkg, 'filelist-forbidden-fhs23', f) for f in invalidopt: self.output.add_info('E', pkg, 'filelist-forbidden-opt', f)
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)
def script_interpreter(chunk): res = shebang_regex.search(chunk) if chunk else None return (byte_to_string(res.group(1)), byte_to_string(res.group(2)).strip()) \ if res and res.start() == 0 else (None, '')
def readlines(path): with open(path, 'rb') as fobj: for line in fobj: yield byte_to_string(line)
def __init__(self, config, output, pkg, path, fname, is_ar, is_shlib): self.readelf_error = False self.needed = [] self.rpath = [] self.undef = [] self.unused = [] self.config = config self.output = output self.comment = False self.soname = False self.non_pic = True self.stack = False self.exec_stack = False self.exit_calls = [] self.forbidden_calls = [] fork_called = False self.tail = '' self.setgid = False self.setuid = False self.setgroups = False self.chroot = False self.chdir = False self.chroot_near_chdir = False self.mktemp = False self.forbidden_functions = self.config.configuration['WarnOnFunction'] if self.forbidden_functions: for name, func in self.forbidden_functions.items(): # precompile regexps f_name = func['f_name'] func['f_regex'] = create_nonlibc_regexp_call(f_name) if func['good_param']: func['waiver_regex'] = re.compile(func['good_param']) # register descriptions self.output.error_details.update({name: func['description']}) is_debug = path.endswith('.debug') # Currently this implementation works only on specific # architectures due to reliance on arch specific assembly. if (pkg.arch.startswith('armv') or pkg.arch == 'aarch64'): # 10450: ebffffec bl 10408 <chroot@plt> self.objdump_call_regex = re.compile(br'\sbl\s+(.*)') elif (pkg.arch.endswith('86') or pkg.arch == 'x86_64'): # 401eb8: e8 c3 f0 ff ff callq 400f80 <chdir@plt> self.objdump_call_regex = re.compile(br'callq?\s(.*)') else: self.objdump_call_regex = None res = Pkg.getstatusoutput( ('readelf', '-W', '-S', '-l', '-d', '-s', path)) if not res[0]: lines = res[1].splitlines() for line in lines: r = self.needed_regex.search(line) if r: self.needed.append(r.group(1)) continue r = self.rpath_regex.search(line) if r: for p in r.group(1).split(':'): self.rpath.append(p) continue if self.comment_regex.search(line): self.comment = True continue if self.pic_regex.search(line): self.non_pic = False continue r = self.soname_regex.search(line) if r: self.soname = r.group(1) continue r = self.stack_regex.search(line) if r: self.stack = True flags = r.group(1) if flags and self.stack_exec_regex.search(flags): self.exec_stack = True continue if line.startswith('Symbol table'): break for line in lines: r = self.call_regex.search(line) if not r: continue line = r.group(1) if self.mktemp_call_regex.search(line): self.mktemp = True if self.setgid_call_regex.search(line): self.setgid = True if self.setuid_call_regex.search(line): self.setuid = True if self.setgroups_call_regex.search(line): self.setgroups = True if self.chdir_call_regex.search(line): self.chdir = True if self.chroot_call_regex.search(line): self.chroot = True if self.forbidden_functions: for r_name, func in self.forbidden_functions.items(): ret = func['f_regex'].search(line) if ret: self.forbidden_calls.append(r_name) if is_shlib: r = self.exit_call_regex.search(line) if r: self.exit_calls.append(r.group(1)) continue r = self.fork_call_regex.search(line) if r: fork_called = True continue # check if we don't have a string that will automatically # waive the presence of a forbidden call if self.forbidden_calls: res = Pkg.getstatusoutput(('strings', path)) if not res[0]: for line in res[1].splitlines(): # as we need to remove elements, iterate backwards for i in range(len(self.forbidden_calls) - 1, -1, -1): func = self.forbidden_calls[i] f = self.forbidden_functions[func] if 'waiver_regex' not in f: continue r = f['waiver_regex'].search(line) if r: del self.forbidden_calls[i] if self.non_pic: self.non_pic = 'TEXTREL' in res[1] # Ignore all exit() calls if fork() is being called. # Does not have any context at all but without this kludge, the # number of false positives would probably be intolerable. if fork_called: self.exit_calls = [] # check if chroot is near chdir (since otherwise, chroot is called # without chdir) if not self.objdump_call_regex and self.chroot and self.chdir: # On some architectures, e.g. PPC, it is to difficult to # find the actual invocations of chroot/chdir, if both # exist assume chroot is fine self.chroot_near_chdir = True elif self.chroot and self.chdir: p = subprocess.Popen(('objdump', '-d', path), stdout=subprocess.PIPE, bufsize=-1, env=dict(os.environ, LC_ALL='C')) with p.stdout: index = 0 chroot_index = -99 chdir_index = -99 for line in p.stdout: res = self.objdump_call_regex.search(line) if not res: continue if b'@plt' not in res.group(1): pass elif b'chroot@plt' in res.group(1): chroot_index = index if abs(chroot_index - chdir_index) <= 2: self.chroot_near_chdir = True break elif b'chdir@plt' in res.group(1): chdir_index = index if abs(chroot_index - chdir_index) <= 2: self.chroot_near_chdir = True break index += 1 if p.wait() and not self.chroot_near_chdir: self.output.add_info('W', pkg, 'binaryinfo-objdump-failed', fname) self.chroot_near_chdir = True # avoid false positive elif chroot_index == -99 and chdir_index == -99: self.chroot_near_chdir = True # avoid false positive else: self.readelf_error = True # Go and others are producing ar archives that don't have ELF # headers, so don't complain about it if not is_ar: self.output.add_info('W', pkg, 'binaryinfo-readelf-failed', fname, re.sub('\n.*', '', res[1])) try: with open(path, 'rb') as fobj: fobj.seek(-12, os.SEEK_END) self.tail = byte_to_string(fobj.read()) except Exception as e: self.output.add_info('W', pkg, 'binaryinfo-tail-failed %s: %s' % (fname, e)) # Undefined symbol and unused direct dependency checks make sense only # for installed packages. # skip debuginfo: https://bugzilla.redhat.com/190599 if not is_ar and not is_debug and isinstance(pkg, Pkg.InstalledPkg): # We could do this with objdump, but it's _much_ simpler with ldd. res = Pkg.getstatusoutput(('ldd', '-d', '-r', path)) if not res[0]: for line in res[1].splitlines(): undef = self.undef_regex.search(line) if undef: self.undef.append(undef.group(1)) if self.undef: try: res = Pkg.getstatusoutput(['c++filt'] + self.undef) if not res[0]: self.undef = res[1].splitlines() except OSError: pass else: self.output.add_info('W', pkg, 'ldd-failed', fname) res = Pkg.getstatusoutput(('ldd', '-r', '-u', path)) if res[0]: # Either ldd doesn't grok -u (added in glibc 2.3.4) or we have # unused direct dependencies in_unused = False for line in res[1].splitlines(): if not line.rstrip(): pass elif line.startswith('Unused direct dependencies'): in_unused = True elif in_unused: unused = self.unused_regex.search(line) if unused: self.unused.append(unused.group(1)) else: in_unused = False