diff --git a/src/strass/cspmailreports/__init__.py b/src/strass/cspmailreports/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/strass/cspmailreports/apps.py b/src/strass/cspmailreports/apps.py deleted file mode 100644 index 18813633f313f54b8193e9e186300a29f684a810..0000000000000000000000000000000000000000 --- a/src/strass/cspmailreports/apps.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.apps import AppConfig -import django.core.checks - -from cspmailreports.conf import app_settings - - -class CspmailreportsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'cspmailreports' - - -@django.core.checks.register() -def check_settings(app_configs, **kwargs): - errors = [] - if app_settings.dos_cooldown <= 0: - errors.append( - django.core.checks.Error( - "CSP_MAIL_REPORTS_COOLDOWN_IN_SECONDS must be greater than 0", - id="cspmailreports.E001", - ) - ) - if app_settings.max_report_before_cooldown <= 0: - errors.append( - django.core.checks.Error( - "CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN must be greater than 0", - id="cspmailreports.E002", - ) - ) - return errors diff --git a/src/strass/cspmailreports/conf.py b/src/strass/cspmailreports/conf.py deleted file mode 100644 index c2b1186dc58f9e17512d506c609ba9985cc8d2dd..0000000000000000000000000000000000000000 --- a/src/strass/cspmailreports/conf.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.conf import settings - - -class Settings: - def __init__(self): - self.__print_in_log = None - self.__max_report_before_cooldown = None - self.__dos_cooldown = None - - def _reset_cache(self): - self.__init__() - - @property - def print_in_log(self) -> bool: - if self.__print_in_log is None: - self.__print_in_log = getattr(settings, "CSP_MAIL_REPORTS_LOG", True) - return self.__print_in_log - - @property - def dos_cooldown(self) -> int: - if self.__dos_cooldown is None: - self.__dos_cooldown = getattr(settings, "CSP_MAIL_REPORTS_COOLDOWN_IN_SECONDS", 30 * 60) - return self.__dos_cooldown - - @property - def max_report_before_cooldown(self) -> bool: - if self.__max_report_before_cooldown is None: - self.__max_report_before_cooldown = getattr(settings, "CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN", 50) - return self.__max_report_before_cooldown - - -app_settings = Settings() diff --git a/src/strass/cspmailreports/tests.py b/src/strass/cspmailreports/tests.py deleted file mode 100644 index f178678b9193f30bc4799f8eec02b871ec7a9a56..0000000000000000000000000000000000000000 --- a/src/strass/cspmailreports/tests.py +++ /dev/null @@ -1,141 +0,0 @@ -import json -import logging -from typing import Dict - -from django.core import mail -from django.test import TestCase, override_settings -from django.urls import reverse -from django.urls import reverse_lazy - -import cspmailreports.apps -import cspmailreports.conf - -logger = logging.getLogger(__name__) - - -class CSPTooledTestCase(TestCase): - url = reverse_lazy('cspmailreports:csp-report') - - def setUp(self): - super().setUp() - cspmailreports.conf.app_settings._reset_cache() - - @staticmethod - def fake_report(referrer="http://127.0.0.1:8080") -> Dict: - return { - "csp-report": { - "blocked-uri": "inline", - "disposition": "enforce", - "document-uri": f"{referrer}/about/", - "effective-directive": "script-src-elem", - "line-number": 215, - "original-policy": "default-src 'self' *; script-src 'self' cdn.datatables.net", - "referrer": referrer, - "script-sample": "", - "source-file": f"{referrer}/about/", - "status-code": 200, - "violated-directive": "script-src-elem", - } - } - - def report(self, report=None): - if report is None: - report = self.fake_report() - return self.client.post(self.url, data=json.dumps(report), content_type='application/csp-report') - - -class TestMain(CSPTooledTestCase): - def test_works(self): - url = reverse('cspmailreports:csp-report') - # check get ko - self.assertNotIn(self.client.get(url).status_code, [200]) - # check post works - self.assertIn(self.report().status_code, [200]) - - def test_invalid_mime_type_refused(self): - self.assertNotIn(self.client.post(self.url, data=self.fake_report()).status_code, [200]) - - def test_invalid_data_accepted(self): - self.assertIn( - self.client.generic( - "POST", - self.url, - 'zerzerz!sdf{', - 'application/csp-report', - ).status_code, - [200], - ) - - -@override_settings( - CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN=10, - ADMIN=(('ada', 'ada@pasteur.fr'),), - DEBUG=False, -) -class TestDOS(CSPTooledTestCase): - def test_it(self): - mail_count = len(mail.outbox) - self.client.defaults['REMOTE_ADDR'] = '1.2.3.4' - # trigger dos - for i in range(cspmailreports.conf.app_settings.max_report_before_cooldown): - self.assertIn(self.report().status_code, [200]) - mail_count += 1 - self.assertEqual(mail_count, len(mail.outbox)) - # check blocked - self.assertIn(self.report().status_code, [429]) - self.assertEqual(mail_count, len(mail.outbox)) - # check other is not blocked - self.client.defaults['REMOTE_ADDR'] = '1.2.3.5' - self.assertIn(self.report().status_code, [200]) - mail_count += 1 - self.assertEqual(mail_count, len(mail.outbox)) - - -@override_settings( - ADMINS=(('ada', 'ada@pasteur.fr'),), -) -class TestMailAdmin(CSPTooledTestCase): - def test_it(self): - mail_count = len(mail.outbox) - self.assertIn(self.report().status_code, [200]) - mail_count += 1 - self.assertEqual(mail_count, len(mail.outbox)) - - -@override_settings( - ADMINS=(), -) -class TestMailNoAdmin(CSPTooledTestCase): - def test_it(self): - mail_count = len(mail.outbox) - self.assertIn(self.report().status_code, [200]) - mail_count += 0 # in debug not mail to admin is sent - self.assertEqual(mail_count, len(mail.outbox)) - - -@override_settings( - CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN=-1, -) -class TestCheck1(CSPTooledTestCase): - def test_it(self): - cspmailreports.conf.app_settings._reset_cache() - self.assertEqual(len(cspmailreports.apps.check_settings(None)), 1) - - -@override_settings( - CSP_MAIL_REPORTS_COOLDOWN_IN_SECONDS=-1, -) -class TestCheck2(CSPTooledTestCase): - def test_it(self): - cspmailreports.conf.app_settings._reset_cache() - self.assertEqual(len(cspmailreports.apps.check_settings(None)), 1) - - -@override_settings( - CSP_MAIL_REPORTS_MAX_BEFORE_COOLDOWN=-1, - CSP_MAIL_REPORTS_COOLDOWN_IN_SECONDS=-1, -) -class TestCheckAll(CSPTooledTestCase): - def test_it(self): - cspmailreports.conf.app_settings._reset_cache() - self.assertEqual(len(cspmailreports.apps.check_settings(None)), 2) diff --git a/src/strass/cspmailreports/urls.py b/src/strass/cspmailreports/urls.py deleted file mode 100644 index 5c0ab9d08a19b2cae922b3b8071907246bf5a456..0000000000000000000000000000000000000000 --- a/src/strass/cspmailreports/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import re_path - -import cspmailreports.views - -app_name = 'cspmailreports' -urlpatterns = [ - re_path(r'^report/$', cspmailreports.views.report_csp, name='csp-report'), -] diff --git a/src/strass/cspmailreports/utils.py b/src/strass/cspmailreports/utils.py deleted file mode 100644 index 59043a4284731284084c86d67e91528a8a062945..0000000000000000000000000000000000000000 --- a/src/strass/cspmailreports/utils.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -import logging - -from django.core.cache import cache -from cspmailreports.conf import app_settings - -logger = logging.getLogger(__name__) - - -def is_flagged_as_dos(request) -> bool: - ip_address = request.META.get("REMOTE_ADDR") - cache_key = f"csp_report_attempts:{ip_address}" - csp_report_attempts = cache.get(cache_key, 0) + 1 - cache.set(cache_key, csp_report_attempts, app_settings.dos_cooldown) - return csp_report_attempts > app_settings.max_report_before_cooldown - - -def create_report(request) -> str: - agent = request.META.get('HTTP_USER_AGENT', '') - report = request.body - if isinstance(report, bytes): - report = report.decode('utf-8') - try: - report = json.dumps( - json.loads(report), - indent=4, - sort_keys=True, - ) - except ValueError as e: - print(e) - ip = request.META.get("REMOTE_ADDR") - logger.warning(f'Invalid CSP report violation from "{ip}": "{report[:20]}..."') - return f"HTTP user agent :\n{agent}\n\nReport:\n{report}" diff --git a/src/strass/cspmailreports/views.py b/src/strass/cspmailreports/views.py deleted file mode 100644 index 4acfa6c605f43487cfeca5d30793bcd6c1ab42f6..0000000000000000000000000000000000000000 --- a/src/strass/cspmailreports/views.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging - -from django.core.mail import mail_admins -from django.http import HttpResponse -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_POST - -import cspmailreports.conf -import cspmailreports.utils - -logger = logging.getLogger(__name__) - - -@require_POST -@csrf_exempt -def report_csp(request): - if request.content_type != 'application/csp-report': - return HttpResponse(status=415) - if cspmailreports.utils.is_flagged_as_dos(request): - return HttpResponse(status=429, headers=[("Retry-After", cspmailreports.conf.app_settings.dos_cooldown)]) - report = cspmailreports.utils.create_report(request) - if cspmailreports.conf.app_settings.print_in_log: - logger.warning(f'CSP violation report:\n{report}') - mail_admins("CSP violation report", report) - return HttpResponse() diff --git a/src/strass/requirements.txt b/src/strass/requirements.txt index 5e13700e94d86a255072b6c8b9d319e325a9d818..b8ab71654ba5c54aed45c171732f60e984f8b1c8 100644 --- a/src/strass/requirements.txt +++ b/src/strass/requirements.txt @@ -7,6 +7,8 @@ python-decouple django-basetheme-bootstrap>=1.8.4 --extra-index-url https://gitlab.pasteur.fr/api/v4/projects/hub%2Fdjango-kubernetes-probes/packages/pypi/simple django-kubernetes-probes>=1.1 +--extra-index-url https://gitlab.pasteur.fr/api/v4/projects/hub%2Fdjango-csp-mail-reports/packages/pypi/simple +django-csp-mail-reports>=1.0 django-formtools csscompressor coverage