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 <."`` 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)