Skip to content

Commit

Permalink
Add observation report linking
Browse files Browse the repository at this point in the history
  • Loading branch information
ColonelThirtyTwo committed Nov 3, 2023
1 parent 2808b9e commit 065a0c0
Show file tree
Hide file tree
Showing 11 changed files with 653 additions and 3 deletions.
19 changes: 19 additions & 0 deletions ghostwriter/modules/custom_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
Finding,
Report,
ReportFindingLink,
ReportObservationLink,
ReportTemplate,
)
from ghostwriter.rolodex.models import (
Expand Down Expand Up @@ -220,6 +221,16 @@ def get_severity_color_hex(self, obj):
return obj.severity.color_hex


class ObservationLinkSerializer(TaggitSerializer, CustomModelSerializer):
"""Serialize :model:`reporting:ObservationLinkSerializer` entries."""

tags = TagListSerializerField()

class Meta:
model = ReportObservationLink
fields = "__all__"


class ReportTemplateSerializer(CustomModelSerializer):
"""Serialize :model:`reporting:ReportTemplate` entries."""

Expand All @@ -238,6 +249,7 @@ class ReportSerializer(TaggitSerializer, CustomModelSerializer):
total_findings = SerializerMethodField("get_total_findings")

findings = FindingLinkSerializer(source="reportfindinglink_set", many=True, exclude=["id", "report"])
observations = ObservationLinkSerializer(source="reportobservationlink_set", many=True, exclude=["id", "report"])

tags = TagListSerializerField()

Expand Down Expand Up @@ -734,6 +746,13 @@ class ReportDataSerializer(CustomModelSerializer):
"report",
],
)
observations = ObservationLinkSerializer(
source="reportobservationlink_set",
many=True,
exclude=[
"report",
],
)
docx_template = ReportTemplateSerializer(
exclude=[
"upload_date",
Expand Down
7 changes: 7 additions & 0 deletions ghostwriter/modules/linting_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,13 @@
"tags": ["tag1", "tag2", "tag3"],
},
],
"observations": [
{
"id": 1,
"title": "test observation",
"description": "",
}
],
"docx_template": {
"id": 1,
"document": "/media/template_oxnfkmX.docx",
Expand Down
3 changes: 2 additions & 1 deletion ghostwriter/modules/reportwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1572,10 +1572,11 @@ def render_subdocument(section, finding, p_style=None):
return self.sacrificial_doc
return None

p_style = self.report_queryset.docx_template.p_style

# Findings
for finding in context["findings"]:
logger.info("Processing %s", finding["title"])
p_style = self.report_queryset.docx_template.p_style
# Create ``RichText()`` object for a colored severity category
finding["severity_rt"] = RichText(finding["severity"], color=finding["severity_color"])
finding["cvss_score_rt"] = RichText(finding["cvss_score"], color=finding["severity_color"])
Expand Down
46 changes: 46 additions & 0 deletions ghostwriter/reporting/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
Observation,
Report,
ReportFindingLink,
ReportObservationLink,
ReportTemplate,
Severity,
)
Expand Down Expand Up @@ -1029,3 +1030,48 @@ def __init__(self, *args, **kwargs):
),
),
)


class ReportObservationLinkUpdateForm(forms.ModelForm):
"""
Update an individual :model:`reporting.ReportObservationLink` associated with an
individual :model:`reporting.Report`.
"""

