1 |
#!/usr/bin/python |
2 |
|
3 |
import optparse |
4 |
import logging |
5 |
import urllib |
6 |
import sys |
7 |
|
8 |
# |
9 |
# See |
10 |
# http://blog.steve.org.uk/wash_your_face_and_try_again__if_you_survive_.html |
11 |
# http://blog.steve.org.uk/i_don_t_have_no_other_pants_.html |
12 |
# for more background |
13 |
# |
14 |
# Copyright (c) 2008 Andrew Pollock <me@andrew.net.au> |
15 |
# |
16 |
# Permission to use, copy, distribute, modify, and otherwise knock yourself out |
17 |
# granted under the terms of the GNU General Public Licence v2 |
18 |
# |
19 |
|
20 |
__author__ = "Andrew Pollock <me@andrew.net.au>" |
21 |
__license__ = "GPLv2" |
22 |
|
23 |
HOSTNAME_FIELD=3 |
24 |
PREFIX_FIELD_START=4 |
25 |
FIELD_AFTER_PREFIX="IN=" |
26 |
SRC_FIELD_OFFSET=3 |
27 |
|
28 |
|
29 |
def setup_logging(options): |
30 |
""" |
31 |
Initialise logging |
32 |
|
33 |
Args: |
34 |
options: options object |
35 |
""" |
36 |
|
37 |
logging.getLogger("").setLevel(1) |
38 |
|
39 |
console = logging.StreamHandler() |
40 |
console_formatter = logging.Formatter("%(levelname)-8s %(message)s") |
41 |
console.setFormatter(console_formatter) |
42 |
if options.verbose: |
43 |
console.setLevel(logging.DEBUG) |
44 |
else: |
45 |
console.setLevel(logging.INFO) |
46 |
|
47 |
logging.getLogger("").addHandler(console) |
48 |
|
49 |
|
50 |
def process_options(): |
51 |
""" |
52 |
Do all the command line option parsing |
53 |
|
54 |
Returns: |
55 |
an options object |
56 |
""" |
57 |
|
58 |
USAGE="usage: %prog [options]" |
59 |
|
60 |
parser = optparse.OptionParser(usage=USAGE) |
61 |
parser.add_option( |
62 |
"-v", "--verbose", |
63 |
default=False, |
64 |
action="store_true", |
65 |
dest="verbose", |
66 |
help="increase verbosity", |
67 |
) |
68 |
parser.add_option( |
69 |
"-p", "--prefix", |
70 |
default="", |
71 |
action="store", |
72 |
dest="prefix", |
73 |
help="prefix to restrict matches to", |
74 |
) |
75 |
parser.add_option( |
76 |
"--host", |
77 |
default="", |
78 |
action="store", |
79 |
dest="hostname", |
80 |
help="host to restrict matches to", |
81 |
) |
82 |
parser.add_option( |
83 |
"-l", |
84 |
"--logfile", |
85 |
default="/var/log/ulog/syslogemu.log", |
86 |
action="store", |
87 |
dest="log", |
88 |
help="log file to process [default: %default]", |
89 |
) |
90 |
parser.add_option( |
91 |
"-n", |
92 |
"--dry-run", |
93 |
default=False, |
94 |
action="store_true", |
95 |
dest="dryrun", |
96 |
help="do nothing, just show what would happen", |
97 |
) |
98 |
(options, unused_args) = parser.parse_args() |
99 |
|
100 |
return options |
101 |
|
102 |
|
103 |
def get_prefix(ulogentry): |
104 |
""" |
105 |
Return the prefix component of a ulog entry |
106 |
|
107 |
Args: |
108 |
ulogentry: a list of strings representing one line of a ulog entry |
109 |
|
110 |
Returns: |
111 |
A list consisting of the prefix and the number of the field after the |
112 |
prefix |
113 |
""" |
114 |
|
115 |
prefix=[] |
116 |
|
117 |
for component in xrange(PREFIX_FIELD_START, len(ulogentry)): |
118 |
if not ulogentry[component].startswith(FIELD_AFTER_PREFIX): |
119 |
prefix.append(ulogentry[component]) |
120 |
else: |
121 |
break |
122 |
|
123 |
# If for some reason we reached the end of the line without finding |
124 |
# FIELD_AFTER_PREFIX, something is probably wrong |
125 |
|
126 |
if component == len(ulogentry): |
127 |
return None |
128 |
else: |
129 |
return (" ".join(prefix), component) |
130 |
|
131 |
|
132 |
def process_log(options): |
133 |
""" |
134 |
Process the log file |
135 |
|
136 |
Args: |
137 |
options object |
138 |
|
139 |
Returns: |
140 |
A set of IP addresses |
141 |
|
142 |
Discussion: |
143 |
|
144 |
A line of the log file can look like: |
145 |
Aug 31 06:46:06 caesar FORWARD too-hard-basket IN=eth0 OUT=eth1 MAC=00:08:02:52: |
146 |
49:d7:00:02:3b:01:f0:87:08:00 SRC=64.142.100.44 DST=172.16.0.7 LEN=128 TOS=00 P |
147 |
REC=0x00 TTL=59 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=42504 SEQ=6344 |
148 |
|
149 |
Notably, the "FORWARD too-hard-basket" is a user definable string, which can |
150 |
contain whitespace, so therefore there's no predetermined position that the |
151 |
SRC= field can be found in, therefore for each line, we need to iterate over |
152 |
each whitespace-separated "word" until we find the right one. Bit of a bummer |
153 |
that. |
154 |
""" |
155 |
|
156 |
# TODO(apollock): This needs refactoring, there's too much duplication |
157 |
|
158 |
ips = set() |
159 |
|
160 |
# TODO(apollock): exception handling! |
161 |
try: |
162 |
log = open(options.log) |
163 |
except IOError, e: |
164 |
logging.critical("Could not open %s (%s)" % (options.log, e.strerror)) |
165 |
sys.exit(1) |
166 |
while True: |
167 |
log_entry = log.readline().split() |
168 |
if not log_entry: |
169 |
logging.debug("Reached the end of the file") |
170 |
break |
171 |
if options.hostname and log_entry[HOSTNAME_FIELD] != options.hostname: |
172 |
logging.debug("Entry not for the host we're looking for") |
173 |
continue |
174 |
# We know where the prefix starts (if there is one) |
175 |
if log_entry[PREFIX_FIELD_START].startswith(FIELD_AFTER_PREFIX): |
176 |
# We have an entry with no prefix at all |
177 |
logging.debug("Entry has no prefix") |
178 |
if options.prefix: |
179 |
continue |
180 |
else: |
181 |
# We need to build the prefix and check |
182 |
(prefix, next_field) = get_prefix(log_entry) |
183 |
if prefix and prefix == options.prefix: |
184 |
# We have a valid line to work with |
185 |
ips.add(log_entry[next_field + SRC_FIELD_OFFSET].split("SRC=")[1]) |
186 |
else: |
187 |
logging.debug("(1) Entry not for the prefix we're looking for") |
188 |
continue |
189 |
else: |
190 |
# We have part of the prefix and need to build it |
191 |
(prefix, next_field) = get_prefix(log_entry) |
192 |
if prefix and prefix == options.prefix: |
193 |
# We have a valid line to work with |
194 |
ips.add(log_entry[next_field + SRC_FIELD_OFFSET].split("SRC=")[1]) |
195 |
else: |
196 |
logging.debug("(2) Entry not for the prefix we're looking for: %s" |
197 |
% " ".join(log_entry)) |
198 |
continue |
199 |
log.close() |
200 |
|
201 |
return ips |
202 |
|
203 |
|
204 |
def report_ips(options, ips): |
205 |
""" |
206 |
Submit the IPs to the blacklist service |
207 |
|
208 |
Args: |
209 |
options: options object |
210 |
ips: set of IP addresses to report |
211 |
""" |
212 |
|
213 |
SUBMISSION_URL="http://blacklist.steve.org.uk/cgi-bin/report.cgi?src=%s" |
214 |
#SUBMISSION_URL="http://www.andrew.net.au/cgi-bin/report.cgi?src=%s" |
215 |
|
216 |
for ip in ips: |
217 |
if not options.dryrun: |
218 |
result = urllib.urlopen(SUBMISSION_URL % (ip)) |
219 |
logging.info("%s: %s" % (ip, "".join(result.readlines()).rstrip())) |
220 |
result.close() |
221 |
else: |
222 |
print SUBMISSION_URL % (ip) |
223 |
|
224 |
|
225 |
def main(): |
226 |
options = process_options() |
227 |
|
228 |
setup_logging(options) |
229 |
|
230 |
ips = process_log(options) |
231 |
|
232 |
if ips: |
233 |
report_ips(options, ips) |
234 |
|
235 |
|
236 |
if __name__ == "__main__": |
237 |
main() |