pyClamd-0.4.0/0000755000175000017500000000000013150641111013001 5ustar xaelxael00000000000000pyClamd-0.4.0/PKG-INFO0000644000175000017500000000217213150641111014100 0ustar xaelxael00000000000000Metadata-Version: 1.1 Name: pyClamd Version: 0.4.0 Summary: pyClamd is a python interface to Clamd (Clamav daemon). Home-page: http://xael.org/pages/pyclamd-en.html Author: Alexandre Norman Author-email: norman@xael.org License: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) Download-URL: http://xael.org/norman/python/pyclamd/ Description: pyClamd is a python interface to Clamd (Clamav daemon). By using pyClamd, you can add virus detection capabilities to your python software in an efficient and easy way. Instead of pyClamav which uses libclamav, pyClamd may be used by a closed source product. Keywords: python,clamav,antivirus,scanner,virus,libclamav Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) Classifier: Operating System :: OS Independent Classifier: Topic :: System Classifier: Topic :: Security pyClamd-0.4.0/README.txt0000644000175000017500000000530013150637235014511 0ustar xaelxael00000000000000pyClamd is a portable Python module to use the ClamAV antivirus engine on Windows, Linux, MacOSX and other platforms. It requires a running instance of the clamd daemon. License: pyClamd is released as open-source software under the LGPLv3 (or later) license. Download: see http://xael.org/norman/python/pyclamd/ How to install clamd: * For Windows: you need an unofficial version from http://hideout.ath.cx/clamav/ or http://oss.netfarm.it/clamav/ (http://w32.clamav.net does not provide clamd anymore, and neither does ClamWin) o Before running clamd, edit clamd.conf and make sure it is configured to use a TCP port instead of a Unix socket: LocalSocket should be disabled, TCPSocket and TCPAddr should be enabled. * For MacOSX: you may install ClamXav, and then run clamd from /usr/local/clamXav/sbin. * For other operating systems such as Linux and *BSD: http://www.clamav.org/download How to run clamd as a service on Windows: See http://www.andornot.com/blog/post/How-to-set-up-ClamAV-as-a-Windows-Service-to-scan-file-streams-on-demand.aspx or http://www.google.com/search?q=clamd+windows+service There used to be instructions on http://www.asspsmtp.org/wiki/ClamAV_Win32 to use either runclamd or the NJH Power Tools, but the website is not available anymore. How to use pyClamd: See source code or Alexandre Norman's website: http://xael.org/norman/python/pyclamd/ Here is an example on Unix: >>> import pyclamd >>> # Create object for using unix socket or network socket >>> cd = pyclamd.ClamdAgnostic() >>> # test if server is OK >>> cd.ping() True >>> # print version >>> print "Version : \n{0}".format(cd.version()) Version : ClamAV 0.98.1/19122/Sun Jun 22 08:24:11 2014 >>> # force a db reload >>> cd.reload() u'RELOADING' >>> # print stats >>> print "{0}".format(cd.stats()) POOLS: 1 STATE: VALID PRIMARY THREADS: live 1 idle 0 max 12 idle-timeout 30 QUEUE: 0 items STATS 0.000048 MEMSTATS: heap 6.098M mmap 0.000M used 3.770M free 2.337M releasable 0.132M pools 1 pools_used 268.122M pools_total 268.136M END >>> # write test file with EICAR test string >>> open('/tmp/EICAR','w').write(cd.EICAR()) >>> # write test file without virus pattern >>> open('/tmp/NO_EICAR','w').write('no virus in this file') >>> # scan files >>> print "{0}".format(cd.scan_file('/tmp/EICAR')) {u'/tmp/EICAR': ('FOUND', 'Eicar-Test-Signature')} >>> print "{0}".format(cd.scan_file('/tmp/NO_EICAR')) None >>> # scan a stream >>> print "{0}".format(cd.scan_stream(cd.EICAR())) {u'stream': ('FOUND', 'Eicar-Test-Signature')} >>> # or via file-like object >>> print "{0}".format(cd.scan_stream(open('/tmp/EICAR', 'rb'))) {u'stream': ('FOUND', 'Eicar-Test-Signature')} pyClamd-0.4.0/pyclamd/0000755000175000017500000000000013150641111014432 5ustar xaelxael00000000000000pyClamd-0.4.0/pyclamd/test_pyclamd.py0000644000175000017500000001543613150637235017521 0ustar xaelxael00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- import pyclamd import sys import datetime import os from nose.tools import assert_equals from nose.tools import raises from nose import with_setup from multiprocessing import Value """ test_pyclamd.py - tests cases for pyclamd Source code : https://bitbucket.org/xael/pyclamd Author : * Alexandre Norman - norman at xael.org Licence : GPL v3 or any later version This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ ########################################################################################## """ This plugin provides ``--pdb`` and ``--pdb-failures`` options. The ``--pdb`` option will drop the test runner into pdb when it encounters an error. To drop into pdb on failure, use ``--pdb-failures``. """ import pdb from nose.plugins.base import Plugin class Pdb(Plugin): """ Provides --pdb and --pdb-failures options that cause the test runner to drop into pdb if it encounters an error or failure, respectively. """ enabled_for_errors = False enabled_for_failures = False score = 5 # run last, among builtins def options(self, parser, env): """Register commandline options. """ parser.add_option( "--pdb", action="store_true", dest="debugBoth", default=env.get('NOSE_PDB', False), help="Drop into debugger on failures or errors") parser.add_option( "--pdb-failures", action="store_true", dest="debugFailures", default=env.get('NOSE_PDB_FAILURES', False), help="Drop into debugger on failures") parser.add_option( "--pdb-errors", action="store_true", dest="debugErrors", default=env.get('NOSE_PDB_ERRORS', False), help="Drop into debugger on errors") def configure(self, options, conf): """Configure which kinds of exceptions trigger plugin. """ self.conf = conf self.enabled_for_errors = options.debugErrors or options.debugBoth self.enabled_for_failures = options.debugFailures or options.debugBoth self.enabled = self.enabled_for_failures or self.enabled_for_errors def addError(self, test, err): """Enter pdb if configured to debug errors. """ if not self.enabled_for_errors: return self.debug(err) def addFailure(self, test, err): """Enter pdb if configured to debug failures. """ if not self.enabled_for_failures: return self.debug(err) def debug(self, err): import sys # FIXME why is this import here? ec, ev, tb = err stdout = sys.stdout sys.stdout = sys.__stdout__ try: pdb.post_mortem(tb) finally: sys.stdout = stdout ########################################################################################## def setup_module(): global cd cd = pyclamd.ClamdAgnostic() return def teardown_module(): os.remove('/tmp/EICAR') os.remove('/tmp/NO_EICAR') os.remove('/tmp/EICAR-éèô请收藏我们的网址') return def test_ping(): """ Tests pinging clamd """ assert(cd.ping()) def test_version(): """ Tests version """ assert_equals(cd.version().split()[0], 'ClamAV') def test_reloading_base(): """ Reloads clamd database """ assert_equals(cd.reload(), 'RELOADING') def test_stats(): """ Checks stats """ assert_equals(cd.stats().split()[0], 'POOLS:') def test_eicar(): """ Tests eicar infected file """ void = open('/tmp/EICAR','wb').write(cd.EICAR()) assert_equals(cd.scan_file('/tmp/EICAR')['/tmp/EICAR'], ('FOUND', 'Eicar-Test-Signature')) def test_no_eicar(): """ Tests standard non infected file """ void = open('/tmp/NO_EICAR','w').write('no virus in this file') assert(cd.scan_file('/tmp/NO_EICAR') is None) def test_stream(): """ Tests eicar infected stream """ assert_equals(cd.scan_stream(cd.EICAR())['stream'], ('FOUND', 'Eicar-Test-Signature')) def test_directory_scanning(): """ Tests directory scanning with eicar infected file """ directory = cd.contscan_file('/tmp/') assert_equals(directory['/tmp/EICAR'], ('FOUND', 'Eicar-Test-Signature')) def test_multiscan_file(): """ Tests multiscan file scanning with eicar infected file """ directory = cd.multiscan_file('/tmp/') assert_equals(directory['/tmp/EICAR'], ('FOUND', 'Eicar-Test-Signature')) def test_unicode_scanning(): """ Tests encoding with non latin characters (Chinese ideograms taken from random site, don't know what it mean, sorry) """ void = open('/tmp/EICAR-éèô请收藏我们的网址','wb').write(cd.EICAR()) r = cd.scan_file('/tmp/EICAR-éèô请收藏我们的网址') assert_equals(list(r.keys())[0], '/tmp/EICAR-éèô请收藏我们的网址') assert_equals(r['/tmp/EICAR-éèô请收藏我们的网址'], ('FOUND', 'Eicar-Test-Signature')) def test_scan_stream_unicode_test_eicar_in_pdf(): """ Tests stream scan with eicar in pdf file. Not detected by design of ClamAv. """ file_data = open('./probleme_data.pdf', 'rb').read() v = cd.scan_stream(file_data) assert_equals(v, None) def test_scan_stream_unicode_test_clean(): """ Tests stream scan with clean pdf file """ file_data = open('./probleme_data_clean.pdf', 'rb').read() v = cd.scan_stream(file_data) assert_equals(v, None) return def test_scan_stream_filelike_eicar(): void = open('/tmp/EICAR','wb').write(cd.EICAR()) f = open('/tmp/EICAR', 'rb') v = cd.scan_stream(f) assert_equals(v, {'stream': ('FOUND', 'Eicar-Test-Signature')}) def test_scan_stream_filelike_clean(): void = open('/tmp/NO_EICAR','w').write('no virus in this file') f = open('/tmp/NO_EICAR', 'rb') v = cd.scan_stream(f) assert_equals(v, None) def test_scan_file_unicode_test_eicar_in_pdf(): """ Tests stream scan with clean pdf file """ v = cd.scan_file('/home/xael/ESPACE_KM/python/pyclamd/probleme_data.pdf') #.assertEqual(v, {u'stream': ('FOUND', 'Eicar-Test-Signature')}) assert_equals(v, None) return ########################################################################################## pyClamd-0.4.0/pyclamd/pyclamd.py0000644000175000017500000007017513150637235016463 0ustar xaelxael00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- #------------------------------------------------------------------------------ # LICENSE: # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 3 of the License, or (at your option) any # later version. See http://www.gnu.org/licenses/lgpl-3.0.txt. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 675 Mass Ave, Cambridge, MA 02139, USA. #------------------------------------------------------------------------------ # CHANGELOG: # 2006-07-15 v0.1.1 AN: - released version # 2007-10-09 v0.2.0 PL: - fixed error with deprecated string exceptions # - added optional timeout to sockets to avoid blocking # operations # 2010-07-11 v0.2.1 AN: - change all raise exception (was deprecated), license # change to LGPL # 2010-07-12 v0.2.2 TK: - PEP8 compliance # isolating send and receive functions # 2012-11-20 v0.3.0 AN: - change API to class model # - using INSTREAM scan method instead of the deprecated STREAM # - added MULTISCAN method # - STATS now return full data on multiline # TK: - changes to API to make it more consistent # 2012-11-20 v0.3.1 AN: - typo change (Connextion to Connexion) # - Fixed Issue 3: scan_stream: AssertionError # 2013-04-20 v0.3.2 TT/AN: - improving encoding support for non latin filenames # TKL: - When pyclamd calls _recv_response, it appears to expect # that it will only get one result at a time. This is not # always the case: it may get multiple results separated # by newlines. # - Typos corrected with pyflakes # - Adding a compatibility layer for the most important # functions in the 0.2 API - init_*_socket, scan_file, # contscan_file, multiscan_file, and version. # 2013-04-21 v0.3.3 AN: - ClamdUnixSocket is now able to get unix socket name # from /etc/clamav/clamd.conf # 2013-11-16 v0.3.4 JB/AN: - Nasty encoding bug in scan_stream # 2014-06-22 v0.3.6 JS/AN: - correction in assert for filename (change to basestring) # 2014-06-23 v0.3.7 AN: - correction in README.txt and example.py # - adding pyclamd.ClamdAgnostic() # 2014-07-06 v0.3.8 AN: - License clarification (use of LGPLv3+) # 2014-07-06 v0.3.9 SK/AN: - Bug correction + setup.py improvment for building # 2014-07-06 v0.3.10 SK/AN: - Bug correction with python3 bytes stream # 2015-03-14 v0.3.14 AN : - Bug correction for clamd.conf default path # 2015-06-04 v0.3.15 AN : - optimization in scan_stream # 2015-10-21 v0.3.16 JMS : - avoid EICAR detection in py3 pyc file # 2016-08-07 v0.3.17 AN: - typo change (Connexion to Connection) # 2017-08-27 v0.4.0 RC: - modified scan_stream() to add support for passing file-like objects # BM: - add allmatchscan with file and directory support #------------------------------------------------------------------------------ # TODO: # - improve tests for Win32 platform (avoid to write EICAR file to disk, or # protect it somehow from on-access AV, inside a ZIP/GZip archive isn't enough) # - use SESSION/END commands to launch several scans in one session # (for example provide session mode in a Clamd class) # - add support for RAWSCAN commands ? # ? Maybe use os.abspath to ensure scan_file uses absolute paths for files #------------------------------------------------------------------------------ # Documentation : http://www.clamav.net/doc/latest/html/node28.html """ pyclamd.py Author : Alexandre Norman - norman()xael.org Contributors : - BM : Brandon Murphy - bitbucket () zoomequipd.com - JB : Joe Brandt - brandt.joe () gmail.com - JMS: Jack Saunders - jack () oldstlabs.com - JS : Joni Salonen - joni.salonen () qindel.com - PL : Philippe Lagadec - philippe.lagadec()laposte.net - RC : Robert Coup - SK : Scott Kitterman - debian () kitterman.com - TK : Thomas Kastner - tk()underground8.com - TKL : Thomas Kluyver - thomas () kluyver.me.uk - TT : Theodoropoulos Theodoros (TeD TeD) - sbujam()gmail.com Licence : LGLPv3+ Usage : Test strings : ^^^^^^^^^^^^ >>> import sys >>> import pyclamd >>> try: ... cd = pyclamd.ClamdUnixSocket() ... # test if server is reachable ... cd.ping() ... except pyclamd.ConnectionError: ... # if failed, test for network socket ... cd = pyclamd.ClamdNetworkSocket() ... try: ... cd.ping() ... except pyclamd.ConnectionError: ... raise ValueError('could not connect to clamd server either by unix or network socket') True >>> print(cd.version().split()[0]) ClamAV >>> print(cd.reload()) RELOADING >>> print(cd.stats().split()[0]) POOLS: >>> void = open('/tmp/EICAR','wb').write(cd.EICAR()) >>> void = open('/tmp/NO_EICAR','w').write('no virus in this file') >>> cd.scan_file('/tmp/EICAR')['/tmp/EICAR'] ('FOUND', 'Eicar-Test-Signature') >>> cd.scan_file('/tmp/NO_EICAR') is None True >>> cd.scan_stream(cd.EICAR())['stream'] ('FOUND', 'Eicar-Test-Signature') >>> directory = cd.contscan_file('/tmp/') >>> directory['/tmp/EICAR'] ('FOUND', 'Eicar-Test-Signature') >>> # Testing encoding with non latin characters (Chinese ideograms taken from random site, don't know what it mean, sorry) >>> void = open('/tmp/EICAR-éèô请收藏我们的网址','wb').write(cd.EICAR()) >>> r = cd.scan_file('/tmp/EICAR-éèô请收藏我们的网址') >>> print(list(r.keys())[0]) /tmp/EICAR-éèô请收藏我们的网址 >>> print(r['/tmp/EICAR-éèô请收藏我们的网址']) ('FOUND', 'Eicar-Test-Signature') >>> import os >>> os.remove('/tmp/EICAR') >>> os.remove('/tmp/NO_EICAR') >>> os.remove('/tmp/EICAR-éèô请收藏我们的网址') """ __version__ = "0.4.0" # $Source$ import os import sys import socket import struct import base64 import time ############################################################################ class BufferTooLongError(ValueError): """Class for errors with clamd using INSTREAM with a buffer lenght > StreamMaxLength in /etc/clamav/clamd.conf or /etc/clamd.conf""" class ConnectionError(socket.error): """Class for errors communication with clamd""" # Python 2/3 compatibility try: basestring # attempt to evaluate basestring def isstr(s): return isinstance(s, basestring) except NameError: def isstr(s): return isinstance(s, str) ############################################################################ class _ClamdGeneric(object): """ Abstract class for clamd """ def EICAR(self): """ returns Eicar test string """ # Eicar test string (encoded for skipping virus scanners) # Return a str with python2 and bytes with python3 # B64 without the final newline to avoid clam picking it up in pyc file eicar_b64 = 'WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNU\nLUZJTEUhJEgrSCo=' # Add new line separately eicar_b64 = '%s\n' % eicar_b64 EICAR = base64.b64decode(eicar_b64.encode('ascii')) return EICAR def ping(self): """ Send a PING to the clamav server, which should reply by a PONG. return: True if the server replies to PING May raise: - ConnectionError: if the server do not reply by PONG """ self._init_socket() try: self._send_command('PING') result = self._recv_response() self._close_socket() except socket.error: raise ConnectionError('Could not ping clamd server') if result == 'PONG': return True else: raise ConnectionError('Could not ping clamd server [{0}]'.format(result)) return def version(self): """ Get Clamscan version return: (string) clamscan version May raise: - ConnectionError: in case of communication problem """ self._init_socket() try: self._send_command('VERSION') result = self._recv_response() self._close_socket() except socket.error: raise ConnectionError('Could not get version information from server') return result def stats(self): """ Get Clamscan stats return: (string) clamscan stats May raise: - ConnectionError: in case of communication problem """ self._init_socket() try: self._send_command('STATS') result = self._recv_response_multiline() self._close_socket() except socket.error: raise ConnectionError('Could not get version information from server') return result def reload(self): """ Force Clamd to reload signature database return: (string) "RELOADING" May raise: - ConnectionError: in case of communication problem """ try: self._init_socket() self._send_command('RELOAD') result = self._recv_response() self._close_socket() except socket.error: raise ConnectionError('Could probably not reload signature database') return result def shutdown(self): """ Force Clamd to shutdown and exit return: nothing May raise: - ConnectionError: in case of communication problem """ try: self._init_socket() self._send_command('SHUTDOWN') self._recv_response() self._close_socket() except socket.error: raise ConnectionError('Could probably not shutdown clamd') def scan_file(self, file): """ Scan a file or directory given by filename and stop on first virus or error found. Scan with archive support enabled. file (string) : filename or directory (MUST BE ABSOLUTE PATH !) return either : - (dict): {filename1: "virusname"} - None: if no virus found May raise : - ConnectionError: in case of communication problem - socket.timeout: if timeout has expired """ assert isstr(file), 'Wrong type for [file], should be a string [was {0}]'.format(type(file)) try: self._init_socket() self._send_command('SCAN {0}'.format(file)) except socket.error: raise ConnectionError('Unable to scan {0}'.format(file)) result='...' dr={} while result: try: result = self._recv_response() except socket.error: raise ConnectionError('Unable to scan {0}'.format(file)) if len(result) > 0: filename, reason, status = self._parse_response(result) if status == 'ERROR': dr[filename] = ('ERROR', '{0}'.format(reason)) return dr elif status == 'FOUND': dr[filename] = ('FOUND', '{0}'.format(reason)) self._close_socket() if not dr: return None return dr def multiscan_file(self, file): """ Scan a file or directory given by filename using multiple threads (faster on SMP machines). Do not stop on error or virus found. Scan with archive support enabled. file (string): filename or directory (MUST BE ABSOLUTE PATH !) return either : - (dict): {filename1: ('FOUND', 'virusname'), filename2: ('ERROR', 'reason')} - None: if no virus found May raise: - ConnectionError: in case of communication problem """ assert isstr(file), 'Wrong type for [file], should be a string [was {0}]'.format(type(file)) try: self._init_socket() self._send_command('MULTISCAN {0}'.format(file)) except socket.error: raise ConnectionError('Unable to scan {0}'.format(file)) result='...' dr={} while result: try: result = self._recv_response() except socket.error: raise ConnectionError('Unable to scan {0}'.format(file)) if len(result) > 0: for resline in result.splitlines(): filename, reason, status = self._parse_response(resline) if status == 'ERROR': dr[filename] = ('ERROR', '{0}'.format(reason)) elif status == 'FOUND': dr[filename] = ('FOUND', '{0}'.format(reason)) self._close_socket() if not dr: return None return dr def allmatchscan(self, file): """ Scan a file or directory given by filename and after finding a virus within a file, continues scanning for additional viruses. Scan with archive support enabled. file (string) : filename or directoy (MUST BE ABSOLUTE PATH !) return either : - (dict): {filename1: [(FOUND', 'virusname1'), (FOUND', 'virusname2')], filename2: [(FOUND', 'virusname1'), (FOUND', 'virusname3')]} - None: if no virus found May raise : - ConnectionError: in case of communication problem - socket.timeout: if timeout has expired """ assert isstr(file), 'Wrong type for [file], should be a string [was {0}]'.format(type(file)) dr={} if os.path.isdir(file): for path, subdirs, files in os.walk(file): for name in files: single_file_result = self.allmatchscan(os.path.join(path,name)) if single_file_result: dr.update(single_file_result) else: try: self._init_socket() self._send_command('ALLMATCHSCAN {0}'.format(file)) except socket.error: raise ConnectionError('Unable to scan {0}'.format(file)) result='...' while result: try: result = self._recv_response() except socket.error: raise ConnectionError('Unable to scan {0}'.format(file)) if len(result) > 0: for resline in result.splitlines(): filename, reason, status = self._parse_response(resline) if status == 'ERROR': if filename not in dr: dr[filename] = [] dr[filename].append(('ERROR', '{0}'.format(reason))) elif status == 'FOUND': if filename not in dr: dr[filename] = [] dr[filename].append(('FOUND', '{0}'.format(reason))) self._close_socket() if not dr: return None return dr def contscan_file(self, file): """ Scan a file or directory given by filename Do not stop on error or virus found. Scan with archive support enabled. file (string): filename or directory (MUST BE ABSOLUTE PATH !) return either : - (dict): {filename1: ('FOUND', 'virusname'), filename2: ('ERROR', 'reason')} - None: if no virus found May raise: - ConnectionError: in case of communication problem """ assert isstr(file), 'Wrong type for [file], should be a string [was {0}]'.format(type(file)) try: self._init_socket() self._send_command('CONTSCAN {0}'.format(file)) except socket.error: raise ConnectionError('Unable to scan {0}'.format(file)) result='...' dr={} while result: try: result = self._recv_response() except socket.error: raise ConnectionError('Unable to scan {0}'.format(file)) if len(result) > 0: for resline in result.splitlines(): filename, reason, status = self._parse_response(resline) if status == 'ERROR': dr[filename] = ('ERROR', '{0}'.format(reason)) elif status == 'FOUND': dr[filename] = ('FOUND', '{0}'.format(reason)) self._close_socket() if not dr: return None return dr def scan_stream(self, stream, chunk_size=4096): """ Scan a buffer on Python2.X : - input (string): buffer to scan on Python3.X : - input (bytes or bytearray): buffer to scan return either: - (dict): {filename1: "virusname"} - None: if no virus found May raise : - BufferTooLongError: if the buffer size exceeds clamd limits - ConnectionError: in case of communication problem """ if sys.version_info[0] <= 2: # Python2 assert hasattr(stream, "read") or isinstance(stream, str), 'Wrong type for [stream], should be str/file-like [was {0}]'.format(type(stream)) else: # Python3 assert hasattr(stream, "read") or isinstance(stream, (bytes, bytearray)), 'Wrong type for [stream], should be bytes/bytearray/file-like [was {0}]'.format(type(stream)) is_file_like = hasattr(stream, 'read') try: self._init_socket() self._send_command('INSTREAM') except socket.error: raise ConnectionError('Unable to scan stream') if is_file_like: while True: chunk = stream.read(chunk_size) if not chunk: break size = struct.pack('!L', len(chunk)) try: self.clamd_socket.send(size) self.clamd_socket.send(chunk) except socket.error: raise # Terminating stream self.clamd_socket.send(struct.pack('!L', 0)) else: # bytearray for n in range(1 + int(len(stream)/chunk_size)): chunk = stream[n*chunk_size:(n+1)*chunk_size] size = struct.pack('!L', len(chunk)) try: self.clamd_socket.send(size) self.clamd_socket.send(chunk) except socket.error: raise else: # Terminating stream self.clamd_socket.send(struct.pack('!L', 0)) result='...' dr = {} while result: try: result = self._recv_response() except socket.error: raise ConnectionError('Unable to scan stream') if len(result) > 0: if result == 'INSTREAM size limit exceeded. ERROR': raise BufferTooLongError(result) filename, reason, status = self._parse_response(result) if status == 'ERROR': dr[filename] = ('ERROR', '{0}'.format(reason)) elif status == 'FOUND': dr[filename] = ('FOUND', '{0}'.format(reason)) self._close_socket() if not dr: return None return dr def _send_command(self, cmd): """ `man clamd` recommends to prefix commands with z, but we will use \n terminated strings, as python<->clamd has some problems with \0x00 """ try: cmd = str.encode('n{0}\n'.format(cmd)) except UnicodeDecodeError: cmd = 'n{0}\n'.format(cmd) self.clamd_socket.send(cmd) return def _recv_response(self): """ receive response from clamd and strip all whitespace characters """ # If we connect too quickly # sometimes we get a connection error # so we retry failed_count = 5 while True: try: data = self.clamd_socket.recv(4096) except socket.error: time.sleep(0.01) failed_count -= 1 if failed_count == 0: raise else: break try: response = bytes.decode(data).strip() except UnicodeDecodeError: response = data.strip() return response def _recv_response_multiline(self): """ receive multiple line response from clamd and strip all whitespace characters """ response = '' c = '...' while c != '': c = self._recv_response() response += '{0}\n'.format(c) return response def _close_socket(self): """ close clamd socket """ self.clamd_socket.close() return def _parse_response(self, msg): """ parses responses for SCAN, CONTSCAN, MULTISCAN and STREAM commands. """ msg = msg.strip() filename = msg.split(': ')[0] left = msg.split(': ')[1:] if isstr(left): result = left else: result = ": ".join(left) if result != 'OK': parts = result.split() reason = ' '.join(parts[:-1]) status = parts[-1] else: reason, status = '', 'OK' return filename, reason, status ############################################################################ class ClamdUnixSocket(_ClamdGeneric): """ Class for using clamd with an unix socket """ def __init__(self, filename=None, timeout=None): """ Unix Socket Class initialisation filename (string) : unix socket filename or None to get the socket from /etc/clamav/clamd.conf or /etc/clamd.conf timeout (float or None) : socket timeout """ # try to get unix socket from clamd.conf if filename is None: for clamdpath in ['/etc/clamav/clamd.conf', '/etc/clamd.conf']: if os.path.isfile(clamdpath): break else: raise ConnectionError('Could not find clamd unix socket from /etc/clamav/clamd.conf or /etc/clamd.conf') with open(clamdpath, 'r') as conffile: for line in conffile.readlines(): try: if line.strip().split()[0] == 'LocalSocket': filename = line.strip().split()[1] break except IndexError: pass else: raise ConnectionError('Could not find clamd unix socket from /etc/clamav/clamd.conf or /etc/clamd.conf') assert isstr(filename), 'Wrong type for [file], should be a string [was {0}]'.format(type(file)) assert isinstance(timeout, (float, int)) or timeout is None, 'Wrong type for [timeout], should be either None or a float [was {0}]'.format(type(timeout)) _ClamdGeneric.__init__(self) self.unix_socket = filename self.timeout = timeout # tests the socket self._init_socket() self._close_socket() return def _init_socket(self): """ internal use only """ self.clamd_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) if not self.timeout is None: self.clamd_socket.settimeout(self.timeout) try: self.clamd_socket.connect(self.unix_socket) except socket.error: raise ConnectionError('Could not reach clamd using unix socket ({0})'.format((self.unix_socket))) return ############################################################################ class ClamdNetworkSocket(_ClamdGeneric): """ Class for using clamd with a network socket """ def __init__(self, host='127.0.0.1', port=3310, timeout=None): """ Network Class initialisation host (string) : hostname or ip address port (int) : TCP port timeout (float or None) : socket timeout """ assert isinstance(host, str), 'Wrong type for [host], should be a string [was {0}]'.format(type(host)) assert isinstance(port, int), 'Wrong type for [port], should be an int [was {0}]'.format(type(port)) assert isinstance(timeout, (float, int)) or timeout is None, 'Wrong type for [timeout], should be either None or a float [was {0}]'.format(type(timeout)) _ClamdGeneric.__init__(self) self.host = host self.port = port self.timeout = timeout # tests the socket self._init_socket() self._close_socket() return def _init_socket(self): """ internal use only """ self.clamd_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if not self.timeout is None: self.clamd_socket.settimeout(self.timeout) try: self.clamd_socket.connect((self.host, self.port)) except socket.error: raise ConnectionError('Could not reach clamd using network ({0}, {1})'.format(self.host, self.port)) return ############################################################################ def ClamdAgnostic(): """ Tries to connect to clamd using ClamdUnixSocket or if it fails, tries with ClamdNetworkSocket and return the corresponding object. Of course, it tries to connect with default settings... """ try: # Create object for using unix socket cd = ClamdUnixSocket() except ConnectionError: # if failed, test for network socket try: cd = ClamdNetworkSocket() except ConnectionError: raise ValueError("could not connect to clamd server either by unix or network socket") return cd ############################################################################ # Backwards compatibility API ############################################## socketinst = None def init_network_socket(host='127.0.0.1', port=3310, timeout=None): """Deprecated API - use ClamdNetworkSocket instead.""" global socketinst socketinst = ClamdNetworkSocket(host=host, port=port, timeout=timeout) def init_unix_socket(filename=None): """Deprecated API - use ClamdUnixSocket instead.""" global socketinst socketinst = ClamdUnixSocket(filename=filename) def _needs_socket(func): """Decorator to check that the global socket is initialised.""" def wrapper(*args, **kw): if socketinst is None: raise ConnectionError('socket not initialised') return func(*args, **kw) wrapper.__doc__ = func.__doc__ return wrapper @_needs_socket def scan_file(file): """Deprecated API - use one of the Clamd*Socket classes instead.""" return socketinst.scan_file(file) @_needs_socket def contscan_file(file): """Deprecated API - use one of the Clamd*Socket classes instead.""" return socketinst.contscan_file(file) @_needs_socket def multiscan_file(file): """Deprecated API - use one of the Clamd*Socket classes instead.""" return socketinst.multiscan_file(file) @_needs_socket def version(): """Deprecated API - use one of the Clamd*Socket classes instead.""" return socketinst.version() ############################################################################ def _non_regression_test(): """ This is for internal use """ import doctest doctest.testmod() return ############################################################################ def _print_doc(): """ This is for internal use """ import os os.system('pydoc ./{0}.py'.format(__name__)) return # MAIN ------------------- if __name__ == '__main__': _non_regression_test() ############################################################################ pyClamd-0.4.0/pyclamd/__init__.py0000664000175000017500000000036712361011054016553 0ustar xaelxael00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import sys if sys.version_info[0] <= 2: from pyclamd import __version__ from pyclamd import * elif sys.version_info[0] >= 3: from .pyclamd import __version__ from .pyclamd import * pyClamd-0.4.0/pyClamd.egg-info/0000755000175000017500000000000013150641111016064 5ustar xaelxael00000000000000pyClamd-0.4.0/pyClamd.egg-info/top_level.txt0000664000175000017500000000001013150641111020607 0ustar xaelxael00000000000000pyclamd pyClamd-0.4.0/pyClamd.egg-info/dependency_links.txt0000664000175000017500000000000113150641111022134 0ustar xaelxael00000000000000 pyClamd-0.4.0/pyClamd.egg-info/SOURCES.txt0000664000175000017500000000031613150641111017752 0ustar xaelxael00000000000000README.txt setup.py pyClamd.egg-info/PKG-INFO pyClamd.egg-info/SOURCES.txt pyClamd.egg-info/dependency_links.txt pyClamd.egg-info/top_level.txt pyclamd/__init__.py pyclamd/pyclamd.py pyclamd/test_pyclamd.pypyClamd-0.4.0/pyClamd.egg-info/PKG-INFO0000664000175000017500000000217213150641111017165 0ustar xaelxael00000000000000Metadata-Version: 1.1 Name: pyClamd Version: 0.4.0 Summary: pyClamd is a python interface to Clamd (Clamav daemon). Home-page: http://xael.org/pages/pyclamd-en.html Author: Alexandre Norman Author-email: norman@xael.org License: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) Download-URL: http://xael.org/norman/python/pyclamd/ Description: pyClamd is a python interface to Clamd (Clamav daemon). By using pyClamd, you can add virus detection capabilities to your python software in an efficient and easy way. Instead of pyClamav which uses libclamav, pyClamd may be used by a closed source product. Keywords: python,clamav,antivirus,scanner,virus,libclamav Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) Classifier: Operating System :: OS Independent Classifier: Topic :: System Classifier: Topic :: Security pyClamd-0.4.0/setup.py0000664000175000017500000000344213150641100014516 0ustar xaelxael00000000000000 #from distutils.core import setup, Extension #pyclamd = Extension('pyclamd', # sources = ['pyclamd.py']) from setuptools import setup # Install : python setup.py install # Register : python setup.py register try: import pyclamd except ImportError as x: # all we want is the version pyclamd.__version__ = 'unknown' setup (name = 'pyClamd', version = pyclamd.__version__, download_url = 'http://xael.org/norman/python/pyclamd/', package_dir={'pyclamd': 'pyclamd'}, packages=['pyclamd'], license ='License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', author = 'Alexandre Norman', author_email = 'norman@xael.org', keywords='python, clamav, antivirus, scanner, virus, libclamav', url = 'http://xael.org/pages/pyclamd-en.html', bugtrack_url = 'https://bitbucket.org/xael/pyclamd', include_dirs = ['/usr/local/include'], description = 'pyClamd is a python interface to Clamd (Clamav daemon).', long_description = 'pyClamd is a python interface to Clamd (Clamav daemon). By using pyClamd, you can add virus detection capabilities to your python software in an efficient and easy way. Instead of pyClamav which uses libclamav, pyClamd may be used by a closed source product.', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', 'Operating System :: OS Independent', 'Topic :: System', 'Topic :: Security', ], ) pyClamd-0.4.0/setup.cfg0000644000175000017500000000004613150641111014622 0ustar xaelxael00000000000000[egg_info] tag_build = tag_date = 0