Skip to content

Commit

Permalink
Merge pull request #526 from GhostManager/staging
Browse files Browse the repository at this point in the history
Release v4.3.1
  • Loading branch information
chrismaddalena authored Sep 25, 2024
2 parents 1137f9d + 6bfe824 commit 88ef6a4
Show file tree
Hide file tree
Showing 15 changed files with 258 additions and 73 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

# CHANGELOG

## [4.3.1] – 25 Sep 2024

### Added

* Added a `replace_blanks` filter to the report template engine to replace blank values in a dictionary with a specified string
* This filter is useful when sorting a list of dictionaries with an attribute that may have a blank value
* Added an option in the change search in the findings library to search findings attached to reports (Closes #400)
* Instead of matches from the library, the search will return results for findings attached to reports to which the user has access

### Changed

* Changed the serializer for report context to replace null values with a blank string (`""`) to help prevent errors when generating reports
* **Note:** This change may affect templates that rely on null values to trigger conditional logic, but most conditional statements should not be affected
* **Example:** The condition `{% if not X %}` will evaluate to `True` if `X` is `None` or `""`
* Changed the report form to allow users with the `admin` or `manager` roles to change the report's project (Closes #368)
* This change allows a report to be moved from one project to another (e.g., you make a copy for a follow-up assessment)
* This feature is only available to users with the `admin` or `manager` roles to prevent accidental data leaks

### Fixed

* Fixed an edge case with the Namecheap sync task that could lead to a domain remaining marked as expired after re-purchasing it or renewing it during the grace period

## [4.3.0] – 23 Sep 2024

### Added
Expand Down
4 changes: 2 additions & 2 deletions VERSION
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
v4.3.0
23 September 2024
v4.3.1
25 September 2024
4 changes: 2 additions & 2 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
# 3rd Party Libraries
import environ

__version__ = "4.3.0"
__version__ = "4.3.1"
VERSION = __version__
RELEASE_DATE = "23 September 2024"
RELEASE_DATE = "25 September 2024"

ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
APPS_DIR = ROOT_DIR / "ghostwriter"
Expand Down
31 changes: 21 additions & 10 deletions ghostwriter/modules/custom_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ def __init__(self, *args, exclude=None, **kwargs):
self.fields.pop(field)
super().__init__(*args, **kwargs)

def to_representation(self, instance):
"""
Override the default method to ensure empty strings are returned for null values. The null values will
cause Jinja2 rendering errors with filters and expressions like `sort()`.
"""
data = super().to_representation(instance)
for key, value in data.items():
try:
if not value:
data[key] = ""
except KeyError:
pass
return data


class OperatorNameField(RelatedField):
"""Customize the string representation of a :model:`users.User` entry."""
Expand Down Expand Up @@ -113,7 +127,7 @@ class ExtraFieldsSerField(serializers.Field):
def __init__(self, model_name, **kwargs):
self.model_name = model_name
self.root_ser = None
kwargs['read_only'] = True
kwargs["read_only"] = True
super().__init__(**kwargs)

def bind(self, field_name, parent):
Expand All @@ -130,7 +144,9 @@ def to_representation(self, value):
if not hasattr(self.root_ser, "_extra_fields_specs") or self.root_ser._extra_fields_specs is None:
self.root_ser._extra_fields_specs = {}
if self.model_name not in self.root_ser._extra_fields_specs:
self.root_ser._extra_fields_specs[self.model_name] = ExtraFieldSpec.objects.filter(target_model=self.model_name)
self.root_ser._extra_fields_specs[self.model_name] = ExtraFieldSpec.objects.filter(
target_model=self.model_name
)

# Populate output
for field in self.root_ser._extra_fields_specs[self.model_name]:
Expand Down Expand Up @@ -514,10 +530,7 @@ class DomainHistorySerializer(CustomModelSerializer):
exclude=["id", "project", "domain"],
)

