#!/usr/bin/perl # # $Id: rule-get,v 1.27 2004/11/15 13:51:45 airmax Exp airmax $ # # For instructions, see : http://maxime.ritter.eu.org/rule-get # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # TODO : * Handle SafeNess variable # * Handle rule-get versions # * write more docs # * restart SA when it is finished ? use Config::IniFiles; use LWP::UserAgent; use Term::ANSIColor; use POSIX qw(mktime strftime); # Thanks to Darren Stalder use File::Copy; use open OUT => ':bytes'; # Open outputfiles as raw - to avoid perls utf8 translation use strict; my $cfg; my $cfg2; my $real_path="/etc/spamassassin"; # Printing something on screen... # error : error # warning : warning # info : important # 1 : a little bit important # 0 : non important # -1 : debug ? sub MyPrint { my $importance=$_[0]; my $message=$_[1]; if($importance eq "error") { print colored ['red on_black'], "[ERROR]"; print color 'reset'; print " : $message"; } elsif($importance eq "warning") { print colored ['yellow on_black'], "[WARNING]"; print color 'reset'; print " : $message"; } elsif($importance eq "info") { print colored ['black on_white'], "[INFO]"; print color 'reset'; print " : $message"; } else { print color 'reset'; print $message; } } # # IsInstalled($rule) # Checks if the $rule is installed # sub IsInstalled { my $rule; $rule = $_[0]; my $FileNames = $cfg->val($rule, "FileNames"); if( ! $FileNames) { MyPrint "error", "IsInstalled Error : $rule does not exist\n"; return -1; } foreach my $fn (split (/ /, $FileNames)) { return 1 if (-e "$real_path/$fn"); } return 0; } # # RemoveRule($rule) # Removes rules $rule sub RemoveRule { my $rule = $_[0]; my $FileNames = $cfg->val($rule, "FileNames"); if(! $FileNames) { MyPrint "error", "$rule : ruleset doesn't exist.\n"; return -1; } MyPrint 1, "Remove of $rule ruleset\n"; # Ok, now also checks wether it is installed... foreach my $fn (split (/ /, $FileNames)) { if (-e "$real_path/$fn") { if( unlink "$real_path/$fn" ) { MyPrint "info", "$rule : ruleset removed.\n"; } else { MyPrint "error", "$rule : Couldn't remove ruleset ($real_path/$fn). Check permissions.\n"; } return 0; } } MyPrint "warning", "$rule : ruleset not installed.\n\n"; } # downloader(url, filename, KeyID) # # Downloads a file located at $url, checks it GPG signature (against $KeyID) # and stores it at $filename. # if $KeyID is none, doesn't do any GPG check # $KeyID is all, it just requires a valid signature sub downloader { my $url=$_[0]; my $filename=$_[1]; my $KeyID=$_[2]; my $headers; # Checks if a given older date exists. In that case, use the "If-Modified-Since" header if($_[3] ne '') { $headers = HTTP::Headers->new("If-Modified-Since", $_[3]); } else { $headers = HTTP::Headers->new; } # Download "files" file my $ua = LWP::UserAgent->new; $ua->agent('rule-get for Spam Assassin - http://maxime.ritter.eu.org/rule-get'); $ua->env_proxy; my $req = HTTP::Request->new(GET => $url, $headers); my $res = $ua->request($req); # Checks if not modified. In that case, quickly returns if( $res->code == 304) { return 304; } if ( ! $res->is_success ) { MyPrint "error", "$url : " . $res->status_line . "\n"; return $res->code; } my $content = $res->content; open(FILE, "> $filename"); print FILE $content; close(FILE); if ( ($KeyID eq "none") or ( !$KeyID)) { return; } # Download "signature" file $req = HTTP::Request->new(GET => $url . ".sig"); $res = $ua->request($req); if ( ! $res->is_success ) { MyPrint "error", "$url.sig : " . $res->status_line . "\n"; return $res->code; } $content = $res->content; open(SIG, "> $filename.sig"); print SIG $content; close(SIG); # Check signature open(PIPE, "gpg --no-auto-check-trustdb --verify $filename.sig $filename 2>&1 |"); my $found = 0; my $linea; my $all_lines = ''; while($linea = ) { print $linea if($KeyID eq "all"); if($linea =~ /^gpg: Signature made .* using .* $KeyID/) { MyPrint 0, $linea; $found = 1; } elsif($linea =~ /^gpg: BAD signature/) { $found = -1; MyPrint 1, $linea; MyPrint "error", "BAD SIGNATURE !!!"; last; } $all_lines = $all_lines . $linea if ($linea =~ /^gpg/); } close(PIPE); unlink "$filename.sig"; # Print the whole output if nothing was found. print $all_lines if (($found==0) && ($KeyID ne "all")); # Is the GPG signature a valid one? if( ($found != 1) && ($KeyID ne "all") ) { unlink $filename; return -1; } return 0; } # # InstallRule # sub InstallRule { my $rule; $rule=$_[0]; my $FileNames = $cfg->val($rule, "FileNames"); if( ! $FileNames) { MyPrint "error", "$rule does not exist\n"; return -1; } # Finds the actual Filename $_ = $FileNames; my $filename; ( $filename )= /^(\S+)/; # Finds the old Filename my $old_filename; my $old_date; foreach my $fn (split (/ /, $FileNames)) { if (-e "$real_path/$fn") { $old_filename=$fn; my @etat=stat("$real_path/$fn"); my $fn_time = $etat[9]; $old_date = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime $fn_time ); } } # Ok, now the message will be of better quality MyPrint 1, "\n"; if(! $old_filename) { MyPrint "info", "Installation of $rule ruleset\n"; } else { # TODO : silent mode MyPrint "info", "Upgrade of $rule ruleset\n"; if($old_filename ne $filename) { MyPrint 1, "File $old_filename new name will be $filename\n"; } } # Checks if the rule is marked as obsolete, and prints a warning if($cfg->val($rule,"Status") eq "Obsolete") { MyPrint "warning", "$rule is known to be Obsolete !!\n"; if($cfg->val($rule, "ObsolatedBy") ne "") { MyPrint 2, "You might use " . $cfg->val($rule, "ObsolatedBy") . " instead.\n"; } } # Checks for uncompatibles rules my %remove_before = (); # Checks for included rules, which should now be removed foreach my $i (split ( / /, $cfg->val($rule, "Includes"))) { if(IsInstalled($i)) { MyPrint 1, "$rule includes $i, so we will remove $i before\n"; $remove_before{$i} = 1; } } # Checks uncompatibility foreach my $uncompat (split ( / /, $cfg->val($rule, "Uncompatible"))) { if(IsInstalled($uncompat)) { my $might_remove = 0; # Checks wether the uncompatible rule hasn't marked the ruleset we # want to install as a better non-obsolete version of this ruleset foreach my $ObsolatedBy (split ( / /, $cfg->val($uncompat, "ObsolatedBy"))) { if($ObsolatedBy eq $rule) { print "Removing old obsolete and uncompatible $uncompat ruleset (obsolated by\n"; print "$rule) needed before installing it.\n"; $remove_before{$uncompat}=1; $might_remove = 1; } } # Returns if nothing was found if(!$might_remove) { print "Uncompatible with already installed $uncompat ruleset.\n"; if( ($cfg->val($uncompat,"Status") eq "Obsolete") and ($cfg->val($rule,"Status") ne "Obsolete") ) { print "You might want to remove the old and obsolete $uncompat ruleset.\n"; } return -2; } } } # Ok, now removes the old rulesets foreach my $i (keys %remove_before) { RemoveRule($i); } #: Downloading of the new rule my $KeyID = $cfg->val($rule, "KeyID"); if ( ($KeyID eq "none") or ( !$KeyID)) { MyPrint "warning", "no GPG signature check\n"; } if ( $KeyID eq "all") { MyPrint "warning", "accepting signatures from ANYONE\n"; } my $i = downloader($cfg->val($rule, "URL"), $filename, $KeyID, $old_date); # Checks wether its not modified if ($i == 304) { return; } if($i == -1) { MyPrint "error", "BAD signature !!\n"; return -1; } elsif($i) { MyPrint "error", "something wrong while downloading files\n"; return -1; } # So now, it's okay. if( ! move($filename, "$real_path/$filename") ) { MyPrint "error", "Couldn't move to $filename to $real_path/$filename. Check permissions.\n"; unlink $filename; return -1, } # Delete the old file, if, it doesn't share the filename if( ($old_filename) && ($old_filename ne $filename)) { unlink "$real_path/$old_filename"; } MyPrint 1, "$rule : is now up to date\n"; return 0; } # # printRule($i) # prints info about rule named $i # sub printRule { my $i = $_[0]; print "* $i : " . $cfg->val($i, "Description"); my $KeyID = $cfg->val($i, "KeyID"); if( (! $KeyID) or ($KeyID eq "none")) { print " - No GPG Check :-((\n"; } elsif ($KeyID eq "all") { print " - Accepting all signature, please give a key\n"; } else { print " - GPG Key Known :-)\n"; } } my $i; my $KeyID; # We want GPG to speak english $ENV{'LANG'}="C"; $ENV{'LC_ALL'}=""; $ENV{'LC_MESSAGES'}="POSIX"; print '$Id: rule-get,v 1.27 2004/11/15 13:51:45 airmax Exp airmax $' . "\n"; print < "$real_path/rules.ini" ); my %keyhash; foreach $i ($cfg->Sections) { $KeyID = $cfg->val($i, 'KeyID'); if( ($KeyID) && ($KeyID ne "all") && ($KeyID ne "none") && ($KeyID ne "7A107F9E" )) { $keyhash{$KeyID} = 'yes'; } } # Converts keyhash into a simple string my $keylist; foreach $KeyID (keys %keyhash) { $keylist .= " $KeyID"; } # calls GPG `gpg --no-auto-check-trustdb --keyserver pgp.mit.edu --recv-key $keylist` if $keylist; # Tries to be verbose, lotta people weren't able to download more files print < show the list of available RuleSets * rule-get install RuleSet1 RuleSet2 RuleSet3 => installs the 3 rulesets called RuleSet1, RuleSet2, RuleSet3. A valid installation using july 10th 2004 rules.ini would be : rule-get install airmax MrWiggly sa-random AntiDrug OR * rule-get update => updates all installed rules, with the help of new rules. * rule-get remove obsoletes => removes all obsoletes rules. EOF exit 0; } # Loads configuration. if (-e "$ENV{HOME}/.rules.ini") { $cfg2= new Config::IniFiles( -file => "$real_path/rules.ini" ); $cfg = new Config::IniFiles( -file => "$ENV{HOME}/.rules.ini", -import => $cfg2 ); } else { $cfg = new Config::IniFiles( -file => "$real_path/rules.ini" ); } # TODO : Handle rule-set Config if($cfg->SectionExists("Options")) { # Don't keep the rule-set config in memory within other rules $cfg->DeleteSection ("Options"); } # Make a clean configuration (not a clean code :-) ) # should be debugged, but hey, old versions didn't have this code... So for now, # I assume if it worked for some limited tests, it should work everytime # I know I shouldn't.... # # After this cleaning, the meaning are : # Uncompatible : you can only install this ruleset if the other one isn't # installed. # Includes : this ruleset includes the other ruleset. Installing it means that # it will remove all Includes ruleset, if they are installed. # # 1. If a rule says another is uncompatible or is included, checks that the other # one also is marked as uncompatible foreach $i ($cfg->Sections) { my $Uncompatibles = $cfg->val($i, "Uncompatible") . ' ' . $cfg->val($i, "Includes"); # adds the name of the actual rule into the Uncompatibility field of the marked ones if( $Uncompatibles =~ /[a-z]/i ) { foreach my $j (split(/ /, $Uncompatibles)) { if( $cfg->SectionExists ( $j )) { $cfg->newval($j, "Uncompatible", $cfg->val($j, "Uncompatible"). " $i"); } } } } # 2. Cleans Uncompatibles, according to 'Includes', and removes doubles ones. foreach $i ($cfg->Sections) { my %deja_vu = (); my $clean = ""; # Stores a clean Includes string my $dirty = $cfg->val($i, "Includes"); if($dirty ne "") { foreach my $j (split (/ /, $dirty)) { unless( $deja_vu{$j} or ($j !~ /[a-z]/i) ) { $deja_vu{$j} = 1; $clean = "$j $clean"; } } chop($clean); $cfg->setval($i, "Includes", $clean); } # Stores a clean Uncompatible string $clean = ""; $dirty = $cfg->val($i, "Uncompatible"); if($dirty ne "") { foreach my $j (split (/ /, $dirty)) { unless ( $deja_vu{$j} or ($j !~ /[a-z]/i)) { $deja_vu{$j} = 1; $clean = "$j $clean"; } } chop($clean); $cfg->setval($i, "Uncompatible", $clean); } } my %RuleOf; # Some sanity checks (and loading) before doing the real things... foreach $i ($cfg->Sections) { my $LocalName; my $FileNames; if( ($i eq "all") or ($i eq "signed") ) { print "$i is not a valid ruleset name !"; exit -1; } if ( ! $cfg->val($i, 'URL') ) { print "Rule $i : no URL"; exit -1; } $LocalName = $cfg->val($i, 'LocalName'); $FileNames = $cfg->val($i, 'FileNames'); if( (! $LocalName) && (! $FileNames)) { print "Rule $i : no filename given for storing the rule!\n"; exit -1; } # Stores (and checks) the associativity between real file names and rules names if ($RuleOf{$LocalName}){ print "Rule $i : LocalName $LocalName already taken by $RuleOf{$LocalName}\n"; exit -1; } if($FileNames) { foreach $LocalName (split(/ /, $FileNames)) { if ($LocalName =~ /[a-z]/) { if ($RuleOf{$LocalName}){ print "Rule $i : LocalName $LocalName already taken by $RuleOf{$LocalName}\n"; exit -1; } $RuleOf{$LocalName} = $i; } } # If needed, stores the old 'LocalName' string inside the new FileNames (last position) if( ($LocalName) && (!$RuleOf{$LocalName})) { $RuleOf{$LocalName} = $i; $cfg->newval($i, "FileNames", "$FileNames $LocalName"); } } else { # Stores the LocalName... $RuleOf{$LocalName} = $i; # Stores the old LocalName as the FileNames $cfg->newval($i, "FileNames", $LocalName); } } # # Ok, now removes the old LocalNames, since they are no longer needed # that way, we're not going to reuse the old variables foreach $i ($cfg->Sections) { $cfg->delval($i, "LocalName"); } # # list command : for compatibility if($ARGV[0] eq "list") { $ARGV[0] = "show"; $ARGV[1] = "avail"; } # # show command if ($ARGV[0] eq "show") { if($ARGV[1] eq "avail") { print "Available rulesets are : \n"; foreach $i ($cfg->Sections) { printRule($i) if ($cfg->val($i, "Status") ne "Obsolete"); } } elsif($ARGV[1] eq "all") { print "Rulesets are :\n"; foreach $i ($cfg->Sections) { printRule($i); } } elsif($ARGV[1] eq "obsoletes") { print "Obsoletes rulesets are :\n"; foreach $i ($cfg->Sections) { printRule($i) if ($cfg->val($i, "Status") eq "Obsolete"); } } elsif($ARGV[1] eq "installed") { print "Installed rulesets recognized by rule-get are :\n"; foreach $i ($cfg->Sections) { printRule($i) if IsInstalled($i); } } elsif($ARGV[1] eq "uninstalled") { print "Rulesets which might be installed are :\n"; foreach $i ($cfg->Sections) { printRule($i) unless (IsInstalled($i) or ($cfg->val($i, "Status") eq "Obsolete")); } } elsif($ARGV[1] eq "might_install") { print "Available rulesets are : \n"; foreach $i ($cfg->Sections) { if ( ($cfg->val($i, "Status") ne "Obsolete") and (!IsInstalled($i))) { # Vérifier qu'il n'y ait pas d'incompatibilité my $file; my $uncompat=0; foreach $file (split ( / /, $cfg->val($i, "Uncompatible"))) { if(IsInstalled($file)) { $uncompat=1; } } printRule($i) if (! $uncompat); } } } else { # Not recognized show option print <val($RuleOf{$file}, "Status") eq "Obsolete") { # Looks if it had been Obsolated by another ruleset my @temp_array = split ( / /, $cfg->val($RuleOf{$file}, "ObsolatedBy")); my $new_instead=$temp_array[0]; if($new_instead ne "") { InstallRule($new_instead); } else { # Delete the old Obsolete rule if no replacement exists RemoveRule($RuleOf{$file}); } } else {# Update, not yet obsolete InstallRule($RuleOf{$file}) } } } } # # soft-update : only updates files, doesn't install new files by way # of de-obsoleting elsif ($ARGV[0] eq "soft-update") { # update mode opendir REP, "$real_path" or die "Couldn't read $real_path ?"; my @files = readdir REP; closedir REP; my $file; foreach $file (@files){ InstallRule($RuleOf{$file}) if $RuleOf{$file}; } } elsif ($ARGV[0] eq "install") { # Installing all rulesets if($ARGV[1] eq "all") { print "Installing ALL rulesets\nBe sure to adjust your required_hits setting !\n"; foreach $i ($cfg->Sections) { InstallRule($i); } } # Installing all signed rulesets elsif($ARGV[1] eq "signed") { print "Installing ALL signed rulesets (with known GPG key)\n"; foreach $i ($cfg->Sections) { $KeyID = $cfg->val($i, "KeyID"); InstallRule($i) if( ($KeyID) && ($KeyID ne "none") && ($KeyID ne "all") ); } } # Installing some new rulesets else { while ($ARGV[1]) { InstallRule($ARGV[1]); shift(@ARGV); } } } # # remove command : removes named rulesets elsif ($ARGV[0] eq "remove") { # Removing all rulesets (will be removed in future) if($ARGV[1] eq "all") { print "Removing ALL rulesets\n"; foreach $i ($cfg->Sections) { RemoveRule($i) if(IsInstalled($i)); } } # Removes only obsoletes elsif($ARGV[1] eq "obsoletes") { print "Removing obsoletes rulesets\n"; foreach $i ($cfg->Sections) { if(IsInstalled($i) && ($cfg->val($i,"Status") eq "Obsolete")) { RemoveRule($i); print "\n"; } } } # Remove some rulesets else { if($ARGV[1] eq "") { print "Give at least the name of a ruleset to remove !\n"; print "Alternately, you might do a 'rule-get remove obsoletes', in order to\nremove all obsolete rules."; } while ($ARGV[1]) { RemoveRule($ARGV[1]); print "\n"; shift(@ARGV); } } } # # remove-all command : removes all rulesets elsif($ARGV[0] eq "remove-all") { print "Removing ALL rulesets\n"; foreach $i ($cfg->Sections) { if(IsInstalled($i)) { RemoveRule($i); print "\n"; } } } # # remove-obsolete command : removes obsolete rulesets elsif($ARGV[0] eq "remove-obsoletes") { print "Removing obsoletes rulesets"; foreach $i ($cfg->Sections) { if(IsInstalled($i) && ($cfg->val($i, "Status") eq "Obsolete")) { RemoveRule($i) } } } else { print "Command not understood\n"; } # Cleanning chdir "/tmp"; rmdir $work_directory;