Added the argparse module to be able to use command line arguments.
[toast/findwwwritable.git] / findwwwritable.py
1 #!/usr/bin/python
2 # python 2.7+ is used
3
4 import os
5 import stat
6 from os.path import join
7 import argparse
8
9
10 def collect_writable_dirs(basedir, uids, gids):
11     """Returns a list of directories below basedir (including basedir) that are writable
12     by the users with the given uids or gids or that are world writable.
13     Normally, uid(s) is the user id of the apache user (e.g. www-data) and gids is a list
14     of group ids this user is member of.
15     
16     :param basedir: string. directory where the search should start at
17     :param uids: list of integer user ids
18     :param gids: list of integer group ids"""
19     assert isinstance(basedir, str)
20     assert isinstance(uids, list)
21     for uid in uids: assert isinstance(uid, int)
22     assert isinstance(gids, list)
23     for gid in gids: assert isinstance(gid, int)
24
25     writable_dirs = [] # dirs with write permissions - this list is filled by this function
26
27     for root, dirs, files in os.walk(basedir):
28         for d in dirs:
29             dp = join(root, d) # dp is the dir with path
30             s = os.lstat(dp)
31             if (s.st_mode & stat.S_IFLNK) == stat.S_IFLNK: continue # skip symlinks
32             if s.st_uid in uids and (s.st_mode & stat.S_IWUSR) > 0:
33                 writable_dirs.append(dp)
34             elif s.st_gid in gids and (s.st_mode & stat.S_IWGRP) > 0:
35                 writable_dirs.append(dp)
36             elif (s.st_mode & stat.S_IWOTH) > 0:
37                 writable_dirs.append(dp)
38
39     return writable_dirs
40
41
42
43 def summarize_dirs(writable_dirs):
44     """Takes a list of directories and omits each subdirectory if its parent directory
45     is also included in the list. The modified list is returned.
46
47     :param writable_dirs: List of directories (strings)."""
48     writable_dirs = sorted(writable_dirs)
49
50     i = 0
51     while i < len(writable_dirs)-1:
52         if writable_dirs[i+1].startswith(writable_dirs[i] + '/'):
53             del writable_dirs[i+1]
54         else:
55             i += 1
56
57     return writable_dirs
58
59
60 def read_whitelist(whitelist_filename):
61     """Reads the given whitelist (one directory name per line) and returns it as list.
62     Empty lines are omitted. Lines beginning with # are omitted as well.
63     If the file does not exist, it returns an empty list."""
64     whitelist = []
65     try: file = open(whitelist_filename, 'r')
66     except IOError: return []
67     for line in file:
68         line = line.strip()
69         if len(line) == 0: continue
70         if line[0] == '#': continue
71         whitelist.append(line)
72     file.close()
73     return sorted(set(whitelist))
74
75
76 def apply_whitelist(writable_dirs, whitelist):
77     """Removes all directories that are contained in the list whitelist from the list writable_dirs.
78     It returns the modified writable_dirs.
79
80     :param writable_dirs: List of directories
81     :param whitelist: List of directories that should be removed from writable_dirs.
82     :return: list of writable directories."""
83     return sorted(list(set(writable_dirs).difference(whitelist)))
84
85
86
87 if __name__ == '__main__':
88     # constants
89     PROGNAME = 'findwwwritable'
90     VERSION = '0.0.2'
91
92     # variables
93     uids = [33]                # user ids of the user whos write permissions should be found
94     gids = [33, 42, 121, 127]  # group ids of the user whos write permissions should be found
95     basedir = '/home'          # directory where the seach is started
96     config_filename = '/etc/findwwwritable/config'
97     whitelist_filename = '/etc/findwwwritable/whitelist' # list of directories that are known to be writable
98                                                          # and that should not be reported.
99
100     # parse command line arguments
101     parser = argparse.ArgumentParser(description='find directories that are writeable by www-data (or an other user)')
102     parser.add_argument('-v', '--version', action='version', version='{} {}'.format(PROGNAME, VERSION))
103     parser.add_argument('-c', '--config', help='configuration file (default: {})'.format(config_filename), default=config_filename)
104     parser.add_argument('-w', '--whitelist', help='filename of whitelist (default: {})'.format(whitelist_filename), default=whitelist_filename)
105     parser.add_argument('-f', '--full', help='do not omit subdirs', action='store_true')
106     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)
107     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)
108     parser.add_argument('basedir', nargs='*', default=basedir, help='directories are searched below basedir (default: {})'.format(basedir))
109     args = parser.parse_args()
110     if args.uids is None: args.uids = uids
111     if args.gids is None: args.gids = gids
112
113     # read whitelist
114     whitelist = read_whitelist(args.whitelist)
115
116     # collect and summarize writable directories
117     writable_dirs = []
118     for basedir in args.basedir:
119         writable_dirs.extend(collect_writable_dirs(basedir, args.uids, args.gids))
120     if not args.full: writable_dirs = summarize_dirs(writable_dirs)
121     writable_dirs = apply_whitelist(writable_dirs, whitelist)
122
123     # print writable directories
124     for d in writable_dirs:
125         print d
126
127