Added the argparse module to be able to use command line arguments.
[toast/findwwwritable.git] / findwwwritable.py
index 346dc90b482578e75c9998d952bbde1824b0ab3f..efd77ad96826356948c94f47cbe95566a5439f89 100755 (executable)
 #!/usr/bin/python
+# python 2.7+ is used
+
 import os
 import stat
 from os.path import join
+import argparse
+
+
+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.2'
 
-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='configuration file (default: {})'.format(config_filename), default=config_filename)
+    parser.add_argument('-w', '--whitelist', help='filename of whitelist (default: {})'.format(whitelist_filename), default=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='*', default=basedir, help='directories are searched below basedir (default: {})'.format(basedir))
+    args = parser.parse_args()
+    if args.uids is None: args.uids = uids
+    if args.gids is None: args.gids = gids
 
-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
+    # read whitelist
+    whitelist = read_whitelist(args.whitelist)
 
+    # collect and summarize writable directories
+    writable_dirs = []
+    for basedir in args.basedir:
+        writable_dirs.extend(collect_writable_dirs(basedir, args.uids, args.gids))
+    if not args.full: writable_dirs = summarize_dirs(writable_dirs)
+    writable_dirs = apply_whitelist(writable_dirs, whitelist)
 
-for fp in bad_dirs:
-       print fp
+    # print writable directories
+    for d in writable_dirs:
+        print d