diff options
author | Dan McGee <dan@archlinux.org> | 2017-10-29 16:16:50 -0500 |
---|---|---|
committer | Dan McGee <dan@archlinux.org> | 2017-10-29 16:16:50 -0500 |
commit | a2d04864e2b500cb8b3a6d20392bd611cd185778 (patch) | |
tree | 503ddc8ed51d7c0b56d680c9e37a9d3ac64235a5 | |
parent | 96cdbb3628139c2ef14ec022988c74df09c1bb88 (diff) | |
parent | f73bf160de19e43d1c22a3db0f32b234b872963f (diff) | |
download | namcap-a2d04864e2b500cb8b3a6d20392bd611cd185778.tar.gz namcap-a2d04864e2b500cb8b3a6d20392bd611cd185778.zip |
29 files changed, 396 insertions, 283 deletions
diff --git a/Namcap/depends.py b/Namcap/depends.py index 3c115bf..40d27bf 100644 --- a/Namcap/depends.py +++ b/Namcap/depends.py @@ -21,10 +21,7 @@ """Checks dependencies semi-smartly.""" -import re, os, os.path -import subprocess -import tempfile -from Namcap.util import is_elf, script_type +import re from Namcap.ruleclass import * import Namcap.tags from Namcap import package diff --git a/Namcap/package.py b/Namcap/package.py index ece81b5..2da5b1d 100644 --- a/Namcap/package.py +++ b/Namcap/package.py @@ -110,7 +110,7 @@ class PacmanPackage(collections.MutableMapping): for line in db.split('\n'): if line.startswith('%'): attrname = line.strip('%').lower() - elif line.strip() != '': + elif line.strip() != '' and attrname: self.setdefault(attrname, []).append(line) elif db is not None: raise TypeError("argument 'pkginfo' must be a string") diff --git a/Namcap/rules/__init__.py b/Namcap/rules/__init__.py index 7d313f7..e8775a0 100644 --- a/Namcap/rules/__init__.py +++ b/Namcap/rules/__init__.py @@ -59,6 +59,7 @@ from . import ( carch, extravars, invalidstartdir, + makepkgfunctions, missingvars, pkginfo, pkgnameindesc, diff --git a/Namcap/rules/anyelf.py b/Namcap/rules/anyelf.py index 90172a3..1e497e4 100644 --- a/Namcap/rules/anyelf.py +++ b/Namcap/rules/anyelf.py @@ -22,7 +22,7 @@ Check for ELF files to see if a package should be 'any' architecture """ import os, re -from Namcap.util import is_elf, clean_filename +from Namcap.util import is_elf, is_static, clean_filename from Namcap.ruleclass import * class package(TarballRule): @@ -38,8 +38,8 @@ class package(TarballRule): if not entry.isfile(): continue f = tar.extractfile(entry) - # Archive files are considered as ELF (FS#24854) - if f.read(4) in (b"\x7fELF", b"!<ar"): + # Ar files (static libs) are also architecture specific (FS#24854) + if is_elf(f) or is_static(f): found_elffiles.append(entry.name) f.close() diff --git a/Namcap/rules/elffiles.py b/Namcap/rules/elffiles.py index f8f16ac..e2dd7f5 100644 --- a/Namcap/rules/elffiles.py +++ b/Namcap/rules/elffiles.py @@ -19,10 +19,10 @@ # import os -import tempfile -import subprocess from elftools.elf.elffile import ELFFile +from elftools.elf.dynamic import DynamicSection +from elftools.elf.sections import SymbolTableSection from Namcap.util import is_elf, clean_filename from Namcap.ruleclass import * @@ -30,55 +30,47 @@ from Namcap.ruleclass import * # Valid directories for ELF files valid_dirs = ['bin/', 'sbin/', 'usr/bin/', 'usr/sbin/', 'lib/', 'usr/lib/', 'usr/lib32/'] +# Questionable directories for ELF files +# (Suppresses some output spam.) +questionable_dirs = ['opt/'] class ELFPaths(TarballRule): name = "elfpaths" description = "Check about ELF files outside some standard paths." def analyze(self, pkginfo, tar): invalid_elffiles = [] + questionable_elffiles = [] for entry in tar: # is it a regular file ? if not entry.isfile(): continue # is it outside standard binary dirs ? - is_outside_std_dirs = True - for d in valid_dirs: - if entry.name.startswith(d): - is_outside_std_dirs = False - break - if not is_outside_std_dirs: + in_std_dirs = any(entry.name.startswith(d) for d in valid_dirs) + in_que_dirs = any(entry.name.startswith(d) for d in questionable_dirs) + + if in_std_dirs: continue # is it an ELF file ? f = tar.extractfile(entry) - if f.read(4) == b"\x7fELF": - invalid_elffiles.append(entry.name) + if is_elf(f): + if in_que_dirs: + questionable_elffiles.append(entry.name) + else: + invalid_elffiles.append(entry.name) + que_elfdirs = [d for d in questionable_dirs if any(f.startswith(d) for f in questionable_elffiles)] self.errors = [("elffile-not-in-allowed-dirs %s", i) for i in invalid_elffiles] + self.errors.extend(("elffile-in-questionable-dirs %s", i) + for i in que_elfdirs) + self.infos = [("elffile-not-in-allowed-dirs %s", i) + for i in questionable_elffiles] -def _test_elf_and_extract(tar, entry): - "Tests whether a Tar entry is an ELF file and returns the name of a temp file." - if not entry.isfile(): - return - f = tar.extractfile(entry) - magic = f.read(4) - if magic != b"\x7fELF": - return - - # read the rest of file - tmp = tempfile.NamedTemporaryFile(delete=False) - tmp.write(magic + f.read()) - tmp.close() - return tmp.name class ELFTextRelocationRule(TarballRule): """ Check for text relocations in ELF files. - - Introduced by FS#26434. Text relocations are detected by the - eu-findtextrel utility from elfutils. eu-findtextrel returns 0 - whenever the input file has a text relocation section. """ name = "elftextrel" @@ -88,18 +80,18 @@ class ELFTextRelocationRule(TarballRule): files_with_textrel = [] for entry in tar: - tmpname = _test_elf_and_extract(tar, entry) - if not tmpname: + if not entry.isfile(): continue - - try: - ret = subprocess.call(["eu-findtextrel", tmpname], - stdout=open(os.devnull, 'w'), - stderr=open(os.devnull, 'w')) - if ret == 0: - files_with_textrel.append(entry.name) - finally: - os.unlink(tmpname) + fp = tar.extractfile(entry) + if not is_elf(fp): + continue + elffile = ELFFile(fp) + for section in elffile.iter_sections(): + if not isinstance(section, DynamicSection): + continue + for tag in section.iter_tags(): + if tag.entry.d_tag == 'DT_TEXTREL': + files_with_textrel.append(entry.name) if files_with_textrel: self.warnings = [("elffile-with-textrel %s", i) @@ -121,26 +113,85 @@ class ELFExecStackRule(TarballRule): exec_stacks = [] for entry in tar: - tmpname = _test_elf_and_extract(tar, entry) - if not tmpname: + if not entry.isfile(): continue + fp = tar.extractfile(entry) + if not is_elf(fp): + continue + elffile = ELFFile(fp) + for segment in elffile.iter_segments(): + if segment['p_type'] != 'PT_GNU_STACK': + continue - try: - fp = open(tmpname, 'rb') - elffile = ELFFile(fp) - - for segment in elffile.iter_segments(): - if segment['p_type'] != 'PT_GNU_STACK': continue - - mode = segment['p_flags'] - if mode & 1: exec_stacks.append(entry.name) - - fp.close() - finally: - os.unlink(tmpname) + mode = segment['p_flags'] + if mode & 1: + exec_stacks.append(entry.name) if exec_stacks: self.warnings = [("elffile-with-execstack %s", i) for i in exec_stacks] +class ELFGnuRelroRule(TarballRule): + """ + Check for read-only relocation in ELF files. + + Introduced by FS#26435. Uses pyelftools to check for GNU_RELRO. + """ + # not smart enough for full/partial RELRO (DT_BIND_NOW?) + + name = "elfgnurelro" + description = "Check for RELRO in ELF files." + + def analyze(self, pkginfo, tar): + missing_relro = [] + + for entry in tar: + if not entry.isfile(): + continue + fp = tar.extractfile(entry) + if not is_elf(fp): + continue + elffile = ELFFile(fp) + if any(seg['p_type'] == 'PT_GNU_RELRO' for seg in elffile.iter_segments()): + continue + missing_relro.append(entry.name) + + if missing_relro: + self.warnings = [("elffile-without-relro %s", i) + for i in missing_relro] + +class ELFUnstrippedRule(TarballRule): + """ + Checks for unstripped ELF files. Uses pyelftools to check if + .symtab exists. + + Introduced by FS#27485. + """ + + name = "elfunstripped" + description = "Check for unstripped ELF files." + + def analyze(self, pkginfo, tar): + unstripped_binaries = [] + + for entry in tar: + if not entry.isfile(): + continue + fp = tar.extractfile(entry) + if not is_elf(fp): + continue + elffile = ELFFile(fp) + for section in elffile.iter_sections(): + if not isinstance(section, SymbolTableSection): + continue + + if section['sh_entsize'] == 0: + continue + + if section.name == '.symtab': + unstripped_binaries.append(entry.name) + if unstripped_binaries: + self.warnings = [("elffile-unstripped %s", i) + for i in unstripped_binaries] + # vim: set ts=4 sw=4 noet: diff --git a/Namcap/rules/externalhooks.py b/Namcap/rules/externalhooks.py index aad2bd2..aa6f465 100644 --- a/Namcap/rules/externalhooks.py +++ b/Namcap/rules/externalhooks.py @@ -31,6 +31,11 @@ class ExternalHooksRule(TarballRule): 'xdg-icon-resource', 'gconfpkg', 'gio-querymodules', + 'fc-cache', + 'mkfontscale', + 'mkfontdir', + 'systemd-sysusers', + 'systemd-tmpfiles', ] def analyze(self, pkginfo, tar): if ".INSTALL" not in tar.getnames(): diff --git a/Namcap/rules/fhs.py b/Namcap/rules/fhs.py index be583ef..6a40ca4 100644 --- a/Namcap/rules/fhs.py +++ b/Namcap/rules/fhs.py @@ -29,7 +29,7 @@ class FHSRule(TarballRule): 'etc/', 'opt/', 'lib/modules', 'usr/bin/', 'usr/include/', 'usr/lib/', 'usr/lib32/', - 'usr/sbin/', 'usr/share/', + 'usr/sbin/', 'usr/share/', 'usr/src/', 'var/cache/', 'var/lib/', 'var/log/', 'var/opt/', 'var/spool/', 'var/state/', '.PKGINFO', '.INSTALL', '.CHANGELOG', '.MTREE', '.BUILDINFO', @@ -79,13 +79,14 @@ class FHSManpagesRule(TarballRule): for i in tar.getmembers(): if not i.isfile(): continue + if i.name.startswith(gooddir): + continue if i.name.startswith(bad_dir): self.errors.append(("non-fhs-man-page %s", i.name)) - elif not i.name.startswith(gooddir): - #Check everything else to see if it has a 'man' path component - for part in i.name.split(os.sep): - if part == "man": - self.warnings.append(("potential-non-fhs-man-page %s", i.name)) + continue + #Check everything else to see if it has a 'man' path component + if "man" in i.name.split(os.sep): + self.warnings.append(("potential-non-fhs-man-page %s", i.name)) class FHSInfoPagesRule(TarballRule): name = "fhs-infopages" @@ -94,12 +95,13 @@ class FHSInfoPagesRule(TarballRule): for i in tar.getmembers(): if not i.isfile(): continue + if i.name.startswith('usr/share/info'): + continue if i.name.startswith('usr/info'): self.errors.append(("non-fhs-info-page %s", i.name)) - elif not i.name.startswith('usr/share/info'): - for part in i.name.split(os.sep): - if part == "info": - self.warnings.append(("potential-non-fhs-info-page %s", i.name)) + continue + if "info" in i.name.split(os.sep): + self.warnings.append(("potential-non-fhs-info-page %s", i.name)) class RubyPathsRule(TarballRule): name = "rubypaths" diff --git a/Namcap/rules/filenames.py b/Namcap/rules/filenames.py index d7cba48..c5f5f0e 100644 --- a/Namcap/rules/filenames.py +++ b/Namcap/rules/filenames.py @@ -31,9 +31,7 @@ class package(TarballRule): description = "Checks for invalid filenames." def analyze(self, pkginfo, tar): for i in tar.getnames(): - for c in i: - if c not in VALID_CHARS: - self.warnings.append(("invalid-filename", i)) - break + if not all(c in VALID_CHARS for c in i): + self.warnings.append(("invalid-filename", i)) # vim: set ts=4 sw=4 noet: diff --git a/Namcap/rules/javafiles.py b/Namcap/rules/javafiles.py index 080174f..40ff856 100644 --- a/Namcap/rules/javafiles.py +++ b/Namcap/rules/javafiles.py @@ -20,6 +20,7 @@ import os from Namcap.ruleclass import * +from Namcap.util import is_java class JavaFiles(TarballRule): name = "javafiles" @@ -38,11 +39,11 @@ class JavaFiles(TarballRule): continue # is it a CLASS file ? f = tar.extractfile(entry) - if f.read(4) == b"\xCA\xFE\xBA\xBE": + if is_java(f): javas.append(entry.name) #self.infos.append( ('java-class-file-found %s', entry.name) ) f.close() - if len(javas) > 0: + if javas: reasons = pkginfo.detected_deps.setdefault('java-runtime', []) reasons.append( ('java-runtime-needed %s', ', '.join(javas)) ) diff --git a/Namcap/rules/licensepkg.py b/Namcap/rules/licensepkg.py index e4b5e9b..026caf4 100644 --- a/Namcap/rules/licensepkg.py +++ b/Namcap/rules/licensepkg.py @@ -26,22 +26,22 @@ class package(TarballRule): def analyze(self, pkginfo, tar): if 'license' not in pkginfo or len(pkginfo["license"]) == 0: self.errors.append(("missing-license", ())) - else: - licensepaths = [x for x in tar.getnames() if x.startswith('usr/share/licenses') and not x.endswith('/')] - licensedirs = [os.path.split(os.path.split(x)[0])[1] for x in licensepaths] - licensefiles = [os.path.split(x)[1] for x in licensepaths] - # Check all licenses for validity - for license in pkginfo["license"]: - lowerlicense, _, sublicense = license.lower().partition(':') - if lowerlicense.startswith('custom') or lowerlicense in ("bsd", "mit", "isc", "python", "zlib", "libpng"): - if pkginfo["name"] not in licensedirs: - self.errors.append(("missing-custom-license-dir usr/share/licenses/%s", pkginfo["name"])) - elif len(licensefiles) == 0: - self.errors.append(("missing-custom-license-file usr/share/licenses/%s/*", pkginfo["name"])) - # A common license - else: - commonlicenses = [x.lower() for x in os.listdir('/usr/share/licenses/common')] - if lowerlicense not in commonlicenses: - self.errors.append(("not-a-common-license %s", license)) + return + licensepaths = [x for x in tar.getnames() if x.startswith('usr/share/licenses') and not x.endswith('/')] + licensedirs = [os.path.split(os.path.split(x)[0])[1] for x in licensepaths] + licensefiles = [os.path.split(x)[1] for x in licensepaths] + # Check all licenses for validity + for license in pkginfo["license"]: + lowerlicense, _, sublicense = license.lower().partition(':') + if lowerlicense.startswith('custom') or lowerlicense in ("bsd", "mit", "isc", "python", "zlib", "libpng"): + if pkginfo["name"] not in licensedirs: + self.errors.append(("missing-custom-license-dir usr/share/licenses/%s", pkginfo["name"])) + elif len(licensefiles) == 0: + self.errors.append(("missing-custom-license-file usr/share/licenses/%s/*", pkginfo["name"])) + # A common license + else: + commonlicenses = [x.lower() for x in os.listdir('/usr/share/licenses/common')] + if lowerlicense not in commonlicenses: + self.errors.append(("not-a-common-license %s", license)) # vim: set ts=4 sw=4 noet: diff --git a/Namcap/rules/makepkgfunctions.py b/Namcap/rules/makepkgfunctions.py new file mode 100644 index 0000000..fa20f5f --- /dev/null +++ b/Namcap/rules/makepkgfunctions.py @@ -0,0 +1,37 @@ +# +# namcap rules - makepkgfunctions +# Copyright (C) 2017 Kyle Keen <keenerd@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import re +from Namcap.ruleclass import * + +class package(PkgbuildRule): + name = "makepkgfunctions" + description = "Looks for calls to makepkg functionality" + def analyze(self, pkginfo, tar): + bad_calls = ['msg', 'msg2', 'warning', 'error', 'plain'] + regex = re.compile('^\s+(%s) ' % '|'.join(bad_calls)) + hits = set() + for i in pkginfo.pkgbuild: + if regex.match(i): + call = regex.match(i).group(1) + hits.add(call) + for i in hits: + self.warnings.append(("makepkg-function-used %s", i)) + +# vim: set ts=4 sw=4 noet: diff --git a/Namcap/rules/missingvars.py b/Namcap/rules/missingvars.py index 440f883..2b8811c 100644 --- a/Namcap/rules/missingvars.py +++ b/Namcap/rules/missingvars.py @@ -65,9 +65,9 @@ class TagsRule(PkgbuildRule): maintainertag = 0 idtag = 0 for i in pkginfo.pkgbuild: - if re.match("#\s*Contributor\s*:", i) != None: + if re.match("#\s*Contributor\s*:", i): contributortag = 1 - if re.match("#\s*Maintainer\s*:", i) != None: + if re.match("#\s*Maintainer\s*:", i): maintainertag = 1 if contributortag != 1: diff --git a/Namcap/rules/pathdepends.py b/Namcap/rules/pathdepends.py index e50a6f6..5bc4313 100644 --- a/Namcap/rules/pathdepends.py +++ b/Namcap/rules/pathdepends.py @@ -24,7 +24,7 @@ If a certain path is detected then a certain dependency is expected. Anything fancier than this should get its own rule. """ -import os, re +import re from Namcap.ruleclass import * class PathDependsRule(TarballRule): diff --git a/Namcap/rules/perllocal.py b/Namcap/rules/perllocal.py index ac622b4..24923d9 100644 --- a/Namcap/rules/perllocal.py +++ b/Namcap/rules/perllocal.py @@ -23,9 +23,8 @@ class package(TarballRule): name = "perllocal" description = "Verifies the absence of perllocal.pod." def analyze(self, pkginfo, tar): - j = 'perllocal.pod' for i in tar.getnames(): - if i[-len(j):] == j: + if i.endswith('perllocal.pod'): self.errors.append(("perllocal-pod-present %s", i)) # vim: set ts=4 sw=4 noet: diff --git a/Namcap/rules/rpath.py b/Namcap/rules/rpath.py index a2d1193..4da040c 100644 --- a/Namcap/rules/rpath.py +++ b/Namcap/rules/rpath.py @@ -17,37 +17,28 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -import os, subprocess, re -import tempfile -from Namcap.util import is_elf, clean_filename +from Namcap.util import is_elf from Namcap.ruleclass import * +from elftools.elf.elffile import ELFFile +from elftools.elf.dynamic import DynamicSection + allowed = ['/usr/lib', '/usr/lib32', '/lib', '$ORIGIN', '${ORIGIN}'] allowed_toplevels = [s + '/' for s in allowed] warn = ['/usr/local/lib'] -libpath = re.compile('Library rpath: \[(.*)\]') - -def get_rpaths(filename): - p = subprocess.Popen(["readelf", "-d", filename], - env={'LANG': 'C'}, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - var = p.communicate() - if p.returncode != 0: - raise IOError("unable to read ELF file") - for j in var[0].decode('ascii').splitlines(): - n = libpath.search(j) - # Is this a Library rpath: line? - if n is None: +def get_rpaths(fileobj): + elffile = ELFFile(fileobj) + for section in elffile.iter_sections(): + if not isinstance(section, DynamicSection): continue - - if ":" in n.group(1): - rpaths = n.group(1).split(':') - else: - rpaths = [n.group(1)] - for path in rpaths: - yield path + for tag in section.iter_tags(): + if tag.entry.d_tag != 'DT_RPATH': + continue + rpaths = tag.rpath + rpaths = rpaths.split(':') + for path in rpaths: + yield path class package(TarballRule): name = "rpath" @@ -58,18 +49,11 @@ class package(TarballRule): continue # is it an ELF file ? - f = tar.extractfile(entry) - elf = f.read() - f.close() - if elf[:4] != b"\x7fELF": - continue # not an ELF file - - # write it to a temporary file - f = tempfile.NamedTemporaryFile(delete = False) - f.write(elf) - f.close() + fileobj = tar.extractfile(entry) + if not is_elf(fileobj): + continue - for path in get_rpaths(f.name): + for path in get_rpaths(fileobj): path_ok = path in allowed for allowed_toplevel in allowed_toplevels: if path.startswith(allowed_toplevel): @@ -83,6 +67,5 @@ class package(TarballRule): self.warnings.append(("insecure-rpath %s %s", (path, entry.name))) - os.unlink(f.name) # vim: set ts=4 sw=4 noet: diff --git a/Namcap/rules/scrollkeeper.py b/Namcap/rules/scrollkeeper.py index 1c09e4e..8c813b8 100644 --- a/Namcap/rules/scrollkeeper.py +++ b/Namcap/rules/scrollkeeper.py @@ -26,8 +26,7 @@ class package(TarballRule): def analyze(self, pkginfo, tar): scroll = re.compile("var.*/scrollkeeper/?$") for i in tar.getnames(): - n = scroll.search(i) - if n != None: + if scroll.search(i): self.errors.append(("scrollkeeper-dir-exists %s", i)) # vim: set ts=4 sw=4 noet: diff --git a/Namcap/rules/sfurl.py b/Namcap/rules/sfurl.py index 7f4dcc6..f3b6874 100644 --- a/Namcap/rules/sfurl.py +++ b/Namcap/rules/sfurl.py @@ -26,9 +26,9 @@ class package(PkgbuildRule): def analyze(self, pkginfo, tar): if 'source' in pkginfo: for source in pkginfo["source"]: - if re.match('(http://|ftp://)\w+.dl.(sourceforge|sf).net', source) != None: + if re.match('(http://|ftp://)\w+.dl.(sourceforge|sf).net', source): self.warnings.append(("specific-sourceforge-mirror", ())) - if re.match('(http://|ftp://)dl.(sourceforge|sf).net', source) != None: + if re.match('(http://|ftp://)dl.(sourceforge|sf).net', source): self.warnings.append(("using-dl-sourceforge", ())) # vim: set ts=4 sw=4 noet: diff --git a/Namcap/rules/shebangdepends.py b/Namcap/rules/shebangdepends.py index 567acf5..7d83ff4 100644 --- a/Namcap/rules/shebangdepends.py +++ b/Namcap/rules/shebangdepends.py @@ -21,13 +21,10 @@ """Checks dependencies on programs specified in shebangs.""" -import re import os -import tempfile -import subprocess -import pyalpm +import shutil import Namcap.package -from Namcap.util import script_type +from Namcap.util import is_script, script_type from Namcap.ruleclass import * def scanshebangs(fileobj, filename, scripts): @@ -39,21 +36,13 @@ def scanshebangs(fileobj, filename, scripts): """ # test magic bytes - magic = fileobj.read(2) - if magic != b"#!": + if not is_script(fileobj): return - # read the rest of file - tmp = tempfile.NamedTemporaryFile(delete=False) - tmp.write(magic + fileobj.read()) - tmp.close() - - try: - cmd = script_type(tmp.name) - if cmd != None: - assert(isinstance(cmd, str)) - scripts.setdefault(cmd, set()).add(filename) - finally: - os.unlink(tmp.name) + # process shebang line + cmd = script_type(fileobj) + if cmd != None: + assert(isinstance(cmd, str)) + scripts.setdefault(cmd, set()).add(filename) def findowners(scriptlist): """ @@ -68,14 +57,12 @@ def findowners(scriptlist): scriptfound = set() for s in scriptlist: - p = subprocess.Popen(["which", s], - stdout = subprocess.PIPE, stderr = subprocess.PIPE) - out, _ = p.communicate() - if p.returncode != 0: + out = shutil.which(s) + if not out: continue # strip leading slash - scriptpath = out.strip()[1:].decode('utf-8', 'surrogateescape') + scriptpath = out.lstrip('/') for pkg in Namcap.package.get_installed_packages(): pkg_files = [fname for fname, fsize, fmode in pkg.files] if scriptpath in pkg_files: @@ -85,13 +72,6 @@ def findowners(scriptlist): orphans = list(set(scriptlist) - scriptfound) return pkglist, orphans -def getprovides(depends, provides): - for i in depends.keys(): - pac = load(i) - - if pac != None and 'provides' in pac and pac["provides"] != None: - provides[i] = pac["provides"] - class ShebangDependsRule(TarballRule): name = "shebangdepends" description = "Checks dependencies semi-smartly." diff --git a/Namcap/rules/sodepends.py b/Namcap/rules/sodepends.py index 92826af..bce3a40 100644 --- a/Namcap/rules/sodepends.py +++ b/Namcap/rules/sodepends.py @@ -25,66 +25,52 @@ from collections import defaultdict import re import os import subprocess -import tempfile import Namcap.package from Namcap.ruleclass import * +from Namcap.util import is_elf +from Namcap.rules.rpath import get_rpaths -libcache = {'i686': {}, 'x86-64': {}} - -def figurebitsize(line): - """ - Given a line of output from readelf (usually Shared library:) return - 'i686' or 'x86-64' if the binary is a 32bit or 64bit binary - """ +from elftools.elf.enums import ENUM_D_TAG +from elftools.elf.elffile import ELFFile +from elftools.elf.dynamic import DynamicSection - address = line.split()[0] - if len(address) == 18: # + '0x' + 16 digits - return 'x86-64' - else: - return 'i686' +libcache = {'i686': {}, 'x86-64': {}} -def scanlibs(fileobj, filename, sharedlibs): +def scanlibs(fileobj, filename, custom_libs): """ - Run "readelf -d" on a file-like object (e.g. a TarFile) + Find shared libraries in a file-like binary object If it depends on a library, store that library's path. - sharedlibs: a dictionary { library => set(ELF files using that library) } + returns: a dictionary { library => set(ELF files using that library) } """ - shared = re.compile('Shared library: \[(.*)\]') - - # test magic bytes - magic = fileobj.read(4) - if magic[:4] != b"\x7fELF": - return - - # read the rest of file - tmp = tempfile.NamedTemporaryFile(delete=False) - tmp.write(magic + fileobj.read()) - tmp.close() - - try: - p = subprocess.Popen(["readelf", "-d", tmp.name], - env = {"LANG": "C"}, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - var = p.communicate() - assert(p.returncode == 0) - for j in var[0].decode('ascii').splitlines(): - n = shared.search(j) - # Is this a Shared library: line? - if n != None: - # Find out its architecture - architecture = figurebitsize(j) - try: - libpath = os.path.abspath( - libcache[architecture][n.group(1)])[1:] - sharedlibs.setdefault(libpath, set()).add(filename) - except KeyError: - # We didn't know about the library, so add it for fail later - sharedlibs.setdefault(n.group(1), set()).add(filename) - finally: - os.unlink(tmp.name) + + if not is_elf(fileobj): + return {} + + elffile = ELFFile(fileobj) + sharedlibs = defaultdict(set) + for section in elffile.iter_sections(): + if not isinstance(section, DynamicSection): + continue + for tag in section.iter_tags(): + # DT_NEEDED means shared library + if tag.entry.d_tag != 'DT_NEEDED': + continue + bitsize = elffile.elfclass + architecture = {32:'i686', 64:'x86-64'}[bitsize] + libname = tag.needed + if libname in custom_libs: + sharedlibs[custom_libs[libname][1:]].add(filename) + continue + try: + libpath = os.path.abspath( + libcache[architecture][libname])[1:] + sharedlibs[libpath].add(filename) + except KeyError: + # We didn't know about the library, so add it for fail later + sharedlibs[libname].add(filename) + return sharedlibs def finddepends(liblist): """ @@ -152,12 +138,21 @@ class SharedLibsRule(TarballRule): dependlist = {} filllibcache() os.environ['LC_ALL'] = 'C' + pkg_so_files = ['/' + n for n in tar.getnames() if '.so' in n] for entry in tar: if not entry.isfile(): continue f = tar.extractfile(entry) - scanlibs(f, entry.name, liblist) + # find anything that could be rpath related + rpath_files = {} + if is_elf(f): + rpaths = list(get_rpaths(f)) + f.seek(0) + for n in pkg_so_files: + if any(n.startswith(rp) for rp in rpaths): + rpath_files[os.path.basename(n)] = n + liblist.update(scanlibs(f, entry.name, rpath_files)) f.close() # Ldd all the files and find all the link and script dependencies diff --git a/Namcap/rules/symlink.py b/Namcap/rules/symlink.py index c43e498..f595bc5 100644 --- a/Namcap/rules/symlink.py +++ b/Namcap/rules/symlink.py @@ -18,12 +18,20 @@ # import os from Namcap.ruleclass import * +from Namcap.package import load_from_db class package(TarballRule): name = "symlink" description = "Checks that symlinks point to the right place" def analyze(self, pkginfo, tar): - filenames = [s.name for s in tar] + filenames = set(s.name for s in tar) + depfilenames = set() + for d in pkginfo['depends']: + p = load_from_db(d) + if not p: + continue + depfilenames |= set(name for name,_,_ in p['files']) + filenames |= depfilenames for i in tar: if i.issym(): self.infos.append(("symlink-found %s points to %s", (i.name, i.linkname))) diff --git a/Namcap/rules/unusedsodepends.py b/Namcap/rules/unusedsodepends.py index 82bfcab..abfb3ba 100644 --- a/Namcap/rules/unusedsodepends.py +++ b/Namcap/rules/unusedsodepends.py @@ -55,10 +55,11 @@ class package(TarballRule): # is it an ELF file ? f = tar.extractfile(entry) + if not is_elf(f): + f.close() + continue elf = f.read() f.close() - if elf[:4] != b"\x7fELF": - continue # not an ELF file # write it to a temporary file f = tempfile.NamedTemporaryFile(delete = False) diff --git a/Namcap/tests/makepkg.py b/Namcap/tests/makepkg.py index a32a477..b3b0725 100644 --- a/Namcap/tests/makepkg.py +++ b/Namcap/tests/makepkg.py @@ -80,13 +80,10 @@ class MakepkgTest(unittest.TestCase): os.chdir(pwd) def run_rule_on_tarball(self, filename, rule): - ret = subprocess.call(["unxz", '-f', filename + ".xz"]) - self.assertEqual(ret, 0) - # process PKGINFO - pkg = Namcap.package.load_from_tarball(filename) + pkg = Namcap.package.load_from_tarball(filename + ".xz") - tar = tarfile.open(filename) + tar = tarfile.open(filename + ".xz") r = rule() r.analyze(pkg, tar) tar.close() diff --git a/Namcap/tests/package/test_shebangdepends.py b/Namcap/tests/package/test_shebangdepends.py new file mode 100644 index 0000000..dbacd86 --- /dev/null +++ b/Namcap/tests/package/test_shebangdepends.py @@ -0,0 +1,64 @@ +# namcap tests - shebangdepends +# Copyright (C) 2016 Kyle Keen <keenerd@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +import os +from Namcap.tests.makepkg import MakepkgTest +import Namcap.rules.shebangdepends + +class ShebangDependsTest(MakepkgTest): + pkgbuild = """ +pkgname=__namcap_test_shebangdepends +pkgver=1.0 +pkgrel=1 +pkgdesc="A package" +arch=('any') +url="http://www.example.com/" +license=('GPL') +depends=() +source=() +options=(!purge !zipman) +build() { + cd "${srcdir}" + echo -e "#! /usr/bin/env python\nprint('a script')" > python_sample + echo -e "#!/bin\\xffary/da\\x00ta\ncrash?" > binary_sample +} +package() { + install -Dm755 "$srcdir/python_sample" "$pkgdir/usr/bin/python_sample" + install -Dm755 "$srcdir/binary_sample" "$pkgdir/usr/share/binary_sample" +} +""" + def test_shebangdepends(self): + "Package with missing python dependency" + pkgfile = "__namcap_test_shebangdepends-1.0-1-any.pkg.tar" + with open(os.path.join(self.tmpdir, "PKGBUILD"), "w") as f: + f.write(self.pkgbuild) + self.run_makepkg() + pkg, r = self.run_rule_on_tarball( + os.path.join(self.tmpdir, pkgfile), + Namcap.rules.shebangdepends.ShebangDependsRule + ) + e, w, i = Namcap.depends.analyze_depends(pkg) + self.assertEqual(e, [ + ('dependency-detected-not-included %s (%s)', + ('python', "programs ['python'] needed in scripts ['usr/bin/python_sample']")) + ]) + self.assertEqual(w, []) + +# vim: set ts=4 sw=4 noet: + diff --git a/Namcap/tests/package/test_symlink.py b/Namcap/tests/package/test_symlink.py index 43d20c5..92c8d51 100644 --- a/Namcap/tests/package/test_symlink.py +++ b/Namcap/tests/package/test_symlink.py @@ -46,6 +46,7 @@ package() { ln -s ../nofile "${pkgdir}/usr/share/somelink2" ln -s //usr/share/somedata "${pkgdir}/usr/share/validlink" ln -s ../share/somedata "${pkgdir}/usr/share/validlink2" + ln -s /usr/include/math.h "${pkgdir}/usr/share/deplink" } """ def test_symlink_files(self): @@ -74,6 +75,8 @@ package() { ("usr/share/validlink", "//usr/share/somedata")), ("symlink-found %s points to %s", ("usr/share/validlink2", "../share/somedata")), + ("symlink-found %s points to %s", + ("usr/share/deplink", "/usr/include/math.h")), ])) # vim: set ts=4 sw=4 noet: diff --git a/Namcap/tests/test_depends.py b/Namcap/tests/test_depends.py index 7c6a9dc..8e74192 100644 --- a/Namcap/tests/test_depends.py +++ b/Namcap/tests/test_depends.py @@ -19,10 +19,7 @@ # USA # -import os import unittest -import tempfile -import shutil import Namcap.depends import Namcap.package diff --git a/Namcap/tests/test_pacman.py b/Namcap/tests/test_pacman.py index d406a27..0fe74ba 100644 --- a/Namcap/tests/test_pacman.py +++ b/Namcap/tests/test_pacman.py @@ -18,7 +18,7 @@ url="http://www.example.com/" license=('GPL') depends=('glibc' 'foobar') optdepends=('libabc: provides the abc feature') -provides=('yourpackage>=0.9') +provides=('yourpackage=0.9') options=('!libtool') source=(ftp://ftp.example.com/pub/mypackage-0.1.tar.gz) md5sums=('abcdefabcdef12345678901234567890') @@ -68,6 +68,6 @@ class PkgbuildLoaderTests(unittest.TestCase): def test_provides(self): self.assertEqual(self.pkginfo['provides'], ["yourpackage"]) self.assertEqual(self.pkginfo['orig_provides'], - ["yourpackage>=0.9"]) + ["yourpackage=0.9"]) # vim: set ts=4 sw=4 noet: diff --git a/Namcap/util.py b/Namcap/util.py index 21d7163..f8d38dd 100644 --- a/Namcap/util.py +++ b/Namcap/util.py @@ -19,61 +19,47 @@ import os import re -import stat -def _read_carefully(path, readcall): - if not os.path.isfile(path): - return False - reset_perms = False - if not os.access(path, os.R_OK): - # don't mess with links we can't read - if os.path.islink(path): - return None - reset_perms = True - # attempt to make it readable if possible - statinfo = os.stat(path) - newmode = statinfo.st_mode | stat.S_IRUSR - try: - os.chmod(path, newmode) - except IOError: - return None - fd = open(path, 'rb') - val = readcall(fd) - fd.close() - # reset permissions if necessary - if reset_perms: - # set file back to original permissions - os.chmod(path, statinfo.st_mode) - return val +def _file_has_magic(fileobj, magic_bytes): + length = len(magic_bytes) + magic = fileobj.read(length) + fileobj.seek(0) + return magic == magic_bytes -def is_elf(path): - """ - Given a file path, ensure it exists and peek at the first few bytes - to determine if it is an ELF file. - """ - magic = _read_carefully(path, lambda fd: fd.read(4)) - if not magic: - return False - # magic elf header, present in binaries and libraries - if magic == b"\x7FELF": - return True - else: - return False +def is_elf(fileobj): + "Take file object, peek at the magic bytes to check if ELF file." + return _file_has_magic(fileobj, b"\x7fELF") -def script_type(path): - firstline = _read_carefully(path, lambda fd: fd.readline()) - firstline = firstline.decode('ascii', 'ignore') +def is_static(fileobj): + "Take file object, peek at the magic bytes to check if static lib." + return _file_has_magic(fileobj, b"!<arch>\n") + +def is_script(fileobj): + "Take file object, peek at the magic bytes to check if script." + return _file_has_magic(fileobj, b"#!") + +def is_java(fileobj): + "Take file object, peek at the magic bytes to check if class file." + return _file_has_magic(fileobj, b"\xCA\xFE\xBA\xBE") + +def script_type(fileobj): + firstline = fileobj.readline() + fileobj.seek(0) + try: + firstline = firstline.decode('utf-8', 'strict') + except UnicodeDecodeError: + return None if not firstline: return None script = re.compile('#!.*/(.*)') m = script.match(firstline) - if m != None: - cmd = m.group(1).split() - name = cmd[0] - if name == 'env': - name = cmd[1] - return name - return None + if m is None: + return None + cmd = m.group(1).split() + name = cmd[0] + if name == 'env': + name = cmd[1] + return name clean_filename = lambda s: re.search(r"/tmp/namcap\.[0-9]*/(.*)", s).group(1) diff --git a/namcap-tags b/namcap-tags index 331bc15..f967724 100644 --- a/namcap-tags +++ b/namcap-tags @@ -16,8 +16,11 @@ dangling-symlink %s points to %s :: Symlink (%s) points to non-existing %s directory-not-world-executable %s :: Directory (%s) does not have the world executable bit set. elffile-in-any-package %s :: ELF file ('%s') found in an 'any' package. elffile-not-in-allowed-dirs %s :: ELF file ('%s') outside of a valid path. +elffile-in-questionable-dirs %s :: ELF files outside of a valid path ('%s'). elffile-with-textrel %s :: ELF file ('%s') has text relocations. elffile-with-execstack %s :: ELF file ('%s') has executable stack. +elffile-without-relro %s :: ELF file ('%s') lacks RELRO, check LDFLAGS. +elffile-unstripped %s :: ELF file ('%s') is unstripped. empty-directory %s :: Directory (%s) is empty error-running-rule %s :: Error running rule '%s' external-hooks-name %s :: .INSTALL file runs a command (%s) provided by hooks. @@ -42,6 +45,7 @@ libtool-file-present %s :: File (%s) is a libtool file library-no-package-associated %s :: Referenced library '%s' is an uninstalled dependency link-level-dependence %s in %s :: Link-level dependence (%s) in file %s lots-of-docs %f :: Package was %.0f%% docs by size; maybe you should split out a docs package +makepkg-function-used %s :: PKGBUILD uses internal makepkg '%s' subroutine missing-backup-file %s :: File in backup array (%s) not found in package missing-description :: Missing description in PKGBUILD missing-contributor :: Missing Contributor tag diff --git a/parsepkgbuild.sh b/parsepkgbuild.sh index 4df298b..12874f3 100644 --- a/parsepkgbuild.sh +++ b/parsepkgbuild.sh @@ -94,6 +94,11 @@ if [ -n "$source" ]; then for i in "${source[@]}"; do echo $i; done echo "" fi +if [ -n "$validpgpkeys" ]; then + echo "%VALIDGPGKEYS%" + for i in "${validpgpkeys[@]}"; do echo $i; done + echo "" +fi if [ -n "$md5sums" ]; then echo "%MD5SUMS%" for i in "${md5sums[@]}"; do echo $i; done |