modsecurity_audit_log_parser 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0bea7e861234b29f8067d0cea8f53d18c72cde3b
4
- data.tar.gz: 35941aff0bee85e3123b5a4b53990f08e2630f7e
3
+ metadata.gz: d0303b7ba962015523229a03ae83e32867129d06
4
+ data.tar.gz: f7bb18fa8a07687dada6ca3c9e7154c7affce545
5
5
  SHA512:
6
- metadata.gz: 2f61b8a6ad41b42e325a28c2f42a5297fea066d1b43da3096733fda1f226511e49909627242832c53bf729cc18e84839cfa3dda579ed69dbc61c46e82dfc08b2
7
- data.tar.gz: 7401e6dfccc01b8ec9c56a8b658bcc2f4f8703f77db5b31b785ec190f6c7457c3c051fd0c0eb06d586146cebacd80fb93acdc8c78fb0d97f7cc3f64ab30ac91b
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 Log
6
- attr_reader :id
7
+ class NativeParser
8
+ class Log
9
+ attr_reader :id
7
10
 
8
- def initialize(id)
9
- @id = id
10
- @parts = {}
11
- end
11
+ def initialize(id)
12
+ @id = id
13
+ @parts = {}
14
+ end
12
15
 
13
- def add(part)
14
- @parts[part.type] = part
15
- end
16
+ def add(part)
17
+ @parts[part.type] = part
18
+ end
16
19
 
17
- MODSEC_TIMESTAMP_FORMAT = '%d/%b/%Y:%H:%M:%S %z'
18
- def time
19
- if ah = audit_log_header
20
- if ts = ah.timestamp
21
- DateTime.strptime(ts, MODSEC_TIMESTAMP_FORMAT).to_time.to_i rescue 0
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
- [:timestamp, :unique_transaction_id, :source_ip_address, :source_port, :destination_ip_address, :destination_port].each do |name|
27
- define_method(name) do
28
- audit_log_header.send(name)
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
- def trailers
33
- audit_log_trailer.trailers
34
- end
35
+ def trailers
36
+ audit_log_trailer.trailers
37
+ end
35
38
 
36
- def rules
37
- audit_log_trailer.rules
38
- end
39
+ def rules
40
+ audit_log_trailer.rules
41
+ end
39
42
 
40
- def audit_log_header
41
- @parts['A'] || EMPTY_AUDIT_LOG_HEADER
42
- end
43
+ def audit_log_header
44
+ @parts['A'] || EMPTY_AUDIT_LOG_HEADER
45
+ end
43
46
 
44
- def request_headers
45
- @parts['B']
46
- end
47
+ def request_headers
48
+ @parts['B']
49
+ end
47
50
 
48
- def request_body
49
- @parts['C']
50
- end
51
+ def request_body
52
+ @parts['C']
53
+ end
51
54
 
52
- def original_response_body
53
- @parts['E']
54
- end
55
+ def original_response_body
56
+ @parts['E']
57
+ end
55
58
 
56
- def response_header
57
- @parts['F']
58
- end
59
+ def response_header
60
+ @parts['F']
61
+ end
59
62
 
60
- def audit_log_trailer
61
- @parts['H'] || EMPTY_AUDIT_LOG_TRAILER
62
- end
63
+ def audit_log_trailer
64
+ @parts['H'] || EMPTY_AUDIT_LOG_TRAILER
65
+ end
63
66
 
64
- def reduced_multipart_request_body
65
- @parts['I']
66
- end
67
+ def reduced_multipart_request_body
68
+ @parts['I']
69
+ end
67
70
 
68
- def multipart_files_information
69
- @parts['J']
70
- end
71
+ def multipart_files_information
72
+ @parts['J']
73
+ end
71
74
 
72
- def matched_rules_information
73
- @parts['K']
74
- end
75
+ def matched_rules_information
76
+ @parts['K']
77
+ end
75
78
 
