From f2f30ecb6fdf323d7732647f0158619d3b9f7a09 Mon Sep 17 00:00:00 2001 From: zeroq Date: Wed, 12 Jun 2019 22:02:47 +0200 Subject: [PATCH] added template tasks, adding of tasks, and incident contacts #14 --- kraut_api/serializers.py | 7 +- kraut_api/urls.py | 1 + kraut_api/views.py | 51 ++++++++- kraut_incident/admin.py | 4 +- kraut_incident/fixtures/initial_data.json | 24 +++++ kraut_incident/models.py | 6 ++ .../kraut_incident/incident_details.html | 101 ++++++++++++++++-- kraut_incident/urls.py | 1 + kraut_incident/views.py | 54 ++++++++-- 9 files changed, 232 insertions(+), 17 deletions(-) diff --git a/kraut_api/serializers.py b/kraut_api/serializers.py index d9f60a7..fe32285 100644 --- a/kraut_api/serializers.py +++ b/kraut_api/serializers.py @@ -5,7 +5,7 @@ from kraut_parser.models import MalwareInstance, AttackPattern from kraut_intel.utils import get_icon_for_namespace from kraut_intel.models import PackageComment, NamespaceIcon, ThreatActorComment, CampaignComment, TTPComment, IndicatorComment, ObservableComment -from kraut_incident.models import Contact, Handler, Incident +from kraut_incident.models import Contact, Handler, Incident, TemplateTask from kraut_sharing.models import TAXII_Remote_Server, TAXII_Remote_Collection import datetime @@ -319,6 +319,11 @@ class Meta: ################### INCIDENT ##################### +class TemplateTaskSerializer(serializers.ModelSerializer): + class Meta: + model = TemplateTask + fields = ('id', 'name', 'description') + class IncidentSerializer(serializers.ModelSerializer): status = serializers.StringRelatedField() category = serializers.StringRelatedField() diff --git a/kraut_api/urls.py b/kraut_api/urls.py index ac4fb9a..211a199 100644 --- a/kraut_api/urls.py +++ b/kraut_api/urls.py @@ -55,6 +55,7 @@ # incident url(r'^incident/contacts/$', views.contact_list), url(r'^incident/handlers/$', views.handler_list), + url(r'^incident/tasks/$', views.template_tasks), url(r'^incident/(?P[0-9]+)/handlers/$', views.handler_list_incident), url(r'^incident/(?P[0-9]+)/contacts/$', views.contact_list_incident), url(r'^incidents/$', views.incident_list), diff --git a/kraut_api/views.py b/kraut_api/views.py index 45dd0fe..8b54bda 100644 --- a/kraut_api/views.py +++ b/kraut_api/views.py @@ -23,7 +23,7 @@ # Package from kraut_api.serializers import PackageSerializer, PackSerializer, PackageCommentSerializer # Incident -from kraut_api.serializers import IncidentSerializer, ContactSerializer, HandlerSerializer +from kraut_api.serializers import IncidentSerializer, ContactSerializer, HandlerSerializer, TemplateTaskSerializer # Objects from kraut_api.serializers import AddressObjectSerializer, URIObjectSerializer, FileObjectSerializer # TTPs @@ -34,7 +34,7 @@ from kraut_api.serializers import CollectionSerializer, ServersSerializer # from kraut_parser.utils import get_object_for_observable, get_related_objects_for_object -from kraut_incident.models import Contact, Handler, Incident +from kraut_incident.models import Contact, Handler, Incident, TemplateTask from kraut_intel.models import PackageComment, NamespaceIcon, ThreatActorComment, CampaignComment, TTPComment, IndicatorComment, ObservableComment from kraut_sharing.models import TAXII_Remote_Server, TAXII_Remote_Collection from kraut_sharing.forms import DiscoveryForm @@ -2051,6 +2051,53 @@ def handler_list(request, format=None): return paginator.get_paginated_response(serializer.data) return HttpResponse(status=status.HTTP_400_BAD_REQUEST) +@api_view(['GET']) +@authentication_classes((SessionAuthentication, )) +@permission_classes((IsAuthenticated,)) +def template_tasks(request, format=None): + if request.method == 'GET': + paginator = CustomPaginator() + max_items = 10 + page = request.query_params.get('page') + if request.query_params: + # number of items to retrieve + if 'length' in request.query_params: + max_items = int(request.query_params['length']) + # page to show + if 'start' in request.query_params: + page = int(int(request.query_params['start'])/int(max_items))+1 + # order + if 'order[0][column]' in request.query_params and 'order[0][dir]' in request.query_params: + order_by_column = request.query_params['columns['+str(request.query_params['order[0][column]'])+'][data]'] + if request.query_params['order[0][dir]'] == 'desc': + order_direction = '-' + else: + order_direction = '' + else: + order_by_column = 'name' + order_direction = '-' + # search + if 'search[value]' in request.query_params: + search_value = request.query_params['search[value]'] + else: + search_value = None + else: + order_by_column = 'name' + order_direction = '-' + search_value = None + # construct queryset + queryset = TemplateTask.objects.all().order_by('%s%s' % (order_direction, order_by_column)) + if search_value: + queryset = queryset.filter( + Q(name__icontains=search_value) + ) + handler = paginator.paginate_queryset(queryset, request) + serializer_context = {'request': request} + serializer = TemplateTaskSerializer(instance=handler, context=serializer_context, many=True) + return paginator.get_paginated_response(serializer.data) + return HttpResponse(status=status.HTTP_400_BAD_REQUEST) + + ################### INCIDENT CONTACTS ##################### diff --git a/kraut_incident/admin.py b/kraut_incident/admin.py index 6e1884f..bb12b6e 100644 --- a/kraut_incident/admin.py +++ b/kraut_incident/admin.py @@ -1,7 +1,7 @@ # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 from django.contrib import admin -from kraut_incident.models import Asset, Incident, Incident_Category, Incident_Status, Contact +from kraut_incident.models import Asset, Incident, Incident_Category, Incident_Status, Contact, Task, TemplateTask # Register your models here. @@ -10,3 +10,5 @@ admin.site.register(Incident_Category) admin.site.register(Incident_Status) admin.site.register(Contact) +admin.site.register(Task) +admin.site.register(TemplateTask) diff --git a/kraut_incident/fixtures/initial_data.json b/kraut_incident/fixtures/initial_data.json index ba41c8f..dbd7184 100644 --- a/kraut_incident/fixtures/initial_data.json +++ b/kraut_incident/fixtures/initial_data.json @@ -126,5 +126,29 @@ "name": "Phishing/Scam/Spam", "description": "This category includes all incidents related to phishing, scam, or spam emails." } + }, + { + "model": "kraut_incident.templatetask", + "pk": 1, + "fields": { + "name": "Reinstall System(s)", + "description": "Initiate reinstallation of affected assets (e.g. because of compromise)." + } + }, + { + "model": "kraut_incident.templatetask", + "pk": 2, + "fields": { + "name": "Forensic Copy", + "description": "Create forensically sound images of affected assets." + } + }, + { + "model": "kraut_incident.templatetask", + "pk": 3, + "fields": { + "name": "Collect Logfiles", + "description": "Collect specific log files from affected assets." + } } ] diff --git a/kraut_incident/models.py b/kraut_incident/models.py index 5db15ab..ffc322d 100644 --- a/kraut_incident/models.py +++ b/kraut_incident/models.py @@ -39,6 +39,12 @@ class Contact(models.Model): class Meta: unique_together = (("firstname", "lastname", "email"),) +class TemplateTask(models.Model): + """Describe a template for a task to attach to an incident + """ + name = models.CharField(max_length=255, default="Template Task") + description = models.TextField(null=True, blank=True, default="Task Description") + class Task(models.Model): """Describe a task to be performed """ diff --git a/kraut_incident/templates/kraut_incident/incident_details.html b/kraut_incident/templates/kraut_incident/incident_details.html index 501029e..279cde7 100644 --- a/kraut_incident/templates/kraut_incident/incident_details.html +++ b/kraut_incident/templates/kraut_incident/incident_details.html @@ -142,7 +142,9 @@

