diff --git a/writehat/lib/statistic.py b/writehat/lib/statistic.py new file mode 100644 index 0000000..ffcae63 --- /dev/null +++ b/writehat/lib/statistic.py @@ -0,0 +1,112 @@ +import logging +import json + +log = logging.getLogger(__name__) + +from writehat.lib.engagement import * +from writehat.lib.findingCategory import * +from writehat.lib.cvss import * +from writehat.lib.dread import * + +def severity_statistics(startDate, endDate): + log.debug('Getting severity statistics') + labels = ['Informational', 'Low', 'Medium', 'High', 'Critical', 'Total'] + informational, low, medium, high, critical, total = 0,0,0,0,0,0 + cvss_findings = CVSSEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]) + dread_findings = DREADEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]) + proactive_findings = ProactiveEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]).count() + + for finding in cvss_findings: + total+=1 + cvss = CVSS(finding.vector) + if cvss.severity == "Informational": + informational+=1 + elif cvss.severity == "Low": + low+=1 + elif cvss.severity == "Medium": + medium+=1 + elif cvss.severity == "High": + high+=1 + else: + critical+=1 + cvss_data = [informational, low, medium, high, critical, total] + informational, low, medium, high, critical, total = 0,0,0,0,0,0 + for finding in dread_findings: + total+=1 + dread = DREAD(finding.vector) + if dread.severity == "Informational": + informational+=1 + elif dread.severity == "Low": + low+=1 + elif dread.severity == "Medium": + medium+=1 + elif dread.severity == "High": + high+=1 + else: + critical+=1 + dread_data = [informational, low, medium, high, critical, total] + + proactive_data = [0, 0, 0, 0, 0, proactive_findings] + return labels, cvss_data, dread_data, proactive_data + +def category_statistics(startDate, endDate): + log.debug('Getting category statistics') + labels, data, category_uuids = [], [], [] + categories = DatabaseFindingCategory.objects.all() + for category in categories: + if category.name != "root": + labels.append(category.name) + category_uuids.append(category.id) + for uuid in category_uuids: + categoryCount = DREADEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]).filter(categoryID=uuid).count() + categoryCount += CVSSEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]).filter(categoryID=uuid).count() + categoryCount += ProactiveEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]).filter(categoryID=uuid).count() + data.append(categoryCount) + + return labels, data + +def customer_statistics(startDate, endDate): + labels, data, engagements = [], [], [] + log.debug('Getting customer statistics') + customers = Customer.objects.all() + for customer in customers: + findingCount=0 + engagements = Engagement.objects.filter(customerID=customer.id) + for engagement in engagements: + findingGroups = BaseFindingGroup.objects.filter(engagementParent=engagement.id) + for findingGroup in findingGroups: + fc = CVSSEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]).filter(findingGroup=findingGroup.id).count() + fc += DREADEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]).filter(findingGroup=findingGroup.id).count() + fc += ProactiveEngagementFinding.objects.filter(createdDate__range=[startDate, endDate]).filter(findingGroup=findingGroup.id).count() + findingCount+=fc + + # don't append customers to the list which have no vulnerabilities + if findingCount != 0: + labels.append(customer.name) + data.append(findingCount) + labels = [x for _, x in sorted(zip(data, labels), reverse=True)] + data = sorted(data, reverse=True) + x = labels[:9] + y = data[:9] + if sum(data[9:]) != 0: + x.append("Other") + y.append(sum(data[9:])) + labels,data = x,y + + return labels, data + +def engagement_statistics(startDate, endDate): + log.debug('Getting engagement statistics (count)') + data = 0 + data = Engagement.objects.filter(createdDate__range=[startDate, endDate]).count() + return data + +def engagement_name_statistics(startDate, endDate): + log.debug('Getting engagement statistics (by name)') + data = [] + engagements = Engagement.objects.filter(createdDate__range=[startDate, endDate]) + for engagement in engagements: + data.append(engagement.name) + if not data: + data.append("No engagements started in the specified time window") + return data \ No newline at end of file diff --git a/writehat/static/js/calendar.js b/writehat/static/js/calendar.js new file mode 100644 index 0000000..34bb77a --- /dev/null +++ b/writehat/static/js/calendar.js @@ -0,0 +1,25 @@ +var p = new URLSearchParams(window.location.search); +if (p.get("startDate") || p.get("endDate")) { + var startDate = p.get("startDate") + var endDate = p.get("endDate") +} else { + const date = new Date(); + var startDate = `1-${date.getMonth() + 1}-${date.getFullYear()}` + var endDate = `${new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()}-${date.getMonth() + 1}-${date.getFullYear()}` +} + +const picker = new Litepicker({ + element: document.getElementById('litepicker'), + format: 'D-M-YYYY', + singleMode: false, + lang: 'en-GB', + startDate: startDate, + endDate: endDate, + setup: (picker) => { + picker.on('selected', (date1, date2) => { + document.getElementById("startDate").value = `${date1.getDate()}-${(date1.getMonth() + 1)}-${date1.getFullYear()}`; + document.getElementById("endDate").value = `${date2.getDate()}-${(date2.getMonth() + 1)}-${date2.getFullYear()}`; + document.getElementById("statisticsForm").submit(); + }); + }, +}); \ No newline at end of file diff --git a/writehat/static/js/statistics.js b/writehat/static/js/statistics.js new file mode 100644 index 0000000..f8412ba --- /dev/null +++ b/writehat/static/js/statistics.js @@ -0,0 +1,230 @@ +$(function() { + var $severityStatistics = $("#severity_statistics"); + var $categoryStatistics = $("#category_statistics"); + var $customerStatistics = $("#customer_statistics"); + var $p = new URLSearchParams(window.location.search); + var dreadBackgroundColors = [ + pattern.draw('diagonal-right-left', 'rgba(0, 98, 255, 0.7)'), + pattern.draw('diagonal-right-left', 'rgba(255, 165, 0, 1)'), + pattern.draw('diagonal-right-left', 'rgba(255, 58, 0, 0.8)'), + pattern.draw('diagonal-right-left', 'rgba(255, 0, 0, 1)'), + pattern.draw('diagonal-right-left', 'rgba(195, 0, 255, 1)'), + pattern.draw('diagonal-right-left', 'rgba(60, 180, 108, 1)'), + ]; + var proactiveBackgroundColors = [ + pattern.draw('line', 'rgba(0, 98, 255, 0.7)'), + pattern.draw('line', 'rgba(255, 165, 0, 1)'), + pattern.draw('line', 'rgba(255, 58, 0, 0.8)'), + pattern.draw('line', 'rgba(255, 0, 0, 1)'), + pattern.draw('line', 'rgba(195, 0, 255, 1)'), + pattern.draw('line', 'rgba(60, 180, 108, 1)'), + ]; + $.ajax({ + url: "get_statistics?startDate=" + $p.get("startDate") + "&endDate=" + $p.get("endDate"), + type: "get", + success: function(data) { + $("h1").text(data.engagement_data); + $.each(data.engagement_name_data, function(index, value) { + $('
  • '+ value + '
  • ').appendTo('#engagement_names'); + }); + var severityCtx = $severityStatistics[0].getContext("2d"); + var categoryCtx = $categoryStatistics[0].getContext("2d"); + var customerCtx = $customerStatistics[0].getContext("2d"); + Chart.register(ChartDataLabels); + new Chart(severityCtx, { + type: 'bar', + data: { + labels: data.cvss_severity_labels, + datasets: [{ + label: 'CVSS', + backgroundColor: ['rgba(0, 98, 255, 0.7)', 'rgba(255, 165, 0, 1)', 'rgba(255, 58, 0, 0.8)', 'rgba(255, 0, 0, 1)', 'rgba(195, 0, 255, 1)', 'rgba(60, 180, 108, 1)'], + data: data.cvss_severity_data, + }, + { + label: 'DREAD', + backgroundColor: dreadBackgroundColors, + data: data.dread_severity_data + }, + { + label: 'PROACTIVE', + backgroundColor: proactiveBackgroundColors, + data: data.proactive_severity_data + }], + }, + options: { + layout: { + padding: { + left: 5, + right: 5, + top: 50, + bottom: 5 + } + }, + responsive: true, + scales: { + x: { + ticks: { + color: 'white', + font: { + size: 14 + } + } + }, + y: { + stepSize: 1, + ticks: { + color: 'white', + callback: function(value) {if (value % 1 === 0) {return value;}}, + font: { + size: 14 + } + } + } + }, + plugins: { + legend: { + position: 'bottom' + }, + tooltip: { + enabled: false + }, + datalabels: { + anchor: 'end', + align: 'end', + offset: 5, + color: 'white', + formatter: function (value, context) { return value || null; }, + font: { + weight: 'bold', + size: 16 + } + } + } + } + }), + new Chart(categoryCtx, { + type: 'bar', + data: { + labels: data.category_labels, + datasets: [{ + label: 'Total', + backgroundColor: ['rgba(0, 98, 255, 0.7)'], + data: data.category_data + }] + }, + options: { + indexAxis: 'y', + layout: { + padding: { + left: 5, + right: 50, + top: 5, + bottom: 5 + } + }, + responsive: true, + scales: { + x: { + ticks: { + color: 'white', + callback: function(value) {if (value % 1 === 0) {return value;}}, + font: { + size: 14 + } + } + }, + y: { + ticks: { + color: 'white', + font: { + size: 14 + } + } + } + }, + plugins: { + legend: { + display: false + }, + tooltip: { + enabled: false + }, + datalabels: { + anchor: 'end', + align: 'end', + offset: 5, + color: 'white', + formatter: function (value, context) { return value || null; }, + font: { + weight: 'bold', + size: 16 + } + } + } + } + } + ),new Chart(customerCtx, { + type: 'bar', + data: { + labels: data.customer_labels, + datasets: [{ + label: 'Total', + backgroundColor: ['rgba(0, 98, 255, 0.7)'], + data: data.customer_data + }] + }, + options: { + indexAxis: 'y', + layout: { + padding: { + left: 5, + right: 50, + top: 5, + bottom: 5 + } + }, + responsive: true, + scales: { + x: { + ticks: { + color: 'white', + callback: function(value) {if (value % 1 === 0) {return value;}}, + font: { + size: 14 + } + } + }, + y: { + ticks: { + color: 'white', + font: { + size: 14 + } + } + } + }, + plugins: { + legend: { + display: false + }, + tooltip: { + enabled: false + }, + datalabels: { + anchor: 'end', + align: 'end', + offset: 5, + color: 'white', + formatter: function (value, context) { return value || null; }, + font: { + weight: 'bold', + size: 16 + } + } + } + } + } + ); + } + }); +}); \ No newline at end of file diff --git a/writehat/templates/layouts/nav.html b/writehat/templates/layouts/nav.html index 98f3a70..d154690 100644 --- a/writehat/templates/layouts/nav.html +++ b/writehat/templates/layouts/nav.html @@ -45,6 +45,10 @@ {% endif %} + + \ No newline at end of file diff --git a/writehat/templates/pages/statistics.html b/writehat/templates/pages/statistics.html new file mode 100644 index 0000000..838194c --- /dev/null +++ b/writehat/templates/pages/statistics.html @@ -0,0 +1,33 @@ +{% extends 'layouts/base.html' %} + +{% block title %}Statistics{% endblock %} + +{% block headerLeft %} + Statistics +{% endblock %} + +{% block headerRight %} + + + + +
    + + +
    + + + +{% endblock %} + +{% block fullPane %} + + + + + + + +{% include 'panes/statistics.html' %} + +{% endblock %} \ No newline at end of file diff --git a/writehat/templates/panes/statistics.html b/writehat/templates/panes/statistics.html new file mode 100644 index 0000000..3b37c34 --- /dev/null +++ b/writehat/templates/panes/statistics.html @@ -0,0 +1,37 @@ +
    +
    +
    +
    Engagements Started (Count)
    +

    +
    +
    +
    +
    +
    Engagements Started (By Name)
    +
    +
    +
      +
    +
    +
    +
    +
    +
    Vulnerabilities by Severity
    +
    + +
    +
    +
    +
    +
    +
    Vulnerabilities by Category
    +
    + +
    +
    +
    +
    Vulnerabilities by Requestor
    +
    + +
    +
    \ No newline at end of file diff --git a/writehat/urls.py b/writehat/urls.py index 3419dc2..14ad59d 100644 --- a/writehat/urls.py +++ b/writehat/urls.py @@ -36,6 +36,10 @@ #url(rf'^images/test/{uuid}$', views.imageTest), + # Statistics URLs + path('statistics', views.statistics, name='statistics'), + path('get_statistics', views.getStatistics, name='get_statistics'), + # findings urls path('findings', views.findingsList), diff --git a/writehat/views.py b/writehat/views.py index 9781f89..c42a7b7 100644 --- a/writehat/views.py +++ b/writehat/views.py @@ -4,6 +4,7 @@ import base64 import logging import uuid as uuidlib +import calendar # django from django.conf import settings @@ -42,6 +43,7 @@ from writehat.lib.findingCategory import * from writehat.lib.engagementFinding import * from writehat.lib.excel import generateExcel +from writehat.lib.statistic import * # Selenium @@ -71,6 +73,46 @@ def index(request): return HttpResponseRedirect('/engagements') +@require_http_methods(['GET']) +@csrf_exempt +def statistics(request): + return render(request,"pages/statistics.html", {}) + +# stats dashboard +@require_http_methods(['GET']) +@csrf_exempt +def getStatistics(request): + try: + startDate = datetime.strptime(request.GET.get("startDate"), '%d-%m-%Y') + endDate = datetime.strptime(request.GET.get("endDate"), '%d-%m-%Y') + except ValueError: + log.error("invalid or null timestamp provided, defaulting to current months statistics") + today = datetime.now() + startDate = datetime.strptime(f'1-{today.month}-{today.year}', '%d-%m-%Y') + endDate = datetime.strptime(f'{calendar.monthrange(today.year, today.month)[1]}-{today.month}-{today.year}', '%d-%m-%Y') + + severity_labels, cvss_severity_data, dread_severity_data, proactive_severity_data = severity_statistics(startDate, endDate) + category_labels, category_data = category_statistics(startDate, endDate) + customer_labels, customer_data = customer_statistics(startDate, endDate) + engagement_data = engagement_statistics(startDate, endDate) + engagement_name_data = engagement_name_statistics(startDate, endDate) + + return JsonResponse(data={ + 'cvss_severity_labels': severity_labels, + 'cvss_severity_data': cvss_severity_data, + 'dread_severity_labels': severity_labels, + 'dread_severity_data': dread_severity_data, + 'proactive_severity_labels': severity_labels, + 'proactive_severity_data': proactive_severity_data, + 'category_labels': category_labels, + 'category_data': category_data, + 'customer_labels': customer_labels, + 'customer_data':customer_data, + 'engagement_labels':'Engagements', + 'engagement_data':engagement_data, + 'engagement_name_label':'Engagement Names', + 'engagement_name_data':engagement_name_data, + }) # Validation - returns allowed characters @require_http_methods(['GET'])