#!/usr/bin/perl
use Time::Local;
use strict;
# Quickpatch version 1.0201
# Created by Michael L. Vince / mike@thebeastie.org Under the BSD License
#
# Source based patch system for FreeBSD.
# Utilizes Perls regular expression power to extract data and patch commands from official FreeBSD security advisories,
# tested from 4.4 to 5.2
# Get the latest version from http://thebeastie.org/projects/quickpatch/
# Disclaimer: In no way/event shall I be liable for any damages done by this script in any way conceivably possible.
# There is no guarantee this script will work forever flawlessly as it must technically be classed as a hack
# Requirements, Perl, cvsup, wget (/usr/ports/ftp/wget) and a internet connection.
# To install perl, cvsup and wget run this command which downloads and installs the package. pkg_add -r cvsup-without-gui.tbz ; pkg_add -r wget.tbz ; pkg_add -r perl.tbz
# Type "./quickpatch.pl updateadv" to download advisories
# Type "./quickpatch.pl notify" to email some one what would be done in patch mode
# Type "./quickpatch.pl patch" or "./quickpatch.pl patch > big_patch_file" to create patch files
# Type "./quickpatch.pl cvsup" to cvsup to update your source tree
# Type "./quickpatch.pl cvsupports" to cvsup update your ports tree, default tag is current
# Optional is pgp (/usr/ports/security/gnupg) to use it type "./quickpatch.pl pgpcheck"
my ($release,$ftpserver,$patchdir,$advdir,$kernelbuild,$buildworld,$runpatchfile,$advisory_age,@emails,$hostname,$cvsupserver);
my ($pgpcheck,$skipoldpgp,$pgpemail,$supdir,$agenotify_skip,$sanity_check,$force,$gpg,$supfile,$supfileports,$cvsupbin,$advdirtmp);
my ($aggressive,$wget,$i,$aged,$datecorrected,$difference,$patchfile,$mode,$dateannounced,$notify_file_skip,$reboot_after_compile,$reboot_after_patch,$reboot_script);
# Configuration is now done via external configuration file /usr/local/etc/quickpatch.conf
# Load configuration from external file
if ( -e "/usr/local/etc/quickpatch.conf")
{
eval `/bin/cat /usr/local/etc/quickpatch.conf`;
}
elsif ( -e "./quickpatch.conf")
{
eval `/bin/cat ./quickpatch.conf`;
} else { print "Couldn't find configuration file should be at /usr/local/etc/quickpatch.conf \n"; exit; }
$supfile="$supdir"."quickpatch-supfile";
$supfileports="$supdir"."quickpatch-supfile-ports";
$advdirtmp="$advdir"."tmp/";
my $advdirlog="$advdir"."log/";
my @files;
my @patchlist;
my @dirs = ($patchdir,$advdir,$advdirtmp,$supdir,$advdirlog);
my $dir;
foreach $dir (@dirs) {
if (! -e "$dir")
{
print "# Directory $dir does not exist, creating\n";
mkdir("$dir",0755);
}
}
if ($pgpcheck) {
if (! -e "$gpg") { print "GPG not installed, cd /usr/ports/security/gnupg ; make install\n"; exit; }
}
if ($sanity_check) { releasecheck($release); }
#Sup-file configuration
my $sup="*default host=$cvsupserver
*default base=/usr
*default prefix=/usr
*default release=cvs tag=$release
*default delete use-rel-suffix compress
src-all
";
# Ports sup file
my $ports_sup="*default host=$cvsupserver
*default base=/usr
*default prefix=/usr
*default release=cvs tag=.
*default delete use-rel-suffix compress
ports-all
";
#if (!$hostname) { my $hostname = `/bin/hostname`; chomp($hostname); }
sub releasecheck {
my $release = $_[0];
my $rv; my $uname; my $releng;
$uname = `/usr/bin/uname -r`;
chomp($uname);
$uname =~ /^(\d\.\d+).*$/; $rv = $1;
$rv =~ s/\./_/g;
$releng="RELENG_"."$rv";
if ("$release" ne "$releng") { print "Release version not the same! Detected version $releng, configured version $release \nUpdate your /usr/local/etc/quickpatch.conf file"; exit; }
}
my $date = currentdate();
########################################################################
if ($ARGV[0] eq 'patch') {
@files = getadvlist();
foreach $i (@files) {
#print "$i \n";
@patchlist = find("$i","$release","$advdir"); }
# print "#!/bin/sh\n";
$mode="patch";
foreach $patchfile (@patchlist)
{
print "#########################################################################################\n";
print "####### $patchfile\n";
($aged,$datecorrected,$difference,$dateannounced) = checkage("$patchfile","$mode");
if (!$aged) { next; }
if ($force) { patches($patchfile,$datecorrected,$difference,$hostname,$mode,$pgpemail,$advdir,$skipoldpgp,$gpg,$dateannounced,@emails);
next; }
if (oldpatch($patchfile)) { next; }
if ($runpatchfile) { my $ready = patchcheck($patchfile,$patchdir,$notify_file_skip);
if ($ready eq "0") { next; }
}
my ($pf,$topic,$patchfile,$patches,$rebuild_world,$rebuild_kernel) = patches($patchfile,$datecorrected,$difference,$hostname,$mode,$pgpemail,$advdir,$skipoldpgp,$gpg,$dateannounced,@emails);
if ($runpatchfile) {
my $patchcommand = "$patchfile";
$patchcommand =~ s/\.asc$//g;
my $pfc = "$patchdir"."$patchcommand";
# system("$pfc");
my $result = `$pfc 2>&1`;
my $advdirpatchfilelog = "$advdirlog"."$patchcommand".".log";
system("/bin/cp $pfc $advdirpatchfilelog");
open(LOGPATCH,">>$advdirpatchfilelog") || die "Sorry, I couldn't create $advdirpatchfilelog\n";
print LOGPATCH $result;
close(LOGPATCH);
print "$result";
$topic = "QuickPatch patch results - "."$hostname"." - Topic: "."$topic";
email_patch_check($advdirpatchfilelog,$topic,$patchfile,$mode,@emails);
# reboot code is done after all patch commands are executed
# Reboot after compile/patch if configured to do so
if ("$reboot_after_compile" eq "1" && "$rebuild_world" eq "1") { system("$reboot_script"); }
if ("$reboot_after_compile" eq "1" && "$rebuild_kernel" eq "1") { system("$reboot_script"); }
if ("$reboot_after_patch" eq "1") { system("$reboot_script"); }
}
}
}
########################################################################
elsif ($ARGV[0] eq 'notify') {
# my @files;
@files = getadvlist();
foreach $i (@files) { @patchlist = find("$i","$release","$advdir"); }
$mode="notify";
foreach $patchfile (@patchlist) {
print "#########################################################################################\n";
print "####### $patchfile\n";
#print "####### $hostname - $date\n";
# if (checkage("$i")) { next; }
($aged,$datecorrected,$difference,$dateannounced) = checkage("$patchfile","$mode");
if (!$aged) { next; }
if (oldpatch($patchfile)) { next; }
#if ($pgpcheck) { pgpcheck($patchfile,$pgpemail,$advdir,$skipoldpgp,$mode,@emails); }
my $nf = "$patchfile";
$nf =~ s/\.asc$//g;
my $notify_file ="$patchdir"."$nf"."\.notified";
if (-e "$notify_file") { print "### Skipping $patchfile, already notified\n"; next; }
my ($pf,$topic,$patchfile,$patches,$rebuild_world,$rebuild_kernel) = patches($patchfile,$datecorrected,$difference,$hostname,$mode,$pgpemail,$advdir,$skipoldpgp,$gpg,$dateannounced,@emails);
if ($mode eq "notify") {
$topic = "QuickPatch notify - "."$hostname"." - Topic: "."$topic";
email_patch_check($pf,$topic,$patchfile,$mode,@emails);
}
chomp($patchfile);
my $notify_file ="$patchdir"."$patchfile"."\.notified";
`/usr/bin/touch $notify_file`;
} exit;
}
########################################################################
elsif ($ARGV[0] eq 'pgpcheck')
{
@files = getadvlist();
foreach $i (@files) { pgpcheck($i,$pgpemail,$advdir,$skipoldpgp,$mode,$gpg,@emails); }
}
elsif ($ARGV[0] eq 'updateadv') {
if (! -e "$wget") { print "wget not installed, cd /usr/ports/ftp/wget ; make install\n"; exit; }
system("$wget -nc --passive-ftp \"ftp://$ftpserver/pub/FreeBSD/CERT/advisories/FreeBSD-SA*:*.asc\" -P $advdir");
print "Finished\n";
}
elsif ($ARGV[0] eq 'cvsup')
{
cvsup("$sup","$supfile");
}
elsif ($ARGV[0] eq 'cvsupports')
{
cvsup("$ports_sup","$supfileports");
}
elsif ($ARGV[0] eq 'cvsuplist')
{
if (! -e "/usr/share/doc/en_US.ISO8859-1/books/handbook/book.txt") { print "Sorry no hand book found \n"; exit; }
system ('/usr/bin/grep -i "\* cvsup.*FreeBSD.org" /usr/share/doc/en_US.ISO8859-1/books/handbook/book.txt');
}
else {
print 'Quickpatch - source based security update system script
"./quickpatch.pl updateadv" to download / update advisories db
"./quickpatch.pl patch" or "./quickpatch.pl patch > big_patch_file" to create patch files
"./quickpatch.pl notify" does not do anything but email you commands of what it would do
"./quickpatch.pl cvsup" to cvsup to update your source tree
"./quickpatch.pl cvsupports" to cvsup update your ports tree, default tag is current
"./quickpatch.pl cvsuplist" to list cvsup servers
"./quickpatch.pl pgpcheck" to PGP check advisories
';
}
sub getadvlist {
opendir DIR, "$advdir" or die "no dir: $!";
my @files = grep /^.*?\.asc$/, readdir DIR;
closedir DIR;
return @files;
}
sub patchcheck {
my ($patchfile,$patchdir,$notify_file_skip) = @_;
chomp($patchfile);
$patchfile =~ s/\.asc$//g;
my $notify_file ="$patchdir"."$patchfile"."\.notified";
if (! -e "$notify_file" && "$notify_file_skip" eq "0") { print "### No notify file!, set \$notify_file_skip=1 to override, skipping\n"; return 0; }
unlink("$notify_file");
return 1;
}
sub currentdate
{
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
my ($day, $month, $year) = (localtime)[3,4,5];
$year = $year + 1900; $month = $month +1;
my $date = "$year-$month-$day $hour:$min:$sec";
return $date;
}
sub oldpatch
{
my $patchfile = "$_[0]";
$patchfile =~ s/\.asc//g;
chomp($patchfile);
my $pfc = "$patchdir"."$patchfile";
if (-e "$pfc") {
print "### Skipping $pfc patch file already created \n"; return 1;
} else { return 0; }
}
sub cvsup {
my $sdata = "$_[0]"; my $sfile = "$_[1]";
open(SUPFILE,">$sfile") || die "Sorry, I couldn't create $supfile\n";
print SUPFILE $sdata;
close(SUPFILE);
if (! -e "$cvsupbin") { print "cvsup not installed, cd /usr/ports/net/cvsup-without-gui ; make install\n"; exit; }
system("$cvsupbin -g -L 2 $sfile");
}
sub find
{
my($i,$release,$advdir)= @_;
#my $i = "$_[0]";
#my $release = "$_[1]";
my $data; my $patch;
my $advfile="$advdir"."$i";
open (HT,"$advfile");
while (<HT>)
{
if (m/UTC.*?$release.*?/si)
{
$data = $_;
$patch="true"; #print "$advfile \n";
@patchlist = (@patchlist,"$i");
close(HT);
}
} close(HT);
return @patchlist;
}
sub pgpcheck {
my ($advfile,$pgpemail,$advdir,$skipoldpgp,$mode,$gpg,@emails) = @_;
if ($skipoldpgp) {
if ($advfile =~ /FreeBSD-SA-0[012].*.asc/) { return 0; }
}
if (! -e "$gpg") { print "GPG not installed, cd /usr/ports/security/gnupg ; make install\n"; exit; }
my $gpgout = `$gpg --no-secmem-warning --verify $advdir$advfile 2>&1`;
#print "$gpgout \n";
if ($gpgout =~ /\ngpg:\s+Good signature from "FreeBSD Security Officer <security-officer\@FreeBSD\.org>"\n/)
{
printf "## PGP check Good: \t\t %s\n", $advfile;
printf PF "## PGP check Good: \t\t %s\n", $advfile;
} else
{
print "#############################\nPGP check FAILED: $advfile be afraid!\n#############################\n";
if ($pgpemail) { pgpemail($advfile,@emails); }
exit;
}
}
sub pgpemail {
my $email;
my ($i,@emails) = @_;
foreach $email (@emails)
{ print "## Sending email to $email \n";
`/bin/echo "PGP check failed! Be afraid! $i" | /usr/bin/mail -s "QuickPatch PGP check failed! $i" $email`;
print "## Emailed $email \n";
}
}
sub email_patch_check
{
my $email;
my($pf,$topic,$patchfile,$mode,@emails)= @_;
foreach $email (@emails)
{
`/bin/cat "$pf" | /usr/bin/tail -n 850 | /usr/bin/mail -s "$topic ${patchfile}" $email`;
print "## Emailed $email \n";
} if ($mode eq "notify") { unlink $pf; }
}
sub checkage() {
# convert current server and advisory / patch creation time into GMT epoch seconds for calculation into advisory age difference
$/ ='\n';
$mode = $_[1];
my $aged;
my $dateannounced;
open (AG,"${advdir}$_[0]");
while (<AG>)
{
if (/Announced:\s+(\d\d\d\d)-(\d\d)-(\d\d)/) { $dateannounced = "$1-$2-$3"; }
if (/\s+(\d\d\d\d)-(\d\d)-(\d\d)\s(\d\d):(\d\d):(\d\d)\s+UTC\s.*?$release/ || /\s+(\d\d\d\d)\/(\d\d)\/(\d\d)\s(\d\d):(\d\d):(\d\d)\s+UTC\s.*?$release/)
{
my $datecorrected = "$1-$2-$3 $4:$5:$6";
my $year = $1; my $month = $2; my $day = $3; my $hours = $4; my $minutes = $5; my $seconds = $6;
my $dce = timegm($seconds, $minutes, $hours, $day, $month-1, $year-1900);
my $timenow = time;
my $difference = $timenow - $dce;
$seconds = $difference % 60;
$difference = ($difference - $seconds) / 60;
$minutes = $difference % 60;
$difference = ($difference - $minutes) / 60;
$hours = $difference % 24;
#print "$timenow - $dce \n";
if ("$mode" eq "notify" && "$agenotify_skip" eq "1") { return (1,$datecorrected,$difference,$dateannounced); }
if ($difference < $advisory_age) { print "### Advisory too new, minimum age $advisory_age hours was $difference \n";
$aged=0; return ($aged,$datecorrected,$difference,$dateannounced); } else { close(AG); $aged = 1; return ($aged,$datecorrected,$difference,$dateannounced); }
#return 0; }
#print "####### Date Corrected: $datecorrected\n####### Hours past since corrected: $difference\n";
#print PF "####### Date Corrected: $datecorrected\n####### Hours past since corrected: $difference\n"
}
} close(AG); print "No Date corrected found! ${advdir}$_[0] \n"; return 0;
}
sub patches {
my $data;
my $topic;
my $mode;
my $pf;
my $pfc;
my $patchfile;
my $advfile;
my ($patches,$key);
#my $os="OS Recompile";
#my $execute="Execute Commands";
#my $kernel="Kernel Recompile";
#my $install="Install";
my %patches;
my $rebuild_kernel;
my $rebuild_world;
my ($i,$datecorrected,$difference,$hostname,$mode,$pgpemail,$advdir,$skipoldpgp,$gpg,$dateannounced,@emails) = @_;
$/ = '';
open (HT,"${advdir}$i");
$patchfile = "$i";
$advfile = "$patchfile";
$patchfile =~ s/\.asc//g;
chomp($patchfile);
$pfc = "$patchdir"."$patchfile";
if ($mode eq "notify") { $pf = "$advdirtmp"."$patchfile"; } else { $pf = "$patchdir"."$patchfile"; }
print "## Stored in file $pf\n";
open(PF,">$pf") || die "Sorry, can't create $pf\n";
#print "$pf\n";
chmod 0755, "$pf";
print PF "#!/bin/sh\n";
print PF "## $i ###########\n";
if ($mode eq "patch") { print PF "## Stored in file $pf\n"; }
#print "####### $i ########################################\n";
if ($pgpcheck) { pgpcheck($advfile,$pgpemail,$advdir,$skipoldpgp,$mode,$gpg,@emails); }
while (<HT>) {
if (m/^Topic:\s+/si)
{ $topic = "$_";
chomp($topic);
$topic =~ s/\s+\[/ \[/g;
$topic =~ s/\n//g;
$topic =~ s/\s+/ /g;
$topic =~ s/Topic:\s+//g;
#printf "# Topic: %s\n", $topic;
printf "## Topic: \t\t\t %s\n", $topic;
printf PF "## Topic: \t\t\t %s\n", $topic;
printf "## Hostname: \t\t\t %s\n", $hostname;
printf PF "## Hostname: \t\t\t %s\n", $hostname;
printf "## Current Date: \t\t %s\n", $date;
printf PF "## Current Date: \t\t %s\n",$date;
printf "## Advisory Announced: \t\t %s\n",$dateannounced;
printf PF "## Advisory Announced: \t\t %s\n",$dateannounced;
printf "## Date Corrected: \t\t %s\n",$datecorrected;
printf "## Hours past since corrected: \t %s\n\n",$difference;
printf PF "## Date Corrected: \t\t %s\n",$datecorrected;
printf PF "## Hours past since corrected: \t %s\n\n",$difference;
print "## Patch Commands\n";
print PF "## Patch Commands\n";
}
#c) Recompile the kernel as described in
if (m/^(\w)\)\s+Re\w+\syour kernel.*?as described in/si || m/^(\w)\)\s+Recompile the kernel as described in/ || m/^Re\w+\s+your\skernel\w?\sas\sdescribed\sin/si)
{
#print "####### PATCH CODE Kernel Recompile $.\n";
$data = $_;
# Check if a recompile execution has already been set
if ("$rebuild_kernel" eq "1") { next; }
# Command execution order is decided by line order in the advisory as would be done from the advisory
my $order = "$.";
#print "## order = $order \n";
$data =~ s/\n/ /g;
$data = "####### "."$data\n"."$kernelbuild\n";
# Load commands into the %patches hash
$patches{$order} = $data;
#$patches{$order}{$kernel} = $data;
$rebuild_kernel="1";
}
if (m/^(\w)\)\sRecompile\s\w+\soperating system\w?\sas\sdescribed\sin/si)
{
$data = $_;
$data =~ s/\n/ /g;
# Check if a $rebuild_world execution has already been set
if ("$rebuild_world" eq "1") { next; }
# Set command order caused by line number in advisory
my $order = "$.";
$data = "### "."$data\n"."$buildworld\n";
# Load commands into the %patches hash
$patches{$order} = $data;
#$patches{$order}{$os} = $data;
$rebuild_world="1";
}
#if (m/^#\scd/si)
if (m/^#\scd\s\/usr\/src/si)
{
#print "####### Patch Commands $.\n"; print PF "####### Patch Commands \n";
$data = $_;
my $order = "$.";
if ($aggressive) {
#print "# Agressive mode\n";
$data =~ s/#\s(\w+) /$1 /g;
$data =~ s/patch\s+</# patch </g;
$data =~ s/patch\s+\-p\s+</# patch -p </g;
$data =~ s/^\[/###\[/g;
$patches{$order} = $data;
# $patches{$order}{$execute} = $data;
} else {
#print "# Safe mode \n";
$data =~ s/#\scd /cd /g;
$data =~ s/#\smake /make /g;
$data =~ s/^\[/###\[/g;
$patches{$order} = $data;
#$patches{$order}{$execute} = $data;
}
}
}
close(HT);
####################################################
my $order;
# Print out the patch commands sorted by line order in the advisory
foreach $order ( sort keys %{ %patches } )
{
# print "####### Patch type: $key\n";
# print PF "####### Patch type: $key\n";
print "$patches{$order}";
print PF "$patches{$order}";
}
close(PF);
return ($pf,$topic,$patchfile,$patches,$rebuild_world,$rebuild_kernel);
}
syntax highlighted by Code2HTML, v. 0.9