diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9627b6b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,56 @@ +name: Tests + +on: + pull_request: + branches: '*' + + workflow_dispatch: + inputs: + branch: + description: 'The branch, tag or SHA to release from' + required: true + default: 'master' + +jobs: + tests: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python: [3.6] + os: [ubuntu-latest] + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.event.inputs.branch }} + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - name: Install test dependencies + run: python -m pip install pytest + + - name: Install current version of the clamd package + run: python -m pip install -e . + + - name: Install and update clamav engine + run: | + sudo apt update + sudo apt-get install clamav-daemon clamav-freshclam clamav-unofficial-sigs --yes + sudo systemctl stop clamav-freshclam.service + sudo freshclam --verbose + sudo systemctl restart clamav-daemon.service + + - name: Wait for 25 seconds until clamd socket becomes available + run: | + secs=25 + while [[ $secs -gt 0 ]] && ! [[ -f "/var/run/clamav/clamd.ctl" ]]; + do + echo -ne "$secs\033[0K\r" + sleep 1 + : $((secs--)) + done + + - name: Run unit tests + run: pytest diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..9ccaad3 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,29 @@ +# Changes + +## 1.0.3 (unreleased) + +- Nothing changed yet. + +## 1.0.2 (2014-08-21) + +- Remove all dependencies. clamd is now standalone! +- Use plain setuptools no d2to1. +- Create universal wheel. + +## 1.0.1 (2013-03-06) + +- Updated d2to1 dependency + +## 1.0.0 (2013-02-08) + +- Change public interface, including exceptions +- Support Python 3.3, withdraw 2.5 support + +## 0.3.4 (2013-02-01) + +- Use regex to parse file status reponse instead of complicated string + split/join + +## 0.3.3 (2013-01-28) + +- First version of clamd that can be installed from PyPI diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index 0477c20..0000000 --- a/CHANGES.rst +++ /dev/null @@ -1,40 +0,0 @@ -Changes -========= - -1.0.3 (unreleased) ------------------- - -- Nothing changed yet. - - -1.0.2 (2014-08-21) ------------------- - -- Remove all dependencies. clamd is now standalone! -- Use plain setuptools no d2to1. -- Create universal wheel. - - -1.0.1 (2013-03-06) ------------------- - -- Updated d2to1 dependency - - -1.0.0 (2013-02-08) ------------------- - -- Change public interface, including exceptions -- Support Python 3.3, withdraw 2.5 support - - -0.3.4 (2013-02-01) ------------------- - -- Use regex to parse file status reponse instead of complicated string split/join - - -0.3.3 (2013-01-28) ------------------- - -- First version of clamd that can be installed from PyPI diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index ad7f912..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include CHANGES.rst -include README.rst -include ez_setup.py - -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b0fe49 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# clamd + +`clamd` is a portable Python module to use the ClamAV anti-virus engine on Windows, Linux, MacOSX and other platforms. It requires a running instance of the clamd daemon. + +### History + +This is a fork of `pyClamd` (v0.2.0) created by Philippe Lagadec and published on [his](http://www.decalage.info/en/python/pyclamd) website, which in turn is a slightly improved version of `pyClamd` (v0.1.1) created by Alexandre Norman and published on [his](http://xael.org/norman/python/pyclamd/) website. + +## Installation + +Make sure you have installed both `clamav` engine and `clamav-daemon`, for instance, you can install it on Ubuntu by running the following commands: + +```bash +apt-get install clamav-daemon clamav-freshclam clamav-unofficial-sigs +freshclam # update the database +systemctl start clamav-daemon +``` + +```bash +pip install clamd +``` + +## Usage/Examples + +To use with a Unix socket: + +```python +>>> import clamd +>>> cd = clamd.ClamdUnixSocket() +>>> cd.ping() +'PONG' +>>> cd.version() # doctest: +ELLIPSIS +'ClamAV ... +>>> cd.reload() +'RELOADING' +``` + +To scan a file: + +```python +>>> open('/tmp/EICAR','wb').write(clamd.EICAR) +>>> cd.scan('/tmp/EICAR') +{'/tmp/EICAR': ('FOUND', 'Eicar-Test-Signature')} +``` + +To scan a stream: +```python +>>> from io import BytesIO +>>> cd.instream(BytesIO(clamd.EICAR)) +{'stream': ('FOUND', 'Eicar-Test-Signature')} +``` +`clamav` daemon runs under `clamav` user and might not be able to scan files owned by other users or root user, in this case you can use `fdscan` function which opens a file and then passes the file descriptor to `clamav` daemon: + +```python +>>> open('/tmp/EICAR','wb').write(clamd.EICAR) +>>> cd.fdscan('/tmp/EICAR') +{'/tmp/EICAR': ('FOUND', 'Eicar-Test-Signature')} +``` + +## License + +clamd is released as open-source software under the LGPL license. diff --git a/README.rst b/README.rst deleted file mode 100644 index 4e9315d..0000000 --- a/README.rst +++ /dev/null @@ -1,53 +0,0 @@ -clamd -===== - -.. image:: https://travis-ci.org/graingert/python-clamd.png?branch=master - :alt: travis build status - :target: https://travis-ci.org/graingert/python-clamd - -About ------ -`clamd` is a portable Python module to use the ClamAV anti-virus engine on -Windows, Linux, MacOSX and other platforms. It requires a running instance of -the `clamd` daemon. - -This is a fork of pyClamd v0.2.0 created by Philippe Lagadec and published on his website: http://www.decalage.info/en/python/pyclamd which in turn is a slightly improved version of pyClamd v0.1.1 created by Alexandre Norman and published on his website: http://xael.org/norman/python/pyclamd/ - -Usage ------ - -To use with a unix socket:: - - >>> import clamd - >>> cd = clamd.ClamdUnixSocket() - >>> cd.ping() - 'PONG' - >>> cd.version() # doctest: +ELLIPSIS - 'ClamAV ... - >>> cd.reload() - 'RELOADING' - -To scan a file:: - - >>> open('/tmp/EICAR','wb').write(clamd.EICAR) - >>> cd.scan('/tmp/EICAR') - {'/tmp/EICAR': ('FOUND', 'Eicar-Test-Signature')} - -To scan a stream:: - - >>> from io import BytesIO - >>> cd.instream(BytesIO(clamd.EICAR)) - {'stream': ('FOUND', 'Eicar-Test-Signature')} - - -License -------- -`clamd` is released as open-source software under the LGPL license. - -clamd Install -------------- -How to install the ClamAV daemon `clamd` under Ubuntu:: - - sudo apt-get install clamav-daemon clamav-freshclam clamav-unofficial-sigs - sudo freshclam - sudo service clamav-daemon start diff --git a/setup.cfg b/setup.cfg index c5f2335..3ed18ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,39 @@ -[nosetests] -with-doctest=1 +[metadata] +author = "Thomas Grainger" +author_email = python-clamd@graingert.co.uk +classifiers = + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 +license = GNU Library or Lesser General Public License (LGPL) +name = clamd +description = "Clamd is a python interface to Clamd (Clamav daemon)." +long_description = file:README.md +long_description_content_type = text/markdown +keywords = python, clamav, antivirus, scanner, virus, libclamav, clamd +project_urls = + Source Code = https://github.com/graingert/python-clamd + Change Log = https://github.com/graingert/python-clamd/blob/master/CHANGES.md +url = https://github.com/graingert/python-clamd +version = 1.0.3.dev0 -[wheel] -universal=1 +[options] +include_package_data = False +zip_safe = True +package_dir = + =src +packages = find: +python_requires = >=3.6 + +[options.packages.find] +where=src +exclude = + src.tests + +[bdist_wheel] +universal = 1 + +[flake8] +max_line_length = 117 diff --git a/setup.py b/setup.py index 901a206..36f7fe7 100755 --- a/setup.py +++ b/setup.py @@ -1,28 +1,4 @@ -#!/usr/bin/env python -from ez_setup import use_setuptools -use_setuptools() +#!/usr/bin/env python3 +from setuptools import setup -from setuptools import setup, find_packages - -readme = open('README.rst').read() -history = open('CHANGES.rst').read().replace('.. :changelog:', '') - -setup( - name="clamd", - version='1.0.3.dev0', - author="Thomas Grainger", - author_email="python-clamd@graingert.co.uk", - maintainer="Thomas Grainger", - maintainer_email = "python-clamd@graingert.co.uk", - keywords = "python, clamav, antivirus, scanner, virus, libclamav, clamd", - description = "Clamd is a python interface to Clamd (Clamav daemon).", - long_description=readme + '\n\n' + history, - url="https://github.com/graingert/python-clamd", - package_dir={'': 'src'}, - packages=find_packages('src', exclude="tests"), - classifiers = [ - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - ], - zip_safe=True, - include_package_data=False, -) +setup() diff --git a/src/clamd/__init__.py b/src/clamd/__init__.py index 92ff640..d414d33 100644 --- a/src/clamd/__init__.py +++ b/src/clamd/__init__.py @@ -1,26 +1,22 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - try: - __version__ = __import__('pkg_resources').get_distribution('clamd').version -except: - __version__ = '' - -# $Source$ - + __version__ = __import__("pkg_resources").get_distribution("clamd").version +except ImportError: + __version__ = "" -import socket -import sys -import struct +import base64 import contextlib import re -import base64 +import socket +import struct +import sys +from multiprocessing.reduction import sendfds -scan_response = re.compile(r"^(?P.*): ((?P.+) )?(?P(FOUND|OK|ERROR))$") +scan_response = re.compile( + r"^(?P.*): ((?P.+) )?(?P(FOUND|OK|ERROR))$" +) EICAR = base64.b64decode( - b'WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5E' - b'QVJELUFOVElWSVJVUy1URVNU\nLUZJTEUhJEgrSCo=\n' + b"WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5E" + b"QVJELUFOVElWSVJVUy1URVNU\nLUZJTEUhJEgrSCo=\n" ) @@ -313,3 +309,16 @@ def _error_message(self, exception): path=self.unix_socket, msg=exception.args[1] ) + + def fdscan(self, file): + """Scan a file referenced by a file descriptor.""" + try: + self._init_socket() + with open(file, mode="rb") as fp: + self._send_command("FILDES") + sendfds(self.clamd_socket, [fp.fileno()]) + result = self._recv_response() + _, reason, status = self._parse_response(result) + return {file: (status, reason)} + finally: + self._close_socket() diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 42423b6..04049f5 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -1,19 +1,17 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -import clamd -from io import BytesIO -from contextlib import contextmanager -import tempfile -import shutil import os +import shutil import stat +import tempfile +from contextlib import contextmanager +from io import BytesIO +import clamd import pytest mine = (stat.S_IREAD | stat.S_IWRITE) other = stat.S_IROTH execute = (stat.S_IEXEC | stat.S_IXOTH) +EICAR_SIG_NAME = "Win.Test.EICAR_HDB-1" @contextmanager @@ -45,7 +43,7 @@ def test_scan(self): f.write(clamd.EICAR) f.flush() os.fchmod(f.fileno(), (mine | other)) - expected = {f.name: ('FOUND', 'Eicar-Test-Signature')} + expected = {f.name: ('FOUND', EICAR_SIG_NAME)} assert self.cd.scan(f.name) == expected @@ -54,7 +52,7 @@ def test_unicode_scan(self): f.write(clamd.EICAR) f.flush() os.fchmod(f.fileno(), (mine | other)) - expected = {f.name: ('FOUND', 'Eicar-Test-Signature')} + expected = {f.name: ('FOUND', EICAR_SIG_NAME)} assert self.cd.scan(f.name) == expected @@ -65,18 +63,27 @@ def test_multiscan(self): with open(os.path.join(d, "file" + str(i)), 'wb') as f: f.write(clamd.EICAR) os.fchmod(f.fileno(), (mine | other)) - expected[f.name] = ('FOUND', 'Eicar-Test-Signature') + expected[f.name] = ('FOUND', EICAR_SIG_NAME) os.chmod(d, (mine | other | execute)) assert self.cd.multiscan(d) == expected def test_instream(self): - expected = {'stream': ('FOUND', 'Eicar-Test-Signature')} + expected = {'stream': ('FOUND', EICAR_SIG_NAME)} assert self.cd.instream(BytesIO(clamd.EICAR)) == expected def test_insteam_success(self): assert self.cd.instream(BytesIO(b"foo")) == {'stream': ('OK', None)} + def test_fdscan(self): + with tempfile.NamedTemporaryFile('wb', prefix="python-clamd") as f: + f.write(clamd.EICAR) + f.flush() + os.fchmod(f.fileno(), (mine | other)) + expected = {f.name: ('FOUND', EICAR_SIG_NAME)} + + assert self.cd.fdscan(f.name) == expected + class TestUnixSocketTimeout(TestUnixSocket): kwargs = {"timeout": 20}