From 26e08b39b39babe7202967e5bd3ae1e4b8d4a9aa Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Sat, 31 Aug 2019 00:09:09 +0600 Subject: [PATCH 01/17] Store deb packages list for a device. --- backend/backend/settings/dev.py | 2 +- backend/device_registry/api_views.py | 15 +++++++++++---- .../migrations/0056_device_deb_packages.py | 19 +++++++++++++++++++ backend/device_registry/models.py | 1 + 4 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 backend/device_registry/migrations/0056_device_deb_packages.py 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..a9279b19a 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({ + 'firewall': { + 'policy': firewallstate_object.policy_string, + firewallstate_object.ports_field_name: portscan_object.block_ports, + 'block_networks': block_networks + }, + 'deb_packages_hash': device.deb_packages.get('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: + device.deb_packages = data['deb_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', 'trust_score']) if datastore_client: task_key = datastore_client.key('Ping') diff --git a/backend/device_registry/migrations/0056_device_deb_packages.py b/backend/device_registry/migrations/0056_device_deb_packages.py new file mode 100644 index 000000000..32a2b9a43 --- /dev/null +++ b/backend/device_registry/migrations/0056_device_deb_packages.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.10 on 2019-08-30 10:48 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('device_registry', '0055_remove_deviceinfo_trust_score'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='deb_packages', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + ] diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 918430de0..1353a9491 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -65,6 +65,7 @@ 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 = JSONField(blank=True, default=dict) @property def certificate_expired(self): From 1f375a619afcece1ed1c06b547cd904eb3c73817 Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Sat, 31 Aug 2019 11:15:12 +0600 Subject: [PATCH 02/17] Fixing tests. --- .../0054_device_calculated_trust_score.py | 2 +- backend/device_registry/tests/test_api.py | 30 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) 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/tests/test_api.py b/backend/device_registry/tests/test_api.py index a732a9d79..40d4e4283 100644 --- a/backend/device_registry/tests/test_api.py +++ b/backend/device_registry/tests/test_api.py @@ -717,15 +717,26 @@ 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, { + 'firewall': { + 'policy': self.device.firewallstate.policy_string, + 'block_ports': [], 'block_networks': settings.SPAM_NETWORKS + }, + 'deb_packages_hash': None + }) 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, { + 'firewall': { + 'block_ports': [], + 'block_networks': settings.SPAM_NETWORKS, + 'policy': self.device.firewallstate.policy_string + }, + 'deb_packages_hash': None + }) # 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 +746,14 @@ 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, { + 'firewall': { + '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': None + }) def test_ping_creates_models(self): devinfo_obj_count_before = DeviceInfo.objects.count() From 2b156df01503897a58eda915bc1959dfa474158e Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Mon, 2 Sep 2019 18:43:33 +0600 Subject: [PATCH 03/17] Warn about insecure services #391 --- backend/device_registry/models.py | 17 +++++++++++++++++ .../templates/device_info_security.html | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 1353a9491..5a95c3177 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -71,6 +71,23 @@ class Device(models.Model): 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): + packages = set([p['name'] for p in self.deb_packages['packages']]) + return set(self.INSECURE_SERVICES) & packages + 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..10f0f0cf0 100644 --- a/backend/device_registry/templates/device_info_security.html +++ b/backend/device_registry/templates/device_info_security.html @@ -78,7 +78,11 @@

Security

Insecure Services - Coming soon! + + {% for service_name in object.insecure_services %} +

{{ service_name }}

+ {% endfor %} + Logins From eba44cce514501b4709e0c43d588c86983df7f0b Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Mon, 2 Sep 2019 18:51:16 +0600 Subject: [PATCH 04/17] Handle case when deb_packages is empty. --- backend/device_registry/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 5a95c3177..a5cf4f51d 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -85,8 +85,9 @@ def certificate_expired(self): ] @property def insecure_services(self): - packages = set([p['name'] for p in self.deb_packages['packages']]) - return set(self.INSECURE_SERVICES) & packages + if 'packages' in self.deb_packages: + packages = set([p['name'] for p in self.deb_packages['packages']]) + return set(self.INSECURE_SERVICES) & packages def get_name(self): if self.name: From c8b3e7bc9b8183bcdc6c40575f10aec793a13426 Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Mon, 2 Sep 2019 22:51:11 +0600 Subject: [PATCH 05/17] Add test for "insecure services" --- .../templates/device_info_security.html | 4 +++- backend/device_registry/tests/test_all.py | 20 ++++++++++++++++++ backend/device_registry/tests/test_api.py | 21 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/backend/device_registry/templates/device_info_security.html b/backend/device_registry/templates/device_info_security.html index 10f0f0cf0..3928cf5d2 100644 --- a/backend/device_registry/templates/device_info_security.html +++ b/backend/device_registry/templates/device_info_security.html @@ -79,9 +79,11 @@

Security

Insecure Services +
    {% for service_name in object.insecure_services %} -

    {{ service_name }}

    +
  • {{ service_name }}

    {% endfor %} +
Logins diff --git a/backend/device_registry/tests/test_all.py b/backend/device_registry/tests/test_all.py index ce88b193f..6f414f6e5 100644 --- a/backend/device_registry/tests/test_all.py +++ b/backend/device_registry/tests/test_all.py @@ -532,6 +532,26 @@ def test_logins(self): self.assertEqual(response.status_code, 200) self.assertContains(response, '
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.device.deb_packages = {
+            'packages': [
+                {'name': 'telnetd', 'version': 'VERSION'},
+                {'name': 'fingerd', 'version': 'VERSION'}
+            ]
+        }
+        self.device.save()
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, '>telnetd<')
+        self.assertContains(response, '>fingerd<')
 
 
 class PairingKeysView(TestCase):
diff --git a/backend/device_registry/tests/test_api.py b/backend/device_registry/tests/test_api.py
index 40d4e4283..3d5865e12 100644
--- a/backend/device_registry/tests/test_api.py
+++ b/backend/device_registry/tests/test_api.py
@@ -849,6 +849,27 @@ 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'}]
+        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, {
+            'firewall': {
+                'block_ports': [],
+                'block_networks': settings.SPAM_NETWORKS,
+                'policy': self.device.firewallstate.policy_string
+            },
+            'deb_packages_hash': 'abcdef'
+        })
+        self.device.refresh_from_db()
+        self.assertListEqual(self.device.deb_packages, packages)
+
 
 class DeviceEnrollView(APITestCase):
     def setUp(self):

From 8484534ab8a0aab86ed36df3a4f81c7ad97b7caf Mon Sep 17 00:00:00 2001
From: Artem Martynovich 
Date: Mon, 2 Sep 2019 22:59:43 +0600
Subject: [PATCH 06/17] Add "no insecure services detected" message. Fix
 test_ping_writes_packages.

---
 .../templates/device_info_security.html          | 16 +++++++++++-----
 backend/device_registry/tests/test_all.py        |  1 +
 backend/device_registry/tests/test_api.py        |  2 +-
 3 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/backend/device_registry/templates/device_info_security.html b/backend/device_registry/templates/device_info_security.html
index 3928cf5d2..ad957d5eb 100644
--- a/backend/device_registry/templates/device_info_security.html
+++ b/backend/device_registry/templates/device_info_security.html
@@ -79,11 +79,17 @@ 

Security

Insecure Services -
    - {% for service_name in object.insecure_services %} -
  • {{ service_name }}

    - {% endfor %} -
+ {% with object.insecure_services as services %} + {% if services %} +
    + {% for service_name in services %} +
  • {{ service_name }}

    + {% endfor %} +
+ {% else %} + No insecure services detected. + {% endif %} + {% endwith %} Logins diff --git a/backend/device_registry/tests/test_all.py b/backend/device_registry/tests/test_all.py index 6f414f6e5..c0c01ac55 100644 --- a/backend/device_registry/tests/test_all.py +++ b/backend/device_registry/tests/test_all.py @@ -540,6 +540,7 @@ def test_insecure_services(self): self.assertEqual(response.status_code, 200) self.assertNotContains(response, '>telnetd<') self.assertNotContains(response, '>fingerd<') + self.assertContains(response, 'No insecure services detected') self.device.deb_packages = { 'packages': [ diff --git a/backend/device_registry/tests/test_api.py b/backend/device_registry/tests/test_api.py index 3d5865e12..fd1677cbf 100644 --- a/backend/device_registry/tests/test_api.py +++ b/backend/device_registry/tests/test_api.py @@ -868,7 +868,7 @@ def test_ping_writes_packages(self): 'deb_packages_hash': 'abcdef' }) self.device.refresh_from_db() - self.assertListEqual(self.device.deb_packages, packages) + self.assertListEqual(self.device.deb_packages['packages'], packages) class DeviceEnrollView(APITestCase): From a0b1a205825d9225127484df8615350b433fce65 Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Mon, 2 Sep 2019 23:09:26 +0600 Subject: [PATCH 07/17] Display N/A for Insecure Services if no deb packages. --- .../templates/device_info_security.html | 4 +++- backend/device_registry/tests/test_all.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/device_registry/templates/device_info_security.html b/backend/device_registry/templates/device_info_security.html index ad957d5eb..8710d152d 100644 --- a/backend/device_registry/templates/device_info_security.html +++ b/backend/device_registry/templates/device_info_security.html @@ -86,8 +86,10 @@

Security

  • {{ service_name }}

    {% endfor %} - {% else %} + {% elif services is not None %} No insecure services detected. + {% else %} + N/A {% endif %} {% endwith %} diff --git a/backend/device_registry/tests/test_all.py b/backend/device_registry/tests/test_all.py index c0c01ac55..404222f36 100644 --- a/backend/device_registry/tests/test_all.py +++ b/backend/device_registry/tests/test_all.py @@ -536,6 +536,20 @@ def test_logins(self): 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.deb_packages = { + 'packages': [ + {'name': 'python2', 'version': 'VERSION'}, + {'name': 'python3', 'version': 'VERSION'} + ] + } + self.device.save() response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertNotContains(response, '>telnetd<') From adb226382f332aee29f1079a84021970e133897e Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Tue, 3 Sep 2019 19:14:20 +0600 Subject: [PATCH 08/17] Make ping response flat again. --- backend/device_registry/api_views.py | 8 ++--- .../templates/device_info_security.html | 2 +- backend/device_registry/tests/test_api.py | 30 +++++++------------ 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/backend/device_registry/api_views.py b/backend/device_registry/api_views.py index a9279b19a..267ba2973 100644 --- a/backend/device_registry/api_views.py +++ b/backend/device_registry/api_views.py @@ -59,11 +59,9 @@ def get(self, request, *args, **kwargs): block_networks = portscan_object.block_networks.copy() block_networks.extend(settings.SPAM_NETWORKS) return Response({ - 'firewall': { - 'policy': firewallstate_object.policy_string, - firewallstate_object.ports_field_name: portscan_object.block_ports, - 'block_networks': block_networks - }, + 'policy': firewallstate_object.policy_string, + firewallstate_object.ports_field_name: portscan_object.block_ports, + 'block_networks': block_networks, 'deb_packages_hash': device.deb_packages.get('hash') }) diff --git a/backend/device_registry/templates/device_info_security.html b/backend/device_registry/templates/device_info_security.html index 8710d152d..4c4c9a432 100644 --- a/backend/device_registry/templates/device_info_security.html +++ b/backend/device_registry/templates/device_info_security.html @@ -83,7 +83,7 @@

    Security

    {% if services %}
      {% for service_name in services %} -
    • {{ service_name }}

      +
    • {{ service_name }}
    • {% endfor %}
    {% elif services is not None %} diff --git a/backend/device_registry/tests/test_api.py b/backend/device_registry/tests/test_api.py index fd1677cbf..92bfbac93 100644 --- a/backend/device_registry/tests/test_api.py +++ b/backend/device_registry/tests/test_api.py @@ -718,10 +718,8 @@ def test_ping_get_success(self): response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.data, { - 'firewall': { - 'policy': self.device.firewallstate.policy_string, - 'block_ports': [], 'block_networks': settings.SPAM_NETWORKS - }, + 'policy': self.device.firewallstate.policy_string, + 'block_ports': [], 'block_networks': settings.SPAM_NETWORKS, 'deb_packages_hash': None }) @@ -730,11 +728,9 @@ def test_pong_data(self): response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.data, { - 'firewall': { - 'block_ports': [], - 'block_networks': settings.SPAM_NETWORKS, - 'policy': self.device.firewallstate.policy_string - }, + 'block_ports': [], + 'block_networks': settings.SPAM_NETWORKS, + 'policy': self.device.firewallstate.policy_string, 'deb_packages_hash': None }) # 2nd request @@ -747,11 +743,9 @@ def test_pong_data(self): response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.data, { - 'firewall': { - '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 - }, + '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': None }) @@ -860,11 +854,9 @@ def test_ping_writes_packages(self): response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.data, { - 'firewall': { - 'block_ports': [], - 'block_networks': settings.SPAM_NETWORKS, - 'policy': self.device.firewallstate.policy_string - }, + 'block_ports': [], + 'block_networks': settings.SPAM_NETWORKS, + 'policy': self.device.firewallstate.policy_string, 'deb_packages_hash': 'abcdef' }) self.device.refresh_from_db() From e08e53549ccf6b0bec89d45f3648af886f58586a Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Thu, 5 Sep 2019 16:14:16 +0600 Subject: [PATCH 09/17] Make deb_packages an m2m relation. --- backend/backend/settings/dev.py | 3 +- .../migrations/0057_auto_20190904_0750.py | 37 +++++++++++++++++++ backend/device_registry/models.py | 22 +++++++++-- .../templates/device_info_security.html | 4 +- 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 backend/device_registry/migrations/0057_auto_20190904_0750.py diff --git a/backend/backend/settings/dev.py b/backend/backend/settings/dev.py index 78fa67a90..c16edd5bb 100644 --- a/backend/backend/settings/dev.py +++ b/backend/backend/settings/dev.py @@ -22,6 +22,7 @@ 'django_extensions' ] -IS_DEV = True +IS_MTLS_API=IS_API=IS_DASH=IS_DEV = True +ALLOWED_HOSTS += ['*'] EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/backend/device_registry/migrations/0057_auto_20190904_0750.py b/backend/device_registry/migrations/0057_auto_20190904_0750.py new file mode 100644 index 000000000..fff919970 --- /dev/null +++ b/backend/device_registry/migrations/0057_auto_20190904_0750.py @@ -0,0 +1,37 @@ +# Generated by Django 2.1.10 on 2019-09-04 07:50 + +import device_registry.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('device_registry', '0056_device_deb_packages'), + ] + + 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)), + ('distro', models.CharField(choices=[(device_registry.models.Distro('debian'), 'debian'), (device_registry.models.Distro('raspbian'), 'raspbian'), (device_registry.models.Distro('ubuntu'), 'ubuntu')], max_length=128)), + ], + ), + migrations.AddField( + model_name='device', + name='deb_packages_hash', + field=models.CharField(blank=True, max_length=16, null=True), + ), + migrations.RemoveField( + model_name='device', + name='deb_packages', + ), + 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 a5cf4f51d..27959660e 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -1,3 +1,4 @@ +from enum import Enum import datetime from statistics import mean import json @@ -39,6 +40,18 @@ def from_db_value(self, value, expression, connection, context): return value +class DebPackage(models.Model): + class Distro(Enum): + DEBIAN = 'debian' + RASPBIAN = 'raspbian' + UBUNTU = 'ubuntu' + + name = models.CharField(max_length=128, null=False, blank=False) + version = models.CharField(max_length=128, null=False, blank=False) + distro = models.CharField(max_length=128, null=False, blank=False, + choices=[(tag, tag.value) for tag in Distro]) + + class Device(models.Model): device_id = models.CharField( max_length=128, @@ -65,7 +78,8 @@ 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 = JSONField(blank=True, default=dict) + deb_packages = models.ManyToManyField(DebPackage) + deb_packages_hash = models.CharField(max_length=16, blank=True, null=True) @property def certificate_expired(self): @@ -85,9 +99,9 @@ def certificate_expired(self): ] @property def insecure_services(self): - if 'packages' in self.deb_packages: - packages = set([p['name'] for p in self.deb_packages['packages']]) - return set(self.INSECURE_SERVICES) & packages + if not self.deb_packages_hash: + return None + return self.deb_packages.filter(name__in=self.INSECURE_SERVICES) def get_name(self): if self.name: diff --git a/backend/device_registry/templates/device_info_security.html b/backend/device_registry/templates/device_info_security.html index 4c4c9a432..6f5b2f2b7 100644 --- a/backend/device_registry/templates/device_info_security.html +++ b/backend/device_registry/templates/device_info_security.html @@ -82,8 +82,8 @@

    Security

    {% with object.insecure_services as services %} {% if services %}
      - {% for service_name in services %} -
    • {{ service_name }}
    • + {% for service in services %} +
    • {{ service.name }}
    • {% endfor %}
    {% elif services is not None %} From 8444970a5aba3651b9b758383c6c560cbd50f3a1 Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Thu, 5 Sep 2019 18:21:47 +0600 Subject: [PATCH 10/17] Fix length of deb_packages_hash. Implement deb_packages as an m2m field. --- backend/device_registry/api_views.py | 8 ++++-- .../migrations/0057_auto_20190904_0750.py | 5 +++- .../migrations/0058_auto_20190905_1152.py | 18 ++++++++++++ backend/device_registry/models.py | 28 ++++++++++++++++++- 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 backend/device_registry/migrations/0058_auto_20190905_1152.py diff --git a/backend/device_registry/api_views.py b/backend/device_registry/api_views.py index 267ba2973..3dfc4c0f1 100644 --- a/backend/device_registry/api_views.py +++ b/backend/device_registry/api_views.py @@ -62,7 +62,7 @@ def get(self, request, *args, **kwargs): 'policy': firewallstate_object.policy_string, firewallstate_object.ports_field_name: portscan_object.block_ports, 'block_networks': block_networks, - 'deb_packages_hash': device.deb_packages.get('hash') + 'deb_packages_hash': device.deb_packages_hash }) def post(self, request, *args, **kwargs): @@ -71,7 +71,9 @@ def post(self, request, *args, **kwargs): device.last_ping = timezone.now() device.agent_version = data.get('agent_version') if 'deb_packages' in data: - device.deb_packages = data['deb_packages'] + deb_packages = data['deb_packages'] + device.deb_packages_hash = deb_packages.get('hash') + device.set_deb_packages(deb_packages.get('packages')) device_info_object, _ = DeviceInfo.objects.get_or_create(device=device) device_info_object.device__last_ping = timezone.now() @@ -107,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', 'deb_packages', '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/0057_auto_20190904_0750.py b/backend/device_registry/migrations/0057_auto_20190904_0750.py index fff919970..fa307b03d 100644 --- a/backend/device_registry/migrations/0057_auto_20190904_0750.py +++ b/backend/device_registry/migrations/0057_auto_20190904_0750.py @@ -17,7 +17,10 @@ class Migration(migrations.Migration): ('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)), - ('distro', models.CharField(choices=[(device_registry.models.Distro('debian'), 'debian'), (device_registry.models.Distro('raspbian'), 'raspbian'), (device_registry.models.Distro('ubuntu'), 'ubuntu')], max_length=128)), + ('distro', models.CharField(choices=[(device_registry.models.DebPackage.Distro('debian'), 'debian'), + (device_registry.models.DebPackage.Distro('raspbian'), 'raspbian'), + (device_registry.models.DebPackage.Distro('ubuntu'), 'ubuntu')], + max_length=128)), ], ), migrations.AddField( diff --git a/backend/device_registry/migrations/0058_auto_20190905_1152.py b/backend/device_registry/migrations/0058_auto_20190905_1152.py new file mode 100644 index 000000000..1104fac63 --- /dev/null +++ b/backend/device_registry/migrations/0058_auto_20190905_1152.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.10 on 2019-09-05 11:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('device_registry', '0057_auto_20190904_0750'), + ] + + operations = [ + migrations.AlterField( + model_name='device', + name='deb_packages_hash', + field=models.CharField(blank=True, max_length=32, null=True), + ), + ] diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 27959660e..060265098 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -79,7 +79,7 @@ class Device(models.Model): 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=16, blank=True, null=True) + deb_packages_hash = models.CharField(max_length=32, blank=True, null=True) @property def certificate_expired(self): @@ -103,6 +103,32 @@ def insecure_services(self): return None return self.deb_packages.filter(name__in=self.INSECURE_SERVICES) + def set_deb_packages(self, packages): + packages_set = set((p['name'], p['version']) for p in packages) + + # Find which packages we already have in db. + existing_packages = DebPackage.objects.filter(name__in=(p[0] for p in packages_set)).intersection( + DebPackage.objects.filter(version__in=(p[1] for p in packages_set))) + existing_packages_set = set((p.name, p.version) for p in existing_packages) + + # Find the difference between the incoming package list and what we already have in db. + # Insert the missing packages. + extra_packages = packages_set.difference(existing_packages_set) + DebPackage.objects.bulk_create(DebPackage(name=p[0], version=p[1]) for p in extra_packages) + + # Since bulk_create doesn't fetch created ids we need to do this ourselves. + extra_packages = DebPackage.objects.filter(name__in=(p[0] for p in extra_packages)).intersection( + DebPackage.objects.filter(version__in=(p[1] for p in extra_packages))) + + # Re-create the m2m relation deb_packages in a bulk. + # The list of all packages is a union of existing_packages (which had existed in the db already) and + # extra_packages (which we've just created). + # FIXME: use new ignore_conflicts arg to bulk_create in Django 2.2. + Device.deb_packages.through.objects.all().delete() + Device.deb_packages.through.objects.bulk_create( + (Device.deb_packages.through(device_id=self.pk, debpackage_id=p.pk) for p in existing_packages|extra_packages) + ) + def get_name(self): if self.name: return self.name From 2393f8f5bf5b7d5277b156cb61d83ae205ddeb06 Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Fri, 6 Sep 2019 15:08:14 +0600 Subject: [PATCH 11/17] Update tests to work with m2m deb_packages. --- backend/device_registry/tests/test_all.py | 22 ++++++++++------------ backend/device_registry/tests/test_api.py | 5 +++-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/backend/device_registry/tests/test_all.py b/backend/device_registry/tests/test_all.py index 404222f36..e13460337 100644 --- a/backend/device_registry/tests/test_all.py +++ b/backend/device_registry/tests/test_all.py @@ -543,12 +543,11 @@ def test_insecure_services(self): self.assertNotContains(response, '>fingerd<') self.assertNotContains(response, 'No insecure services detected') - self.device.deb_packages = { - 'packages': [ - {'name': 'python2', 'version': 'VERSION'}, - {'name': 'python3', 'version': 'VERSION'} - ] - } + self.device.set_deb_packages([ + {'name': 'python2', 'version': 'VERSION'}, + {'name': 'python3', 'version': 'VERSION'} + ]) + self.device.deb_packages_hash = 'abcdef' self.device.save() response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -556,17 +555,16 @@ def test_insecure_services(self): self.assertNotContains(response, '>fingerd<') self.assertContains(response, 'No insecure services detected') - self.device.deb_packages = { - 'packages': [ - {'name': 'telnetd', 'version': 'VERSION'}, - {'name': 'fingerd', 'version': 'VERSION'} - ] - } + self.device.set_deb_packages([ + {'name': 'telnetd', 'version': 'VERSION'}, + {'name': 'fingerd', 'version': 'VERSION'} + ]) 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') class PairingKeysView(TestCase): diff --git a/backend/device_registry/tests/test_api.py b/backend/device_registry/tests/test_api.py index 92bfbac93..3bfab9acc 100644 --- a/backend/device_registry/tests/test_api.py +++ b/backend/device_registry/tests/test_api.py @@ -17,7 +17,7 @@ from rest_framework.exceptions import ErrorDetail from rest_framework.authtoken.models import Token -from device_registry.models import Credential, Device, DeviceInfo, Tag, FirewallState, PortScan, PairingKey +from device_registry.models import Credential, Device, DeviceInfo, Tag, FirewallState, PortScan, PairingKey, DebPackage from device_registry.serializers import DeviceListSerializer @@ -860,7 +860,8 @@ def test_ping_writes_packages(self): 'deb_packages_hash': 'abcdef' }) self.device.refresh_from_db() - self.assertListEqual(self.device.deb_packages['packages'], packages) + self.assertQuerysetEqual(self.device.deb_packages.all(), packages, + transform=lambda p: {'name': p.name, 'version': p.version}) class DeviceEnrollView(APITestCase): From 4282e2730eb28a91d9d09fb172145f8c8c175c7f Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Sun, 8 Sep 2019 10:30:43 +0600 Subject: [PATCH 12/17] Remove dev code in dev.py. Remove unused import in test_api. Don't allow missing fields in deb_packages inside ping message. Add unique constraint to DebPackage model. --- backend/backend/settings/dev.py | 3 +-- backend/device_registry/api_views.py | 4 ++-- backend/device_registry/models.py | 16 ++++++++++++---- backend/device_registry/tests/test_api.py | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/backend/backend/settings/dev.py b/backend/backend/settings/dev.py index c16edd5bb..78fa67a90 100644 --- a/backend/backend/settings/dev.py +++ b/backend/backend/settings/dev.py @@ -22,7 +22,6 @@ 'django_extensions' ] -IS_MTLS_API=IS_API=IS_DASH=IS_DEV = True -ALLOWED_HOSTS += ['*'] +IS_DEV = True EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/backend/device_registry/api_views.py b/backend/device_registry/api_views.py index 3dfc4c0f1..5288ba40b 100644 --- a/backend/device_registry/api_views.py +++ b/backend/device_registry/api_views.py @@ -72,8 +72,8 @@ def post(self, request, *args, **kwargs): device.agent_version = data.get('agent_version') if 'deb_packages' in data: deb_packages = data['deb_packages'] - device.deb_packages_hash = deb_packages.get('hash') - device.set_deb_packages(deb_packages.get('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() diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 060265098..3baf3cd03 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -46,10 +46,18 @@ class Distro(Enum): RASPBIAN = 'raspbian' UBUNTU = 'ubuntu' - name = models.CharField(max_length=128, null=False, blank=False) - version = models.CharField(max_length=128, null=False, blank=False) - distro = models.CharField(max_length=128, null=False, blank=False, - choices=[(tag, tag.value) for tag in Distro]) + class Arch(Enum): + i386 = 'i386' + amd64 = 'amd64' + armhf = 'armhf' + + name = models.CharField(max_length=128) + version = models.CharField(max_length=128) + distro = models.CharField(max_length=128, choices=[(tag, tag.value) for tag in Distro]) + arch = models.CharField(max_length=16, choices=[(tag, tag.value) for tag in Distro]) + + class Meta: + unique_together = ['name', 'version', 'distro'] class Device(models.Model): diff --git a/backend/device_registry/tests/test_api.py b/backend/device_registry/tests/test_api.py index 3bfab9acc..10110346a 100644 --- a/backend/device_registry/tests/test_api.py +++ b/backend/device_registry/tests/test_api.py @@ -17,7 +17,7 @@ from rest_framework.exceptions import ErrorDetail from rest_framework.authtoken.models import Token -from device_registry.models import Credential, Device, DeviceInfo, Tag, FirewallState, PortScan, PairingKey, DebPackage +from device_registry.models import Credential, Device, DeviceInfo, Tag, FirewallState, PortScan, PairingKey from device_registry.serializers import DeviceListSerializer From e6e826f9b4d1123f4c6579cd6eb92626101d0606 Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Sun, 8 Sep 2019 11:06:53 +0600 Subject: [PATCH 13/17] Add missing migration. --- .../migrations/0059_auto_20190908_0452.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 backend/device_registry/migrations/0059_auto_20190908_0452.py diff --git a/backend/device_registry/migrations/0059_auto_20190908_0452.py b/backend/device_registry/migrations/0059_auto_20190908_0452.py new file mode 100644 index 000000000..ea9e0f418 --- /dev/null +++ b/backend/device_registry/migrations/0059_auto_20190908_0452.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.10 on 2019-09-08 04:52 + +import device_registry.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('device_registry', '0058_auto_20190905_1152'), + ] + + operations = [ + migrations.AddField( + model_name='debpackage', + name='arch', + field=models.CharField(choices=[(device_registry.models.DebPackage.Distro('debian'), 'debian'), (device_registry.models.DebPackage.Distro('raspbian'), 'raspbian'), (device_registry.models.DebPackage.Distro('ubuntu'), 'ubuntu')], default='i386', max_length=16), + preserve_default=False, + ), + migrations.AlterUniqueTogether( + name='debpackage', + unique_together={('name', 'version', 'distro')}, + ), + ] From 747ea8afa70f25fb82616aa1e0024c3363bd2bae Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Mon, 9 Sep 2019 17:27:46 +0600 Subject: [PATCH 14/17] Remove DebPackage.distro, add unique constraint and Arch.all. Merge migrations. --- ...904_0750.py => 0056_auto_20190909_1128.py} | 17 +++++------- .../migrations/0056_device_deb_packages.py | 19 -------------- .../migrations/0058_auto_20190905_1152.py | 18 ------------- .../migrations/0059_auto_20190908_0452.py | 24 ----------------- backend/device_registry/models.py | 26 ++++++++++--------- backend/device_registry/tests/test_all.py | 8 +++--- backend/device_registry/tests/test_api.py | 4 +-- 7 files changed, 27 insertions(+), 89 deletions(-) rename backend/device_registry/migrations/{0057_auto_20190904_0750.py => 0056_auto_20190909_1128.py} (54%) delete mode 100644 backend/device_registry/migrations/0056_device_deb_packages.py delete mode 100644 backend/device_registry/migrations/0058_auto_20190905_1152.py delete mode 100644 backend/device_registry/migrations/0059_auto_20190908_0452.py diff --git a/backend/device_registry/migrations/0057_auto_20190904_0750.py b/backend/device_registry/migrations/0056_auto_20190909_1128.py similarity index 54% rename from backend/device_registry/migrations/0057_auto_20190904_0750.py rename to backend/device_registry/migrations/0056_auto_20190909_1128.py index fa307b03d..19b69c4af 100644 --- a/backend/device_registry/migrations/0057_auto_20190904_0750.py +++ b/backend/device_registry/migrations/0056_auto_20190909_1128.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.10 on 2019-09-04 07:50 +# Generated by Django 2.1.10 on 2019-09-09 11:28 import device_registry.models from django.db import migrations, models @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('device_registry', '0056_device_deb_packages'), + ('device_registry', '0055_remove_deviceinfo_trust_score'), ] operations = [ @@ -17,20 +17,17 @@ class Migration(migrations.Migration): ('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)), - ('distro', models.CharField(choices=[(device_registry.models.DebPackage.Distro('debian'), 'debian'), - (device_registry.models.DebPackage.Distro('raspbian'), 'raspbian'), - (device_registry.models.DebPackage.Distro('ubuntu'), 'ubuntu')], - 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=16, null=True), + field=models.CharField(blank=True, max_length=32, null=True), ), - migrations.RemoveField( - model_name='device', - name='deb_packages', + migrations.AlterUniqueTogether( + name='debpackage', + unique_together={('name', 'version', 'arch')}, ), migrations.AddField( model_name='device', diff --git a/backend/device_registry/migrations/0056_device_deb_packages.py b/backend/device_registry/migrations/0056_device_deb_packages.py deleted file mode 100644 index 32a2b9a43..000000000 --- a/backend/device_registry/migrations/0056_device_deb_packages.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.10 on 2019-08-30 10:48 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('device_registry', '0055_remove_deviceinfo_trust_score'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='deb_packages', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - ] diff --git a/backend/device_registry/migrations/0058_auto_20190905_1152.py b/backend/device_registry/migrations/0058_auto_20190905_1152.py deleted file mode 100644 index 1104fac63..000000000 --- a/backend/device_registry/migrations/0058_auto_20190905_1152.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.10 on 2019-09-05 11:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('device_registry', '0057_auto_20190904_0750'), - ] - - operations = [ - migrations.AlterField( - model_name='device', - name='deb_packages_hash', - field=models.CharField(blank=True, max_length=32, null=True), - ), - ] diff --git a/backend/device_registry/migrations/0059_auto_20190908_0452.py b/backend/device_registry/migrations/0059_auto_20190908_0452.py deleted file mode 100644 index ea9e0f418..000000000 --- a/backend/device_registry/migrations/0059_auto_20190908_0452.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.1.10 on 2019-09-08 04:52 - -import device_registry.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('device_registry', '0058_auto_20190905_1152'), - ] - - operations = [ - migrations.AddField( - model_name='debpackage', - name='arch', - field=models.CharField(choices=[(device_registry.models.DebPackage.Distro('debian'), 'debian'), (device_registry.models.DebPackage.Distro('raspbian'), 'raspbian'), (device_registry.models.DebPackage.Distro('ubuntu'), 'ubuntu')], default='i386', max_length=16), - preserve_default=False, - ), - migrations.AlterUniqueTogether( - name='debpackage', - unique_together={('name', 'version', 'distro')}, - ), - ] diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 3baf3cd03..8b81e80fe 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -48,16 +48,16 @@ class Distro(Enum): class Arch(Enum): i386 = 'i386' - amd64 = 'amd64' - armhf = 'armhf' + AMD64 = 'amd64' + ARMHF = 'armhf' + ALL = 'all' name = models.CharField(max_length=128) version = models.CharField(max_length=128) - distro = models.CharField(max_length=128, choices=[(tag, tag.value) for tag in Distro]) - arch = models.CharField(max_length=16, choices=[(tag, tag.value) for tag in Distro]) + arch = models.CharField(max_length=16, choices=[(tag, tag.value) for tag in Arch]) class Meta: - unique_together = ['name', 'version', 'distro'] + unique_together = ['name', 'version', 'arch'] class Device(models.Model): @@ -112,21 +112,23 @@ def insecure_services(self): return self.deb_packages.filter(name__in=self.INSECURE_SERVICES) def set_deb_packages(self, packages): - packages_set = set((p['name'], p['version']) for p in packages) + packages_set = set((p['name'], p['version'], p['arch']) for p in packages) # Find which packages we already have in db. - existing_packages = DebPackage.objects.filter(name__in=(p[0] for p in packages_set)).intersection( - DebPackage.objects.filter(version__in=(p[1] for p in packages_set))) - existing_packages_set = set((p.name, p.version) for p in existing_packages) + existing_packages = DebPackage.objects.filter(name__in=(p[0] for p in packages_set))\ + .filter(version__in=(p[1] for p in packages_set))\ + .filter(arch__in=(p[2] for p in packages_set)) + existing_packages_set = set((p.name, p.version, p.arch) for p in existing_packages) # Find the difference between the incoming package list and what we already have in db. # Insert the missing packages. extra_packages = packages_set.difference(existing_packages_set) - DebPackage.objects.bulk_create(DebPackage(name=p[0], version=p[1]) for p in extra_packages) + DebPackage.objects.bulk_create(DebPackage(name=p[0], version=p[1], arch=p[2]) for p in extra_packages) # Since bulk_create doesn't fetch created ids we need to do this ourselves. - extra_packages = DebPackage.objects.filter(name__in=(p[0] for p in extra_packages)).intersection( - DebPackage.objects.filter(version__in=(p[1] for p in extra_packages))) + extra_packages = DebPackage.objects.filter(name__in=(p[0] for p in extra_packages))\ + .filter(version__in=(p[1] for p in extra_packages))\ + .filter(arch__in=(p[2] for p in extra_packages)) # Re-create the m2m relation deb_packages in a bulk. # The list of all packages is a union of existing_packages (which had existed in the db already) and diff --git a/backend/device_registry/tests/test_all.py b/backend/device_registry/tests/test_all.py index e13460337..97f075290 100644 --- a/backend/device_registry/tests/test_all.py +++ b/backend/device_registry/tests/test_all.py @@ -544,8 +544,8 @@ def test_insecure_services(self): self.assertNotContains(response, 'No insecure services detected') self.device.set_deb_packages([ - {'name': 'python2', 'version': 'VERSION'}, - {'name': 'python3', 'version': 'VERSION'} + {'name': 'python2', 'version': 'VERSION', 'arch': 'i386'}, + {'name': 'python3', 'version': 'VERSION', 'arch': 'i386'} ]) self.device.deb_packages_hash = 'abcdef' self.device.save() @@ -556,8 +556,8 @@ def test_insecure_services(self): self.assertContains(response, 'No insecure services detected') self.device.set_deb_packages([ - {'name': 'telnetd', 'version': 'VERSION'}, - {'name': 'fingerd', 'version': 'VERSION'} + {'name': 'telnetd', 'version': 'VERSION', 'arch': 'i386'}, + {'name': 'fingerd', 'version': 'VERSION', 'arch': 'i386'} ]) self.device.save() response = self.client.get(url) diff --git a/backend/device_registry/tests/test_api.py b/backend/device_registry/tests/test_api.py index 10110346a..48f678ec3 100644 --- a/backend/device_registry/tests/test_api.py +++ b/backend/device_registry/tests/test_api.py @@ -844,7 +844,7 @@ def test_ping_writes_trust_score(self): self.assertGreater(self.device.trust_score, 0.42) def test_ping_writes_packages(self): - packages = [{'name': 'PACKAGE', 'version': 'VERSION'}] + packages = [{'name': 'PACKAGE', 'version': 'VERSION', 'arch': 'all'}] self.ping_payload['deb_packages'] = { 'hash': 'abcdef', 'packages': packages @@ -861,7 +861,7 @@ def test_ping_writes_packages(self): }) self.device.refresh_from_db() self.assertQuerysetEqual(self.device.deb_packages.all(), packages, - transform=lambda p: {'name': p.name, 'version': p.version}) + transform=lambda p: {'name': p.name, 'version': p.version, 'arch': p.arch}) class DeviceEnrollView(APITestCase): From c264acd9f2dfb0f02cc67ca1f176571f833eb32c Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Mon, 9 Sep 2019 18:43:00 +0600 Subject: [PATCH 15/17] Don't allow blank in deb_packages_hash. --- .../device_registry/migrations/0056_auto_20190909_1128.py | 2 +- backend/device_registry/models.py | 2 +- backend/device_registry/tests/test_api.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/device_registry/migrations/0056_auto_20190909_1128.py b/backend/device_registry/migrations/0056_auto_20190909_1128.py index 19b69c4af..64542024e 100644 --- a/backend/device_registry/migrations/0056_auto_20190909_1128.py +++ b/backend/device_registry/migrations/0056_auto_20190909_1128.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='device', name='deb_packages_hash', - field=models.CharField(blank=True, max_length=32, null=True), + field=models.CharField(blank=True, max_length=32), ), migrations.AlterUniqueTogether( name='debpackage', diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 8b81e80fe..eca67837e 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -87,7 +87,7 @@ class Device(models.Model): 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, null=True) + deb_packages_hash = models.CharField(max_length=32, blank=True) @property def certificate_expired(self): diff --git a/backend/device_registry/tests/test_api.py b/backend/device_registry/tests/test_api.py index 48f678ec3..ac4c5f8e4 100644 --- a/backend/device_registry/tests/test_api.py +++ b/backend/device_registry/tests/test_api.py @@ -720,7 +720,7 @@ def test_ping_get_success(self): self.assertDictEqual(response.data, { 'policy': self.device.firewallstate.policy_string, 'block_ports': [], 'block_networks': settings.SPAM_NETWORKS, - 'deb_packages_hash': None + 'deb_packages_hash': '' }) def test_pong_data(self): @@ -731,7 +731,7 @@ def test_pong_data(self): 'block_ports': [], 'block_networks': settings.SPAM_NETWORKS, 'policy': self.device.firewallstate.policy_string, - 'deb_packages_hash': None + 'deb_packages_hash': '' }) # 2nd request self.device.portscan.block_ports = [['192.168.1.178', 'tcp', 22, False]] @@ -746,7 +746,7 @@ def test_pong_data(self): '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': None + 'deb_packages_hash': '' }) def test_ping_creates_models(self): From c76eece775b20c0e5c752991aa45eed596732b89 Mon Sep 17 00:00:00 2001 From: Artem Martynovich Date: Mon, 9 Sep 2019 18:50:20 +0600 Subject: [PATCH 16/17] Add docstrings to insecure_services() and set_deb_packages(). --- backend/device_registry/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index eca67837e..120b455bd 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -107,11 +107,19 @@ def certificate_expired(self): ] @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. + """ packages_set = set((p['name'], p['version'], p['arch']) for p in packages) # Find which packages we already have in db. From 7d0c78aef3d097da835813e83c4760ebf2bf7968 Mon Sep 17 00:00:00 2001 From: Roman P Date: Wed, 11 Sep 2019 08:01:23 +0400 Subject: [PATCH 17/17] Optimize 'set_deb_packages' --- backend/device_registry/models.py | 41 +++++++---------------- backend/device_registry/tests/test_all.py | 14 ++++++-- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/backend/device_registry/models.py b/backend/device_registry/models.py index 120b455bd..4b3213fb6 100644 --- a/backend/device_registry/models.py +++ b/backend/device_registry/models.py @@ -5,10 +5,9 @@ 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 @@ -105,6 +104,7 @@ def certificate_expired(self): 'rsh-server', 'rsh-redone-server' ] + @property def insecure_services(self): """ @@ -120,32 +120,17 @@ 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. """ - packages_set = set((p['name'], p['version'], p['arch']) for p in packages) - - # Find which packages we already have in db. - existing_packages = DebPackage.objects.filter(name__in=(p[0] for p in packages_set))\ - .filter(version__in=(p[1] for p in packages_set))\ - .filter(arch__in=(p[2] for p in packages_set)) - existing_packages_set = set((p.name, p.version, p.arch) for p in existing_packages) - - # Find the difference between the incoming package list and what we already have in db. - # Insert the missing packages. - extra_packages = packages_set.difference(existing_packages_set) - DebPackage.objects.bulk_create(DebPackage(name=p[0], version=p[1], arch=p[2]) for p in extra_packages) - - # Since bulk_create doesn't fetch created ids we need to do this ourselves. - extra_packages = DebPackage.objects.filter(name__in=(p[0] for p in extra_packages))\ - .filter(version__in=(p[1] for p in extra_packages))\ - .filter(arch__in=(p[2] for p in extra_packages)) - - # Re-create the m2m relation deb_packages in a bulk. - # The list of all packages is a union of existing_packages (which had existed in the db already) and - # extra_packages (which we've just created). - # FIXME: use new ignore_conflicts arg to bulk_create in Django 2.2. - Device.deb_packages.through.objects.all().delete() - Device.deb_packages.through.objects.bulk_create( - (Device.deb_packages.through(device_id=self.pk, debpackage_id=p.pk) for p in existing_packages|extra_packages) - ) + # 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: diff --git a/backend/device_registry/tests/test_all.py b/backend/device_registry/tests/test_all.py index 97f075290..303ecd8d3 100644 --- a/backend/device_registry/tests/test_all.py +++ b/backend/device_registry/tests/test_all.py @@ -532,11 +532,11 @@ def test_logins(self): self.assertEqual(response.status_code, 200) self.assertContains(response, '
    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<')
    @@ -554,6 +554,10 @@ def test_insecure_services(self):
             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'},
    @@ -565,6 +569,10 @@ def test_insecure_services(self):
             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):
    @@ -605,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,