pvl.syslog.rule: implement proper match/apply support
authorTero Marttila <terom@paivola.fi>
Thu, 10 Jan 2013 17:54:55 +0200
changeset 76 60bdff4bedfb
parent 75 2e675eda5e33
child 77 05d6cfa9efac
pvl.syslog.rule: implement proper match/apply support
bin/pvl.verkko-syslog
etc/syslog.conf
pvl/syslog/rule.py
--- a/bin/pvl.verkko-syslog	Thu Jan 10 17:52:43 2013 +0200
+++ b/bin/pvl.verkko-syslog	Thu Jan 10 17:54:55 2013 +0200
@@ -39,6 +39,9 @@
     parser.add_option_group(pvl.syslog.rule.parser(parser))
     parser.add_option_group(pvl.irker.parser(parser))
 
+    parser.add_option('--irker-target', metavar='IRC',
+            help="Irker target URL")
+
     # parse
     options, args = parser.parse_args(argv[1:])
     
@@ -63,28 +66,20 @@
     rules = pvl.syslog.rule.apply(options)
 
     log.info("Connect IRK..")
-    irk, target = pvl.irker.apply(options, target=target)
+    irk = pvl.irker.apply(options)
 
-  
     log.info("Process syslog messages...")
     for item in syslog.main() :
-        match = rules.apply(item)
-
-        if match :
-            rule = str(match.get('rule'))
+        match, rulepath, apply = rules.apply(item)
 
-            out = { rule: match.get(rule) }
-        else :
-            out = { }
-
-        log.info("%s: %s", item, out)
+        log.info("%s: %s", item, apply)
+        
+        target = apply.get('irk', options.irker_target)
+        
+        tag = '/'.join(str(rule) for rule in reversed(rulepath[:-1]))
+        text = apply.get('text')
 
-        for tag, message in out.iteritems() :
-            # TODO: map tag -> target?
-            if target :
-                target(message)
-            else :
-                print tag, message
+        print target, tag, text
     
     # done
     log.info("Exiting...")
--- a/etc/syslog.conf	Thu Jan 10 17:52:43 2013 +0200
+++ b/etc/syslog.conf	Thu Jan 10 17:54:55 2013 +0200
@@ -1,33 +1,65 @@
-[sudo]
-program     = sudo
-
-[[sudo_command]]
-pattern     = (?P<login>\S+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>.+?) ; USER=(?P<user>\S+) ; (?:ENV=(?P<env>.+?) ; )?COMMAND=(?P<command>.*)
-format      = {login}:{tty} - {user}@{host}:{pwd} - {command!r}
-
-[[[puppet_readshadow]]]
-login       = puppet
-user        = root
-command     = /usr/bin/getent shadow \w+
-
-[[sudo_unknown]]
-format      = {host} {msg}
+irk     = irc://irc-test/test
 
-[ssh]
-program     = sshd
-pattern     = Accepted password for (?P<user>\S+) from (?P<ip>\S+) port (?P<port>\S+) (?P<proto>\S+)
-format      = SSH login for {user}@{host} from {ip}
-
-[cron]
-program     = cron
+# TODO: implements meta-attrs across rule tree to classify hosts?
+#[tag]
+#    [[puppetmaster]]
+#        host    = guru
+#
+#    [[auth-high]]
+#        host    = guru
 
-[su_nobody]
-program     = su
-pattern     = Successful su for nobody by root|\+ \?\?\? root:nobody
+# auth on normal hosts
+[auth]
+    facility    = auth
 
-[puppet]
-program     = puppet
+    [[sudo]]
+    program     = sudo
+    pattern     = (?P<login>\S+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>.+?) ; USER=(?P<user>\S+) ; (?:ENV=(?P<env>.+?) ; )?COMMAND=(?P<command>.*)
+    format      = {login}:{tty} - {user}@{host}:{pwd} - {command!r}
+    
+    # ignore puppet readshadow on puppetmasters
+    [[[puppet_readshadow]]]
+    login       = puppet
+    user        = root
+    command     = /usr/bin/getent shadow \w+
+    format      = # ignore
 
-[all]
-format      = {host} {msg}
+    [[[env]]]
+    env         = .+
+    format      = {login}:{tty} - {user}@{host}:{pwd} - {env}{command!r}
 
+    [[sudo-unknown]]
+    program     = sudo
+    format      = {host} {msg}
+    
+# auth on high-sec hosts
+[auth]
+    host        = ...
+    facility    = auth
+    
+    # TODO: pubkey, failures?
+    [[ssh]]
+    program     = sshd
+    pattern     = Accepted password for (?P<user>\S+) from (?P<ip>\S+) port (?P<port>\S+) (?P<proto>\S+)
+    format      = SSH login for {user}@{host} from {ip}
+
+    [[cron]]
+    program     = cron
+    format      = # ignore
+
+    [[su_nobody]]
+    program     = su
+    pattern     = Successful su for nobody by root|\+ \?\?\? root:nobody
+    format      = # ignore
+
+    [[all]]
+    format      = {host} {msg}
+
+# user
+[user]
+    facility    = user
+
+    [[puppet]]
+    program     = puppet
+    format      = {host} {msg}
+
--- a/pvl/syslog/rule.py	Thu Jan 10 17:52:43 2013 +0200
+++ b/pvl/syslog/rule.py	Thu Jan 10 17:54:55 2013 +0200
@@ -30,6 +30,9 @@
     else :
         return SyslogRule('default', formats={ 'default': '{msg}' })
 