extra_fields = ExtraFieldsSerField(
Domain._meta.label,
source="domain.extra_fields"
)
extra_fields = ExtraFieldsSerField(Domain._meta.label, source="domain.extra_fields")

class Meta:
model = History
Expand Down Expand Up @@ -567,10 +580,7 @@ class ServerHistorySerializer(CustomModelSerializer):
exclude=["id", "project", "static_server", "transient_server"],
)

extra_fields = ExtraFieldsSerField(
StaticServer._meta.label,
source="server.extra_fields"
)
extra_fields = ExtraFieldsSerField(StaticServer._meta.label, source="server.extra_fields")

class Meta:
model = ServerHistory
Expand Down Expand Up @@ -756,6 +766,7 @@ class Meta:

class FullProjectSerializer(serializers.Serializer):
"""Serialize :model:`rolodex:Project` and related entries."""

project = ProjectSerializer(source="*")
client = ClientSerializer()
contacts = ProjectContactSerializer(source="projectcontact_set", many=True, exclude=["id", "project"])
Expand Down
2 changes: 2 additions & 0 deletions ghostwriter/modules/reportwriter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __iter__(self):
def __bool__(self):
self._record()
return super().__bool__()

undefined = RecordUndefined
else:
undefined = jinja2.make_logging_undefined(logger=logger, base=jinja2.Undefined)
Expand All @@ -55,6 +56,7 @@ def __bool__(self):
env.filters["get_item"] = jinja_funcs.get_item
env.filters["regex_search"] = jinja_funcs.regex_search
env.filters["filter_tags"] = jinja_funcs.filter_tags
env.filters["replace_blanks"] = jinja_funcs.replace_blanks

if debug:
return env, undefined_vars
Expand Down
23 changes: 23 additions & 0 deletions ghostwriter/modules/reportwriter/jinja_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,26 @@ def mk_evidence(context: jinja2.runtime.Context, evidence_name: str) -> Markup:

def raw_mk_evidence(evidence_id) -> Markup:
return Markup('<span data-gw-evidence="' + html.escape(str(evidence_id)) + '"></span>')


def replace_blanks(list_of_dicts, placeholder=""):
"""
Replace blank strings in a dictionary with a placeholder string.
**Parameters**
``dict``
Dictionary to replace blanks in
"""

try:
for d in list_of_dicts:
for key, value in d.items():
if value is None:
d[key] = placeholder
except (AttributeError, TypeError) as e:
logger.exception("Error parsing ``list_of_dicts`` as a list of dictionaries: %s", list_of_dicts)
raise InvalidFilterValue(
"Invalid list of dictionaries passed into `replace_blanks()` filter; must be a list of dictionaries"
) from e
return list_of_dicts
21 changes: 21 additions & 0 deletions ghostwriter/reporting/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ class FindingFilter(django_filters.FilterSet):
),
)

# Dummy filter to add a checkbox onto the form, which the view uses to select Findings vs
# ReportFindingLinks
on_reports = django_filters.BooleanFilter(
method="filter_on_reports",
label="Search findings on reports",
widget=forms.CheckboxInput,
)

def filter_on_reports(self, queryset, *args, **kwargs):
return queryset

