tenshi-0.13/0000755000175000017500000000000011607400554011760 5ustar lcarsuserstenshi-0.13/FAQ0000644000175000017500000000745611607400554012326 0ustar lcarsuserstenshi -- Frequently Asked Questions http://www.inversepath.com/tenshi_faq.html - Why don't you use File::Tail ? File::Tail has serious performance and synchronization issues, if you use it for high performance log analysis things will go terribly wrong. Trust us on that. - How's tenshi different from other tools like swatch, logwatch, logsentry, sec, etc, etc... ? Quoting http://marc.info/?l=loganalysis&m=113657727325755&w=2 There are no definitive points against one or other apps for most people, sometimes you just have to "feel" what works for you. Anyway the biggest difference from logcheck/logsentry (besides tenshi being actively maintained) is flexibility, tenshi is much more powerful in how the reports should be assigned/constructed, and the whole concept of queues and timing of the different queue checks that logsentry lacks. We also like to think that tenshi is cleaner implementation/documentation and packaging wise ;). Tenshi also runs as a daemon btw and it's not driven by crontab. Also we are really tired of pointing out that we are not like swatch! Since tenshi came out everyone was asking why are we doing this since swatch is out there. Again swatch has no concept of queues and we don't provide an exec target at the moment along with no interactive things like the "beep" action. Tenshi's main point is summarization (along with instant notifications) and that's something swatch can't do (like logsentry). We don't have throttling btw (while swatch has something for that), but it wasn't an immediate need since summarization is what we rely on. I think that with thresholding you can have swatch doing something similar to tenshi but still tenshi should provide a better/easier implementation for these kind of things. Swatch seems over-complicated to us. Logwatch values its default set of rules, we don't have such thing and we ask users to understand and feed their own rules, and let's say that we find logwatch messy and overly complicated for what it has to do. Our main objective was providing something powerful and actually useful without overcomplicating configuration and the code, performance was also a main issue for us along with a clean distribution/packaging. - How do I use the CSV output feature? This is an example for graphing tenshi output using AfterGlow (http://afterglow.sf.net) and GraphViz (http://www.graphviz.org): Use something like this in your tenshi configuration: set csv [0 * * * *] /usr/local/bin/tenshi_graph.sh Where tenshi_graph.sh could be: #!/bin/sh /usr/local/bin/afterglow.pl -c /etc/afterglow.conf -t | neato -v -Tpng -o /var/lib/tenshi/tenshi_graph.png and afterglow.conf configuration could be something like: color.source="green"; color.target="red" if ($fields[2] > 1000); color.target="orange" if ($fields[2] > 500); color.target="blue" if ($fields[2] > 100); color.target="lightblue" if ($fields[2] > 50); color.target="yellow" if ($fields[2] == 1); color.target="white"; This allows having target node colours depending on the number of hits of the affected log, but of course it might be whatever conditions you want. The configuration results in the example screenshot available at http://dev.inversepath.com/tenshi/tenshi_afterglow.gif . You can see how it's possible to quickly evaluate logs that are common to different servers and their frequency. Keep in mind that in order to have useful and readable graphs your tenshi configuration must be accordingly tuned. Arbitrary logs in the csv queue would quickly generate huge and unreadable node maps. This is just an example, more advanced processing can be done. Please share new and useful ways for visualizing logs with mailing list and/or the SecViz (http://secviz.org) portal. tenshi-0.13/tenshi.debian-init0000644000175000017500000000140511607400554015357 0ustar lcarsusers#!/bin/bash test -f /usr/sbin/tenshi || exit 0 case "$1" in start) echo -n "Starting log monitor: tenshi" start-stop-daemon --start --exec /usr/sbin/tenshi echo "." ;; stop) echo -n "Stopping log monitor: tenshi" start-stop-daemon --stop --quiet --pidfile /var/run/tenshi/tenshi.pid echo "." ;; reload) echo -n "Reloading log monitor: tenshi" kill -HUP `cat /var/lib/tenshi/tenshi.pid` &>/dev/null echo "." ;; restart) echo -n "Stopping log monitor: tenshi" start-stop-daemon --stop --quiet --pidfile /var/run/tenshi/tenshi.pid echo "." sleep 1 echo -n "Starting log monitor: tenshi" start-stop-daemon --start --quiet --exec /usr/sbin/tenshi echo "." ;; *) echo "Usage: /etc/init.d/tenshi {start|stop|restart}" >&2 exit 1 ;; esac exit 0 tenshi-0.13/Makefile0000644000175000017500000000211411607400554013416 0ustar lcarsusers# Noddy Makefile for dist # $Id$ VERSION = 0.13 bindir = /usr/sbin sysconfdir = /etc docdir = /usr/share/doc/tenshi-${VERSION} mandir = /usr/share/man libdir = /var/lib/tenshi DOCS = README INSTALL CREDITS LICENSE Changelog FAQ SAMPLES = tenshi.conf tenshi.debian-init tenshi.gentoo-init tenshi.solaris-init tenshi.redhat-init tenshi.suse-init tenshi.redhat-spec tenshi.suse-spec BIN = Makefile tenshi MAN = tenshi.8 DIST_DIR = tenshi-${VERSION} TARBALL = tenshi-${VERSION}.tar.gz FILES = ${DOCS} ${SAMPLES} ${BIN} ${MAN} all: ${FILES} clean: rm -rf tenshi-*.tar.gz dist: ${TARBALL} ${TARBALL}: mkdir -p ${DIST_DIR} cp ${FILES} ${DIST_DIR} tar czvf tenshi-${VERSION}.tar.gz ${DIST_DIR} rm -rf ${DIST_DIR} install: install -D tenshi ${DESTDIR}${bindir}/tenshi [ -f ${DESTDIR}${sysconfdir}/tenshi/tenshi.conf ] || \ install -g root -m 0644 -D tenshi.conf ${DESTDIR}${sysconfdir}/tenshi/tenshi.conf install -d ${DESTDIR}${docdir} install -m 0644 ${DOCS} ${DESTDIR}${docdir}/ install -g root -m 0644 tenshi.8 ${DESTDIR}${mandir}/man8/ install -g root -m 755 -d ${DESTDIR}${libdir} tenshi-0.13/LICENSE0000644000175000017500000000140011607400554012760 0ustar lcarsuserstenshi 0.13 Copyright 2004-2011 Andrea Barisani Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. tenshi-0.13/README0000644000175000017500000000606011607400554012642 0ustar lcarsusers tenshi 0.13 README Copyright 2004-2011 Andrea Barisani - What's tenshi? tenshi is a log monitoring program, designed to watch one or more log files for lines matching user defined regular expressions and report on the matches. The regular expressions are assigned to queues which have an alert interval and a list of mail recipients. Please read the example tenshi.conf and tenshi.8 man page for usage instructions. tenshi was formerly known as wasabi. The name was changed to tenshi after we were informed that wasabi is a registered trademark relating to another piece of software. It should be noted that tenshi was initially a perl rewrite of Oak (http://www.ktools.org). - Examples: Consider the following settings in tenshi.conf: set hidepid on set queue mail tenshi@localhost sysadmin@localhost [0 */12 * * *] set queue misc tenshi@localhost sysadmin@localhost [0 */24 * * *] set queue critical tenshi@localhost sysadmin@localhost [now] group ^ipop3d: mail ^ipop3d: Login user=(.+) mail ^ipop3d: Logout user=(.+) mail ^ipop3d: pop3s SSL service init from (.+) mail ^ipop3d: pop3 service init from (.+) mail ^ipop3d: Command stream end of file, while reading.+ mail ^ipop3d: Command stream end of file while reading.+ critical ^ipop3d: Login failed.+ trash ^ipop3d:.+ group_end critical ^sudo: (.+) : TTY=(.+) ; PWD=(.+) ; USER=root ; COMMAND=(.+) misc .* Every ipop3d message not matched by the regexps assigned to the queue mail or critical will be matched by the queue trash (a builtin null queue), any other message will be matched by queue misc. Fields enclosed in (.+) are masked. This is a sample report for the mail queue (sent every 12 hours): host1: 79: ipop3d: Login user=___ 74: ipop3d: Logout user=___ host2: 30: ipop3d: Login user=___ 30: ipop3d: Logout user=___ 19: ipop3d: pop3 service init from ___ 12: ipop3d: pop3s SSL service init from ___ 1: ipop3d: Command stream end of file while reading line user=??? host=bogus.domain.net [192.168.0.1] 1: ipop3d: Command stream end of file, while reading authentication host=bogus1.domain.net [10.1.7.1] These are sample reports for the critical queue (sent every time a message matches the regexp): host1: 1: /usr/bin/sudo: ___ : TTY=___ ; PWD=___ ; USER=root ; COMMAND=/bin/dmesg host1: 1: /usr/bin/sudo: ___ : TTY=___ ; PWD=___ ; USER=root ; COMMAND=/bin/bash host2: 1: ipop3d: Login failed user=admin auth=admin host=bogus1.domain.net [10.1.7.1] host2: 1: ipop3d: Autologout user=??? host=bogus.domain.net [192.168.0.1] - Requirements: This is a perl program that uses 'tail' (when not using a FIFO) which should be included in your OS. It also requires Net::SMTP module for mailing reports, which should be included in your perl installation, and IO::BufferedSelect. If you miss any of them you can grab them at CPAN (http://www.cpan.org) or using the CPAN shell (perl -e shell -MCPAN). - Resources: The tenshi project page is http://www.inversepath.com/tenshi.html Please report any bugs you find at . tenshi-0.13/tenshi.conf0000644000175000017500000001335011607400554014123 0ustar lcarsusers## ## tenshi 0.13 sample conf ## # general settings set uid tenshi set gid tenshi set pidfile /var/run/tenshi.pid set logfile /var/log/messages set logfile /var/log/mail.log # set fifo /var/log/tenshi.fifo # set listen 127.0.0.1:514 ## GNU coreutils # set tail /usr/bin/tail -q --follow=name --retry -n 0 ## FreeBSD / NetBSD # set tail /usr/bin/tail -F -n 0 ## OpenBSD / HP-UX # set tail /usr/bin/tail -f -n 0 set tail_multiple off set sleep 5 set limit 800 set pager_limit 2 set mask ___ set mailserver localhost set subject tenshi report set hidepid on ## queues # syntax: set queue [pager:] [] set queue mail tenshi@localhost sysadmin@localhost [30 18 * * *] set queue nf tenshi@localhost sysadmin@localhost [*/30 * * * *] set queue report tenshi@localhost sysadmin@localhost [0 9-17/2 * * *] set queue misc tenshi@localhost sysadmin@localhost [0 9-17/2 * * *] set queue critical tenshi@localhost sysadmin@localhost,noc@localhost [now] tenshi CRITICAL report set queue root tenshi@localhost sysadmin@localhost [now] set queue pager tenshi@localhost pager:pager@localhost [now] tenshi alert set queue mobile tenshi@localhost pager:93384@localhost,pager:235953@localhost [now] tenshi alert set queue noprefix tenshi@localhost sysadmin@localhost [now] tenshi unprefixed alert ## sample filter # set filter report /usr/bin/gpg --clearsign --batch -a -r sysadmin@localhost ## sample csv pipe # set csv [0 * * * *] /usr/local/bin/afterglow.pl -c /etc/afterglow.conf -t > /var/lib/tenshi/tenshi.dot ## regexp definitions # syntax: [,..] ## note: If you are not using the hidepid option for some reason, the regexps ## below will need to be slightly different, for example: # # mail ^sendmail: (.+): to=(.+),(.+)relay=(.+),(.+)stat=Sent(.+) # would need to be: # mail ^sendmail\[(.*)\]: to=(.+),(.+)relay=(.+),(.+)stat=Sent(.+) # in order to match the sendmail line and mask the PID. repeat ^(?:last message repeated|above message repeats) (\\d+) time trash ^hub.c trash ^usb.c trash ^uhci.c trash ^sda trash ^Initializing USB trash ^scsi0 : SCSI emulation trash ^Vendor: trash ^Type: trash ^Attached scsi removable trash ^SCSI device sda trash ^sda: Write trash ^/dev/scsi trash ^WARNING: USB trash ^USB Mass Storage trash ^/dev trash ^ISO trash ^floppy0 trash ^end_request trash ^Directory trash ^I/O error: dev 08:(.+), sector nf ^netfilter group ^sendmail: mail ^sendmail: (.+): to=(.+),(.+)relay=(.+),(.+)stat=Sent(.+) mail ^sendmail: (.+): to=(.+),(.+)relay=(.+),(.+)stat=Sent mail ^sendmail: (.+): from=(.+),(.+)relay=(.+) mail ^sendmail: STARTTLS=client(.+) mail ^sendmail group_end group ^sm-mta: mail ^sm-mta: (.+): to=(.+),(.+)delay=(.+) mail ^sm-mta: (.+): to=(.+),(.+)relay=(.+),(.+)stat=Sent(.+) mail ^sm-mta: (.+): to=(.+),(.+)relay=(.+),(.+)stat=Sent mail ^sm-mta: (.+): to=(.+),(.+)relay=local(.+)stat=Sent(.+) mail ^sm-mta: (.+): to=(.+),(.+)relay=local(.+)stat=Sent mail ^sm-mta: (.+): to=(.+),(.+)stat=Sent(.+) mail ^sm-mta: (.+): to=(.+),(.+)stat=Sent mail ^sm-mta: (.+): from=(.+),(.+)relay=local(.+) mail ^sm-mta: (.+): from=(.+),(.+)relay=(.+) mail ^sm-mta: STARTTLS=server(.+) mail ^sm-mta: STARTTLS=client(.+) trash ^sm-mta:.+User unknown mail ^sm-mta: ETRN mail ^sm-mta group_end group ^ipop3d: mail ^ipop3d: Login user=(.+) mail ^ipop3d: Logout user=(.+) mail ^ipop3d: pop3s SSL service init from (.+) mail ^ipop3d: pop3 service init from (.+) mail ^ipop3d: Auth user=(.+) mail ^ipop3d: Command stream end of file, while reading mail ^ipop3d: Command stream end of file while reading mail ^ipop3d: AUTHENTICATE LOGIN failure host=(.+) mail ^ipop3d: AUTHENTICATE PLAIN failure host=(.+) mail ^ipop3d: Login failed mail,critical ^ipop3d: group_end group ^imapd: mail ^imapd: Login user=(.+) mail ^imapd: Logout user=(.+) mail ^imapd: port (.+) service init from (.+) mail ^imapd: imaps SSL service init from (.+) mail ^imapd: Command stream end of file, while reading mail ^imapd: Command stream end of file while reading mail ^imapd: Authenticated user=(.+) mail ^imapd: AUTHENTICATE LOGIN failure host=(.+) mail ^imapd: AUTHENTICATE PLAIN failure host=(.+) mail ^imapd: Autologout(.+) mail ^imapd: Login failed mail,critical ^imapd: group_end group ^sshd(?:\(pam_unix\))?: report ^sshd: fatal: Timeout before authentication for (.+) critical ^sshd: Illegal user report ^sshd: Connection from (.+) report ^sshd: Connection closed (.+) report ^sshd: Closing connection (.+) report ^sshd: Found matching (.+) key: (.+) report ^sshd: Accepted publickey (.+) report ^sshd: Accepted rsa for (.+) from (.+) port (.+) report ^sshd: Accepted keyboard-interactive/pam for (.+) from (.+) port (.+) root ^sshd\(pam_unix\): session opened for user root by root\(uid=0\) root ^sshd\(pam_unix\): session opened for user root by \(uid=0\) report ^sshd\(pam_unix\): session closed for user (.+) report ^sshd\(pam_unix\): session opened for user (.+) report ^sshd\(pam_unix\): authentication failure; logname= group_end group ^login\(pam_unix\): critical ^login\(pam_unix\): session opened for user root by root\(uid=0\) critical ^login\(pam_unix\): session opened for user root by \(uid=0\) report ^login\(pam_unix\): session closed for user (.+) report ^login\(pam_unix\): session opened for user (.+) group_end report ^passwd\(pam_unix\): group ^su\(pam_unix\): root,report ^su\(pam_unix\): session opened for user root root,report ^su\(pam_unix\): session closed for user root(.+) report ^su\(pam_unix\): session opened for user (.+) report ^su\(pam_unix\): session closed for user (.+) group_end critical ^(?:/usr/bin)?sudo: critical,pager ^Oops critical,pager ^Linux critical ^init misc .* tenshi-0.13/tenshi.80000644000175000017500000004510411607400554013347 0ustar lcarsusers.\" SH section heading .\" SS subsection heading .\" LP paragraph .\" IP indented paragraph .\" TP hanging label .TH "tenshi" 8 "13 Jul 2011" "version 0.13" .SH NAME tenshi - Log Monitoring and Reporting tool .SH SYNOPSIS .B tenshi .B [ -c .I .B ] .B [ -C ] .B [ -d ] .B [ -f ] .B [ -h ] .B [ -p ] .B [ -P ] .SH DESCRIPTION .LP tenshi is a log monitoring program, designed to watch one or more log files for lines matching user defined regular expressions and report on the matches. The regular expressions are assigned to queues which have an alert interval and a list of mail recipients. Queues can be set to send a notification as soon as there is a log line assigned to it, or to send periodic reports. Additionally, uninteresting fields in the log lines (such as PID numbers) can be masked with the standard regular expression grouping operators ( ). This allows cleaner and more readable reports. All reports are separated by hostname and all messages are condensed when possible. The program reads a configuration file .RI ( tenshi.conf ) and then forks a deamon for monitoring the specified log files. .SH OPTIONS .SS .TP .I -c Read configuration from file. The default file is /etc/tenshi/tenshi.conf . .TP .I -C Perform a syntax check of the configuration file. The program exits after parsing the configuration with either a return code of 0 or an error. .TP .I -d Enable debug messages. Default level is 1 if none is specified, level 2 enables SMTP connection debug messages. In this mode the main process remains in the foreground. .TP .I -f Enable foreground mode. In this mode the main process operates normally but remains in the foreground, this is needed for some process supervisors. .TP .I -p Enable profiling mode. In this mode the main process remains in the foreground and expects log lines to be fed to standard in. When it receives an EOF it will stop processing. No alerts will be sent in this mode, it is used solely for measuring tenshi's line processing speed. For example: time $(cat /var/log/messages|tenshi -p > /dev/null) .TP .I -P Define the file containing the PID of the process, this overrides any 'pidfile' option present in the configuration file. .SH CONFIGURATION FILE .br All directives are shown with the standard default value where applicable, if omitted the default value will be used. .I EXTERNAL CONFIGURATION FILES .br All configuration directives can be optionally split into different configuration files and then read with the two following statements. .TP .I include Parse the specified configuration file. .TP .I includedir Parse all files inside . The files will be parsed in alphabetical order, keep in mind that regexps order matters so .I includedir should be used carefully, see .I REGEXP DEFINITIONS for details. .LP .I STATIC OPTIONS .br These options will be read the first time tenshi reads its config file. They cannot be changed by re-reading the config file. If you change one of these options and HUP tenshi it will die. You have been warned. .TP .I set uid tenshi Specify the effective user ID of the process when in daemon mode. The user must be able to read the selected log files, the configuration file and write the specified pid file. Never use privileged users here since it's not usually necessary (log files perms can be set accordingly with most syslog implementations). .TP .I set gid tenshi Specify the effective group ID of the process when in daemon mode. .TP .I set pidfile /var/run/tenshi.pid The file containing the PID of the process, useful for start/stop scripts. .TP .I set logfile A log file to monitor, this may be specified multiple times to watch more than one log file. Depending on your tail implementation you might need to use the .I tail_multiple setting for multiple files to work. This mode can be used along with .I fifo and .I listen settings. .TP .I set tail /usr/bin/tail -q --follow=name --retry -n 0 Specify the path and arguments for the tail binary used for reading the log files. The invocation must be tuned against your current 'tail' implementation. Default values are configured for standard GNU coreutils tail. The .I --follow=name and .I --retry flags should deal properly with log rotation, if missing on your implementation we suggest that you use something like 'cp /dev/null logfile' as a safe way for clearing the log file upon rotation. .TP .I set tail_multiple Some tail implementations do not handle more than one log file. When this option is enabled multiple tail commands will be forked, instead of a single command with multiple arguments. This option is disabled by default. .TP .I set fifo A FIFO file to monitor. This option allows you to use a syslog-ng .I pipe() destination (or any other syslog implementation that allows FIFO usage). This may be specified multiple times to watch more than one fifo file. This option is meant to be used only when the installed 'tail' binary doesn't behave properly with FIFOs, please test your tail implementation before using this. This mode can be used along with .I logfile and .I listen settings. .TP .I set listen 0.0.0.0:514 Enables syslog server mode. With this option tenshi will bind to the specified address:port pair and read messages acting like a syslog server. We always recommend to filter the port accordingly and possibly use something like stunnel for encrypting the traffic. This mode can be used along with .I logfile and .I fifo settings. .LP .I DYNAMIC OPTIONS .br These options are set each time the config file is read. tenshi reads its config file once on start-up and whenever it receives a HUP. .TP .I set sleep 5 The loop sleep time for the notification process. The value must be \<\= 60 seconds. .TP .I set limit The maximum number of messages per hostname allowed in a report, any lines after the maximum will be omitted and a warning included. If this option is omitted then no limit is applied. .TP .I set pager_limit The maximum number of messages per hostname allowed in pager friendly reports, any lines after the maximum will be omitted. If this option is omitted then no limit is applied. .TP .I set logprefix All valid syslog messages are parsed by default, while non-syslog ones are discarded unless the special .I noprefix queue is set. This option allows to define an additional valid prefix for watching other type of logs. If the regexp is matched then the prefix is removed from the log and the first grouped string is used for the hostname field. This may be specified multiple times to watch many different non-syslog logs. .TP .I set mask ______ The mask for strings enclosed by the grouping operators ( ). See the .I REGEXP DEFINITIONS section. 'set mask' on its own will set the mask to an empty string. .TP .I set mailserver localhost The mail server to be contacted for sending out reports. .TP .I set mailtimeout 10 The timeout in seconds for mail server reply. .TP .I set subject tenshi report The subject of report emails, the queue name is always automatically appended. .TP .I set hidepid This option turns on automatic stripping of 'foo[1234]:' style PID strings from the start of log lines i.e. 'foo[1234]:' becomes 'foo:'. This allows you to write regexps without worrying about masking the PID. Bear in mind that any time you change this option you will need to re-write your regex rules or they will not work. This option is disabled by default. .TP .I set filter When this option is enabled all reports matching the specified queue will be passed as STDIN to the specified filter, the resulting output is sent via smtp instead of the original report. The full path of the filter application must be specified. .TP .I set csv This feature allows periodic reporting, using a five-field cron-style specification like the .I set queue option, to the specified filter. The output is pre-formatted as CSV (Comma Separated Values) with hostname,log,hits format. This feature was coded for using .BI AfterGlow .BI (http://afterglow.sf.net) as a filter and graphing tenshi output. Check the FAQ for sample usage. .TP .I set sort_order The sorting order for reports. It can be either descending or ascending, the number of messages is used as a key for sorting the log messages. The default order is ascending. .TP .I set resolve This option turns on resolution of the fully qualified domain name for the hostname passed along with log messages and, if found, reports it along with the original one. This only affects reports and not pager messages. The name resolution is cached in order to avoid re-resolving addresses that have been seen already, you have to restart or HUP tenshi in order to flush the cache. This option is disabled by default. .TP .I set threshold This option can be used to discard lines from a report that have a count below the given threshold. If a line matches the regex in the given queue but has fewer hits than count, it is discarded and omitted from the report. Note that this matches on the content of the lines that will actually appear in the report, in contrast to queue escalation which uses a count based on the regex that is matched. .LP .I QUEUES OPTIONS .br .br All messages are assigned to queues. Every queue is processed periodically according to its notification interval. There are four default builtin queues, .I trash to which unwanted messages can be assigned (think /dev/null), .I repeat which is used for smart repeat messages handling, .I group and .I group_host , see .I REGEXP DEFINITIONS for details. There's also a special .I noprefix queue, read further for details about it. .br All queues are automatically flushed before shutdown when a SIGTERM is received. Please see section .I SIGNALS for additional information. The syntax is the following: .TP .I set queue [pager:] [] .TP .I The queue name. Can be any alphanumeric character string except for the builtin queues name. .TP .I The mail sender for reports related to the queue. .TP .I The mail recipient(s) for reports related to the queue. Multiple address can be specified, separated by commas. Using the .I pager: prefix enables a pager friendly report. .TP .I [] This is a five-field cron-style specification for when the reports should be emailed. Ranges and skip values are supported as per the de facto crontab syntax with a few exceptions. Please see .I crontab man page for crontab syntax explanation. The supported day names are: Mon, Tue, Wed, Thu, Fri, Sat, Sun. Monday is 1, Sunday 0 or 7. Supported month names are: Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec. Day and Month names are not case sensitive. Additionally, 'now' can be specified for immediate notifications. .TP .I This is the subject for to use for email reports regarding this queue. If this isn't specified then the default subject will be used. .LP The special .I noprefix queue can be used and defined like any other queue with the difference that it will get all messages that don't match any configured prefix. Examples: .br set queue report tenshi@localhost sysadmin@localhost [0 9-17 * * *] .br set queue report tenshi@localhost sysadmin@localhost [30 18 * * *] .br set queue report tenshi@localhost sysadmin@localhost [*/10 * * * *] .br set queue critical tenshi@localhost sysadmin@localhost,noc@localhost [now] CRITICAL WARNING - .br set queue pager tenshi@localhost pager:sysadmin_pager@localhost,pager:noc_pager@localhost [now] ALERT .LP .I REGEXP DEFINITIONS .br .br All valid syslog messages are matched against standard perl regexps, all regexps are defined with the following syntax: .TP .I [,[:]..] .LP The regexps are evaluated in order so a matched message is not checked against the subsequent regexps. Keep this in mind when assembling the configuration file. It's advisable to catch all messages by placing an all matching regexp at the end of the configuration file. It's also good for performance having trash rules not logically connected with other matching rules at the beginning of the section. Multiple queues can be defined with a comma separated list, builtin queues cannot be used when using this syntax. .br If an escalation number is provided for a queue, the matched message will only be placed into the queue when messages have matched the regexp. The queue will receive the message that matched the regexp at the time of escalation, with a count equal to the escalation number. The count of messages matching the regexp will be reset when the left most queue mentioned in the queue list is mailed. The left most queue cannot have an escalation number unless it is the only queue listed. When the number of messages that match the regexp reaches the greatest escalation number mentioned, escalation will begin again into the escalation queues, modulus the greatest escalation number. For example, using the queues `a,b:10,c:50', when 10 messages match the regexp, a message will go into b, when 50 match, one will go into c. At 60, another will go into b, and at 100, another into c, 110 to b, 150 to c, and so on. Escalation numbers must be positive integers greater than zero and must be listed in increasing order from left to right. All queues without escalation numbers must be listed more left than the queues with escalation numbers. .br The standard grouping operators .I ( ) can be used for string masking, literal "(" and ")" can be protected with the standard quotation operator "\\". There's a lot of documentation about regular expressions, a good start could be perl .I perlre and .I perlretut manual pages. .br You can also use the (?: ) operators to use groups without masking. This allows you to match, for example, output from several programs in a similar format. There is an example of this below (the sudo/su line). .br The builtin queue .I repeat can be used for special handling of "last message repeated x times" style log lines. When the assigned regexps are matched the line count for the last line received from the same host is incremented by the first grouped string. Keep in mind that it is possible for syslog lines to be received from remote hosts out of order. If this happens you should not use this feature because tenshi will mis-report line counts. .br The builtin queue .I group can be used to group sets of regex together to speed up line matching. If a line fails to match a regex assigned to the group queue then tenshi will skip all the regex up until the next .I group_end statement. Nested groups are allowed. An example of this is included below. .br The builtin .I group_host queue can be used for selective hostname matching. Like the .I group queue it is also terminated with the .I group_end statement. All regex definitions within that group will only apply if the hostname associated to the log entries matches the regex passed to the .I group_host definition. .br The regexps below assume .I hidepid is turned on. If you have it turned off then you will need to add in \\[(.+)\\] to the regex following the progam name to get them to work. .br For example: mail ^sendmail: (.+): to=(.+),(.+)delay=(.+) becomes: mail ^sendmail\\[(.+)\\]: (.+): to=(.+),(.+)delay=(.+) Examples: .br trash ^xinetd .br repeat ^(?:last message repeated|above message repeats) (\\d+) time .br group ^sendmail: .br mail ^sendmail: (.+): to=(.+),(.+)delay=(.+) .br mail ^sendmail: (.+): to=(.+),(.+)relay=(.+),(.+)stat=Sent .br group_end .br group_host mailserver1 .br mail1 ^sendmail .br mail1 ^sendmail:.+ .br critical,mail1 ^sendmail:.+SYSERR.+ .br group_end .br mail ^ipop3d: Login user=(.+) .br critical,report ^sshd: Illegal user .br general,urgent:200,critical:1000 ^sshd: Illegal user .br root ^sshd\\(pam_unix\\): session opened for user root by root\\(uid=0\\) .br report ^sshd: Accepted rsa for (.+) from (.+) port (.+) .br trash ^sshd .br critical ^(?:sudo|su): .br critical,pager ^Oops .br misc .* .SH SIGNALS .br tenshi can handle different signals sent to the process, here's the list of supported ones: .TP .B TERM flush all queues and then exit .TP .B INT flush all queues and then exit .TP .B USR1 flush any queues which have reached their notification interval .TP .B USR2 force all queues to be flushed, even if they have not reached their notification interval .TP .B HUP force all queues to be flushed, even if they have not reached their notification interval, re-read the config file and continue as normal. .LP .I WARNING: If you change a STATIC OPTION in the config file and send tenshi a HUP it will die. You will need to restart tenshi for changes to STATIC OPTIONs to take effect. .SH EXAMPLES See the included tenshi.conf. .SH REQUIREMENTS tenshi needs a working 'tail' implementation when not using FIFO mode. .br It also requires Net::SMTP module for mailing reports, which should be included in your perl installation, and IO::BufferedSelect. If you miss any of them you can grab them at CPAN (http://www.cpan.org) or using the CPAN shell (`perl -e shell -MCPAN`). .SH BUGS Double quotation characters present in your logs might break csv output (depending on how you pipe/process it in the filter) since there's no escape code (yet). Please report any bugs you find at .BI .SH LICENSE .B tenshi is distributed under the terms of the following ISC-style license: Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .SH DISTRIBUTION The tenshi project page is .BI http://www.inversepath.com/tenshi.html .SH NOTES tenshi was once known as .I wasabi but the name was changed as we were informed that wasabi is a registered a trademark relating to another piece of software. .SH SEE ALSO It should be noted that tenshi was initially a perl rewrite of .I oak .RI ( http://www.ktools.org ). .I Friedl, Jeffrey E. F. Mastering Regular Expressions, 2nd Edition. O'Reilly .SH AUTHORS Copyright 2004-2011 Andrea Barisani tenshi-0.13/tenshi.suse-init0000644000175000017500000000541211607400554015116 0ustar lcarsusers#!/bin/sh # # /etc/init.d/tenshi # and its symbolic link # /(usr/)sbin/rctenshi # ### BEGIN INIT INFO # Provides: tenshi # Required-Start: $ALL # Should-Start: # Required-Stop: $ALL # Should-Stop: # Default-Start: 3 5 # Default-Stop: 0 1 2 6 # Short-Description: start tenshi log file analysis # Description: "$ALL" says tenshi will start as last and end as first service ### END INIT INFO # # Note on runlevels: # 0 - halt/poweroff 6 - reboot # 1 - single user 2 - multiuser without network exported # 3 - multiuser w/ network (text mode) 5 - multiuser w/ network and X11 (xdm) # TENSHI_BIN=/usr/sbin/tenshi test -x $TENSHI_BIN || { echo "$TENSHI_BIN not installed"; if [ "$1" = "stop" ]; then exit 0; else exit 5; fi; } TENSHI_CONFIG=/etc/sysconfig/tenshi test -r $TENSHI_CONFIG || { echo "$TENSHI_CONFIG not existing"; if [ "$1" = "stop" ]; then exit 0; else exit 6; fi; } . $TENSHI_CONFIG TENSHI_PIDFILE=/var/run/tenshi/tenshi.pid test -d /var/run/tenshi/ && /bin/chown root:tenshi /var/run/tenshi/ . /etc/rc.status # Reset status of this service rc_reset case "$1" in start) if [ -r $TENSHI_PIDFILE ] then echo echo " Service 'tenshi' seems to be running" echo " Check the status or restart the service" echo exit 255 fi echo -n "Starting tenshi " ## Start daemon with startproc(8). If this fails ## the return value is set appropriately by startproc. /sbin/startproc /usr/sbin/tenshi # Remember status and be verbose rc_status -v ;; stop) echo -n "Shutting down tenshi " ## Stop daemon with killproc(8) and if this fails ## killproc sets the return value according to LSB. # tenshi is running as daemon and doesn't show full path /sbin/killproc tenshi # Remember status and be verbose rc_status -v ;; restart) ## Stop the service and regardless of whether it was ## running or not, start it again. $0 stop sleep 2 $0 start # Remember status and be quiet rc_status ;; status) echo -n "Checking for service tenshi " ## Check status with checkproc(8), if process is running ## checkproc will return with exit status 0. # Return value is slightly different for the status command: # 0 - service up and running # 1 - service dead, but /var/run/ pid file exists # 2 - service dead, but /var/lock/ lock file exists # 3 - service not running (unused) # 4 - service status unknown :-( # 5--199 reserved (5--99 LSB, 100--149 distro, 150--199 appl.) # NOTE: checkproc returns LSB compliant status values. # tenshi is running as daemon and doesn't show full path /sbin/checkproc tenshi # NOTE: rc_status knows that we called this init script with # "status" option and adapts its messages accordingly. rc_status -v ;; *) echo "Usage: $0 {start|status|stop|restart}" exit 1 ;; esac rc_exit tenshi-0.13/tenshi.suse-spec0000644000175000017500000001377511607400554015120 0ustar lcarsusers#-------------------------------------------------------------------------------- # Program: tenshi.spec # # Purpose: This is the data file user to generate RPM files so that we can # distrbute 'canned' versions of what we have done more easily. #-------------------------------------------------------------------------------- # 10-Nov-06 - REP - Initial version #-------------------------------------------------------------------------------- # Some basic definitions for use to use later in the file. We really only want # to define things once, and have to change things in only one place. #-------------------------------------------------------------------------------- %define name tenshi %define version 0.13 %define release 1 %define mandir /usr/share/man %define docdir /usr/share/doc/packages %define sbindir /usr/sbin %define bindir /bin #-------------------------------------------------------------------------------- # Basic package information #-------------------------------------------------------------------------------- Summary: tenshi log monitoring program Name: %{name} Version: %{version} Release: %{release} Group: System Environment/Daemons License: ISC-style Url: http://www.inversepath.com/tenshi.html Source0: %{name}-%{version}.tar.gz Requires: perl Buildroot: %{_tmppath}/%{name}-buildroot #-------------------------------------------------------------------------------- # Description of the package #-------------------------------------------------------------------------------- %description tenshi is a log monitoring program, designed to watch one or more log files for lines matching user defined regular expressions and report on the matches. The regular expressions are assigned to queues which have an alert interval and a list of mail recipients. #-------------------------------------------------------------------------------- # What things to do in preparation of making the package #-------------------------------------------------------------------------------- %prep %setup #-------------------------------------------------------------------------------- # The build process for the package #-------------------------------------------------------------------------------- %build #-------------------------------------------------------------------------------- # Configuration process for the package #-------------------------------------------------------------------------------- ### No configure process for tenshi #-------------------------------------------------------------------------------- # The install process for the package #-------------------------------------------------------------------------------- %install [ -n "%{buildroot}" -a "%{buildroot}" != / ] && rm -rf %{buildroot} mkdir -p %{buildroot}%{_initrddir} mkdir -p %{buildroot}%{_sysconfdir}/%{name} mkdir -p %{buildroot}/etc/sysconfig mkdir -p %{buildroot}/usr/share/man/man8/ mkdir -p %{buildroot}%{docdir}/%{name} mkdir -p %{buildroot}/var/run/%{name}/ make DESTDIR=%{buildroot} mandir=%{_mandir} docdir=%{docdir}/%{name} install install -m755 tenshi.suse-init %{buildroot}%{_initrddir}/%{name} touch %{buildroot}/etc/sysconfig/%{name} touch %{buildroot}/var/run/%{name}/%{name}.pid #-------------------------------------------------------------------------------- # Things to run after it has been installed. #-------------------------------------------------------------------------------- %post /sbin/chkconfig --add %{name} # Manually add user/group %{sbindir}/groupadd %{name} %{sbindir}/useradd -g %{name} -d %{_sysconfdir}/%{name} -s /bin/false %{name} # Generate link %{bindir}/ln -s /etc/init.d/%{name} /sbin/rc%{name} #-------------------------------------------------------------------------------- # Take tenshi out of runlevels #-------------------------------------------------------------------------------- %preun if [ "$1" = 0 ]; then /sbin/chkconfig --del %{name} fi #-------------------------------------------------------------------------------- # Remove tenshi user/group if necessary (since "tenshi" is the only # member of group "tenshi", then deletion of "tenshi" user deletes # the group. #-------------------------------------------------------------------------------- %postun if [ "$1" = 0 ]; then %{sbindir}/userdel %{name} %{sbindir}/groupdel %{name} fi # Delete the link %{bindir}/rm /sbin/rc%{name} #-------------------------------------------------------------------------------- # What files and permissions are included in the package #-------------------------------------------------------------------------------- %files %defattr(644,root,root,755) %doc README INSTALL CREDITS LICENSE Changelog FAQ %config(noreplace) %{_sysconfdir}/%{name}/%{name}.conf %config(noreplace) /etc/sysconfig/%{name} %attr(755,root,root) %config(noreplace) %{_initrddir}/%{name} %attr(755,root,root) %{sbindir}/%{name} %{_mandir}/man8/%{name}.8* %dir %attr(775,tenshi,tenshi) /var/run/%{name} %ghost %attr(600,tenshi,tenshi) /var/run/%{name}/%{name}.pid #-------------------------------------------------------------------------------- # What final cleanup should occur after the package construction has been # completed #-------------------------------------------------------------------------------- %clean [ -n "%{buildroot}" -a "%{buildroot}" != / ] && rm -rf %{buildroot} #-------------------------------------------------------------------------------- # Changelog #-------------------------------------------------------------------------------- %changelog * Thu Sep 16 2010 Heinrich Terhardt 0.12 - some specials for opensuse * Mon Aug 27 2007 Andrea Barisani 0.8 - assorted fixes, thanks goes to Elan Ruusamäe * Fri Nov 10 2006 Steven McCoy, Jr. 0.6-1 - Initial specfile/rpm creation. Used syslog-ng.spec as template - Thanks to: Richard E. Perlotto II (syslog-ng spec maintainer) tenshi-0.13/tenshi0000755000175000017500000012015211607400554013201 0ustar lcarsusers#!/usr/bin/perl # # tenshi 0.13 2011/07/13 # # # Copyright 2004-2011 Andrea Barisani # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. use strict; use warnings; use Net::SMTP; use File::Temp; use Sys::Hostname; use IO::Socket::INET; use filetest 'access'; use IO::BufferedSelect; use Term::ANSIColor qw(:constants); use Getopt::Long qw(:config no_ignore_case); use POSIX qw(locale_h setsid setuid setgid strftime floor); setlocale(LC_TIME, "C"); File::Temp->safe_level(File::Temp::HIGH); $Term::ANSIColor::AUTORESET = 1; my $version = '0.13'; my %opts; GetOptions('configuration=s' => \$opts{'c'}, 'Check' => \$opts{'C'}, 'debug:i' => \$opts{'d'}, 'foreground' => \$opts{'f'}, 'profile' => \$opts{'p'}, 'Pid=s' => \$opts{'P'}, 'help' => \$opts{'h'}); if ($opts{'h'}) { usage(); } my $our_hostname = hostname(); my @startup_time = localtime(); my ($uid, $gid); my $last_check = 0; my $last_minute = 0; my $sleep = 5; my $mailtimeout = 10; my $select_timeout = 1; my $saved_pid = 0; my $config_reinit = 1; my ($mailserver, $limit, $pager_limit, $hidepid, $status, $listen, $resolve); my (%main, %last_match, %last_queue, %filter_file, %filter_args, %hostnames, %regexp_matches); my ($config_read, $queue_flush_needed, $queue_check_needed, $time_to_die); my (@log_files, @fifo_files, @log_prefix, @regexp, @queues, @skip, @group_stack, @queues_escalation); my ($syslog_sender, $syslog_listen_socket); my $profile = $opts{'p'} || 0; my $foreground = $opts{'f'} || 0; my $config_file = $opts{'c'} || '/etc/tenshi/tenshi.conf'; my $pid_file = $opts{'P'} || '/var/run/tenshi.pid'; my ($debug, $debug_smtp); $debug = (defined($opts{'d'}) && $opts{'d'} == 0) ? 1 : $opts{'d'} || 0; $debug_smtp = ($debug > 1) ? 1 : 0; my $tail_file = '/usr/bin/tail'; my $tail_args = '-q --follow=name --retry -n 0'; my $tail_multiple = 'off'; my @tail_pids; my @fhs; my %days = ( 'mon' => 1, 'tue' => 2, 'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6, 'sun' => 0 ); my %months = ( 'jan' => 1, 'feb' => 2, 'mar' => 3, 'apr' => 4, 'may' => 5, 'jun' => 6, 'jul' => 7, 'aug' => 8, 'sep' => 9, 'oct' => 10, 'nov' => 11, 'dec' => 12 ); my @cron_specs = ( { 'min' => 0, 'max' => 59, 'shift' => 0, 'wrap' => 0, 'localtime_field' => 1 }, { 'min' => 0, 'max' => 23, 'shift' => 0, 'wrap' => 0, 'localtime_field' => 2 }, { 'min' => 1, 'max' => 31, 'shift' => 0, 'wrap' => 0, 'localtime_field' => 3 }, { 'min' => 1, 'max' => 12, 'shift' => -1, 'wrap' => 0, 'localtime_field' => 4, 'strings' => \%months }, { 'min' => 0, 'max' => 7, 'shift' => 0, 'wrap' => 1, 'localtime_field' => 6, 'strings' => \%days }, ); my $mask = '______'; my $mask_length = length $mask; my $subject = 'tenshi report'; my $sort_order = 'descending'; # prototype for clean_up so we don't need parens sub clean_up; config_read($config_file); $config_read = 1; if ($opts{'C'}) { exit 0; } if (not defined($uid)) { $uid = getpwnam('tenshi') or clean_up and die RED "[ERROR] no such user: tenshi\n"; } if (not defined($gid)) { $gid = getgrnam('tenshi') or clean_up and die RED "[ERROR] no such group: tenshi\n"; } if ($listen) { $syslog_listen_socket = IO::Socket::INET->new( LocalAddr => $listen, Proto => 'udp' ) or clean_up and die "[ERROR] can't bind UDP socket: $!\n"; push @fhs, $syslog_listen_socket; } $SIG{'CHLD'} = sub { $debug && debug(5,'CHLD') ; print RED "[ERROR] Child died. Bailing out\n"; $time_to_die = 1; }; prepare_process(); # # sanity checks # if (!$profile) { foreach my $queue (@queues) { if ($filter_file{$queue} and ! -x $filter_file{$queue}) { clean_up and die RED "[ERROR] $filter_file{$queue}: not executable"; } } if ($main{'csv'} and ! -x $main{'csv'}{'path'}) { clean_up and die RED "[ERROR] $main{'csv'}{'path'}: not executable"; } my @readable_log_files; foreach my $log (@log_files) { unless (-f $log) { print STDERR RED "[WARNING] $log: no such file\n"; next; } unless (-r $log) { print STDERR RED "[WARNING] $log: file not readable\n"; next; } push @readable_log_files, $log; } @readable_log_files > 0 || @fifo_files > 0 or clean_up and die RED "[ERROR] no readable log files"; @log_files = @readable_log_files; } # # log file parsing # if ($profile) { open(my $fh, "-") or clean_up and die RED "[ERROR] could not open standard input: $!\n"; push @fhs, $fh; } else { if (scalar(@log_files) > 0) { if ($tail_multiple eq 'on') { foreach my $log (@log_files) { $debug && debug(20, "$tail_file $tail_args $log"); clean_up and die RED "[ERROR] $tail_file: $!\n" if (! -f $tail_file); my $tail_pid = open(my $fh, "-|"); if (not defined($tail_pid)) { clean_up and die RED "[ERROR] could not open pipe for tail command: $!\n"; } elsif ($tail_pid == 0) { close STDERR; exec("$tail_file $tail_args $log") or die; # this is the child process so don't clean_up } push @fhs, $fh; push @tail_pids, $tail_pid; } } else { my $log_files = join(' ', @log_files); $debug && debug(20, "$tail_file $tail_args $log_files"); clean_up and die RED "[ERROR] $tail_file: $!\n" if (! -f $tail_file); my $tail_pid = open(my $fh, "-|"); if (not defined($tail_pid)) { clean_up and die RED "[ERROR] could not open pipe for tail command: $!\n"; } elsif ($tail_pid == 0) { close STDERR; exec("$tail_file $tail_args $log_files") or die; # this is the child process so don't clean_up } push @fhs, $fh; push @tail_pids, $tail_pid; } } if (scalar(@fifo_files) > 0) { foreach my $fifo_file (@fifo_files) { $debug && debug(19, "$fifo_file"); open(my $fh, "+<$fifo_file") or clean_up and die RED "[ERROR] could not open $fifo_file: $!\n"; push @fhs, $fh; } } } if (scalar(@fhs) < 1) { clean_up and die RED "[ERROR] No log file has been specified\n"; } if (!($debug or $profile or $foreground)) { close STDOUT or clean_up and die RED "[ERROR] can't close STDOUT: $!\n"; close STDERR or clean_up and die RED "[ERROR] can't close STDERR: $!\n"; } $debug && debug(3); $SIG{'TERM'} = sub { $debug && debug(5,'TERM') ; $status = 'terminating'; $queue_flush_needed = 1; $time_to_die = 1; }; $SIG{'INT'} = sub { $debug && debug(5, 'INT') ; $status = 'terminating'; $queue_flush_needed = 1; $time_to_die = 1; }; $SIG{'HUP'} = sub { $debug && debug(5, 'HUP') ; $status = 'reloading' ; $queue_flush_needed = 1; $config_reinit = 1; }; $SIG{'USR1'} = sub { $debug && debug(5,'USR1') ; $status = 'queue check'; $queue_check_needed = 1; }; $SIG{'USR2'} = sub { $debug && debug(5,'USR2') ; $status = 'flushing' ; $queue_flush_needed = 1; }; my $bs = IO::BufferedSelect->new(@fhs); if (!($debug || $profile || $foreground)) { daemonize(); } save_pid(); while (!$time_to_die) { my $now = time; if ($now > ($last_check + $sleep)) { $queue_check_needed = 1; } if ($queue_flush_needed) { queues_flush(); $queue_flush_needed = 0; $queue_check_needed = 0; } if ($queue_check_needed) { queues_check($now); $queue_check_needed = 0; } if ($config_reinit) { config_read($config_file); $config_reinit = 0; } if ($time_to_die) { last; } my @ready = $bs->read_line($select_timeout); foreach (@ready) { my ($fh, $line) = @$_; if ($listen and $fh == $syslog_listen_socket) { $line =~ s/^<\d+>//; if (! ($line =~ /^[A-Z][a-z]{2}\s(?:\s|\d)\d\s\d{2}:\d{2}:\d{2}\s(\S+)\s/)) { my ($port, $ipaddr) = sockaddr_in(getpeername($syslog_listen_socket)); if (not defined $hostnames{$ipaddr}) { $hostnames{$ipaddr} = gethostbyaddr($ipaddr, AF_INET); } my $time = strftime "%b %e %H:%M:%S", localtime; $line = sprintf("%s %s %s", $time, $hostnames{$ipaddr}, $line); } } if ($profile and not defined($line)) { print BLUE, "[PROFILE] Reached end of file\n"; $time_to_die = 1; next; } parse_line($line); } } queues_flush(); clean_up(); exit 0; # # subs # sub config_read { my $config_file = shift; $debug && debug(0,$config_file); if ($config_reinit) { %main = (); %hostnames = (); @regexp = (); %regexp_matches = (); @queues = (); @queues_escalation = (); @skip = (); @log_prefix = (); $main{'group'} = {}; $main{'trash'} = {}; $main{'repeat'} = {}; $main{'group_host'} = {}; $hidepid = 0; $resolve = 0; push @log_prefix, qr/^[A-Z][a-z]{2}\s(?:\s|\d)\d\s\d{2}:\d{2}:\d{2}\s(\S+)\s/; $config_reinit = 0; } # # configuration file parsing # open(my $CONF,$config_file) or clean_up and die RED "[ERROR] could not open configuration file $config_file: $!\n"; while (<$CONF>) { s/^\s+//; next if (/^#|^$/); chomp; if (/^include\s+(\S+)/) { $debug && debug(1,$_) ; config_read($1); next; } if (/^includedir\s+(\S+)/) { $debug && debug(1,$_); opendir(my $DIR, $1) or clean_up and die RED "[ERROR] could not open directory $1: $!\n"; foreach my $file (sort readdir($DIR)) { next if ($file =~ /^\./); next unless -f "$1/$file"; config_read("$1/$file"); } next; } if (/^set\s+logfile\s+(\S+)/) { if ($config_read and (!grep(/^$1$/, @log_files))) { clean_up and die debug(100,'logfile'); } elsif (!$config_read) { $debug && debug(1,$_); } push @log_files, $1; } elsif (/^set\s+fifo\s+(\S+)/) { if ($config_read and (!grep(/^$1$/, @fifo_files))) { clean_up and die debug(100,'fifo'); } elsif (!$config_read) { $debug && debug(1, $_); } push @fifo_files, $1; } elsif (/^set\s+pidfile\s+(\S+)/) { next if $opts{'P'}; if ($config_read and ($1 ne $pid_file)) { clean_up and die debug(100,'pidfile'); } elsif (!$config_read and (!$opts{'P'})) { $debug && debug(1,$_); } $pid_file = $1; } elsif (/^set\s+tail\s+(\S+)\s*(\S+.*)?/) { if ($config_read and ($1 ne $tail_file)) { clean_up and die debug(100,'tail'); } elsif (!$config_read) { $debug && debug(1,$_); } $tail_file = $1; $tail_args = $2 || $tail_args; } elsif (/^set\s+tail_multiple\s+(off|on)/) { if ($config_read and ($1 ne $tail_multiple)) { clean_up and die debug(100,'tail_multiple'); } elsif (!$config_read) { $debug && debug(1,$_); } if ($1 eq 'on') { $tail_multiple = 'on'; } else { $tail_multiple = 'off'; } } elsif (/^set\s+uid\s+(.+)/) { if ($config_read and (getpwnam($1) ne $uid)) { clean_up and die debug(100,'uid'); } elsif (!$config_read) { $debug && debug(1,$_); } $uid = getpwnam($1); clean_up and die RED "[ERROR] no such user: $1\n" if not defined $uid; if (not defined $uid) { clean_up and die RED "[ERROR] no such user: $1\n"; } } elsif (/^set\s+gid\s+(.+)/) { if ($config_read and (getgrnam($1) ne $gid)) { clean_up and die debug(100,'gid'); } elsif (!$config_read) { $debug && debug(1,$_); } $gid = getgrnam($1); if (not defined $gid) { clean_up and die RED "[ERROR] no such group: $1\n"; } } elsif (/^set\s+listen\s+(\d+\.\d+\.\d+\.\d+:\d+)/) { if ($config_read and ($1 ne $listen)) { clean_up and die debug(100,'listen'); } elsif (!$config_read) { $debug && debug(1,$_); } $listen = $1; } elsif (/^set\s+sort_order\s+(\S+)/) { if ($1 =~ /^(ascending|descending)$/) { $sort_order = $1; $debug && debug(1,$_); next; } else { clean_up and die RED "[ERROR] sort_order is invalid"; } } elsif (/^set\s+limit\s+(\d+)/) { $limit = $1; $debug && debug(1,$_); next; } elsif (/^set\s+subject\s+(.+)/) { $subject = $1; $debug && debug(1,$_); next; } elsif (/^set\s+mailserver\s+(.+)/) { $mailserver = $1; $debug && debug(1,$_); next; } elsif (/^set\s+pager_limit\s+(\d+)/) { $pager_limit = $1; $debug && debug(1,$_); next; } elsif (/^set\s+mailtimeout\s+(\d+)/) { $mailtimeout = $1; $debug && debug(1,$_); next; } elsif (/^set\s+filter\s+(\S+)\s+(\S+)\s*(\S+.*)?/) { $filter_file{$1} = $2; $filter_args{$1} = $3 || ""; $debug && debug(1,$_); next; } elsif (/^set\s+sleep\s+(\d+)/) { if ($sleep > 60) { clean_up and die RED "[ERROR] sleep time should be <= 60 seconds\n"; } else { $sleep = $1; $debug && debug(1,$_); next; } } elsif (/^set\s+logprefix\s+(.+)/) { push @log_prefix, qr/$1/; $debug && debug(1,$_); } elsif (/^set\s+mask(\s+(\S+))?/) { $mask = ($1 ? $2: ''); $mask_length = length $mask; $debug && debug(1,$_); } elsif (/^set\s+hidepid\s+(off|on)/) { if ($1 eq 'on') { $hidepid = 1; } else { $hidepid = 0; } $debug && debug(1,$_); } elsif (/^set\s+resolve\s+(off|on)/) { if ($1 eq 'on') { $resolve = 1; } else { $resolve = 0; } $debug && debug(1,$_); } elsif (/^set\s+queue\s+(\S+)\s+(\S+(?:\@\S+)?)\s+(pager:)?(\S+(?:\@\S+)?)\s+\[((?:\S+(?:\s+)?){5}|now)\]\s*(\S+.*)?/o) { my ($queue, $mail_from, $pager, $mail_to, $cron_spec, $subject) = ($1, $2, $3, $4, $5, $6); if (queue_is_builtin($queue)) { clean_up and die RED "[ERROR] '$queue' is a builtin queue\n"; } if ($queue eq 'csv') { clean_up and die RED "[ERROR] '$queue' is a special queue and must be defined with 'set csv' option\n"; } if ($cron_spec eq 'now') { $main{$queue}{'now'} = 1; } else { $main{$queue}{'cron_mask'} = cron_spec_to_mask($cron_spec); } if ($pager) { $main{$queue}{'pager'} = 1; } $main{$queue}{'mailfrom'} = $mail_from; $debug && debug(1,"queue: $queue - mail_from => $mail_from"); $main{$queue}{'mailto'} = $mail_to; $debug && debug(1,"queue: $queue - mailto => $mail_to"); if ($subject) { $main{$queue}{'subject'} = $subject; $debug && debug(1,"queue: $queue - subject => $subject"); } } elsif (/^set\s+csv\s+\[((?:\S+(?:\s+)?){5}|now)\]\s+(\S+)\s*(.*)?/o) { my $cron_spec = $1; ($main{'csv'}{'path'}, $main{'csv'}{'args'}) = ($2, $3); if ($cron_spec eq 'now') { $main{'csv'}{'now'} = 1; } else { $main{'csv'}{'cron_mask'} = cron_spec_to_mask($cron_spec); } } elsif (/^set\s+threshold\s+(\S+)\s+(\d+)\s+(.+$)/) { my ($queue, $count, $re) = ($1, $2, $3); unless (defined $main{$queue}) { clean_up and die RED "[ERROR] Invalid queue in threshold set directive: $_\n" } if ($count <= 0) { clean_up and die RED "[ERROR] Invalid count in threshold set directive: $_\n" } unless(defined $main{$queue}{'threshold'}) { $main{$queue}{'threshold'} = [] } # store $re, $count pair in the array push @{$main{$queue}{'threshold'}}, qr/$re/, $count; } elsif (/^set\s+/) { clean_up and die RED "[ERROR] Invalid set directive: $_\n"; } elsif (my ($queue, $reg) = $_ =~ /(^\S+)\s+(.+$)/) { $debug && debug(1,"queue: $queue regexp: $reg"); my @queue = split(/,/, $queue); my %queue_escalation; my $max_escalation = 0; foreach my $q (@queue) { my ($queue, $escalation) = split(/:/, $q); if (!($main{$queue})) { clean_up and die RED "[ERROR] Invalid configuration directive: queue $queue not defined\n"; } if ((scalar(@queue) > 1) and queue_is_builtin($queue)) { clean_up and die RED "[ERROR] builtin queue not allowed in multiple queues declaration\n"; } if (($queue =~ /:/) && queue_is_builtin($queue)) { clean_up and die RED "[ERROR] builtin queues are not allowed to have an escalation number\n"; } if (defined($escalation)) { if ($escalation !~ m/^[1-9]\d*$/) { clean_up and die RED "[ERROR] Escalation number must be a positive integer greater than zero\n"; } if ($escalation >= $max_escalation) { $max_escalation = $escalation; } else { clean_up and die RED "[ERROR] Escalation numbers must increase from left to right in the queue list\n"; } $queue_escalation{$queue} = $escalation; } else { if ($max_escalation) { clean_up and die RED "[ERROR] All queues without escalation numbers must be listed more left than the queues with escalation numbers\n"; } } } if (@queue > 1 and $queue[0] =~ /:/) { clean_up and die RED "[ERROR] Left most queue in a multiple queue declaration can not have an escalation number\n"; } if ($queue eq 'group') { push @group_stack, scalar(@regexp); } if ($queue eq 'group_host') { push @group_stack, scalar(@regexp); } push @regexp, qr/$reg/; $queue =~ s/:\d*//g; push @queues, $queue; push @queues_escalation, \%queue_escalation; push @skip, 0; } elsif (/^group_end/) { if (scalar(@group_stack) < 1) { clean_up and die RED "[ERROR] Tried to close a group when there are non open\n"; } $skip[pop @group_stack] = scalar(@regexp) || 0; } else { clean_up and die RED "[ERROR] Invalid configuration directive: $_\n"; } } close $CONF; clean_up and die RED "[ERROR] no smtp server specified" if (!$mailserver); $debug && debug(2,$config_file); if ($debug) { for (my $i = 0; $i < scalar(@regexp); $i++) { debug(18, $i, $regexp[$i]); } } } sub parse_line { my $line = $_[0]; if (defined $line) { if ($time_to_die) { next; } my $hostname; my $has_prefix; chomp($line); $debug && debug(6,$line); foreach my $log_prefix (@log_prefix) { if ($line =~ s/$log_prefix//) { $has_prefix = 1; $hostname = $1; last; } } if (!$has_prefix && $main{'noprefix'}) { $debug && debug(7,'noprefix',$line); $main{'noprefix'}{'logs'}{'[unprefixed logs]'}{$line}++; } return unless defined($hostname); if ($hidepid) { $line =~ s/^(\S+)\[\d+\]: /$1: /o; } for (my $index = 0; $index <= $#regexp; $index++) { my $regexp = $regexp[$index]; my $queue = $queues[$index]; my $queue_escalation = $queues_escalation[$index]; my @queue = split(/,/, $queues[$index]); if ($queue eq 'group_host') { if ($hostname =~ /$regexp/) { next; } elsif ($skip[$index] > 0) { $debug && debug(9,$skip[$index],$line); $index = ($skip[$index] - 1); next; } } if ($line =~ /$regexp/) { $debug && debug(7,$queue,$line); last if ($queue eq 'trash'); next if ($queue eq 'group'); if ($queue eq 'repeat' and $last_match{$hostname}) { my @last_queue = split(/,/, $last_queue{$hostname}); foreach my $last_queue (@last_queue) { $main{$last_queue}{'logs'}{$hostname}{$last_match{$hostname}} += $1; } last; } my $offset = 0; my ($begin, $end); foreach my $i (1 .. $#-) { next unless defined($-[$i]); $begin = $-[$i] + $offset; $end = $+[$i] + $offset; my $length = ($end - $begin); substr($line, $begin, $length, $mask); $offset += ($mask_length - $length); } $debug && debug(8,$line); $last_queue{$hostname} = $queue; $last_match{$hostname} = $line; $regexp_matches{$hostname}[$index]++; my $max_escalation = $queue_escalation->{$queue[$#queue]}; foreach my $queue (@queue) { my $escalation = $queue_escalation->{$queue}; if ($escalation) { # If the regexp has matched enough lines to escalate, put the line in the queue. # When the queue with the largest escalation number receives a message, then we # want the escalation count to essentially reset, looping escalation back around # through the other escalation queues, but without actually resetting the number # of matches. my $lines = $regexp_matches{$hostname}[$index] % $max_escalation; if ($escalation == $lines || ($escalation == $max_escalation && $lines == 0)) { $main{$queue}{'logs'}{$hostname}{$line} = $regexp_matches{$hostname}[$index]; } } else { $main{$queue}{'logs'}{$hostname}{$line}++; } } last; } elsif ($skip[$index] > 0) { $debug && debug(9,$skip[$index],$line); $index = ($skip[$index] - 1); } } } } sub queue_is_builtin { my $queue = shift; if (($queue eq 'trash') or ($queue eq 'repeat') or ($queue eq 'group') or ($queue eq 'group_host')) { return 1; } return 0; } sub queues_check { my $now = shift; my @time = localtime($now); my $check_crons = 0; $last_check = $now; my $current_minute = floor($now / 60); if ($current_minute > $last_minute) { $check_crons = 1; $last_minute = $current_minute; } $debug && debug(12); foreach my $queue (keys %main) { next if queue_is_builtin($queue); if ($main{$queue}{'now'} || ($check_crons && cron_mask_match(\@time, $main{$queue}{'cron_mask'}))) { if ($queue eq 'csv') { csv_out(); next; } queue_mail($queue) if (!$profile); } } } sub queues_flush { $debug && debug(13); foreach my $queue (keys %main) { next if queue_is_builtin($queue); if ($queue eq 'csv') { csv_out(); next; } queue_mail($queue) if (!$profile); } if ($status) { $status = 0; } } sub queue_mail { my $queue = shift; my @lines; # evaluate threshold first to prevent report if none of the lines appeared often enough if($main{$queue}{'threshold'}) { my @t = @{$main{$queue}{'threshold'}}; foreach my $hostname (keys %{$main{$queue}{'logs'}}) { foreach my $key (keys %{$main{$queue}{'logs'}{$hostname}}) { for (my $x = 0; $x < @t; $x += 2) { # compare count first then regex if ($main{$queue}{'logs'}{$hostname}{$key} < $t[$x+1] && $key =~ $t[$x]) { delete $main{$queue}{'logs'}{$hostname}{$key}; last; } } } delete $main{$queue}{'logs'}{$hostname} unless(keys %{$main{$queue}{'logs'}{$hostname}}); } delete $main{$queue}{'logs'} unless(keys %{$main{$queue}{'logs'}}); } return unless (keys %{$main{$queue}{'logs'}}); $debug && debug(11,$queue); my $tmp = new File::Temp(UNLINK => 1, DIR => '/tmp/', SUFFIX => '.tenshi') or clean_up and die RED "[ERROR] could not open temporary file: $!\n"; $debug && debug(19,$tmp); if ($status and (!$main{$queue}{'pager'})) { print $tmp "*** Status: $status ***\n"; } foreach my $hostname (keys %{$main{$queue}{'logs'}}) { my $index = 0; next unless (keys %{$main{$queue}{'logs'}{$hostname}}); if ($resolve && !$main{$queue}{'pager'}) { my $ipaddr = inet_aton($hostname); $hostnames{$ipaddr} = gethostbyaddr($ipaddr, AF_INET) if($ipaddr && !defined $hostnames{$ipaddr}); if(defined $ipaddr && defined $hostnames{$ipaddr}) { print $tmp "\n$hostname ($hostnames{$ipaddr}): \n"; } else { print $tmp "\n$hostname: \n"; } } else { print $tmp "\n$hostname: \n" if (!$main{$queue}{'pager'}); } my @sorted_keys; if ($sort_order eq 'descending') { @sorted_keys = reverse sort { $main{$queue}{'logs'}{$hostname}{$a} <=> $main{$queue}{'logs'}{$hostname}{$b} } keys %{$main{$queue}{'logs'}{$hostname}}; } elsif ($sort_order eq 'ascending') { @sorted_keys = sort { $main{$queue}{'logs'}{$hostname}{$a} <=> $main{$queue}{'logs'}{$hostname}{$b} } keys %{$main{$queue}{'logs'}{$hostname}}; } foreach my $key (@sorted_keys) { if ($main{$queue}{'pager'}) { last if ($pager_limit and ($index >= $pager_limit)); print $tmp "$hostname,$main{$queue}{'logs'}{$hostname}{$key},$key\n"; $index++; } else { last if ($limit and ($index >= $limit)); print $tmp " $main{$queue}{'logs'}{$hostname}{$key}: $key\n"; $index++; } } print $tmp "\n *** Too many alerts (limit: $limit) ***\n" if ($limit and ($index >= $limit)); # clear regexp match count for every regexp whose left most queue is the one being mailed for (my $index = 0; $index <= $#regexp; $index++) { my @q = split(/,/, $queues[$index]); $regexp_matches{$hostname}[$index] = 0 if $q[0] eq $queue; } } seek($tmp, 0, 0) or clean_up and die RED "[ERROR] can't rewind $tmp->filename: $!\n"; if ($filter_file{$queue}) { local $SIG{CHLD} = 'IGNORE'; # FIXME it's ugly I know, need something smarter here $debug && debug(20,"$filter_file{$queue} $filter_args{$queue} < $tmp"); open(my $filter, "$filter_file{$queue} $filter_args{$queue} < $tmp|") or clean_up and die RED "[ERROR] '$filter_file{$queue} $filter_args{$queue} < $tmp' failed: $!\n"; while (<$filter>) { push @lines, $_; } } else { while (<$tmp>) { push @lines, $_; } } return unless (scalar(@lines) > 0); my $smtp = Net::SMTP->new($mailserver, Timeout => $mailtimeout, Debug => $debug_smtp); if (!$smtp) { print RED "[ERROR] could not contact $mailserver:25\n"; return; } if (!$smtp->mail($main{$queue}{'mailfrom'})) { print RED "[ERROR] mail from: $main{$queue}{'mailfrom'} rejected\n"; return; } if (!$smtp->to(split(/,/, $main{$queue}{'mailto'}))) { print RED "[ERROR] rcpt to: $main{$queue}{'mailto'} rejected\n"; return; } if (!$smtp->data()) { print RED "[ERROR] data rejected\n"; return; } my $timezone = get_timezone(); my $subject = $main{$queue}{'subject'} || $subject; $smtp->datasend("From: $main{$queue}{'mailfrom'}\n"); $smtp->datasend("To: $main{$queue}{'mailto'}\n"); $smtp->datasend("Date: " . strftime("%a, %d %b %Y %H:%M:%S $timezone", localtime()) . "\n"); $smtp->datasend("X-tenshi-version: $version\n"); $smtp->datasend("X-tenshi-hostname: $our_hostname\n"); if (!$main{$queue}{'now'}) { my @now = localtime(); $main{$queue}{'report_time'} = [ @startup_time ] if (!$main{$queue}{'report_time'}); $smtp->datasend("X-tenshi-report-start: " . strftime("%a %b %d %H:%M:%S $timezone %Y", @{$main{$queue}{'report_time'}}) . "\n"); $main{$queue}{'report_time'} = [ @now ]; } $smtp->datasend("Subject: $subject [$queue]\n\n"); $smtp->datasend(@lines); $smtp->quit; $main{$queue}{'logs'} = {}; } sub csv_out { # FIXME: too much code duplication here, need better functions return unless (keys %{$main{'csv'}{'logs'}}); if (!$main{'csv'}{'now'}) { my @now = localtime(); $main{'csv'}{'report_time'} = [ @startup_time ] if (!$main{'csv'}{'report_time'}); $main{'csv'}{'report_time'} = [ @now ]; } my $tmp = new File::Temp(UNLINK => 1, DIR => '/tmp/', SUFFIX => '.tenshi') or clean_up and die RED "[ERROR] could not open temporary file: $!\n"; $debug && debug(19,$tmp); foreach my $hostname (keys %{$main{'csv'}{'logs'}}) { my $index = 0; next unless (keys %{$main{'csv'}{'logs'}{$hostname}}); my @sorted_keys = sort { $main{'csv'}{'logs'}{$hostname}{$a} <=> $main{'csv'}{'logs'}{$hostname}{$b} } keys %{$main{'csv'}{'logs'}{$hostname}}; foreach my $key (@sorted_keys) { print $tmp "$hostname,\"$key\",$main{'csv'}{'logs'}{$hostname}{$key}\n"; $index++; } # clear regexp match count for every regexp whose left most queue is the one being mailed for (my $index = 0; $index <= $#regexp; $index++) { my @q = split(/,/, $queues[$index]); $regexp_matches{$hostname}[$index] = 0 if $q[0] eq 'csv'; } } seek($tmp, 0, 0) or clean_up and die RED "[ERROR] can't rewind $tmp->filename: $!\n"; local $SIG{CHLD} = 'IGNORE'; # FIXME it's ugly I know, need something smarter here $debug && debug(20,"$main{'csv'}{'path'} $main{'csv'}{'args'} < $tmp"); open(my $filter, "$main{'csv'}{'path'} $main{'csv'}{'args'} < $tmp|") or die RED "[ERROR] '$main{'csv'}{'path'} $main{'csv'}{'args'} < $tmp' failed: $!\n"; $main{'csv'}{'logs'} = {}; } sub prepare_process { $0 = 'tenshi'; chdir '/' or clean_up and die RED "[ERROR] can't chdir to /: $!\n"; $) = "$gid $gid" or clean_up and die RED "[ERROR] can't reset supplementary groups: $!\n"; setgid($gid) or clean_up and die RED "[ERROR] can't setgid to $gid: $!\n"; setuid($uid) or clean_up and die RED "[ERROR] can't setuid to $uid: $!\n"; close STDIN or clean_up and die RED "[ERROR] can't close STDIN: $!\n"; } sub daemonize { defined(my $pid = fork) or clean_up and die RED "[ERROR] can't fork: $!\n"; exit if $pid; setsid() or clean_up and die RED "[ERROR] can't start a new session: $!\n"; } sub save_pid { open (PIDFILE,">$pid_file") or clean_up and die RED "[ERROR] could not open pid file $pid_file: $!\n"; print PIDFILE $$; $debug && debug(4,$$); close PIDFILE; $saved_pid = 1; } sub clean_up { my $save = $!; # preserve $! for the call to die local $SIG{CHLD} = 'IGNORE'; unlink $pid_file if $saved_pid; if (scalar(@tail_pids) > 0) { $debug && debug(21, join(' ', @tail_pids)); kill("SIGTERM", @tail_pids); } foreach my $fh (@fhs) { close $fh; if ($listen and $fh == $syslog_listen_socket) { $syslog_listen_socket->close(); } } $! = $save; return 1; } sub get_timezone { use Time::Local; my @time = localtime(); my $timediff = (timegm(@time) - timelocal(@time)); return sprintf("%+03d%02d", $timediff/3600 , $timediff%3600/60); } sub cron_field_resolve { my $field = lc(shift); my $strings_ref = shift; if (ref($strings_ref) && $strings_ref->{$field}) { return $strings_ref->{$field}; } else { return $field; } } sub cron_spec_to_mask { my $string = shift; my @mask; for (my $i = 0; $i < scalar(@cron_specs); $i++) { $string =~ s/^(\S+)\s*//o or clean_up and die RED "[ERROR] Unable to parse cron string: $string\n"; my $cron_spec = $cron_specs[$i]; my @mask_fields; $#mask_fields = $cron_spec->{'max'} + $cron_spec->{'shift'}; @mask_fields = map { 0 } @mask_fields; foreach my $field (split(/,/, $1)) { my $start = 0; my $end = 0; my $skip = 1; if ($field =~ /\*(?:\/([0-9]+))?/o) { $start = $cron_spec->{'min'}; $end = $cron_spec->{'max'}; if ($1) { $skip = $1 } } else { if (!($field =~ /(\w+)(?:-(\w+)(?:\/([0-9]+))?)?/o)) { clean_up and die RED "[ERROR] Error in field syntax: $field\n"; } if ($#- == 1) { $start = cron_field_resolve($1, $cron_spec->{'strings'}); $end = cron_field_resolve($1, $cron_spec->{'strings'}); } elsif ($#- == 2) { $start = cron_field_resolve($1, $cron_spec->{'strings'}); $end = cron_field_resolve($2, $cron_spec->{'strings'}); } elsif ($#- == 3) { $start = cron_field_resolve($1, $cron_spec->{'strings'}); $end = cron_field_resolve($2, $cron_spec->{'strings'}); $skip = $3; } if ($start > $end) { clean_up and die RED "[ERROR] Error in field syntax. Ranges should be -: $field\n" } } if ($start < $cron_spec->{'min'}) { clean_up and die RED "[ERROR] $start is below minimum value for field in: $field\n"; } if ($end > $cron_spec->{'max'}) { clean_up and die RED "[ERROR] $end is above maximum value for field in: $field\n"; } if ($cron_spec->{'shift'}) { $start += $cron_spec->{'shift'}; $end += $cron_spec->{'shift'}; } for (my $j = $start; $j <= $end; $j += $skip) { if (($j == $end) && $cron_spec->{'wrap'} && ($j == $cron_spec->{'max'})) { $mask_fields[$cron_spec->{'min'}] = 1; last; } $mask_fields[$j] = 1; } $mask[$i] = \@mask_fields; } } return \@mask; } sub cron_mask_match { my @time = @{shift()}; my @mask = @{shift()}; $debug && debug(15, join(' - ', map { join(',', @{$_}) } @mask), join(',', @time)); for (my $i = 0; $i < scalar(@mask); $i++) { if (!$mask[$i]->[$time[$cron_specs[$i]->{'localtime_field'}]]) { $debug && debug(16); return 0; } } $debug && debug(17); return 1; } sub debug { if (!defined($_[1])) { $_[1] = 'foo'; } if (!defined($_[2])) { $_[2] = 'foo'; } my (%debug_msg); $debug_msg{'0'}{'msg'} = "[CONF] reading config file $_[1]\n"; $debug_msg{'0'}{'col'} = CYAN; $debug_msg{'1'}{'msg'} = "[CONF] parsing conf directive - $_[1]\n"; $debug_msg{'1'}{'col'} = CYAN; $debug_msg{'2'}{'msg'} = "[CONF] configuration file $_[1] successfully parsed\n"; $debug_msg{'2'}{'col'} = WHITE; $debug_msg{'3'}{'msg'} = "[INIT] entering tail loop\n"; $debug_msg{'3'}{'col'} = BLUE; $debug_msg{'4'}{'msg'} = "[INIT] saving pid $$ in $pid_file\n"; $debug_msg{'4'}{'col'} = MAGENTA; $debug_msg{'5'}{'msg'} = "[MAIN] trapped $_[1] signal!\n"; $debug_msg{'5'}{'col'} = RED; $debug_msg{'6'}{'msg'} = "[MAIN] got message: $_[1]\n"; $debug_msg{'6'}{'col'} = WHITE; $debug_msg{'7'}{'msg'} = "[MAIN] matched message for queue $_[1]: $_[2]\n"; $debug_msg{'7'}{'col'} = GREEN; $debug_msg{'8'}{'msg'} = "[MAIN] masked message: $_[1]\n"; $debug_msg{'8'}{'col'} = RED; $debug_msg{'9'}{'msg'} = "[MAIN] skipping to regex: $_[1] after failed match for group regex on line: $_[2]\n"; $debug_msg{'9'}{'col'} = YELLOW; $debug_msg{'11'}{'msg'} = "[QUEUE] flushing queue $_[1]\n"; $debug_msg{'11'}{'col'} = RED; $debug_msg{'12'}{'msg'} = "[QUEUE] checking queues\n"; $debug_msg{'12'}{'col'} = CYAN; $debug_msg{'13'}{'msg'} = "[QUEUE] flushing all queues\n"; $debug_msg{'13'}{'col'} = RED; $debug_msg{'14'}{'msg'} = "[CRON] creating cron mask from: $_[1]\n"; $debug_msg{'14'}{'col'} = GREEN; $debug_msg{'15'}{'msg'} = "[CRON] testing mask: $_[1] against current time: $_[2]\n"; $debug_msg{'15'}{'col'} = GREEN; $debug_msg{'16'}{'msg'} = "[CRON] test returned negative\n"; $debug_msg{'16'}{'col'} = GREEN; $debug_msg{'17'}{'msg'} = "[CRON] test returned positive\n"; $debug_msg{'17'}{'col'} = GREEN; $debug_msg{'18'}{'msg'} = "[REGEX] Set regex: $_[1] to: $_[2]\n"; $debug_msg{'18'}{'col'} = YELLOW; $debug_msg{'19'}{'msg'} = "[FILE] opening $_[1]\n"; $debug_msg{'19'}{'col'} = BLUE; $debug_msg{'20'}{'msg'} = "[EXEC] executing $_[1]\n"; $debug_msg{'20'}{'col'} = CYAN; $debug_msg{'21'}{'msg'} = "[EXEC] killing child processes [pids: $_[1]]\n"; $debug_msg{'21'}{'col'} = CYAN; $debug_msg{'100'}{'msg'} = "[ERROR] Tried to change a protected setting: $_[1]. Please restart tenshi for this change to take effect\n"; $debug_msg{'100'}{'col'} = RED; print $debug_msg{$_[0]}{'col'}, $debug_msg{$_[0]}{'msg'}, RESET; } sub usage { die "tenshi $version http://dev.inversepath.com/tenshi || Copyright 2004-2011 Andrea Barisani || \n Usage: $0 [-c ] [-C|-f|-p] [-d ] [-P ] -c configuration file -C test configuration syntax -d debug level -f foreground mode -p profile mode -P pid file -h this help\n\n"; } # vim: set ts=4 sw=4 expandtab: tenshi-0.13/tenshi.gentoo-init0000644000175000017500000000175411607400554015437 0ustar lcarsusers#!/sbin/runscript opts="depend start stop reload flush checkconfig" depend() { need clock hostname logger } checkconfig() { /usr/sbin/tenshi -C -c /etc/tenshi/tenshi.conf return $? } start() { ebegin "Starting tenshi" start-stop-daemon --start --quiet --exec /usr/sbin/tenshi -- -c /etc/tenshi/tenshi.conf -P /var/lib/tenshi/tenshi.pid eend $? } stop() { ebegin "Stopping tenshi" start-stop-daemon --stop --quiet --pidfile /var/lib/tenshi/tenshi.pid eend $? } reload() { if [ ! -f /var/lib/tenshi/tenshi.pid ]; then eerror "tenshi isn't running" return 1 fi if checkconfig; then ebegin "Reloading configuration and flushing all queues" kill -HUP `cat /var/lib/tenshi/tenshi.pid` &>/dev/null eend $? else eerror "tenshi config file has syntax error, not restarting" fi } flush() { if [ ! -f /var/lib/tenshi/tenshi.pid ]; then eerror "tenshi isn't running" return 1 fi ebegin "Flushing all queues" kill -USR2 `cat /var/lib/tenshi/tenshi.pid` &>/dev/null eend $? } tenshi-0.13/INSTALL0000644000175000017500000000135011607400554013010 0ustar lcarsusers To install tenshi: Add a user and group named tenshi. As root: $ make install Edit the config in /etc/tenshi/tenshi.conf Please Read The Fine Manual once through before running tenshi to make sure you understand whats going on and that you satisfy the REQUIREMENTS section. Copy the tenshi.-init to wherever your init scripts live. Currently we only have init scripts for Gentoo, Debian and Solaris but we would be very happy to accept init scripts for other operating systems. Add tenshi to whatever run levels you need it to run in. To start run as root: $ /etc/init.d/tenshi start (or whatever is appropriate for your OS, again, comments welcome :) ) Thats it! tenshi should now email you reports as per your configuration. tenshi-0.13/tenshi.redhat-init0000644000175000017500000000320711607400554015406 0ustar lcarsusers#!/bin/sh # # tenshi Start tenshi log monitor # # chkconfig: 2345 08 92 # description: Starts, stops tenshi log monitor daemon # # config: /etc/tenshi/tenshi.conf # Source function library. . /etc/init.d/functions TENSHI=tenshi VAR_SUBSYS_TENSHI=/var/lock/subsys/$TENSHI start() { echo -n $"Starting tenshi: " daemon /usr/sbin/tenshi -c /etc/tenshi/tenshi.conf -P /var/run/tenshi/tenshi.pid RETVAL=$? echo [ $RETVAL -eq 0 ] && touch $VAR_SUBSYS_TENSHI return $RETVAL } stop() { echo -n $"Stopping tenshi: " killproc tenshi RETVAL=$? echo [ $RETVAL -eq 0 ] && rm -f $VAR_SUBSYS_TENSHI return $RETVAL } status() { if [ ! -f "$VAR_SUBSYS_TENSHI" ]; then echo $"tenshi is stopped." return 1 fi echo "Running: `cat /var/run/tenshi/tenshi.pid`" return $RETVAL } reload() { status RETVAL=$? if [ $RETVAL -eq 0 ]; then echo -n $"Reloading tenshi: " kill -HUP `cat /var/run/tenshi/tenshi.pid` &>/dev/null RETVAL=$? echo [ $RETVAL -eq 0 ] return $RETVAL else return $RETVAL fi } flush() { echo -n $"Flushing Queues: " kill -USR2 `cat /var/run/tenshi/tenshi.pid` &>/dev/null RETVAL=$? echo [ $RETVAL -eq 0 ] return $RETVAL } restart() { stop start } case "$1" in start) start ;; stop) stop ;; restart) restart ;; reload) reload ;; status) status ;; *) echo $"Usage: $0 {start|stop|restart|reload|flush|status}" exit 1 ;; esac exit $? tenshi-0.13/tenshi.redhat-spec0000644000175000017500000001323711607400554015401 0ustar lcarsusers#-------------------------------------------------------------------------------- # Program: tenshi.spec # # Purpose: This is the data file user to generate RPM files so that we can # distrbute 'canned' versions of what we have done more easily. #-------------------------------------------------------------------------------- # 10-Nov-06 - REP - Initial version #-------------------------------------------------------------------------------- # Some basic definitions for use to use later in the file. We really only want # to define things once, and have to change things in only one place. #-------------------------------------------------------------------------------- %define name tenshi %define version 0.13 %define release 1 %define mandir /usr/share/man %define sbindir /usr/sbin #-------------------------------------------------------------------------------- # Basic package information #-------------------------------------------------------------------------------- Summary: tenshi log monitoring program Name: %{name} Version: %{version} Release: %{release} Group: System Environment/Daemons License: ISC-style Url: http://www.inversepath.com/tenshi.html Source0: %{name}-%{version}.tar.gz Requires: perl Buildroot: %{_tmppath}/%{name}-buildroot #-------------------------------------------------------------------------------- # Description of the package #-------------------------------------------------------------------------------- %description tenshi is a log monitoring program, designed to watch one or more log files for lines matching user defined regular expressions and report on the matches. The regular expressions are assigned to queues which have an alert interval and a list of mail recipients. #-------------------------------------------------------------------------------- # What things to do in preparation of making the package #-------------------------------------------------------------------------------- %prep %setup #-------------------------------------------------------------------------------- # The build process for the package #-------------------------------------------------------------------------------- %build #-------------------------------------------------------------------------------- # Configuration process for the package #-------------------------------------------------------------------------------- ### No configure process for tenshi #-------------------------------------------------------------------------------- # The install process for the package #-------------------------------------------------------------------------------- %install [ -n "%{buildroot}" -a "%{buildroot}" != / ] && rm -rf %{buildroot} mkdir -p %{buildroot}%{_initrddir} mkdir -p %{buildroot}%{_sysconfdir}/%{name} mkdir -p %{buildroot}/etc/sysconfig mkdir -p %{buildroot}/usr/share/man/man8/ mkdir -p %{buildroot}/var/run/%{name}/ make DESTDIR=%{buildroot} mandir=%{_mandir} install install -m755 tenshi.redhat-init %{buildroot}%{_initrddir}/%{name} touch %{buildroot}/etc/sysconfig/%{name} touch %{buildroot}/var/run/%{name}/%{name}.pid #-------------------------------------------------------------------------------- # Things to run after it has been installed. #-------------------------------------------------------------------------------- %post /sbin/chkconfig --add %{name} # Manually add user/group %{sbindir}/groupadd %{name} %{sbindir}/useradd -g %{name} -d %{_sysconfdir}/%{name} -s /bin/false %{name} #-------------------------------------------------------------------------------- # Take tenshi out of runlevels #-------------------------------------------------------------------------------- %preun if [ "$1" = 0 ]; then /sbin/chkconfig --del %{name} fi #-------------------------------------------------------------------------------- # Remove tenshi user/group if necessary (since "tenshi" is the only # member of group "tenshi", then deletion of "tenshi" user deletes # the group. #-------------------------------------------------------------------------------- %postun if [ "$1" = 0 ]; then %{sbindir}/userdel %{name} %{sbindir}/groupdel %{name} fi #-------------------------------------------------------------------------------- # What files and permissions are included in the package #-------------------------------------------------------------------------------- %files %defattr(644,root,root,755) %doc README INSTALL CREDITS LICENSE Changelog FAQ %config(noreplace) %{_sysconfdir}/%{name}/%{name}.conf %config(noreplace) /etc/sysconfig/%{name} %attr(755,root,root) %config(noreplace) %{_initrddir}/%{name} %attr(755,root,root) %{sbindir}/%{name} %{_mandir}/man8/%{name}.8* %dir %attr(775,tenshi,tenshi) /var/run/%{name} %ghost %attr(600,tenshi,tenshi) /var/run/%{name}/%{name}.pid #-------------------------------------------------------------------------------- # What final cleanup should occur after the package construction has been # completed #-------------------------------------------------------------------------------- %clean [ -n "%{buildroot}" -a "%{buildroot}" != / ] && rm -rf %{buildroot} #-------------------------------------------------------------------------------- # Changelog #-------------------------------------------------------------------------------- %changelog * Mon Aug 27 2007 Andrea Barisani 0.8 - assorted fixes, thanks goes to Elan Ruusamäe * Fri Nov 10 2006 Steven McCoy, Jr. 0.6-1 - Initial specfile/rpm creation. Used syslog-ng.spec as template - Thanks to: Richard E. Perlotto II (syslog-ng spec maintainer) tenshi-0.13/Changelog0000644000175000017500000001503111607400554013572 0ustar lcarsusers0.13 (20110713) - added threshold feature for discarding logs with count below specified value - fixed broken queue flushing on exit - delay daemonization to catch start up errors - make sure terminal colour is always reset - improved clean up on exit all changes in this release thanks to Brian De Wolf 0.12 (20100917) - open FIFO in rw mode, fixes bug that causes CPU spinning and processing interruption Michael Braun - redhat rpm spec fixes + opensuse rpm spec and init script Heinrich Terhardt 0.11 (20090719) - change process name to tenshi when in daemon mode - timezone is no longer cached, preventing incorrect timestamps on DST change - allow leftmost queue escalation, warn on unreadable log files instead of exiting Heath Caldwell - added resolve option Brian De Wolf 0.10 (20080313) - includedir now skips dotfiles - pid file is now created in debug, profile and foreground modes - fixed incorrect uid check not handling root user - fixed buggy time_to_die - fixed buggy tail_multiple option parsing - fixed empty report sending if filter program returns no output - fixed bug on unmatched subpatterns Adam James - die before pipe if tail_file is not readable, prevents fd leak - replaced IO::Select with IO::BufferedSelect, fixes parsing delay of buffered lines 0.9.1 (20071004) - improved debug messages - moved CHLD signal handler earlier in the code - fixed buggy multiple tail processes handling Paul B. Henson 0.9 (20070919) - added tail_multiple option - added csv option for comma separated output to custom parser - merged tailargs and filterargs features in tail, filter ones - added FAQ file - another round of rpm spec fixes + fd leak fix Elan Ruusamäe 0.8 (20070819) - fixed some rpm spec issues - added support for queue escalation Heath Caldwell 0.7 (20070314) - fifo option can be specified multiple times - logfile and fifo mode can be simultaneous - better pipe opening method for the tail process Paul B. Henson - includedir now sorts entries alphabetically Andreas Niederl - stdin is now closed as soon as possible - replaced alarm based timeout with IO::Select - minor fix of getpwnam checks - added sort_order option - added listen option for syslog server mode Daniele Cortesi - added RedHat init file and rpm spec from Steven McCoy 0.6 (20060928) - added group_host feature for selective hostname matching 0.5.1 (20060618) - fixed buggy noprefix queue implementation 0.5 (20060609) - improved sanity checks - added debug levels, level 2 enables Net::SMTP debug messages - replaced Getopt::Std with Getopt::Long - filter feature is now per queue definable - added special noprefix queue for unprefixed logs - added supplementary groups clearing when dropping privileges - moved to ISC-style license 0.4 (20060104) - added uid, gid options for setting effective UID, GID - added colourization of debug messages - added filter, filterargs options for report pre-processing - added [pager:] syntax for sending pager friendly reports - added mailtimeout option - fixed tail, tailargs incorrectly being treated as dynamic options 0.3.4 (20060616) - added include, includedir options for parsing external configuration files - added multiple queues feature - fixed missing USR1 signal handler - added filetest pragma for proper perms check when using POSIX ACL - added debian init file 0.3.3 (20050318) - fixed whitespaces only lines handling in configuration file - added configuration check mode and checkconfig target in init scripts 0.3.2 (20041109) - added X-tenshi-version, X-tenshi-hostname, X-tenshi-report-start headers - fixed orphan tail process problem, filehandle is now closed on shutdown - fixed bug in cron specs handling - LC_TIME locale is internally set to "C" now for compliant Date header - mail address specification allows local part only - added foreground mode 0.3.1 (20040721) - added tailargs option - fixed buggy logprefix implementation - fixed buggy cron */skip implementation - fixed broken cron specs examples 0.3 (20040715) - changed name due to trademark issues - added skip groups to speed parsing - added cron-style specs instead of time periods - added ability to send to multiple recipients, suggested by Marc Doumayrou - added solaris init file from Marc Doumayrou - added logprefix option to handle non-syslog logs - added status messages - added log files perms check - fixed strftime portability issue Andrew Eason 0.2.1 (20040622) - fixed missing headers in smtp code - added hidepid feature - replaced expand_repeats with builtin repeat queue 0.2 (20040621) - added multiple file support - big performance improvements: BerkeleyDB and Storable are no longer required - better signal handling including configuration reload, see tenshi.8 SIGNALS section - replaced mda option with mailserver, now reports are handled by Net::SMTP - added per-queue report subjects - added expand_repeats option to handle "last message repeated x times" nicely - added profiling mode - mask option without argument now allows an empty mask - pidfile is removed on shutdown - die if our child process dies - removed prescan option 0.1 (20040604) - first public release tenshi-0.13/tenshi.solaris-init0000644000175000017500000000257311607400554015620 0ustar lcarsusers#!/bin/sh # # tenshi startup script # # Author: Marc Doumayrou # Tested on Solaris 8 # tenshiconf=/usr/local/etc/tenshi.conf [ -f /usr/local/bin/tenshi ] || exit 0 # See how we were called. case "$1" in start) # Start daemons. # check if tenshi is running or not if [ -r /var/run/tenshi.pid ] ; then echo "There are one or more tenshi instances running, please stop them manually"; else echo "Starting tenshi using $tenshiconf " /usr/local/bin/tenshi -c $tenshiconf sleep 2 echo "forked at Pid `cat /var/run/tenshi.pid`" echo "done." ; fi ;; stop) # Stop daemons. # checking if thereis pid available [ -f /var/run/tenshi.pid ] || exit 0 echo "Shutting down tenshi... " kill `cat /var/run/tenshi.pid` echo "done." ;; restart) $0 stop $0 start ;; status) if [ -r /var/run/tenshi.pid ] ; then echo "tenshi is currently forked at Pid `cat /var/run/tenshi.pid`" else echo "tenshi is not running!" ; fi ;; *) echo "Usage: $0 {start|stop|restart|status}" exit 1 esac exit 0 tenshi-0.13/CREDITS0000644000175000017500000000055011607400554013000 0ustar lcarsusers Author and maintainer: Andrea Barisani Additional thanks goes to: Brian De Wolf Robert Coie The Gentoo Infrastructure Team lwithers@users.sf.net Daniele Cortesi Paul B. Henson Heath Caldwell Rob Holland