#!/usr/bin/perl # these are turned off for distribution # use strict; # use warnings; $|++; use locale; use POSIX qw/locale_h strftime/; use File::Basename; use File::Find::Rule; use File::Path qw/mkpath/; use Date::Calc qw/Delta_Days/; use Locale::gettext; use File::Copy; textdomain("clamtk"); setlocale( LC_MESSAGES, "" ); bind_textdomain_codeset( "clamtk", "UTF-8" ); use Gtk2; use Gtk2::SimpleList; use Glib qw/TRUE FALSE/; Gtk2->init; my $VERSION = '3.05'; my $virus_log = ''; my ( $save_log, $hidden, $showall ) = (0) x 3; my $follow_symlinks = 0; my @virus; my $count = 0; # keeps track of displayed files my $num_scanned = 0; # actual number of files scanned my $num_so_far = 0; # number of viruses my %found; my $start_time; my ( $q_state, $l_state ); my ( @files, @quoted ); my $directory = $ENV{HOME} || glob "~"; my $c_dir = "$directory/.clamtk"; my $v_dir = "$c_dir/viruses"; my $l_dir = "$c_dir/history"; my ( $a_tooltip, $authenticate ); my %dirs_scanned; my $scan_pid = ''; my $SCAN; my $toolbar = ''; my $top_label; my $step = 0; # for progressbar; has to be global my $current = 0; # progressbar's current level my $quarantine = 0; my $size_set = 1; my $stopped = 0; # user hit the stop button # maintenance subroutine variables below my ( $new_slist, $new_hlist ); my @q_files = (); my $q_label; my @h_files = (); my $h_label; if ( $> == 0 ) { $authenticate = 'gtk-yes'; $a_tooltip = gettext("Check for signature updates"); } else { $authenticate = 'gtk-no'; $a_tooltip = gettext("You must be root to install updates"); } my $command = ''; # important clamav paths my $FRESHPATH = ( -e '/usr/bin/freshclam' ) ? '/usr/bin/freshclam' : ( -e '/usr/local/bin/freshclam' ) ? '/usr/local/bin/freshclam' : ( -e '/opt/local/bin/freshclam' ) ? '/opt/local/bin/freshclam' : die "freshclam not found!\n"; my $SIGPATH = ( -e '/usr/bin/sigtool' ) ? '/usr/bin/sigtool' : ( -e '/usr/local/bin/sigtool' ) ? '/usr/local/bin/sigtool' : ( -e '/opt/local/bin/sigtool' ) ? '/opt/local/bin/sigtool' : die "sigtool not found!\n"; my $CLAMPATH = ( -e '/usr/bin/clamscan' ) ? '/usr/bin/clamscan' : ( -e '/usr/local/bin/clamscan' ) ? '/usr/local/bin/clamscan' : ( -e '/opt/local/bin/clamscan' ) ? '/opt/local/bin/clamscan' : die "clamscan not found!\n"; $command .= $CLAMPATH; # the $INFO_*'s are for parsing ClamAV 0.90 information my $INFO_DAILY = ''; my $INFO_MAIN = ''; my $INFO_DATE = ''; my $DAILY_PATH = ''; my $MAIN_PATH = ''; my $RARPATH = ( -e '/usr/bin/unrar' ) ? '/usr/bin/unrar' : ( -e '/usr/bin/rar' ) ? '/usr/bin/rar' : ( -e '/usr/local/bin/unrar' ) ? '/usr/local/bin/unrar' : ( -e '/usr/local/bin/rar' ) ? '/usr/local/bin/rar' : ''; $command .= " --unrar=$RARPATH" if ($RARPATH); my $ZIPPATH = ( -e '/usr/bin/unzip' ) ? '/usr/bin/unzip' : ( -e '/usr/local/bin/unzip' ) ? '/usr/local/bin/unzip' : ''; $command .= " --unzip=$ZIPPATH" if ($ZIPPATH); my $FILE = ( -e '/usr/bin/file' ) ? '/usr/bin/file' : ( -e '/usr/local/bin/file' ) ? '/usr/local/bin/file' : die gettext("\"file\" command not found!\n"); $command .= " --no-summary --block-encrypted --detect-broken "; if ( !-d $v_dir ) { eval { mkpath( $v_dir, 0, oct(777) ); }; if ($@) { $q_state = "disabled"; } else { $q_state = "normal"; } } else { $q_state = "normal"; } if ( !-d $l_dir ) { eval { mkpath( $l_dir, 0, oct(777) ); }; if ($@) { $l_state = "disabled"; } else { $l_state = "normal"; } } else { $l_state = "normal"; } my $window = Gtk2::Window->new(); $window->signal_connect( destroy => sub { Gtk2->main_quit; } ); $window->set_default_size( 550, 325 ); $window->set_title("ClamTk Virus Scanner"); $window->set_border_width(5); $window->set_position('center-always'); # I'll leave this here for now (i.e., clam.xpm AND clamtk.png) since # most packagers won't notice that it's been changed. # No big whoop if it stays in, though, since Debian wants an # xpm file for the .menu file. if ( -e "/usr/share/pixmaps/clamtk.png" ) { $window->set_default_icon_from_file("/usr/share/pixmaps/clamtk.png"); } elsif ( -e "/usr/share/pixmaps/clam.xpm" ) { $window->set_default_icon_from_file("/usr/share/pixmaps/clam.xpm"); } my $main_vbox = Gtk2::VBox->new( FALSE, 0 ); $window->add($main_vbox); $main_vbox->show; my @entries = ( [ "FileMenu", undef, gettext("_File") ], [ "ViewMenu", undef, gettext("_View") ], [ "OptionsMenu", undef, gettext("_Options") ], [ "QuarantineMenu", undef, gettext("_Quarantine") ], [ "HelpMenu", undef, gettext("_Help") ], [ "Scan_File", 'gtk-find', gettext("Scan a _File"), "F", gettext("Scan a file"), sub { getfile('file') } ], [ "Quick_Home", 'gtk-go-down', gettext("_Quick Home Scan"), "Q", gettext("Quick Home Scan"), sub { getfile('home') } ], [ "Full_Home", 'gtk-goto-bottom', gettext("Full Home Scan"), "Z", gettext("Full Home Scan"), sub { getfile('full-home') } ], [ "Scan_Directory", 'gtk-zoom-in', gettext("Scan a _Directory"), "D", gettext("Scan a Directory"), sub { getfile('dir') } ], [ "Recursive_Scan", 'gtk-zoom-fit', gettext("_Recursive Scan"), "R", gettext("Recursively scan a directory"), sub { getfile('recur') } ], [ "Exit", 'gtk-quit', gettext("E_xit"), "X", gettext("Quit this program"), sub { Gtk2->main_quit } ], [ "Status", 'gtk-edit', gettext("_Status"), "S", gettext("See how many files are quarantined"), \&quarantine_check ], [ "Maintenance", 'gtk-preferences', gettext("_Maintenance"), "M", gettext("View files that have been quarantined"), \&maintenance, ], [ "Empty", 'gtk-delete', gettext("_Empty Quarantine Folder"), "E", gettext("Delete all files that have been quarantined"), \&del_quarantined ], [ "SysInfo", 'gtk-properties', gettext("System _Information"), "I", gettext("Status of Antivirus programs"), \&sys_info ], [ "UpdateSig", $authenticate, gettext("_Update Signatures"), "U", gettext("Update your virus signatures"), \&update ], [ "About", 'gtk-about', gettext("_About"), "A", gettext("About this program..."), \&about ], ); my @view_entries = ( [ "ManageHistories", undef, gettext("Manage _Histories"), "H", gettext("Select Histories to Delete"), sub { history('delete') }, FALSE ], [ "ClearOutput", 'gtk-clear', gettext("Clear _Output"), "O", gettext("Clear the Display"), \&clear_output, FALSE ], ); my @option_entries = ( [ "SaveToLog", undef, gettext("Save To Log"), "F1", gettext("Save a record of this scan"), sub { $save_log ^= 1 }, FALSE ], [ "ScanHidden", undef, gettext("Scan Hidden Files (.*)"), "F2", gettext("Scan the hidden files"), sub { $hidden ^= 1 }, FALSE ], [ "DisplayAll", undef, gettext("Display All Files"), "F3", gettext("Display all files scanned"), sub { $showall ^= 1 }, FALSE ], [ "FollowLinks", undef, gettext("Follow Symbolic Links"), "F4", gettext("Follow Symbolic Links"), sub { $follow_symlinks ^= 1 }, FALSE ], [ "Quarantine", 'gtk-refresh', gettext("Quarantine Infected Files"), "F5", gettext("Quarantine Infected Files"), sub { $quarantine ^= 1 }, FALSE ], [ "SizeLimit", undef, gettext("No Maximum Size"), "F6", gettext("No Maximum Size"), sub { $size_set ^= 1 }, FALSE ], ); my $ui_info = " "; my $actions = Gtk2::ActionGroup->new("Actions"); $actions->add_actions( \@entries, undef ); $actions->add_actions( \@view_entries, undef ); $actions->add_toggle_actions( \@option_entries, undef ); my $ui = Gtk2::UIManager->new; $ui->insert_action_group( $actions, 0 ); $window->add_accel_group( $ui->get_accel_group ); $ui->add_ui_from_string($ui_info); $main_vbox->pack_start( $ui->get_widget("/MenuBar"), FALSE, FALSE, 0 ); # These are the GUI's for scanning, clearing and exiting $toolbar = Gtk2::Toolbar->new; $toolbar->set_style('icons'); my $tt = Gtk2::Tooltips->new(); # We can set the size of the toolbar stuff here, but # for now, I like the default # $toolbar->set_icon_size('menu'); my $scan_file = Gtk2::ToolButton->new_from_stock('gtk-find'); $scan_file->signal_connect( 'clicked' => sub { getfile('file') } ); $scan_file->set_tooltip( $tt, gettext("Scan a file"), "" ); $toolbar->insert( $scan_file, -1 ); my $scan_home = Gtk2::ToolButton->new_from_stock('gtk-home'); $scan_home->signal_connect( 'clicked' => sub { getfile('home') } ); $scan_home->set_tooltip( $tt, gettext("Scan your home directory"), "" ); $toolbar->insert( $scan_home, -1 ); my $scan_dir = Gtk2::ToolButton->new_from_stock('gtk-zoom-in'); $scan_dir->signal_connect( 'clicked' => sub { getfile('dir') } ); $scan_dir->set_tooltip( $tt, gettext("Scan a directory"), "" ); $toolbar->insert( $scan_dir, -1 ); $toolbar->insert( Gtk2::SeparatorToolItem->new, -1 ); my $scan_clear = Gtk2::ToolButton->new_from_stock('gtk-clear'); $scan_clear->signal_connect( 'clicked' => sub { clear_output(); } ); $scan_clear->set_tooltip( $tt, gettext("Clear the display"), "" ); $toolbar->insert( $scan_clear, -1 ); my $scan_stop = Gtk2::ToolButton->new_from_stock('gtk-stop'); $scan_stop->signal_connect( 'clicked' => sub { @quoted = (); kill 15, $scan_pid if ($scan_pid); $top_label->set_text( gettext("Please wait...") ); waitpid( $scan_pid, 0 ); # this close returns the stupid readline() error. # not sure how to fix it yet, besides commenting # out 'use warnings' :) it's the only way to immediately # stop the $SCAN so far... close($SCAN); # or warn "Unable to close scanner! $!\n"; $top_label->set_text(""); $stopped = 1; } ); $scan_stop->set_tooltip( $tt, gettext("Stop scanning now"), "" ); $toolbar->insert( $scan_stop, -1 ); my $scan_exit = Gtk2::ToolButton->new_from_stock('gtk-quit'); $scan_exit->signal_connect( 'clicked' => sub { Gtk2->main_quit } ); $scan_exit->set_tooltip( $tt, gettext("Quit"), "" ); $toolbar->insert( $scan_exit, -1 ); $main_vbox->pack_start( $toolbar, FALSE, FALSE, 1 ); my $top_frame = Gtk2::Frame->new( gettext("Information") ); $main_vbox->pack_start( $top_frame, FALSE, FALSE, 0 ); # This is the top label where scanning messages are displayed $top_label = Gtk2::Label->new(); $top_frame->add($top_label); $top_label->set_justify('center'); $top_label->set_ellipsize('middle'); # This scrolled window holds the slist my $scrolled_win = Gtk2::ScrolledWindow->new; $scrolled_win->set_shadow_type('etched_in'); $scrolled_win->set_policy( 'automatic', 'automatic' ); $main_vbox->pack_start( $scrolled_win, TRUE, TRUE, 0 ); my $slist = create_list(); $scrolled_win->add($slist); $scrolled_win->grab_focus(); $slist->get_selection->set_mode('single'); $slist->set_rules_hint(TRUE); $slist->set_headers_clickable(TRUE); # can't be reorderable - messes up the \&row_clicked function $slist->set_reorderable(FALSE); map { $_->set_fixed_width(250) } $slist->get_columns; map { $_->set_sizing('fixed') } $slist->get_columns; $slist->set( hover_selection => TRUE, hover_expand => TRUE ); my $tooltips = Gtk2::Tooltips->new; $tooltips->set_tip( $slist, gettext("Select a file and right-click for options...") ); $tooltips->disable; # this anonymous sub handles the row_clicked feature $slist->get_selection->signal_connect( changed => sub { my @sel = $slist->get_selected_indices; my $deref = $sel[0]; defined $deref or return; $top_label->set_markup( sprintf gettext("File: %s Status: %s"), $virus[$deref]{full}, $virus[$deref]{status} ); } ); # below: the right-click functionality. also uses 'sub confirm'. $slist->signal_connect( button_press_event => sub { my ( $widget, $event ) = @_; return FALSE unless $event->button == 3; my @sel = $slist->get_selected_indices; my $deref = $sel[0]; defined $deref or return; my $menu = Gtk2::Menu->new(); my $quar_pop = Gtk2::ImageMenuItem->new( gettext('Quarantine this file') ); my $quar_image = Gtk2::Image->new_from_stock( 'gtk-refresh', 'menu' ); $quar_pop->set_image($quar_image); $quar_pop->signal_connect( activate => sub { main_confirm( $deref, "q" ) } ); $quar_pop->show(); $menu->append($quar_pop) unless dirname( $virus[$deref]{full} ) =~ /^\/(proc|sys|dev)/; my $delete_pop = Gtk2::ImageMenuItem->new( gettext('Delete this file') ); my $del_image = Gtk2::Image->new_from_stock( 'gtk-delete', 'menu' ); $delete_pop->set_image($del_image); $delete_pop->signal_connect( activate => sub { main_confirm( $deref, "d" ) } ); $delete_pop->show(); $menu->append($delete_pop) unless dirname( $virus[$deref]{full} ) =~ /^\/(proc|sys|dev)/; my $save_pop = Gtk2::ImageMenuItem->new_from_stock( 'gtk-save-as', undef ); $save_pop->signal_connect( activate => sub { my $save_dialog = Gtk2::FileChooserDialog->new( gettext('Save As...'), undef, 'save', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok', ); $save_dialog->set_do_overwrite_confirmation(TRUE); if ( "ok" eq $save_dialog->run ) { my $tmp = $save_dialog->get_filename; $save_dialog->destroy(); move( $virus[$deref]{full}, $tmp ) or do { show_message_dialog( $window, 'error', 'close', gettext("Could not save that file.") ); return TRUE; }; show_message_dialog( $window, 'info', 'close', gettext("File saved.") ); } else { $save_dialog->destroy(); } } ); $save_pop->show(); $menu->append($save_pop); my $cancel_pop = Gtk2::ImageMenuItem->new_from_stock( 'gtk-cancel', undef ); $cancel_pop->signal_connect( activate => sub { return; } ); $cancel_pop->show(); $menu->append($cancel_pop); $menu->popup( undef, undef, undef, undef, $event->button, $event->time ); return TRUE; } ); my $stats_box = Gtk2::Frame->new( gettext("Status") ); $main_vbox->pack_start( $stats_box, FALSE, FALSE, 2 ); # bottom_box keeps track of # scanned, # of viruses, and time my $bottom_box = Gtk2::HBox->new( FALSE, 0 ); $stats_box->add($bottom_box); my $left_status = Gtk2::Label->new( gettext("Files Scanned: ") ); $left_status->set_alignment( 0.0, 0.5 ); $bottom_box->pack_start( $left_status, TRUE, TRUE, 4 ); my $mid_status = Gtk2::Label->new( gettext("Viruses Found: ") ); $mid_status->set_alignment( 0.0, 0.5 ); $bottom_box->pack_start( $mid_status, TRUE, TRUE, 0 ); my $right_status = Gtk2::Label->new( gettext("Ready") ); $right_status->set_alignment( 0.0, 0.5 ); $bottom_box->pack_start( $right_status, TRUE, TRUE, 0 ); my $pb = Gtk2::ProgressBar->new; $main_vbox->pack_start( $pb, FALSE, FALSE, 0 ); $pb->set_fraction(0); $window->show_all(); # This is to combine any and all startup checks startup_prefs(); if (@ARGV) { my $input = $ARGV[0]; # safety net unless ( -d $input || -f $input ) { die sprintf gettext("Unable to scan %s\n"), $input; } # doesn't like the end slash ('/') $input =~ s/\/$//; # it's either a full path... if ( $input =~ /^\// ) { getfile( 'cmd-scan', $input ); } # ...or it's not. else { my $top = glob "~"; $top .= "/$input"; getfile( 'cmd-scan', $top ); } } Gtk2->main; sub about { my $about = Gtk2::AboutDialog->new; $about->set_authors("Dave M, dave.nerd gmail.com"); $about->set_version($VERSION); $about->set_copyright("Copyright © 2004-2007"); my @translators = ( 'Karel Hudan, Czech (cs_CZ)', 'Jimmy Christensen, Danish (da_DK)', 'Ronny Steiner, German (de_DE)', 'Mariano Rojo, Spanish (es_ES)', 'Alain Bernard, French (fr_FR)', 'Román Pena, Galician (gl_ES)', 'Edoardo Tosca, Italian (it_IT)', 'Tobia Fasciati, Italian (it_IT)', 'Robert Tomasik, Polish (pl_PL)', 'Bruno Diniz, Portugese (pt_BR)', 'Vitaly Lipatov, Russian (ru_RU)', 'Petter Viklund, Swedish (sv_SE)', 'Tao Wei, Chinese (zh_CN)', ); my @artists = ( 'Edoardo Tosca (Website Design)', 'Gerald Ganson (Icon Design)', ); my $t_list = join "\n", @translators; $about->set_translator_credits($t_list); my $a_list = join "\n", @artists; $about->set_artists($a_list); my $logo = -e '/usr/share/pixmaps/clamtk.png' ? '/usr/share/pixmaps/clamtk.png' : 'usr/share/pixmaps/clamtk.xpm' ? '/usr/share/pixmaps/clamtk.xpm' : ''; my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file($logo); $about->set_logo($pixbuf); $about->set_website('http://clamtk.sf.net'); $about->set_comments( "ClamTk is a GUI front-end for the ClamAV antivirus using gtk2-perl." ); $about->set_license( "ClamTk, (c) 2004-2007. All rights reserved.\n\n" . "This program is free software; you can redistribute it\n" . "and/or modify it under the same terms as Perl itself." ); $about->run; $about->destroy; } sub create_list { my $list = Gtk2::SimpleList->new( gettext('File') => 'markup', gettext('Status') => 'markup', ); return $list; } sub getfile { my ($option) = shift; my $cmd_input = shift; # $option will be either "home", "full-home", "file", "dir", # "recur", or "cmd-scan" $pb->set_fraction(0); Gtk2->main_iteration while ( Gtk2->events_pending ); clear_output(); chdir($directory) or chdir("/tmp"); my ( $filename, $dir, $dialog ); if ( $option eq 'home' ) { $top_label->set_text( gettext("Please wait...") ); Gtk2->main_iteration while ( Gtk2->events_pending ); my $rule = File::Find::Rule->new; $rule->file; $rule->readable; $rule->maxdepth(1); if ($follow_symlinks) { $rule->extras( { follow => 1 } ) unless $directory =~ /\/(proc|sys|dev)/; } Gtk2->main_iteration while ( Gtk2->events_pending ); @files = $rule->in($directory); } elsif ( $option eq 'full-home' ) { $top_label->set_text( gettext("Please wait...") ); Gtk2->main_iteration while ( Gtk2->events_pending ); my $rule = File::Find::Rule->new; $rule->file; $rule->readable; if ($follow_symlinks) { $rule->extras( { follow => 1 } ) unless $directory =~ /\/(proc|sys|dev)/; } Gtk2->main_iteration while ( Gtk2->events_pending ); @files = $rule->in($directory); } elsif ( $option eq 'file' ) { $dialog = Gtk2::FileChooserDialog->new( gettext('Select File'), undef, 'open', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok', ); $dialog->set_select_multiple(TRUE); if ( "ok" eq $dialog->run ) { $top_label->set_text( gettext("Please wait...") ); $window->queue_draw; Gtk2->main_iteration while ( Gtk2->events_pending ); @files = $dialog->get_filenames; $window->queue_draw; $dialog->destroy; $window->queue_draw; Gtk2->main_iteration while ( Gtk2->events_pending ); } else { $dialog->destroy; return; } } elsif ( $option eq 'dir' ) { $dialog = Gtk2::FileChooserDialog->new( gettext('Select a Directory (directory scan)'), undef, 'select-folder', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok', ); if ( "ok" eq $dialog->run ) { $dir = $dialog->get_filename; $top_label->set_text( gettext("Please wait...") ); Gtk2->main_iteration while ( Gtk2->events_pending ); $window->queue_draw; $dialog->destroy; $window->queue_draw; $dir ||= $directory; my $rule = File::Find::Rule->new; $rule->file; $rule->readable; $rule->maxdepth(1); if ($follow_symlinks) { $rule->extras( { follow => 1 } ) unless $dir =~ /\/(proc|sys|dev)/; } Gtk2->main_iteration while ( Gtk2->events_pending ); @files = $rule->in($dir); } else { $dialog->destroy; return; } } elsif ( $option eq 'recur' ) { $dialog = Gtk2::FileChooserDialog->new( gettext('Select a Directory (recursive scan)'), undef, 'select-folder', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok' ); if ( "ok" eq $dialog->run ) { $dir = $dialog->get_filename; $top_label->set_text( gettext("Please wait...") ); Gtk2->main_iteration while ( Gtk2->events_pending ); $window->queue_draw; $dialog->destroy; $window->queue_draw; $dir ||= $directory; my $rule = File::Find::Rule->new; $rule->file; $rule->readable; if ($follow_symlinks) { $rule->extras( { follow => 1 } ) unless $dir =~ /\/(proc|sys|dev)/; } Gtk2->main_iteration while ( Gtk2->events_pending ); @files = $rule->in($dir); } else { $dialog->destroy; return; } } elsif ( $option eq 'cmd-scan' ) { # toggle the DisplayAll switch to show all files (for save-as) my $change_on = $actions->get_action( $option_entries[2]->[0] ); $change_on->set_active(TRUE); if ( -d $cmd_input ) { $top_label->set_text( gettext("Please wait...") ); $window->queue_draw; my $rule = File::Find::Rule->new; $rule->file; $rule->readable; $rule->maxdepth(1); if ($follow_symlinks) { $rule->extras( { follow => 1 } ) unless $dir =~ /\/(proc|sys|dev)/; } Gtk2->main_iteration while ( Gtk2->events_pending ); @files = $rule->in($cmd_input); } else { $top_label->set_text( gettext("Please wait...") ); $window->queue_draw; Gtk2->main_iteration while ( Gtk2->events_pending ); @files = $cmd_input; } } else { die gettext("Shouldn't reach this."), "\n"; } # start the timer - replaces the "Ready" $start_time = time; $right_status->set_text( gettext("Elapsed time: ") ); if ( $option eq 'file' ) { scan(@files); } else { my @new; if ( $hidden == 0 && $option ne 'recur' && $option ne 'full-home' ) { @files = grep { basename($_) !~ /^\./ } @files; } if ( scalar(@files) == 1 ) { $step = 1; } elsif ( scalar(@files) > 1 ) { $step = 1 / scalar(@files); } else { $step = 1; } if ($size_set) { my @large; foreach my $foo (@files) { if ( -s $foo >= 20_000_000 ) { push( @large, $foo ); } else { push( @new, $foo ); } } if (@large) { foreach my $too_big (@large) { $top_label->set_text( sprintf gettext("Scanning %s..."), $too_big ); $num_scanned++; timer(); $virus[$count]{full} = $too_big; $virus[$count]{base} = basename($too_big); $virus[$count]{status} = gettext("Not scanned (size)"); display(); next; } } } else { @new = @files; } if ( @new ) { my @send; while ( my $t = pop(@new) ) { last if ($stopped); push( @send, $t ); if ( scalar(@send) == 255 ) { scan(@send); @send = (); } else { next; } } scan(@send) if ( @send ); } } clean_up(); } sub scan { my @get = @_; @quoted = map { quotemeta($_) } @get; timer(); my $pid = open( $SCAN, "$command @quoted |" ); defined($pid) or die gettext("couldn't fork: "), "$!\n"; my $scan_count = 0; $top_label->set_text( sprintf gettext("Scanning %s..."), $get[$scan_count] ); $scan_pid = $pid; # this is for the 'stop button' while (<$SCAN>) { Gtk2->main_iteration while Gtk2->events_pending; my ( $file, $status ) = split /:/; chomp($file) if ( defined $file ); chomp($status) if ( defined $status ); next unless ( -e $file && $status ); next if ( $status =~ /module failure/ ); $dirs_scanned{ dirname($file) } = 1 unless ( dirname($file) =~ /\/tmp\/clamav/ || dirname($file) eq "." ); $virus[$count]{base} = basename($file); $status =~ s/\s+FOUND$//; $virus[$count]{full} = $file; # ignore files in archives - we just want the end-result. # we still allow it to scan; clamav will show the result # this method doesn't count it, though... next if ( $virus[$count]{full} =~ /\/tmp\/clamav/ ); $virus[$count]{status} = $status; timer(); # clean_words mean no viruses... haven't seen any others than this my $clean_words = join( '|', "OK", "Zip module failure", "RAR module failure", "Encrypted.RAR", "Encrypted.Zip", "Empty file", "Excluded", "Input/Output error", "Broken.Executable", "Oversized.Zip" ); if ( $status !~ /$clean_words/ ) { # a virus $found{ $virus[$count]{full} } = $virus[$count]{status}; my $current_status = $virus[$count]{status}; if ($quarantine) { if ( dirname( $virus[$count]{full} ) !~ /\/tmp\/clamav/ and dirname( $virus[$count]{full} ) !~ /^\/(proc|sys|dev)/ ) { move_to_quarantine($count); $virus[$count]{status} = "$current_status " . gettext("(Quarantined)"); } } } $num_so_far = keys %found; if ( $num_so_far > 0 ) { $mid_status->set_markup( sprintf gettext("Viruses Found: %d"), $num_so_far ); } else { $mid_status->set_text( sprintf gettext("Viruses Found: %d"), $num_so_far ); } $num_scanned++; display(); # resize hack below my ( $w, $h ) = $window->get_size; unless ( $w == 550 && $h == 325 ) { $window->resize( 550, 325 ); } $scan_count++; if ( defined( $quoted[$scan_count] ) ) { Gtk2->main_iteration while ( Gtk2->events_pending ); $top_label->set_text( sprintf gettext("Scanning %s..."), basename( $get[$scan_count] ) ); Gtk2->main_iteration while ( Gtk2->events_pending ); } } close($SCAN); # or warn "Unable to close scanner! $!\n"; $pb->set_text( gettext("Percent complete: 100") ); Gtk2->main_iteration while ( Gtk2->events_pending ); } sub display { timer(); use encoding 'utf8'; $virus[$count]{status} =~ s/\s+$//; $virus[$count]{status} =~ s/^\s//; if (( $virus[$count]{status} ne "OK" && $virus[$count]{status} ne "Empty file" ) || $showall ) { push @{ $slist->{data} }, [ $virus[$count]{base}, $virus[$count]{status} ]; $count++; } map { $_->set_fixed_width(250) } $slist->get_columns; map { $_->set_sizing('fixed') } $slist->get_columns; map { $_->set_resizable(TRUE) } $slist->get_columns; timer(); if ( $current + $step <= 0 || $current + $step >= 1.0 ) { $current = .99; } else { $current += $step; } $pb->set_fraction($current); $pb->set_text( sprintf gettext("Percent complete: %2d"), $current * 100 ); Gtk2->main_iteration while ( Gtk2->events_pending ); } sub timer { Gtk2->main_iteration while ( Gtk2->events_pending ); my $now = time; my $seconds = $now - $start_time; my $s = sprintf "%02d", ( $seconds % 60 ); my $m = sprintf "%02d", ( $seconds - $s ) / 60; $right_status->set_text( sprintf gettext("Elapsed time: %s"), "$m:$s" ); $left_status->set_text( sprintf gettext("Files Scanned: %d"), $num_scanned ); $window->queue_draw; Gtk2->main_iteration while ( Gtk2->events_pending ); } sub clean_up { $pb->set_fraction(1.0); $pb->set_text(""); $count ||= 0; # highlight the quarantined or deleted files my $utf8_string = gettext("(Quarantined)"); for ( 0 .. $#virus ) { if ( $virus[$_]{status} =~ /$utf8_string/ ) { main_slist_delete($_); } } $tooltips->enable; my $db_total = num_of_sigs(); my $REPORT; # filehandle for histories log if ($save_log) { my ( $mon, $day, $year ) = split / /, strftime( '%b %d %Y', localtime ); $virus_log = "$mon-$day-$year" . ".log"; # sort the directories scanned for display my @sorted = sort { length $a <=> length $b } keys %dirs_scanned; if ( open $REPORT, ">>", "$l_dir/$virus_log" ) { print $REPORT "\nClamTk, v$VERSION\n", scalar localtime, "\n"; print $REPORT sprintf gettext("ClamAV Signatures: %d\n"), $db_total; print $REPORT gettext("Infected files set to be quarantined.\n") if ($quarantine); print $REPORT gettext("Directories Scanned:\n"); for my $list (@sorted) { print $REPORT "$list\n"; } printf $REPORT gettext( "\nFound %d possible %s (%d %s scanned).\n\n"), $num_so_far, $num_so_far == 1 ? gettext("virus") : gettext("viruses"), $num_scanned, $num_scanned == 1 ? gettext("file") : gettext("files"); } else { $top_label->set_text( gettext("Could not write to logfile. Check permissions.") ); $save_log = 0; } } $db_total =~ s/(\w+)\s+$/$1/; $top_label->set_text( sprintf gettext("Scanning complete (%d signatures)"), $db_total ); $left_status->set_text( sprintf gettext("Files Scanned: %d"), $num_scanned ); if ( $num_so_far == 0 ) { $mid_status->set_text( sprintf gettext("Viruses Found: %d"), $num_so_far ); } $right_status->set_text( gettext("Ready") ); $window->queue_draw; if ( $num_so_far == 0 ) { print $REPORT gettext("No viruses found.\n") if ($save_log); } else { if ($save_log) { while ( my ( $key, $value ) = each %found ) { if ( length($key) > 33 ) { substr( $key, 33 ) = '...'; } if ( length($value) > 33 ) { $value = substr( $value, 33, "..." ); substr( $value, 33 ) = '...'; } printf $REPORT "%-38s %38s\n", $key, $value; } } } if ($save_log) { print $REPORT "-" x 77, "\n"; close($REPORT) if ( fileno($REPORT) ); } # reset things $count = 0; $num_so_far = 0; $num_scanned = 0; %found = (); %dirs_scanned = (); @files = (); @quoted = (); $current = 0; $stopped = 0; } sub clear_output { return if ( scalar(@files) > 0 ); $pb->set_fraction(0); @{ $slist->{data} } = (); $window->resize( 550, 325 ); $window->queue_draw; $left_status->set_text( gettext("Files Scanned: ") ); $mid_status->set_text( gettext("Viruses Found: ") ); $right_status->set_text( gettext("Ready") ); $top_label->set_text(""); $tooltips->disable; map { $_->set_fixed_width(250) } $slist->get_columns; map { $_->set_sizing('fixed') } $slist->get_columns; } sub update { if ( $> != 0 ) { show_message_dialog( $window, 'info', 'close', gettext("You must be root to install updates.") ); return; } $top_label->set_text( gettext("Please wait, checking for updates...") ); Gtk2->main_iteration while Gtk2->events_pending; $main_vbox->queue_draw; $window->queue_draw; my @result; eval { local $SIG{ALRM} = sub { die "failed" }; alarm 60; @result = `$FRESHPATH --stdout`; alarm 0; }; if ( $@ && $@ eq "failed" ) { $top_label->set_text( gettext("Unable to retrieve updates. Try again later.") ); return; } my $showthis; if ( !@result ) { $top_label->set_text( gettext("Unable to retrieve updates. Try again later.") ); } else { foreach my $line (@result) { if ( $line =~ /Database updated .(\d+) signatures/ ) { $showthis = sprintf gettext( "Your virus signatures have been updated (%d signatures)." ), $1; $top_label->set_text($showthis); last; } elsif ( $line =~ /WARNING: Incremental update failed/ ) { $showthis = sprintf gettext( "Unable to retrieve update. Please try again later."); $top_label->set_text($showthis); } } $top_label->set_text( gettext("Your virus signatures are up-to-date.") ) if ( !$showthis ); } $window->queue_draw; } sub quarantine_check { if ( !-d $v_dir ) { show_message_dialog( $window, 'error', 'close', gettext("No virus directory available.") ); return; } my @trash; unless ( opendir( DIR, $v_dir ) ) { show_message_dialog( $window, 'error', 'close', gettext("Unable to open the virus directory.") ); return; } @trash = grep { -f "$v_dir/$_" } readdir(DIR); closedir(DIR); my $del = scalar(@trash); if ( !$del ) { show_message_dialog( $window, 'info', 'ok', gettext("No items currently quarantined.") ); } else { my $notice = sprintf gettext("%d item(s) currently quarantined."), $del; show_message_dialog( $window, 'info', 'ok', $notice ); } } sub del_quarantined { unless ( -e $v_dir ) { show_message_dialog( $window, 'error', 'close', gettext("There is no quarantine directory to empty.") ); return; } else { my @trash; unless ( opendir( DIR, $v_dir ) ) { show_message_dialog( $window, 'error', 'close', gettext("Unable to open the virus directory.") ); } @trash = grep { -f "$v_dir/$_" } readdir(DIR); closedir(DIR); if ( scalar(@trash) == 0 ) { show_message_dialog( $window, 'info', 'close', gettext("There are no quarantined items to delete.") ); } else { my $del = 0; foreach (@trash) { unlink "$v_dir/$_" and $del++; } my $notice = sprintf gettext("Removed %d item(s)."), $del; show_message_dialog( $window, 'info', 'close', $notice ); } } } sub move_to_quarantine { my $number = shift; my $basename = $virus[$number]{base}; if ( not -e $v_dir or not -d $v_dir ) { show_message_dialog( $window, 'info', 'close', gettext("Quarantine directory does not exist.") ); return; } chmod oct(600), $virus[$number]{full}; system( "mv", $virus[$number]{full}, "$v_dir/$basename" ); rename( "$v_dir/$basename", "$v_dir/$basename.VIRUS" ); if ( not -e $virus[$number]{full} ) { return 1; } else { return -1; } } #------------------history and history files stuff------------------- sub history { @h_files = glob "$l_dir/*.log"; my $new_win = Gtk2::Window->new; $new_win->signal_connect( destroy => sub { $new_win->destroy } ); $new_win->set_default_size( 260, 200 ); $new_win->set_title( gettext("Scanning Histories") ); my $new_vbox = Gtk2::VBox->new; $new_win->add($new_vbox); my $s_win = Gtk2::ScrolledWindow->new; $s_win->set_shadow_type('etched-in'); $s_win->set_policy( 'automatic', 'automatic' ); $new_vbox->pack_start( $s_win, TRUE, TRUE, 0 ); $new_hlist = Gtk2::SimpleList->new( gettext('Histories') => 'text', ); $s_win->add($new_hlist); my $new_hbox = Gtk2::HButtonBox->new; $new_vbox->pack_start( $new_hbox, FALSE, FALSE, 0 ); my $hist_view = Gtk2::Button->new_with_label( gettext("View") ); $new_hbox->add($hist_view); $hist_view->signal_connect( clicked => \&view_box, "viewer" ); my $pos_quit = Gtk2::Button->new_with_label( gettext("Close Window") ); $new_hbox->add($pos_quit); $pos_quit->signal_connect( clicked => sub { $new_win->destroy } ); my $del_single = Gtk2::Button->new_with_label( gettext("Delete") ); $new_hbox->add($del_single); $del_single->signal_connect( clicked => \&history_del_single, "del_single" ); my $del_all = Gtk2::Button->new_with_label( gettext("Delete All") ); $new_hbox->add($del_all); $del_all->signal_connect( clicked => \&history_del_all, "del_all" ); $h_label = Gtk2::Label->new(); $new_vbox->pack_start( $h_label, FALSE, FALSE, 2 ); for my $opt (@h_files) { push @{ $new_hlist->{data} }, basename($opt); } $new_win->set_position('mouse'); $new_win->show_all; } sub view_box { my @sel = $new_hlist->get_selected_indices; return if ( !@sel ); my $deref = $sel[0]; return if ( not exists $h_files[$deref] ); my $base = basename( $h_files[$deref] ); my $view_win = Gtk2::Dialog->new( sprintf( gettext("Viewing %s"), $base ), undef, [], 'gtk-close' => 'close' ); $view_win->set_default_response('close'); $view_win->signal_connect( response => sub { $view_win->destroy } ); $view_win->set_default_size( 600, 350 ); my $textview = Gtk2::TextView->new; $textview->set( editable => FALSE ); my $FILE; # filehandle for histories log unless ( open( $FILE, "<", $h_files[$deref] ) ) { my $notice = sprintf gettext("Problems opening %s..."), $h_files[$deref]; show_message_dialog( $window, 'error', 'ok', $notice ); return; } my $text; $text = do { local $/ = undef; $text = <$FILE>; }; close($FILE) or warn sprintf gettext("Unable to close FILE %s! %s\n"), $h_files[$deref]; my $textbuffer = $textview->get_buffer; $textbuffer->create_tag( 'mono', family => 'Monospace' ); $textbuffer->insert_with_tags_by_name( $textbuffer->get_start_iter, $text, 'mono' ); my $scroll_win = Gtk2::ScrolledWindow->new; $scroll_win->set_border_width(5); $scroll_win->set_shadow_type('etched-in'); $scroll_win->set_policy( 'automatic', 'automatic' ); $view_win->vbox->pack_start( $scroll_win, TRUE, TRUE, 0 ); $scroll_win->add($textview); $view_win->show_all(); } sub history_del_single { my @sel = $new_hlist->get_selected_indices; return if ( !@sel ); my $deref = $sel[0]; return if ( not exists $h_files[$deref] ); unlink $h_files[$deref]; if ( -e $h_files[$deref] ) { my $notice = sprintf gettext("Unable to delete %s!"), $h_files[$deref]; show_message_dialog( $window, 'error', 'ok', $notice ); return; } splice @{ $new_hlist->{data} }, $deref, 1; my $base = basename( $h_files[$deref] ); $h_label->set_text( sprintf gettext("Deleted %s."), $base ); @h_files = glob "$l_dir/*"; } sub history_del_all { return unless (@h_files); my $confirm_message = gettext("Really delete all history logs?"); my $confirm = Gtk2::MessageDialog->new( $window, [qw(modal destroy-with-parent)], 'question', 'ok-cancel', $confirm_message ); if ( "cancel" eq $confirm->run ) { $confirm->destroy; return; } else { $confirm->destroy; my @not_del; my $size = @h_files; foreach (@h_files) { unlink($_) or push( @not_del, $_ ); } if ( scalar(@not_del) >= 1 ) { $h_label->set_text( sprintf gettext("Could not delete files: %s!"), @not_del ); } else { show_message_dialog( $window, 'info', 'ok', gettext("Successfully removed history logs.") ); } splice @{ $new_hlist->{data} }, 0, $size; @h_files = glob "$l_dir/*"; } } #-----------------^history and history files stuff^------------------ sub startup_prefs { # this is an effort to combine any # and all startup things, such as the upcoming prefs file # theoretically, this next line shouldn't be necessary if ( $q_state eq "disabled" || $l_state eq "disabled" ) { show_message_dialog( $window, 'error', 'close', gettext( "Unable to create personal directory - check permissions." ) ); } num_of_sigs(); date_diff(); first_run(); } sub sys_info { my ( $return, $version, $number ); # ClamAV version my $ver_info = `$CLAMPATH -V`; chomp($ver_info); ( $version = $ver_info ) =~ s/^(\S+\s+\S+\.\S+(?:\.\d+))\/.*$/$1/; # Number of signatures $number = num_of_sigs(); my $total = sprintf gettext( "\nBuild: %s\t\n\n" . "Signatures: %d\t\n" . "(%s)\n\n" . "GUI Version: %s\n" ), $version, $number, $INFO_DATE, $VERSION; show_message_dialog( $window, 'info', 'ok', $total ); } sub date_diff { return unless ($INFO_DATE); my ( $day1, $month1, $year1 ) = split / /, strftime( '%d %m %Y', localtime ); my %months = ( 'Jan' => 1, 'Feb' => 2, 'Mar' => 3, 'Apr' => 4, 'May' => 5, 'Jun' => 6, 'Jul' => 7, 'Aug' => 8, 'Sep' => 9, 'Oct' => 10, 'Nov' => 11, 'Dec' => 12, ); my ( $day2, $month2, $year2 ) = split / /, $INFO_DATE; return unless ( $day2 && $month2 && $year2 ); my $diff = Delta_Days( $year1, $month1, $day1, $year2, $months{$month2}, $day2 ); if ( $diff <= -5 ) { $diff *= -1; # $diff returns a negative number, so... my $warning = sprintf gettext( "Warning:\nYour virus signatures are %d days old!"), $diff; show_message_dialog( $window, 'warning', 'ok', $warning ); } else { return; } } sub first_run { return if ( -e "$c_dir/first_run" || $> != 0 ); my $warning = gettext( "Some distributions do not automatically edit\n" . "freshclam.conf and clamd.conf under /etc.\n" . "Please edit those before attempting signature updates.\n" ); show_message_dialog( $window, 'warning', 'ok', $warning ); my $FILE; # filehandle to create first_run txt file open( $FILE, ">", "$c_dir/first_run" ) or warn gettext("Couldn't create 'first_run' file...\n"); close($FILE) or warn sprintf gettext("Couldn't close FILE %s! %s\n"), "$c_dir/first_run", $!; } sub num_of_sigs { find_defs(); if ($MAIN_PATH) { my $FILE; if ( open( $FILE, "<", $MAIN_PATH ) ) { while (<$FILE>) { if (/ClamAV-VDB:\S+\s+\S+\s+\S+.*?\+\d+:\d+:(\d+)/) { $INFO_MAIN = $1; last; } close($FILE); } } else { $INFO_MAIN = 0; } } else { $INFO_MAIN = 0; } if ($DAILY_PATH) { my $FILE; if ( open( $FILE, "<", $DAILY_PATH ) ) { while (<$FILE>) { if (/ClamAV-VDB:(\S+\s+\S+\s+\S+).*\+\d+:\d+:(\d+)/) { $INFO_DATE = $1; $INFO_DAILY = $2; last; } } close($FILE); } else { $INFO_DATE = '01 Jan 1970'; $INFO_DAILY = 0; } } if ( $INFO_MAIN && $INFO_DAILY ) { return ( $INFO_MAIN + $INFO_DAILY ); } else { return "Unknown"; } } sub find_defs { for my $dir_list ( '/var/lib/clamav', '/var/clamav', '/opt/local/share/clamav', '/usr/share/clamav', '/usr/local/share/clamav' ) { for my $daily_info_file ( 'daily.inc/daily.info', 'daily.cvd' ) { if ( -e "$dir_list/$daily_info_file" ) { $DAILY_PATH = "$dir_list/$daily_info_file"; last; } } for my $main_info_file ( 'main.inc/main.info', 'main.cvd' ) { if ( -e "$dir_list/$main_info_file" ) { $MAIN_PATH = "$dir_list/$main_info_file"; last; } } } } sub maintenance { my $main_win = Gtk2::Window->new; $main_win->signal_connect( destroy => sub { $main_win->destroy; } ); $main_win->set_default_size( 250, 200 ); $main_win->set_title( gettext("Quarantine") ); my $new_vbox = Gtk2::VBox->new; $main_win->add($new_vbox); @q_files = glob "$v_dir/*"; my $s_win = Gtk2::ScrolledWindow->new; $s_win->set_shadow_type('etched-in'); $s_win->set_policy( 'automatic', 'automatic' ); $new_vbox->pack_start( $s_win, TRUE, TRUE, 0 ); $new_slist = Gtk2::SimpleList->new( gettext('File') => 'text', ); $s_win->add($new_slist); my $new_hbox = Gtk2::HButtonBox->new; $new_vbox->pack_start( $new_hbox, FALSE, FALSE, 0 ); my $pos_quit = Gtk2::Button->new_with_label( gettext("Close Window") ); $new_hbox->add($pos_quit); $pos_quit->signal_connect( clicked => sub { $main_win->destroy } ); my $false_pos = Gtk2::Button->new_with_label( gettext("False Positive") ); $new_hbox->add($false_pos); $false_pos->signal_connect( clicked => \&main_false_pos, "false_pos" ); my $del_pos = Gtk2::Button->new_with_label( gettext("Delete") ); $new_hbox->add($del_pos); $del_pos->signal_connect( clicked => \&main_del_pos, "false_pos" ); $q_label = Gtk2::Label->new(); $new_vbox->pack_start( $q_label, FALSE, FALSE, 2 ); for my $opt (@q_files) { push @{ $new_slist->{data} }, basename($opt); } $main_win->set_position('mouse'); $main_win->show_all; } sub main_false_pos { my @sel = $new_slist->get_selected_indices; return if ( !@sel ); my $deref = $sel[0]; return if ( not exists $q_files[$deref] ); my $base = basename( $q_files[$deref] ); system( "mv", $q_files[$deref], $directory ); my $new_name = $base; $new_name =~ s/.VIRUS$//; rename( "$directory/$base", "$directory/$new_name" ); if ( -e $q_files[$deref] ) { $q_label->set_text( gettext("Operation failed.") ); return; } splice @{ $new_slist->{data} }, $deref, 1; $q_label->set_text( gettext("Moved to home directory.") ); @q_files = glob "$v_dir/*"; } sub main_del_pos { my @sel = $new_slist->get_selected_indices; return if ( !@sel ); my $deref = $sel[0]; return if ( not exists $q_files[$deref] ); my $base = basename( $q_files[$deref] ); unlink $q_files[$deref]; if ( -e $q_files[$deref] ) { $q_label->set_text( gettext("Operation failed.") ); return; } splice @{ $new_slist->{data} }, $deref, 1; $q_label->set_text( gettext("Deleted.") ); @q_files = glob "$v_dir/*"; } sub main_confirm { my $number = shift; my $do_this = shift; my $current_status = $virus[$number]{status}; if ( $do_this eq "q" ) { if ( not -e $virus[$number]{full} ) { $top_label->set_text( sprintf gettext("File has been moved or deleted already.") ); $slist->{data}[$number][0] = "$virus[$number]{base}"; $slist->{data}[$number][1] = "$virus[$number]{status}"; return; } if ( move_to_quarantine($number) ) { $top_label->set_text( gettext("File has been quarantined.") ); $virus[$number]{status} = sprintf gettext("%s (Quarantined)"), $current_status; } else { $top_label->set_text( gettext("File could not be quarantined.") ); return; } } elsif ( $do_this eq "d" ) { if ( not -e $virus[$number]{full} ) { $top_label->set_text( gettext("File has been moved or deleted already.") ); $slist->{data}[$number][0] = "$virus[$number]{base}"; $slist->{data}[$number][1] = "$virus[$number]{status}"; return; } my $confirm_message = gettext("Really delete this file?"); my $confirm = Gtk2::MessageDialog->new( $window, [qw(modal destroy-with-parent)], 'question', 'ok-cancel', $confirm_message ); if ( "cancel" eq $confirm->run ) { $confirm->destroy; return; } else { $confirm->destroy; if ( unlink( $virus[$number]{full} ) ) { $top_label->set_text( gettext("File has been deleted.") ); $virus[$number]{status} = sprintf gettext("%s (Deleted)"), $current_status; } else { $top_label->set_text( gettext("File could not be deleted.") ); return; } } } main_slist_delete($number); } sub main_slist_delete { my $number = shift; $slist->{data}[$number][0] = "$virus[$number]{base}"; $slist->{data}[$number][1] = "$virus[$number]{status}"; $window->queue_draw; } sub show_message_dialog { #parent window, type = info, warning, question, etc, button = ok, cancel my ( $parent, $type, $button, $message ) = @_; my $dialog; $dialog = Gtk2::MessageDialog->new_with_markup( $parent, [qw(modal destroy-with-parent)], $type, $button, $message ); $dialog->run; $dialog->destroy; return; }