2026-02-13 01:27:38 -05:00
|
|
|
#!/usr/bin/env python3
|
2026-03-11 23:15:07 -04:00
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
2026-02-13 01:27:38 -05:00
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
[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
2026-08-07 03:35:20 -04:00
|
|
|
import re
|
2026-02-13 01:27:38 -05:00
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
|
2026-02-14 05:53:31 -05:00
|
|
|
try:
|
|
|
|
|
import humanize
|
2026-02-19 12:27:35 -05:00
|
|
|
except ModuleNotFoundError as e:
|
2026-02-14 05:53:31 -05:00
|
|
|
humanize = None
|
2026-02-13 01:27:38 -05:00
|
|
|
|
2026-03-10 03:06:07 -04:00
|
|
|
VENV_ERPLIBRE = ".venv.erplibre"
|
2026-02-13 01:27:38 -05:00
|
|
|
|
[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
2026-08-07 03:35:20 -04:00
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 01:27:38 -05:00
|
|
|
new_path = os.path.normpath(
|
|
|
|
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
|
|
|
)
|
|
|
|
|
sys.path.append(new_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
format=(
|
|
|
|
|
"%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d]"
|
|
|
|
|
" %(message)s"
|
|
|
|
|
),
|
|
|
|
|
datefmt="%Y-%m-%d:%H:%M:%S",
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
)
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Execute:
|
2026-03-10 04:07:38 -04:00
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.cmd_source_erplibre: str = ""
|
|
|
|
|
self.cmd_source_default: str = ""
|
2026-02-13 01:27:38 -05:00
|
|
|
exec_path_gnome_terminal = shutil.which("gnome-terminal")
|
|
|
|
|
if exec_path_gnome_terminal:
|
|
|
|
|
self.cmd_source_erplibre = (
|
|
|
|
|
f"gnome-terminal -- bash -c 'source"
|
2026-03-10 03:06:07 -04:00
|
|
|
f" ./{VENV_ERPLIBRE}/bin/activate;%s'"
|
2026-02-13 01:27:38 -05:00
|
|
|
)
|
|
|
|
|
self.cmd_source_default = "gnome-terminal -- bash -c '" f"%s'"
|
|
|
|
|
else:
|
|
|
|
|
exec_path_tell = shutil.which("osascript")
|
|
|
|
|
if exec_path_tell:
|
|
|
|
|
self.cmd_source_erplibre = (
|
|
|
|
|
"osascript -e 'tell application \"Terminal\"'"
|
|
|
|
|
)
|
|
|
|
|
self.cmd_source_erplibre += " -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \""
|
2026-03-10 03:06:07 -04:00
|
|
|
self.cmd_source_erplibre += f"cd {os.getcwd()}; source ./{VENV_ERPLIBRE}/bin/activate; %s\" in front window'"
|
2026-02-13 01:27:38 -05:00
|
|
|
self.cmd_source_erplibre += " -e 'end tell'"
|
|
|
|
|
else:
|
|
|
|
|
self.cmd_source_erplibre = (
|
2026-03-10 03:06:07 -04:00
|
|
|
f"source ./{VENV_ERPLIBRE}/bin/activate;%s"
|
2026-02-13 01:27:38 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def exec_command_live(
|
|
|
|
|
self,
|
2026-03-10 04:07:38 -04:00
|
|
|
command: str,
|
|
|
|
|
source_erplibre: bool = True,
|
|
|
|
|
quiet: bool = False,
|
|
|
|
|
single_source_erplibre: bool = False,
|
|
|
|
|
new_window: bool = False,
|
|
|
|
|
single_source_odoo: bool = False,
|
|
|
|
|
source_odoo: str = "",
|
|
|
|
|
new_env: dict | None = None,
|
|
|
|
|
return_status_and_command: bool = False,
|
|
|
|
|
return_status_and_output: bool = False,
|
|
|
|
|
return_status_and_output_and_command: bool = False,
|
|
|
|
|
) -> (
|
|
|
|
|
int
|
|
|
|
|
| tuple[int, str]
|
|
|
|
|
| tuple[int, list[str]]
|
|
|
|
|
| tuple[int, str, list[str]]
|
2026-02-13 01:27:38 -05:00
|
|
|
):
|
|
|
|
|
"""
|
2026-03-10 03:45:11 -04:00
|
|
|
Execute a command and display its output live.
|
2026-02-13 01:27:38 -05:00
|
|
|
|
|
|
|
|
Args:
|
2026-03-10 03:45:11 -04:00
|
|
|
command (str): The command to execute.
|
2026-02-13 01:27:38 -05:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
my_env = os.environ.copy()
|
|
|
|
|
if new_env:
|
|
|
|
|
my_env.update(new_env)
|
|
|
|
|
|
|
|
|
|
process_start_time = time.time()
|
2026-03-10 03:06:07 -04:00
|
|
|
exit_code = None
|
2026-02-13 01:27:38 -05:00
|
|
|
if source_erplibre:
|
2026-03-10 03:06:07 -04:00
|
|
|
# command = f"source ./{VENV_ERPLIBRE}/bin/activate && " + command
|
2026-02-13 01:27:38 -05:00
|
|
|
# cmd = (
|
|
|
|
|
# f"gnome-terminal --tab -- bash -c 'source"
|
2026-03-10 03:06:07 -04:00
|
|
|
# f" ./{VENV_ERPLIBRE}/bin/activate;{command}'"
|
2026-02-13 01:27:38 -05:00
|
|
|
# )
|
2026-03-10 03:06:07 -04:00
|
|
|
command = self.cmd_source_erplibre % command
|
|
|
|
|
# os.system(f"./script/terminal/open_terminal.sh {command}")
|
2026-02-13 01:27:38 -05:00
|
|
|
elif single_source_erplibre:
|
2026-03-10 04:07:38 -04:00
|
|
|
command = f"source ./{VENV_ERPLIBRE}/bin/activate && %s" % command
|
2026-02-13 01:27:38 -05:00
|
|
|
elif single_source_odoo:
|
|
|
|
|
if not source_odoo and os.path.exists("./.erplibre-version"):
|
|
|
|
|
with open("./.erplibre-version") as f:
|
|
|
|
|
source_odoo = f.read()
|
|
|
|
|
if not source_odoo:
|
|
|
|
|
_logger.error(
|
[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
2026-08-07 03:35:20 -04:00
|
|
|
"You cannot execute Odoo command if no version is"
|
|
|
|
|
f" installed. Command : {redact_secrets(command)}"
|
2026-02-13 01:27:38 -05:00
|
|
|
)
|
|
|
|
|
return -1
|
2026-03-10 04:07:38 -04:00
|
|
|
command = f"source ./.venv.{source_odoo}/bin/activate && {command}"
|
2026-02-13 04:07:02 -05:00
|
|
|
if new_window and self.cmd_source_default:
|
2026-03-10 03:06:07 -04:00
|
|
|
command = self.cmd_source_default % command
|
2026-02-13 01:27:38 -05:00
|
|
|
|
|
|
|
|
if not quiet:
|
2026-03-12 05:20:32 -04:00
|
|
|
print("🏠 ⬇ Execute command :\n")
|
[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
2026-08-07 03:35:20 -04:00
|
|
|
print(redact_secrets(command))
|
2026-03-10 03:06:07 -04:00
|
|
|
output_lines = []
|
2026-02-13 01:27:38 -05:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
process = subprocess.Popen(
|
2026-03-10 03:06:07 -04:00
|
|
|
command,
|
2026-02-13 01:27:38 -05:00
|
|
|
shell=True,
|
|
|
|
|
executable="/bin/bash",
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
|
text=True,
|
2026-03-10 03:45:11 -04:00
|
|
|
bufsize=1, # Disable buffering for live output
|
|
|
|
|
universal_newlines=True, # Handle line breaks correctly
|
2026-02-13 01:27:38 -05:00
|
|
|
env=my_env,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
while True:
|
2026-03-10 03:06:07 -04:00
|
|
|
line = process.stdout.readline()
|
|
|
|
|
if not line:
|
2026-02-13 01:27:38 -05:00
|
|
|
break
|
|
|
|
|
if not quiet:
|
2026-03-10 03:06:07 -04:00
|
|
|
print(line, end="")
|
2026-02-13 01:27:38 -05:00
|
|
|
if (
|
|
|
|
|
return_status_and_output
|
|
|
|
|
or return_status_and_output_and_command
|
|
|
|
|
):
|
2026-02-13 03:46:27 -05:00
|
|
|
# Remove last \n char
|
2026-03-10 03:06:07 -04:00
|
|
|
output_lines.append(
|
|
|
|
|
line.removesuffix("\r\n")
|
2026-02-13 03:46:27 -05:00
|
|
|
.removesuffix("\n")
|
|
|
|
|
.removesuffix("\r")
|
|
|
|
|
)
|
2026-02-13 01:27:38 -05:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
process.wait()
|
2026-03-10 03:06:07 -04:00
|
|
|
exit_code = process.returncode
|
2026-02-13 01:27:38 -05:00
|
|
|
if process.returncode != 0 and not quiet:
|
2026-03-10 04:07:38 -04:00
|
|
|
print("Command returned error code:" f" {process.returncode}")
|
2026-02-13 01:27:38 -05:00
|
|
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
if not quiet:
|
[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
2026-08-07 03:35:20 -04:00
|
|
|
print(f"Error: Command '{redact_secrets(command)}' not found.")
|
2026-02-13 01:27:38 -05:00
|
|
|
except Exception as e:
|
|
|
|
|
if not quiet:
|
[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
2026-08-07 03:35:20 -04:00
|
|
|
print(f"An error occurred: {redact_secrets(str(e))}")
|
2026-02-13 01:27:38 -05:00
|
|
|
process_end_time = time.time()
|
|
|
|
|
duration_sec = process_end_time - process_start_time
|
|
|
|
|
if humanize:
|
|
|
|
|
duration_delta = datetime.timedelta(seconds=duration_sec)
|
2026-03-10 03:45:11 -04:00
|
|
|
human_time = humanize.precisedelta(duration_delta)
|
2026-02-13 01:27:38 -05:00
|
|
|
if not quiet:
|
2026-03-12 05:20:32 -04:00
|
|
|
print(f"🏠 ⬆ Executed ({human_time}) :\n")
|
2026-02-13 01:27:38 -05:00
|
|
|
else:
|
|
|
|
|
if not quiet:
|
2026-03-12 05:20:32 -04:00
|
|
|
print(f"🏠 ⬆ Executed ({duration_sec:.2f} sec.) :\n")
|
2026-02-13 01:27:38 -05:00
|
|
|
if not quiet:
|
[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
2026-08-07 03:35:20 -04:00
|
|
|
print(redact_secrets(command))
|
2026-02-13 01:27:38 -05:00
|
|
|
print()
|
|
|
|
|
if return_status_and_output_and_command:
|
2026-03-10 03:06:07 -04:00
|
|
|
return exit_code, command, output_lines
|
2026-02-13 01:27:38 -05:00
|
|
|
if return_status_and_command:
|
2026-03-10 03:06:07 -04:00
|
|
|
return exit_code, command
|
2026-02-13 01:27:38 -05:00
|
|
|
if return_status_and_output:
|
2026-03-10 03:06:07 -04:00
|
|
|
return exit_code, output_lines
|
|
|
|
|
return exit_code
|