# ClamTk, copyright (C) 2004-2016 Dave M # # This file is part of ClamTk (https://dave-theunsub.github.io/clamtk). # # ClamTk is free software; you can redistribute it and/or modify it # under the terms of either: # # a) the GNU General Public License as published by the Free Software # Foundation; either version 1, or (at your option) any later version, or # # b) the "Artistic License". package ClamTk::Analysis; use Glib 'TRUE', 'FALSE'; # use strict; # use warnings; $| = 1; use LWP::UserAgent; use LWP::Protocol::https; use MIME::Base64 'decode_base64'; use Digest::SHA 'sha256_hex'; use File::Basename 'basename'; use File::Copy 'move'; use Text::CSV; use JSON; use Encode 'encode'; use POSIX 'locale_h'; use Locale::gettext; binmode STDIN, ":encoding(UTF-8)"; my $store; # Gtk2::ListStore for results my $model; # Gtk2::ListStore for combobox my $combobox; # Gtk2::ComboBox for previous analysis my $nb; # Gtk2::Notebook my $filename = ''; # name of file analyzed my $bar; # Gtk2::InfoBar my $window; # Gtk2::Dialog; global for queue_draw; my $submitted = 0; # Boolean value to keep track if file # has been submitted or not my $from_scan; sub show_window { ( undef, $from_scan, $parent ) = @_; $window = Gtk2::Dialog->new( _( 'Analysis' ), $parent, [ qw| modal destroy-with-parent no-separator | ], ); $window->signal_connect( destroy => sub { $window->destroy; 1; } ); $window->set_border_width( 5 ); $window->set_default_size( 450, 250 ); $window->set_position( 'mouse' ); my $box = Gtk2::VBox->new( FALSE, 5 ); $window->get_content_area->add( $box ); $nb = Gtk2::Notebook->new; $box->pack_start( $nb, TRUE, TRUE, 0 ); #<<< # Add initial page (select file, etc) $nb->insert_page( page_one(), Gtk2::Label->new( _( 'File Analysis' ) ), 0, ); # Add results page $nb->insert_page( page_two(), Gtk2::Label->new( _( 'Results' ) ), 1, ); #>>> $bar = Gtk2::InfoBar->new; $box->pack_start( $bar, FALSE, FALSE, 0 ); $bar->set_message_type( 'info' ); $bar->add_button( 'gtk-close', -7 ); $bar->signal_connect( response => sub { $submitted = 0; $window->destroy; } ); $window->show_all; $window->run; $window->destroy; 1; } sub page_one { # Analysis page my $box = Gtk2::VBox->new( FALSE, 5 ); my $separator = Gtk2::HSeparator->new; $box->pack_start( analysis_frame_one(), FALSE, FALSE, 5 ); $box->pack_start( $separator, FALSE, FALSE, 5 ); $box->pack_start( analysis_frame_two(), FALSE, FALSE, 5 ); return $box; } sub page_two { # Results page my $box = Gtk2::VBox->new( FALSE, 0 ); my $sw = Gtk2::ScrolledWindow->new; $sw->set_policy( 'never', 'automatic' ); $box->pack_start( $sw, TRUE, TRUE, 0 ); use constant VENDOR => 0; use constant RESULT => 1; use constant DATE => 2; #<<< $store = Gtk2::ListStore->new( # VENDOR 'Glib::String', # RESULT 'Glib::String', # DATE 'Glib::String', ); my $tree = Gtk2::TreeView->new_with_model( $store ); $tree->set_rules_hint( TRUE ); $sw->add( $tree ); my $renderer = Gtk2::CellRendererText->new; my $column = Gtk2::TreeViewColumn->new_with_attributes( _( 'Vendor' ), $renderer, text => VENDOR, ); $column->set_expand( TRUE ); $column->set_sort_column_id( VENDOR ); $tree->append_column( $column ); $column = Gtk2::TreeViewColumn->new_with_attributes( _( 'Date' ), $renderer, text => DATE, ); $column->set_sort_column_id( DATE ); $column->set_expand( TRUE ); $tree->append_column( $column ); $column = Gtk2::TreeViewColumn->new_with_attributes( _( 'Result' ), $renderer, text => RESULT, ); $column->set_expand( TRUE ); $column->set_sort_column_id( RESULT ); $tree->append_column( $column ); #>>> my $toolbar = Gtk2::Toolbar->new; $box->pack_start( $toolbar, FALSE, FALSE, 3 ); $toolbar->set_style( 'both-horiz' ); my $separator = Gtk2::SeparatorToolItem->new; $separator->set_draw( FALSE ); $separator->set_expand( TRUE ); $toolbar->insert( $separator, -1 ); my $button = Gtk2::ToolButton->new_from_stock( 'gtk-save-as' ); $button->set_tooltip_text( _( 'Save results' ) ); $toolbar->insert( $button, -1 ); $button->signal_connect( clicked => sub { return unless ( $store->iter_n_children ); save_file(); } ); return $box; } sub analysis_frame_one { my $box = Gtk2::VBox->new( FALSE, 5 ); my $label = Gtk2::Label->new( _( "Check or recheck a file's reputation" ) ); $label->set_alignment( 0.0, 0.5 ); $box->pack_start( $label, FALSE, FALSE, 5 ); my $grid = Gtk2::Table->new( 1, 3, FALSE ); $box->pack_start( $grid, TRUE, TRUE, 5 ); $grid->set_col_spacings( 10 ); $grid->set_row_spacings( 10 ); $grid->set_homogeneous( TRUE ); #<<< # Declaring this now for setting sensitive/insensitive my $button = Gtk2::ToolButton->new_from_stock( 'gtk-find' ); #$button->set_sensitive( FALSE ); my $select_button = Gtk2::FileChooserButton->new( _( 'Select a file' ), 'open', ); # If we've arrived from scanning results, set the file: if( $from_scan ) { $select_button->set_filename( $from_scan ); } $button->set_sensitive( TRUE ); $select_button->signal_connect( 'file-set' => sub { # Ensure it does not become re-enabled # after submitting a file. The stupid # FileChooserButton cannot be reliably # reset. if( ! $submitted ) { $button->set_sensitive( TRUE ); } } ); $select_button->set_current_folder( ClamTk::App->get_path( 'directory' ) ); #$grid->attach( $select_button, 0, 0, 1, 1 ); $grid->attach_defaults( $select_button, 0, 1, 0, 1 ); #>>> my $separator = Gtk2::SeparatorToolItem->new; $separator->set_draw( FALSE ); #$grid->attach( $separator, 1, 0, 1, 1 ); $grid->attach_defaults( $separator, 1, 2, 0, 1 ); #$grid->attach( $button, 2, 0, 1, 1 ); $grid->attach_defaults( $button, 2, 3, 0, 1 ); $button->set_tooltip_text( _( 'Submit file for analysis' ) ); $button->signal_connect( clicked => sub { $filename = $select_button->get_filename; return unless -e $filename; # Since FileChooserButton cannot reset itself, # we'll just disable the submit button. # Getting real tired of Gtk2. $button->set_sensitive( FALSE ); $select_button->set_sensitive( FALSE ); # To be sure: $submitted++; # VT size limit using the API is 32MB # https://www.virustotal.com/en/faq/ my $size = -s $filename; my $mb = $size / ( 1024 * 1024 ); if ( $mb > 32 ) { warn "filesize too large - must be smaller than 32MB\n"; popup( _( 'Uploaded files must be smaller than 32MB' ) ); return; } Gtk2->main_iteration while ( Gtk2->events_pending ); my ( $vt_results, $new_window, $is_error ) = check_for_existing( $filename ); # warn "VT results = >$vt_results<, ", # "open new_win = >$new_window<, error = >$is_error<\n"; #$results->set_text( $vt_results ); # Information exists on this file; show results if ( $new_window ) { $nb->show_all; $nb->set_current_page( 1 ); #} elsif ( $new_window && !$is_error ) { } elsif ( $vt_results == 0 ) { # No information exists; offer to submit file my $confirm = popup( _( 'No information exists for this file.' . ' ' . 'Press OK to submit this file for analysis.' ), 1 ); if ( $confirm ) { $submitted++; submit_new(); return; } } else { popup( _( 'Unable to submit file: try again later' ) ); return; } } ); return $box; } sub analysis_frame_two { my $box = Gtk2::VBox->new( FALSE, 5 ); my $label = Gtk2::Label->new( _( 'View or delete previous results' ) ); $label->set_alignment( 0.0, 0.5 ); $box->pack_start( $label, FALSE, FALSE, 0 ); my $grid = Gtk2::Table->new( 1, 4, FALSE ); $box->pack_start( $grid, TRUE, TRUE, 5 ); $grid->set_col_spacings( 10 ); $grid->set_row_spacings( 10 ); $grid->set_homogeneous( FALSE ); $model = Gtk2::ListStore->new( 'Glib::String' ); $combobox = Gtk2::ComboBox->new_with_model( $model ); $grid->attach_defaults( $combobox, 0, 1, 0, 1 ); my $render = Gtk2::CellRendererText->new; $combobox->pack_start( $render, TRUE ); $combobox->add_attribute( $render, text => 0 ); # $combobox->signal_connect( changed => \&read_files ); read_files(); my $separator = Gtk2::SeparatorToolItem->new; $separator->set_draw( FALSE ); $grid->attach_defaults( $separator, 1, 2, 0, 1 ); my $button = Gtk2::ToolButton->new_from_stock( 'gtk-index' ); $grid->attach_defaults( $button, 2, 3, 0, 1 ); $button->set_tooltip_text( _( 'View file results' ) ); $button->signal_connect( clicked => sub { my $file = $combobox->get_active_text; return unless ( $file ); my $file_to_use = ''; my $files = read_files(); # Written so as to not choke older Perls for my $key ( sort @$files ) { if ( $key->{ basefilepath } eq $file ) { $file_to_use = $key->{ readpath }; } } # For some reason, we'll use csv files my $csv = Text::CSV->new( { binary => 1, eol => "\n" } ) or do { warn "unable to begin Text::CSV: $!\n"; # popup( _( 'Error with Text::CSV file' ) ); return; }; open( my $f, '<:encoding(utf8)', $file_to_use ) or do { warn "unable to opening VT CSV file: $!\n"; # popup( _( 'Error opening VT CSV file' ) ); return; }; # File, Hash, Date my $fields; my $counter = 0; while ( my $row = <$f> ) { chomp( $row ); next if ( $row =~ /^#/ ); next if ( $row =~ /^\s*$/ ); if ( $csv->parse( $row ) ) { $fields->[ $counter ] = [ $csv->fields() ]; $counter++; } } close( $f ); $store->clear; #<<< # Written so as to not choke older Perls for my $v ( sort @{ $fields } ) { my $iter = $store->append; $store->set( $iter, VENDOR, $v->[0], RESULT, $v->[1], DATE, $v->[2], ); #>>> } $nb->show_all; $nb->set_current_page( 1 ); } ); $button = Gtk2::ToolButton->new_from_stock( 'gtk-delete' ); $grid->attach_defaults( $button, 3, 4, 0, 1 ); $button->set_tooltip_text( _( 'Delete file results' ) ); $button->signal_connect( clicked => sub { my $file = $combobox->get_active_text; return unless ( $file ); my $file_to_use = ''; my $active_text = $combobox->get_active_text; my $active_iter = $combobox->get_active; my $files = read_files(); # This has to be written a certain way so # that older Perls don't $choke for my $key ( sort @{ $files } ) { if ( $key->{ basefilepath } eq $file ) { $file_to_use = $key->{ readpath }; if ( -e $file_to_use ) { unlink( $file_to_use ) or do { warn "unable to delete VT $file_to_use: $!\n"; return; }; $combobox->set_active( -1 ); } read_files(); # we're done! last; } } } ); return $box; } sub read_files { my $submit_dir = ClamTk::App->get_path( 'submit' ); my $results; $model->clear; my $arr_count = 0; # Grab all files for my $f ( glob "$submit_dir/*" ) { my $basename = basename( $f ); # Might be other garbage in the folder; just # look for ones that look like hashes next unless ( length $basename == 64 ); $results->[ $arr_count ]->{ readpath } = $f; $results->[ $arr_count ]->{ hash } = basename( $f ); open( my $t, '<:encoding(utf8)', $f ) or do { warn "unable to open analysis file >$f<: $!\n"; next; }; while ( <$t> ) { chomp; if ( /^# File (.*?)$/ ) { $results->[ $arr_count ]->{ filepath } = $1; $results->[ $arr_count ]->{ basefilepath } = basename( $1 ); $model->set( $model->append, 0, basename( $results->[ $arr_count ]->{ filepath } ) ); last; } } close( $t ); $arr_count++; } return $results; } sub check_for_existing { # First, set the infobar so the # user knows something is happening: Gtk2->main_iteration while ( Gtk2->events_pending ); set_infobar_mode( _( 'Please wait...' ) ); $window->queue_draw; Gtk2->main_iteration while ( Gtk2->events_pending ); my $file = shift; my $url = 'https://www.virustotal.com/vtapi/v2/file/report'; my $local_tk_version = ClamTk::App->get_TK_version(); my $hash = get_hash( $file ); my $ua = add_ua_proxy(); #<<< my @req = ( $url, [ resource => $hash, apikey => getapi(), ] ); #>>> set_infobar_mode( ' ' ); # $new_window = switch to results tab my $new_window = TRUE; # $is_error = connection (or other) issue my $is_error = FALSE; Gtk2->main_iteration while ( Gtk2->events_pending ); my $response = $ua->post( @req ); # warn "is_err = >", $response->is_error, "<\n"; Gtk2->main_iteration while ( Gtk2->events_pending ); my $data; if ( $response->is_success ) { my $json = JSON->new->utf8->allow_nonref; eval { $data = $json->decode( $response->decoded_content ); }; if ( $@ ) { warn "error reading/decoding json: $@\n"; $new_window = FALSE; $is_error = TRUE; $data->{ response_code } = -4; return ( $data->{ response_code }, $new_window, $is_error ); } # warn "response_code from submission >", $data->{ response_code }, # "<\n"; # Response codes: # 0 = not present in dataset # -2 = queued for analysis # 1 = present and can be retrieved if ( $data->{ response_code } == 1 ) { # $return = $data->{ positives } . ' / ' . $data->{ total }; for my $mark ( $data->{ scans } ) { while ( my ( $vendor, $v ) = each %$mark ) { my $iter = $store->append; #<<< $store->set( $iter, VENDOR, $vendor, RESULT, ( $v->{ result } ) ? $v->{ result } : '---', DATE, $v->{ update }, ); #>>> } } } elsif ( $data->{ response_code } == 0 ) { # $return = _( 'No information on this file' ); $new_window = FALSE; $is_error = FALSE; } elsif ( $data->{ response_code } == -2 ) { # $return = _( 'File is pending analysis' ); $new_window = FALSE; $is_error = FALSE; } else { # $return = _( 'An unknown error occurred' ); $data->{ response_code } = -4; $new_window = FALSE; $is_error = TRUE; } } else { $data->{ response_code } = -4; $new_window = FALSE; $is_error = TRUE; warn "Unable to connect in submission: ", $response->status_line, "\n"; } Gtk2->main_iteration while ( Gtk2->events_pending ); set_infobar_mode( ' ' ); Gtk2->main_iteration while ( Gtk2->events_pending ); return ( $data->{ response_code }, $new_window, $is_error ); } sub submit_new { # First, set the infobar so the user knows # something is happening: Gtk2->main_iteration while ( Gtk2->events_pending ); set_infobar_mode( _( 'Please wait...' ) ); $window->queue_draw; my $url = 'https://www.virustotal.com/vtapi/v2/file/scan'; my $ua = add_ua_proxy(); my $local_tk_version = ClamTk::App->get_TK_version(); my $u_filename = encode( 'UTF-8', $filename ); #<<< my $response = $ua->post( $url, Content_Type => 'multipart/form-data', Content => [ apikey => getapi(), file => [ $u_filename ] #file => [ $filename ] ], ); #>>> Gtk2->main_iteration while ( Gtk2->events_pending ); set_infobar_mode( ' ' ); $bar->show_all; $window->queue_draw; Gtk2->main_iteration while ( Gtk2->events_pending ); if ( $response->is_success ) { # Save off the results; might use them later my $file = ClamTk::App->get_path( 'virustotal_links' ); my $hash = get_hash( $filename ); my $json_text = $response->decoded_content; my $data = $response->as_string; my $json_hash = decode_json( $json_text ); open( my $f, '>>:encoding(UTF-8)', $file ); # or warn "unable to save virustotal results: $!\n"; my $csv = Text::CSV->new( { binary => 1, eol => "\n" } ); my $ref; $ref->[ 0 ] = [ $filename, $hash, $json_hash->{ permalink } ]; $csv->print( $f, $_ ) for ( @$ref ); close( $f ); popup( _( 'File successfully submitted for analysis.' ) ); } else { popup( _( 'Unable to submit file: try again later' ) ); } } sub getapi { #<<< return decode_base64( 'MDMwNmU3MT' . 'ZjYjQxNTMz' . 'OWFkZDQ5ND' . 'JkOTg4ZWVm' . 'MDU3MjlmND' . 'AxYmM0NjIw' . 'MjZmNDQ2OD' . 'gzYjcxNmIy' . 'NTAxZg==' ); #>>> } sub get_hash { my ( $file ) = shift; my $slurp = do { local $/ = undef; open( my $f, '<', $file ) or do { warn "unable to open >$file< for hashing: $!\n"; return; }; binmode( $f ); <$f>; }; return sha256_hex( $slurp ); } sub save_file { # VENDOR, RESULT, DATE # temporary filename my $hash = get_hash( $filename ); my $file = ClamTk::App->get_path( 'submit' ) . '/' . $hash; my $ref; my $count = 0; $store->foreach( sub { my ( $liststore, $path, $iter ) = @_; my ( $vendor, $result, $date ) = $liststore->get( $iter, 0, 1, 2 ); $ref->[ $count ] = [ $vendor, $result, $date ]; $count++; return FALSE; } ); # For some reason, we'll use csv files my $csv = Text::CSV->new( { binary => 1, eol => "\n" } ) or do { popup( _( 'Unable to save file' ) ); return; }; open( my $f, '>:encoding(utf8)', $file ) or do { popup( _( 'Unable to save file' ) ); return; }; print $f '# File', ' ', $filename, "\n"; print $f '# Hash', ' ', $hash, "\n"; print $f '# Date', ' ', scalar localtime, "\n\n"; $csv->print( $f, $_ ) for ( @$ref ); close( $f ); popup( _( 'File has been saved' ) ); read_files(); return TRUE; } sub popup { my ( $message, $option ) = @_; my $dialog = Gtk2::MessageDialog->new( undef, # no parent [ qw| modal destroy-with-parent | ], 'info', $option ? 'ok-cancel' : 'close', $message, ); if ( 'ok' eq $dialog->run ) { $dialog->destroy; return TRUE; } $dialog->destroy; return FALSE; } sub set_infobar_mode { my $text = shift; for my $c ( $bar->get_content_area->get_children ) { if ( $c->isa( 'Gtk2::Label' ) ) { $c->set_text( $text ); $bar->show_all; $window->queue_draw; Gtk2->main_iteration while Gtk2->events_pending; return; } } # We don't have a Label, so make one: my $label = Gtk2::Label->new( $text ); $label->set_use_markup( TRUE ); $bar->get_content_area->add( $label ); $bar->show_all; $window->queue_draw; Gtk2->main_iteration while Gtk2->events_pending; return; } sub add_ua_proxy { my $agent = LWP::UserAgent->new( ssl_opts => { verify_hostname => 1 } ); my $local_tk_version = ClamTk::App->get_TK_version(); $agent->agent( "ClamTk/$local_tk_version" ); $agent->timeout( 60 ); $agent->protocols_allowed( [ 'http', 'https' ] ); if ( ClamTk::Prefs->get_preference( 'HTTPProxy' ) ) { if ( ClamTk::Prefs->get_preference( 'HTTPProxy' ) == 1 ) { $agent->env_proxy; } elsif ( ClamTk::Prefs->get_preference( 'HTTPProxy' ) == 2 ) { my $path = ClamTk::App->get_path( 'db' ); $path .= '/local.conf'; my ( $url, $port ); if ( -e $path ) { if ( open( my $FH, '<', $path ) ) { while ( <$FH> ) { if ( /HTTPProxyServer\s+(.*?)$/ ) { $url = $1; } last if ( !$url ); if ( /HTTPProxyPort\s+(\d+)$/ ) { $port = $1; } } close( $FH ); $agent->proxy( http => "$url:$port" ); $agent->proxy( https => "$url:$port" ); $ENV{ HTTPS_PROXY } = "$url:$port"; $ENV{ HTTP_PROXY } = "$url:$port"; $ENV{ PERL_LWP_SSL_VERIFY_HOSTNAME } = 0; $ENV{ HTTPS_DEBUG } = 1; } } } } return $agent; } sub button_test { # This is the worst thing ever. The set_filename function # only works with certain versions of gtk2. So, this is a test # to determine if it works, which is tested by seeing if we # can get_filename. # Return TRUE if it succeeds so we can add the Analysis button # to the Results window. # Otherwise return FALSE, and don't draw it. my $fcb = Gtk2::FileChooserButton->new( 'just a test', 'open', ); $fcb->set_filename( '/usr/bin/clamtk' ); if ( $fcb->get_filename ) { return TRUE; } else { return FALSE; } } 1;