Skip to content
Open
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
2 changes: 1 addition & 1 deletion docker/entrypoint-first-boot.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash
# called from entrypoint-initializer.sh when no admin user exists (first boot)
cat <<EOD | python manage.py shell
cat <<EOD | python manage.py shell --no-imports
import os
from django.contrib.auth.models import User
User.objects.create_superuser(
Expand Down
2 changes: 1 addition & 1 deletion docker/reach_database.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ wait_for_database_to_be_reachable() {
exit 1
fi
done
cat <<EOD | python manage.py shell
cat <<EOD | python manage.py shell --no-imports
from django.db import connections
connections['default'].cursor()
EOD
Expand Down
40 changes: 40 additions & 0 deletions dojo/management/commands/shell.py
Original file line number Diff line number Diff line change
@@ -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
``"<module>.<name>"`` 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
47 changes: 47 additions & 0 deletions unittests/test_shell_command.py
Original file line number Diff line number Diff line change
@@ -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)