From e5a0303c61c85cb0ea25068fbe039f62a6d01fa7 Mon Sep 17 00:00:00 2001 From: Andrew Savchenko Date: Mon, 16 Jun 2025 02:06:42 +0930 Subject: [PATCH 1/7] Set screen formatter colours based on terminal BG Two-stage background detection: $COLORFGBG followed by OSC11. Fallback to dark if colour can't be detected using the methods above. Can be overriden by $BANDIT_LIGHT_BG environment variable. --- bandit/formatters/screen.py | 164 ++++++++++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 7 deletions(-) diff --git a/bandit/formatters/screen.py b/bandit/formatters/screen.py index 906e86b73..77a332e3a 100644 --- a/bandit/formatters/screen.py +++ b/bandit/formatters/screen.py @@ -31,10 +31,20 @@ .. versionchanged:: 1.7.3 New field `CWE` added to output +.. versionchanged:: 1.8.5 + Automatic colours configuration with optional override via + "BANDIT_LIGHT_BG" environment variable. + """ + import datetime import logging +import os +import select import sys +import termios +import time +import tty from bandit.core import constants from bandit.core import docs_utils @@ -57,13 +67,153 @@ LOG = logging.getLogger(__name__) -COLOR = { - "DEFAULT": "\033[0m", - "HEADER": "\033[95m", - "LOW": "\033[94m", - "MEDIUM": "\033[93m", - "HIGH": "\033[91m", -} + +def term_detect_bg() -> bool | None: + """Detects if terminal is using dark BG. + + Returns: + True - Light + False - Dark + None - Undetermined + """ + colorfgbg = os.environ.get("COLORFGBG") + if colorfgbg and ";" in colorfgbg: + try: + parts = colorfgbg.split(";") + bg_color = int(parts[-1]) + # Ref. https://github.com/rocky/shell-term-background + if bg_color in {0, 1, 2, 3, 4, 5, 6, 8}: + return False + elif bg_color in {7, 9, 10, 11, 12, 13, 14, 15}: + return True + except (ValueError, IndexError): + pass + if sys.stdin.isatty(): + try: + result = term_get_osc() + if result is not None: + return result + except Exception: + pass + if os.environ.get("BANDIT_LIGHT_BG", "").lower() in ( + "light", "bright", "white", "1", "true", "yes" + ): + return True + + return None + + +def term_get_osc() -> bool | None: + """Query terminal BG colour using OSC11. + + Returns: + True - Light + False - Dark + None - Undetermined + """ + if not sys.stdin.isatty(): + return None + + old_settings = None + + try: + old_settings = termios.tcgetattr(sys.stdin) + + _ = tty.setraw(sys.stdin.fileno()) + _ = sys.stdout.write("\x1b]11;?\x1b\\") # ESC\ + _ = sys.stdout.flush() + + ready, _, _ = select.select([sys.stdin], [], [], 0.2) + if not ready: + return None # Bail out, this term is cursed + + response = "" + start_time = time.time() + while time.time() - start_time < 0.5: + ready, _, _ = select.select([sys.stdin], [], [], 0.01) + if not ready: + break + char = sys.stdin.read(1) + response += char + # Break on ESC\, BEL or sufficient data + if response.endswith('\x1b\\') or response.endswith('\x07') or len(response) > 50: + break + # Bail out if ESC isn't followed by ] + if len(response) >= 2 and response.startswith('\x1b') and not response.startswith('\x1b]'): + return None + + if response.startswith('\x1b]11;rgb:'): + try: + rgb_start = response.find("rgb:") + rgb_part = response[rgb_start + 4:] + # Find terminator + for term in ['\x1b\\', '\x07']: + if term in rgb_part: + rgb_part = rgb_part[:rgb_part.find(term)] + break + + r, g, b = rgb_part.split("/")[:3] + + # HEX -> DEC + r_val = int(r[:4], 16) if len(r) >= 4 else int(r, 16) + g_val = int(g[:4], 16) if len(g) >= 4 else int(g, 16) + b_val = int(b[:4], 16) if len(b) >= 4 else int(b, 16) + + # 16b -> 8b + if r_val > 255: + r_val = r_val >> 8 + if g_val > 255: + g_val = g_val >> 8 + if b_val > 255: + b_val = b_val >> 8 + + # BT601 + lum = 0.299 * r_val + 0.587 * g_val + 0.114 * b_val + return lum > 128 # Light if luma > 50% grey + except (ValueError, IndexError): + pass + else: + return None + + except Exception: + pass + finally: + try: + if old_settings: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) + except Exception: + pass + + return None + + +def term_serve_colourscheme() -> dict[str, str]: + """Appropriate colour scheme based on the detected background. + + Returns: + Dictionary with colour codes + """ + light = term_detect_bg() + + if light: + return { + "DEFAULT": "\033[0m", + "HEADER": "\033[1;34m", # Dark blue + "LOW": "\033[1;32m", # Dark green + "MEDIUM": "\033[1;35m", # Dark magenta + "HIGH": "\033[1;31m", # Dark red + } + else: + return { + "DEFAULT": "\033[0m", + "HEADER": "\033[1;96m", # Bright cyan + "LOW": "\033[1;92m", # Bright green + "MEDIUM": "\033[1;93m", # Bright yellow + "HIGH": "\033[1;91m", # Bright red + } + + +COLOR = term_serve_colourscheme() def header(text, *args): From 9d0cea60d8311998e96e945b8642a37019b93042 Mon Sep 17 00:00:00 2001 From: Andrew Savchenko Date: Mon, 16 Jun 2025 02:06:42 +0930 Subject: [PATCH 2/7] Set screen formatter colours based on terminal BG Two-stage background detection: $COLORFGBG followed by OSC11. Fallback to dark if colour can't be detected using the methods above. Can be overriden by $BANDIT_LIGHT_BG environment variable. --- bandit/formatters/screen.py | 164 ++++++++++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 7 deletions(-) diff --git a/bandit/formatters/screen.py b/bandit/formatters/screen.py index 906e86b73..77a332e3a 100644 --- a/bandit/formatters/screen.py +++ b/bandit/formatters/screen.py @@ -31,10 +31,20 @@ .. versionchanged:: 1.7.3 New field `CWE` added to output +.. versionchanged:: 1.8.5 + Automatic colours configuration with optional override via + "BANDIT_LIGHT_BG" environment variable. + """ + import datetime import logging +import os +import select import sys +import termios +import time +import tty from bandit.core import constants from bandit.core import docs_utils @@ -57,13 +67,153 @@ LOG = logging.getLogger(__name__) -COLOR = { - "DEFAULT": "\033[0m", - "HEADER": "\033[95m", - "LOW": "\033[94m", - "MEDIUM": "\033[93m", - "HIGH": "\033[91m", -} + +def term_detect_bg() -> bool | None: + """Detects if terminal is using dark BG. + + Returns: + True - Light + False - Dark + None - Undetermined + """ + colorfgbg = os.environ.get("COLORFGBG") + if colorfgbg and ";" in colorfgbg: + try: + parts = colorfgbg.split(";") + bg_color = int(parts[-1]) + # Ref. https://github.com/rocky/shell-term-background + if bg_color in {0, 1, 2, 3, 4, 5, 6, 8}: + return False + elif bg_color in {7, 9, 10, 11, 12, 13, 14, 15}: + return True + except (ValueError, IndexError): + pass + if sys.stdin.isatty(): + try: + result = term_get_osc() + if result is not None: + return result + except Exception: + pass + if os.environ.get("BANDIT_LIGHT_BG", "").lower() in ( + "light", "bright", "white", "1", "true", "yes" + ): + return True + + return None + + +def term_get_osc() -> bool | None: + """Query terminal BG colour using OSC11. + + Returns: + True - Light + False - Dark + None - Undetermined + """ + if not sys.stdin.isatty(): + return None + + old_settings = None + + try: + old_settings = termios.tcgetattr(sys.stdin) + + _ = tty.setraw(sys.stdin.fileno()) + _ = sys.stdout.write("\x1b]11;?\x1b\\") # ESC\ + _ = sys.stdout.flush() + + ready, _, _ = select.select([sys.stdin], [], [], 0.2) + if not ready: + return None # Bail out, this term is cursed + + response = "" + start_time = time.time() + while time.time() - start_time < 0.5: + ready, _, _ = select.select([sys.stdin], [], [], 0.01) + if not ready: + break + char = sys.stdin.read(1) + response += char + # Break on ESC\, BEL or sufficient data + if response.endswith('\x1b\\') or response.endswith('\x07') or len(response) > 50: + break + # Bail out if ESC isn't followed by ] + if len(response) >= 2 and response.startswith('\x1b') and not response.startswith('\x1b]'): + return None + + if response.startswith('\x1b]11;rgb:'): + try: + rgb_start = response.find("rgb:") + rgb_part = response[rgb_start + 4:] + # Find terminator + for term in ['\x1b\\', '\x07']: + if term in rgb_part: + rgb_part = rgb_part[:rgb_part.find(term)] + break + + r, g, b = rgb_part.split("/")[:3] + + # HEX -> DEC + r_val = int(r[:4], 16) if len(r) >= 4 else int(r, 16) + g_val = int(g[:4], 16) if len(g) >= 4 else int(g, 16) + b_val = int(b[:4], 16) if len(b) >= 4 else int(b, 16) + + # 16b -> 8b + if r_val > 255: + r_val = r_val >> 8 + if g_val > 255: + g_val = g_val >> 8 + if b_val > 255: + b_val = b_val >> 8 + + # BT601 + lum = 0.299 * r_val + 0.587 * g_val + 0.114 * b_val + return lum > 128 # Light if luma > 50% grey + except (ValueError, IndexError): + pass + else: + return None + + except Exception: + pass + finally: + try: + if old_settings: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) + except Exception: + pass + + return None + + +def term_serve_colourscheme() -> dict[str, str]: + """Appropriate colour scheme based on the detected background. + + Returns: + Dictionary with colour codes + """ + light = term_detect_bg() + + if light: + return { + "DEFAULT": "\033[0m", + "HEADER": "\033[1;34m", # Dark blue + "LOW": "\033[1;32m", # Dark green + "MEDIUM": "\033[1;35m", # Dark magenta + "HIGH": "\033[1;31m", # Dark red + } + else: + return { + "DEFAULT": "\033[0m", + "HEADER": "\033[1;96m", # Bright cyan + "LOW": "\033[1;92m", # Bright green + "MEDIUM": "\033[1;93m", # Bright yellow + "HIGH": "\033[1;91m", # Bright red + } + + +COLOR = term_serve_colourscheme() def header(text, *args): From e16f98f79c7c05352c69c3bfaa901636f834d4d0 Mon Sep 17 00:00:00 2001 From: Andrew Savchenko Date: Sat, 28 Jun 2025 00:19:08 +0930 Subject: [PATCH 3/7] Reformat screen.py --- bandit/formatters/screen.py | 38 ++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/bandit/formatters/screen.py b/bandit/formatters/screen.py index 77a332e3a..cee9a7afd 100644 --- a/bandit/formatters/screen.py +++ b/bandit/formatters/screen.py @@ -32,11 +32,10 @@ New field `CWE` added to output .. versionchanged:: 1.8.5 - Automatic colours configuration with optional override via + Automatic colours configuration with optional override via "BANDIT_LIGHT_BG" environment variable. """ - import datetime import logging import os @@ -96,7 +95,12 @@ def term_detect_bg() -> bool | None: except Exception: pass if os.environ.get("BANDIT_LIGHT_BG", "").lower() in ( - "light", "bright", "white", "1", "true", "yes" + "light", + "bright", + "white", + "1", + "true", + "yes", ): return True @@ -136,20 +140,28 @@ def term_get_osc() -> bool | None: char = sys.stdin.read(1) response += char # Break on ESC\, BEL or sufficient data - if response.endswith('\x1b\\') or response.endswith('\x07') or len(response) > 50: + if ( + response.endswith("\x1b\\") + or response.endswith("\x07") + or len(response) > 50 + ): break # Bail out if ESC isn't followed by ] - if len(response) >= 2 and response.startswith('\x1b') and not response.startswith('\x1b]'): + if ( + len(response) >= 2 + and response.startswith("\x1b") + and not response.startswith("\x1b]") + ): return None - if response.startswith('\x1b]11;rgb:'): + if response.startswith("\x1b]11;rgb:"): try: rgb_start = response.find("rgb:") - rgb_part = response[rgb_start + 4:] + rgb_part = response[rgb_start + 4 :] # Find terminator - for term in ['\x1b\\', '\x07']: + for term in ["\x1b\\", "\x07"]: if term in rgb_part: - rgb_part = rgb_part[:rgb_part.find(term)] + rgb_part = rgb_part[: rgb_part.find(term)] break r, g, b = rgb_part.split("/")[:3] @@ -199,17 +211,17 @@ def term_serve_colourscheme() -> dict[str, str]: return { "DEFAULT": "\033[0m", "HEADER": "\033[1;34m", # Dark blue - "LOW": "\033[1;32m", # Dark green + "LOW": "\033[1;32m", # Dark green "MEDIUM": "\033[1;35m", # Dark magenta - "HIGH": "\033[1;31m", # Dark red + "HIGH": "\033[1;31m", # Dark red } else: return { "DEFAULT": "\033[0m", "HEADER": "\033[1;96m", # Bright cyan - "LOW": "\033[1;92m", # Bright green + "LOW": "\033[1;92m", # Bright green "MEDIUM": "\033[1;93m", # Bright yellow - "HIGH": "\033[1;91m", # Bright red + "HIGH": "\033[1;91m", # Bright red } From 6d9ffff9a036a32bd9ab3cd8476029937b5b3b2a Mon Sep 17 00:00:00 2001 From: Andrew Savchenko Date: Sun, 29 Jun 2025 00:04:33 +0930 Subject: [PATCH 4/7] Make Flake8 happy --- bandit/formatters/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bandit/formatters/screen.py b/bandit/formatters/screen.py index cee9a7afd..d9895fec7 100644 --- a/bandit/formatters/screen.py +++ b/bandit/formatters/screen.py @@ -157,7 +157,7 @@ def term_get_osc() -> bool | None: if response.startswith("\x1b]11;rgb:"): try: rgb_start = response.find("rgb:") - rgb_part = response[rgb_start + 4 :] + rgb_part = response[rgb_start + 4:] # Find terminator for term in ["\x1b\\", "\x07"]: if term in rgb_part: From c1d9db876dd1f83cd095d66d946909b06a2ad72a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 14:39:36 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- bandit/formatters/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bandit/formatters/screen.py b/bandit/formatters/screen.py index d9895fec7..cee9a7afd 100644 --- a/bandit/formatters/screen.py +++ b/bandit/formatters/screen.py @@ -157,7 +157,7 @@ def term_get_osc() -> bool | None: if response.startswith("\x1b]11;rgb:"): try: rgb_start = response.find("rgb:") - rgb_part = response[rgb_start + 4:] + rgb_part = response[rgb_start + 4 :] # Find terminator for term in ["\x1b\\", "\x07"]: if term in rgb_part: From c87792c05167508261bb075483fea80f710fdb5a Mon Sep 17 00:00:00 2001 From: Andrew Savchenko Date: Sun, 29 Jun 2025 00:44:17 +0930 Subject: [PATCH 6/7] Antiquate code to be Python 3.9-compliant --- bandit/formatters/screen.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bandit/formatters/screen.py b/bandit/formatters/screen.py index d9895fec7..e8dae47e6 100644 --- a/bandit/formatters/screen.py +++ b/bandit/formatters/screen.py @@ -31,7 +31,7 @@ .. versionchanged:: 1.7.3 New field `CWE` added to output -.. versionchanged:: 1.8.5 +.. versionchanged:: 1.8.6 Automatic colours configuration with optional override via "BANDIT_LIGHT_BG" environment variable. @@ -45,6 +45,7 @@ import time import tty +from typing import Union, Dict # Tests use Python 3.7 (!) and 3.9 from bandit.core import constants from bandit.core import docs_utils from bandit.core import test_properties @@ -67,7 +68,7 @@ LOG = logging.getLogger(__name__) -def term_detect_bg() -> bool | None: +def term_detect_bg() -> Union[bool, None]: """Detects if terminal is using dark BG. Returns: @@ -107,7 +108,7 @@ def term_detect_bg() -> bool | None: return None -def term_get_osc() -> bool | None: +def term_get_osc() -> Union[bool, None]: """Query terminal BG colour using OSC11. Returns: From d63326b1fc09daa7b18f7714b400e7a371f0e1bb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 15:15:29 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- bandit/formatters/screen.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bandit/formatters/screen.py b/bandit/formatters/screen.py index 15c94b2c1..d2c27bedf 100644 --- a/bandit/formatters/screen.py +++ b/bandit/formatters/screen.py @@ -44,8 +44,9 @@ import termios import time import tty +from typing import Dict +from typing import Union -from typing import Union, Dict # Tests use Python 3.7 (!) and 3.9 from bandit.core import constants from bandit.core import docs_utils from bandit.core import test_properties