rec 1.0.0
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.
- data/lib/rec/alert.rb +115 -0
- data/lib/rec/correlator.rb +121 -0
- data/lib/rec/mock-alert.rb +25 -0
- data/lib/rec/rule.rb +85 -0
- data/lib/rec/state.rb +102 -0
- data/lib/rec.rb +4 -0
- data/lib/string.rb +11 -0
- metadata +74 -0
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
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
|
+
|