+def merge (*dicts, **kwargs) :
+    return dict((k, v) for d in (dicts + (kwargs, )) for k, v in d.iteritems())
+
 # TODO: combine SyslogRule/Rules into one heirarchial SyslogRule -type?
 class SyslogRule (object) :
     """
@@ -55,29 +58,34 @@
         rules = [cls.config_section(subsection, section[subsection]) for subsection in section.sections]
         attrs = dict((name, section[name]) for name in section.scalars)
          
-        return cls.config(name, rules=rules, **attrs)
+        return cls.config(name, rules, **attrs)
 
     @classmethod
-    def config (cls, name, format=None, program=None, pattern=None, rules=None, **filters) :
+    def config (cls, name, rules=None, format=None, irk=None, program=None, facility=None, pattern=None, **filters) :
         """
             Build SyslogRule from config options
         """
 
-        if pattern :
-            pattern = re.compile(pattern)
+        if format :
+            format = { 'text': format }
+        else :
+            format = { }
 
-        if format :
-            format = { name: format }
+        if irk :
+            format['irk'] = irk
 
         filters = dict(
             (attr, re.compile(regex)) for attr, regex in filters.iteritems()
         )
 
+        if facility :
+            filters['facility'] = facility # glob
+
         if program :
-            filters['prog'] = program
+            filters['prog'] = program # glob
 
         if pattern :
-            filters['msg'] = pattern
+            filters['msg'] = re.compile(pattern)
 
         filter = SyslogFilter(**filters)
         
@@ -93,7 +101,7 @@
 
     def match (self, item) :
         """
-            Match item against our filter, applying any matches.
+            Match item against our filter, returning match-dict (empty?) or None.
         """
         
         if self.filter :
@@ -101,75 +109,75 @@
             matches = self.filter.filter(item)
 
         else :
-            # match all
-            matches = True
+            # match all, we probably have sub-rules that we're interested in
+            return { }
 
         log.debug("%s: %s", self, matches)
 
-        if not matches :
-            # ignore
+        if matches :
+            return matches
+        else :
+            # no match
             return None
 
-        # apply
-        item['rule'] = self
-
-        if matches is True :
-            # no-op filter
-            pass
-        else :
-            item.update(matches)
-        
-        # XXX: copy, not mutate?
-        return item
-
     def format (self, item) :
         """
-            Apply output formats
+            Apply our output formats to given base apply, yielding (unique) attr, value tuples.
         """
-
-        out = {}
         
         for attr, format in self.formats.iteritems() :
-            value = out[attr] = format.format(**item)
+            value = format.format(**item)
+
             log.debug("%s: %s: %s", self, attr, value)
-        
-        return out
 
-    # TODO: __call__?
+            yield attr, value
+
     def apply (self, item) :
         """
-            Match item against ourself, apply against any sub-rules, and return output, if hit.
+            Recursively match item against ourself and sub-rules. Returns applied output.
+
+            Matches are passed down the tree, and applies are passed up.
         """
 
-        item = self.match(item)
+        log.debug("%s", self)
 
-        if not item :
+        # match rule -> matches
+        matches = self.match(item)
+
+        if matches is None :
             # skip
-            return
+            return None, None, None
+        
+        # merge matches down
+        item = merge(item, matches)
 
-        # sub-rules
+        # recursive sub-rules -> apply
         for rule in self.rules :
             try :
-                out = rule.apply(item)
+                # pass matches down
+                match, rules, apply = rule.apply(item)
 
             except Exception as ex :
-                log.exception("rule %s: %r", rule, item)
-                return # XXX: skip?
+                log.exception("%s -> %s: %r", self, rule, item)
+                continue # XXX: skip?
 
-            if out :
-                # hit
-                return out
-            
-        # TODO: routing/formatting
-        #if self.tag is False :
-        #    # drop
-        #    return False
-    
+            if apply :
+                # pass apply up
+                break
+        else :
+            # self-match
+            match, rules, apply = item, [], { }
+        
+        rules.append(self)
+
+        # formats?
         if self.formats :
-            # hit
-            item.update(self.format(item))
+            # merge apply up
+            apply = merge(dict(self.format(item)), apply)
         
-        return item
+        log.debug("%s: %s", '/'.join(str(rule) for rule in rules), apply)
+        
+        return match, rules, apply
 
     def __iter__ (self, items) :
         """
@@ -177,10 +185,10 @@
         """
 
         for item in items :
-            match = self.apply(item)
+            match, rules, apply = self.apply(item)
 
-            if match :
-                yield match
+            if apply :
+                yield apply
  
     def __str__ (self) :
         return self.name