#!/usr/bin/perl # # We want to avoid 'use lib' if at all possible.. so, it's 1 huge file. :-( # # # # package ReportEntry; use strict; sub new { my($this,$conf,$rdr) = @_; $this = ref($this) || $this; my $self = bless({},$this); $self->{_CONF} = $conf; $self->parse($rdr); return($self); } sub parse { my($self,$rdr) = (shift,shift); while( my $line = $rdr->getline()){ my($k,$v,@w) = $rdr->words($line); if(scalar(@w)){ warn "$line trailing characters ignored: " . join(" ",@w); } $self->{$k} = $v; } } sub validate { 1; } sub description { return($_[0]->{description}); } sub mime_file { return($_[0]->{mime_file}); } sub run { } sub finish { } sub encrypted { 1; } sub encrypt_cmd { my($self) = shift; return($self->{pgp} || $self->conf()->get("pgp") ); } sub conf { return($_[0]->{_CONF}); } sub hostname { my($self) = shift; return($self->{hostname} || $self->conf()->hostname()); } sub debug { my($self) = shift; # Produce volumes of output. if($self->conf()->get('debug')){ print STDERR "dbg:",ref($self),":",@_,"\n"; } } 1; package Util::Pattern; use strict; # Probably silly to use objects here, but require Exporter # and friends don't work in this non-conventional way, where # we avoid using separate files... sub new { bless({},shift()); } sub build_pattern { my($self) = shift; my(@tests); while( my $c = shift() ){ push(@tests,"(\$_ $c->[0] $c->[1])"); } my $perl = 'sub {' . join('&',@tests) . '}'; my($code); eval { local($_) = undef; $code = eval($perl); $code->(); # Make sure it runs. }; if($@){ die $@; } return($code); } sub stacked_pattern { my($self,$rdr) = (shift,shift); my(@stack); while($_ = $rdr->getline()){ my($op,$regex) = split(/\s/,$_,2); $op =~ s/^\s+//g; if($op ne '#'){ push(@stack,[$op,$regex]); } } my $code = $self->build_pattern(@stack); return($code); } 1; package ReportEntry::FileWatch; use IO::Handle; use IPC::Open2; use File::Find; our(@ISA) = qw(ReportEntry); sub parse { my($self,$rdr) = (shift,shift); require Digest::MD5; my($ut) = new Util::Pattern(); my(@ignore,@watch); while( my $line = $rdr->getline()){ my($k,@v) = $rdr->words($line); if($k eq 'dir'){ if(! $self->{_Dirs}){ $self->{_Dirs} = []; } push(@{$self->{_Dirs}},@v); next; } if($k eq 'do'){ $self->debug("Found do ... section, parsing.."); my $code = $ut->stacked_pattern($rdr->section_reader('end')); $k = shift(@v); if($k eq 'ignore'){ $self->debug("it's do ignore"); push(@ignore,$code); next; } if($k eq 'filter'){ $self->debug("it's do filter"); push(@watch,$code); next; } warn "Unrecognized do '$k'"; } if($k eq 'ignore'){ $self->debug("Found an ignore chunk"); my($cmd,$op,$pat) = split(/\s+/,$line,3); push(@ignore,$ut->build_pattern([$op,$pat])); next; } if($k eq 'filter'){ $self->debug("Found filter chunk"); my($cmd,$op,$pat) = split(/\s+/,$line,3); push(@watch,$ut->build_pattern([$op,$pat])); next; } $self->{$k} = $v[0]; } $self->{_Ignore} = \@ignore; if(scalar(@watch)){ $self->{_Watch} = \@watch; } } sub validate { my($self) = shift; $self->{_Dirs} || die "I need at least 1 dir parameter"; } sub run { my($self,$pout) = @_; my($out) = new IO::Handle(); my($in) = new IO::Handle(); my($enc) = $self->encrypt_cmd(); open2($in, $out, $enc) || die "Can't run encryption program"; $out->print("\n"); my $wanted = sub { local($_); my($name) = $File::Find::name; if(-f $name){ # It's an ignore. foreach my $code (@{$self->{_Ignore}}){ $_ = $name; # In case a sub were to modify it. if($code->()){ $self->debug("$name is on ignore list, skip"); return; } } # Watching only certain ones? if($self->{_Watch}){ $self->debug("We have a filter list. Testing $name"); foreach my $code (@{$self->{_Watch}}){ $_ = $name; # In case a sub were to modify it. if(! $code->()){ $self->debug("Name doesn't match patterns we want, skipping"); return; } } } $self->debug("Sending $name to client"); my(@rec) = $self->_make_record($out,$name); $out->print(join("\t",@rec) . "\n"); } }; # Run find. $out->print("begin\t" . $self->hostname() . "\n"); find($wanted,@{$self->{_Dirs}}); $out->print("end\t" . $self->hostname() . "\n"); $out->close(); # Read encrypted data. while($_ = $in->getline()){ $pout->print($_); } } sub _make_record { my($self,$handle,$name) = @_; my($dig) = Digest::MD5->new(); my($h) = new IO::File($name) || return('warning',$name,$!); if($h){ $dig->addfile($h); } my(@stat) = stat($name); my $key = $dig->b64digest(); return("b64digest",$key,$stat[7],$name); } 1; package ReportEntry::GrogLog; use IO::File; use IO::Handle; our(@ISA) = qw(ReportEntry); use IPC::Open2; sub parse { my($self,$rdr) = (shift,shift); my($ut) = new Util::Pattern(); my($line,@ignore,@watch); while( $line = $rdr->getline()){ my($cmd,@arg) = $rdr->words($line); # It's a stacked command. if($cmd eq 'do'){ $cmd = shift(@arg); $self->debug("We have a do list: do $cmd .. reading till end"); my $code = $ut->stacked_pattern($rdr->section_reader('end')); if($cmd eq 'ignore'){ push(@ignore,$code); } # Not recommended.. if($cmd eq 'filter'){ push(@watch,$code); } next; } # It's not nested. if($cmd eq 'ignore'){ my($cmd,$op,$pat) = split(/\s+/,$line,3); $self->debug("ignore pattern $op $pat"); push(@ignore,$ut->build_pattern([$op,$pat])); next; } # Not recommended. if($cmd eq 'filter'){ my($cmd,$op,$pat) = split(/\s+/,$line,3); $self->debug("filter pattern $op $pat"); push(@watch,$ut->build_pattern([$op,$pat])); next; } # Otherwise, it's just a setting. $self->{$cmd} = shift(@arg); } $self->{_Ignore} = \@ignore; if(scalar(@watch)){ $self->{_Filter} = \@watch; } } sub validate { my($self) = shift; $self->{logfile} || die "Missing logile parameter"; } sub run { my($self,$pout) = (shift,shift); my($fn) = $self->{logfile}; $self->debug("Looking at log: $fn"); my($out) = new IO::Handle(); my($in) = new IO::Handle(); my($enc) = $self->encrypt_cmd(); # We're talking to a SMTP server with dataend. Could use it as # a file handle, but I've been burnt by Net::Cmd in the past doing that. open2($in, $out,$enc) || die "Can't run encryption program"; # For some reason... GPG seems to need this? (Maybe pipe issue) $out->print("\n"); my($lh) = new IO::File($fn) || die "$fn:$!"; my(@ignore) = @{$self->{_Ignore}}; my(@filter); if($self->{_Filter}){ @filter = @{$self->{_Filter}}; } GROG: while(my $line = $lh->getline()){ chomp($line); if($self->{trim} eq 'yes'){ $line =~ s/^\s+//; $line =~ s/\s+$//; } $self->debug("Looking at ignore list"); # First skip through the ignore's. foreach my $code (@ignore){ $_ = $line; # We do this because a sub/pattern might alter $_! if($code->()){ $self->debug("$_ is ignored"); next GROG; } } # Then, if there are any filters.. if(scalar(@filter)){ $self->debug("We have a watch list"); foreach my $code (@filter){ $_ = $line; if($code->()){ $out->print($line,"\n"); last; }else{ $self->debug("Line $line is NOT on watch, ignoring"); } } next GROG; } # If we get this far, we print it. $out->print($line,"\n"); } $out->close(); $self->debug("Encrypting"); while($_ = $in->getline()){ $pout->print($_); } } 1; package ReportEntry::Cmd; our(@ISA) = qw(ReportEntry); sub run { my($self,$h) = (shift,shift); my($cmd) = $self->{'exec'} || die "Missing command parameter"; my($enc) = $self->encrypt_cmd(); $self->debug("Running '$cmd 2>&1 |$enc 2>&1 |' through the shell"); open(CMD,"$cmd 2>&1 |$enc 2>&1 |"); while(){ $h->print($_); } close(CMD); } sub validate { my($self) = shift; if(! $self->{'exec'}){ die "Missing 'exec' setting for " . $self->description(); } 1; } sub encrypted { 1; } 1; package InnerMessage; use strict; sub new { my($this,$smtp) = (shift,shift); # Get the boundary from a memory reference. my($id) = []; $id =~ /\((.+)\)/; my($self) = { SMTP => $smtp, BOUNDARY => $1 }; return(bless($self,$this)); } sub start { my($self,$runner) = (shift,shift); if($runner->encrypted()){ $self->start_encrypt($runner); }else{ $self->start_plain($runner); } } sub start_plain { my($self,$runner) = (shift,shift); my($smtp) = $self->{SMTP}; my($diz) = $runner->description(); my($rfile) = $runner->mime_file(); $smtp->datasend("Content-Disposition: attachement; filename=\"$rfile\"\n"); $smtp->datasend("Content-Description: $diz\n"); $smtp->datasend("Content-Type: text/plain\n"); $smtp->datasend("\n"); } # Begin a multipart-multipart message. # I don't know if this is correct.. probably isn't, but it works. sub start_encrypt { my($self,$runner) = (shift,shift); my($smtp) = $self->{SMTP}; my($diz) = $runner->description(); my($rfile) = $runner->mime_file(); $smtp->datasend("Content-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\";"); $smtp->datasend("\tboundary=\"$self->{BOUNDARY}\"\n"); $smtp->datasend("Content-Disposition: attachment; filename=\"$rfile\"\n"); $smtp->datasend("Content-Description: $diz\n"); # The first Version: attachment. $smtp->datasend("\n--" . $self->{BOUNDARY} . "\n"); $smtp->datasend("Content-Type: application/pgp-encrypted\n"); $smtp->datasend("Content-Disposition: attachment\n\n"); $smtp->datasend("Version: 1\n\n"); $smtp->datasend("--" . $self->{BOUNDARY} . "\n"); $smtp->datasend("Content-Type: application/octet-stream\n"); $smtp->datasend("Content-Disposition: inline; filename=\"msg.asc\"\n\n"); $self->{_finish_enc} = 1; } sub print { my($self) = shift; $self->{SMTP}->datasend(join('',@_)); } sub finish { my($self) = shift; my($smtp) = $self->{SMTP}; if($self->{_finish_enc}){ $smtp->datasend("\n--" . $self->{BOUNDARY} . "--\n"); }else{ $smtp->datasend("\n"); } } 1; package MyCheckedSMTP; use Net::SMTP; our(@ISA) = qw(Net::SMTP); #This is a wrapper around Net::SMTP, it dies() when ok() doesn't #return .T., easier than if(! $smtp->ok() ... ) sub datasend { return(shift()->_checked('datasend',@_)); } sub data { return(shift()->_checked('data',@_)); } sub dataend { return(shift()->_checked('dataend',@_)); } sub to { return(shift()->_checked('to',@_)); } sub mail { return(shift()->_checked('mail',@_)); } sub quit { return(shift()->_checked('quit',@_)); } sub _checked { my($self,$meth) = (shift,shift); $meth = "SUPER::$meth"; my(@rv); if(wantarray()){ @rv = $self->$meth(@_); }else{ $rv[0] = $self->$meth(@_); } if(! $self->ok()){ die "SMTP error: " . $self->code() . ":" . $self->message(); } if(wantarray()){ return(@rv); }else{ return($rv[0]); } } 1; package ConfReader; use strict; use IO::File; use Text::ParseWords; use Sys::Hostname; # These are the modules we know about. our(%KNOWN_MODULES) = ( 'ReportEntry::Cmd' => 'command', 'ReportEntry::GrogLog' => 'watch', 'ReportEntry::FileWatch' => 'monitor' ); sub new { my($this) = (shift); $this = ref($this) || $this; # Kind of silly, we're not going to override it... my($self) = {}; # List of Report runners. $self->{Runners} = []; # Load in our aliases. foreach $_ (keys(%KNOWN_MODULES)){ my $alias = $KNOWN_MODULES{$_}; $self->{_Alias}{$alias} = $_; } return(bless($self,$this)); } # Parse a handle. sub parse { my($self,$io,$filename) = (shift,shift,shift); if($self->{_seen}{$filename}){ die "Recursive sourcing not permitted: $filename"; } $self->{_seen}{$filename} = 1; $filename ||= "Unknown"; my $line; while( $line = $io->getline()){ ($line,undef) = split(/\#/,$line,2); chomp($line); $line =~ s/^\s+//; if(! length($line)){ next; } my(@cmd) = shellwords($line); my $cmd = shift(@cmd); if(! $cmd){ next; } # Something to ignore. if($cmd =~ /^\<<(.+)/){ $self->do_ignore($io,$filename,$1); next; } # Can *we* handle it? my($imeth) = 'cfg_' . $cmd; if($self->can($imeth)){ $self->$imeth(@cmd); next; }else{ # Nope, it's a module. $self->do_section($io,$filename,$cmd,@cmd); } } } sub get { my($self,$prop) = (shift,shift); return($self->{_Set}{$prop}); } sub hostname { my($self) = shift; return($self->{_Set}{hostname} || Sys::Hostname::hostname()); } sub cfg_source { my($self,$file) = (shift,shift); my($io) = new IO::File($file) || die "$file:$!"; $self->parse($io,$file); } sub get_runners { return(@{$_[0]->{Runners}}); } sub do_section { my($self,$io,$filename,$name,@args) = @_; # Find the package name, (Perhaps an alias?) my $package = $KNOWN_MODULES{$name}?$name:$self->{_Alias}{$name}; if(! $package){ warn "$name unknown"; return; } my($obj); my($delimit) = pop(@args); if($delimit =~ /\<\<(\w+)/){ $delimit = $1; my($rdr) = $self->section_reader($io,$delimit,$filename); eval { $obj = $package->new($self,$rdr,@args); }; if($@){ $rdr->error($@); } $rdr->skip(); }else{ # The module doesn't have a <<__HERE doc. $obj = $package->new($self,@args,$delimit); } if(! $obj->validate()){ warn "Configuration failty $filename: $."; } push(@{$self->{Runners}},$obj); } sub do_ignore { my($self,$io,$name,$del) = @_; my($rdr) = $self->section_reader($io,$del,$name); $rdr->interpret(0); $rdr->skip(); } # Read a chunk ($handle, $delimit, $name) sub section_reader { { package ConfReader::Handle; use strict; use Text::ParseWords; use IO::File; sub new { my($this,$io,$delimit,$name) = (shift,shift,shift,shift); $this = ref($this) || $this; return(bless( {_interpret => 1, Handle => $io, Delimit => $delimit, Name => $name}, $this)); } # Get reader within reader, within a reader .... sub section_reader { my($self,$del) = (shift,shift); return(new ConfReader::Handle($self,$del)); } sub error { my($self,$msg) = (shift,shift); die $self->{Name} . "\[$.\]:" . $msg; } sub getline { my($self) = shift; if($self->{_Chain}){ my($chain) = $self->{_Chain}; my $rv = $chain->getline(); if(! $rv){ delete($self->{_Chain}); }else{ return($rv); } } return($self->_getline()); } sub _getline { my($self) = shift; if($self->{_finished}){ return(); } my($line); my($del) = $self->{Delimit}; # Note: Handle may be some day be another instance of *us*, so don't use # any IO::Handle stuff. (if possible) just use getline. while($line = $self->{Handle}->getline()){ # End of file? (Well, sort of... remember, we could be chained, delimiters don't apply then.) if($del){ if($line =~ /^\s*$del$/){ $self->{_finished} = 1; return(); } } # Strip comments, interpret lines? if($self->interpret()){ chomp($line); ($line,undef) = split(/\#/,$line); $line =~ s/^\s+//; $line =~ s/\s+$//; if(! length($line)){ next; } if($line =~ /^source\s(.+)/){ my($filename) = $1; my($io) = new IO::File($filename) || die "$filename:$!"; my($nself) = new ConfReader::Handle($io,undef(),$filename); $self->{_Chain} = $nself; return($nself->getline()); } } return($line); } } sub skip { my($self) = shift; if($self->{_finished}){ return(); } while($self->getline()){ } if($self->{Delimit}){ if(! $self->{_finished}){ die "Can't find terminator: $self->{Delimit} anywhere"; } }else{ $self->{_finished} = 1; } } sub words { my($self,$line) = (shift,shift); if($line){ return(shellwords($line)); } return(); } sub interpret { (@_ == 1) && return($_[0]->{_interpret}); my $rv; ($rv,$_[0]->{_interpret}) = ($_[0]->{_interpret},$_[1]); return($rv); } 1; } my($self,$handle,$del,$filename) = (shift,shift,shift,shift); my($rdr) = new ConfReader::Handle($handle,$del,$filename); } sub cfg_module { my($self,$module,$alias,$file) = @_; if($file){ require($file); } # Aliases are easier to type. if($alias){ $self->{_Alias}{$alias} = $module; } # Mark that we know about it. $KNOWN_MODULES{$module} = 1; } sub cfg_set { my($self,$key,$val) = (shift,shift,shift); $self->{_Set}{$key} = $val; } 1; package main; use strict; use IO::File; use IO::Handle; use Net::Cmd qw(CMD_OK); use Sys::Hostname; use Getopt::Long; use vars qw($BOUNDARY); $BOUNDARY = time() . "A_Savrqf" . $$; exit(&main()); sub main { my($conf_file) = undef(); my($check) = 0; my($gen_conf) = 0; GetOptions( 'check:s' => \$check, 'conf|C:s' => \$conf_file, 'generate|g' => \$gen_conf ); if($check){ return(cmp_md5($check)); } if($gen_conf){ return(make_conf()); } if(! $conf_file){ return(usage()); } my($conf) = new ConfReader(); my($h) = new IO::File($conf_file) || die "$conf_file:$!"; $conf->parse($h,'conf.txt'); my $host = $conf->get('smtp'); my($smtp) = MyCheckedSMTP->new($host); if($conf->get('debug')){ $smtp->debug($conf->get('debug')); } $smtp->mail($conf->get('from')); $smtp->to($conf->get('to')); $smtp->data(); header($conf,$smtp); do_reports($conf,$smtp); # Outer boundary. $smtp->datasend("--" . $BOUNDARY . "--\n"); $smtp->quit(); return(0) if($smtp->ok()); return(1); } sub log_error { my($msg) = shift; warn $msg; } sub usage { print qq( $0 --conf /conf/file --generate --check /path/to/log This is a system monitoring program, options are: --generate will create a starting point configuration file. --check compares files being monitored agains a local log listing. --conf Need this option to run. Do perldoc $0 to find out more. ); return(1); } sub cmp_md5 { my($log) = shift; my($files); my($current_host,$found_end_marker); my($err) = 0; while(){ chomp; my($op,$md5,$size,$name) = split(/\t/,$_,4); if($op eq 'begin'){ $current_host = $md5; print "Checking files on $current_host\n"; $files = load_md5_log($current_host,$log); next; } if(! $current_host){ next; } if($op eq 'end'){ $found_end_marker = 1; undef($current_host); next; } if($op eq 'b64digest'){ my($rec) = $files->{$name}; if(! $rec){ print "new:$name\n"; next; } if($md5 ne $rec->{md5}){ $err = 1; print "md5dif:$_\n"; print "local: $rec->{md5}\t$rec->{size}\t$name\n"; next; } if($size ne $rec->{size}){ $err = 1; print "size:$_\n"; next; } } } if(! $found_end_marker){ $err = 2; print "error: end section not found\n"; } return($err); } sub load_md5_log { my($remote_host,$log) = (shift,shift); open(FH,$log) || die "$log:$!"; my(%files); while(){ chomp; my($op,$md5,$size,$name) = split(/\t/,$_,4); if($op eq 'b64digest'){ $files{$name} = { size => $size, md5 => $md5, }; } } close(FH); return(\%files); } sub do_reports { my($conf,$smtp) = (shift,shift); foreach my $run ($conf->get_runners()){ $smtp->datasend("--" . $BOUNDARY . "\n"); my($h) = new InnerMessage($smtp); $h->start($run); eval { $run->run($h); $run->finish(); }; if($@){ log_error($@); } $h->finish(); } } sub header { my($conf,$smtp) = (shift,shift); my($to,$from) = ($conf->get('to'),$conf->get('from')); my($subject) = $conf->get('subject'); my($hostname) = $conf->hostname(); if(! $subject){ $subject = "$hostname: Inspection"; } $smtp->datasend("To: $to"); $smtp->datasend("From: $from\n"); $smtp->datasend("Subject: $subject\n"); $smtp->datasend("MIME-Version: 1.0\n"); # Nice to be able to divide them. $smtp->datasend("X-Mon-Host: $hostname\n"); $smtp->datasend("Content-Type: multipart/report; boundary=\"$BOUNDARY\"\n\n"); $smtp->datasend("\n"); # First description line. $smtp->datasend("This is a system monitor program\n\n"); } sub make_conf { while(){ if(/__END__/){ last; } print; } return(0); } __DATA__ # This is a comment. <<__BLOCK This is also a comment. __BLOCK set to 'you@example.com' set from 'me@example.com' set smtp localhost set debug 0 set subject "Example subject" set pgp 'gpg -ear you@example.com --no-tty --batch' watch <<__WATCH description "My description" logfile "/var/log/messages" mime_file "attachment.txt" trim yes ignore =~ /Pattern/ do ignore =~ /this/ =~ /and/ =~ /that/ end __WATCH monitor <<__EOM description "files in /usr/sbin" dir /usr/sbin ignore =~ m|~$| mime_file "attachment.txt" __EOM command <<__EOC description "Directory listing" mime_file "ls.txt" exec "ls -l /tmp" __EOC __END__ =pod =head1 NAME logmail.pl -C conf_file.conf =head1 DESCRIPTION This monitors logfiles, run system commands from a configuration file. It uses GPG encryption, it may be used to securely monitor remote hosts. =over 4 =item --generate Generate a new config file, dump to standard output. =item -C "file.conf" Use configuration "file.conf" =item --conf="file.txt" Same as -C, this is required - It uses no default configuration. =item --check /prior/values.md5 Check monitored files against a local log. (Probably should use a more convenient program for that) =back =head1 PURPOSE I wrote it to monitor clients machines remotely, most existing scripts or programs I've seen don't offer encryption, which is absolutely required if you are going to transfer logs and other sensitive system information across email channels. It is also useful if you need to run cron processes that may output sensitive information. The second reason, as pointed out here: http://www.jammed.com/~jwa/hacks/security/checksyslog/checksyslog-doc.html Mr. Abendschan states: There are at least two ways to scan your syslogs for security problems or other errors. One is to grep for data you know is indicative of a problem .. "refused connect from", "REPEATED LOGIN FAILURES", "STOR .rhosts". This method requires you to generate huge lists of things you think are "suspicious behaviour" -- lists which are by their nature incomplete; you need to know that a problem exists in order to detect it. Most stuff I've seen involve grepping log files for key words, this package can do that too, however, it's really designed to grep out patterns it does B know about. The idea is to train it to ignore the usual stuff, but show the unusual. =head1 CONFIGURATION The configuration syntax is meant to be "shell like" so that it'll work with editors supporting "shell" syntax highlighting. Words need to be quoted if they have spaces, and __HERE documents are supported, similiar to shell script here documents. The general format is command argument "argument two" Some commands accept (require actually) sub-sections, these subsections are accomplished with __HERE documents. command <<__HERE Configuration local to "command" __HERE It looks rather ugly, I know.. but, if you've ever worked with __HERE docs, it's pretty clear that anything up-to __HERE is intended for 'command'. Comments are either hash marks or <<__HERE documents that point to no command. This way, you can easily comment out large chunks. # This is a comment. <<__EOC This is also a comment __EOC =head1 DEPENDANCIES Pretty much standard core modules, with a few exceptions: It uses Net::SMTP to send email. It uses Digest::MD5 for file monitoring. =head1 COMMANDS =over 4 =item source "filename" Source a config file. Useful if you want to include one configuration into another. Not useful if you're trying to diagnose a problem.. =item set key value Change a global setting. Settings are =over 4 =item pgp "gpg -a -e -r 'you@example.com' --no-tty --batch" The PGP program used for encryption. This must exist and slurp all it's input in before outputting anything. =item to 'you@example.com' Who email is sent to. =item from 'you@example.com' Who email is sent to. =item smtp "smtp.example.com" SMTP server for email. =item debug Turn debugging on/off =item subject Set the subject line. =item host Fake the hostname. (Not required) =back =item module Perl::Package alias /path/to/file.pm Load a perl module. The module can then be used as alias <<__HERE command command command __HERE TODO: write about Modules and the ReportEntry package. =back =head1 BUILTIN MODULES =over 4 =item command - Execute commands. Lets you execute commands and mail results. (Like cron, but with PGP) command <<__EOC exec 'ls -l' description "Show directory" mime_file "ls.txt" __EOC Options for command are: =over 4 =item exec "command name" ls -l =item mime_file "attachment_filename.txt" ls.txt will be the attachment filename, (Each command is a separate mime attachment) =item description "Mime description" Shows up as the Content-Description in the MIME data. =back =item monitor One of the problems with monitoring a checksum of your files is that if an attacker breaks in, they might have access to your database of checksums. They can then modify files and merely update the list of checksums to reflect their changes, making the whole thing a moot point. This is why you're supposed to keep the list on another server, but if you are working remotely, this can be a real hassle. (And sending a complete directory listing in the clear through email isn't much better) That is where this module comes in. This module allows you to monitor file stamps. It will create an MD5 digest of all files in the specified directories and email you a listing. The solution presented here is to email a PGP attachment of these checksums, then compare with a local known copy, looking for differences. =over 4 =item dir /patha /pathb /pathc Specify which directories to monitor. Only regular files will be checked. (It won't try to do an md5sum on a pipe) Multiple dir statements are possible, or, you can just put them all in one. dir /patha dir /pathb dir /pathc Is the same as dir /path1 /pathb /pathc The directory is searched recursively. =head2 example monitor <<__EOM mime_file "monitor.txt" description "MD5 sum of files" dir /etc dir /opt dir /usr/bin /usr/lib /usr/local /usr/sbin __EOM You can save the results and then compare them with future listings by running logmail.pl with the --check /path/to/good_listing.log =back The output format is b64digest$digest_keysizefilename warnError message about one of the files. =item watch - Watch a log file. This will scan log files, weeding out regex's that you're not interested in. =over 4 =item logfile "/path/to/logfile.txt" The file to watch. =item mime_file "local.txt" Attachment filename =item description "Description" The Content-Description field. =item ignore $op /Pattern/ This needs some explaining.. the ignore tells it which patterns to ignore, it's VERY important to note that /Pattern/ is actually perl code, and will be compiled into a subroutine. So, the syntax has to be valid perl syntax. $op can be =~ or !~ meaning "match" "don't match" example ignore =~ /MARK/ ignore =~ /Another/ Multiple ignore commands are OK, unless they appear within do .. end, they are OR'd. (Above ignores lines containing /MARK/ AND /Another/) =item filter $op /Pattern/ This turns on filtering, not recommended.. But if you do use it, it will only show lines matching the pattern. (Multiple filters can be supplied, like ignore) =item trim 'yes' If set to 'yes' spaces will be trimmed off either side of each line. Some logfiles like to pad the ends with spaces, which can confuse patterns. =item do (ignore|filter) ... end do .. end allow you to group multiple patterns into AND cases. An example shows it best do ignore =~ /pattern/ =~ /pattern 2/ end This literally creates this: sub { ($_ =~ /pattern/) && ($_ =~ /pattern 2/) } Which says, Ignore lines that contain BOTH /pattern/ and /pattern 2/ You can do the same with filter do filter =~ /THIS/ =~ /AND/ =~ /THAT/ end =back =head1 COPYRIGHT Copyright (C) Jamie Hoglund 2005 It's free software, same conditions as Perl itself. I'm always available for custom programming, system maintenance and monitoring. Please contact me for details. Your business helps keep these things available, it would be a pleasure to work with you. http://www.geniegate.com/contact.php =head1 MODULES Read this section if you want to write your own modules, this section is more for developers than end users. It is possible to write custom modules by extending the ReportEntry package, providing your own run() method. This section is hardly complete, I'll just give the basics. You've probably noticed, everything is one huge perl file. This makes the code harder to maintain but this way it's not terribly difficult to install on multiple hosts. package MyModule; use strict; our(@ISA) = qw(ReportEntry); sub run { my($self,$out) = @_; my $enc = $self->encrypt_cmd(); open(P,"ls | $enc |"); while(

){ $out->print($_); } close(P); } 1; In your configuration, you would add something like this: module MyModule custom /path/to/MyModule.pm After you do this, a new instance of your module will be created each time a C command is encountered in the configuration. custom <<__MY_MODULE directive_a "argument" ... directive_b "argument" __MY_MODULE Your module will have directive_a and directive_b available to C<$self>. =over 4 =item run($obj) This method is the only critical one, it must accept as a parameter an object that with a print() method. Your module should then print with that object, $obj->print() to have your information inserted into the current MIME attachment. You'll have to encrypt the information yourself however. Use $self->encrypt_cmd() to get the PGP program name. =item parse() If you need to do your own parsing, you can overload the parse($rdr) method. where $rdr is an object that supports a getline() method. (as well as interpret(T|F) and words($str).) Simply call $rdr->getline() to fetch the next line in your configuration. You can Cinterpret(0)> if you need the literal data. You can call C<@words = $rdr->words($string)> to have Text::ParseWords::shellwords() parse a line for you. Unless your module needs this, the default parse() method should suffice. =item validate() You should overload the validate() method if your module requires certain things. In this method, you could test for required parameters, etc.. and die() with a message if they aren't present. sub validate { my($self) = shift; $self->{directive_a} || die "Missing directive_a!"; } =item finish() If you need to do special things after the run() but before DESTRUCT, this is the place to do it. (Not normally used) =item encrypted() This returns TRUE usually. If your data is not encrypted, overload this and return a FALSE value. =item encrypt_cmd() Returns the C command, either the local one or a global one if there is no local pgp setting. As pointed out before, the data isn't automatically encrypted, you need to run it through a pipe yourself. =item mime_file() Returns $self->{mime_file} =item description() Returns $self->{description} =back =cut