Skip to content
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [7.2.5] - 27 July 2026

### Changed

* Improved logging for skipped signal imports and background task failures

### Fixed

* Corrected the AJAX URL for deleting report observations
* This fixed a typo in the path, but had no effect on functionality
* Fixed activity-log sanitization confirmation and scoped its CSRF header to its own request
* Fixed expired API and service token feedback so the appropriate token row and empty state are updated in the UI

### Security

* Hardened user-controlled values rendered in JavaScript contexts to prevent stored cross-site scripting
* Autocomplete data is now serialized as inert JSON instead of being interpolated into JavaScript source
* Tag autocomplete suggestions are scoped to objects the current user can access
* Additional inline JavaScript values and activity-log rich-text previews are escaped or sanitized for their output context
* These changes are related to [GHSA-5xvc-cm65-jw3p](https://github.com/GhostManager/Ghostwriter/security/advisories/GHSA-5xvc-cm65-jw3p), but go beyond that to further harden sanitization (Thank you to [@hippiiee](https://github.com/hippiiee) for reporting the original issue!)
* Added matching Django and Hasura validation for domain and static server names while preserving user access to create and manage shared inventory
* Restricted Django Q scheduled tasks to a server-controlled allowlist (Closes #911)
* The admin panel now exposes only approved functions, Slack notification hooks, and validated task arguments
Expand Down
4 changes: 2 additions & 2 deletions VERSION
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
v7.2.4
21 July 2026
v7.2.5
27 July 2026
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__ = "7.2.4"
__version__ = "7.2.5"
VERSION = __version__
RELEASE_DATE = "21 July 2026"
RELEASE_DATE = "27 July 2026"

ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
APPS_DIR = ROOT_DIR / "ghostwriter"
Expand Down
14 changes: 12 additions & 2 deletions ghostwriter/api/apps.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""This contains the configuration of the GraphQL application."""

# Standard Libraries
import logging

# Django Imports
from django.apps import AppConfig

logger = logging.getLogger(__name__)


class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
Expand All @@ -11,5 +16,10 @@ class ApiConfig(AppConfig):
def ready(self):
try:
import ghostwriter.graphql.signals # noqa F401 isort:skip
except ImportError:
pass
except ModuleNotFoundError as exception:
if exception.name not in {
"ghostwriter.graphql",
"ghostwriter.graphql.signals",
}:
raise
logger.debug("No GraphQL signal handlers are configured.")
Comment thread
chrismaddalena marked this conversation as resolved.
1 change: 0 additions & 1 deletion ghostwriter/api/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Standard Libraries
import logging
from datetime import datetime, timedelta
from zipfile import error

# Django Imports
from django.test import TestCase
Expand Down
11 changes: 9 additions & 2 deletions ghostwriter/commandcenter/apps.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
"""This contains the configuration of the CommandCenter application."""

# Standard Libraries
import logging

# Django Imports
from django.apps import AppConfig

logger = logging.getLogger(__name__)


class CommandCenterConfig(AppConfig):
name = "ghostwriter.commandcenter"

def ready(self):
try:
import ghostwriter.commandcenter.signals # noqa F401 isort:skip
except ImportError:
pass
except ModuleNotFoundError as exception:
if exception.name != "ghostwriter.commandcenter.signals":
raise
logger.debug("No CommandCenter signal handlers are configured.")
6 changes: 1 addition & 5 deletions ghostwriter/home/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@
# Django Imports
from django.apps import AppConfig


class HomeConfig(AppConfig):
name = "ghostwriter.home"

def ready(self):
try:
import ghostwriter.home.signals # noqa F401 isort:skip
except ImportError:
pass
import ghostwriter.home.signals # noqa F401 isort:skip

# Ghostwriter Libraries
from ghostwriter.home.django_q_integration import install_django_q_restrictions
Expand Down
6 changes: 6 additions & 0 deletions ghostwriter/home/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def get(self, request, *args, **kwargs):
if not db_status["default"] or not cache_status["default"]:
system_health = "WARNING"
except Exception: # pragma: no cover
logger.exception("Unable to retrieve dashboard system health.")
system_health = "ERROR"

# Assemble the context dictionary to pass to the dashboard
Expand Down Expand Up @@ -274,6 +275,7 @@ def post(self, request, *args, **kwargs):
)
message = "AWS access key test has been successfully queued."
except Exception: # pragma: no cover
logger.exception("Unable to queue AWS access key test.")
result = "error"
message = "AWS access key test could not be queued"

Expand Down Expand Up @@ -309,6 +311,7 @@ def post(self, request, *args, **kwargs):
)
message = "Digital Ocean API key test has been successfully queued."
except Exception: # pragma: no cover
logger.exception("Unable to queue Digital Ocean API key test.")
result = "error"
message = "Digital Ocean API key test could not be queued."

Expand Down Expand Up @@ -344,6 +347,7 @@ def post(self, request, *args, **kwargs):
)
message = "Namecheap API test has been successfully queued."
except Exception: # pragma: no cover
logger.exception("Unable to queue Namecheap API key test.")
result = "error"
message = "Namecheap API test could not be queued."

Expand Down Expand Up @@ -379,6 +383,7 @@ def post(self, request, *args, **kwargs):
)
message = "Slack Webhook test has been successfully queued."
except Exception: # pragma: no cover
logger.exception("Unable to queue Slack webhook test.")
result = "error"
message = "Slack Webhook test could not be queued."

Expand Down Expand Up @@ -414,6 +419,7 @@ def post(self, request, *args, **kwargs):
)
message = "VirusTotal API test has been successfully queued."
except Exception: # pragma: no cover
logger.exception("Unable to queue VirusTotal API key test.")
result = "error"
message = "VirusTotal API test could not be queued."

Expand Down
1 change: 1 addition & 0 deletions ghostwriter/modules/cloud_monitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class BearerAuth(requests.auth.AuthBase):
token = None

def __init__(self, token):
super().__init__()
self.token = token

def __call__(self, r):
Expand Down
4 changes: 1 addition & 3 deletions ghostwriter/modules/custom_layout_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def __init__(
helper_context_name=None,
object_context_name=None,
):
super().__init__()
self.formset_context_name = formset_context_name
self.helper_context_name = helper_context_name
if object_context_name:
Expand Down Expand Up @@ -71,9 +72,6 @@ class CustomTab(Container):
# Default CSS class for the tab pane
css_class = "tab-pane"

# Custom CSS for the ``nav-link`` element
# link_css_class = ""

def __init__(self, name, *fields, **kwargs):
link_css_class = kwargs.pop("link_css_class", None)
tab_hash_id = kwargs.pop("css_id", None) or slugify(name, allow_unicode=True)
Expand Down
1 change: 1 addition & 0 deletions ghostwriter/modules/reportwriter/base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ReportExportError(Exception):
code_context: str | None

def __init__(self, display_text: str, location: str | None = None, code_context: str | None = None):
super().__init__(display_text)
self.display_text = display_text
self.location = location
self.code_context = code_context
Expand Down
2 changes: 2 additions & 0 deletions ghostwriter/modules/reportwriter/base/html_rich_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ class LazilyRenderedTemplate(RichTextBase):
rendering: bool

def __init__(self, template: jinja2.Template, location: str | None, context: dict):
super().__init__()
self.template = template
self.context = context
self.location = location
Expand Down Expand Up @@ -189,6 +190,7 @@ class HtmlAndObject(RichTextBase):
obj: Any

def __init__(self, html: str, obj, location: str | None = None):
super().__init__()
self.html = html
self.obj = obj
self.location = location
Expand Down
6 changes: 1 addition & 5 deletions ghostwriter/oplog/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@
# Django Imports
from django.apps import AppConfig


class OplogConfig(AppConfig):
name = "ghostwriter.oplog"
verbose_name = "Activity Logging"

def ready(self):
try:
import ghostwriter.oplog.signals # noqa F401 isort:skip
except ImportError:
pass
import ghostwriter.oplog.signals # noqa F401 isort:skip
2 changes: 1 addition & 1 deletion ghostwriter/oplog/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def create_oplog_entry(oplog_id, user):
"Failed to create log entry for log ID %s because that log ID does not exist.",
oplog_id,
)
return
return None

if oplog.project.user_can_edit(user):
entry = OplogEntry.objects.create(
Expand Down
2 changes: 1 addition & 1 deletion ghostwriter/oplog/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def test_valid_data(self):
oplog = OplogFactory.build(project=project)
form = self.form_data(user=self.user, **oplog.__dict__)
self.assertFalse(form.is_valid())
self.assertTrue(form.errors.as_data()["project"][0].code == "invalid_choice")
self.assertEqual(form.errors.as_data()["project"][0].code, "invalid_choice")

ProjectAssignmentFactory(operator=self.user, project=project)
form = self.form_data(user=self.user, **oplog.__dict__)
Expand Down
2 changes: 1 addition & 1 deletion ghostwriter/oplog/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ def setUpTestData(cls):

cls.oplog = OplogFactory()
cls.num_of_entries = 5
for x in range(cls.num_of_entries):
for _ in range(cls.num_of_entries):
OplogEntryFactory(oplog_id=cls.oplog)

cls.user = UserFactory(password=PASSWORD)
Expand Down
6 changes: 1 addition & 5 deletions ghostwriter/reporting/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@
# Django Imports
from django.apps import AppConfig


class ReportingConfig(AppConfig):
name = "ghostwriter.reporting"

def ready(self):
try:
import ghostwriter.reporting.signals # noqa F401 isort:skip
except ImportError:
pass
import ghostwriter.reporting.signals # noqa F401 isort:skip
3 changes: 1 addition & 2 deletions ghostwriter/reporting/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
ProjectAssignmentFactory,
ProjectFactory,
ReportFactory,
ReportFindingLinkFactory,
ReportObservationLinkFactory,
ReportDocxTemplateFactory,
ReportPptxTemplateFactory,
Expand Down Expand Up @@ -82,7 +81,7 @@ def test_valid_data(self):
report = self.report_dict.copy()
form = self.form_data(user=self.user, **report)
self.assertFalse(form.is_valid())
self.assertTrue(form.errors.as_data()["project"][0].code == "invalid_choice")
self.assertEqual(form.errors.as_data()["project"][0].code, "invalid_choice")

ProjectAssignmentFactory(operator=self.user, project=self.project)
form = self.form_data(user=self.user, **report)
Expand Down
2 changes: 1 addition & 1 deletion ghostwriter/reporting/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def test_prop_count(self):

def test_adjust_severity_weight_signals(self):
self.Severity.objects.all().delete()
self.assertTrue(self.Severity.objects.all().count() == 0)
self.assertEqual(self.Severity.objects.all().count(), 0)

critical = SeverityFactory(severity="Critical", weight=2, color="FFFFFF")
high = SeverityFactory(severity="High", weight=2, color="FFF000")
Expand Down
Loading
Loading