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