Skip to content

Commit

Permalink
add tcp/ip socket support
Browse files Browse the repository at this point in the history
  • Loading branch information
mutantsan committed Dec 6, 2021
1 parent af79550 commit 2bc6c7b
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 28 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ To install ckanext-clamav:
# (optional, default: True).
ckanext.clamav.upload_unscanned = False

# ClamAV connection mechanism. There are two options: `tcp` or `unix`.
# If `tcp` selected, you must provide host:port (check next options).
# ( optional, default: unix)
ckanext.clamav.socket_type = unix

# TCP/IP hostname
ckanext.clamav.tcp.host = your.hostname.address

# TCP/IP port
ckanext.clamav.tcp.port = 3310

# ClamAV connection timeout. Either `tcp` or `unix`
# By default, there is no timeout.
# ( optional, default: 60)
ckanext.clamav.timeout = 120


## Developer installation

Expand Down
26 changes: 26 additions & 0 deletions ckanext/clamav/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import socket
import sys

from clamd import ClamdNetworkSocket, ConnectionError


class CustomClamdNetworkSocket(ClamdNetworkSocket):
"""Patches the default ClamdNetworkSocket adapter
with changed _init_socket method. The default implementation doesn't
respect timeout properly.
Args:
ClamdNetworkSocket (ClamdNetworkSocket): original clamd network adapter
"""
def _init_socket(self):
"""
internal use only
"""
try:
self.clamd_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.clamd_socket.settimeout(self.timeout)
self.clamd_socket.connect((self.host, self.port))

except (socket.error, socket.timeout):
e = sys.exc_info()[1]
raise ConnectionError(self._error_message(e))
15 changes: 15 additions & 0 deletions ckanext/clamav/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CLAMAV_STATUS_FOUND: str = "FOUND"
CLAMAV_STATUS_ERR_FILELIMIT: str = "ERR_FILELIMIT"
CLAMAV_STATUS_ERR_DISABLED: str = "ERR_DISABLED"
CLAMAV_CONF_SOCKET_PATH: str = "ckanext.clamav.socket_path"
CLAMAV_CONF_SOCKET_PATH_DF: str = "/var/run/clamav/clamd.ctl"
CLAMAV_CONF_UPLOAD_UNSCANNED: str = "ckanext.clamav.upload_unscanned"
CLAMAV_CONF_UPLOAD_UNSCANNED_DF: bool = True
CLAMAV_SOCK_UNIX: str = "unix"
CLAMAV_SOCK_TCP: str = "tcp"
CLAMAV_CONF_SOCKET_TYPE: str = "ckanext.clamav.socket_type"
CLAMAV_CONF_SOCKET_TYPE_DF: str = CLAMAV_SOCK_UNIX
CLAMAV_CONF_SOCK_TCP_HOST: str = "ckanext.clamav.tcp.host"
CLAMAV_CONF_SOCK_TCP_PORT: str = "ckanext.clamav.tcp.port"
CLAMAV_CONF_CONN_TIMEOUT: str = "ckanext.clamav.timeout"
CLAMAV_CONF_CONN_TIMEOUT_DF: int = 60
90 changes: 64 additions & 26 deletions ckanext/clamav/utils.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
from __future__ import annotations
import logging
from typing import Optional, Any
from typing import Optional, Any, Union

import clamd
from clamd import (
ClamdUnixSocket,
ClamdNetworkSocket,
ConnectionError as ClamConnectionError,
BufferTooLongError,
)
from werkzeug.datastructures import FileStorage

import ckan.logic as logic
import ckan.plugins.toolkit as tk
from ckan.exceptions import CkanConfigurationException

import ckanext.clamav.config as c
from ckanext.clamav.adapters import CustomClamdNetworkSocket

log = logging.getLogger(__name__)

CLAMAV_STATUS_FOUND: str = "FOUND"
CLAMAV_STATUS_ERR_FILELIMIT: str = "ERR_FILELIMIT"
CLAMAV_STATUS_ERR_DISABLED: str = "ERR_DISABLED"
CLAMAV_CONF_SOCKET_PATH: str = "ckanext.clamav.socket_path"
CLAMAV_CONF_SOCKET_PATH_DF: str = "/var/run/clamav/clamd.ctl"
CLAMAV_CONF_UPLOAD_UNSCANNED: str = "ckanext.clamav.upload_unscanned"
CLAMAV_CONF_UPLOAD_UNSCANNED_DF: bool = True
log = logging.getLogger(__name__)


