From d40021aba18a197f9733e38bd52847d537b3cfbf Mon Sep 17 00:00:00 2001
From: szarawata <9322134+Loreweavr@users.noreply.github.com>
Date: Fri, 5 Jun 2026 17:27:06 +0200
Subject: [PATCH] fix: security (bandit) detector now honours config-level
excludes
`desloppify exclude
` is respected by the file-discovery detectors but not
by the Python security detector: detect_python_security built bandit's --exclude
from collect_exclude_dirs(scan_root), which only sees the runtime context's
exclusions (empty there) plus the built-in defaults. The persisted config
excludes were never threaded in, so bandit recursively re-scanned excluded
directories and reported security findings from them.
Load the config excludes and pass them via collect_exclude_dirs(extra_exclusions=)
alongside get_exclusions(). Adds a regression test (there was no bandit
exclude-flag test, which is how this slipped through).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
desloppify/languages/python/_security.py | 11 ++++++--
.../test_external_adapters_exclude_flags.py | 28 +++++++++++++++++++
2 files changed, 37 insertions(+), 2 deletions(-)
diff --git a/desloppify/languages/python/_security.py b/desloppify/languages/python/_security.py
index 9b623657d..eed920095 100644
--- a/desloppify/languages/python/_security.py
+++ b/desloppify/languages/python/_security.py
@@ -5,7 +5,7 @@
import shutil
from desloppify.base.config import load_config
-from desloppify.base.discovery.source import collect_exclude_dirs
+from desloppify.base.discovery.source import collect_exclude_dirs, get_exclusions
from desloppify.languages._framework.base.types import DetectorCoverageStatus, LangSecurityResult
from desloppify.languages.python.detectors.bandit_adapter import detect_with_bandit
from desloppify.languages.python._helpers import scan_root_from_files
@@ -49,7 +49,14 @@ def detect_python_security(files, zone_map) -> LangSecurityResult:
if scan_root is None:
return LangSecurityResult(entries=[], files_scanned=0)
- exclude_dirs = collect_exclude_dirs(scan_root)
+ # Config-level excludes (`desloppify exclude `) are stored in the project
+ # config, not the runtime context that get_exclusions() reads here. Without
+ # loading them explicitly, bandit rescans excluded directories even though every
+ # other detector honours the exclude.
+ config_excludes = tuple(p for p in (load_config().get("exclude") or ()) if isinstance(p, str))
+ exclude_dirs = collect_exclude_dirs(
+ scan_root, extra_exclusions=tuple(get_exclusions()) + config_excludes
+ )
skip_tests = _load_bandit_skip_tests()
result = detect_with_bandit(
scan_root, zone_map, exclude_dirs=exclude_dirs, skip_tests=skip_tests,
diff --git a/desloppify/tests/detectors/test_external_adapters_exclude_flags.py b/desloppify/tests/detectors/test_external_adapters_exclude_flags.py
index c4b3dc870..dcd3cf453 100644
--- a/desloppify/tests/detectors/test_external_adapters_exclude_flags.py
+++ b/desloppify/tests/detectors/test_external_adapters_exclude_flags.py
@@ -4,6 +4,7 @@
from unittest.mock import MagicMock, patch
+from desloppify.languages.python._security import detect_python_security
from desloppify.languages.python.detectors.ruff_smells import detect_with_ruff_smells
from desloppify.languages.python.detectors.unused import detect_unused
@@ -104,3 +105,30 @@ def _capture_run(cmd, **kwargs):
detect_unused(tmp_path)
assert "--exclude" not in captured_cmd
+
+
+class TestBanditSecurityExcludeFlag:
+ """Regression: config-level excludes (`desloppify exclude `) must reach
+ bandit's --exclude. Previously only the file-discovery detectors honoured them,
+ so the security detector kept reporting findings from excluded directories."""
+
+ def test_security_passes_config_excludes_to_bandit(self, tmp_path):
+ (tmp_path / "app.py").write_text("x = 1\n")
+ captured_cmd: list[str] = []
+
+ def _capture_run(cmd, **kwargs):
+ captured_cmd.extend(cmd)
+ mock_result = MagicMock()
+ mock_result.stdout = ""
+ mock_result.returncode = 0
+ return mock_result
+
+ with patch("subprocess.run", side_effect=_capture_run), patch(
+ "desloppify.languages.python._security.load_config",
+ return_value={"exclude": ["vendored_lib"]},
+ ):
+ detect_python_security([str(tmp_path / "app.py")], zone_map=None)
+
+ assert "--exclude" in captured_cmd
+ exclude_value = captured_cmd[captured_cmd.index("--exclude") + 1]
+ assert "vendored_lib" in exclude_value