diff --git a/plinth/modules/security/__init__.py b/plinth/modules/security/__init__.py index 5f8f763cb..433d7506d 100644 --- a/plinth/modules/security/__init__.py +++ b/plinth/modules/security/__init__.py @@ -18,13 +18,16 @@ FreedomBox app for security configuration. """ +import subprocess +from collections import defaultdict + from django.utils.translation import ugettext_lazy as _ from plinth import actions from plinth import app as app_module -from plinth import menu +from plinth import menu, module_loader -from .manifest import backup # noqa, pylint: disable=unused-import +from .manifest import backup # noqa, pylint: disable=unused-import version = 6 @@ -32,7 +35,7 @@ is_essential = True name = _('Security') -managed_packages = ['fail2ban'] +managed_packages = ['fail2ban', 'debsecan'] managed_services = ['fail2ban'] @@ -107,3 +110,43 @@ def set_restricted_access(enabled): action = 'enable-restricted-access' actions.superuser_run('security', [action]) + + +def get_vulnerability_counts(): + """Return number of security vulnerabilities for each app""" + lines = subprocess.check_output(['debsecan']).decode().split('\n') + cves = defaultdict(set) + for line in lines: + if line: + (label, package, *_) = line.split() + cves[label].add(package) + + apps = { + 'freedombox': { + 'name': 'freedombox', + 'packages': {'freedombox'}, + 'count': 0, + } + } + for module_name, module in module_loader.loaded_modules.items(): + try: + packages = module.managed_packages + except AttributeError: + continue # app has no managed packages + + # filter out apps not setup yet + if module.setup_helper.get_state() == 'needs-setup': + continue + + apps[module_name] = { + 'name': module_name, + 'packages': set(packages), + 'count': 0, + } + + for cve_packages in cves.values(): + for app_ in apps.values(): + if cve_packages & app_['packages']: + app['count'] += 1 + + return apps diff --git a/plinth/modules/security/templates/security.html b/plinth/modules/security/templates/security.html index 3e4a999c0..7d4ddf20c 100644 --- a/plinth/modules/security/templates/security.html +++ b/plinth/modules/security/templates/security.html @@ -1,4 +1,4 @@ -{% extends "simple_app.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of FreedomBox. @@ -21,15 +21,34 @@ {% load bootstrap %} {% load i18n %} -{% block configuration %} - -
- {% csrf_token %} - - {{ form|bootstrap }} - - -
- +{% block status %} +

{% trans "Status" %}

+

+ {% blocktrans trimmed with count=freedombox_vulns.count %} + The installed version of FreedomBox has {{ count }} reported security + vulnerabilities. + {% endblocktrans %} +

+

+ {% blocktrans trimmed %} + The following table lists the reported number of security vulnerabilities + for each installed app. + {% endblocktrans %} +

+ + + + + + + + + {% for app in apps_vulns %} + + + + + {% endfor %} + +
{% trans "App Name" %}{% trans "Vulnerabilities Reported" %}
{{ app.name }}{{ app.count }}
{% endblock %} diff --git a/plinth/modules/security/views.py b/plinth/modules/security/views.py index 6aca171e2..77e9490aa 100644 --- a/plinth/modules/security/views.py +++ b/plinth/modules/security/views.py @@ -43,11 +43,21 @@ def index(request): else: form = SecurityForm(initial=status, prefix='security') - return TemplateResponse(request, 'security.html', { - 'title': _('Security'), - 'manual_page': security.manual_page, - 'form': form - }) + vulnerability_counts = security.get_vulnerability_counts() + return TemplateResponse( + request, 'security.html', { + 'name': + _('Security'), + 'manual_page': + security.manual_page, + 'form': + form, + 'freedombox_vulns': + vulnerability_counts.pop('freedombox'), + 'apps_vulns': + sorted(vulnerability_counts.values(), + key=lambda app: app['name']), + }) def get_status(request): @@ -64,9 +74,10 @@ def _apply_changes(request, old_status, new_status): try: security.set_restricted_access(new_status['restricted_access']) except Exception as exception: - messages.error(request, - _('Error setting restricted access: {exception}') - .format(exception=exception)) + messages.error( + request, + _('Error setting restricted access: {exception}').format( + exception=exception)) else: messages.success(request, _('Updated security configuration'))