diff --git a/backend/backend/settings/dev.py b/backend/backend/settings/dev.py index 09b969fa1..78fa67a90 100644 --- a/backend/backend/settings/dev.py +++ b/backend/backend/settings/dev.py @@ -12,7 +12,7 @@ 'PORT': os.getenv('DB_PORT', '5432'), 'OPTIONS': { 'connect_timeout': 3, - }, + } } } COMMON_NAME_PREFIX = 'd.wott-dev.local' diff --git a/backend/device_registry/api_views.py b/backend/device_registry/api_views.py index 334728c3e..5288ba40b 100644 --- a/backend/device_registry/api_views.py +++ b/backend/device_registry/api_views.py @@ -58,15 +58,22 @@ def get(self, request, *args, **kwargs): firewallstate_object, _ = FirewallState.objects.get_or_create(device=device) block_networks = portscan_object.block_networks.copy() block_networks.extend(settings.SPAM_NETWORKS) - return Response({'policy': firewallstate_object.policy_string, - firewallstate_object.ports_field_name: portscan_object.block_ports, - 'block_networks': block_networks}) + return Response({ + 'policy': firewallstate_object.policy_string, + firewallstate_object.ports_field_name: portscan_object.block_ports, + 'block_networks': block_networks, + 'deb_packages_hash': device.deb_packages_hash + }) def post(self, request, *args, **kwargs): data = request.data device = Device.objects.get(device_id=request.device_id) device.last_ping = timezone.now() device.agent_version = data.get('agent_version') + if 'deb_packages' in data: + deb_packages = data['deb_packages'] + device.deb_packages_hash = deb_packages['hash'] + device.set_deb_packages(deb_packages['packages']) device_info_object, _ = DeviceInfo.objects.get_or_create(device=device) device_info_object.device__last_ping = timezone.now() @@ -102,7 +109,7 @@ def post(self, request, *args, **kwargs): firewall_state.rules = firewall_rules firewall_state.save() - device.save(update_fields=['last_ping', 'agent_version', 'trust_score']) + device.save(update_fields=['last_ping', 'agent_version', 'deb_packages_hash', 'trust_score']) if datastore_client: task_key = datastore_client.key('Ping') diff --git a/backend/device_registry/migrations/0054_device_calculated_trust_score.py b/backend/device_registry/migrations/0054_device_calculated_trust_score.py index add0931b7..569900de3 100644 --- a/backend/device_registry/migrations/0054_device_calculated_trust_score.py +++ b/backend/device_registry/migrations/0054_device_calculated_trust_score.py @@ -1,10 +1,10 @@ # Generated by Django 2.1.10 on 2019-08-06 05:10 from django.db import migrations, models -from device_registry.models import Device def save_trust_score(apps, schema_editor): + Device = apps.get_model('device_registry', 'Device') for d in Device.objects.all(): d.save(update_fields=['trust_score']) diff --git a/backend/device_registry/migrations/0056_auto_20190909_1128.py b/backend/device_registry/migrations/0056_auto_20190909_1128.py new file mode 100644 index 000000000..64542024e --- /dev/null +++ b/backend/device_registry/migrations/0056_auto_20190909_1128.py @@ -0,0 +1,37 @@ +# Generated by Django 2.1.10 on 2019-09-09 11:28 + +import device_registry.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('device_registry', '0055_remove_deviceinfo_trust_score'), + ] + + operations = [ + migrations.CreateModel( + name='DebPackage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('version', models.CharField(max_length=128)), + ('arch', models.CharField(choices=[(device_registry.models.DebPackage.Arch('i386'), 'i386'), (device_registry.models.DebPackage.Arch('amd64'), 'amd64'), (device_registry.models.DebPackage.Arch('armhf'), 'armhf'), (device_registry.models.DebPackage.Arch('all'), 'all')], max_length=16)), + ], + ), + migrations.AddField( + model_name='device', + name='deb_packages_hash', + field=models.CharField(blank=True, max_length=32), + ), + migrations.AlterUniqueTogether( + name='debpackage', + unique_together={('name', 'version', 'arch')}, + ), + migrations.AddField( + model_name='device', + name='deb_packages', + field=models.ManyToManyField(to='device_registry.DebPackage'), + ), + ] diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 918430de0..4b3213fb6 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -1,13 +1,13 @@ +from enum import Enum import datetime from statistics import mean import json import uuid from django.conf import settings -from django.db import models +from django.db import models, transaction from django.utils import timezone from django.contrib.postgres.fields import JSONField -from django.db import transaction import yaml import tagulous.models @@ -39,6 +39,26 @@ def from_db_value(self, value, expression, connection, context): return value +class DebPackage(models.Model): + class Distro(Enum): + DEBIAN = 'debian' + RASPBIAN = 'raspbian' + UBUNTU = 'ubuntu' + + class Arch(Enum): + i386 = 'i386' + AMD64 = 'amd64' + ARMHF = 'armhf' + ALL = 'all' + + name = models.CharField(max_length=128) + version = models.CharField(max_length=128) + arch = models.CharField(max_length=16, choices=[(tag, tag.value) for tag in Arch]) + + class Meta: + unique_together = ['name', 'version', 'arch'] + + class Device(models.Model): device_id = models.CharField( max_length=128, @@ -65,11 +85,53 @@ class Device(models.Model): agent_version = models.CharField(max_length=36, blank=True, null=True) tags = tagulous.models.TagField(to=Tag, blank=True) trust_score = models.FloatField(null=True) + deb_packages = models.ManyToManyField(DebPackage) + deb_packages_hash = models.CharField(max_length=32, blank=True) @property def certificate_expired(self): return self.certificate_expires < timezone.now() + INSECURE_SERVICES = [ + 'fingerd', + 'tftpd', + 'telnetd', + 'snmpd', + 'xinetd', + 'nis', + 'atftpd', + 'tftpd-hpa', + 'rsh-server', + 'rsh-redone-server' + ] + + @property + def insecure_services(self): + """ + Get a list of deb packages which are marked "insecure", i.e. their names are in INSECURE_SERVICES list. + :return: list of DebPackage or None if set_deb_packages() wasn't called before. + """ + if not self.deb_packages_hash: + return None + return self.deb_packages.filter(name__in=self.INSECURE_SERVICES) + + def set_deb_packages(self, packages): + """ + Assign the list of installed deb packages to this device. + :param packages: list of dicts with the following values: 'name': str, 'version': str, 'arch': DebPackage.Arch. + """ + # Save new packages to DB. + DebPackage.objects.bulk_create([DebPackage(name=package['name'], version=package['version'], + arch=package['arch']) for package in packages], + ignore_conflicts=True) + # Get packages qs. + q_objects = models.Q() + for package in packages: + q_objects.add(models.Q(name=package['name'], version=package['version'], arch=package['arch']), models.Q.OR) + + # Set deb_packages. + self.deb_packages.set(DebPackage.objects.filter(q_objects).only('pk')) + def get_name(self): if self.name: return self.name diff --git a/backend/device_registry/templates/device_info_security.html b/backend/device_registry/templates/device_info_security.html index f7605eb77..6f5b2f2b7 100644 --- a/backend/device_registry/templates/device_info_security.html +++ b/backend/device_registry/templates/device_info_security.html @@ -78,7 +78,21 @@
pi:')
self.assertContains(response, 'success: 1')
+ def test_insecure_services(self):
+ self.client.login(username='test', password='123')
+ url = reverse('device-detail-security', kwargs={'pk': self.device.pk})
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, '>telnetd<')
+ self.assertNotContains(response, '>fingerd<')
+ self.assertNotContains(response, 'No insecure services detected')
+
+ self.device.set_deb_packages([
+ {'name': 'python2', 'version': 'VERSION', 'arch': 'i386'},
+ {'name': 'python3', 'version': 'VERSION', 'arch': 'i386'}
+ ])
+ self.device.deb_packages_hash = 'abcdef'
+ self.device.save()
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, '>telnetd<')
+ self.assertNotContains(response, '>fingerd<')
+ self.assertContains(response, 'No insecure services detected')
+ self.assertListEqual(list(self.device.deb_packages.values('name', 'version', 'arch')), [
+ {'name': 'python2', 'version': 'VERSION', 'arch': 'i386'},
+ {'name': 'python3', 'version': 'VERSION', 'arch': 'i386'}
+ ])
+
+ self.device.set_deb_packages([
+ {'name': 'telnetd', 'version': 'VERSION', 'arch': 'i386'},
+ {'name': 'fingerd', 'version': 'VERSION', 'arch': 'i386'}
+ ])
+ self.device.save()
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '>telnetd<')
+ self.assertContains(response, '>fingerd<')
+ self.assertNotContains(response, 'No insecure services detected')
+ self.assertListEqual(list(self.device.deb_packages.values('name', 'version', 'arch')), [
+ {'name': 'telnetd', 'version': 'VERSION', 'arch': 'i386'},
+ {'name': 'fingerd', 'version': 'VERSION', 'arch': 'i386'}
+ ])
+
class PairingKeysView(TestCase):
def setUp(self):
@@ -572,7 +613,7 @@ def setUp(self):
owner=self.user,
certificate=TEST_CERT,
name='First',
- last_ping=timezone.now()-datetime.timedelta(days=1, hours=1)
+ last_ping=timezone.now() - datetime.timedelta(days=1, hours=1)
)
self.deviceinfo0 = DeviceInfo.objects.create(
device=self.device0,
diff --git a/backend/device_registry/tests/test_api.py b/backend/device_registry/tests/test_api.py
index a732a9d79..ac4c5f8e4 100644
--- a/backend/device_registry/tests/test_api.py
+++ b/backend/device_registry/tests/test_api.py
@@ -717,15 +717,22 @@ def setUp(self):
def test_ping_get_success(self):
response = self.client.get(self.url, **self.headers)
self.assertEqual(response.status_code, 200)
- self.assertDictEqual(response.data, {'policy': self.device.firewallstate.policy_string,
- 'block_ports': [], 'block_networks': settings.SPAM_NETWORKS})
+ self.assertDictEqual(response.data, {
+ 'policy': self.device.firewallstate.policy_string,
+ 'block_ports': [], 'block_networks': settings.SPAM_NETWORKS,
+ 'deb_packages_hash': ''
+ })
def test_pong_data(self):
# 1st request
response = self.client.get(self.url, **self.headers)
self.assertEqual(response.status_code, 200)
- self.assertDictEqual(response.data, {'block_ports': [], 'block_networks': settings.SPAM_NETWORKS,
- 'policy': self.device.firewallstate.policy_string})
+ self.assertDictEqual(response.data, {
+ 'block_ports': [],
+ 'block_networks': settings.SPAM_NETWORKS,
+ 'policy': self.device.firewallstate.policy_string,
+ 'deb_packages_hash': ''
+ })
# 2nd request
self.device.portscan.block_ports = [['192.168.1.178', 'tcp', 22, False]]
self.device.portscan.block_networks = [['192.168.1.177', False]]
@@ -735,9 +742,12 @@ def test_pong_data(self):
response = self.client.get(self.url, **self.headers)
self.assertEqual(response.status_code, 200)
- self.assertDictEqual(response.data, {'policy': self.device.firewallstate.policy_string,
- 'block_ports': [['192.168.1.178', 'tcp', 22, False]],
- 'block_networks': [['192.168.1.177', False]] + settings.SPAM_NETWORKS})
+ self.assertDictEqual(response.data, {
+ 'policy': self.device.firewallstate.policy_string,
+ 'block_ports': [['192.168.1.178', 'tcp', 22, False]],
+ 'block_networks': [['192.168.1.177', False]] + settings.SPAM_NETWORKS,
+ 'deb_packages_hash': ''
+ })
def test_ping_creates_models(self):
devinfo_obj_count_before = DeviceInfo.objects.count()
@@ -833,6 +843,26 @@ def test_ping_writes_trust_score(self):
self.device.refresh_from_db()
self.assertGreater(self.device.trust_score, 0.42)
+ def test_ping_writes_packages(self):
+ packages = [{'name': 'PACKAGE', 'version': 'VERSION', 'arch': 'all'}]
+ self.ping_payload['deb_packages'] = {
+ 'hash': 'abcdef',
+ 'packages': packages
+ }
+ self.client.post(self.url, self.ping_payload, **self.headers)
+
+ response = self.client.get(self.url, **self.headers)
+ self.assertEqual(response.status_code, 200)
+ self.assertDictEqual(response.data, {
+ 'block_ports': [],
+ 'block_networks': settings.SPAM_NETWORKS,
+ 'policy': self.device.firewallstate.policy_string,
+ 'deb_packages_hash': 'abcdef'
+ })
+ self.device.refresh_from_db()
+ self.assertQuerysetEqual(self.device.deb_packages.all(), packages,
+ transform=lambda p: {'name': p.name, 'version': p.version, 'arch': p.arch})
+
class DeviceEnrollView(APITestCase):
def setUp(self):