Skip to content

Commit

Permalink
Add evidence tied to reports rather than findings
Browse files Browse the repository at this point in the history
  • Loading branch information
ColonelThirtyTwo committed Nov 22, 2023
1 parent b24a65d commit d1ec2e5
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 77 deletions.
8 changes: 4 additions & 4 deletions ghostwriter/api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
ClientFactory,
DomainFactory,
DomainStatusFactory,
EvidenceFactory,
EvidenceOnFindingFactory,
FindingFactory,
HistoryFactory,
OplogEntryFactory,
Expand Down Expand Up @@ -974,7 +974,7 @@ class GraphqlDeleteEvidenceActionTests(TestCase):

@classmethod
def setUpTestData(cls):
cls.Evidence = EvidenceFactory._meta.model
cls.Evidence = EvidenceOnFindingFactory._meta.model

cls.user = UserFactory(password=PASSWORD)
cls.uri = reverse("api:graphql_delete_evidence")
Expand All @@ -989,8 +989,8 @@ def setUpTestData(cls):
cls.finding = ReportFindingLinkFactory(report=cls.report)
cls.other_finding = ReportFindingLinkFactory(report=cls.other_report)

cls.evidence = EvidenceFactory(finding=cls.finding)
cls.other_evidence = EvidenceFactory(finding=cls.other_finding)
cls.evidence = EvidenceOnFindingFactory(finding=cls.finding)
cls.other_evidence = EvidenceOnFindingFactory(finding=cls.other_finding)

def setUp(self):
self.client = Client()
Expand Down
11 changes: 9 additions & 2 deletions ghostwriter/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,15 +406,14 @@ class Meta:
report = factory.SubFactory(ReportFactory)


class EvidenceFactory(factory.django.DjangoModelFactory):
class BaseEvidenceFactory(factory.django.DjangoModelFactory):
class Meta:
model = "reporting.Evidence"

document = factory.django.FileField(filename="evidence.png", data=b"lorem ipsum")
friendly_name = factory.Sequence(lambda n: "Evidence %s" % n)
caption = Faker("sentence")
description = Faker("sentence")
finding = factory.SubFactory(ReportFindingLinkFactory)
uploaded_by = factory.SubFactory(UserFactory)

class Params:
Expand All @@ -432,6 +431,14 @@ def tags(self, create, extracted, **kwargs):
self.tags.add(tag)


class EvidenceOnFindingFactory(BaseEvidenceFactory):
finding = factory.SubFactory(ReportFindingLinkFactory)


class EvidenceOnReportFactory(BaseEvidenceFactory):
report = factory.SubFactory(ReportFactory)


class ArchiveFactory(factory.django.DjangoModelFactory):
class Meta:
model = "reporting.Archive"
Expand Down
4 changes: 2 additions & 2 deletions ghostwriter/home/management/commands/generate_test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
ClientFactory,
DomainFactory,
DomainServerConnectionFactory,
EvidenceFactory,
EvidenceOnFindingFactory,
FindingFactory,
HistoryFactory,
OplogEntryFactory,
Expand Down Expand Up @@ -267,7 +267,7 @@ def handle(self, *args, **kwargs):

