Skip to content

Commit 21a7254

Browse files
Maffoochclaude
andauthored
fix(shell): silence the shell model auto-import banner (docker + manual runs) (#15234)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 31f6ab3 commit 21a7254

4 files changed

Lines changed: 89 additions & 2 deletions

File tree

docker/entrypoint-first-boot.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/bash
22
# called from entrypoint-initializer.sh when no admin user exists (first boot)
3-
cat <<EOD | python manage.py shell
3+
cat <<EOD | python manage.py shell --no-imports
44
import os
55
from django.contrib.auth.models import User
66
User.objects.create_superuser(

docker/reach_database.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ wait_for_database_to_be_reachable() {
1313
exit 1
1414
fi
1515
done
16-
cat <<EOD | python manage.py shell
16+
cat <<EOD | python manage.py shell --no-imports
1717
from django.db import connections
1818
connections['default'].cursor()
1919
EOD

dojo/management/commands/shell.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from importlib import import_module
2+
3+
from django.core.management.commands.shell import Command as ShellCommand
4+
from django.utils.module_loading import import_string as import_dotted_path
5+
6+
7+
class Command(ShellCommand):
8+
9+
"""
10+
DefectDojo override of Django's ``shell`` command.
11+
12+
Django's shell auto-imports every model in ``INSTALLED_APPS`` by its
13+
``"<module>.<name>"`` path. DefectDojo has models that exist on the app
14+
registry but are not importable by that path:
15+
16+
* dynamically generated Tagulous tag models (``Tagulous_*_tags`` /
17+
``Tagulous_*_inherited_tags``), and
18+
* auditlog proxy models (e.g. ``dojo.auditlog.services.*`` and the Pro
19+
``*Proxy`` models).
20+
21+
The stock command lists ~36 of these as "could not be automatically
22+
imported" on every launch, which reads like an error even though nothing
23+
is wrong. Drop the non-importable paths from the auto-import list so the
24+
banner is clean, without losing any auto-import that would actually have
25+
worked. The filter is generic (it tries the import and keeps what
26+
succeeds), so it covers both open-source and Pro models.
27+
"""
28+
29+
def get_auto_imports(self):
30+
paths = super().get_auto_imports()
31+
if not paths:
32+
return paths
33+
importable = []
34+
for path in paths:
35+
try:
36+
import_dotted_path(path) if "." in path else import_module(path)
37+
except ImportError:
38+
continue
39+
importable.append(path)
40+
return importable

unittests/test_shell_command.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from importlib import import_module
2+
3+
from django.core.management.commands.shell import Command as BaseShellCommand
4+
from django.utils.module_loading import import_string
5+
6+
from dojo.management.commands.shell import Command
7+
8+
from .dojo_test_case import DojoTestCase
9+
10+
11+
class TestShellAutoImportFilter(DojoTestCase):
12+
13+
"""
14+
The overridden ``shell`` command must drop non-importable auto-imports.
15+
16+
Django's stock shell lists dynamically generated Tagulous tag models and
17+
auditlog proxy models as "could not be automatically imported" on every
18+
launch. The override filters the auto-import list down to paths that
19+
actually import, so the banner is clean without losing real auto-imports.
20+
"""
21+
22+
def _resolve(self, path):
23+
return import_string(path) if "." in path else import_module(path)
24+
25+
def test_all_returned_paths_are_importable(self):
26+
paths = Command().get_auto_imports()
27+
self.assertTrue(paths, "expected a non-empty auto-import list")
28+
for path in paths:
29+
try:
30+
self._resolve(path)
31+
except ImportError:
32+
self.fail(f"get_auto_imports() returned a non-importable path: {path}")
33+
34+
def test_real_models_are_kept(self):
35+
self.assertIn("dojo.finding.models.Finding", Command().get_auto_imports())
36+
37+
def test_only_non_importable_paths_are_dropped(self):
38+
base_paths = BaseShellCommand().get_auto_imports()
39+
kept = set(Command().get_auto_imports())
40+
dropped = [path for path in base_paths if path not in kept]
41+
# The override must actually remove something (the environment has
42+
# dynamically generated / proxy models that Django cannot import).
43+
self.assertTrue(dropped, "expected the override to drop at least one non-importable path")
44+
# Everything it drops must genuinely be non-importable.
45+
for path in dropped:
46+
with self.assertRaises(ImportError, msg=f"{path} was dropped but is importable"):
47+
self._resolve(path)

0 commit comments

Comments
 (0)