{{ incident.title }} Add Incident Contact - Add Task + + Add Task + Add Asset

@@ -227,13 +229,16 @@

Incident Information
- + + {% for t in incident.tasks.all %} - - + + + + {% endfor %}
NameShort Description
NameStatusShort Description
{{ t.name }}{{ t.get_status_display }}{{ t.description }}
@@ -303,9 +308,11 @@

@@ -448,6 +483,39 @@ }); }); +$(document).ready( function () { + var ttTable = $('#ttask_table').DataTable({ + processing: true, + info: false, + bLengthChange: false, + serverSide: true, + oLanguage: { + sProcessing: "", + }, + order: [[ 1, "desc" ]], + ajax: { + processing: true, + url: "/api/incident/tasks/", + dataSrc: "results", + type: "GET", + dataType: "json" + }, + columns: [ + { + "data": 'id', + 'sName': 'operations', + 'bSortable': false, + 'aTargets': [ 1 ], + "mRender": function (data, type, full) { + return '
' + } + }, + {'data': 'name', 'sName': 'Name', 'aTargets': [ 2 ]}, + {'data': 'description', 'sName': 'Description', 'aTargets': [ 3 ]}, + ], + }); +}); + $(document).ready( function () { var cTable = $('#co_table').DataTable({ processing: true, @@ -472,7 +540,7 @@ 'bSortable': false, 'aTargets': [ 1 ], "mRender": function (data, type, full) { - return '
' + return '
' } }, {'data': 'lastname', 'sName': 'lastname', 'aTargets': [ 2 ]}, @@ -530,5 +598,26 @@ }); }); +$('#post-task').submit(function(){ + $('input[type=checkbox]').each(function () { + if (this.checked) { + $('').attr('type', 'hidden') + .attr('name', this.id) + .attr('value', this.value) + .appendTo('#post-task'); + } + }); +}); + +$('#post-contact').submit(function(){ + $('input[type=checkbox]').each(function () { + if (this.checked) { + $('').attr('type', 'hidden') + .attr('name', this.id) + .attr('value', this.value) + .appendTo('#post-contact'); + } + }); +}); {% endblock%} diff --git a/kraut_incident/urls.py b/kraut_incident/urls.py index 9ce4204..47a108d 100644 --- a/kraut_incident/urls.py +++ b/kraut_incident/urls.py @@ -10,6 +10,7 @@ url(r'^incident/(?P\d+)/update/header/$', views.update_incident_header, name='update_incident_header'), url(r'^incident/(?P\d+)/add/task/$', views.add_task, name='add_task'), url(r'^incident/(?P\d+)/add/handler/$', views.add_handler_incident, name='add_handler_incident'), + url(r'^incident/(?P\d+)/add/contact/$', views.add_contact_incident, name='add_contact_incident'), url(r'^incident/(?P\d+)/remove/handler/(?P\d+)/$', views.remove_handler_incident, name='remove_handler_incident'), url(r'^incident/(?P\d+)/remove/contact/(?P\d+)/$', views.remove_contact_incident, name='remove_contact_incident'), url(r'^incident/(?P\d+)/add/comment/$', views.comment_incident, name='comment_incident'), diff --git a/kraut_incident/views.py b/kraut_incident/views.py index e38a212..28fa92a 100644 --- a/kraut_incident/views.py +++ b/kraut_incident/views.py @@ -12,7 +12,7 @@ from kraut_intel.utils import get_icon_for_namespace from kraut_incident.forms import IncidentForm, ContactForm, HandlerForm, IncidentCommentForm -from kraut_incident.models import Contact, Handler, Incident, IncidentComment +from kraut_incident.models import Contact, Handler, Incident, IncidentComment, TemplateTask, Task from kraut_incident.utils import slicedict # Create your views here. @@ -65,6 +65,28 @@ def remove_handler_incident(request, incident_id, handler_id): inc.save() return HttpResponseRedirect(reverse("incidents:view_incident", kwargs={'incident_id': incident_id})) +@login_required +def add_contact_incident(request, incident_id): + """add incident contact to incident + """ + if request.method == 'POST': + try: + inc = Incident.objects.get(id=incident_id) + except Incident.DoesNotExist: + messages.error(request, 'The requested incident does not exist!') + return render(request, 'kraut_incident/incident_list.html', context) + co_dict = slicedict(request.POST, 'ContactCheckBox') + for key in co_dict: + co_id = int(co_dict[key]) + try: + co = Contact.objects.get(id=co_id) + inc.contacts.add(co) + except Contact.DoesNotExist: + messages.error(request, 'Failed getting incident contact with ID: %s' % (co_id)) + return HttpResponseRedirect(reverse("incidents:view_incident", kwargs={'incident_id': incident_id})) + inc.save() + return HttpResponseRedirect(reverse("incidents:view_incident", kwargs={'incident_id': incident_id})) + @login_required def add_handler_incident(request, incident_id): """add incident handler to incident @@ -91,6 +113,29 @@ def add_handler_incident(request, incident_id): def add_task(request, incident_id): """add task to incident """ + if request.method == 'POST': + try: + inc = Incident.objects.get(id=incident_id) + except Incident.DoesNotExist: + messages.error(request, 'The requested incident does not exist!') + return render(request, 'kraut_incident/incident_list.html', context) + task_dict = slicedict(request.POST, 'TaskCheckBox') + for key in task_dict: + task_id = int(task_dict[key]) + try: + tt = TemplateTask.objects.get(id=task_id) + template = { + "name": tt.name, + "description": tt.description + } + t = Task(**template) + t.save() + inc.tasks.add(t) + except: + messages.error(request, 'Failed getting incident task with ID: %s' % (task_id)) + return HttpResponseRedirect(reverse("incidents:view_incident", kwargs={'incident_id': incident_id})) + messages.info(request, 'Task "%s" added' % (t.name)) + inc.save() return HttpResponseRedirect(reverse("incidents:view_incident", kwargs={'incident_id': incident_id})) @login_required @@ -177,12 +222,7 @@ def view_incident(request, incident_id): return render(request, 'kraut_incident/incident_list.html', context) context['incident'] = inc context['severities'] = ['High', 'Medium', 'Low'] - if inc.severity == 'h': - context['severity'] = 'High' - elif inc.severity == 'm': - context['severity'] = 'Medium' - else: - context['severity'] = 'Low' + context['severity'] = inc.get_severity_display() if hasattr(request.user.userextension, 'namespaces'): context['usernamespace'] = request.user.userextension.namespaces.last().namespace.split(':')[0] context['namespaceicon'] = get_icon_for_namespace(request.user.userextension.namespaces.last().namespace)