# Create fake evidence
for f in report_findings:
EvidenceFactory(
EvidenceOnFindingFactory(
finding=f,
uploaded_by=random.choice(assignments).operator,
)
Expand Down
2 changes: 2 additions & 0 deletions ghostwriter/modules/custom_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class FindingLinkSerializer(TaggitSerializer, CustomModelSerializer):
source="evidence_set",
many=True,
exclude=[
"report",
"finding",
"uploaded_by",
],
Expand Down Expand Up @@ -727,6 +728,7 @@ class ReportDataSerializer(CustomModelSerializer):
deconflictions = DeconflictionSerializer(source="project.deconfliction_set", many=True, exclude=["id", "project"])
whitecards = WhiteCardSerializer(source="project.whitecard_set", many=True, exclude=["id", "project"])
infrastructure = ProjectInfrastructureSerializer(source="project")
evidence = EvidenceSerializer(source="evidence_set", many=True, exclude=["id", "report", "finding"])
findings = FindingLinkSerializer(
source="reportfindinglink_set",
many=True,
Expand Down
13 changes: 13 additions & 0 deletions ghostwriter/modules/linting_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,4 +461,17 @@
"targets": 1,
},
"tools": ["beacon", "covenant", "mythic", "poseidon"],
"evidence": [
{
"id": 1,
"file_path": "evidence/2/ghost.png",
"url": "/media/evidence/2/ghost.png",
"document": "/media/evidence/2/ghost.png",
"friendly_name": "Ghostwriter",
"upload_date": "2021-03-22",
"caption": "Brief Caption for This Evidence",
"description": "",
"tags": ["tag1", "tag2", "tag3"],
}
],
}
2 changes: 1 addition & 1 deletion ghostwriter/reporting/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
evidence_upload_url = reverse(
"reporting:upload_evidence_modal",
kwargs={"pk": self.instance.id, "modal": "modal"},
kwargs={"parent_type": "finding", "pk": self.instance.id, "modal": "modal"},
)
for field in self.fields:
self.fields[field].widget.attrs["autocomplete"] = "off"
Expand Down
32 changes: 32 additions & 0 deletions ghostwriter/reporting/migrations/0043_auto_20231116_1810.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 3.2.19 on 2023-11-16 18:10

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('reporting', '0042_auto_20230919_1809'),
]

operations = [
migrations.AlterModelOptions(
name='evidence',
options={'ordering': ['finding', 'report', 'document'], 'verbose_name': 'Evidence', 'verbose_name_plural': 'Evidence'},
),
migrations.AddField(
model_name='evidence',
name='report',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='reporting.report'),
),
migrations.AlterField(
model_name='evidence',
name='finding',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='reporting.reportfindinglink'),
),
migrations.AddConstraint(
model_name='evidence',
constraint=models.CheckConstraint(check=models.Q(models.Q(('finding__isnull', True), ('report__isnull', False)), models.Q(('finding__isnull', False), ('report__isnull', True)), _connector='OR'), name='reporting_evidence_finding_or_report'),
),
]
25 changes: 23 additions & 2 deletions ghostwriter/reporting/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,17 +577,38 @@ class Evidence(models.Model):
)
tags = TaggableManager(blank=True)
# Foreign Keys
finding = models.ForeignKey("ReportFindingLink", on_delete=models.CASCADE)
finding = models.ForeignKey("ReportFindingLink", on_delete=models.CASCADE, null=True, blank=True)
report = models.ForeignKey("Report", on_delete=models.CASCADE, null=True, blank=True)
uploaded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)

class Meta:
ordering = ["finding", "document"]
ordering = ["finding", "report", "document"]
verbose_name = "Evidence"
verbose_name_plural = "Evidence"

constraints = [
models.CheckConstraint(
name="%(app_label)s_%(class)s_finding_or_report",
check=(
models.Q(finding__isnull=True, report__isnull=False)
| models.Q(finding__isnull=False, report__isnull=True)
)
)
]

def get_absolute_url(self):
return reverse("reporting:evidence_detail", args=[str(self.id)])

@property
def associated_report(self):
"""
The report associated with this evidence, either directly through `self.report` or indirectly through
`self.finding.report`.
"""
if self.finding:
return self.finding.report
return self.report

def __str__(self):
return f"{self.document.name}"

Expand Down
4 changes: 2 additions & 2 deletions ghostwriter/reporting/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def backup_evidence_values(sender, instance, **kwargs):
def evidence_update(sender, instance, **kwargs):
"""
On change, delete the old evidence file in the :model:`reporting.Evidence` instance when a
new file is uploaded and update teh related :model:`reporting.ReportFindingLink` instance if
new file is uploaded and update the related :model:`reporting.ReportFindingLink` instance if
the `friendly_name` value changed.
"""
if hasattr(instance, "_current_evidence"):
Expand All @@ -50,7 +50,7 @@ def evidence_update(sender, instance, **kwargs):
instance._current_evidence.path,
)