class Meta:
model = ReportObservationLink
exclude = (
"report",
"position",
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs["autocomplete"] = "off"
self.fields["title"].widget.attrs["placeholder"] = "Observation Title"
self.fields["description"].widget.attrs["placeholder"] = "What is this ..."
self.fields["tags"].widget.attrs["placeholder"] = "ATT&CK:T1555, privesc, ..."

self.helper = FormHelper()
self.helper.form_show_labels = True
self.helper.form_method = "post"
self.helper.form_id = "report-observation-form"
self.helper.layout = Layout(
Row(
Column("title", css_class="form-group col-md-6 mb-0"),
Column("tags", css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
Field("description", css_class="enable-evidence-upload"),
ButtonHolder(
Submit("submit_btn", "Submit", css_class="btn btn-primary col-md-4"),
HTML(
"""
<button onclick="window.location.href='{{ cancel_link }}'"
class="btn btn-outline-secondary col-md-4" type="button">Cancel
</button>
"""
),
),
)
37 changes: 37 additions & 0 deletions ghostwriter/reporting/migrations/0044_reportobservationlink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 3.2.19 on 2023-10-31 17:21

from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers


class Migration(migrations.Migration):

dependencies = [
('taggit', '0005_auto_20220424_2025'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('reporting', '0043_observation'),
]

operations = [
migrations.CreateModel(
name='ReportObservationLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='Enter a title for this observation that will appear in the reports', max_length=255, verbose_name='Title')),
('position', models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Report Position')),
('description', models.TextField(blank=True, help_text='Provide a description for this observation that introduces it', null=True, verbose_name='Description')),
('added_as_blank', models.BooleanField(default=False, help_text='Identify an observation that was created for this report instead of copied from the library', verbose_name='Added as Blank')),
('assigned_to', models.ForeignKey(blank=True, help_text='Assign the task of editing this observation to a specific operator - defaults to the operator that added it to the report', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('report', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='reporting.report')),
('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
],
options={
'verbose_name': 'Report observation',
'verbose_name_plural': 'Report observations',
'ordering': ['report', 'position'],
},
),
]
43 changes: 43 additions & 0 deletions ghostwriter/reporting/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,3 +687,46 @@ def __str__(self):

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


class ReportObservationLink(models.Model):

title = models.CharField(
"Title",
max_length=255,
help_text="Enter a title for this observation that will appear in the reports",
)
position = models.IntegerField(
"Report Position",
default=1,
validators=[MinValueValidator(1)],
)
description = models.TextField(
"Description",
null=True,
blank=True,
help_text="Provide a description for this observation that introduces it",
)
added_as_blank = models.BooleanField(
"Added as Blank",
default=False,
help_text="Identify an observation that was created for this report instead of copied from the library",
)
tags = TaggableManager(blank=True)
# Foreign Keys
report = models.ForeignKey("Report", on_delete=models.CASCADE, null=True)
assigned_to = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Assign the task of editing this observation to a specific operator - defaults to the operator that added it to the report",
)

class Meta:
ordering = ["report", "position"]
verbose_name = "Report observation"
verbose_name_plural = "Report observations"

def __str__(self):
return self.title
124 changes: 124 additions & 0 deletions ghostwriter/reporting/templates/reporting/local_observation_edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{% extends "base_generic.html" %}

{% load crispy_forms_tags %}

{% load bleach_tags %}

{% load custom_tags %}

{% block pagetitle %}Finding Entry{% endblock %}

{% block breadcrumbs %}
<nav aria-label="breadcrumb">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a
href="{% url 'rolodex:client_detail' reportobservationlink.report.project.client.id %}">{{ reportobservationlink.report.project.client.name }}</a>
</li>
<li class="breadcrumb-item"><a
href="{% url 'rolodex:project_detail' reportobservationlink.report.project.id %}">{{ reportobservationlink.report.project.start_date|date:"DATE_FORMAT" }} {{ reportobservationlink.report.project.project_type }}</a>
</li>
<li class="breadcrumb-item"><a
href="{% url 'reporting:report_detail' reportobservationlink.report.id %}">{{ reportobservationlink.report }}</a></li>
<li class="breadcrumb-item active" aria-current="page">Update Finding</li>
</ul>
</nav>
{% endblock %}

{% block content %}
<div id="accordion" class="finding-accordion">
<!-- Keyword Cheatsheet -->
<div class="card">
<div class="card-header" id="keyword" data-toggle="collapse" data-target="#collapseKeyword">
<a class="accordion-toggle btn btn-link icon code-icon" data-toggle="collapse" data-target="#collapseKeyword"
aria-expanded="false" aria-controls="collapseKeyword">Keyword Reference & Instructions</a>
</div>
<div id="collapseKeyword" class="collapse" aria-labelledby="keyword" data-parent="#accordion">
<div class="card-body">
<p>Ghostwriter supports several template keywords you may utilize to format text and insert various pieces of
information. Begin typing <strong>@</strong> to open the autocomplete dialog for keywords.</p>
<table class="table table-striped">
<tr>
<th class="smallCell">Keyword</th>
<th>Usage</th>
</tr>
<tr>
{% verbatim %}
<td class="text-left align-middle">{{.client}}</td>
{% endverbatim %}
<td class="text-left align-middle">
{% if reportobservationlink.report.project.client.short_name %}
This keyword will be replaced with the client's short name, "
{{ reportobservationlink.report.project.client.short_name }}."
{% else %}
No short name is set for this client, so this keyword will be replaced by the full name, "
{{ reportobservationlink.report.project.client.name }}."
{% endif %}
</td>
</tr>
<tr>
{% verbatim %}
<td class="text-left align-middle">{{.project_type}}</td>
{% endverbatim %}
<td class="text-left align-middle">This keyword will be replaced with the project type in
lowercase, {{ reportobservationlink.report.project.project_type|lower }}.
</td>
</tr>
<tr>
{% verbatim %}
<td class="text-left align-middle">{{.project_start}}</td>
{% endverbatim %}
<td class="text-left align-middle">This keyword will be replaced with the project's start date in
<em>{% settings_value "DATE_FORMAT" %}</em>
format: {{ reportobservationlink.report.project.start_date|date:"DATE_FORMAT" }}</td>
</tr>
<tr>
{% verbatim %}
<td class="text-left align-middle">{{.project_end}}</td>
{% endverbatim %}
<td class="text-left align-middle">This keyword will be replaced with the project's end date in
<em>{% settings_value "DATE_FORMAT" %}</em>
format: {{ reportobservationlink.report.project.end_date|date:"DATE_FORMAT" }}</td>
</tr>
{% verbatim %}
<tr>
<td class="text-left align-middle">{{.caption}}</td>
<td class="text-left align-middle">Start a line of text with this keyword to make it a caption. This is
intended to follow a code block.
</td>
</tr>
{% endverbatim %}
</table>
<p>For additional formatting, utilize the WYSIWIG HTML formatting to apply bold, italic, code, inline code, and other text styles.</p>
<p>These styles will carry over to the Word and PowerPoint reports. See the documentaiton for more
details.</p>
</div>
</div>
</div>
</div>

<!-- Instructions Section -->
<p>Use this form to edit "{{ reportobservationlink.title }}" just for this report.</p>
<p>When the finding is complete and ready for review, make sure you check the box down below.</p>

<!-- Form Error Section -->
{% if form.errors %}
<script>
{% for field in form %}
{% for error in field.errors %}
displayToastTop({type: 'error', string: '{{ error }}', context: 'form'});
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}
displayToastTop({type: 'error', string: '{{ error }}', context: 'form'});
{% endfor %}
</script>
{% endif %}

<!-- Form Section -->
{% crispy form form.helper %}
{% endblock %}

{% block morescripts %}
{% comment %} Include the reusable delete confirmation modal and related scripts {% endcomment %}
{% include "confirm_delete_modal.html" %}
{% endblock %}
Loading

0 comments on commit 065a0c0

Please sign in to comment.