#!/usr/bin/env perl use strict; use Getopt::Std; # Versioning: my $VER = '1.01'; # [KK 2010-10-04] 1.01 Added opcodes and storage of actions in a separate # program, so that daemonized version doesn't re-read the configuration all # the time. Signal 1 (HUP) re-scans the configuration. # [KK 2010-10-01] 1.00 First version, based on a years-old syscheck which # worked but wasn't as flexible. # Program opcodes use constant { op_none => 0, op_eval => 1, op_system => 2, op_populate => 3, op_expect => 4, op_correct => 5 }; # Get flags, expect one argument 'test' or 'go'. my %opts = (c => $0 . '.conf', v => undef, s => undef); getopts('c:sv', \%opts) or usage(); my $testing = undef; if ($ARGV[0] eq 'test' and $#ARGV == 0) { $testing++; single_run(read_config()); } elsif ($ARGV[0] eq 'go' and $#ARGV == 0) { single_run(read_config()); } elsif ($ARGV[0] eq 'daemonize' and $#ARGV == 1) { daemonize($ARGV[1]); } else { usage(); } # Daemonize and run repeatedly my @program; sub reload_conf() { my @newprog; eval { @newprog = read_config(); }; if ($@) { warn("New configuration NOT activated.\n"); } else { print("Configuration reloaded.\n"); } } sub daemonize($) { my $period = int(shift); die("Bad daemonization period $period (must be positive seconds)\n") if ($period < 1); @program = read_config(); my $pid = fork(); die("Failed to fork: $!\n") unless defined($pid); exit(0) if ($pid); $SIG{HUP} = \&reload_conf; while (1) { single_run(@program); sleep($period); } } # Parse the configuration file, store as a program sub read_config() { open(my $if, $opts{c}) or die("Cannot read configuration $opts{c}: $!\n"); my @prog; while (my $line = getline($if)) { my ($cmd, $tail) = split(/\s+/, $line, 2); die("Bad configuration (expected: action and tail) in '$line'\n") if ($cmd eq '' or $tail eq ''); msg("[configuration:$cmd] $tail\n"); if ($cmd eq 'eval') { push(@prog, [ op_eval, $tail ]); } elsif ($cmd eq 'system') { push(@prog, [ op_system, $tail ]); } elsif ($cmd eq 'populate') { my ($list, $cmd) = split(/\s+/, $tail, 2); die("Bad populate command (expected: listname and system cmd)\n") if ($list eq '' or $cmd eq ''); push(@prog, [ op_populate, $list, $cmd ]); } elsif ($cmd eq 'expect') { my ($list, $str) = split(/\s+/, $tail, 2); die("Bad expect command (expected: listname and regex)\n") if ($list eq '' or $str eq ''); push(@prog, [ op_expect, $list, $str ]); } elsif ($cmd eq 'correct') { push(@prog, [ op_correct, $tail ]); } else { die("Unrecognized command '$cmd'\n"); } } # use Data::Dumper; print Dumper @prog; return @prog; } # Do a single test sub single_run(@) { my @prog = @_; # use Data::Dumper; print Dumper(@prog); my %poplist; my $to_match = 0; my $matched = 0; my $last_op = op_none; # Run thru program, take actions for my $p (@prog) { my @inst = @{ $p }; my $op = shift(@inst); if ($op == op_eval) { msg("[eval] $inst[0]\n"); eval($inst[0]); warn("Bad eval command '$inst[0]'\n") if ($@); } elsif ($op == op_system) { if ($testing) { print("$inst[0]\n"); } else { msg("[system] $inst[0]\n"); system($inst[0]) and warn("System command '$inst[0]' failed\n"); } } elsif ($op == op_populate) { msg("[populate] $inst[0] $inst[1]\n"); open(my $if, "$inst[1] |"); if (! $if) { warn("Cannot start populate command '$inst[1]': $!\n"); next; } my @lines; while (my $line = <$if>) { chomp($line); msg("[populate:$inst[0]] $line\n"); push(@lines, $line); } $poplist{$inst[0]} = [ @lines ]; } elsif ($op == op_expect) { msg("[expect] $inst[0] $inst[1]\n"); if (! $poplist{$inst[0]}) { warn("Expect list '$inst[0]' was never populated\n"); next; } if ($last_op != op_expect) { $to_match = 0; $matched = 0; } $to_match++; my $found = undef; for my $line (@{ $poplist{$inst[0]} }) { if ($line =~ /$inst[1]/) { $found++; $matched++; last; } } msg("[expect] $inst[0] $inst[1]: ", $found ? "matched" : "NOT matched", "\n"); } elsif ($op == op_correct) { msg("[correct] $inst[0], to-match:$to_match, matched:$matched\n"); if ($matched < $to_match) { if ($testing) { print("$inst[0]\n"); } else { system($inst[0]) and warn("Correct command '$inst[0]' failed\n"); } } } else { die("Internal jam: unrecognized opcode '$op'\n"); } } } # Show usage and stop sub usage() { die <<"ENDUSAGE"; This is syscheck V$VER Copyright (c) Karel Kubat / karel\@kubat.nl 2010ff. Please visit http://www.kubat.nl/pages/syscheck for more information. Usage: syscheck [flags] test - single test run or: syscheck [flags] go - single actual run or: syscheck [flags] daemonize NSEC - deamonizes and runs each NSEC sec During a test run the commands that would be executed are printed to stdout. During an actual run they are executed. The flags may be: -c CONF - defines configuration file, default is syscheck.conf in the directory where syscheck is located -s - during actual runs display the corrrecting system commands -v - increases verbosity The format of the configuration is as follows. Comments are allowed in the form #..., one line per comment (no trailing comments). The following entries in syscheck.conf are possible: eval PERLCODE - special actions and instructions to Perl e.g., eval \$ENV{PATH} .= '/my/bin/dir'; system CMD - unconditionally run command(s) e.g., system echo `date` syscheck started on `hostname` populate LISTNAME CMD - store output of CMD into LISTNAME e.g. populate pslist ps ax expect LISTNAME REGEX - (try to) match REGEX in LISTNAME e.g. expect pslist httpd Several expect's may be stated following each other. When even one fails to match, then the next correcting command will run. correct CMD - run correction CMD when previous expect(s) failed to match e.g. correct /opt/local/apache2/bin/apachectl start ENDUSAGE } # Verbose messaging sub msg { print(@_) if ($opts{v}); } # Get one line from the configuration. Skip comments etc. sub getline($) { my $if = shift; my $ret = undef; while (1) { my $line = <$if> or last; chomp($line); $line =~ s{^\s*}{}; $line =~ s{\s*$}{}; next if ($line eq '' or $line =~ /^#/); $ret .= ' ' if ($ret); $ret .= $line; if ($ret =~ /\\$/) { $ret =~ s{\\$}{}; } else { last; } } return $ret; }