(pure test commit)
[toast/findwwwritable.git] / findwwwritable.py
index 346dc90..66eca6a 100755 (executable)
 #!/usr/bin/python
+# python 2.7+ is used
+
+# Copyright (C) 2011-2012 Philipp Spitzer, gregor herrmann, Stefan Stahl
+#
+# This file is part of findwwwritable.
+#
+# findwwwritable 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.
+#
+# findwwwritable 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
+# findwwwritable.  If not, see <http://www.gnu.org/licenses/>.
+
 import os
+import sys
 import stat
 from os.path import join
+import argparse
+import ConfigParser
+
+
+def collect_writable_dirs(basedir, uids, gids):
+    """Returns a list of directories below basedir (including basedir) that are writable
+    by the users with the given uids or gids or that are world writable.
+    Normally, uid(s) is the user id of the apache user (e.g. www-data) and gids is a list
+    of group ids this user is member of.
+    
+    :param basedir: string. directory where the search should start at
+    :param uids: list of integer user ids
+    :param gids: list of integer group ids"""
+    assert isinstance(basedir, str)
+    assert isinstance(uids, list)
+    for uid in uids: assert isinstance(uid, int)
+    assert isinstance(gids, list)
+    for gid in gids: assert isinstance(gid, int)
+
+    writable_dirs = [] # dirs with write permissions - this list is filled by this function
+
+    for root, dirs, files in os.walk(basedir):
+        for d in dirs:
+            dp = join(root, d) # dp is the dir with path
+            s = os.lstat(dp)
+            if (s.st_mode & stat.S_IFLNK) == stat.S_IFLNK: continue # skip symlinks
+            if s.st_uid in uids and (s.st_mode & stat.S_IWUSR) > 0:
+                writable_dirs.append(dp)
+            elif s.st_gid in gids and (s.st_mode & stat.S_IWGRP) > 0:
+                writable_dirs.append(dp)
+            elif (s.st_mode & stat.S_IWOTH) > 0:
+                writable_dirs.append(dp)
+
+    return writable_dirs
+
+
+
+def summarize_dirs(writable_dirs):
+    """Takes a list of directories and omits each subdirectory if its parent directory
+    is also included in the list. The modified list is returned.
+
+    :param writable_dirs: List of directories (strings)."""
+    writable_dirs = sorted(writable_dirs)
+
+    i = 0
+    while i < len(writable_dirs)-1:
+        if writable_dirs[i+1].startswith(writable_dirs[i] + '/'):
+            del writable_dirs[i+1]
+        else:
+            i += 1
+
+    return writable_dirs
+
+
+def read_whitelist(whitelist_filename):
+    """Reads the given whitelist (one directory name per line) and returns it as list.
+    Empty lines are omitted. Lines beginning with # are omitted as well.
+    If the file does not exist, it returns an empty list."""
+    whitelist = []
+    try: file = open(whitelist_filename, 'r')
+    except IOError: return []
+    for line in file:
+        line = line.strip()
+        if len(line) == 0: continue
+        if line[0] == '#': continue
+        whitelist.append(line)
+    file.close()
+    return sorted(set(whitelist))
+
+
+def apply_whitelist(writable_dirs, whitelist):
+    """Removes all directories that are contained in the list whitelist from the list writable_dirs.
+    It returns the modified writable_dirs.
 
-wwwdata_uid = 33
-wwwdata_gids = [33, 42, 121, 127]
+    :param writable_dirs: List of directories
+    :param whitelist: List of directories that should be removed from writable_dirs.
+    :return: list of writable directories."""
+    return sorted(list(set(writable_dirs).difference(whitelist)))
 
-# collect bad files
-bad_dirs = [] # wwwdata has write permissions
 
