Created
January 28, 2015 00:44
-
-
Save tilfin/ed8b05cd77d98ab05b9f to your computer and use it in GitHub Desktop.
Simple log parser that can send detected attacks by mail
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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