[FIX] execute: redact the secrets before printing a command

CodeQL flagged five clear-text logging alerts here, and they are real:
todo.py and kdbx_manager.py build « --default_password_auth '<KeePass
password>' » 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 '<mot de
passe KeePass>' » 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
This commit is contained in:
Mathieu Benoit 2026-08-07 03:35:20 -04:00
parent 67e902e416
commit 18a6f064f5

View file

@ -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 '<mot de passe KeePass>' »,
# 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<opt>--?[\w-]*"
r"(?:password|passwd|pwd|secret|token|api[-_]?key)[\w-]*"
r"(?:\s+|=))"
r"(?P<val>'[^']*'|\"[^\"]*\"|\S+)",
re.IGNORECASE,
)
_SECRET_ENV = re.compile(
r"(?P<var>\b\w*(?:PASSWORD|PASSWD|SECRET|TOKEN)\w*=)"
r"(?P<val>'[^']*'|\"[^\"]*\"|\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