-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathfg_log_parser.py
executable file
·361 lines (314 loc) · 13.2 KB
/
fg_log_parser.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#!/usr/bin/python
"""Fortigate Log Parser
Parses a Fortigate log file and presents a communication matrix.
Usage: fg_log_parser.py
fg_log_parser.py (-f <logfile> | --file <logfile>) [options]
Options:
-s --showaction Show action field.
-b --countbytes Count bytes for each communication quartet
-h --help Show this message
-v --verbose Activate verbose messages
--version Shows version information
-n --noipcheck Do not check if src and dst ip are present
-c --csv Print matrix in csv format (default is nested format)
Log Format Options (case sensitive):
--srcipfield=<srcipfield> Src ip address field [default: srcip]
--dstipfield=<dstipfield> Dst ip address field [default: dstip]
--dstportfield=<dstportfield> Dst port field [default: dstport]
--protofield=<protofield> Protocol field [default: proto]
--actionfield=<actionfield> Action field [default: action]
If countbytes options is set you may have to specify:
--sentbytesfield=<sentbytesfield> Field for sent bytes [default: sentbyte]
--rcvdbytesfield=<rcvdbytesfield> Field for rcvd bytes [default: rcvdbyte]
Examples:
Parse Fortigate Log:
fg_log_parser.py -f fg.log
Parse Iptables Log:
fg_log_parser.py -f filter --srcipfield=SRC --dstipfield=DST --dstportfield=DPT --protofield=PROTO
Parse Fortianalyzer Log:
fg_log_parser.py -f faz.log --srcipfield=src --dstipfield=dst
"""
__author__ = 'olivier'
__title__ = 'fg_log_parser'
__version__ = '0.3'
try:
from docopt import docopt
import re
import sys
import logging as log
except ImportError as ioex:
log.error("Could not import a required module")
log.error(ioex)
sys.exit(1)
def split_kv(line):
"""
Splits lines in key and value pairs and returns a dictionary.
Example:
>>> line = 'srcip=192.168.1.1 dstip=8.8.8.8 \
... dport=53 proto=53 dstcountry="United States"'
>>> split_kv(line)
{'srcip': '192.168.1.1', 'dport': '53', 'dstip': '8.8.8.8', 'dstcountry': '"United States"', 'proto': '53'}
"""
kvdelim = '=' # key and value deliminator
logline = {} # dictionary for logline
# split line in key and value pairs
# regex matches internal sub strings such as key = "word1 word2"
for field in re.findall(r'(?:[^\s,""]|"(?:\\.|[^""])*")+', line):
if kvdelim in field:
key, value = field.split(kvdelim)
logline[key] = value
return logline
def check_log_format(line, srcipfield, dstipfield):
"""
checks if srcipfield and dstipfield are in logline
Examples:
>>> line ='srcip=192.168.1.1 dstip=8.8.8.8 dstport=53 proto=53'
>>> check_log_format(line, "srcip", "dstip")
True
>>> line ='srcip=192.168.1.1 dstport=53 proto=53'
>>> check_log_format(line, "srcip", "dstip")
False
>>> line = ''
>>> check_log_format(line, "srcip", "dstip")
False
"""
log.info("check_log_format: checking line: ")
log.info(line)
if srcipfield in line and dstipfield in line:
log.info("check_log_format: found srcipfield %s", srcipfield)
log.info("check_log_format: found dstipfield %s", dstipfield)
return True
else:
return False
def translate_protonr(protocolnr):
"""
Translates ports as names.
Examples:
>>> translate_protonr(53)
53
>>> translate_protonr(1)
'ICMP'
>>> translate_protonr(6)
'TCP'
>>> translate_protonr(17)
'UDP'
"""
# check if function input was a integer
# and translate if we know translation
try:
if int(protocolnr) == 1:
return "ICMP" # icmp has protocol nr 1
elif int(protocolnr) == 6:
return "TCP" # tcp has protocol nr 6
elif int(protocolnr) == 17:
return "UDP" # udp has protocol nr 17
else:
return int(protocolnr)
# if function input was something else than int
except (ValueError, AttributeError, TypeError):
return protocolnr
def get_communication_matrix(logfile,
logformat,
countbytes=False,
noipcheck=False,
showaction=False):
"""
Reads firewall logfile and returns communication matrix as a dictionary.
Parameters:
logfile Logfile to parse
logformat dictionary containing log format
countbytes sum up bytes sent and received
Sample return matrix (one logline parsed):
{'192.168.1.1': {'8.8.8.8': {'53': {'UDP': {'count': 1}}}}}
Example:
"""
log.info("get_communication_matrix() started with parameters: ")
log.info("Option logfile: %s", logfile)
log.info("Option countbytes: %s", countbytes)
log.info("Option showaction: %s", showaction)
# assign log format options from logformat dict
srcipfield = logformat['srcipfield']
dstipfield = logformat['dstipfield']
dstportfield = logformat['dstportfield']
protofield = logformat['protofield']
sentbytesfield = logformat['sentbytesfield']
rcvdbytesfield = logformat['rcvdbytesfield']
actionfield = logformat['actionfield']
matrix = {} # communication matrix
with open(logfile, 'r') as infile:
# parse each line in file
linecount = 1 # linecount for detailed error message
for line in infile:
"""
For loop creates a nested dictionary with multiple levels.
Level description:
Level 1: srcips (source ips)
Level 2: dstips (destination ips)
Level 3: dstport (destination port number)
Level 4: proto (protocol number)
Level 4.5: action (Fortigate action)
Level 5: occurrence count
sentbytes
rcvdbytes
"""
# check if necessary fields are in first line
if linecount is 1 and not noipcheck:
# print error message if srcip or dstip are missing
if not check_log_format(line, srcipfield, dstipfield):
log.error("srcipfield or dstipfield not in line: %s ", linecount)
log.error("Check Log Format options and consult help message!")
sys.exit(1)
# split each line in key and value pairs.
logline = split_kv(line)
linecount += 1
# get() does substitute missing values with None
# missing log fields will show None in the matrix
srcip = logline.get(srcipfield)
dstip = logline.get(dstipfield)
dstport = logline.get(dstportfield)
proto = translate_protonr(logline.get(protofield))
# user has set --action
if showaction:
action = logline.get(actionfield)
# if user has set --countbytes
if countbytes:
sentbytes = logline.get(sentbytesfield)
rcvdbytes = logline.get(rcvdbytesfield)
# extend matrix for each source ip
if srcip not in matrix:
log.info("Found new srcip %s", srcip)
matrix[srcip] = {}
# extend matrix for each dstip in srcip
if dstip not in matrix[srcip]:
log.info("Found new dstip: %s for sourceip: %s", dstip, srcip)
matrix[srcip][dstip] = {}
# extend matrix for each port in comm. pair
if dstport not in matrix[srcip][dstip]:
matrix[srcip][dstip][dstport] = {}
# if proto not in matrix extend matrix
if proto not in matrix[srcip][dstip][dstport]:
matrix[srcip][dstip][dstport][proto] = {}
matrix[srcip][dstip][dstport][proto]["count"] = 1
if showaction:
matrix[srcip][dstip][dstport][proto]["action"] = action
if countbytes:
matrix[srcip][dstip][dstport][proto]["sentbytes"] \
= int(sentbytes)
matrix[srcip][dstip][dstport][proto]["rcvdbytes"] \
= int(rcvdbytes)
# if proto is already in matrix
# increase count of variable count and sum bytes
elif proto in matrix[srcip][dstip][dstport]:
matrix[srcip][dstip][dstport][proto]["count"] += 1
if countbytes:
try:
matrix[srcip][dstip][dstport][proto]["sentbytes"] \
+= int(sentbytes)
except TypeError:
pass
try:
matrix[srcip][dstip][dstport][proto]["rcvdbytes"] \
+= int(rcvdbytes)
except TypeError:
pass
log.info("Parsed %s lines in logfile: %s ", linecount, logfile)
return matrix
def print_communication_matrix(matrix, indent=0):
"""
Prints the communication matrix in a nice format.
Example:
>>> matrix = {'192.168.1.1': {'8.8.8.8': {'53': {'UDP': {'count': 1}}}}}
>>> print_communication_matrix(matrix)
192.168.1.1
8.8.8.8
53
UDP
count
1
"""
for key, value in matrix.iteritems():
# values are printed with 4 whitespace indent
print ' ' * indent + str(key)
if isinstance(value, dict):
print_communication_matrix(value, indent+1)
else:
print ' ' * (indent+1) + str(value)
return None
def print_communication_matrix_as_csv(matrix, countbytes=False, showaction=False):
"""
Prints communication matrix in csv format.
Example:
>>> matrix = {'192.168.1.1': {'8.8.8.8': {'53': {'UDP': {'count': 1}}}}}
>>> print_communication_matrix_as_csv(matrix)
srcip;dstip;dport;proto;count;action;sentbytes;rcvdbytes
192.168.1.1;8.8.8.8;53;UDP;1;None
Example 2 (option countbytes set):
>>> matrix = {'192.168.1.1': {'8.8.8.8': {'53': {'UDP': {'count': 1, 'sentbytes': 10, 'rcvdbytes': 10}}}}}
>>> print_communication_matrix_as_csv(matrix, countbytes=True)
srcip;dstip;dport;proto;count;action;sentbytes;rcvdbytes
192.168.1.1;8.8.8.8;53;UDP;1;None;10;10
"""
# Header
print "srcip;dstip;dport;proto;count;action;sentbytes;rcvdbytes"
for srcip in matrix.keys():
for dstip in matrix.get(srcip):
for dport in matrix[srcip][dstip].keys():
for proto in matrix[srcip][dstip].get(dport):
count = matrix[srcip][dstip][dport][proto].get("count")
if showaction:
action = matrix[srcip][dstip][dport][proto].get("action")
else:
action = "None"
if countbytes:
rcvdbytes = matrix[srcip][dstip][dport][proto].get("rcvdbytes")
sentbytes = matrix[srcip][dstip][dport][proto].get("sentbytes")
print "%s;%s;%s;%s;%s;%s;%s;%s" % (srcip, dstip, dport, proto, count, action, sentbytes, rcvdbytes)
else:
print "%s;%s;%s;%s;%s;%s" % (srcip, dstip, dport, proto, count, action)
def main():
"""
Main function.
"""
# get arguments from docopt
arguments = docopt(__doc__)
arguments = docopt(__doc__, version='Fortigate Log Parser 0.3')
# assign docopt argument
# check module documentation for argument description
logfile = arguments['<logfile>']
countbytes = arguments['--countbytes']
verbose = arguments['--verbose']
noipcheck = arguments['--noipcheck']
csv = arguments['--csv']
showaction = arguments['--showaction']
# define logfile format
# note: default values are set in the docopt string, see __doc__
logformat = {'srcipfield': arguments['--srcipfield'],
'dstipfield': arguments['--dstipfield'],
'dstportfield': arguments['--dstportfield'],
'protofield': arguments['--protofield'],
'sentbytesfield': arguments['--sentbytesfield'],
'rcvdbytesfield': arguments['--rcvdbytesfield'],
'actionfield': arguments['--actionfield']
}
# set loglevel
if verbose:
log.basicConfig(format="%(levelname)s: %(message)s", level=log.DEBUG)
log.info("Verbose output activated.")
else:
log.basicConfig(format="%(levelname)s: %(message)s")
log.info("Script was started with arguments: ")
log.info(arguments)
# check if logfile argument is present
if logfile is None:
print __doc__
sys.exit(1)
# parse log
log.info("Reading firewall log...")
matrix = get_communication_matrix(logfile, logformat, countbytes, noipcheck, showaction)
if csv:
print_communication_matrix_as_csv(matrix, countbytes, showaction)
else:
print_communication_matrix(matrix)
return 0
if __name__ == "__main__":
sys.exit(main())