Skip to content
This repository has been archived by the owner on Sep 16, 2022. It is now read-only.

Store the list of deb packages. #401

Merged
merged 17 commits into from
Sep 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/backend/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
'PORT': os.getenv('DB_PORT', '5432'),
'OPTIONS': {
'connect_timeout': 3,
},
}
}
}
COMMON_NAME_PREFIX = 'd.wott-dev.local'
Expand Down
15 changes: 11 additions & 4 deletions backend/device_registry/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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'])

Expand Down
37 changes: 37 additions & 0 deletions backend/device_registry/migrations/0056_auto_20190909_1128.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
66 changes: 64 additions & 2 deletions backend/device_registry/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -39,6 +39,26 @@ def from_db_value(self, value, expression, connection, context):
return value


class DebPackage(models.Model):
a-martynovich marked this conversation as resolved.
Show resolved Hide resolved
class Distro(Enum):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Снимок экрана 2019-09-06 в 14 38 08

Usually in Django they do this ^ way. What's the advantage of your approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want the field value to be converted to Enum class, this way we can have some type checking later when we work with deb packages in a separate worker which will scan them for CVEs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give me some example of such type checking that requires enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the advantage of your approach?
can you give me some example of such type checking that requires enum?

This is self-documented, has less code and will fail quickly (before even getting to the db backend) if we do package.distro = SomeOtherEnum.Other instead of package.distro = DebPackage.Distro.Raspbian.

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,
Expand All @@ -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)
a-martynovich marked this conversation as resolved.
Show resolved Hide resolved
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):
a-martynovich marked this conversation as resolved.
Show resolved Hide resolved
"""
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
Expand Down
16 changes: 15 additions & 1 deletion backend/device_registry/templates/device_info_security.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,21 @@ <h4 class="tab-title">Security</h4>
<th scope="row">
Insecure Services
</th>
<td>Coming soon!</td>
<td>
{% with object.insecure_services as services %}
{% if services %}
<ul>
{% for service in services %}
<li>{{ service.name }}</li>
{% endfor %}
</ul>
{% elif services is not None %}
No insecure services detected.
{% else %}
N/A
{% endif %}
{% endwith %}
</td>
</tr>
<th scope="row">Logins</th>
<td>
Expand Down
43 changes: 42 additions & 1 deletion backend/device_registry/tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,47 @@ def test_logins(self):
self.assertContains(response, '<pre>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):
Expand Down Expand Up @@ -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,
Expand Down
44 changes: 37 additions & 7 deletions backend/device_registry/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand All @@ -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()
Expand Down Expand Up @@ -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):
Expand Down