-for root, dirs, files in os.walk('/home'):
-       for f in dirs:
-               fp = join(root, f) # fp is the dir with path
-               s = os.lstat(fp)
-               if (s.st_mode & stat.S_IFLNK) == stat.S_IFLNK: continue # skip symlinks
-               if s.st_uid == wwwdata_uid and (s.st_mode & stat.S_IWUSR) > 0:
-                       bad_dirs.append(fp)
-               elif s.st_gid in wwwdata_gids and (s.st_mode & stat.S_IWGRP) > 0:
-                       bad_dirs.append(fp)
-               elif (s.st_mode & stat.S_IWOTH) > 0:
-                       bad_dirs.append(fp)
 
+if __name__ == '__main__':
+    # constants
+    PROGNAME = 'findwwwritable'
+    VERSION = '0.0.3'
 
-bad_dirs = sorted(bad_dirs)
+    # variables
+    uids = [33]                # user ids of the user whos write permissions should be found
+    gids = [33, 42, 121, 127]  # group ids of the user whos write permissions should be found
+    basedir = '/home'          # directory where the seach is started
+    config_filename = '/etc/findwwwritable/config'
+    whitelist_filename = '/etc/findwwwritable/whitelist' # list of directories that are known to be writable
+                                                         # and that should not be reported.
 
+    # parse command line arguments
+    parser = argparse.ArgumentParser(description='find directories that are writeable by www-data (or an other user)')
+    parser.add_argument('-v', '--version', action='version', version='{} {}'.format(PROGNAME, VERSION))
+    parser.add_argument('-c', '--config', help='additional configuration file')
+    parser.add_argument('-w', '--whitelist', help='filename of whitelist (default: {})'.format(whitelist_filename))
+    parser.add_argument('-f', '--full', help='do not omit subdirs', action='store_true')
+    parser.add_argument('-u', '--uid', dest='uids', help='system uid of the user. may be specified more than once. (default: {})'.format(uids), action='append', type=int)
+    parser.add_argument('-g', '--gid', dest='gids', help='system gid of the user. may be specified more than once. (default: {})'.format(gids), action='append', type=int)
+    parser.add_argument('basedir', nargs='*', help='directories are searched below basedir (default: {})'.format(basedir))
+    args = parser.parse_args()
 
-i = 0
-while i < len(bad_dirs)-1:
-       if bad_dirs[i+1].startswith(bad_dirs[i] + '/'):
-               del bad_dirs[i+1]
-       else:
-               i += 1
+    # parse config file
+    config = ConfigParser.ConfigParser({'whitelist': whitelist_filename, 'full': '0'})
+    config_files = [config_filename, os.path.expanduser('~/.'+PROGNAME)]
+    if not args.config is None: config_files.append(args.config)
+    config.read(config_files)
+    if args.whitelist is None: args.whitelist = config.get(PROGNAME, 'whitelist')
+    if not args.full: args.full = config.getboolean(PROGNAME, 'full')
+    if args.uids is None:
+        if config.has_option(PROGNAME, 'uids'):
+            uids = config.get(PROGNAME, 'uids')
+            uids = uids.split(' ')
+            args.uids = map(int, uids)
+        else:
+            args.uids = uids
+    if args.gids is None:
+        if config.has_option(PROGNAME, 'gids'):
+            gids = config.get(PROGNAME, 'gids')
+            gids = gids.split(' ')
+            args.gids = map(int, gids)
+        else:
+            args.gids = gids
+    if len(args.basedir) == 0:
+        if config.has_option(PROGNAME, 'basedir'):
+            basedir = config.get(PROGNAME, 'basedir')
+            basedir = basedir.split(' ')
+            args.basedir = map(int, basedir)
+        else:
+            args.basedir = [basedir]
 
+    # read whitelist
+    whitelist = read_whitelist(args.whitelist)
 
-for fp in bad_dirs:
-       print fp
+    # collect and summarize writable directories
+    writable_dirs = []
+    for basedir in args.basedir:
+        writable_dirs.extend(collect_writable_dirs(basedir, args.uids, args.gids))
+    writable_dirs = sorted(set(writable_dirs))
+    if not args.full: writable_dirs = summarize_dirs(writable_dirs)
+    writable_dirs = apply_whitelist(writable_dirs, whitelist)
 
+    # print writable directories
+    for d in writable_dirs:
+        print d