You can rename a snapshot. If you rename one to "PreVirus" it is pretty easy to write a little script that will find zero-length files that weren't zero-length in the snapshot and email the owner of the file (or do the restore automatically). [ Disclaimer: I haven't done this myself, but I'm told it is possible. ]
Its not just theoretical. NetApp got hit and we did exactly that.
FYI, here's the perl script we use; it is intended to be run from a Unix host accessing the affect filesystem via NFS; it does depend on cpio behavior, but that's pretty standard across Unixes. I think we've used it from bot Solaris and DEC OSF/1...:
% cat /usr/local/bin/lazurus-expworm #!/usr/local/bin/perl5 # -*-Fundamental-*-
# $Id: //depot/tools/main/lazurus/lazurus-expworm#3 $
# Richard Geiger Network Appliance June 10 1999 # # (Resurrected :-) form the the original "lazurus" # of late 1997... #
select STDERR; $| = 1; select STDOUT; $| = 1;
use Carp; $| = 1;
($Myname = $0) =~ s%^.*/%%;
$Usage = <<LIT; $Myname: usage:
$Myname [-snap <snapshot>] [-mode find|fix|signoff] [-repint <n>] [-tracelev <n>]
-snap specified the snapshot to compare to
-mode specifies whether to build the list to stdout, or recover based on the list on stdin
-repint set the progress report interval, (per # of files checked).
-tracelev sets the directory coverage trace message depth
LIT
sub usage { print STDERR $Usage; exit 1; }
sub help { print STDERR <<LIT; $Usage
$Myname is a helper for recovering files from a snapshot. This version deals with files deleted by the "WormExplore.zip" virus, so by default, it uses the snapshot ".snapshot/virus-snap-one" as the basis for finding files that may have been removed by the virus.
It works on files in the tree rooted at the current directory where it is run. E.g., to repair your home directory, run it while cd'ed to your home directory.
The idea is to run it once in "find" mode, to build a list of files in the snapshot that are nononzero length in the snapshot, but zero length (with a mod time after the first know incidence of the virus) in the current filesystem. You can examine this list, and edit it however you like, and then feed it back to $Myname in "-mode fix" mode.
The -repint and -tracelev options allow you to adjust the verbosity with which $Myname will display information progress messages (to the standard error stream).
In find mode, the list of potentially lost file paths is written to the standard output.
In fix mode, $Myname starts a "cpio -p" process, set up so as to restore files based on the pathnames it reads from the standard input. Files in the "live" tree newer than those in the snapshot will not be overwritten. Files will be recovered with modification times as per those in the snapshot. Restored files will be created with the ownership and group of the user running $Myname, unless it is executed by "root", in which case the file will be created with the owner and group of the original file in the snapshot.
In fix mode, lines beginning with a " " or "$Myname: " will be ignored; thus you can run
cd ~ { for example, to repair any damage in your home diorectory } $Myname > FILES { edit FILES, removing lines or commenting them out with a leading space } $Myname -mode fix < FILES
LIT exit 1; }
$First = "."; $Go = 1;
sub traverse { local($dir, $lev, $onfile, $ondir, $onsymlink) = @_;
local($dirent); local($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks);
local($dirhandle) = "dh$lev";
opendir($dirhandle, $dir);
while (($dirent = readdir($dirhandle))) { if ($dirent eq "." || $dirent eq "..") { next; } ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = lstat("$dir/$dirent"); typsw: { -f _ && do { if ($Go && defined(&$onfile)) { &$onfile("$dir", "$dirent", $lev); } last typsw; } ; -d _ && do { if (defined(&$ondir)) { &$ondir("$dir", "$dirent", $lev); } if ($lev == 0 && $dirent =~ /$First/) { $Go = 1; } if ($Go) { do traverse("$dir/$dirent", $lev+1, $onfile, $ondir, $onsymlink) if -d _; } last typsw; } ; -l "$dir/$dirent" && do { if ($Go && defined(&$onsymlink)) { &$onsymlink("$dir", "$dirent", $lev); } last typsw; } ; } } closedir($dirhandle); }
$Nfound = 0; $Nchecked = 0;
sub dir { my($dir, $file, $lev) = @_;
my($path) = "$dir/$file"; $path =~ s/^.///; my($rpath) = "$Here/$path";
if ($lev < $Tracelev) { printf STDERR "$Myname: checking in $path\n"; } }
sub find { my($dir, $file, $lev) = @_;
my($path) = "$dir/$file"; $path =~ s/^.///; my($rpath) = "$Here/$path";
if (! -z $path && -z $rpath) { @s = stat(_); if ($#s < 0) { die "stat retrieve on "$path" failed?: $!"; } if ($s[9] >= 929041200) # 6/10/99 12noon PDT { print STDOUT "$path\n"; $Nfound++; } }
$Nchecked++; if (($Nchecked % $Repint) == 0) { &report; } }
sub ts { my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); return sprintf("%04d/%02d/%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec); }
sub report { printf STDERR "$Myname: %s checked $Nchecked files; found $Nfound candidates.\n", &ts; }
sub fix { $ret = 0;
chdir "$Snapshot/$Snap" || die "Can't chdir $Snapshot/$Snap: $!";
# fire us up our ole pal cpio... # $cmd = "|/bin/cpio -pdmv $Here"; if (! open(CPIO, $cmd)) { die "couldn't start "$cmd": $!"; }
print STDERR "$Myname: > $cmd\n";
while (<>) { if (! /^($Myname: | )/) { print CPIO $_; } } close CPIO; $sts = $?; if ($sts) { my $sig = $sts & 0x0f; $sts = $sts >> 8; print STDERR "$Myname: *** "$cmd" exited with signal $sig status $sts.\n"; $ret = $sts; }
chdir $Here || dir; open(LAZ, ">>.lazurus") || die "Can't open ">>.lazurus": $!"; printf LAZ "%s $Username fix exit $ret\n", &ts; close LAZ;
exit $ret; }
sub signoff { open(LAZ, ">>.lazurus") || die "Can't open ">>.lazurus": $!"; printf LAZ "%s $Username signoff\n", &ts; close LAZ; exit 0; }
# option switch variables get defaults here...
(@pwent) = getpwuid($<); if ($#pwent < 7) { print STDERR "$Myname: can't get your passwd file entry.\n"; exit 1; } $Username = $pwent[0];
$Snapshot = ".snapshot"; $Snap = "virus-snap-one"; $Mode = "find"; $Repint = 1000; $Tracelev = 0;
while ($#ARGV >= 0) { if ($ARGV[0] eq "-boolopt") { $Boolopt = 1; shift; next; } elsif ($ARGV[0] eq "-snap") { shift; if ($ARGV[0] < 0) { &usage; } $Snap = $ARGV[0]; shift; next; } elsif ($ARGV[0] =~ /^[-]{0,1}signoff/) { shift; $Mode = "signoff"; next; } elsif ($ARGV[0] eq "-mode") { shift; if ($ARGV[0] < 0) { &usage; } $Mode = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-repint") { shift; if ($ARGV[0] < 0) { &usage; } $Repint = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-tracelev") { shift; if ($ARGV[0] < 0) { &usage; } $Tracelev = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-shot") { shift; if ($ARGV[0] < 0) { &usage; } $Snapshot = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-help") { &help; } elsif ($ARGV[0] =~ /^-/) { &usage; } if ($Args ne "") { $Args .= " "; } push(@Args, $ARGV[0]); shift; }
if ($Mode !~ /^(find|fix|signoff)$/) { die "bad mode"; } # TBD
$Here = `/bin/pwd`; chop $Here;
print STDERR "$Myname: starting in $Mode mode in "$Here"...\n";
if ($Mode eq "fix") { &fix; }
if ($Mode eq "signoff") { &signoff; }
open(LAZ, ">>.lazurus") || die "Can't open ">>.lazurus": $!"; printf LAZ "%s $Username\n", &ts; close LAZ;
chdir "$Snapshot/$Snap" || die "Can't chdir $Snapshot/$Snap: $!";
&traverse(".", 0, $Mode, "dir", undef);
&report; print STDERR "$Myname: done.\n";