1 |
apollock |
45 |
#!/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 |
|
|
__licence__ = "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 |
|
|
def main(): |
225 |
|
|
options = process_options() |
226 |
|
|
|
227 |
|
|
setup_logging(options) |
228 |
|
|
|
229 |
|
|
ips = process_log(options) |
230 |
|
|
|
231 |
|
|
if ips: |
232 |
|
|
report_ips(options, ips) |
233 |
|
|
|
234 |
|
|
|
235 |
|
|
if __name__ == "__main__": |
236 |
|
|
main() |