From 13801d842b73794a67a972900dd22617797e0902 Mon Sep 17 00:00:00 2001 From: MendixMau Date: Thu, 15 Oct 2026 22:37:12 +0800 Subject: [PATCH] fix(app-restart): mask Name=value secrets such as --constant X.Password=v (TK-003) Field report from a five-app lab project (Mendix 11.15, mxcli v0.26.0). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + project-bin/app-restart.sh | 9 +++++++-- tests/wave2/test-app-restart.sh | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e238acd..99a0ff72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- fix(app-restart): mask the value of any `Name=value` whose name holds password/secret/token/key (e.g. `--constant Module.XPassword=v`), not only values after secret-named flags; the printed `start:` line leaked them (TK-003) — five-app lab project (Mendix 11.15, mxcli v0.26.0) - new(mxpatch, exec): **`project-bin/mxpatch.py`, the shared patch core, and a `mxcli fix hashes` check around every `exec.sh --patch` run: the two shipped workflow patchers no longer leave a stale ContentsHash.** mxpatch finds a unit by type + name (+ module) with a byte pre-filter, finds an element by Name, gets/sets a value by key, and writes the unit and its `.mpr` ContentsHash (base64 SHA-256 of the file) in one step; dry run by default, `already applied` on a re-run, and a refusal (exit 3) when the current value is not the expected old one. `wf-set-call-captions.py` and `wf-add-path-terminators.py` now write through it, refuse drift (a caption is replaced only while it is still the stamped microflow name or the `--expect-caption` value; `--expect-paths N` for terminators), and also take the `exec.sh --patch` form (` Module.Workflow`). The index is committed before the file swap, so a kill mid-write leaves a MISMATCH that `fix hashes` reports and a re-run repairs. `mxunit_bson.py` reads BSON 0x09 (UTC datetime). `exec.sh --patch` runs `mxcli fix hashes` (v0.25.0+, probed via `fix --help`) before and after the script and restores the snapshot on any issue the baseline did not have, before mxbuild, which cannot see it. Field evidence: captured mxcli v0.25.0/v0.26.0 runs on a public model (`tests/wave2/fixtures/mxpatch/CAPTURE.txt`): master's patchers leave a MISMATCH, the ported ones verify clean; the hash routine agrees with mxcli on 67 of 67 mismatched and 302 matching units of a second model. Widget-shape copying and pluggable-widget properties wait on a captured page unit. Field run ([#390](https://github.com/MendixMau/mxcli-project-toolkit/issues/390), mxcli v0.26.0, macOS): PASS, and the patch repaired a ContentsHash MISMATCH an old-style `--patch` had left on the unit it rewrote. Fixes from it: the backup of a rewritten unit is kept outside `mprcontents/` (in `$MXPATCH_BACKUP_DIR`, which `exec.sh --patch` points into the snapshot it just took, else a 0700 `/mxpatch-bak-/`; refused when it resolves inside `mprcontents/`, through a symlink or another case spelling; named with pid and counter so two writes in one second keep both), no longer as an untracked `.mxunit.bak` that `git add mprcontents` would commit; the BUILD-LOG row carries the hash bracket (`hashes: baseline 1 mismatch (pre-existing), verify clean (1 pre-existing repaired by this patch's rewrite)`); and the baseline message says that a unit the patch rewrites gets a fresh hash, and to repair the others with `fix hashes --repair`. A baseline MISMATCH that is still reported on a unit the patch rewrote counts as new (restore, exit 1), not as pre-existing. — jopterhorst ([#294](https://github.com/MendixMau/mxcli-project-toolkit/issues/294), [#295](https://github.com/MendixMau/mxcli-project-toolkit/issues/295)) - learn(pipelines/mendix-mpr/samples): **three minimal mxcli repros from the native-module bug hunt, the files the upstream draft cites** (split from [#293](https://github.com/MendixMau/mxcli-project-toolkit/pull/293), 6/6). `fsm-template-9.24.2/native-bughunt/` holds CAPTURE.txt and D1–D3, which are written from scratch in a `Repro` module: `describe entity` printing `index ( desc);`, the native Repeater `content` slot, and no flow writable to a 9.24.2 project. They are the repros for U3, U2 and U14 in `bug-logs/pending-github-issues/2026-10-12-mxcli-0.26-round-trip-report.md`. The other 472 sample files of #293 had no citing reader, because the skills and Rehost splits state their field results inline, so none of them ships. — MendixMau, Decompose/Rehost lab runs 2026-10-12 - learn(inbox, pending-github-issues): **ten field-note drops from the Decompose/Rehost lab runs, and one not-filed draft of 53 mxcli 0.26 defects for mendixlabs/mxcli** (split from [#293](https://github.com/MendixMau/mxcli-project-toolkit/pull/293), 5/6). `contrib/inbox/2026-10-1{2,3}-*.md`: increments 1–8 of the split-a-monolith runs on a public Marketplace template (FSM 9.24.2), the native-module bug hunt, and the GenAI Showcase regeneration (0.26.0 and the 2026-10-13 nightly); each now has the template's front matter and is at most 600 words. `bug-logs/pending-github-issues/2026-10-12-mxcli-0.26-round-trip-report.md`: the U1–U53 table the inbox drops refer to, marked DRAFT — NOT FILED, to be split into one issue per defect and retested before anything is posted. No BUG numbers assigned; triage promotes. — MendixMau, Decompose/Rehost lab runs 2026-10-12/13 diff --git a/project-bin/app-restart.sh b/project-bin/app-restart.sh index 4126cad8..dbf84ae9 100755 --- a/project-bin/app-restart.sh +++ b/project-bin/app-restart.sh @@ -40,7 +40,7 @@ # Git Bash, so there --cmd is needed the first time. Every successful restart remembers the # command in .claude/loop/app-restart/start.cmd (self-gitignored, machine-local), so the next one # — or one after the app died — needs no --cmd. It must be an `mxcli run --local` command; this -# script starts nothing else. Values after password/secret/token/key flags are masked on screen. +# script starts nothing else. Values after password/secret/token/key flags, and of any Name=value whose Name holds one, are masked on screen. # # READY means: a new record with a live pid, a Mendix answer on its port (the login/index page # carries Mendix markers — any 4xx-answering process is NOT enough, see test-stack-up.sh), and the @@ -134,8 +134,13 @@ bundle_ok() { # $1 port — the script bundle index.html loads must answer 2xx mask() { # print argv joined, with the value after any password/secret/token/key flag masked local a out="" for a in "$@"; do case "$a" in *" "*) a="'$a'" ;; esac; out="$out $a"; done + # Two passes (TK-003): (1) a secret-named FLAG masks its value, as `--db-password v` or + # `--db-password=v`; (2) ANY `Name=value` whose Name holds a secret word masks the value, which + # is how `--constant Module.HubApiPassword=v` (flag name `--constant`) used to print in clear. + local _w='([Pp][Aa][Ss][Ss]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Tt][Oo][Kk][Ee][Nn]|[Kk][Ee][Yy])' printf '%s\n' "${out# }" | sed -E \ - "s/(-[-A-Za-z_]*([Pp][Aa][Ss][Ss]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Tt][Oo][Kk][Ee][Nn]|[Kk][Ee][Yy])[-A-Za-z_]*)(=| +)[^ ']+/\1\3****/g" + -e "s/(-[-A-Za-z_]*${_w}[-A-Za-z_]*)(=| +)[^ ']+/\1\3****/g" \ + -e "s/([A-Za-z0-9_.-]*${_w}[A-Za-z0-9_.-]*)=[^ ']+/\1=****/g" } # Port and process helpers (mxtk_port_holder, mxtk_holder_text, mxtk_pid_in_tree) live in diff --git a/tests/wave2/test-app-restart.sh b/tests/wave2/test-app-restart.sh index d556ba43..bfed6027 100755 --- a/tests/wave2/test-app-restart.sh +++ b/tests/wave2/test-app-restart.sh @@ -267,6 +267,12 @@ has "points at --cmd" "$OUT" "--cmd" echo "-- secrets masked" OUT="$(restart "$P" --dry-run --cmd './mxcli run --local --db-password hunter2 -p Fixture.mpr')" hasnt "a password value never reaches the screen" "$OUT" "hunter2" +OUT="$(restart "$P" --dry-run --cmd './mxcli run --local --constant Mod.HubApiPassword=s3cretA --constant Mod.Api_Token=tokB --db-password hunter3 --db-password=hunter4 --constant Mod.Port=8080 -p Fixture.mpr')" +hasnt "--constant Name=value with a Password name is masked" "$OUT" "s3cretA" +hasnt "a Token-named constant is masked" "$OUT" "tokB" +hasnt "--db-password and --db-password= are masked" "$OUT" "hunter3" +hasnt "the = form is masked" "$OUT" "hunter4" +has "a non-secret constant stays readable" "$OUT" "Mod.Port=8080" alive "$DECOY" && ok "decoy survived the whole run" || bad "decoy killed" echo "-- #160: a bin/_common.sh older than the moved helpers → exit 2 naming sync, nothing stopped"