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 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