Skip to content

Instantly share code, notes, and snippets.

@tilfin
Created January 28, 2015 00:44
Show Gist options
  • Save tilfin/ed8b05cd77d98ab05b9f to your computer and use it in GitHub Desktop.
Save tilfin/ed8b05cd77d98ab05b9f to your computer and use it in GitHub Desktop.
Simple log parser that can send detected attacks by mail
#!/usr/bin/env ruby
require 'date'
require 'net/smtp'
$ACCESS_WRONG_STATUS_MIN = 400
$ACCESS_WRONG_STATUS_MAX = 500
$ACCESS_TIMERANGE_SEC = 3
$ACCESS_COUNT_THRESHOLDS = 2
$SMTP_HOST = ''
$MAIL_FROM = ''
$MAIL_TO = ''
class LogParser
def initialize
@re_string = Regexp.new("\s*\"([^\"]+)\"\s?(.*)")
@re_number = Regexp.new("\s*([0-9]+)\s?(.*)")
@re_datetime = Regexp.new("^([0-9]+)\/([^/]+)\/([0-9]+):([0-9]+):([0-9]+):([0-9]+)\s+(.*)")
@month_names = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
end
def extract_string(str)
m = @re_string.match(str)
return m[1], m[2]
end
def extract_number(str)
m = @re_number.match(str)
return m[1], m[2]
end
def extract_datetime(str)
m = @re_datetime.match(str)
mon = m[2].to_i
if mon == 0
mon = @month_names.index(m[2]) / 4 + 1
end
DateTime.new(m[3].to_i, mon, m[1].to_i, m[4].to_i, m[5].to_i, m[6].to_i, m[7])
end
def parse(line)
host, rfc931, authuser, parts = line.split(' ', 4)
p = parts.index(']')
datestr = parts[1..p-1]
parts = parts[p+1..-1]
request, parts = extract_string(parts)
status, parts = extract_number(parts)
byteslen, parts = extract_number(parts)
referer, parts = extract_string(parts)
user_agent, parts = extract_string(parts)
method, path, protocol = request.split(' ', 3)
dt = extract_datetime(datestr)
{ :host => host,
:date => dt,
:method => method,
:path => path,
:protocol => protocol,
:status => status.to_i,
:bytes => byteslen,
:referer => referer,
:user_agent => user_agent }
end
end
class Analyzer
def initialize(attack_found_proc)
@hostmap = Hash.new
@secrange = $ACCESS_TIMERANGE_SEC.to_f / 86400
@proc = attack_found_proc
end
def add_access_item(ai)
if ai.status < $ACCESS_WRONG_STATUS_MIN or ai.status > $ACCESS_WRONG_STATUS_MAX
return
end
if @hostmap.include?(ai.host)
aitems = @hostmap[ai.host]
aitems.push(ai)
if aitems.size < $ACCESS_COUNT_THRESHOLDS
return
end
if check_access_items!(aitems)
@proc.call(aitems)
aitems.clear()
end
else
@hostmap.store(ai.host, [ ai ])
end
end
def check_access_items!(aitems)
start_date = aitems[0].date
end_date = aitems[-1].date
if (end_date - start_date) <= @secrange
# Attack Found
true
else
# discard the oldest item.
aitems.shift
false
end
end
end
class AccessItem
attr_reader :host, :item, :date, :status, :line
def initialize(item, line)
@host = item[:host]
@date = item[:date]
@status = item[:status]
@line = line
end
end
file = ARGV[0]
unless File.exists?(file)
puts "no input file"
exit 1
end
attack_callback = Proc.new do |aitems|
# send mail.
mailstr = "================ Attack Log ===================\n"
lines = aitems.map { |ai| ai.line }
mailstr += lines.join("\n")
if $SMTP_HOST.empty?
puts mailstr
else
Net::SMTP.start($SMTP_HOST) do |smtp|
smtp.send_message(mailstr, $MAIL_FROM, $MAIL_TO)
end
end
end
analyzer = Analyzer.new(attack_callback)
parser = LogParser.new
File.foreach(file) do |line|
item = parser.parse(line)
analyzer.add_access_item( AccessItem.new(item, line) )
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment