diff --git a/detect_secrets/settings.py b/detect_secrets/settings.py index 0d89e6f34..eb8181b67 100644 --- a/detect_secrets/settings.py +++ b/detect_secrets/settings.py @@ -1,4 +1,5 @@ import contextlib +import threading from contextlib import contextmanager from copy import deepcopy from functools import lru_cache @@ -13,6 +14,15 @@ from .util.importlib import import_file_as_module +# Lock to protect transient_settings from concurrent access. +# On macOS, checkov's ParallelRunner uses ThreadPoolExecutor, +# so all scanners share the same process and global state. Without this lock, +# concurrent calls to transient_settings() can corrupt the LRU-cached singletons +# (get_settings, get_plugins, get_mapping_from_secret_type_to_class), causing +# the secrets scanner to silently produce 0 findings. +_settings_lock = threading.Lock() + + @lru_cache(maxsize=1) def get_settings() -> 'Settings': """ @@ -77,21 +87,33 @@ def default_settings() -> Generator['Settings', None, None]: @contextmanager def transient_settings(config: Dict[str, Any]) -> Generator['Settings', None, None]: - """Allows the customizability of non-global settings per invocation.""" - original_settings = get_settings().json() + """Allows the customizability of non-global settings per invocation. + + Protected by _settings_lock to prevent race conditions when + multiple threads call this concurrently (e.g., IAC + SECRETS scanners + running in parallel via ThreadPoolExecutor on macOS). + """ + with _settings_lock: + original_settings = get_settings().json() - cache_bust() - try: - yield configure_settings_from_baseline(config) - finally: cache_bust() - configure_settings_from_baseline(original_settings) + try: + yield configure_settings_from_baseline(config) + finally: + cache_bust() + configure_settings_from_baseline(original_settings) def cache_bust() -> None: get_plugins.cache_clear() get_filters.cache_clear() + + # BCE-56937: Clear the plugin-type mapping cache to prevent stale mappings + # built from empty settings during a race window. + from .core.plugins.util import get_mapping_from_secret_type_to_class + get_mapping_from_secret_type_to_class.cache_clear() + for path in get_settings().filters: # Need to also clear the individual caches (e.g. cached regex patterns). parts = urlparse(path)