1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
|
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app for security configuration."""
import re
import subprocess
from collections import defaultdict
from django.utils.translation import gettext_lazy as _
from plinth import app as app_module
from plinth import menu
from plinth.config import DropinConfigs
from plinth.daemon import Daemon, RelatedDaemon
from plinth.modules.backups.components import BackupRestore
from plinth.package import Packages
from plinth.privileged import service as service_privileged
from . import manifest, privileged
class SecurityApp(app_module.App):
"""FreedomBox app for security."""
app_id = 'security'
_version = 9
can_be_disabled = False
def __init__(self) -> None:
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, name=_('Security'),
icon='fa-lock', manual_page='Security',
tags=manifest.tags)
self.add(info)
menu_item = menu.Menu('menu-security', info.name, info.icon, info.tags,
'security:index',
parent_url_name='system:security', order=10)
self.add(menu_item)
packages = Packages('packages-security', ['fail2ban', 'debsecan'])
self.add(packages)
dropin_configs = DropinConfigs('dropin-configs-security', [
'/etc/fail2ban/fail2ban.d/freedombox.conf',
'/etc/fail2ban/jail.d/freedombox.conf',
])
self.add(dropin_configs)
daemon = RelatedDaemon('related-daemon-fail2ban', 'fail2ban')
self.add(daemon)
backup_restore = BackupRestore('backup-restore-security',
**manifest.backup)
self.add(backup_restore)
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
if not old_version:
enable_fail2ban()
service_privileged.reload('fail2ban')
# Drop the legacy restriction access configuration
privileged.disable_restricted_access()
def enable_fail2ban():
"""Unmask, enable and run the fail2ban service."""
service_privileged.unmask('fail2ban')
service_privileged.enable('fail2ban')
def get_apps_report():
"""Return a security report 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)
service_exposure_lines = subprocess.check_output(
['systemd-analyze', 'security']).decode().strip().split('\n')
service_exposure_lines.pop(0)
sandbox_coverage = {}
for line in service_exposure_lines:
fields = line.split()
name = re.sub(r'\.service$', '', fields[0])
score = round(100 - float(fields[1]) * 10)
sandbox_coverage[name] = score
apps = {
'freedombox': {
'name': 'freedombox',
'packages': {'freedombox'},
'vulns': 0,
}
}
for app_ in app_module.App.list():
components = app_.get_components_of_type(Packages)
packages = []
for component in components:
packages += component.possible_packages
if not packages:
continue # app has no managed packages
components = app_.get_components_of_type(Daemon)
services = []
for component in components:
services.append(component.unit)
# filter out apps not setup yet
if app_.needs_setup():
continue
apps[app_.app_id] = {
'name': app_.app_id,
'packages': set(packages),
'vulns': 0,
'sandboxed': None,
}
if services:
apps[app_.app_id]['sandboxed'] = False
for service in services:
# If an app lists a timer, work on the associated service
# instead
if service.rpartition('.')[-1] == 'timer':
service = service.rpartition('.')[0]
if _get_service_is_sandboxed(service):
apps[app_.app_id]['sandboxed'] = True
apps[app_.app_id][
'sandbox_coverage'] = sandbox_coverage.get(service)
for cve_packages in cves.values():
for app_ in apps.values():
if cve_packages & app_['packages']:
app_['vulns'] += 1
return apps
def _get_service_is_sandboxed(service):
"""Return whether service is sandboxed."""
lines = subprocess.check_output([
'systemctl',
'show',
service,
'--property=ProtectSystem',
'--property=ProtectHome',
'--property=PrivateTmp',
'--property=PrivateDevices',
'--property=PrivateNetwork',
'--property=PrivateUsers',
'--property=PrivateMounts',
]).decode().strip().split('\n')
pairs = [line.partition('=')[::2] for line in lines]
properties = dict(pairs)
if properties.get('ProtectSystem') in ['yes', 'full', 'strict']:
return True
if properties.get('ProtectHome') in ['yes', 'read-only', 'tmpfs']:
return True
for name in [
'PrivateTmp', 'PrivateDevices', 'PrivateNetwork', 'PrivateUsers',
'PrivateMounts'
]:
if properties.get(name) == 'yes':
return True
return False
|