diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd3db0a2..475d19d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index 0852c00de..59a108f1f 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -v7.2.4 -21 July 2026 +v7.2.5 +27 July 2026 diff --git a/config/settings/base.py b/config/settings/base.py index ec619f7bc..a5c4bdfd1 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -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" diff --git a/ghostwriter/api/apps.py b/ghostwriter/api/apps.py index 3e13120b1..61962dee8 100644 --- a/ghostwriter/api/apps.py +++ b/ghostwriter/api/apps.py @@ -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" @@ -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.") diff --git a/ghostwriter/api/tests/test_forms.py b/ghostwriter/api/tests/test_forms.py index cd09f05ef..18fcb642e 100644 --- a/ghostwriter/api/tests/test_forms.py +++ b/ghostwriter/api/tests/test_forms.py @@ -1,7 +1,6 @@ # Standard Libraries import logging from datetime import datetime, timedelta -from zipfile import error # Django Imports from django.test import TestCase diff --git a/ghostwriter/commandcenter/apps.py b/ghostwriter/commandcenter/apps.py index 36e93b394..780216389 100644 --- a/ghostwriter/commandcenter/apps.py +++ b/ghostwriter/commandcenter/apps.py @@ -1,8 +1,13 @@ """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" @@ -10,5 +15,7 @@ class CommandCenterConfig(AppConfig): 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.") diff --git a/ghostwriter/home/apps.py b/ghostwriter/home/apps.py index 257f506f3..7c6905d5b 100644 --- a/ghostwriter/home/apps.py +++ b/ghostwriter/home/apps.py @@ -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 diff --git a/ghostwriter/home/views.py b/ghostwriter/home/views.py index b75850239..56175d1f5 100644 --- a/ghostwriter/home/views.py +++ b/ghostwriter/home/views.py @@ -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 @@ -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" @@ -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." @@ -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." @@ -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." @@ -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." diff --git a/ghostwriter/modules/cloud_monitors.py b/ghostwriter/modules/cloud_monitors.py index 2da4043c6..d798a0eff 100644 --- a/ghostwriter/modules/cloud_monitors.py +++ b/ghostwriter/modules/cloud_monitors.py @@ -29,6 +29,7 @@ class BearerAuth(requests.auth.AuthBase): token = None def __init__(self, token): + super().__init__() self.token = token def __call__(self, r): diff --git a/ghostwriter/modules/custom_layout_object.py b/ghostwriter/modules/custom_layout_object.py index e4e369bd7..a101bc26a 100644 --- a/ghostwriter/modules/custom_layout_object.py +++ b/ghostwriter/modules/custom_layout_object.py @@ -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: @@ -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) diff --git a/ghostwriter/modules/reportwriter/base/__init__.py b/ghostwriter/modules/reportwriter/base/__init__.py index 890713996..ca96d6a37 100644 --- a/ghostwriter/modules/reportwriter/base/__init__.py +++ b/ghostwriter/modules/reportwriter/base/__init__.py @@ -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 diff --git a/ghostwriter/modules/reportwriter/base/html_rich_text.py b/ghostwriter/modules/reportwriter/base/html_rich_text.py index edd2352c9..73ba4dd08 100644 --- a/ghostwriter/modules/reportwriter/base/html_rich_text.py +++ b/ghostwriter/modules/reportwriter/base/html_rich_text.py @@ -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 @@ -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 diff --git a/ghostwriter/oplog/apps.py b/ghostwriter/oplog/apps.py index a23b1bac3..f99f48154 100644 --- a/ghostwriter/oplog/apps.py +++ b/ghostwriter/oplog/apps.py @@ -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 diff --git a/ghostwriter/oplog/consumers.py b/ghostwriter/oplog/consumers.py index 8913b6739..920eb964c 100644 --- a/ghostwriter/oplog/consumers.py +++ b/ghostwriter/oplog/consumers.py @@ -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( diff --git a/ghostwriter/oplog/tests/test_forms.py b/ghostwriter/oplog/tests/test_forms.py index f15843d77..bdf6e641f 100644 --- a/ghostwriter/oplog/tests/test_forms.py +++ b/ghostwriter/oplog/tests/test_forms.py @@ -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__) diff --git a/ghostwriter/oplog/tests/test_views.py b/ghostwriter/oplog/tests/test_views.py index c1bed8029..c63753956 100644 --- a/ghostwriter/oplog/tests/test_views.py +++ b/ghostwriter/oplog/tests/test_views.py @@ -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) diff --git a/ghostwriter/reporting/apps.py b/ghostwriter/reporting/apps.py index f59ed3196..0c4606175 100644 --- a/ghostwriter/reporting/apps.py +++ b/ghostwriter/reporting/apps.py @@ -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 diff --git a/ghostwriter/reporting/tests/test_forms.py b/ghostwriter/reporting/tests/test_forms.py index 76127e25b..27ce4d5ac 100644 --- a/ghostwriter/reporting/tests/test_forms.py +++ b/ghostwriter/reporting/tests/test_forms.py @@ -14,7 +14,6 @@ ProjectAssignmentFactory, ProjectFactory, ReportFactory, - ReportFindingLinkFactory, ReportObservationLinkFactory, ReportDocxTemplateFactory, ReportPptxTemplateFactory, @@ -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) diff --git a/ghostwriter/reporting/tests/test_models.py b/ghostwriter/reporting/tests/test_models.py index 5b6c2740a..05c67068c 100644 --- a/ghostwriter/reporting/tests/test_models.py +++ b/ghostwriter/reporting/tests/test_models.py @@ -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") diff --git a/ghostwriter/reporting/tests/test_views.py b/ghostwriter/reporting/tests/test_views.py index 8a78d84e2..659ca7171 100644 --- a/ghostwriter/reporting/tests/test_views.py +++ b/ghostwriter/reporting/tests/test_views.py @@ -13,7 +13,7 @@ from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from django.template.loader import render_to_string -from django.test import Client, TestCase +from django.test import Client, SimpleTestCase, TestCase from django.test import override_settings from django.urls import reverse from django.utils.dateformat import format as dateformat @@ -87,6 +87,16 @@ PASSWORD = "SuperNaturalReporting!" +class ReportObservationLinkDeleteUrlTests(SimpleTestCase): + """Tests for the reported-observation delete URL.""" + + def test_url_uses_correct_observation_spelling(self): + self.assertEqual( + reverse("reporting:ajax_delete_local_observation", kwargs={"pk": 1}), + "/reporting/ajax/observation/delete/1", + ) + + class IndexViewTests(TestCase): """Collection of tests for :view:`reporting.index`.""" @@ -123,7 +133,7 @@ class TemplateTagTests(TestCase): def setUpTestData(cls): cls.ReportFindingLink = ReportFindingLinkFactory._meta.model cls.report = ReportFactory() - for x in range(3): + for _ in range(3): ReportFindingLinkFactory(report=cls.report) def setUp(self): @@ -147,10 +157,10 @@ def test_file_filers(self): deleted_evidence = EvidenceFactory() os.remove(deleted_evidence.document.path) - self.assertTrue(report_tags.get_file_type(img_evidence) == "image") - self.assertTrue(report_tags.get_file_type(txt_evidence) == "text") - self.assertTrue(report_tags.get_file_type(unknown_evidence) == "unknown") - self.assertTrue(report_tags.get_file_type(deleted_evidence) == "missing") + self.assertEqual(report_tags.get_file_type(img_evidence), "image") + self.assertEqual(report_tags.get_file_type(txt_evidence), "text") + self.assertEqual(report_tags.get_file_type(unknown_evidence), "unknown") + self.assertEqual(report_tags.get_file_type(deleted_evidence), "missing") self.assertEqual(report_tags.get_file_content(txt_evidence), "lorem ipsum") self.assertEqual( @@ -570,8 +580,6 @@ def test_form_error_template_uses_escapejs_filter(self): so without |escapejs a crafted extension (e.g. containing a newline or backslash) could break the surrounding JS string literal. """ - import re - template_path = os.path.normpath( os.path.join( os.path.dirname(__file__), @@ -999,17 +1007,17 @@ def test_custom_context_exists(self): def test_lists_all_findings(self): response = self.client_auth.get(self.uri) self.assertEqual(response.status_code, 200) - self.assertTrue(len(response.context["filter"].qs) == len(self.findings)) + self.assertEqual(len(response.context["filter"].qs), len(self.findings)) def test_search_findings(self): response = self.client_auth.get(self.uri + "?finding=Finding+2") self.assertEqual(response.status_code, 200) - self.assertTrue(len(response.context["filter"].qs) == 1) + self.assertEqual(len(response.context["filter"].qs), 1) def test_filter_findings(self): response = self.client_auth.get(self.uri + "?title=Finding+2&submit=Filter") self.assertEqual(response.status_code, 200) - self.assertTrue(len(response.context["filter"].qs) == 1) + self.assertEqual(len(response.context["filter"].qs), 1) def test_tags_are_scoped_to_findings(self): visible_finding = FindingFactory(title="Tagged Finding") @@ -1039,7 +1047,7 @@ def test_search_report_findings(self): response = self.client_auth.get(self.uri + "?on_reports=on¬_cloned=on") self.assertEqual(response.status_code, 200) - self.assertTrue(len(response.context["filter"].qs) == 1) + self.assertEqual(len(response.context["filter"].qs), 1) blank_findings = self.ReportFindingLink.objects.filter( added_as_blank=True, report=self.accessibleReport ) @@ -1403,19 +1411,19 @@ def test_custom_context_exists(self): def test_lists_all_reports(self): response = self.client_mgr.get(self.uri) self.assertEqual(response.status_code, 200) - self.assertTrue(len(response.context["filter"].qs) == len(self.reports)) + self.assertEqual(len(response.context["filter"].qs), len(self.reports)) def test_lists_filtered_reports(self): response = self.client_auth.get(self.uri) self.assertEqual(response.status_code, 200) - self.assertTrue(len(response.context["filter"].qs) == 0) + self.assertEqual(len(response.context["filter"].qs), 0) for report in self.reports[:5]: ProjectAssignmentFactory(project=report.project, operator=self.user) response = self.client_auth.get(self.uri) self.assertEqual(response.status_code, 200) - self.assertTrue(len(response.context["filter"].qs) == 5) + self.assertEqual(len(response.context["filter"].qs), 5) def test_tags_are_scoped_to_visible_reports(self): visible_report = ReportFactory(title="Visible Report") diff --git a/ghostwriter/reporting/urls.py b/ghostwriter/reporting/urls.py index b16099105..7470d8793 100644 --- a/ghostwriter/reporting/urls.py +++ b/ghostwriter/reporting/urls.py @@ -91,7 +91,7 @@ name="ajax_assign_observation", ), path( - "ajax/obseravation/delete/", + "ajax/observation/delete/", ghostwriter.reporting.views2.report_observation_link.ReportObservationLinkDelete.as_view(), name="ajax_delete_local_observation", ), diff --git a/ghostwriter/rolodex/apps.py b/ghostwriter/rolodex/apps.py index 2c03c62a2..d6f0466f9 100644 --- a/ghostwriter/rolodex/apps.py +++ b/ghostwriter/rolodex/apps.py @@ -3,13 +3,9 @@ # Django Imports from django.apps import AppConfig - class RolodexConfig(AppConfig): name = "ghostwriter.rolodex" verbose_name = "Clients & Projects" def ready(self): - try: - import ghostwriter.rolodex.signals # noqa F401 isort:skip - except ImportError: - pass + import ghostwriter.rolodex.signals # noqa F401 isort:skip diff --git a/ghostwriter/rolodex/templates/rolodex/client_detail.html b/ghostwriter/rolodex/templates/rolodex/client_detail.html index a7b5da7e6..855987053 100644 --- a/ghostwriter/rolodex/templates/rolodex/client_detail.html +++ b/ghostwriter/rolodex/templates/rolodex/client_detail.html @@ -573,7 +573,7 @@

Client Notes

{% comment %} Insert modals for additional domain details {% endcomment %} {% for project in projects %} {% if project.current_user_can_view %} - {% include "snippets/project_history_detail_modal.html" with checkout=checkout %} + {% include "snippets/project_history_detail_modal.html" %} {% endif %} {% endfor %} diff --git a/ghostwriter/rolodex/tests/test_models.py b/ghostwriter/rolodex/tests/test_models.py index ffdff49dc..16c9fa522 100644 --- a/ghostwriter/rolodex/tests/test_models.py +++ b/ghostwriter/rolodex/tests/test_models.py @@ -232,7 +232,7 @@ def test_checkout_adjustment_signal(self): def test_prop_count_findings(self): project = ProjectFactory() report = ReportFactory(project=project) - for x in range(3): + for _ in range(3): ReportFindingLinkFactory(report=report) self.assertEqual(project.count_findings(), 3) diff --git a/ghostwriter/rolodex/tests/test_views.py b/ghostwriter/rolodex/tests/test_views.py index 1259ab441..6d4fb7c35 100644 --- a/ghostwriter/rolodex/tests/test_views.py +++ b/ghostwriter/rolodex/tests/test_views.py @@ -103,7 +103,7 @@ class TemplateTagTests(TestCase): def setUpTestData(cls): cls.ProjectObjective = ProjectObjectiveFactory._meta.model cls.project = ProjectFactory() - for x in range(3): + for _ in range(3): ProjectObjectiveFactory(project=cls.project) cls.server = StaticServerFactory() diff --git a/ghostwriter/shepherd/apps.py b/ghostwriter/shepherd/apps.py index 539106274..f0823aa5d 100644 --- a/ghostwriter/shepherd/apps.py +++ b/ghostwriter/shepherd/apps.py @@ -3,13 +3,9 @@ # Django Imports from django.apps import AppConfig - class ShepherdConfig(AppConfig): name = "ghostwriter.shepherd" verbose_name = "Infrastructure Management" def ready(self): - try: - import ghostwriter.shepherd.signals # noqa F401 isort:skip - except ImportError: - pass + import ghostwriter.shepherd.signals # noqa F401 isort:skip diff --git a/ghostwriter/shepherd/external/bloodhound/client.py b/ghostwriter/shepherd/external/bloodhound/client.py index 39b5d1059..7099afa2a 100644 --- a/ghostwriter/shepherd/external/bloodhound/client.py +++ b/ghostwriter/shepherd/external/bloodhound/client.py @@ -493,7 +493,7 @@ def _build_target_entry(finding: dict, severity: str) -> dict: grouped[unique_key]["principals"].append(_build_target_entry(finding, severity)) # Now we set the ``severity`` at the top level based on the highest severity of its target(s) - for finding_key, finding_value in grouped.items(): + for _, finding_value in grouped.items(): highest_severity = "Low" for target in finding_value["principals"]: target_severity = target.get("severity", "Low") diff --git a/ghostwriter/shepherd/tasks.py b/ghostwriter/shepherd/tasks.py index 5d0000ab3..d16612853 100644 --- a/ghostwriter/shepherd/tasks.py +++ b/ghostwriter/shepherd/tasks.py @@ -62,6 +62,7 @@ class BearerAuth(requests.auth.AuthBase): token = None def __init__(self, token): + super().__init__() self.token = token def __call__(self, r): diff --git a/ghostwriter/shepherd/tests/test_forms.py b/ghostwriter/shepherd/tests/test_forms.py index 958ea3253..5f38969c2 100644 --- a/ghostwriter/shepherd/tests/test_forms.py +++ b/ghostwriter/shepherd/tests/test_forms.py @@ -141,7 +141,7 @@ def test_valid_data(self): checkout = HistoryFactory(client=self.project.client, project=self.project, domain=self.domain) form = self.form_data(**checkout.__dict__, user=self.other_user) self.assertFalse(form.is_valid()) - self.assertTrue(form.errors.as_data()["client"][0].code == "invalid_choice") + self.assertEqual(form.errors.as_data()["client"][0].code, "invalid_choice") form = self.form_data(**checkout.__dict__, user=self.user) self.assertTrue(form.is_valid()) @@ -638,7 +638,7 @@ def test_valid_data(self): checkout = ServerHistoryFactory(client=self.project.client, project=self.project, server=self.server) form = self.form_data(**checkout.__dict__, user=self.other_user) self.assertFalse(form.is_valid()) - self.assertTrue(form.errors.as_data()["client"][0].code == "invalid_choice") + self.assertEqual(form.errors.as_data()["client"][0].code, "invalid_choice") form = self.form_data(**checkout.__dict__, user=self.user) self.assertTrue(form.is_valid()) diff --git a/ghostwriter/shepherd/tests/test_models.py b/ghostwriter/shepherd/tests/test_models.py index 956b6654f..fa634aa0e 100644 --- a/ghostwriter/shepherd/tests/test_models.py +++ b/ghostwriter/shepherd/tests/test_models.py @@ -323,12 +323,12 @@ def test_delete_signal(self): # Deleting this older checkout should not impact the domain's status history_1.delete() domain.refresh_from_db() - self.assertTrue(domain.domain_status == self.unavailable_status) + self.assertEqual(domain.domain_status, self.unavailable_status) # Deleting this newer checkout should impact the domain's status history_2.delete() domain.refresh_from_db() - self.assertTrue(domain.domain_status == self.available_status) + self.assertEqual(domain.domain_status, self.available_status) class ServerStatusModelTests(TestCase): @@ -591,12 +591,12 @@ def test_delete_signal(self): # Deleting this older checkout should not impact the server's status history_1.delete() server.refresh_from_db() - self.assertTrue(server.server_status == self.unavailable_status) + self.assertEqual(server.server_status, self.unavailable_status) # Deleting this newer checkout should impact the server's status history_2.delete() server.refresh_from_db() - self.assertTrue(server.server_status == self.available_status) + self.assertEqual(server.server_status, self.available_status) class DomainServerConnectionModelTests(TestCase): diff --git a/ghostwriter/shepherd/tests/test_views.py b/ghostwriter/shepherd/tests/test_views.py index 8363cc691..9b1e239a8 100644 --- a/ghostwriter/shepherd/tests/test_views.py +++ b/ghostwriter/shepherd/tests/test_views.py @@ -799,8 +799,8 @@ def test_domain_release_failure(self): "message": "You do not have permission to release this domain.", } self.assertJSONEqual(force_str(response.content), data) - self.assertFalse(self.domain.domain_status == self.available_status) - self.assertTrue(self.checkout.end_date == self.end_date) + self.assertNotEqual(self.domain.domain_status, self.available_status) + self.assertEqual(self.checkout.end_date, self.end_date) # Tests related to :model:`shepherd.StaticServer` @@ -1331,8 +1331,8 @@ def test_server_release_failure(self): "message": "You do not have permission to release this server.", } self.assertJSONEqual(force_str(response.content), data) - self.assertFalse(self.server.server_status == self.available_status) - self.assertTrue(self.checkout.end_date == self.end_date) + self.assertNotEqual(self.server.server_status, self.available_status) + self.assertEqual(self.checkout.end_date, self.end_date) # Tests related to :model:`shepherd.TransientServer` @@ -1684,7 +1684,7 @@ def setUpTestData(cls): domain_status = DomainStatusFactory(domain_status="Unavailable") server_status = ServerStatusFactory(server_status="Unavailable") - for x in range(3): + for _ in range(3): HistoryFactory(operator=cls.user, domain=DomainFactory(domain_status=domain_status)) ServerHistoryFactory( operator=cls.user, diff --git a/ghostwriter/shepherd/views.py b/ghostwriter/shepherd/views.py index 72ba6f783..2dd66fa14 100644 --- a/ghostwriter/shepherd/views.py +++ b/ghostwriter/shepherd/views.py @@ -904,7 +904,7 @@ def get_queryset(self): self.autocomplete.append(domain.name) if domain.categorization: try: - for key, value in domain.categorization.items(): + for _, value in domain.categorization.items(): if "," in value: for item in value.split(","): self.autocomplete.append(item.strip().lower()) diff --git a/ghostwriter/singleton/apps.py b/ghostwriter/singleton/apps.py index b4068d75a..9ba876d4f 100644 --- a/ghostwriter/singleton/apps.py +++ b/ghostwriter/singleton/apps.py @@ -1,8 +1,13 @@ """This contains the configuration of the Singleton application.""" +# Standard Libraries +import logging + # Django Imports from django.apps import AppConfig +logger = logging.getLogger(__name__) + class SingletonConfig(AppConfig): name = "ghostwriter.singleton" @@ -10,5 +15,7 @@ class SingletonConfig(AppConfig): def ready(self): try: import ghostwriter.singleton.signals # noqa F401 isort:skip - except ImportError: - pass + except ModuleNotFoundError as exception: + if exception.name != "ghostwriter.singleton.signals": + raise + logger.debug("No Singleton signal handlers are configured.") diff --git a/ghostwriter/status/apps.py b/ghostwriter/status/apps.py index 6f1c30cc3..4a0499e77 100644 --- a/ghostwriter/status/apps.py +++ b/ghostwriter/status/apps.py @@ -1,8 +1,13 @@ """This contains all the views used by the Status application.""" +# Standard Libraries +import logging + # Django Imports from django.apps import AppConfig +logger = logging.getLogger(__name__) + class StatusConfig(AppConfig): name = "ghostwriter.status" @@ -10,5 +15,7 @@ class StatusConfig(AppConfig): def ready(self): try: import ghostwriter.status.signals # noqa F401 isort:skip - except ImportError: - pass + except ModuleNotFoundError as exception: + if exception.name != "ghostwriter.status.signals": + raise + logger.debug("No Status signal handlers are configured.") diff --git a/ghostwriter/templates/confirm_revoke_modal.html b/ghostwriter/templates/confirm_revoke_modal.html index 83262121c..674a324a9 100644 --- a/ghostwriter/templates/confirm_revoke_modal.html +++ b/ghostwriter/templates/confirm_revoke_modal.html @@ -89,7 +89,13 @@ cell.colSpan = 7 cell.innerHTML = 'Add ' + placeholder_group + ' findings or drag-and-drop a finding here to update its severity.' } else { - $targetTable.hide(); + let $tokenSection = $targetTable.closest('.js-token-table-section'); + if ($tokenSection.length) { + $targetTable.closest('.js-token-table-responsive').addClass('d-none'); + $tokenSection.find('.js-token-table-empty-alert').removeClass('d-none').show(); + } else { + $targetTable.hide(); + } } } update_badges(); diff --git a/ghostwriter/templates/confirm_sanitize_modal.html b/ghostwriter/templates/confirm_sanitize_modal.html index 2797632ae..c6b7285a8 100644 --- a/ghostwriter/templates/confirm_sanitize_modal.html +++ b/ghostwriter/templates/confirm_sanitize_modal.html @@ -16,7 +16,7 @@ @@ -42,7 +42,7 @@ let targetId = $target.attr('sanitize-target-id'); let $previewContent = $('#key_name_' + targetId); - if ($previewContent) { + if ($previewContent.length) { $('#object-preview-content').html(escapeHtml($previewContent.text())); } }); @@ -86,19 +86,14 @@ let formData = $('#sanitize-checklist-form').serializeArray(); formData = JSON.stringify(formData); - console.log(formData) - $.ajaxSetup({ - beforeSend: function (xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - } - }); $.ajax({ url: url, type: 'POST', dataType: 'json', + headers: { + 'X-CSRFToken': csrftoken, + }, data: { 'target': targetId, 'fields': formData, diff --git a/ghostwriter/users/apps.py b/ghostwriter/users/apps.py index 87e56034c..4dfb22c85 100644 --- a/ghostwriter/users/apps.py +++ b/ghostwriter/users/apps.py @@ -1,9 +1,14 @@ """This contains the configuration of the Users application.""" +# Standard Libraries +import logging + # Django Imports from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ +logger = logging.getLogger(__name__) + class UsersConfig(AppConfig): name = "ghostwriter.users" @@ -12,5 +17,7 @@ class UsersConfig(AppConfig): def ready(self): try: import ghostwriter.users.signals # noqa F401 isort:skip - except ImportError: - pass + except ModuleNotFoundError as exception: + if exception.name != "ghostwriter.users.signals": + raise + logger.debug("No Users signal handlers are configured.") diff --git a/ghostwriter/users/templates/users/profile.html b/ghostwriter/users/templates/users/profile.html index 69090825a..d0ba3ccd6 100644 --- a/ghostwriter/users/templates/users/profile.html +++ b/ghostwriter/users/templates/users/profile.html @@ -507,7 +507,9 @@