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