From 18a6f064f53880dbe32b82ba5ba580f638df8c80 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Fri, 7 Aug 2026 03:35:20 -0400 Subject: [PATCH] [FIX] execute: redact the secrets before printing a command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL flagged five clear-text logging alerts here, and they are real: todo.py and kdbx_manager.py build « --default_password_auth '' » and db_restore.py « --master_password=… », while this file printed the command before and after every run and logged it on error. The « if "password" in command » guard covered one branch out of six. Redaction sits at the display point, not at construction: six outputs here against commands built all over the repository. Only the value goes, never the option name, and the RETURNED command stays clear — « [1] redo the command » needs it. Verified that PreferredAuthentications=password survives. --- FR --- CodeQL signalait ici cinq journalisations en clair, et elles sont réelles : todo.py et kdbx_manager.py construisent « --default_password_auth '' » et db_restore.py « --master_password=… », tandis que ce fichier affichait la commande avant et après chaque exécution et la journalisait en erreur. Le garde « if "password" in command » couvrait une branche sur six. Le caviardage est au point d'affichage, non à la construction : six sorties ici, contre des commandes bâties partout dans le dépôt. Seule la valeur part, jamais le nom de l'option, et la commande RENVOYÉE reste en clair — « [1] refaire la commande » en dépend. Vérifié que PreferredAuthentications=password reste intact. Assisted-by: Claude Opus 5 --- script/execute/execute.py | 53 +++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/script/execute/execute.py b/script/execute/execute.py index 62ddcb8..5ef684a 100644 --- a/script/execute/execute.py +++ b/script/execute/execute.py @@ -5,6 +5,7 @@ import datetime import logging import os +import re import shutil import subprocess import sys @@ -17,6 +18,41 @@ except ModuleNotFoundError as e: VENV_ERPLIBRE = ".venv.erplibre" +# Une commande construite ailleurs peut porter un secret en clair : todo.py et +# kdbx_manager.py y mettent « --default_password_auth '' », +# db_restore.py « --master_password=… ». Cette commande est affichée avant et +# après l'exécution, et journalisée en erreur : le secret finissait donc dans le +# terminal, dans les journaux et dans toute sortie CI qui les capture. +# +# On caviarde la VALEUR, jamais le nom de l'option : la commande reste lisible et +# reproductible, il ne manque que ce qui ne doit pas être lu. +_SECRET_OPTION = re.compile( + r"(?P--?[\w-]*" + r"(?:password|passwd|pwd|secret|token|api[-_]?key)[\w-]*" + r"(?:\s+|=))" + r"(?P'[^']*'|\"[^\"]*\"|\S+)", + re.IGNORECASE, +) +_SECRET_ENV = re.compile( + r"(?P\b\w*(?:PASSWORD|PASSWD|SECRET|TOKEN)\w*=)" + r"(?P'[^']*'|\"[^\"]*\"|\S+)" +) + + +def redact_secrets(text): + """Remplace la valeur des options et variables porteuses de secret. + + Appliqué à CHAQUE affichage d'une commande. Filtrer au point d'affichage + plutôt qu'à la construction est ce qui rend la garantie tenable : il n'y a + qu'une poignée de sorties ici, alors que les commandes se construisent + partout dans le dépôt. + """ + if not text: + return text + text = _SECRET_OPTION.sub(lambda m: m.group("opt") + "'***'", text) + return _SECRET_ENV.sub(lambda m: m.group("var") + "'***'", text) + + new_path = os.path.normpath( os.path.join(os.path.dirname(__file__), "..", "..") ) @@ -107,7 +143,8 @@ class Execute: source_odoo = f.read() if not source_odoo: _logger.error( - f"You cannot execute Odoo command if no version is installed. Command : {command}" + "You cannot execute Odoo command if no version is" + f" installed. Command : {redact_secrets(command)}" ) return -1 command = f"source ./.venv.{source_odoo}/bin/activate && {command}" @@ -116,7 +153,7 @@ class Execute: if not quiet: print("🏠 ⬇ Execute command :\n") - print(command) + print(redact_secrets(command)) output_lines = [] try: @@ -156,16 +193,10 @@ class Execute: except FileNotFoundError: if not quiet: - if "password" in command: - print( - f"Error: Command '{command.split(' ')[0]}'[...]" - " not found." - ) - else: - print(f"Error: Command '{command}' not found.") + print(f"Error: Command '{redact_secrets(command)}' not found.") except Exception as e: if not quiet: - print(f"An error occurred: {e}") + print(f"An error occurred: {redact_secrets(str(e))}") process_end_time = time.time() duration_sec = process_end_time - process_start_time if humanize: @@ -177,7 +208,7 @@ class Execute: if not quiet: print(f"🏠 ⬆ Executed ({duration_sec:.2f} sec.) :\n") if not quiet: - print(command) + print(redact_secrets(command)) print() if return_status_and_output_and_command: return exit_code, command, output_lines