Skip to content

Commit

Permalink
Merge branch 'master' into cvssv4-2
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismaddalena authored Sep 30, 2024
2 parents 3ed869a + 32fabbe commit 721b4ac
Show file tree
Hide file tree
Showing 31 changed files with 1,276 additions and 766 deletions.
29 changes: 26 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

# CHANGELOG

## [4.3.0] – 10 Sep 2024
## [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 All @@ -15,11 +37,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* The adapter fills-in a nearly full profile for any new accounts (full name, email address, username)
* Usernames for new accounts will default to the first half of the email address
* If an existing account has the same email address, the accounts will be linked
* Review the wiki for more information: [https://www.ghostwriter.wiki/features/single-sign-on](https://www.ghostwriter.wiki/features/single-sign-on)
* Review the wiki for more information: [https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/single-sign-on](https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/single-sign-on)
* Added support for loading customized config files
* These are files you can use to modify settings normally found in _/config/settings/base.py_ and _production.py_
* Admins can make changes to the custom config files without worrying about the changes needing to be stashed prior to pulling an update
* Review this section of the wiki for information: [https://www.ghostwriter.wiki/features/single-sign-on#configuring-an-sso-provider](https://www.ghostwriter.wiki/features/single-sign-on#configuring-an-sso-provider)
* Review this section of the wiki for information: [https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/single-sign-on#configuring-an-sso-provider](https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/single-sign-on#configuring-an-sso-provider)
* Added support for a JSON field type for custom fields
* Added a "Tags" column to the domain and server library tables

Expand All @@ -32,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Cloud hosts tracked on a project no longer require a unique IP address
* A warning is displayed if a cloud host is tracked on a project with multiple hosts sharing the same IP address
* Changed filtering on tags to be case-insensitive
* On the report dashboard, clicking an autocomplete suggestion for a finding or observation will now add the item to the report

### Fixed

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
10 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 = "10 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
25 changes: 20 additions & 5 deletions ghostwriter/modules/reportwriter/richtext/docx.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ def text(self, el, *, par=None, style={}, **kwargs):
run._r.append(hyperlink)
# A workaround for the lack of a hyperlink style
if "Hyperlink" in self.doc.styles:
run.style = "Hyperlink"
try:
run.style = "Hyperlink"
except KeyError:
pass
else:
run.font.color.theme_color = MSO_THEME_COLOR_INDEX.HYPERLINK
run.font.underline = True
Expand All @@ -80,7 +83,10 @@ def text(self, el, *, par=None, style={}, **kwargs):
def style_run(self, run, style):
super().style_run(run, style)
if style.get("inline_code"):
run.style = "CodeInline"
try:
run.style = "CodeInline"
except KeyError:
pass
run.font.no_proof = True
if style.get("highlight"):
run.font.highlight_color = WD_COLOR_INDEX.YELLOW
Expand Down Expand Up @@ -297,7 +303,10 @@ def tag_span(self, el, *, par, **kwargs):
self.make_evidence(par, evidence)
elif "data-gw-caption" in el.attrs:
ref_name = el.attrs["data-gw-caption"]
par.style = "Caption"
try:
par.style = "Caption"
except KeyError:
pass
par._gw_is_caption = True
self.make_figure(par, ref_name or None)
elif "data-gw-ref" in el.attrs:
Expand Down Expand Up @@ -537,7 +546,10 @@ def create(self, doc):
level_list_is_ordered = self.level_list_is_ordered

# Create a new numbering
numbering = doc.part.numbering_part.numbering_definitions._numbering
try:
numbering = doc.part.numbering_part.numbering_definitions._numbering
except NotImplementedError as e:
raise ReportExportError("Tried to use a list in a template without list styles") from e
last_used_id = max(
(int(id) for id in numbering.xpath("w:abstractNum/@w:abstractNumId")),
default=-1,
Expand Down Expand Up @@ -594,6 +606,9 @@ def create(self, doc):
numbering_id = numbering.add_num(abstract_numbering_id).numId

for par, level in self.paragraphs:
par.style = "ListParagraph"
try:
par.style = "ListParagraph"
except KeyError:
pass
par._p.get_or_add_pPr().get_or_add_numPr().get_or_add_numId().val = numbering_id
par._p.get_or_add_pPr().get_or_add_numPr().get_or_add_ilvl().val = level
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 @@ -186,19 +186,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
Loading

0 comments on commit 721b4ac

Please sign in to comment.