Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report Observations #358

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions ghostwriter/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,23 @@ def tags(self, create, extracted, **kwargs):
self.tags.add(tag)


class ObservationFactory(factory.django.DjangoModelFactory):
class Meta:
model = "reporting.Observation"

title = factory.Sequence(lambda n: "Observation %s" % n)
description = Faker("paragraph")

@factory.post_generation
def tags(self, create, extracted, **kwargs):
if not create:
return

if extracted:
for tag in extracted:
self.tags.add(tag)


class DocTypeFactory(factory.django.DjangoModelFactory):
class Meta:
model = "reporting.DocType"
Expand Down Expand Up @@ -393,6 +410,25 @@ def tags(self, create, extracted, **kwargs):
self.tags.add(tag)


class ReportObservationLinkFactory(factory.django.DjangoModelFactory):
class Meta:
model = "reporting.ReportObservationLink"

title = factory.Sequence(lambda n: "Local Observation %s" % n)
position = 1
description = Faker("paragraph")
added_as_blank = Faker("boolean")

@factory.post_generation
def tags(self, create, extracted, **kwargs):
if not create:
return

if extracted:
for tag in extracted:
self.tags.add(tag)


class BlankReportFindingLinkFactory(factory.django.DjangoModelFactory):
class Meta:
model = "reporting.ReportFindingLink"
Expand Down
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
6 changes: 6 additions & 0 deletions ghostwriter/reporting/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
FindingNote,
FindingType,
LocalFindingNote,
Observation,
Report,
ReportFindingLink,
ReportTemplate,
Expand Down Expand Up @@ -258,3 +259,8 @@ def get_queryset(self, request):

def tag_list(self, obj):
return ", ".join(o.name for o in obj.tags.all())


@admin.register(Observation)
class ObservationAdmin(admin.ModelAdmin):
pass
53 changes: 52 additions & 1 deletion ghostwriter/reporting/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from crispy_forms.layout import HTML, ButtonHolder, Column, Div, Layout, Row, Submit

# Ghostwriter Libraries
from ghostwriter.reporting.models import Archive, Finding, FindingType, Report, Severity
from ghostwriter.reporting.models import Archive, Finding, FindingType, Observation, Report, Severity


class FindingFilter(django_filters.FilterSet):
Expand Down Expand Up @@ -94,6 +94,57 @@ def __init__(self, *args, **kwargs):
)


class ObservationFilter(django_filters.FilterSet):
"""
Filter :model:`reporting.Observation` model for searching.

**Fields**

``title``
Case insensitive search of the title field contents.
"""

title = django_filters.CharFilter(
lookup_expr="icontains",
label="Observation Title Contains",
widget=TextInput(attrs={"placeholder": "Observation Title Contains", "autocomplete": "off"}),
)

class Meta:
model = Observation
fields = ["title"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "get"
self.helper.layout = Layout(
Div(
Row(
Column(
PrependedText("title", '<i class="fas fa-filter"></i>'),
css_class="form-group col-md-6 offset-md-3 mb-0",
),
css_class="form-row",
),
ButtonHolder(
HTML(
"""
<a class="btn btn-info col-md-2" role="button" href="{% url 'reporting:observation_create' %}">Create</a>
"""
),
Submit("submit_btn", "Filter", css_class="col-md-2"),
HTML(
"""
<a class="btn btn-outline-secondary col-md-2" role="button" href="{% url 'reporting:observations' %}">Reset</a>
"""
),
),
css_class="justify-content-center",
),
)


class ReportFilter(django_filters.FilterSet):
"""
Filter :model:`reporting.Report` model for searching.
Expand Down
82 changes: 82 additions & 0 deletions ghostwriter/reporting/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
Finding,
FindingNote,
LocalFindingNote,
Observation,
Report,
ReportFindingLink,
ReportObservationLink,
ReportTemplate,
Severity,
)
Expand Down Expand Up @@ -995,3 +997,83 @@ def clean_color(self, *args, **kwargs):
)

return color


class ObservationForm(forms.ModelForm):
"""Save an individual :model:`reporting.Observation`."""

class Meta:
model = Observation
fields = "__all__"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs["autocomplete"] = "off"
self.helper = FormHelper()
self.helper.form_show_labels = True
self.helper.form_method = "post"
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"),
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>
"""
),
),
)


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>
"""
),
),
)
29 changes: 29 additions & 0 deletions ghostwriter/reporting/migrations/0043_observation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 3.2.19 on 2023-10-26 15:57

from django.db import migrations, models
import taggit.managers


class Migration(migrations.Migration):

dependencies = [
('taggit', '0005_auto_20220424_2025'),
('reporting', '0042_auto_20230919_1809'),
]

operations = [
migrations.CreateModel(
name='Observation',
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 finding that will appear in reports', max_length=255, unique=True, verbose_name='Title')),
('description', models.TextField(blank=True, help_text='Provide a description for this observation that introduces it', null=True, verbose_name='Description')),
('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': 'Observation',
'verbose_name_plural': 'Observations',
'ordering': ['title'],
},
),
]
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'],
},
),
]
Loading
Loading