class Meta:
model = Finding
fields = ["title", "severity", "finding_type"]
Expand Down Expand Up @@ -101,6 +112,16 @@ def __init__(self, *args, **kwargs):
),
css_class="form-row",
),
Row(
Column(
"on_reports",
css_class="col-md-12 m-1",
data_toggle="tooltip",
data_placement="top",
title="Return results from reports instead of the library",
),
css_class="form-row",
),
ButtonHolder(
HTML(
"""
Expand Down
21 changes: 14 additions & 7 deletions ghostwriter/reporting/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)

# Ghostwriter Libraries
from ghostwriter.api.utils import get_client_list, get_project_list
from ghostwriter.api.utils import get_client_list, get_project_list, verify_user_is_privileged
from ghostwriter.commandcenter.forms import ExtraFieldsField
from ghostwriter.commandcenter.models import ReportConfiguration
from ghostwriter.modules.custom_layout_object import SwitchToggle
Expand Down Expand Up @@ -286,19 +286,26 @@ class Meta:

def __init__(self, user=None, project=None, *args, **kwargs):
super().__init__(*args, **kwargs)
# If this is an update, mark the project field as read-only
# Don't allow non-manager users to move a report's project
instance = getattr(self, "instance", None)
user_is_privileged = verify_user_is_privileged(user)
if instance and instance.pk:
self.fields["project"].disabled = True
if user is None or not user_is_privileged:
self.fields["project"].disabled = True

# Limit the list to the pre-selected project and disable the field
if project:
# If there is a project and user is not privileged,
# limit the list to the pre-selected project and disable the field
if project and not user_is_privileged:
self.fields["project"].queryset = Project.objects.filter(pk=project.pk)
self.fields["project"].disabled = True

if not project:
# If no project is selected, limit the list to what the user can access
# Checks for privilege so that privileged users get a list with only active projects
if not project or user_is_privileged:
projects = get_project_list(user)
active_projects = projects.filter(complete=False).order_by("-start_date", "client", "project_type").defer("extra_fields")
active_projects = (
projects.filter(complete=False).order_by("-start_date", "client", "project_type").defer("extra_fields")
)
if active_projects:
self.fields["project"].empty_label = "-- Select an Active Project --"
else:
Expand Down
11 changes: 10 additions & 1 deletion ghostwriter/reporting/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ class Meta:
def get_absolute_url(self):
return reverse("reporting:finding_detail", args=[str(self.id)])

def get_edit_url(self):
return reverse("reporting:finding_update", kwargs={"pk": self.pk})

def __str__(self):
return f"[{self.severity}] {self.title}"

Expand Down Expand Up @@ -661,7 +664,13 @@ class Meta:
verbose_name_plural = "Report findings"

def __str__(self):
return f"{self.title}"
return f"{self.title} on {self.report}"

def get_absolute_url(self):
return reverse("reporting:report_detail", kwargs={"pk": self.report.pk}) + "#findings"

def get_edit_url(self):
return reverse("reporting:local_edit", kwargs={"pk": self.pk})


def set_evidence_upload_destination(this, filename):
Expand Down
90 changes: 61 additions & 29 deletions ghostwriter/reporting/templates/reporting/finding_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,38 +24,58 @@
</div>
{% else %}
<!-- Instructions Section -->
<p class="mt-3">Click the <span class="add-icon"></span> icon to add a finding to the <em>current report</em>
displayed at the top of the page.</p>
{% if not searching_report_findings %}
<p class="mt-3">Click the <span class="add-icon"></span> icon to add a finding to the <em>current report</em>
displayed at the top of the page.</p>
{% else %}
<p class="mt-3">The following results include findings attached to reports, not findings in your library.</p>
{% endif %}

<!-- Findings Table Section -->
<div id="findings_table">
<table id="findingsTable" class="tablesorter table table-striped table-sm">
<thead>
<tr>
<th class="sortable pr-4">Severity</th>
<th class="sortable pr-4">Type</span></th>
<th class="sortable pr-4">Title</th>
<th class="sortable pr-4 align-middle">Severity</th>
<th class="sortable pr-4 align-middle">Type</th>
<th class="sortable pr-4 align-middle">Title</th>
{% if searching_report_findings %}
<th class="sortable pr-4 align-middle">Report</th>
{% endif %}
<th class="sorter-false align-middle">Tags</th>
<th class="sorter-false align-middle">
<div class="dropdown dropleft">
<span id="finding-info-btn" class="dropdown-info" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">Add Report</span>
<div id="finding-info" class="dropdown-menu dropdown-info-content" aria-labelledby="finding-info-btn">
<p>Clicking <span class="add-icon"></span> will add the finding to your active report displayed at the
top of the screen.</p>

{% if searching_report_findings %}
<th class="sorter-false align-middle">
<div class="dropdown dropleft">
<span id="edit-info-btn" class="dropdown-info" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">Edit Finding on Report</span>
<div id="edit-info" class="dropdown-menu dropdown-info-content" aria-labelledby="edit-info-btn">
<p>Editing a finding here edits the <em>version on the report</em>.</p>
</div>
</div>
</th>
{% else %}
<th class="sorter-false align-middle">
<div class="dropdown dropleft">
<span id="finding-info-btn" class="dropdown-info" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">Add to Report</span>
<div id="finding-info" class="dropdown-menu dropdown-info-content" aria-labelledby="finding-info-btn">
<p>Clicking <span class="add-icon"></span> will add the finding to your active report displayed at the
top of the screen.</p>
</div>
</div>
</div>
</th>
<th class="sorter-false align-middle">
<div class="dropdown dropleft">
<span id="edit-info-btn" class="dropdown-info" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">Edit Finding</span>
<div id="edit-info" class="dropdown-menu dropdown-info-content" aria-labelledby="edit-info-btn">
<p>Editing a finding here edits the <em>master record</em> for every other user. Only customize a
finding after adding it to your report.</p>
</th>
<th class="sorter-false align-middle">
<div class="dropdown dropleft">
<span id="edit-info-btn" class="dropdown-info" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">Edit Finding</span>
<div id="edit-info" class="dropdown-menu dropdown-info-content" aria-labelledby="edit-info-btn">
<p>Editing a finding here edits the <em>master record</em> for every other user. Only customize a
finding after adding it to your report.</p>
</div>
</div>
</div>
</th>
</th>
{% endif %}
</tr>
</thead>
<tbody class="list">
Expand Down Expand Up @@ -111,15 +131,27 @@
</td>

<!-- Other Information -->
<td class="text-justify"><a class="clickable" href="{{ finding.get_absolute_url }}">{{ finding.title }}</a>
<td class="text-justify">
<a class="clickable" href="{{ finding.get_absolute_url }}">{{ finding.title }}</a>
</td>
{% if searching_report_findings %}
<td class="text-justify">
<a class="clickable" href="{{ finding.report.get_absolute_url }}">{{ finding.report.title }}</a>
</td>
{% endif %}
<td class="text-justify align-middle">
{% for tag in finding.tags.all %}<span class="badge badge-secondary">{{ tag.name }}</span>{% endfor %}
</td>
<td class="text-justify align-middle">{% for tag in finding.tags.all %}<span class="badge badge-secondary">{{ tag.name }}</span>{% endfor %}</td>
<td class="align-middle"><a class="js-assign-finding icon add-icon" assign-finding-id="{{ finding.id }}"
{% if not searching_report_findings %}
<td class="align-middle">
<a class="js-assign-finding icon add-icon" assign-finding-id="{{ finding.id }}"
href="javascript:void(0);" assign-finding-csrftoken="{{ csrf_token }}"
assign-finding-url="{% url 'reporting:ajax_assign_finding' finding.id %}"></a>
</td>
<td class="align-middle"><a class="icon edit-icon"
href="{% url 'reporting:finding_update' finding.id %}"></a></td>
{% endif %}
<td class="align-middle">
<a class="icon edit-icon" href="{{finding.get_edit_url}}"></a>
</td>
</tr>
{% endfor %}
</tbody>
Expand Down Expand Up @@ -178,7 +210,7 @@
$(function () {
let availableTitles = [
{% for entry in autocomplete %}
'{{ entry|escapejs }}',
'{{ entry.title|escapejs }}',
{% endfor %}
];
$("#id_title").autocomplete({
Expand Down
Loading

0 comments on commit 88ef6a4

Please sign in to comment.