594 lines
13 KiB
Perl
594 lines
13 KiB
Perl
use strict;
|
|
package Net::ClamAV::Client;
|
|
# ABSTRACT: A client class for the ClamAV C<clamd> virus scanner daemon
|
|
|
|
use warnings;
|
|
use Moose;
|
|
use IO::Socket;
|
|
use IO::Handle;
|
|
use IO::File;
|
|
use Net::ClamAV::Exception::Connect;
|
|
use Net::ClamAV::Exception::Command;
|
|
use Net::ClamAV::Exception::Result;
|
|
use Net::ClamAV::Exception::Other;
|
|
use Net::ClamAV::Exception::Unsupported;
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
=head2 Creating a scanner client
|
|
|
|
use Net::ClamAV::Client;
|
|
|
|
# Use a TCP inet domain socket
|
|
my $scanner = Net::ClamAV::Client->new(url => "localhost:3310");
|
|
|
|
# Use a local Unix domain socket:
|
|
$scanner = Net::ClamAV::Client->new(url => "/var/run/clamav/clamd.ctl");
|
|
|
|
die("ClamAV daemon not alive")
|
|
if not defined($scanner) or not $scanner->ping();
|
|
|
|
|
|
=head2 Daemon maintenance
|
|
|
|
my $scanner = Net::ClamAV::Client->new(url => "localhost:3310");
|
|
|
|
my $version = $scanner->version;
|
|
# Retrieve the ClamAV version string.
|
|
|
|
$scanner->reload(); # Reload the malware pattern database.
|
|
|
|
$scanner->quit(); # Terminates the ClamAV daemon.
|
|
$scanner->shutdown(); # Likewise.
|
|
|
|
=head2 Path scanning
|
|
|
|
# Scan a single file or a whole directory structure,
|
|
# and stop at the first infected file. For this to work
|
|
# the clamd has to run on the local host:
|
|
|
|
my $scanner = Net::ClamAV::Client->new(url => "localhost:3310");
|
|
my @results = $scanner->scanLocalPath("/etc/groups");
|
|
|
|
=head2 Path scanning (complete)
|
|
|
|
# Scan a single file or a whole directory structure,
|
|
# and scan all files without stopping at the first infected one:
|
|
my $scanner = Net::ClamAV::Client->new(url => "localhost:3310");
|
|
my @results2 = $scanner->scanLocalPathContinous("/etc/");
|
|
|
|
=head2 Other scanning methods
|
|
|
|
my $handle;
|
|
my $scanner = Net::ClamAV::Client->new(url => "localhost:3310");
|
|
# Scan a stream, i.e. read from an I/O handle:
|
|
my $result = $scanner->scanStream($handle);
|
|
|
|
# Scan a scalar value:
|
|
my $value; # some file in a scalar
|
|
my $result2 = $scanner->scanScalar(\$value);
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
B<Net::ClamAV::Client> is a class acting as a client for a ClamAV C<clamd> virus
|
|
scanner daemon. The daemon may run locally or on a remote system as
|
|
B<Net::ClamAV::Client> can use both Unix domain sockets and TCP/IP sockets. The
|
|
full functionality of the C<clamd> client/server protocol is supported.
|
|
|
|
This Module is based on the B<ClamAV::Client> class written by Julian Mehnle <julian@mehnle.net>
|
|
which is not developed anymore but everything has been written from scratch.
|
|
|
|
=head1 Methods
|
|
|
|
=head2 Constructor
|
|
|
|
The following constructor is provided:
|
|
|
|
=over
|
|
|
|
=item B<new(%options)>: RETURNS Net::ClamAV::Client
|
|
|
|
Creates a new C<Net::ClamAV::Client> object.
|
|
|
|
C<%options> is a list of key/value pairs representing any of the following
|
|
options:
|
|
|
|
=over
|
|
|
|
=item B<url>
|
|
|
|
A scalar containing the url to the clamd server (e.g. localhost:3310 or /var/run/clamav/clamd.ctl)
|
|
|
|
=back
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
has 'socket' => (is => 'rw');
|
|
has 'url' => (is => 'rw', required => 1);
|
|
has 'runningIDsession' => (is => 'rw', isa=>'Int', default=>0);
|
|
has 'streamBlockSize' => (is => 'rw', isa=>'Int', default=>4096);
|
|
|
|
sub _connect
|
|
{
|
|
my $self = shift;
|
|
|
|
if ($self->url() =~/:/)
|
|
{
|
|
my @url = split /\:/, $self->url();
|
|
|
|
$self->socket(IO::Socket->new(
|
|
Domain => IO::Socket::AF_INET,
|
|
Type => SOCK_STREAM,
|
|
Proto => "tcp",
|
|
PeerHost => $url[0],
|
|
PeerPort => $url[1]
|
|
))
|
|
|| throw Net::ClamAV::Exception::Connect("Can't open socket: $@");
|
|
}
|
|
else
|
|
{
|
|
$self->socket(IO::Socket->new(
|
|
Domain => IO::Socket::AF_UNIX,
|
|
Type => SOCK_STREAM,
|
|
Peer => $self->url(),
|
|
))
|
|
|| throw Net::ClamAV::Exception::Connect("Can't open socket: $@");
|
|
}
|
|
|
|
$self->socket()->autoflush(1);
|
|
}
|
|
|
|
|
|
sub _basicCommand
|
|
{
|
|
my $self = shift;
|
|
my $command = shift;
|
|
|
|
$self->_connect() unless $self->runningIDsession();
|
|
|
|
$self->socket()->send($command);
|
|
my $reply = $self->socket()->getline();
|
|
chomp($reply);
|
|
|
|
if (length($reply) < 3)
|
|
{
|
|
throw Net::ClamAV::Exception::Command("unknown reply to command \"$reply\"");
|
|
}
|
|
|
|
$self->socket()->close() unless $self->runningIDsession();
|
|
return $reply;
|
|
}
|
|
|
|
sub _multiCommand
|
|
{
|
|
my $self = shift;
|
|
my $command = shift;
|
|
my @reply;
|
|
|
|
$self->_connect() unless $self->runningIDsession();
|
|
$self->socket()->send($command);
|
|
|
|
while(defined(my $line = $self->socket()->getline()))
|
|
{
|
|
chomp($line);
|
|
if (length($line) < 3)
|
|
{
|
|
throw Net::ClamAV::Exception::Command("unknown reply to command \"$line\"");
|
|
}
|
|
|
|
push(@reply,$line);
|
|
}
|
|
|
|
$self->socket()->close() unless $self->runningIDsession();
|
|
return @reply;
|
|
}
|
|
|
|
sub _parse_multi_result {
|
|
my ($self,@reply) = @_;
|
|
my @status;
|
|
|
|
for my $r (@reply)
|
|
{
|
|
if ( $r !~ /^(.*):\s+(OK|(\S+)\s+FOUND)$/ )
|
|
{
|
|
throw Net::ClamAV::Exception::Result("Invalid server scanning result \"$r\"");
|
|
}
|
|
|
|
my $file = $1;
|
|
my $res = $2;
|
|
|
|
if ($res !~ /OK/)
|
|
{
|
|
$res = $3;
|
|
}
|
|
|
|
my $stat = {
|
|
file => $file,
|
|
result => $res
|
|
};
|
|
|
|
push (@status,$stat);
|
|
}
|
|
|
|
return @status;
|
|
}
|
|
|
|
sub _parse_result {
|
|
my $self = shift;
|
|
my $result = shift;
|
|
|
|
if ( $result !~ /^(.*):\s+(OK|(\S+)\s+FOUND)$/ )
|
|
{
|
|
throw Net::ClamAV::Exception::Result("Invalid server scanning result \"$result\"");
|
|
}
|
|
|
|
my $file = $1;
|
|
my $res = $2;
|
|
|
|
if ($res !~ /OK/)
|
|
{
|
|
$res = $3;
|
|
}
|
|
|
|
return ($file, $res);
|
|
}
|
|
|
|
|
|
=head2 Public Instance Methods
|
|
|
|
The following public methods are provided:
|
|
|
|
=head3 B<ping> : RETURNS SCALAR
|
|
|
|
Returns B<true> ('PONG') if the ClamAV daemon is alive. Throws a
|
|
Net::ClamAV::Exception otherwise.
|
|
|
|
=cut
|
|
sub ping
|
|
{
|
|
my $self = shift;
|
|
|
|
my $reply = $self->_basicCommand("PING");
|
|
|
|
throw Net::ClamAV::Exception::Result("No PONG reply") unless $reply eq "PONG";
|
|
|
|
return $reply;
|
|
}
|
|
|
|
=head3 B<version> : RETURNS SCALAR
|
|
|
|
Returns the Version String of the clamd server. Throws a
|
|
Net::ClamAV::Exception otherwise.
|
|
|
|
=cut
|
|
sub version
|
|
{
|
|
my $self = shift;
|
|
|
|
return $self->_basicCommand("VERSION");
|
|
}
|
|
|
|
=head3 B<reload> : RETURNS SCALAR
|
|
|
|
Reloads the clamd virus databases and returns B<true> ('RELOADING') when successfull.
|
|
Throws a Net::ClamAV::Exception otherwise.
|
|
|
|
=cut
|
|
sub reload
|
|
{
|
|
my $self = shift;
|
|
|
|
return $self->_basicCommand("RELOAD");
|
|
}
|
|
|
|
=head3 B<shutdown> : RETURNS SCALAR
|
|
|
|
Shutdowns the clamd server. Throws a Net::ClamAV::Exception
|
|
when unseccessfull.
|
|
|
|
=cut
|
|
sub shutdown
|
|
{
|
|
my $self = shift;
|
|
|
|
return $self->_basicCommand("SHUTDOWN");
|
|
}
|
|
|
|
=head3 B<quit> : RETURNS SCALAR
|
|
|
|
śame as B<shutdown>
|
|
|
|
=cut
|
|
sub quit
|
|
{
|
|
my $self = shift;
|
|
|
|
return $self->shutdown();
|
|
}
|
|
|
|
=head3 B<scanLocalPath> : RETURNS HASH
|
|
|
|
Scan a file or directory given as path. B<Important:> The used clamd
|
|
has to run on the local host for this method to work. Clamd will
|
|
directly access the given path. Make sure the user running clamd
|
|
has access rights to it. Scanning stops when the first virus is
|
|
found or all files within path has been scanned.
|
|
|
|
The Method returns a Hash with attributes B<file> and B<result>.
|
|
|
|
my $hash = {
|
|
file => "the filename a virus was found in",
|
|
result => "the result of file"
|
|
};
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub scanLocalPath
|
|
{
|
|
my $self = shift;
|
|
my $file = shift;
|
|
|
|
throw Net::ClamAV::Exception::Other("file \"$file\" not found") unless ( -e $file );
|
|
|
|
my @reply = $self->_multiCommand("nSCAN $file\n");
|
|
my @result= $self->_parse_multi_result(@reply);
|
|
|
|
return $result[0];
|
|
}
|
|
|
|
=head3 B<scanLocalPathContinous> : RETURNS HASH
|
|
|
|
Scan a file or directory given as path and do B<not> stop on first virus found.
|
|
B<Important:> The used clamd has to run on the local host for this method to work.
|
|
Clamd will directly access the given path. Make sure the user running clamd
|
|
has access rights to it. Scanning stops when the first virus is
|
|
found or all files within path has been scanned.
|
|
|
|
The Method returns an array of hashes with attributes B<file> and B<result>.
|
|
|
|
my $hash = {
|
|
file => "the filename a virus was found in",
|
|
result => "the result of file"
|
|
};
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub scanLocalPathContinous
|
|
{
|
|
my $self = shift;
|
|
my $file = shift;
|
|
|
|
throw Net::ClamAV::Exception::Other("file \"$file\" not found") unless ( -e $file );
|
|
|
|
my @reply = $self->_multiCommand("nCONTSCAN $file\n");
|
|
return $self->_parse_multi_result(@reply);
|
|
}
|
|
|
|
=head3 B<scanLocalPathMulti> : RETURNS HASH
|
|
|
|
Scan a file or directory given as path concurrently. B<Important:> The used clamd
|
|
has to run on the local host for this method to work. Clamd will
|
|
directly access the given path. Make sure the user running clamd
|
|
has access rights to it. Scanning stops when the first virus is
|
|
found or all files within path has been scanned.
|
|
|
|
The Method returns an array of hashes with attributes B<file> and B<result>.
|
|
|
|
my $hash = {
|
|
file => "the filename a virus was found in",
|
|
result => "the result of file"
|
|
};
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub scanLocalPathMulti
|
|
{
|
|
my $self = shift;
|
|
my $file = shift;
|
|
|
|
throw Net::ClamAV::Exception::Other("file \"$file\" not found") unless ( -e $file );
|
|
|
|
my @reply = $self->_multiCommand("nMULTISCAN $file\n");
|
|
return $self->_parse_multi_result(@reply);
|
|
}
|
|
|
|
=head3 B<scanLocalFile> : RETURNS HASH
|
|
|
|
Scan B<one> file. B<Important:> The used clamd
|
|
has to run on the local host for this method to work. Clamd will
|
|
directly access the given path. Make sure the user running clamd
|
|
has access rights to it. Scanning stops when the first virus is
|
|
found or all files within path has been scanned.
|
|
|
|
The Method returns a hashe with attributes B<file> and B<result>.
|
|
s
|
|
my $hash = {
|
|
file => "the filename a virus was found in",
|
|
result => "the result of file"
|
|
};
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub scanLocalFile
|
|
{
|
|
my $self = shift;
|
|
my $file = shift;
|
|
|
|
return $self->scanLocalPath($file);
|
|
}
|
|
|
|
=head3 B<stats> : RETURNS HASH
|
|
|
|
Return the stats of the clamd. B<NOT SUPPORTED YET>
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub stats
|
|
{
|
|
throw Net::ClamAV::Exception::Unsupported("stats command not supported");
|
|
}
|
|
|
|
=head3 B<scanFileDescriptor> : RETURNS HASH
|
|
|
|
Scans a file given by a file descriptor. B<NOT SUPPORTED YET>
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub scanFileDescriptor
|
|
{
|
|
throw Net::ClamAV::Exception::Unsupported("FILDES command not supported");
|
|
}
|
|
|
|
=head3 B<startSession>
|
|
|
|
Starts a session with the clamd server within multiple scan commands can
|
|
be issued.
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub startSession
|
|
{
|
|
my $self = shift;
|
|
|
|
$self->_connect();
|
|
$self->socket()->send("nIDSESSION\n");
|
|
$self->runningIDsession(1);
|
|
|
|
}
|
|
|
|
=head3 B<runningSession> : RETURNS SCALAR
|
|
|
|
Checks if a session is running with the clamd server.
|
|
|
|
Returns 1 if yes, else 0.
|
|
|
|
=cut
|
|
sub runningSession
|
|
{
|
|
my $self = shift;
|
|
|
|
return $self->runningIDsession();
|
|
}
|
|
|
|
=head3 B<endSession>
|
|
|
|
Ends a session with the clamd server within multiple scan commands can
|
|
be issued.
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub endSession
|
|
{
|
|
my $self = shift;
|
|
|
|
$self->socket()->send("nEND\n");
|
|
$self->socket()->close();
|
|
$self->runningIDsession(0);
|
|
}
|
|
|
|
=head3 B<scanStreamFH> : RETURNS SCALAR
|
|
|
|
Scans a file by transmitting it as a stream to the clamd server.
|
|
The file is given as a IO::Handle.
|
|
|
|
The Method returns a SCALAR with attributes B<undef> or B<virusname>.
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub scanStreamFH
|
|
{
|
|
my $self = shift;
|
|
my $handle = shift;
|
|
|
|
throw Net::ClamAV::Exception::Other("no file handle given") unless $handle;
|
|
throw Net::ClamAV::Exception::Other("handle is not a IO::Handle") unless ref($handle) eq "IO::Handle";
|
|
|
|
$self->_connect() unless $self->runningIDsession();
|
|
$self->socket()->send("nINSTREAM\n");
|
|
|
|
my $block;
|
|
while (my $nr=$handle->read($block, $self->streamBlockSize()))
|
|
{
|
|
my $size = pack("N",$nr);
|
|
$self->socket()->send($size);
|
|
$self->socket()->send($block);
|
|
}
|
|
my $size = pack("N",0);
|
|
$self->socket()->send($size);
|
|
|
|
my $status=$self->socket()->getline() . "\n";
|
|
|
|
|
|
if ($status!~/^stream:\s*(.*)\s+FOUND/)
|
|
{
|
|
return;
|
|
}
|
|
my $ret=$1;
|
|
|
|
$self->socket()->close() unless $self->runningIDsession();
|
|
|
|
|
|
return $ret;
|
|
}
|
|
|
|
=head3 B<scanStreamFile> : RETURNS SCALAR
|
|
|
|
Scans a file by transmitting it as a stream to the clamd server.
|
|
The file is given as a path.
|
|
|
|
The Method returns a SCALAR with attributes B<undef> or B<virusname>.
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub scanStreamFile
|
|
{
|
|
my $self = shift;
|
|
my $file = shift;
|
|
|
|
my $fh = IO::File->new($file, "r");
|
|
my $handle = IO::Handle->new_from_fd($fh, "r");
|
|
|
|
my $status = $self->scanStreamFH($handle);
|
|
|
|
$handle->close();
|
|
|
|
return $status;
|
|
}
|
|
|
|
=head3 B<scanScalar> : RETURNS SCALAR
|
|
|
|
Scans a SCALAR by transmitting it as a stream to the clamd server.
|
|
The file is given as a path.
|
|
|
|
The Method returns a SCALAR with attributes B<undef> or B<virusname>.
|
|
|
|
|
|
Throws a Net::ClamAV::Exception on error.
|
|
|
|
=cut
|
|
sub scanScalar
|
|
{
|
|
my $self = shift;
|
|
my $data = shift;
|
|
|
|
my $fh = IO::File->new($data, "r");
|
|
my $handle = IO::Handle->new_from_fd($fh, "r");
|
|
|
|
return $self->scanStreamFH($handle);
|
|
}
|
|
|
|
|
|
1;
|