From 752dec7b29094d387544dba0179747cf215f5cfe Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 10:48:45 -0700 Subject: [PATCH 01/19] Improve test assertion precision --- ghostwriter/api/tests/test_forms.py | 1 - ghostwriter/modules/custom_layout_object.py | 3 --- ghostwriter/oplog/tests/test_forms.py | 2 +- ghostwriter/reporting/tests/test_forms.py | 3 +-- ghostwriter/reporting/tests/test_models.py | 2 +- ghostwriter/reporting/tests/test_views.py | 22 ++++++++++----------- ghostwriter/shepherd/tests/test_forms.py | 4 ++-- ghostwriter/shepherd/tests/test_models.py | 8 ++++---- ghostwriter/shepherd/tests/test_views.py | 8 ++++---- 9 files changed, 24 insertions(+), 29 deletions(-) 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/modules/custom_layout_object.py b/ghostwriter/modules/custom_layout_object.py index e4e369bd7..8daf32f1b 100644 --- a/ghostwriter/modules/custom_layout_object.py +++ b/ghostwriter/modules/custom_layout_object.py @@ -71,9 +71,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/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/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..aad9725af 100644 --- a/ghostwriter/reporting/tests/test_views.py +++ b/ghostwriter/reporting/tests/test_views.py @@ -147,10 +147,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( @@ -999,17 +999,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 +1039,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 +1403,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/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..1a76586c7 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` From 0cc0544369bb61ce75597f7b65ab377c8c6d4068 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 11:02:35 -0700 Subject: [PATCH 02/19] Initialize base classes explicitly --- ghostwriter/modules/cloud_monitors.py | 1 + ghostwriter/modules/custom_layout_object.py | 1 + ghostwriter/modules/reportwriter/base/__init__.py | 1 + ghostwriter/modules/reportwriter/base/html_rich_text.py | 2 ++ ghostwriter/shepherd/tasks.py | 1 + 5 files changed, 6 insertions(+) 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 8daf32f1b..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: 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/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): From c80701875d963c1e71886c58e6f23b1764adcc8b Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 11:09:11 -0700 Subject: [PATCH 03/19] Log skipped signal handler imports --- ghostwriter/api/apps.py | 7 ++++++- ghostwriter/commandcenter/apps.py | 7 ++++++- ghostwriter/home/apps.py | 7 ++++++- ghostwriter/oplog/apps.py | 7 ++++++- ghostwriter/reporting/apps.py | 7 ++++++- ghostwriter/rolodex/apps.py | 7 ++++++- ghostwriter/shepherd/apps.py | 7 ++++++- ghostwriter/singleton/apps.py | 7 ++++++- ghostwriter/status/apps.py | 7 ++++++- ghostwriter/users/apps.py | 7 ++++++- 10 files changed, 60 insertions(+), 10 deletions(-) diff --git a/ghostwriter/api/apps.py b/ghostwriter/api/apps.py index 3e13120b1..bbbbc84a0 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" @@ -12,4 +17,4 @@ def ready(self): try: import ghostwriter.graphql.signals # noqa F401 isort:skip except ImportError: - pass + logger.debug("No GraphQL signal handlers are configured.") diff --git a/ghostwriter/commandcenter/apps.py b/ghostwriter/commandcenter/apps.py index 36e93b394..4c351e9ba 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" @@ -11,4 +16,4 @@ def ready(self): try: import ghostwriter.commandcenter.signals # noqa F401 isort:skip except ImportError: - pass + logger.debug("No CommandCenter signal handlers are configured.") diff --git a/ghostwriter/home/apps.py b/ghostwriter/home/apps.py index 257f506f3..1886b8d41 100644 --- a/ghostwriter/home/apps.py +++ b/ghostwriter/home/apps.py @@ -1,8 +1,13 @@ """This contains the configuration of the Home application.""" +# Standard Libraries +import logging + # Django Imports from django.apps import AppConfig +logger = logging.getLogger(__name__) + class HomeConfig(AppConfig): name = "ghostwriter.home" @@ -11,7 +16,7 @@ def ready(self): try: import ghostwriter.home.signals # noqa F401 isort:skip except ImportError: - pass + logger.warning("Unable to import Home signal handlers.", exc_info=True) # Ghostwriter Libraries from ghostwriter.home.django_q_integration import install_django_q_restrictions diff --git a/ghostwriter/oplog/apps.py b/ghostwriter/oplog/apps.py index a23b1bac3..e584a4501 100644 --- a/ghostwriter/oplog/apps.py +++ b/ghostwriter/oplog/apps.py @@ -1,8 +1,13 @@ """This contains the configuration of the Oplog application.""" +# Standard Libraries +import logging + # Django Imports from django.apps import AppConfig +logger = logging.getLogger(__name__) + class OplogConfig(AppConfig): name = "ghostwriter.oplog" @@ -12,4 +17,4 @@ def ready(self): try: import ghostwriter.oplog.signals # noqa F401 isort:skip except ImportError: - pass + logger.warning("Unable to import Oplog signal handlers.", exc_info=True) diff --git a/ghostwriter/reporting/apps.py b/ghostwriter/reporting/apps.py index f59ed3196..8f06c2c8b 100644 --- a/ghostwriter/reporting/apps.py +++ b/ghostwriter/reporting/apps.py @@ -1,8 +1,13 @@ """This contains the configuration of the Reporting application.""" +# Standard Libraries +import logging + # Django Imports from django.apps import AppConfig +logger = logging.getLogger(__name__) + class ReportingConfig(AppConfig): name = "ghostwriter.reporting" @@ -11,4 +16,4 @@ def ready(self): try: import ghostwriter.reporting.signals # noqa F401 isort:skip except ImportError: - pass + logger.warning("Unable to import Reporting signal handlers.", exc_info=True) diff --git a/ghostwriter/rolodex/apps.py b/ghostwriter/rolodex/apps.py index 2c03c62a2..0f44082be 100644 --- a/ghostwriter/rolodex/apps.py +++ b/ghostwriter/rolodex/apps.py @@ -1,8 +1,13 @@ """This contains the configuration of the Rolodex application.""" +# Standard Libraries +import logging + # Django Imports from django.apps import AppConfig +logger = logging.getLogger(__name__) + class RolodexConfig(AppConfig): name = "ghostwriter.rolodex" @@ -12,4 +17,4 @@ def ready(self): try: import ghostwriter.rolodex.signals # noqa F401 isort:skip except ImportError: - pass + logger.warning("Unable to import Rolodex signal handlers.", exc_info=True) diff --git a/ghostwriter/shepherd/apps.py b/ghostwriter/shepherd/apps.py index 539106274..221f62ed7 100644 --- a/ghostwriter/shepherd/apps.py +++ b/ghostwriter/shepherd/apps.py @@ -1,8 +1,13 @@ """This contains the configuration of the Shepherd application.""" +# Standard Libraries +import logging + # Django Imports from django.apps import AppConfig +logger = logging.getLogger(__name__) + class ShepherdConfig(AppConfig): name = "ghostwriter.shepherd" @@ -12,4 +17,4 @@ def ready(self): try: import ghostwriter.shepherd.signals # noqa F401 isort:skip except ImportError: - pass + logger.warning("Unable to import Shepherd signal handlers.", exc_info=True) diff --git a/ghostwriter/singleton/apps.py b/ghostwriter/singleton/apps.py index b4068d75a..226842c68 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" @@ -11,4 +16,4 @@ def ready(self): try: import ghostwriter.singleton.signals # noqa F401 isort:skip except ImportError: - pass + logger.debug("No Singleton signal handlers are configured.") diff --git a/ghostwriter/status/apps.py b/ghostwriter/status/apps.py index 6f1c30cc3..8a7fb4cba 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" @@ -11,4 +16,4 @@ def ready(self): try: import ghostwriter.status.signals # noqa F401 isort:skip except ImportError: - pass + logger.debug("No Status signal handlers are configured.") diff --git a/ghostwriter/users/apps.py b/ghostwriter/users/apps.py index 87e56034c..2a3e51a66 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" @@ -13,4 +18,4 @@ def ready(self): try: import ghostwriter.users.signals # noqa F401 isort:skip except ImportError: - pass + logger.debug("No Users signal handlers are configured.") From 7504465d6c3a8a2218c9db7af66012e371164d79 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 11:12:00 -0700 Subject: [PATCH 04/19] Reuse reporting test imports --- ghostwriter/reporting/tests/test_views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ghostwriter/reporting/tests/test_views.py b/ghostwriter/reporting/tests/test_views.py index aad9725af..abe6c1706 100644 --- a/ghostwriter/reporting/tests/test_views.py +++ b/ghostwriter/reporting/tests/test_views.py @@ -570,8 +570,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__), From ba2e5f2e0466c0d4539bd355f816fbdb5705e159 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 11:13:02 -0700 Subject: [PATCH 05/19] Log dashboard task queue failures --- ghostwriter/home/views.py | 6 ++++++ 1 file changed, 6 insertions(+) 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." From c1ca9dfbf4a9460287440b05a26d36cd61f721a3 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 11:14:09 -0700 Subject: [PATCH 06/19] Make oplog entry failures explicit --- ghostwriter/oplog/consumers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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( From 5619985ec47c9c9c492a8ed04edd49d462f64ed9 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 11:16:23 -0700 Subject: [PATCH 07/19] Mark intentionally unused variables --- ghostwriter/oplog/tests/test_views.py | 2 +- ghostwriter/reporting/tests/test_views.py | 2 +- ghostwriter/rolodex/tests/test_models.py | 2 +- ghostwriter/rolodex/tests/test_views.py | 2 +- ghostwriter/shepherd/external/bloodhound/client.py | 2 +- ghostwriter/shepherd/tests/test_views.py | 4 ++-- ghostwriter/shepherd/views.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) 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/tests/test_views.py b/ghostwriter/reporting/tests/test_views.py index abe6c1706..f21702f88 100644 --- a/ghostwriter/reporting/tests/test_views.py +++ b/ghostwriter/reporting/tests/test_views.py @@ -123,7 +123,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): 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/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/tests/test_views.py b/ghostwriter/shepherd/tests/test_views.py index 1a76586c7..39d69a201 100644 --- a/ghostwriter/shepherd/tests/test_views.py +++ b/ghostwriter/shepherd/tests/test_views.py @@ -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, @@ -1734,7 +1734,7 @@ def setUpTestData(cls): cls.servers = [] cls.addresses = [] cls.cloud_servers = [] - for x in range(3): + for _ in range(3): server = StaticServerFactory(ip_address=f"192.168.1.{x}") addy = AuxServerAddressFactory(ip_address=f"192.168.2.{x}", static_server=server) vps = TransientServerFactory(ip_address=f"192.168.3.{x}") 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()) From c6871c64c176a26d51463c7cacc163c61bc3e580 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 11:25:36 -0700 Subject: [PATCH 08/19] Correct sanitize modal interactions --- ghostwriter/rolodex/templates/rolodex/client_detail.html | 2 +- ghostwriter/templates/confirm_sanitize_modal.html | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ghostwriter/rolodex/templates/rolodex/client_detail.html b/ghostwriter/rolodex/templates/rolodex/client_detail.html index a7b5da7e6..f60f2d882 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" with checkout=project %} {% endif %} {% endfor %} diff --git a/ghostwriter/templates/confirm_sanitize_modal.html b/ghostwriter/templates/confirm_sanitize_modal.html index 2797632ae..27e539060 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,7 +86,6 @@ let formData = $('#sanitize-checklist-form').serializeArray(); formData = JSON.stringify(formData); - console.log(formData) $.ajaxSetup({ beforeSend: function (xhr, settings) { From 8b7e7530394285f5ce018db016b7581cbeebb178 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 27 Jul 2026 11:27:27 -0700 Subject: [PATCH 09/19] Fix expired token details feedback --- .../users/templates/users/profile.html | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/ghostwriter/users/templates/users/profile.html b/ghostwriter/users/templates/users/profile.html index 69090825a..d3a4fd310 100644 --- a/ghostwriter/users/templates/users/profile.html +++ b/ghostwriter/users/templates/users/profile.html @@ -672,14 +672,23 @@