if hasattr(instance, "_current_friendly_name"):
if hasattr(instance, "_current_friendly_name") and instance.finding:
if instance._current_friendly_name != instance.friendly_name:
ignore = [
"id",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
<nav aria-label="breadcrumb">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'home:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'rolodex:client_detail' evidence.finding.report.project.client.id %}">{{ evidence.finding.report.project.client.name }}</a></li>
<li class="breadcrumb-item"><a href="{% url 'rolodex:project_detail' evidence.finding.report.project.id %}">{{ evidence.finding.report.project }}</a></li>
<li class="breadcrumb-item"><a href="{% url 'reporting:report_detail' evidence.finding.report.id %}">{{ evidence.finding.report }}</a></li>
<li class="breadcrumb-item"><a href="{% url 'rolodex:client_detail' evidence.associated_report.project.client.id %}">{{ evidence.associated_report.project.client.name }}</a></li>
<li class="breadcrumb-item"><a href="{% url 'rolodex:project_detail' evidence.associated_report.project.id %}">{{ evidence.associated_report.project.start_date }} {{ evidence.associated_report.project.project_type }}</a></li>
<li class="breadcrumb-item"><a href="{% url 'reporting:report_detail' evidence.associated_report.id %}">{{ evidence.associated_report }}</a></li>
<li class="breadcrumb-item active" aria-current="page">Evidence</li>
</ul>
</nav>
Expand All @@ -26,7 +26,7 @@
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="evidence-dropdown-btn">
<a class="dropdown-item icon edit-icon" href="{% url 'reporting:evidence_update' evidence.id %}">Edit</a>
<a class="dropdown-item icon trash-icon" href="{% url 'reporting:evidence_delete' evidence.id %}">Delete</a>
<a class="dropdown-item icon back-arrow-icon" href="{% url 'reporting:report_detail' evidence.finding.report.id %}">Return to Report</a>
<a class="dropdown-item icon back-arrow-icon" href="{% url 'reporting:report_detail' evidence.associated_report.id %}">Return to Report</a>
</div>
</div>

Expand Down
40 changes: 38 additions & 2 deletions ghostwriter/reporting/templates/reporting/report_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ <h4>Current Findings</h4>
<td class="align-middle">
<a
class="icon lg-attach-icon"
href="{% url 'reporting:upload_evidence' finding.id %}"
href="{% url 'reporting:upload_evidence' 'finding' finding.id %}"
data-toggle="tooltip"
data-placement="top"
title="Attach a file as evidence"
Expand Down Expand Up @@ -278,7 +278,7 @@ <h4>Current Findings</h4>
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div id="finding-dropdown-menu-{{ finding.id }}" class="dropdown-menu"
aria-labelledby="dns-dropdown-btn_{{ finding.id }}">
<a class="dropdown-item icon attach-icon" href="{% url 'reporting:upload_evidence' finding.id %}">Attach
<a class="dropdown-item icon attach-icon" href="{% url 'reporting:upload_evidence' 'finding' finding.id %}">Attach
Evidence</a>
<a class="dropdown-item icon edit-icon"
href="{% url 'reporting:local_edit' finding.id %}">Edit</a>
Expand Down Expand Up @@ -318,6 +318,42 @@ <h4>Current Findings</h4>
<p>No findings have been added to this report yet.</p>
{% endif %}

{% comment %} Report Evidence Section {% endcomment %}
<h4>Report Evidence</h4>
<hr>

<table id="evidences-table" class="table table-sm table-hover">
<tbody>
{% for evidence_file in report.evidence_set.all %}
<tr data-id="{{ evidence_file.id }}">
<td class="align-middle"><a href="{% url 'reporting:evidence_detail' evidence_file.id %}">{{evidence_file.friendly_name}}</a></td>
<td class="align-middle">
<div class="dropdown dropleft">
<button
id="evidence-dropdown-btn-{{ evidence_file.id }}"
class="dropdown-menu-btn-table"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
></button>
<div
id="evidence-dropdown-menu-{{ evidence_file.id }}"
class="dropdown-menu"
aria-labelledby="evidence-dropdown-btn_{{ evidence_file.id }}"
>
<a href="{% url 'reporting:evidence_update' evidence_file.id %}" class="dropdown-item icon edit-icon">Edit</a>
<a href="{% url 'reporting:evidence_delete' evidence_file.id %}" class="dropdown-item icon trash-icon">Delete</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

<p class="mt-3"><a class="icon add-icon btn btn-primary col-md-4"
href="{% url 'reporting:upload_evidence' 'report' report.id %}" data-toggle="tooltip"
data-placement="top" title="Upload evidence, attaching to a report">Upload Evidence</a></p>

{% comment %} Generate Report Sections {% endcomment %}
<h4>Generate Reports</h4>
<hr>
Expand Down
45 changes: 35 additions & 10 deletions ghostwriter/reporting/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

# Ghostwriter Libraries
from ghostwriter.factories import (
EvidenceFactory,
EvidenceOnFindingFactory,
EvidenceOnReportFactory,
FindingFactory,
FindingNoteFactory,
LocalFindingNoteFactory,
Expand Down Expand Up @@ -241,15 +242,24 @@ def test_complete_field(self):
self.assertTrue(self.complete_finding.complete)


class EvidenceFormTests(TestCase):
class BaseEvidenceFormTests:
"""Collection of tests for :form:`reporting.EvidenceForm`."""

@classmethod
def factory(cls):
raise NotImplementedError()

Check warning on line 250 in ghostwriter/reporting/tests/test_forms.py

View check run for this annotation

Codecov / codecov/patch

ghostwriter/reporting/tests/test_forms.py#L250

Added line #L250 was not covered by tests

@classmethod
def querySet(cls):
raise NotImplementedError()

Check warning on line 254 in ghostwriter/reporting/tests/test_forms.py

View check run for this annotation

Codecov / codecov/patch

ghostwriter/reporting/tests/test_forms.py#L254

Added line #L254 was not covered by tests

@classmethod
def setUpTestData(cls):
cls.Evidence = EvidenceFactory._meta.model
cls.evidence = EvidenceFactory()
cls.Factory = cls.factory()
cls.Evidence = cls.Factory._meta.model
cls.evidence = cls.Factory()
cls.evidence_dict = cls.evidence.__dict__
cls.evidence_queryset = cls.Evidence.objects.filter(finding=cls.evidence.finding)
cls.evidence_queryset = cls.querySet()

def setUp(self):
pass
Expand All @@ -260,8 +270,6 @@ def form_data(
friendly_name=None,
caption=None,
description=None,
finding_id=None,
uploaded_by_id=None,
evidence_queryset=None,
modal=False,
**kwargs,
Expand All @@ -274,8 +282,6 @@ def form_data(
"friendly_name": friendly_name,
"caption": caption,
"description": description,
"finding": finding_id,
"uploaded_by": uploaded_by_id,
},
files={
"document": document,
Expand Down Expand Up @@ -303,7 +309,6 @@ def test_blank_evidence(self):

def test_duplicate_friendly_name(self):
new_evidence = self.evidence_dict.copy()
new_evidence["finding"] = self.evidence.finding
new_evidence["friendly_name"] = self.evidence.friendly_name

form = self.form_data(**new_evidence)
Expand All @@ -326,6 +331,26 @@ def test_null_evidence_queryset_argument(self):
self.assertTrue(form.is_valid())


class EvidenceFormForFindingTests(BaseEvidenceFormTests, TestCase):
@classmethod
def factory(cls):
return EvidenceOnFindingFactory

@classmethod
def querySet(cls):
return cls.Evidence.objects.filter(finding=cls.evidence.finding)


class EvidenceFormForReportTests(BaseEvidenceFormTests, TestCase):
@classmethod
def factory(cls):
return EvidenceOnReportFactory

@classmethod
def querySet(cls):
return cls.Evidence.objects.filter(report=cls.evidence.report)


class FindingNoteFormTests(TestCase):
"""Collection of tests for :form:`reporting.FindingNoteForm`."""

Expand Down
Loading

0 comments on commit d1ec2e5

Please sign in to comment.