Skip to content

Commit cd18071

Browse files
author
Greg Anderson
authored
Merge pull request #158 from grendel513/master
Takes care of #64:
2 parents 8127fdc + 67202ed commit cd18071

File tree

16 files changed

+338
-21
lines changed

16 files changed

+338
-21
lines changed

dojo/api.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from tastypie.validation import CleanedDataFormValidation
1111

1212
from dojo.models import Product, Engagement, Test, Finding, \
13-
User, ScanSettings, IPScan, Scan, Stub_Finding, Risk_Acceptance
13+
User, ScanSettings, IPScan, Scan, Stub_Finding, Risk_Acceptance, Finding_Template
1414
from dojo.forms import ProductForm, EngForm2, TestForm, \
15-
ScanSettingsForm, FindingForm, StubFindingForm
15+
ScanSettingsForm, FindingForm, StubFindingForm, FindingTemplateForm
1616

1717
"""
1818
Setup logging for the api
@@ -420,7 +420,7 @@ class FindingResource(BaseModelResource):
420420
class Meta:
421421
resource_name = 'findings'
422422
queryset = Finding.objects.select_related("test")
423-
# deleting of findings is not allowed via UI or API.
423+
# deleting of findings is not allowed via API.
424424
# Admin interface can be used for this.
425425
list_allowed_methods = ['get', 'post']
426426
detail_allowed_methods = ['get', 'post', 'put']
@@ -458,6 +458,55 @@ def dehydrate(self, bundle):
458458
"/api/v1/products/%s/" % engagement[0].product.id
459459
return bundle
460460

461+
"""
462+
/api/v1/finding_templates/
463+
GET [/id/], DELETE [/id/]
464+
Expects: no params or test_id
465+
Returns test: ALL or by test_id
466+
Relevant apply filter ?active=True, ?id=?, ?severity=?
467+
468+
POST, PUT [/id/]
469+
Expects *title, *severity, *description, *mitigation, *impact,
470+
*endpoint, *test, cwe, active, false_p, verified,
471+
mitigated, *reporter
472+
473+
"""
474+
475+
476+
class FindingTemplateResource(BaseModelResource):
477+
478+
class Meta:
479+
resource_name = 'finding_templates'
480+
queryset = Finding_Template.objects.all()
481+
excludes= ['numerical_severity']
482+
# deleting of Finding_Template is not allowed via API.
483+
# Admin interface can be used for this.
484+
list_allowed_methods = ['get', 'post']
485+
detail_allowed_methods = ['get', 'post', 'put']
486+
include_resource_uri = True
487+
"""
488+
title = models.TextField(max_length=1000)
489+
cwe = models.IntegerField(default=None, null=True, blank=True)
490+
severity = models.CharField(max_length=200, null=True, blank=True)
491+
description = models.TextField(null=True, blank=True)
492+
mitigation = models.TextField(null=True, blank=True)
493+
impact = models.TextField(null=True, blank=True)
494+
references = models.TextField(null=True, blank=True, db_column="refs")
495+
numerical_severity
496+
"""
497+
filtering = {
498+
'id': ALL,
499+
'title': ALL,
500+
'cwe': ALL,
501+
'severity': ALL,
502+
'description': ALL,
503+
'mitigated': ALL,
504+
}
505+
authentication = DojoApiKeyAuthentication()
506+
authorization = DjangoAuthorization()
507+
serializer = Serializer(formats=['json'])
508+
validation = CleanedDataFormValidation(form_class=FindingTemplateForm)
509+
461510

462511
class StubFindingResource(BaseModelResource):
463512
reporter = fields.ForeignKey(UserResource, 'reporter', null=False)

dojo/finding/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
views.edit_finding, name='edit_finding'),
1919
url(r'^finding/(?P<fid>\d+)/touch',
2020
views.touch_finding, name='touch_finding'),
21+
url(r'^finding/(?P<fid>\d+)/request_review',
22+
views.request_finding_review, name='request_finding_review'),
23+
url(r'^finding/(?P<fid>\d+)/review',
24+
views.clear_finding_review, name='clear_finding_review'),
2125
url(r'^finding/(?P<fid>\d+)/delete$',
2226
views.delete_finding, name='delete_finding'),
2327
url(r'^finding/(?P<fid>\d+)/mktemplate$', views.mktemplate,

dojo/finding/views.py

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
OpenFingingSuperFilter, AcceptedFingingSuperFilter, \
2424
ClosedFingingSuperFilter, TemplateFindingFilter
2525
from dojo.forms import NoteForm, CloseFindingForm, FindingForm, PromoteFindingForm, FindingTemplateForm, \
26-
DeleteFindingTemplateForm, FindingImageFormSet
26+
DeleteFindingTemplateForm, FindingImageFormSet, ReviewFindingForm, ClearFindingReviewForm
2727
from dojo.models import Product_Type, Finding, Notes, \
2828
Risk_Acceptance, BurpRawRequestResponse, Stub_Finding, Endpoint, Finding_Template, FindingImage, \
29-
FindingImageAccessToken
30-
from dojo.utils import get_page_items, add_breadcrumb, FileIterWrapper
29+
FindingImageAccessToken, Dojo_User
30+
from dojo.utils import get_page_items, add_breadcrumb, FileIterWrapper, send_review_email
3131

3232
localtz = timezone(settings.TIME_ZONE)
3333

@@ -137,7 +137,9 @@ def closed_findings(request):
137137

138138
def view_finding(request, fid):
139139
finding = get_object_or_404(Finding, id=fid)
140+
140141
user = request.user
142+
dojo_user = get_object_or_404(Dojo_User, id=user.id)
141143
if user.is_staff or user in finding.test.engagement.product.authorized_users.all():
142144
pass # user is authorized for this product
143145
else:
@@ -177,7 +179,7 @@ def view_finding(request, fid):
177179
return render(request, 'dojo/view_finding.html',
178180
{'finding': finding,
179181
'burp_request': burp_request,
180-
'burp_response': burp_response,
182+
'burp_response': burp_response, 'dojo_user': dojo_user,
181183
'user': user, 'notes': notes, 'form': form})
182184

183185

@@ -340,6 +342,101 @@ def touch_finding(request, fid):
340342
return HttpResponseRedirect(reverse('view_finding', args=(finding.id,)))
341343

342344

345+
@user_passes_test(lambda u: u.is_staff)
346+
def request_finding_review(request, fid):
347+
finding = get_object_or_404(Finding, id=fid)
348+
user = get_object_or_404(Dojo_User, id=request.user.id)
349+
# in order to review a finding, we need to capture why a review is needed
350+
# we can do this with a Note
351+
if request.method == 'POST':
352+
form = ReviewFindingForm(request.POST)
353+
354+
if form.is_valid():
355+
now = datetime.now(tz=localtz)
356+
new_note = Notes()
357+
358+
new_note.entry = "Review Request: " + form.cleaned_data['entry']
359+
new_note.author = request.user
360+
new_note.date = now
361+
new_note.save()
362+
finding.notes.add(new_note)
363+
finding.active = False
364+
finding.verified = False
365+
finding.under_review = True
366+
finding.review_requested_by = user
367+
finding.last_reviewed = now
368+
finding.last_reviewed_by = request.user
369+
370+
users = form.cleaned_data['reviewers']
371+
finding.reviewers = users
372+
finding.save()
373+
374+
send_review_email(request, user, finding, users, new_note)
375+
376+
messages.add_message(request,
377+
messages.SUCCESS,
378+
'Finding marked for review and reviewers notified.',
379+
extra_tags='alert-success')
380+
return HttpResponseRedirect(reverse('view_finding', args=(finding.id,)))
381+
382+
else:
383+
form = ReviewFindingForm()
384+
385+
add_breadcrumb(parent=finding, title="Review Finding", top_level=False, request=request)
386+
return render(request, 'dojo/review_finding.html',
387+
{'finding': finding,
388+
'user': user, 'form': form})
389+
390+
391+
@user_passes_test(lambda u: u.is_staff)
392+
def clear_finding_review(request, fid):
393+
finding = get_object_or_404(Finding, id=fid)
394+
user = get_object_or_404(Dojo_User, id=request.user.id)
395+
# in order to clear a review for a finding, we need to capture why and how it was reviewed
396+
# we can do this with a Note
397+
398+
if user == finding.review_requested_by or user in finding.reviewers.all():
399+
pass
400+
else:
401+
return HttpResponseForbidden()
402+
403+
if request.method == 'POST':
404+
form = ClearFindingReviewForm(request.POST, instance=finding)
405+
406+
if form.is_valid():
407+
now = datetime.now(tz=localtz)
408+
new_note = Notes()
409+
new_note.entry = "Review Cleared: " + form.cleaned_data['entry']
410+
new_note.author = request.user
411+
new_note.date = now
412+
new_note.save()
413+
414+
finding = form.save(commit=False)
415+
416+
finding.under_review = False
417+
finding.last_reviewed = now
418+
finding.last_reviewed_by = request.user
419+
420+
finding.reviewers = []
421+
finding.save()
422+
423+
finding.notes.add(new_note)
424+
425+
messages.add_message(request,
426+
messages.SUCCESS,
427+
'Finding review has been updated successfully.',
428+
extra_tags='alert-success')
429+
return HttpResponseRedirect(reverse('view_finding', args=(finding.id,)))
430+
431+
else:
432+
form = ClearFindingReviewForm(instance=finding)
433+
434+
add_breadcrumb(parent=finding, title="Clear Finding Review", top_level=False, request=request)
435+
return render(request, 'dojo/clear_finding_review.html',
436+
{'finding': finding,
437+
'user': user, 'form': form})
438+
439+
343440
@user_passes_test(lambda u: u.is_staff)
344441
def mktemplate(request, fid):
345442
finding = get_object_or_404(Finding, id=fid)

dojo/forms.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ def __init__(self, *args, **kwargs):
178178
self.fields['authorized_users'].queryset = non_staff
179179
self.fields['tags'].widget.choices = t
180180

181-
182181
class Meta:
183182
model = Product
184183
fields = ['name', 'description', 'tags', 'product_manager', 'technical_contact', 'team_manager', 'prod_type',
@@ -220,7 +219,8 @@ def __init__(self, *args, **kwargs):
220219

221220
class Meta:
222221
model = Product
223-
fields = ['name', 'description', 'product_manager', 'technical_contact', 'team_manager', 'prod_type', 'authorized_users']
222+
fields = ['name', 'description', 'product_manager', 'technical_contact', 'team_manager', 'prod_type',
223+
'authorized_users']
224224

225225

226226
class ImportScanForm(forms.Form):
@@ -582,7 +582,8 @@ def clean(self):
582582
class Meta:
583583
model = Finding
584584
order = ('title', 'severity', 'endpoints', 'description', 'impact')
585-
exclude = ('reporter', 'url', 'numerical_severity', 'endpoint', 'images')
585+
exclude = ('reporter', 'url', 'numerical_severity', 'endpoint', 'images', 'under_review', 'reviewers',
586+
'review_requested_by')
586587

587588

588589
class PromoteFindingForm(forms.ModelForm):
@@ -609,7 +610,7 @@ class Meta:
609610
model = Finding
610611
order = ('title', 'severity', 'endpoints', 'description', 'impact')
611612
exclude = ('reporter', 'url', 'numerical_severity', 'endpoint', 'active', 'false_p', 'verified', 'is_template',
612-
'duplicate', 'out_of_scope', 'images')
613+
'duplicate', 'out_of_scope', 'images', 'under_review', 'reviewers', 'review_requested_by')
613614

614615

615616
class FindingForm(forms.ModelForm):
@@ -657,7 +658,8 @@ def clean(self):
657658
class Meta:
658659
model = Finding
659660
order = ('title', 'severity', 'endpoints', 'description', 'impact')
660-
exclude = ('reporter', 'url', 'numerical_severity', 'endpoint', 'images')
661+
exclude = ('reporter', 'url', 'numerical_severity', 'endpoint', 'images', 'under_review', 'reviewers',
662+
'review_requested_by')
661663

662664

663665
class StubFindingForm(forms.ModelForm):
@@ -730,7 +732,7 @@ def clean(self):
730732

731733
class Meta:
732734
model = Finding
733-
fields = ('severity', 'active', 'verified', 'false_p','duplicate','out_of_scope')
735+
fields = ('severity', 'active', 'verified', 'false_p', 'duplicate', 'out_of_scope')
734736

735737

736738
class EditEndpointForm(forms.ModelForm):
@@ -956,6 +958,35 @@ class Meta:
956958
fields = ['entry']
957959

958960

961+
class ClearFindingReviewForm(forms.ModelForm):
962+
entry = forms.CharField(
963+
required=True, max_length=2400,
964+
help_text='Please provide a message.',
965+
widget=forms.Textarea, label='Notes:',
966+
error_messages={'required': ('The reason for clearing a review is '
967+
'required, please use the text area '
968+
'below to provide documentation.')})
969+
970+
class Meta:
971+
model = Finding
972+
fields = ['active', 'verified', 'false_p', 'out_of_scope', 'duplicate']
973+
974+
975+
class ReviewFindingForm(forms.Form):
976+
reviewers = forms.ModelMultipleChoiceField(queryset=Dojo_User.objects.filter(is_staff=True, is_active=True),
977+
help_text="Select all users who can review Finding.")
978+
entry = forms.CharField(
979+
required=True, max_length=2400,
980+
help_text='Please provide a message for reviewers.',
981+
widget=forms.Textarea, label='Notes:',
982+
error_messages={'required': ('The reason for requesting a review is '
983+
'required, please use the text area '
984+
'below to provide documentation.')})
985+
986+
class Meta:
987+
fields = ['reviewers', 'entry']
988+
989+
959990
class WeeklyMetricsForm(forms.Form):
960991
dates = forms.ChoiceField()
961992

dojo/models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,9 @@ class Finding(models.Model):
531531
false_p = models.BooleanField(default=False, verbose_name="False Positive")
532532
duplicate = models.BooleanField(default=False)
533533
out_of_scope = models.BooleanField(default=False)
534+
under_review = models.BooleanField(default=False)
535+
review_requested_by = models.ForeignKey(Dojo_User, null=True, blank=True, related_name='review_requested_by')
536+
reviewers = models.ManyToManyField(Dojo_User, blank=True)
534537
thread_id = models.IntegerField(default=0, editable=False)
535538
mitigated = models.DateTimeField(editable=False, null=True, blank=True)
536539
mitigated_by = models.ForeignKey(User, null=True, editable=False, related_name="mitigated_by")
@@ -684,7 +687,7 @@ class Finding_Template(models.Model):
684687
mitigation = models.TextField(null=True, blank=True)
685688
impact = models.TextField(null=True, blank=True)
686689
references = models.TextField(null=True, blank=True, db_column="refs")
687-
numerical_severity = models.CharField(max_length=4, null=True, blank=True)
690+
numerical_severity = models.CharField(max_length=4, null=True, blank=True, editable=False)
688691

689692
SEVERITIES = {'Info': 4, 'Low': 3, 'Medium': 2,
690693
'High': 1, 'Critical': 0}
@@ -867,6 +870,7 @@ def save(self, *args, **kwargs):
867870
auditlog.register(Product)
868871
auditlog.register(Test)
869872
auditlog.register(Risk_Acceptance)
873+
auditlog.register(Finding_Template)
870874

871875
# Register tagging for models
872876
tag_register(Product)

dojo/reports/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ def report_url_resolver(request):
4444
url_resolver = request.META['HTTP_X_FORWARDED_PROTO'] + "://" + request.META['HTTP_X_FORWARDED_FOR']
4545
except:
4646
url_resolver = request.scheme + "://" + request.META['HTTP_HOST']
47-
pass
47+
4848
return url_resolver
4949

50+
5051
def report_builder(request):
5152
add_breadcrumb(title="Report Builder", top_level=True, request=request)
5253
findings = Finding.objects.all()

dojo/static/dojo/css/dojo.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,10 @@ th a {
754754
content: "\e901";
755755
}
756756

757+
.icon-user-check:before {
758+
content: "\e975";
759+
}
760+
757761
#side-menu ul a {
758762
/*border-top: 1px solid #ddd;*/
759763
border-radius: 4px;

dojo/static/dojo/fonts/icomoon.eot

132 Bytes
Binary file not shown.

dojo/static/dojo/fonts/icomoon.svg

Lines changed: 1 addition & 0 deletions
Loading

dojo/static/dojo/fonts/icomoon.ttf

132 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)