(pure test commit)
[toast/findwwwritable.git] / findwwwritable.py
index bcbb917..66eca6a 100755 (executable)
@@ -1,33 +1,54 @@
 #!/usr/bin/python
-# python 2.x is used
+# 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(rootdir, uid, gids):
-    """Returns a list of directories below rootdir (including rootdir) that are writeable 
-    by the user with the given uid or gids or that are world writeable.
-    Normally, uid is the user id of the apache user (e.g. www-data) and gids is a list
+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 rootdir: string. directory where the search should start at
-    :param uid: integer user id
+    :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(rootdir, str)
-    assert isinstance(uid, int)
+    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(rootdir):
+    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 == uid and (s.st_mode & stat.S_IWUSR) > 0:
+            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)
@@ -40,9 +61,9 @@ def collect_writable_dirs(rootdir, uid, gids):
 
 def summarize_dirs(writable_dirs):
     """Takes a list of directories and omits each subdirectory if its parent directory
-    is also included in the list. This list is modified "in place" (nothing is returned).
+    is also included in the list. The modified list is returned.
 
-    :param writeable_dirs: List of directories (strings)."""
+    :param writable_dirs: List of directories (strings)."""
     writable_dirs = sorted(writable_dirs)
 
     i = 0
@@ -52,21 +73,101 @@ def summarize_dirs(writable_dirs):
         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.
+
+    :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)))
+
 
 
 if __name__ == '__main__':
+    # constants
+    PROGNAME = 'findwwwritable'
+    VERSION = '0.0.3'
 
     # variables
-    uid = 33                   # user id of the user whos write permissions should be found
+    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
-    rootdir = '/home'          # directory where the seach is started
+    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()
+
+    # 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)
 
-    # collect and summarize writeable directories
-    writable_dirs = collect_writable_dirs(rootdir, uid, gids)
-    summarize_dirs(writable_dirs)
+    # 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 writeable directories
+    # print writable directories
     for d in writable_dirs:
         print d
 
-