From fec655768070782517eb117dce820d42cee006ff Mon Sep 17 00:00:00 2001
From: Steven Ly <6807939+orcsly@users.noreply.github.com>
Date: Fri, 21 Aug 2020 15:26:36 -0700
Subject: [PATCH] DIV-986: Add antivirus poc
---
.env.example | 6 ++-
.gitignore | 3 ++
docker-compose.yml | 12 +++++
edivorce/apps/core/templates/poc/upload.html | 47 ++++++++++++++++++++
edivorce/apps/core/urls.py | 10 ++++-
edivorce/apps/core/validators.py | 44 ++++++++++++++++++
edivorce/apps/core/views/poc.py | 23 ++++++++++
edivorce/settings/base.py | 10 +++++
requirements.txt | 39 ++++++++++------
9 files changed, 179 insertions(+), 15 deletions(-)
create mode 100644 docker-compose.yml
create mode 100644 edivorce/apps/core/templates/poc/upload.html
create mode 100644 edivorce/apps/core/validators.py
create mode 100644 edivorce/apps/core/views/poc.py
diff --git a/.env.example b/.env.example
index 05d0a483..c3708431 100644
--- a/.env.example
+++ b/.env.example
@@ -1,9 +1,13 @@
DEBUG=True
TEMPLATE_DEBUG=True
-DJANGO_SECRET_KEY=
DATABASE_ENGINE=django.db.backends.sqlite3
DATABASE_NAME=db.sqlite3
DATABASE_USER=
DATABASE_PASSWORD=
DATABASE_HOST=
DATABASE_PORT=
+
+# ClamAV settings
+CLAMAV_ENABLED=True
+CLAMAV_TCP_PORT=3310
+CLAMAV_TCP_ADDR=localhost
diff --git a/.gitignore b/.gitignore
index f1a96cf4..38c3a5e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,7 @@ var/
*.egg-info/
.installed.cfg
*.egg
+.python-version
# PyInstaller
# Usually these files are written by a python script from a template
@@ -411,3 +412,5 @@ ASALocalRun/
# MFractors (Xamarin productivity tool) working folder
.mfractor/
+
+edivorce/share
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..7344e9f9
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,12 @@
+version: "3.7"
+services:
+
+ # Antivirus
+ antivirus:
+ container_name: edivorce-antivirus
+ hostname: antivirus
+ build: https://github.com/bcgov/clamav.git
+ ports:
+ - "3310:3310"
+ restart: always
+
diff --git a/edivorce/apps/core/templates/poc/upload.html b/edivorce/apps/core/templates/poc/upload.html
new file mode 100644
index 00000000..84a12da3
--- /dev/null
+++ b/edivorce/apps/core/templates/poc/upload.html
@@ -0,0 +1,47 @@
+{% extends 'base.html' %}
+{% load input_field %}
+{% load step_order %}
+{% load load_json %}
+
+{% block title %}{{ block.super }}: POC{% endblock %}
+
+{% block progress %}{% include "partials/progress.html" %}{% endblock %}
+
+{% block content %}
+
Proof of Concept:File scanning
+
+
+
+{% endblock %}
+
+{% block formbuttons %}
+
+{% endblock %}
+
+{% block sidebarNav %}
+
+{% endblock %}
+
+{% block sidebar %}
+
+{% endblock %}
diff --git a/edivorce/apps/core/urls.py b/edivorce/apps/core/urls.py
index 40d486c6..8eb2850b 100644
--- a/edivorce/apps/core/urls.py
+++ b/edivorce/apps/core/urls.py
@@ -1,6 +1,9 @@
from django.conf.urls import url
+from django.conf import settings
+
+from .views import main, system, pdf, api, localdev, poc
+from .decorators import bceid_required
-from .views import main, system, pdf, api, localdev
urlpatterns = [
# url(r'^guide$', styleguide.guide),
@@ -32,3 +35,8 @@ urlpatterns = [
url(r'^current$', system.current, name="current"),
url(r'^$', main.home, name="home"),
]
+
+if settings.DEBUG:
+ urlpatterns = urlpatterns + [
+ url(r'poc/upload', bceid_required(poc.UploadScan.as_view()), name="poc-upload"),
+ ]
\ No newline at end of file
diff --git a/edivorce/apps/core/validators.py b/edivorce/apps/core/validators.py
new file mode 100644
index 00000000..adf83148
--- /dev/null
+++ b/edivorce/apps/core/validators.py
@@ -0,0 +1,44 @@
+import logging
+import clamd
+import sys
+
+from django.core.exceptions import ValidationError
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+
+def file_scan_validation(file):
+ """
+ This validator sends the file to ClamAV for scanning and returns returns to the form. By default, if antivirus
+ service is not available or there are errors, the validation will fail.
+
+ Usage:
+ class UploadForm(forms.Form):
+ file = forms.FileField(validators=[file_scan_validation])
+ :param file:
+ :return:
+ """
+ logger.debug("starting file scanning with clamav")
+ if not settings.CLAMAV_ENABLED:
+ logger.warning('File scanning has been disabled.')
+ return
+
+ # make sure we're at the beginning of the file stream
+ file.seek(0)
+
+ # we're just going to assume a network connection to clamav here .. no local unix socket support
+ scanner = clamd.ClamdNetworkSocket(settings.CLAMAV_TCP_ADDR, settings.CLAMAV_TCP_PORT)
+ try:
+ result = scanner.instream(file)
+ except:
+ # it doesn't really matter what the actual error is .. log it and raise validation error
+ logger.error('Error occurred while trying to scan file. "{}"'.format(sys.exc_info()[0]))
+ raise ValidationError('Unable to scan file.', code='scanerror')
+ finally:
+ # reset file stream
+ file.seek(0)
+
+ if result and result['stream'][0] == 'FOUND':
+ logger.warning('Virus found: {}'.format(file.name))
+ raise ValidationError('Infected file found.', code='infected')
diff --git a/edivorce/apps/core/views/poc.py b/edivorce/apps/core/views/poc.py
new file mode 100644
index 00000000..ef04a514
--- /dev/null
+++ b/edivorce/apps/core/views/poc.py
@@ -0,0 +1,23 @@
+from django.shortcuts import render
+from django.views.generic.edit import FormView
+from django import forms
+
+from ..validators import file_scan_validation
+
+"""
+Everything in this file is considered as proof of concept work and should not be used for production code.
+"""
+
+
+class UploadForm(forms.Form):
+ upload_file = forms.FileField(validators=[file_scan_validation])
+
+
+class UploadScan(FormView):
+ form_class = UploadForm
+ template_name = "poc/upload.html"
+
+ def form_valid(self, form):
+ context = self.get_context_data()
+ context['validation_success'] = True
+ return render(self.request, self.template_name, context)
diff --git a/edivorce/settings/base.py b/edivorce/settings/base.py
index 516a0deb..e5e106dc 100644
--- a/edivorce/settings/base.py
+++ b/edivorce/settings/base.py
@@ -11,8 +11,12 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
"""
import os
+from environs import Env
from unipath import Path
+env = Env()
+env.read_env() # read .env file, if it exists
+
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
PROJECT_ROOT = Path(__file__).parent.parent.parent
BASE_DIR = Path(__file__).parent.parent
@@ -145,3 +149,9 @@ DEBUG_TOOLBAR_CONFIG = {
SECURE_BROWSER_XSS_FILTER = True
LOGOUT_URL = '/accounts/logout/'
+
+
+# CLAMAV settings
+CLAMAV_ENABLED = env.bool('CLAMAV_ENABLED', True)
+CLAMAV_TCP_PORT = env.int('CLAMAV_TCP_PORT', 3310)
+CLAMAV_TCP_ADDR = env('CLAMAV_TCP_ADDR', 'localhost')
diff --git a/requirements.txt b/requirements.txt
index e6239d63..d150b7ab 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,26 @@
-Django>=2.2,<3.0
-django-compressor>=2.4,<3.0
-django-crispy-forms>=1.9,<2.0
-django-debug-toolbar>=2.2,<3.0
-django-sass-processor>=0.8,<1.0
-djangorestframework>=3.11,<4.0
-gunicorn>=20.0,<21.0
-libsass>=0.20.0<1.0
-psycopg2>=2.8,<3.0
-requests>=2.24,<3.0
-six>=1.15,<2.0
-Unipath>=1.1,<2.0
-whitenoise>=3.3.1,<4.0
+certifi==2020.6.20
+chardet==3.0.4
+clamd==1.0.2
+Django==2.2.15
+django-appconf==1.0.4
+django-compressor==2.4
+django-crispy-forms==1.9.2
+django-debug-toolbar==2.2
+django-sass-processor==0.8
+djangorestframework==3.11.1
+environs==8.0.0
+gunicorn==20.0.4
+idna==2.10
+libsass==0.20.0
+marshmallow==3.7.1
+psycopg2==2.8.5
+python-dotenv==0.14.0
+pytz==2020.1
+rcssmin==1.0.6
+requests==2.24.0
+rjsmin==1.1.0
+six==1.15.0
+sqlparse==0.3.1
+Unipath==1.1
+urllib3==1.25.10
+whitenoise==3.3.1