modsecurity_audit_log_parser 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/modsecurity_audit_log_parser.rb +291 -198
- data/lib/modsecurity_audit_log_parser/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0303b7ba962015523229a03ae83e32867129d06
|
4
|
+
data.tar.gz: f7bb18fa8a07687dada6ca3c9e7154c7affce545
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bffeaa91475d4e19b2d927d160972c3633298322d48dfd19cdee7bf0c2349f584a0473a04c7180b92320bd42602541516424f0d0fa767a7dd64c819097ba918f
|
7
|
+
data.tar.gz: abb675b5710ddb327fa0b12d0294807197613d699371265577d00940623f4d99d56998dbe540995594520c7e78cb906a6f41b0a07fd4da4ccd95a80803eff7ae
|
@@ -1,291 +1,371 @@
|
|
1
1
|
require 'modsecurity_audit_log_parser/version'
|
2
2
|
require 'date'
|
3
|
+
require 'json'
|
4
|
+
require 'time'
|
3
5
|
|
4
6
|
class ModsecurityAuditLogParser
|
5
|
-
class
|
6
|
-
|
7
|
+
class NativeParser
|
8
|
+
class Log
|
9
|
+
attr_reader :id
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
def initialize(id)
|
12
|
+
@id = id
|
13
|
+
@parts = {}
|
14
|
+
end
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
def add(part)
|
17
|
+
@parts[part.type] = part
|
18
|
+
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
MODSEC_TIMESTAMP_FORMAT = '%d/%b/%Y:%H:%M:%S %z'
|
21
|
+
def time
|
22
|
+
if ah = audit_log_header
|
23
|
+
if ts = ah.timestamp
|
24
|
+
DateTime.strptime(ts, MODSEC_TIMESTAMP_FORMAT).to_time.to_i rescue 0
|
25
|
+
end
|
22
26
|
end
|
23
27
|
end
|
24
|
-
end
|
25
28
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
+
[:timestamp, :unique_transaction_id, :source_ip_address, :source_port, :destination_ip_address, :destination_port].each do |name|
|
30
|
+
define_method(name) do
|
31
|
+
audit_log_header.send(name)
|
32
|
+
end
|
29
33
|
end
|
30
|
-
end
|
31
34
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
+
def trailers
|
36
|
+
audit_log_trailer.trailers
|
37
|
+
end
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
def rules
|
40
|
+
audit_log_trailer.rules
|
41
|
+
end
|
39
42
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
def audit_log_header
|
44
|
+
@parts['A'] || EMPTY_AUDIT_LOG_HEADER
|
45
|
+
end
|
43
46
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
+
def request_headers
|
48
|
+
@parts['B']
|
49
|
+
end
|
47
50
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
+
def request_body
|
52
|
+
@parts['C']
|
53
|
+
end
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
+
def original_response_body
|
56
|
+
@parts['E']
|
57
|
+
end
|
55
58
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
+
def response_header
|
60
|
+
@parts['F']
|
61
|
+
end
|
59
62
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
+
def audit_log_trailer
|
64
|
+
@parts['H'] || EMPTY_AUDIT_LOG_TRAILER
|
65
|
+
end
|
63
66
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
+
def reduced_multipart_request_body
|
68
|
+
@parts['I']
|
69
|
+
end
|
67
70
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
+
def multipart_files_information
|
72
|
+
@parts['J']
|
73
|
+
end
|
71
74
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
+
def matched_rules_information
|
76
|
+
@parts['K']
|
77
|
+
end
|
75
78
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
+
def to_h
|
80
|
+
@parts.inject(Hash.new) { |r, (k, v)|
|
81
|
+
v.merge!(r)
|
79
82
|
r
|
80
|
-
|
83
|
+
}
|
84
|
+
end
|
81
85
|
end
|
82
|
-
end
|
83
86
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
87
|
+
class Part
|
88
|
+
module PartClassMethods
|
89
|
+
def register(type, klass)
|
90
|
+
(@@registry ||= {})[type] = klass
|
91
|
+
@type = type
|
92
|
+
end
|
90
93
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
94
|
+
def type
|
95
|
+
@type
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.class_for_type(type)
|
99
|
+
@@registry[type]
|
100
|
+
end
|
97
101
|
end
|
98
|
-
end
|
99
102
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
+
def self.inherited(klass)
|
104
|
+
klass.extend(PartClassMethods)
|
105
|
+
end
|
103
106
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
+
def type
|
108
|
+
self.class.type
|
109
|
+
end
|
107
110
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
+
def self.new_subclass(type)
|
112
|
+
PartClassMethods.class_for_type(type).new
|
113
|
+
end
|
111
114
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
+
def add(line)
|
116
|
+
raise
|
117
|
+
end
|
115
118
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
119
|
+
def to_hash
|
120
|
+
hash = {}
|
121
|
+
merge!(hash)
|
122
|
+
hash
|
123
|
+
end
|
121
124
|
|
122
|
-
|
123
|
-
|
125
|
+
def merge!(hash)
|
126
|
+
raise
|
127
|
+
end
|
124
128
|
end
|
125
|
-
end
|
126
129
|
|
127
|
-
|
128
|
-
|
130
|
+
class ContentPart < Part
|
131
|
+
attr_reader :content
|
129
132
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
+
def initialize
|
134
|
+
@content = ''
|
135
|
+
end
|
133
136
|
|
134
|
-
|
135
|
-
|
137
|
+
def add(line)
|
138
|
+
@content << line
|
139
|
+
end
|
136
140
|
end
|
137
|
-
end
|
138
141
|
|
139
|
-
|
140
|
-
|
142
|
+
class AuditLogHeaderPart < Part
|
143
|
+
register('A', self)
|
141
144
|
|
142
|
-
|
145
|
+
attr_reader :timestamp, :unique_transaction_id, :source_ip_address, :source_port, :destination_ip_address, :destination_port
|
143
146
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
147
|
+
def add(line)
|
148
|
+
datetime, rest = line.chomp.split(/\] /, 2)
|
149
|
+
@timestamp = datetime.sub(/\[/, '')
|
150
|
+
@unique_transaction_id, @source_ip_address, @source_port, @destination_ip_address, @destination_port = rest.split(/ /, 5)
|
151
|
+
end
|
149
152
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
153
|
+
def merge!(hash)
|
154
|
+
hash[:timestamp] = @timestamp
|
155
|
+
hash[:unique_transaction_id] = @unique_transaction_id
|
156
|
+
hash[:source_ip_address] = @source_ip_address
|
157
|
+
hash[:source_port] = @source_port
|
158
|
+
hash[:destination_ip_address] = @destination_ip_address
|
159
|
+
hash[:destination_port] = @destination_port
|
160
|
+
end
|
157
161
|
end
|
158
|
-
|
159
|
-
EMPTY_AUDIT_LOG_HEADER = AuditLogHeaderPart.new
|
162
|
+
EMPTY_AUDIT_LOG_HEADER = AuditLogHeaderPart.new
|
160
163
|
|
161
|
-
|
162
|
-
|
164
|
+
class RequestHeadersPart < ContentPart
|
165
|
+
register('B', self)
|
163
166
|
|
164
|
-
|
165
|
-
|
167
|
+
def merge!(hash)
|
168
|
+
hash[:request_headers] = @content
|
169
|
+
end
|
166
170
|
end
|
167
|
-
end
|
168
171
|
|
169
|
-
|
170
|
-
|
172
|
+
class RequestBodyPart < ContentPart
|
173
|
+
register('C', self)
|
171
174
|
|
172
|
-
|
173
|
-
|
175
|
+
def merge!(hash)
|
176
|
+
hash[:request_body] = @content
|
177
|
+
end
|
174
178
|
end
|
175
|
-
end
|
176
179
|
|
177
|
-
|
178
|
-
|
180
|
+
class OriginalResponseBodyPart < ContentPart
|
181
|
+
register('E', self)
|
179
182
|
|
180
|
-
|
181
|
-
|
183
|
+
def merge!(hash)
|
184
|
+
hash[:original_response_body] = @content
|
185
|
+
end
|
182
186
|
end
|
183
|
-
end
|
184
187
|
|
185
|
-
|
186
|
-
|
188
|
+
class ResponseHeadersPart < ContentPart
|
189
|
+
register('F', self)
|
187
190
|
|
188
|
-
|
189
|
-
|
191
|
+
def merge!(hash)
|
192
|
+
hash[:response_headers] = @content
|
193
|
+
end
|
190
194
|
end
|
191
|
-
end
|
192
195
|
|
193
|
-
|
194
|
-
|
196
|
+
class AuditLogTrailerPart < Part
|
197
|
+
register('H', self)
|
195
198
|
|
196
|
-
|
199
|
+
attr_reader :trailers
|
197
200
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
+
def initialize
|
202
|
+
@trailers = {}
|
203
|
+
end
|
201
204
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
205
|
+
def add(line)
|
206
|
+
key, value = line.chomp.split(/: /, 2)
|
207
|
+
if key == 'Message'
|
208
|
+
(@trailers[:Message] ||= '') << value << "\n"
|
209
|
+
elsif key
|
210
|
+
@trailers[key.intern] = value
|
211
|
+
end
|
208
212
|
end
|
209
|
-
end
|
210
213
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
214
|
+
def rules
|
215
|
+
if message = @trailers[:Message]
|
216
|
+
if pairs = message.scan(/\[(\w+) "([^\\"]*(?:\\.[^\\"]*)*)"\]/)
|
217
|
+
pairs.inject({}) { |r, (k, v)|
|
218
|
+
r["rule_#{k}".intern] = v
|
216
219
|
r
|
217
|
-
|
220
|
+
}
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def merge!(hash)
|
226
|
+
hash.merge!(@trailers)
|
227
|
+
if h = rules
|
228
|
+
hash.merge!(h)
|
218
229
|
end
|
219
230
|
end
|
220
231
|
end
|
232
|
+
EMPTY_AUDIT_LOG_TRAILER = AuditLogTrailerPart.new
|
233
|
+
|
234
|
+
class ReducedMultipartRequestBodyPart < ContentPart
|
235
|
+
register('I', self)
|
221
236
|
|
222
|
-
|
223
|
-
|
224
|
-
if h = rules
|
225
|
-
hash.merge!(h)
|
237
|
+
def merge!(hash)
|
238
|
+
hash[:reduced_multipart_request_body] = @content
|
226
239
|
end
|
227
240
|
end
|
228
|
-
end
|
229
|
-
EMPTY_AUDIT_LOG_TRAILER = AuditLogTrailerPart.new
|
230
241
|
|
231
|
-
|
232
|
-
|
242
|
+
class MultipartFilesInformationPart < ContentPart
|
243
|
+
register('J', self)
|
233
244
|
|
234
|
-
|
235
|
-
|
245
|
+
def merge!(hash)
|
246
|
+
hash[:multipart_files_information] = @content
|
247
|
+
end
|
236
248
|
end
|
237
|
-
end
|
238
249
|
|
239
|
-
|
240
|
-
|
250
|
+
class MatchedRulesInformationPart < ContentPart
|
251
|
+
register('K', self)
|
241
252
|
|
242
|
-
|
243
|
-
|
253
|
+
def merge!(hash)
|
254
|
+
hash[:matched_rules_information] = @content
|
255
|
+
end
|
244
256
|
end
|
245
|
-
end
|
246
257
|
|
247
|
-
|
248
|
-
|
258
|
+
class AuditLogFooterPart < Part
|
259
|
+
register('Z', self)
|
260
|
+
|
261
|
+
def add(line)
|
262
|
+
# ignore
|
263
|
+
end
|
249
264
|
|
250
|
-
|
251
|
-
|
265
|
+
def merge!(hash)
|
266
|
+
# ignore
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def initialize(targets)
|
271
|
+
@log = @part = nil
|
272
|
+
@targets = targets.split('')
|
273
|
+
end
|
274
|
+
|
275
|
+
def parse(str)
|
276
|
+
str.each_line do |line|
|
277
|
+
if /\A--([0-9a-z]+)-(.)--/ =~ line
|
278
|
+
id, type = $1, $2
|
279
|
+
if @log.nil? or @log.id != id
|
280
|
+
@log = Log.new(id)
|
281
|
+
end
|
282
|
+
yield @log if type == 'Z'
|
283
|
+
unless @targets.include?(type)
|
284
|
+
@part = nil
|
285
|
+
next
|
286
|
+
end
|
287
|
+
@part = Part.new_subclass(type)
|
288
|
+
@log.add(@part)
|
289
|
+
else
|
290
|
+
@part.add(line) if @part
|
291
|
+
end
|
292
|
+
end
|
252
293
|
end
|
253
294
|
end
|
254
295
|
|
255
|
-
class
|
256
|
-
|
296
|
+
class JSONParser
|
297
|
+
class Log
|
298
|
+
def initialize(json)
|
299
|
+
@tran = json[:transaction] || {}
|
300
|
+
@producer = @tran[:producer] || {}
|
301
|
+
@msg = (@tran[:messages] || []).first
|
302
|
+
@detail = @msg[:details] || {}
|
303
|
+
end
|
304
|
+
|
305
|
+
def id
|
306
|
+
@tran[:id]
|
307
|
+
end
|
308
|
+
|
309
|
+
MODSEC_TIMESTAMP_FORMAT = '%a %b %d %H:%M:%S %Y'
|
310
|
+
def time
|
311
|
+
Time.strptime(@tran[:time_stamp], MODSEC_TIMESTAMP_FORMAT).to_i rescue 0
|
312
|
+
end
|
257
313
|
|
258
|
-
|
259
|
-
|
314
|
+
def to_h
|
315
|
+
{
|
316
|
+
id: id,
|
317
|
+
time: time,
|
318
|
+
time_stamp: @tran[:time_stamp],
|
319
|
+
client_ip: @tran[:client_ip],
|
320
|
+
client_port: @tran[:client_port],
|
321
|
+
host_ip: @tran[:host_ip],
|
322
|
+
host_port: @tran[:host_port],
|
323
|
+
request: @tran[:request], # Hash
|
324
|
+
response: @tran[:response], # Hash
|
325
|
+
producer: "#{@producer[:modsecurity]}; #{(@producer[:components] || []).join(', ')}",
|
326
|
+
connector: @producer[:connector],
|
327
|
+
secrules_engine: @producer[:secrules_engine],
|
328
|
+
rule_message: @msg[:message],
|
329
|
+
rule_id: @detail[:ruleId],
|
330
|
+
rule_ver: @detail[:ver],
|
331
|
+
rule_rev: @detail[:rev],
|
332
|
+
rule_tag: (@detail[:tags] || []).last,
|
333
|
+
rule_tags: (@detail[:tags] || []).join(', '),
|
334
|
+
rule_file: @detail[:file],
|
335
|
+
rule_line_number: @detail[:lineNumber],
|
336
|
+
rule_data: @detail[:data],
|
337
|
+
rule_severity: @detail[:severity],
|
338
|
+
rule_maturity: @detail[:maturity],
|
339
|
+
rule_accuracy: @detail[:accuracy],
|
340
|
+
messages: @tran[:messages], # Array of Hash
|
341
|
+
}
|
342
|
+
end
|
260
343
|
end
|
261
344
|
|
262
|
-
def
|
263
|
-
|
345
|
+
def initialize
|
346
|
+
@buf = ''
|
347
|
+
end
|
348
|
+
|
349
|
+
def parse(str, &block)
|
350
|
+
@buf += str
|
351
|
+
begin
|
352
|
+
json = JSON.parse(@buf, symbolize_names: true, create_additions: false)
|
353
|
+
yield Log.new(json)
|
354
|
+
@buf.clear
|
355
|
+
rescue
|
356
|
+
# incomplete
|
357
|
+
end
|
264
358
|
end
|
265
359
|
end
|
266
360
|
|
267
|
-
def initialize(targets
|
268
|
-
@
|
361
|
+
def initialize(format: :Native, targets: 'ABCEFHIJKZ')
|
362
|
+
@parser = create_parser(format, targets)
|
269
363
|
@records = []
|
270
364
|
end
|
271
365
|
|
272
366
|
def parse(str)
|
273
|
-
str
|
274
|
-
|
275
|
-
id, type = $1, $2
|
276
|
-
if @log.nil? or @log.id != id
|
277
|
-
@log = Log.new(id)
|
278
|
-
end
|
279
|
-
@records << @log if type == 'Z'
|
280
|
-
unless @targets.include?(type)
|
281
|
-
@part = nil
|
282
|
-
next
|
283
|
-
end
|
284
|
-
@part = Part.new_subclass(type)
|
285
|
-
@log.add(@part)
|
286
|
-
else
|
287
|
-
@part.add(line) if @part
|
288
|
-
end
|
367
|
+
@parser.parse(str) do |log|
|
368
|
+
@records << log
|
289
369
|
end
|
290
370
|
self
|
291
371
|
end
|
@@ -294,6 +374,19 @@ class ModsecurityAuditLogParser
|
|
294
374
|
def shift(*a)
|
295
375
|
@records.shift(*a)
|
296
376
|
end
|
377
|
+
|
378
|
+
private
|
379
|
+
|
380
|
+
def create_parser(format, targets)
|
381
|
+
case format.intern
|
382
|
+
when :Native
|
383
|
+
NativeParser.new(targets)
|
384
|
+
when :JSON
|
385
|
+
JSONParser.new
|
386
|
+
else
|
387
|
+
raise ArgumentError.new("unknown parser type: #{format}")
|
388
|
+
end
|
389
|
+
end
|
297
390
|
end
|
298
391
|
|
299
392
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modsecurity_audit_log_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hiroshi Nakamura
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|