From 5f5d16fbd47b47d6cdfd09d53abf8644991eef3e Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sat, 3 Oct 2026 22:34:02 +0200 Subject: [PATCH] fleetd #638: stop the verdict userinfo mask from crossing / or whitespace mask_verdict_userinfo's character class [^@]* crossed a '/' or a space, so a verdict line with a URI that has no userinfo plus a later @ (e.g. an email address in diagnostic prose) had everything between them destroyed. Restrict the class to [^@/[:space:]]* so the match stops at the end of the URI. Adds the uncovered-direction test: a URI with no userinfo plus a later @ in the same line must pass through byte for byte. Also drops the design-rationale sentence from the helper's comment (now in the PR description). --- scripts/config-edit.sh | 6 ++---- scripts/test-config-edit.sh | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/scripts/config-edit.sh b/scripts/config-edit.sh index 24132a32..3f1e61fc 100755 --- a/scripts/config-edit.sh +++ b/scripts/config-edit.sh @@ -581,11 +581,9 @@ install_candidate() { # -------------------------------------------------------------------------------- the report path # # Masks basic-auth userinfo (scheme://user:pass@host) in a daemon verdict line before it reaches -# the terminal. Not routed through redact(): that function's key:value masking does not match this -# line's prose, and masking anything beyond the userinfo would remove the detail an operator needs -# to diagnose a refusal. +# the terminal. mask_verdict_userinfo() { - printf '%s\n' "$1" | sed -E 's#://[^@]*@#://@#g' + printf '%s\n' "$1" | sed -E 's#://[^@/[:space:]]*@#://@#g' } # Prints the literal command the operator (or a test) can run to restore the backup by hand — the diff --git a/scripts/test-config-edit.sh b/scripts/test-config-edit.sh index 326f6648..6b957f83 100755 --- a/scripts/test-config-edit.sh +++ b/scripts/test-config-edit.sh @@ -670,6 +670,24 @@ test_ordinary_refusal_line_passes_through_unchanged() { "an ordinary refusal with no userinfo must pass through byte for byte, unchanged" } +# A verdict line can hold a URI with NO userinfo and a later, unrelated @ further on in the same +# line (an email address in diagnostic prose, for example). The rewrite must stop at the end of +# the URI and must not treat the later @ as a second userinfo delimiter. +test_uri_without_userinfo_survives_a_later_at_sign() { + local dir real_line + dir="$(new_fixture)" + real_line="config reload from $dir/fleetd.yaml refused, keeping the running config: broker.uri amqp://broker.local/vhost unreachable, contact ops@example.com" + + start_run "$dir" 5 --set '.profiles.sonnet.weight=4' + sleep 1 + printf '%s\n' "$real_line" >> "$dir/fleetd.out" + collect_run "$dir" + + assert_equals 4 "$RUN_RC" "no-userinfo-with-later-at-sign exit code" + assert_contains "$real_line" "$RUN_OUTPUT" \ + "a URI with no userinfo plus a later @ in the same line must pass through byte for byte" +} + # --set runs yq over the whole candidate. It warns when that changes more lines than the requested # pairs, but a simple file with only the intended changed line must stay quiet. new_fixture_reformat_sensitive() { @@ -765,6 +783,8 @@ echo "== verdict-redaction criteria 2+3: verdict userinfo is masked, rest of lin test_verdict_userinfo_is_masked_with_positive_control echo "== verdict-redaction criterion 4: an ordinary refusal passes through unchanged ==" test_ordinary_refusal_line_passes_through_unchanged +echo "== fleetd #638: a URI with no userinfo survives a later @ in the same line ==" +test_uri_without_userinfo_survives_a_later_at_sign echo "== acceptance criterion 17: --set warns about yq formatting churn ==" test_set_warns_when_yq_reformats_extra_lines echo "== acceptance criterion 18: --set stays quiet without formatting churn =="