def scan_file_for_viruses(data_dict: dict[str, Any]):
Expand All @@ -37,29 +33,31 @@ def scan_file_for_viruses(data_dict: dict[str, Any]):
upload form
"""
upload_unscanned: bool = tk.asbool(tk.config.get(
CLAMAV_CONF_UPLOAD_UNSCANNED, CLAMAV_CONF_UPLOAD_UNSCANNED_DF
c.CLAMAV_CONF_UPLOAD_UNSCANNED, c.CLAMAV_CONF_UPLOAD_UNSCANNED_DF
))

file: FileStorage = data_dict["upload"]
status: str
signature: Optional[str]
status, signature = _scan_filestream(file)

if status == CLAMAV_STATUS_ERR_DISABLED:
log.info("Unable to connect to clamav. Can't scan the file")
if status == c.CLAMAV_STATUS_ERR_DISABLED:
log.info("Clamd: unable to connect to clamav. Can't scan the file")
if upload_unscanned:
log.info(_get_unscanned_file_message(file, data_dict['package_id']))
log.info(_get_unscanned_file_message(
file, data_dict['package_id']))
else:
raise logic.ValidationError({"Virus checker": [
"The clamav is disabled. Can't uploade the file. Contact administrator"
]})
elif status in (CLAMAV_STATUS_ERR_FILELIMIT,):
elif status in (c.CLAMAV_STATUS_ERR_FILELIMIT,):
log.warning(signature)
if upload_unscanned:
log.info(_get_unscanned_file_message(file, data_dict['package_id']))
log.info(_get_unscanned_file_message(
file, data_dict['package_id']))
else:
raise logic.ValidationError({"Virus checker": [signature]})
elif status == CLAMAV_STATUS_FOUND:
elif status == c.CLAMAV_STATUS_FOUND:
error_msg: str = (
"malware has been found. "
f"Filename: {file.filename}, signature: {signature}."
Expand All @@ -79,30 +77,70 @@ def _scan_filestream(file: FileStorage) -> tuple[str, Optional[str]]:
if status is returned error code, then instead of the signature there will
be an error message.
"""
socket_path: str = tk.config.get(
CLAMAV_CONF_SOCKET_PATH, CLAMAV_CONF_SOCKET_PATH_DF
)
cd: ClamdUnixSocket = clamd.ClamdUnixSocket(socket_path)

cd: Union[ClamdUnixSocket, ClamdNetworkSocket] = _get_conn()

try:
scan_result: dict[str, tuple[str, Optional[str]]] = cd.instream(file.stream)
scan_result: dict[str, tuple[str, Optional[str]]
] = cd.instream(file.stream)
except BufferTooLongError:
error_msg: str = (
"the uploaded file exceeds the filesize limit "
"The file will not be scanned"
)
log.error(error_msg)
return (CLAMAV_STATUS_ERR_FILELIMIT, error_msg)
return (c.CLAMAV_STATUS_ERR_FILELIMIT, error_msg)
except ClamConnectionError:
error_msg: str = "clamav is not accessible, check its status."
log.critical(error_msg)
return (CLAMAV_STATUS_ERR_DISABLED, error_msg)
return (c.CLAMAV_STATUS_ERR_DISABLED, error_msg)

return scan_result["stream"]


def _get_unscanned_file_message(file: FileStorage, pkg_id: str) -> str:
def _get_conn() -> Union[ClamdUnixSocket, CustomClamdNetworkSocket]:
"""
Simply connects to the ClamAV via TCP/IP or Unix socket and returns
the connection object
Returns:
Union[ClamdUnixSocket, CustomClamdNetworkSocket]: a connection to ClamAV
Support two type of connection mechanism - TCP/IP or Unix socket
Raises:
CkanConfigurationException: if the TCP/IP connection mechanism has been choosen,
the host:port must be provided, otherwise, raises an exception
CkanConfigurationException: raises an exception, if the unsupported connection
mechanism has been choosen
"""
socket_type: str = tk.config.get(
c.CLAMAV_CONF_SOCKET_TYPE, c.CLAMAV_CONF_SOCKET_TYPE_DF
)
conn_timeout: int = ftk.asint(tk.config.get(
c.CLAMAV_CONF_CONN_TIMEOUT, c.CLAMAV_CONF_CONN_TIMEOUT_DF
))

if socket_type not in (c.CLAMAV_SOCK_UNIX, c.CLAMAV_SOCK_TCP):
raise CkanConfigurationException("Clamd: unsupported connection type")

if socket_type == c.CLAMAV_SOCK_UNIX:
socket_path: str = tk.config.get(
c.CLAMAV_CONF_SOCKET_PATH, c.CLAMAV_CONF_SOCKET_PATH_DF
)
return ClamdUnixSocket(socket_path, conn_timeout)

tcp_host: str = tk.config.get(c.CLAMAV_CONF_SOCK_TCP_HOST)
tcp_port: str = tk.asint(tk.config.get(c.CLAMAV_CONF_SOCK_TCP_PORT))

if not all((tcp_port, tcp_host)):
raise CkanConfigurationException("Clamd: please, provide TCP/IP host:port for ClamAV")

return CustomClamdNetworkSocket(tcp_host, tcp_port, conn_timeout)


def _get_unscanned_file_message(file: FileStorage, pkg_id: str) -> str:
return (
"The unscanned file will be uploaded because unscanned fileupload is enabled. "
f"Filename: {file.filename}, package_id: {pkg_id}, name: {file.filename or None}"
)
)
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# http://packaging.python.org/en/latest/tutorial.html#version
version='0.0.1',
version='1.0.2',

description='''Check upload resources for viruses''',
long_description=long_description,
Expand Down Expand Up @@ -56,13 +56,14 @@
# simple. Or you can use find_packages().
packages=find_packages(exclude=['contrib', 'docs', 'tests*']),
namespace_packages=['ckanext'],

python_requires=">=3.7",
install_requires=[
# CKAN extensions should not list dependencies here, but in a separate
# ``requirements.txt`` file.
#
# http://docs.ckan.org/en/latest/extensions/best-practices.html
# add-third-party-libraries-to-requirements-txt
"clamd",
],

# If there are data files included in your packages that need to be
Expand Down

0 comments on commit 2bc6c7b

Please sign in to comment.