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