From 78afb38191695a18832afaec6374d04eb6ff4bb1 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Aug 10 2016 12:39:51 +0000 Subject: Implement scanning of attached files for viruses Signed-off-by: Patrick Uiterwijk --- diff --git a/doc/configuration.rst b/doc/configuration.rst index eec82c7..42a36c0 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -171,6 +171,15 @@ when building the ``msg-id`` header of the emails sent. Defaults to: ``pagure.org`` +VIRUS_SCAN_ATTACHMENTS +~~~~~~~~~~~~~~~~~~~~~~ + +This setting configures whether attachments are scanned for viruses on +upload. For more information, see the install.rst guide. + +Defaults to: ``False`` + + Configure Gitolite ------------------ diff --git a/doc/install.rst b/doc/install.rst index 7490087..74ccb81 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -262,3 +262,20 @@ The ``alembic stamp`` command is the one actually saving the current revision into the database. This current revision is found using ``alembic heads`` which returns the most recent revision found by alembic, and since the database was just created, it is at the latest revision. + + +Set up virus scannining +----------------------- +Pagure can automatically scan uploaded attachments for viruses using Clam. +To set this up, first install clamav-data-empty, clamav-server, +clamav-server-systemd and clamav-update. + +Then edit /etc/freshclam.conf, removing the Example line and run freshclam once +to get an up to date database. + +Copy /usr/share/doc/clamav-server/clamd.conf to /etc/clamd.conf and edit that +too, again making sure to remove the Example line. Make sure to set LocalSocket +to a file in a directory that exists, and set User to an existing system user. + +Then start the clamd service and set VIRUS_SCAN_ATTACHMENTS = True in the +Pagure configuration. diff --git a/files/pagure.cfg.sample b/files/pagure.cfg.sample index a160c98..bd6d8f0 100644 --- a/files/pagure.cfg.sample +++ b/files/pagure.cfg.sample @@ -92,6 +92,9 @@ REMOTE_GIT_FOLDER = os.path.join( 'remotes' ) +### Whether to enable scanning for viruses in attachments +VIRUS_SCAN_ATTACHMENTS = False + ### Configuration file for gitolite GITOLITE_CONFIG = os.path.join( diff --git a/pagure/default_config.py b/pagure/default_config.py index 6fe64e6..d9a497c 100644 --- a/pagure/default_config.py +++ b/pagure/default_config.py @@ -123,6 +123,8 @@ REMOTE_GIT_FOLDER = os.path.join( 'remotes' ) +### Whether to enable scanning for viruses in attachments +VIRUS_SCAN_ATTACHMENTS = False # Configuration file for gitolite GITOLITE_CONFIG = os.path.join( diff --git a/pagure/forms.py b/pagure/forms.py index a9d1e0a..d6d0e24 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -10,9 +10,13 @@ import re from flask.ext import wtf +import flask import wtforms +import tempfile # pylint: disable=R0903,W0232,E1002 +import pagure + STRICT_REGEX = '^[a-zA-Z0-9-_]+$' TAGS_REGEX = '^[a-zA-Z0-9-_, .]+$' @@ -20,6 +24,32 @@ PROJECT_NAME_REGEX = \ '^[a-zA-z0-9_][a-zA-Z0-9-_]*(/?[a-zA-z0-9_][a-zA-Z0-9-_]+)?$' +def file_virus_validator(form, field): + if not pagure.APP.config['VIRUS_SCAN_ATTACHMENTS']: + return + from pyclamd import ClamdUnixSocket + + if not field.name in flask.request.files or \ + flask.request.files[field.name].filename == '': + # If no file was uploaded, this field is correct + return + uploaded = flask.request.files[field.name] + clam = ClamdUnixSocket() + if not clam.ping(): + raise wtforms.ValidationError('Unable to communicate with virus scanner') + results = clam.scan_stream(uploaded.stream.read()) + if results is None: + uploaded.stream.seek(0) + return + else: + result = results.values() + res_type, res_msg = result + if res_type == 'FOUND': + raise wtforms.ValidationError('Virus found: %s' % res_msg) + else: + raise wtforms.ValidationError('Error scanning uploaded file') + + class ProjectFormSimplified(wtf.Form): ''' Form to edit the description of a project. ''' description = wtforms.TextField( @@ -290,7 +320,7 @@ class UploadFileForm(wtf.Form): ''' Form to upload a file. ''' filestream = wtforms.FileField( 'File', - [wtforms.validators.Required()]) + [wtforms.validators.Required(), file_virus_validator]) class UserEmailForm(wtf.Form): @@ -326,7 +356,7 @@ class CommentForm(wtf.Form): ''' Form to upload a file. ''' comment = wtforms.FileField( 'Comment', - [wtforms.validators.Required()]) + [wtforms.validators.Required(), file_virus_validator]) class NewGroupForm(wtf.Form): diff --git a/requirements.txt b/requirements.txt index 7f97847..9e6ad4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ markdown munch Pillow psutil +pyclamd # This is only needed if VIRUS_SCAN_ATTACHMENTS is enabled pygit2 >= 0.20.1 pygments python-openid diff --git a/tests/test_pagure_flask_ui_issues.py b/tests/test_pagure_flask_ui_issues.py index e9d5366..b7c4b00 100644 --- a/tests/test_pagure_flask_ui_issues.py +++ b/tests/test_pagure_flask_ui_issues.py @@ -16,6 +16,8 @@ import unittest import shutil import sys import os +import pyclamd +import tempfile import pygit2 from mock import patch @@ -35,6 +37,7 @@ class PagureFlaskIssuestests(tests.Modeltests): super(PagureFlaskIssuestests, self).setUp() pagure.APP.config['TESTING'] = True + pagure.APP.config['VIRUS_SCAN_ATTACHMENTS'] = True pagure.SESSION = self.session pagure.ui.SESSION = self.session pagure.ui.app.SESSION = self.session @@ -1084,6 +1087,26 @@ class PagureFlaskIssuestests(tests.Modeltests): exp = {'output': 'notok'} self.assertDictEqual(json_data, exp) + # Try to attach a virus + with tempfile.NamedTemporaryFile() as eicarfile: + eicarfile.write(pyclamd.ClamdUnixSocket().EICAR()) + eicarfile.flush() + stream = open(eicarfile.name, 'rb') + data = { + 'csrf_token': csrf_token, + 'filestream': stream, + 'enctype': 'multipart/form-data', + } + output = self.app.post( + '/test/issue/1/upload', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + stream.close() + json_data = json.loads(output.data) + exp = { + 'output': 'notok', + } + self.assertDictEqual(json_data, exp) + # Attach a file to a ticket stream = open(os.path.join(tests.HERE, 'placebo.png'), 'rb') data = {