76
- def to_h
77
- @parts.inject(Hash.new) { |r, (k, v)|
78
- v.merge!(r)
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
- class Part
85
- module PartClassMethods
86
- def register(type, klass)
87
- (@@registry ||= {})[type] = klass
88
- @type = type
89
- end
87
+ class Part
88
+ module PartClassMethods
89
+ def register(type, klass)
90
+ (@@registry ||= {})[type] = klass
91
+ @type = type
92
+ end
90
93
 
91
- def type
92
- @type
93
- end
94
-
95
- def self.class_for_type(type)
96
- @@registry[type]
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
- def self.inherited(klass)
101
- klass.extend(PartClassMethods)
102
- end
103
+ def self.inherited(klass)
104
+ klass.extend(PartClassMethods)
105
+ end
103
106
 
104
- def type
105
- self.class.type
106
- end
107
+ def type
108
+ self.class.type
109
+ end
107
110
 
108
- def self.new_subclass(type)
109
- PartClassMethods.class_for_type(type).new
110
- end
111
+ def self.new_subclass(type)
112
+ PartClassMethods.class_for_type(type).new
113
+ end
111
114
 
112
- def add(line)
113
- raise
114
- end
115
+ def add(line)
116
+ raise
117
+ end
115
118
 
116
- def to_hash
117
- hash = {}
118
- merge!(hash)
119
- hash
120
- end
119
+ def to_hash
120
+ hash = {}
121
+ merge!(hash)
122
+ hash
123
+ end
121
124
 
122
- def merge!(hash)
123
- raise
125
+ def merge!(hash)
126
+ raise
127
+ end
124
128
  end
125
- end
126
129
 
127
- class ContentPart < Part
128
- attr_reader :content
130
+ class ContentPart < Part
131
+ attr_reader :content
129
132
 
130
- def initialize
131
- @content = ''
132
- end
133
+ def initialize
134
+ @content = ''
135
+ end
133
136
 
134
- def add(line)
135
- @content << line
137
+ def add(line)
138
+ @content << line
139
+ end
136
140
  end
137
- end
138
141
 
139
- class AuditLogHeaderPart < Part
140
- register('A', self)
142
+ class AuditLogHeaderPart < Part
143
+ register('A', self)
141
144
 
142
- attr_reader :timestamp, :unique_transaction_id, :source_ip_address, :source_port, :destination_ip_address, :destination_port
145
+ attr_reader :timestamp, :unique_transaction_id, :source_ip_address, :source_port, :destination_ip_address, :destination_port
143
146
 
144
- def add(line)
145
- datetime, rest = line.chomp.split(/\] /, 2)
146
- @timestamp = datetime.sub(/\[/, '')
147
- @unique_transaction_id, @source_ip_address, @source_port, @destination_ip_address, @destination_port = rest.split(/ /, 5)
148
- end
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
- def merge!(hash)
151
- hash[:timestamp] = @timestamp
152
- hash[:unique_transaction_id] = @unique_transaction_id
153
- hash[:source_ip_address] = @source_ip_address
154
- hash[:source_port] = @source_port
155
- hash[:destination_ip_address] = @destination_ip_address
156
- hash[:destination_port] = @destination_port
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
- end
159
- EMPTY_AUDIT_LOG_HEADER = AuditLogHeaderPart.new
162
+ EMPTY_AUDIT_LOG_HEADER = AuditLogHeaderPart.new
160
163
 
161
- class RequestHeadersPart < ContentPart
162
- register('B', self)
164
+ class RequestHeadersPart < ContentPart
165
+ register('B', self)
163
166
 
164
- def merge!(hash)
165
- hash[:request_headers] = @content
167
+ def merge!(hash)
168
+ hash[:request_headers] = @content
169
+ end
166
170
  end
167
- end
168
171
 
169
- class RequestBodyPart < ContentPart
170
- register('C', self)
172
+ class RequestBodyPart < ContentPart
173
+ register('C', self)
171
174
 
172
- def merge!(hash)
173
- hash[:request_body] = @content
175
+ def merge!(hash)
176
+ hash[:request_body] = @content
177
+ end
174
178
  end
175
- end
176
179
 
177
- class OriginalResponseBodyPart < ContentPart
178
- register('E', self)
180
+ class OriginalResponseBodyPart < ContentPart
181
+ register('E', self)
179
182
 
180
- def merge!(hash)
181
- hash[:original_response_body] = @content
183
+ def merge!(hash)
184
+ hash[:original_response_body] = @content
185
+ end
182
186
  end
183
- end
184
187
 
185
- class ResponseHeadersPart < ContentPart
186
- register('F', self)
188
+ class ResponseHeadersPart < ContentPart
189
+ register('F', self)
187
190
 
188
- def merge!(hash)
189
- hash[:response_headers] = @content
191
+ def merge!(hash)
192
+ hash[:response_headers] = @content
193
+ end
190
194
  end
191
- end
192
195
 
193
- class AuditLogTrailerPart < Part
194
- register('H', self)
196
+ class AuditLogTrailerPart < Part
197
+ register('H', self)
195
198
 
196
- attr_reader :trailers
199
+ attr_reader :trailers
197
200
 
198
- def initialize
199
- @trailers = {}
200
- end
201
+ def initialize
202
+ @trailers = {}
203
+ end
201
204
 
202
- def add(line)
203
- key, value = line.chomp.split(/: /, 2)
204
- if key == 'Message'
205
- (@trailers[:Message] ||= '') << value << "\n"
206
- elsif key
207
- @trailers[key.intern] = value
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
- def rules
212
- if message = @trailers[:Message]
213
- if pairs = message.scan(/\[(\w+) "([^\\"]*(?:\\.[^\\"]*)*)"\]/)
214
- pairs.inject({}) { |r, (k, v)|
215
- r["rule_#{k}".intern] = v
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
- def merge!(hash)
223
- hash.merge!(@trailers)
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
- class ReducedMultipartRequestBodyPart < ContentPart
232
- register('I', self)
242
+ class MultipartFilesInformationPart < ContentPart
243
+ register('J', self)
233
244
 
234
- def merge!(hash)
235
- hash[:reduce_multipart_request_body] = @content
245
+ def merge!(hash)
246
+ hash[:multipart_files_information] = @content
247
+ end
236
248
  end
237
- end
238
249
 
239
- class MultipartFilesInformationPart < ContentPart
240
- register('J', self)
250
+ class MatchedRulesInformationPart < ContentPart
251
+ register('K', self)
241
252
 
242
- def merge!(hash)
243
- hash[:multipart_files_information] = @content
253
+ def merge!(hash)
254
+ hash[:matched_rules_information] = @content
255
+ end
244
256
  end
245
- end
246
257
 
247
- class MatchedRulesInformationPart < ContentPart
248
- register('K', self)
258
+ class AuditLogFooterPart < Part
259
+ register('Z', self)
260
+
261
+ def add(line)
262
+ # ignore
263
+ end
249
264
 
250
- def merge!(hash)
251
- hash[:matched_rules_information] = @content
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 AuditLogFooterPart < Part
256
- register('Z', self)
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
- def add(line)
259
- # ignore
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 merge!(hash)
263
- # ignore
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 = 'ABCEFHIJKZ')
268
- @targets = targets.split('')
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.each_line do |line|
274
- if /\A--([0-9a-z]+)-(.)--/ =~ line
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
 
@@ -1,3 +1,3 @@
1
1
  class ModsecurityAuditLogParser
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.3'
3
3
  end
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.2
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-05-30 00:00:00.000000000 Z
11
+ date: 2017-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler