Skip to content

Commit

Permalink
added open findings burndown for product metrics (#8558)
Browse files Browse the repository at this point in the history
* added open findings burndown for product metrics

* flake8

* flake8 again

* drastically reduced number of queries

* code optimizations

* made burndown plot async

* flake8 again
  • Loading branch information
blakeaowens authored Aug 29, 2023
1 parent 78f18fc commit 358367c
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 10 deletions.
2 changes: 2 additions & 0 deletions dojo/product/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
views.import_scan_results_prod, name='import_scan_results_prod'),
re_path(r'^product/(?P<pid>\d+)/metrics$', views.view_product_metrics,
name='view_product_metrics'),
re_path(r'^product/(?P<pid>\d+)/async_burndown_metrics$', views.async_burndown_metrics,
name='async_burndown_metrics'),
re_path(r'^product/(?P<pid>\d+)/edit$', views.edit_product,
name='edit_product'),
re_path(r'^product/(?P<pid>\d+)/delete$', views.delete_product,
Expand Down
20 changes: 18 additions & 2 deletions dojo/product/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from django.db.models import Sum, Count, Q, Max, Prefetch, F, OuterRef, Subquery
from django.db.models.query import QuerySet
from django.core.exceptions import ValidationError, PermissionDenied
from django.http import HttpResponseRedirect, Http404
from django.http import HttpResponseRedirect, Http404, JsonResponse
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.utils import timezone
Expand All @@ -39,7 +39,7 @@
from dojo.utils import add_external_issue, add_error_message_to_response, add_field_errors_to_response, get_page_items, \
add_breadcrumb, async_delete, \
get_system_setting, get_setting, Product_Tab, get_punchcard_data, queryset_check, is_title_in_breadcrumbs, \
get_enabled_notifications_list, get_zero_severity_level, sum_by_severity_level
get_enabled_notifications_list, get_zero_severity_level, sum_by_severity_level, get_open_findings_burndown

from dojo.notifications.helper import create_notification
from dojo.components.sql_group_concat import Sql_GroupConcat
Expand Down Expand Up @@ -673,6 +673,22 @@ def view_product_metrics(request, pid):
'user': request.user})


@user_is_authorized(Product, Permissions.Product_View, 'pid')
def async_burndown_metrics(request, pid):
prod = get_object_or_404(Product, id=pid)
open_findings_burndown = get_open_findings_burndown(prod)

return JsonResponse({
'critical': open_findings_burndown.get('Critical', []),
'high': open_findings_burndown.get('High', []),
'medium': open_findings_burndown.get('Medium', []),
'low': open_findings_burndown.get('Low', []),
'info': open_findings_burndown.get('Info', []),
'max': open_findings_burndown.get('y_max', 0),
'min': open_findings_burndown.get('y_min', 0)
})


@user_is_authorized(Product, Permissions.Engagement_View, 'pid')
def view_engagements(request, pid):
prod = get_object_or_404(Product, id=pid)
Expand Down
11 changes: 11 additions & 0 deletions dojo/static/dojo/css/dojo.css
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,17 @@ div.custom-search-form {
border-left: 1px solid #dcdedf;
}

.graph {
min-height: 158px;
}

.graph-loader {
min-height: 158px;
display: flex;
flex-direction: column;
justify-content: center;
}

.panel-footer {
background-color: #dddedf;
border-top: 1px solid #ddd;
Expand Down
55 changes: 55 additions & 0 deletions dojo/static/dojo/js/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,61 @@ function accepted_per_week_2(critical, high, medium, low) {
product_metrics.html
*/

function open_findings_burndown(critical, high, medium, low, info, y_max, y_min) {
var options = {
xaxes: [{
mode: "time",
timeformat: "%Y/%m/%d"
}],
yaxes: [{
max: y_max,
min: y_min
}],
series: {
lines: {
show: true
},
points: {
show: true,
radius: 1
}
},
grid: {
hoverable: true,
borderWidth: 1,
borderColor: '#e7e7e7',

},
legend: {
position: 'nw'
},
tooltip: true,
};

var plotObj = $.plot($("#open_findings_burndown"), [{
data: critical,
label: " Critical",
color: "#d9534f",
}, {
data: high,
label: " High",
color: '#f0ad4e',
}, {
data: medium,
label: " Medium",
color: '#f0de28',
}, {
data: low,
label: " Low",
color: '#4cae4c',
}, {
data: info,
label: " Info",
color: '#337ab7',
}],
options);
}

function accepted_objs(d1, d2, d3, d4, d5, ticks) {
var data = [
{
Expand Down
51 changes: 49 additions & 2 deletions dojo/templates/dojo/product_metrics.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
{% load static %}
{% block add_styles %}
{{ block.super }}
.graph {min-height: 158px;}
{% endblock %}
{% block content %}
{{ block.super }}
Expand Down Expand Up @@ -290,7 +289,27 @@ <h3 class="pull-left">
</div>
</div>
</div>
<div class="panel-body product-graphs">
<div class="panel-body product-graphs">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
Open Day to Day by Severity
<i title="Days are only displayed if findings are available."
class="text-info fa-solid fa-circle-info"></i>
</div>
<div class="panel-body text-center">
<div id="id_open_findings_burndown_loader" class="graph-loader">
<p><i class="fas fa-spinner fa-spin fa-2x"></i></p>
<p>Loading Finding Burndown Metrics...</p>
</div>
<div id="open_findings_burndown" class="graph"></div>
</div>
<div class="panel-footer">
<i class="text-info fa-solid fa-circle-info"></i>
<small>Days are only displayed if findings are available.</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
Expand Down Expand Up @@ -515,6 +534,34 @@ <h3 class="pull-left">
[0, "Critical"], [1, "High"], [2, "Medium"], [3, "Low"], [4, "Info"]
];

$(document).ready(function() {
fetch_burndown_metrics({{ prod.id }});
})

function fetch_burndown_metrics(pid) {
$.ajax({
type: 'GET',
url: '/product/' + pid + '/async_burndown_metrics',
beforeSend: function() {
$('#open_findings_burndown').hide();
$('#id_open_findings_burndown_loader').show();
},
success: function(response) {
$('#open_findings_burndown').show();
$('#id_open_findings_burndown_loader').hide();
open_findings_burndown(
response.critical,
response.high,
response.medium,
response.low,
response.info,
response.max,
response.min,
);
}
});
}

accepted_objs(
[[0, {{ accepted_objs_by_severity.Critical }}]],
[[1, {{ accepted_objs_by_severity.High }}]],
Expand Down
85 changes: 79 additions & 6 deletions dojo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from calendar import monthrange
from datetime import date, datetime
from datetime import date, datetime, timedelta
from math import pi, sqrt
import vobject
from dateutil.relativedelta import relativedelta, MO, SU
Expand Down Expand Up @@ -1573,7 +1573,6 @@ def get_work_days(start: date, end: date):
about specific country holidays or extra working days.
https://stackoverflow.com/questions/3615375/number-of-days-between-2-dates-excluding-weekends/71977946#71977946
"""
from datetime import timedelta

# if the start date is on a weekend, forward the date to next Monday
if start.weekday() > WEEKDAY_FRIDAY:
Expand Down Expand Up @@ -2193,16 +2192,16 @@ def get_product(obj):
if not obj:
return None

if type(obj) == Finding or type(obj) == Finding_Group:
if isinstance(obj, Finding) or isinstance(obj, Finding_Group):
return obj.test.engagement.product

if type(obj) == Test:
if isinstance(obj, Test):
return obj.engagement.product

if type(obj) == Engagement:
if isinstance(obj, Engagement):
return obj.product

if type(obj) == Product:
if isinstance(obj, Product):
return obj


Expand Down Expand Up @@ -2393,3 +2392,77 @@ def sum_by_severity_level(metrics):
values[m.severity] += 1

return values


def get_open_findings_burndown(product):
findings = Finding.objects.filter(test__engagement__product=product)
f_list = list(findings)

curr_date = datetime.combine(datetime.now(), datetime.min.time())
start_date = curr_date - timedelta(days=90)

critical_count = len(list(findings.filter(date__lt=start_date).filter(severity='Critical')))
high_count = len(list(findings.filter(date__lt=start_date).filter(severity='High')))
medium_count = len(list(findings.filter(date__lt=start_date).filter(severity='Medium')))
low_count = len(list(findings.filter(date__lt=start_date).filter(severity='Low')))
info_count = len(list(findings.filter(date__lt=start_date).filter(severity='Info')))

running_min, running_max = float('inf'), float('-inf')
past_90_days = {
'Critical': [],
'High': [],
'Medium': [],
'Low': [],
'Info': []
}

for i in range(90, -1, -1):
start = (curr_date - timedelta(days=i))

d_start = start.timestamp()
d_end = (start + timedelta(days=1)).timestamp()

for f in f_list:
f_open_date = datetime.combine(f.date, datetime.min.time()).timestamp()
if f_open_date >= d_start and f_open_date < d_end:
if f.severity == 'Critical':
critical_count += 1
if f.severity == 'High':
high_count += 1
if f.severity == 'Medium':
medium_count += 1
if f.severity == 'Low':
low_count += 1
if f.severity == 'Info':
info_count += 1

if f.is_mitigated:
f_mitigated_date = f.mitigated.timestamp()
if f_mitigated_date >= d_start and f_mitigated_date < d_end:
if f.severity == 'Critical':
critical_count -= 1
if f.severity == 'High':
high_count -= 1
if f.severity == 'Medium':
medium_count -= 1
if f.severity == 'Low':
low_count -= 1
if f.severity == 'Info':
info_count -= 1

f_day = [critical_count, high_count, medium_count, low_count, info_count]
if min(f_day) < running_min:
running_min = min(f_day)
if max(f_day) > running_max:
running_max = max(f_day)

past_90_days['Critical'].append([d_start * 1000, critical_count])
past_90_days['High'].append([d_start * 1000, high_count])
past_90_days['Medium'].append([d_start * 1000, medium_count])
past_90_days['Low'].append([d_start * 1000, low_count])
past_90_days['Info'].append([d_start * 1000, info_count])

past_90_days['y_max'] = running_max
past_90_days['y_min'] = running_min

return past_90_days

0 comments on commit 358367c

Please sign in to comment.