rec 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rec/alert.rb ADDED
@@ -0,0 +1,115 @@
1
+ require 'rubygems'
2
+ require 'net/smtp'
3
+ require 'xmpp4r'
4
+
5
+ module REC
6
+ module Alert
7
+
8
+ # Provides the capability to send alerts
9
+ # --mail--
10
+ # The simplest approach is to use the native +mail+ program (no credentials required)
11
+ # Alert.mail(alert)
12
+ #
13
+ # --email and jabber--
14
+ # You can also send emails and instant messages via servers, but you'll need to provide
15
+ # credentials to do that.
16
+ # - Alert.smtp_credentials(user, password, domain, server, port)
17
+ # - Alert.jabber_credentials(user, password, server)
18
+ #
19
+ # Then you can send messages:
20
+ # - Alert.email(alert)
21
+ # - Alert.jabber(alert)
22
+ # or send messages to another recipient, with another subject
23
+ # - Alert.email(alert, you@example.com, "Serious problem")
24
+ # - Alert.jabber(alert, boss@example.com)
25
+ #
26
+ # --Sleeping--
27
+ # If you want to avoid being sent instant messages during sleeping hours, you can
28
+ # specify a range of working hours during which urgent alerts may be sent by jabber
29
+ # and outside those working hours the alert will be sent by email instead
30
+ # Alert.workHours(9,18) # IMs only between 9am and 6pm
31
+ # Alert.urgent(alert) # sent as instant message if during work hours, else by email
32
+ # Alert.jabber(alert) # sent as instant message regardless of the time
33
+ # Alert.normal(alert) # sent by email
34
+
35
+
36
+ def self.default_subject(subject)
37
+ @@defaultSubject = subject
38
+ end
39
+ @@defaultSubject = "Alert"
40
+
41
+ #load("/home/rec/alert.conf") can contain something like this:
42
+ # Alert.email_credentials("rec@gmail.com", "tricky", "mydomain.com")
43
+ # Alert.jabber_credentials("rec@gmail.com", "tricky")
44
+ #so the rules script need not contain passwords.
45
+ #/home/rec/alert.conf should be readable only by the otherwise unprivileged user (sec)
46
+ # running the script
47
+
48
+ # provides the credentials needed for sending email
49
+ def self.smtp_credentials(user, password, domain, server="smtp.gmail.com", port=587)
50
+ @@emailUser = user
51
+ @@emailPassword = password
52
+ @@emailDomain = domain
53
+ @@emailServer = server
54
+ @@emailPort = port
55
+ end
56
+
57
+ # provides the credentials needed for sending instant messages
58
+ def self.jabber_credentials(user, password, server="talk.google.com")
59
+ @@jabberUser = user
60
+ @@jabberPassword = password
61
+ @@jabberServer = server
62
+ end
63
+
64
+ def self.emailTo=(address)
65
+ @@emailTo = address
66
+ end
67
+
68
+ def self.jabberTo=(address)
69
+ @@jabberTo = address
70
+ end
71
+
72
+ # Send the alert via local mailer
73
+ def self.mail(alert, recipient=@@emailTo, subject=@@defaultSubject)
74
+ `echo "#{alert}" | mail -s #{subject.gsub(/\s/,'\ ')} #{recipient} 1<&2`
75
+ end
76
+
77
+ # Send the alert via an email server
78
+ def self.email(alert, recipient=@@emailTo, subject=@@defaultSubject)
79
+ smtp = Net::SMTP.new(@@emailServer, @@emailPort)
80
+ smtp.enable_starttls()
81
+ smtp.start(@@emailDomain, @@emailUsername, @@emailPassword, :plain)
82
+ smtp.send_message(alert, @@emailUsername, recipient)
83
+ end
84
+
85
+ # Send the alert via google talk (or any other XMPP service)
86
+ def self.jabber(alert, recipient=@@jabberTo, subject=@@defaultSubject)
87
+ client = Jabber::Client::new(Jabber::JID.new(@@JabberUser))
88
+ client.connect(@@jabberServer)
89
+ client.auth(@@jabberPassword)
90
+ message = Jabber::Message::new(recipient, alert).set_type(:normal).set_id('1').set_subject(subject)
91
+ client.send(message)
92
+ end
93
+
94
+ # define the working hours during which instant messages are allowed
95
+ # Note that Alert.work_hours(7,21) means "7am-9pm" as you would think, so from 07:00 to 20:59
96
+ def self.work_hours(start, finish)
97
+ @@workHours = start..finish
98
+ end
99
+ @@workHours = 7...21
100
+
101
+ # Send an instant message during work hours, else send an email
102
+ def self.urgent(alert)
103
+ if @@workhours.include?(Time.now.hour)
104
+ self.jabber(alert)
105
+ else
106
+ self.email(alert)
107
+ end
108
+ end
109
+
110
+ def self.normal(alert)
111
+ self.email(alert)
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,121 @@
1
+ require 'rec/rule'
2
+
3
+ module REC
4
+ class Correlator
5
+
6
+ @@eventsIn = 0
7
+ @@eventsMissed = 0
8
+
9
+ def self.start(opts={})
10
+ $debug = opts[:debug] || false
11
+ self.new().start()
12
+ end
13
+
14
+ def initialize()
15
+ @time = @startupTime = Time.now()
16
+ @year = @startupTime.year
17
+ @running = false
18
+ end
19
+
20
+ def start()
21
+ Signal.trap("INT") { finish() }
22
+ Signal.trap("TERM") { finish() }
23
+ Signal.trap("USR1") {
24
+ stats()
25
+ run()
26
+ }
27
+ $stderr.puts("srec is starting...")
28
+ begin
29
+ $miss = IO.open(3, "a") # for missed events
30
+ rescue
31
+ $miss = nil
32
+ end
33
+ @running = true
34
+ run()
35
+ end
36
+
37
+ def run()
38
+ while @running and !$stdin.eof? do
39
+ logLine = gets()
40
+ next if logLine.nil?
41
+ logLine.strip!()
42
+ next if logLine.empty?
43
+ @@eventsIn += 1
44
+ @time, message = parse(logLine)
45
+ $stderr.puts("< "+message) if $debug
46
+ State.expire_states(@time) # remove expired states before we check the rules
47
+ eventMatched = false
48
+ Rule.each { |rule|
49
+ title = rule.check(message)
50
+ eventMatched = true unless title.nil? # empty match is still a match
51
+ next if title.nil?
52
+ break if title.empty? # match without a message means 'swallow this event'
53
+ state = State[title] || rule.create_state(title, @time)
54
+ rule.react(state, @time, logLine)
55
+ $stderr.puts("breaking after rule #{rule.rid}") unless (!$debug or rule.continue())
56
+ break unless rule.continue()
57
+ }
58
+ if !eventMatched
59
+ @@eventsMissed += 1
60
+ $miss.puts("* "+logLine) unless $miss.nil?
61
+ end
62
+ end
63
+ finish() if $stdin.eof?
64
+ end
65
+
66
+ def finish()
67
+ @running = false
68
+ $miss.close() unless $miss.nil? or $miss.closed?
69
+ # NOTE: some states may have something useful to say, or maybe we could store them
70
+ stats()
71
+ $stderr.puts("srec is finished.")
72
+ exit 0
73
+ end
74
+
75
+ def parse(logLine)
76
+ if logLine =~ /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d+)\s(\d\d)\:(\d\d)\:(\d\d)/
77
+ # Apr 22 16:40:18 aqua Firewall[205]: Skype is listening from 0.0.0.0:51304 proto=6
78
+ time = Time.local(@year, $1, $2, $3, $4, $5, 0)
79
+ message = $'
80
+ elsif logLine =~ /^\[\w+\]\s[Sun|Mon|Tue|Wed|Thu|Fri|Sat]\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d+)\s(\d\d)\:(\d\d)\:(\d\d)\s(\d{4})/
81
+ # [err] Fri Dec 30 23:58:56 2011 - scan error: 451 SCAN Engine error 2 ...
82
+ time = Time.local($6, $1, $2, $3, $4, $5, 0)
83
+ message = $'
84
+ elseif logLine =~ /^(\d{4})\-(\d\d)\-(\d\d)\s(\d\d)\:(\d\d)\:(\d\d)\.(\d+)\s(\w+)/
85
+ # 2012-04-22 08:43:22.099 EST - Module: PlistFile ...
86
+ if $7 == "UTC" or $7 == "GMT"
87
+ time = Time.utc($1, $2, $3, $4, $5, $6, $7)
88
+ else
89
+ time = Time.local($1, $2, $3, $4, $5, $6, $7)
90
+ end
91
+ message = $'
92
+ else
93
+ time = @time # time stands still
94
+ message = logLine
95
+ end
96
+ [time, message]
97
+ end
98
+
99
+ def stats()
100
+ # report statistics to stderr
101
+ checked, matched, created, reacted, rules = Rule.stats()
102
+ statesCount, eventsOut = State.stats()
103
+ $stderr.puts("-"*40)
104
+ $stderr.puts("srec has been running for %.1fs" % [Time.now - @startupTime])
105
+ $stderr.puts("events: in %-8d out %-8d missed %-8d" % [@@eventsIn, eventsOut, @@eventsMissed])
106
+ $stderr.puts("states: active %-8d created %-8d " % [statesCount, created.values.reduce(:+)])
107
+ $stderr.puts("rules: checked %-8d matched %-8d reacted %-8d" % [checked.values.reduce(:+),matched.values.reduce(:+), reacted.values.reduce(:+)])
108
+ $stderr.puts("Rule ID checked matched freq % reacted ")
109
+ #checked.keys.sort { |a,b| matched[b] <=> matched[a] }.each {|rid| # descending by matches
110
+ rules.collect { |rule| rule.rid }.each { |rid|
111
+ if checked[rid] > 0
112
+ freq = "%5.2f" % [matched[rid].to_f() / checked[rid].to_f() * 100]
113
+ else
114
+ freq = "never"
115
+ end
116
+ $stderr.puts("%-8d %-8d %-8d %-8s %-8d" % [rid, checked[rid], matched[rid], freq, reacted[rid]])
117
+ }
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,25 @@
1
+ module REC
2
+ # mock the Alert class for testing purposes
3
+ class Alert
4
+
5
+ @@emailsSent = []
6
+ @@jabbersSent = []
7
+
8
+ def self.email(alert, recipient=@@emailTo, subject=@@defaultSubject)
9
+ @@emailsSent << [alert, recipient, subject]
10
+ end
11
+
12
+ def self.jabber(alert, recipient=@@jabberTo, subject=@@defaultSubject)
13
+ @@jabbersSent << [alert, recipient, subject]
14
+ end
15
+
16
+ def emailsSent()
17
+ @@emailsSent
18
+ end
19
+
20
+ def jabbersSent()
21
+ @@jabberSent
22
+ end
23
+
24
+ end
25
+ end
data/lib/rec/rule.rb ADDED
@@ -0,0 +1,85 @@
1
+ require 'rec/state'
2
+
3
+ module REC
4
+ class Rule
5
+
6
+ @@rules = []
7
+ @@index = {} # index of rules to allow lookup of messages etc.
8
+ @@checked = {} # number of times each rule was checked
9
+ @@matched = {} # number of times each rule was matched
10
+ @@created = {} # number of states created by each rule
11
+ @@reacted = {} # number of times #react was called on each rule
12
+
13
+ def self.each(&block)
14
+ @@rules.each(&block)
15
+ end
16
+
17
+ def self.<<(rule)
18
+ @@rules << rule
19
+ @@index[rule.rid] = rule
20
+ @@checked[rule.rid] = 0
21
+ @@matched[rule.rid] = 0
22
+ @@created[rule.rid] = 0
23
+ @@reacted[rule.rid] = 0
24
+ end
25
+
26
+ def self.[](rid)
27
+ @@index[rid]
28
+ end
29
+
30
+ def self.stats()
31
+ [@@checked, @@matched, @@created, @@reacted, @@rules]
32
+ end
33
+
34
+ attr_reader(:rid, :pattern, :message, :lifespan, :alert, :params, :action)
35
+
36
+ def initialize(rid, params={}, &action)
37
+ @rid = rid
38
+ @pattern = params[:pattern] || raise("No pattern specified for rule #{@ruleId}")
39
+ @message = params[:message] || "" # no message means no state created - ie. ignore event
40
+ @lifespan = params[:lifespan] || 0
41
+ @alert = params[:alert] || @message
42
+ @allstates = params[:allstates] || []
43
+ @anystates = params[:anystates] || []
44
+ @notstates = params[:notstates] || []
45
+ @details = params[:details] || []
46
+ @params = params
47
+ @action = action
48
+ @matches = nil
49
+ Rule << self
50
+ end
51
+
52
+ def check(logMessage)
53
+ @@checked[@rid] += 1
54
+ matchData = @pattern.match(logMessage) || return
55
+ @matches = Hash[@details.zip(matchData.to_a()[1..-1])] # map matched values to detail keys
56
+ # if any notstates are specified, make sure none are present
57
+ @notstates.each { |str| return if State[str.sprinth(@matches)] }
58
+ # if any allstates are specified, they all need to be present
59
+ @allstates.each { |str| return unless State[str.sprinth(@matches)] }
60
+ # if anystates are specified, we must find one that matches
61
+ return unless @anystates.empty? or @anystates.detect {|str| State[str.sprinth(@matches)] }
62
+ title = @message.sprinth(@matches)
63
+ @@matched[@rid] += 1
64
+ return(title)
65
+ end
66
+
67
+ def create_state(title, time)
68
+ @@created[@rid] += 1
69
+ $stderr.puts("+ Creating new state #{title}") if $debug
70
+ State.new(title, time, @lifespan, @params)
71
+ end
72
+
73
+ def react(state, time, logLine)
74
+ @@reacted[@rid] += 1
75
+ state.update(time, @rid, @matches, @alert, logLine)
76
+ $stderr.puts("~ Rule #{@rid}, state = #{state.inspect()}") if $debug
77
+ @action.call(state) if @action
78
+ end
79
+
80
+ def continue()
81
+ @params[:continue]
82
+ end
83
+
84
+ end
85
+ end
data/lib/rec/state.rb ADDED
@@ -0,0 +1,102 @@
1
+ require 'time'
2
+
3
+ module REC
4
+ class State
5
+
6
+ @@timeouts = []
7
+ @@states = {}
8
+ @@eventsOut = 0
9
+
10
+ Struct.new("Timeout", :expiry, :key)
11
+
12
+ def self.[](key)
13
+ @@states[key]
14
+ end
15
+
16
+ def self.timeout_at(time, title)
17
+ tnew = Struct::Timeout.new(time, title)
18
+ n = @@timeouts.find_index { |to|
19
+ to.expiry > time
20
+ }
21
+ if n.nil?
22
+ @@timeouts = @@timeouts << tnew
23
+ else
24
+ @@timeouts[n..n] = [tnew, @@timeouts[n]]
25
+ end
26
+ end
27
+
28
+ def self.expire_states(time)
29
+ timeout = @@timeouts.first()
30
+ while @@timeouts.length > 0 and timeout.expiry < time do
31
+ state = State[timeout.key]
32
+ if state.nil?
33
+ @@timeouts.shift
34
+ timeout = @@timeouts.first()
35
+ next
36
+ end
37
+ #$stderr.puts("Releasing state #{state.title} with count of #{state.count}")
38
+ @@states.delete(@@timeouts.shift().key)
39
+ timeout = @@timeouts.first()
40
+ end
41
+ end
42
+
43
+ def self.stats()
44
+ statesCount = @@states.keys.length
45
+ [statesCount, @@eventsOut]
46
+ end
47
+
48
+ attr_reader(:rid, :title, :lifespan, :alert, :params, :count, :created, :updated, :dur, :details)
49
+
50
+ def initialize(title, time, lifespan, params={})
51
+ @title = title
52
+ @lifespan = lifespan.to_f
53
+ @params = params
54
+ @count = 0
55
+ @created = time
56
+ @updated = time
57
+ @dur = 0
58
+ @rid = 0
59
+ @alert = ""
60
+ @logs = [] # array of remembered logLines
61
+ @details = {} # hash of remembered details
62
+ @@states[title] = self
63
+ State.timeout_at(time + @lifespan, @title)
64
+ end
65
+
66
+ def update(time, rid, matches, alert, logLine=nil)
67
+ @count = @count.succ
68
+ @updated = time
69
+ @dur = @updated - @created
70
+ @rid = rid
71
+ @details.merge!(matches)
72
+ @alert = alert
73
+ @logs << logLine if @params[:capture]
74
+ end
75
+
76
+ def release()
77
+ @@states.delete(@title)
78
+ end
79
+
80
+ def stats()
81
+ @details.merge({"count"=>@count, "dur"=>@dur, "created"=>@created, "updated"=>@updated})
82
+ end
83
+
84
+ def generate_alert()
85
+ message = @alert.sprinth(stats())
86
+ event = "%s %s" % [@created.iso8601, message] + @logs.join("\n")
87
+ print("> ") if $debug
88
+ puts(event)
89
+ @@eventsOut = @@eventsOut + 1
90
+ event
91
+ end
92
+
93
+ def alert_first_only()
94
+ generate_alert() if @count == 1
95
+ end
96
+
97
+ def method_missing(symbol, *args)
98
+ @params[symbol]
99
+ end
100
+
101
+ end
102
+ end
data/lib/rec.rb ADDED
@@ -0,0 +1,4 @@
1
+ # Ruby Event Correlation
2
+ require 'string'
3
+ require 'rec/correlator'
4
+ require 'rec/alert'
data/lib/string.rb ADDED
@@ -0,0 +1,11 @@
1
+ class String
2
+ # interpolate hash values into a formatted string
3
+ # The string should have a form like "Stats uid %uid$-5d belongs to %userid$s"
4
+ # '%uid$-5d' is replaced by '%-5d' and '%userid$s' becomes '%s'
5
+ # and the relevant values are pulled from the hash into an array
6
+ # and interpolated using normal sprintf features
7
+ def sprinth(hash={})
8
+ raise ArgumentError.new("sprinth argument must be a Hash") unless hash.is_a?(Hash)
9
+ self.gsub(/\%\w+\$/,"%") % self.scan(/\%\w+\$/).collect { |token| hash[token[1..-2]] }
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rec
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Richard Kernahan
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-09-06 00:00:00 +10:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: "\tSifts through your log files in real time, using stateful intelligence to determine\n\
22
+ \twhat is really important. REC can alert you (by email or IM) or it can simply condense\n\
23
+ \ta large log file into a much shorter and more meaningful log.\n\
24
+ \tREC is inspired by Risto Vaarandi's brilliant *sec* (simple-evcorr.sourceforge.net)\n\
25
+ \tbut is original code and any defects are entirely mine.\n\
26
+ \tWhile event correlation is inherently complex, REC attempts to make common tasks easy\n\
27
+ \twhile preserving plenty of power and flexibility for ambitious tasks.\n"
28
+ email: rec@finalstep.com.au
29
+ executables: []
30
+
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - lib/rec.rb
37
+ - lib/rec/rule.rb
38
+ - lib/rec/state.rb
39
+ - lib/rec/correlator.rb
40
+ - lib/rec/alert.rb
41
+ - lib/rec/mock-alert.rb
42
+ - lib/string.rb
43
+ has_rdoc: true
44
+ homepage: http://rubygems.org/gems/rec
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ requirements: []
67
+
68
+ rubyforge_project: rec
69
+ rubygems_version: 1.3.6
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Ruby event correlation
73
+ test_files: []
74
+