From 727dc238dbda3c45bf6dd9aad2d0cea1d066a5cb Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:18:26 -0600 Subject: [PATCH 1/2] fix(docker): silence shell auto-import banner in DB-reach / first-boot scripts reach_database.sh and entrypoint-first-boot.sh pipe short scripts into `manage.py shell` (a DB connectivity check and superuser creation). Since Django 5.1 the shell command auto-imports every model on startup and prints a banner like: 36 objects could not be automatically imported: dojo.auditlog.services.ObjectsProductTags ... 237 objects imported automatically (use -v 2 for details). The "could not be imported" entries are dynamically-generated Tagulous tag models and auditlog proxies that aren't importable by their dotted path, so the list looks alarming on every container start even though nothing is wrong. Both scripts import exactly what they need, so pass --no-imports (Django >= 5.2) to skip the auto-import and drop the noise. The piped code still runs and exit codes are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker/entrypoint-first-boot.sh | 2 +- docker/reach_database.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/entrypoint-first-boot.sh b/docker/entrypoint-first-boot.sh index ffc782b4ccc..75950ce5935 100644 --- a/docker/entrypoint-first-boot.sh +++ b/docker/entrypoint-first-boot.sh @@ -1,6 +1,6 @@ #!/bin/bash # called from entrypoint-initializer.sh when no admin user exists (first boot) -cat < Date: Mon, 13 Jul 2026 13:30:45 -0600 Subject: [PATCH 2/2] fix(shell): drop non-importable models from shell auto-import Manual `manage.py shell` runs still showed the auto-import failure banner (the docker --no-imports change only covered the DB-reach / first-boot scripts). Override the shell command's get_auto_imports() to keep only paths that actually import, so dynamically-generated Tagulous tag models and auditlog proxy models no longer appear as "could not be automatically imported". Real models still auto-import; the filter is generic (tries the import, keeps what succeeds) so it also covers Pro's proxy models. Adds unittests/test_shell_command.py asserting every returned path imports, real models are kept, and the dropped paths are all genuinely non-importable. Co-Authored-By: Claude Opus 4.8 (1M context) --- dojo/management/commands/shell.py | 40 ++++++++++++++++++++++++++ unittests/test_shell_command.py | 47 +++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 dojo/management/commands/shell.py create mode 100644 unittests/test_shell_command.py diff --git a/dojo/management/commands/shell.py b/dojo/management/commands/shell.py new file mode 100644 index 00000000000..8c5db56db4e --- /dev/null +++ b/dojo/management/commands/shell.py @@ -0,0 +1,40 @@ +from importlib import import_module + +from django.core.management.commands.shell import Command as ShellCommand +from django.utils.module_loading import import_string as import_dotted_path + + +class Command(ShellCommand): + + """ + DefectDojo override of Django's ``shell`` command. + + Django's shell auto-imports every model in ``INSTALLED_APPS`` by its + ``"."`` path. DefectDojo has models that exist on the app + registry but are not importable by that path: + + * dynamically generated Tagulous tag models (``Tagulous_*_tags`` / + ``Tagulous_*_inherited_tags``), and + * auditlog proxy models (e.g. ``dojo.auditlog.services.*`` and the Pro + ``*Proxy`` models). + + The stock command lists ~36 of these as "could not be automatically + imported" on every launch, which reads like an error even though nothing + is wrong. Drop the non-importable paths from the auto-import list so the + banner is clean, without losing any auto-import that would actually have + worked. The filter is generic (it tries the import and keeps what + succeeds), so it covers both open-source and Pro models. + """ + + def get_auto_imports(self): + paths = super().get_auto_imports() + if not paths: + return paths + importable = [] + for path in paths: + try: + import_dotted_path(path) if "." in path else import_module(path) + except ImportError: + continue + importable.append(path) + return importable diff --git a/unittests/test_shell_command.py b/unittests/test_shell_command.py new file mode 100644 index 00000000000..1b8472cdd95 --- /dev/null +++ b/unittests/test_shell_command.py @@ -0,0 +1,47 @@ +from importlib import import_module + +from django.core.management.commands.shell import Command as BaseShellCommand +from django.utils.module_loading import import_string + +from dojo.management.commands.shell import Command + +from .dojo_test_case import DojoTestCase + + +class TestShellAutoImportFilter(DojoTestCase): + + """ + The overridden ``shell`` command must drop non-importable auto-imports. + + Django's stock shell lists dynamically generated Tagulous tag models and + auditlog proxy models as "could not be automatically imported" on every + launch. The override filters the auto-import list down to paths that + actually import, so the banner is clean without losing real auto-imports. + """ + + def _resolve(self, path): + return import_string(path) if "." in path else import_module(path) + + def test_all_returned_paths_are_importable(self): + paths = Command().get_auto_imports() + self.assertTrue(paths, "expected a non-empty auto-import list") + for path in paths: + try: + self._resolve(path) + except ImportError: + self.fail(f"get_auto_imports() returned a non-importable path: {path}") + + def test_real_models_are_kept(self): + self.assertIn("dojo.finding.models.Finding", Command().get_auto_imports()) + + def test_only_non_importable_paths_are_dropped(self): + base_paths = BaseShellCommand().get_auto_imports() + kept = set(Command().get_auto_imports()) + dropped = [path for path in base_paths if path not in kept] + # The override must actually remove something (the environment has + # dynamically generated / proxy models that Django cannot import). + self.assertTrue(dropped, "expected the override to drop at least one non-importable path") + # Everything it drops must genuinely be non-importable. + for path in dropped: + with self.assertRaises(ImportError, msg=f"{path} was dropped but is importable"): + self._resolve(path)