diff --git a/scripts/config-edit.sh b/scripts/config-edit.sh index 6e1c8e8..00d63c9 100755 --- a/scripts/config-edit.sh +++ b/scripts/config-edit.sh @@ -154,18 +154,10 @@ done # story: a YAML block scalar (`|`, `|-`, `>`, `>-`, ...) puts the VALUE on the lines that follow # the key, each indented deeper than it. The key-name match above only ever sees the key line # itself, so those continuation lines used to flow straight through unredacted while the key line -# right above them printed a reassuring "" — an incomplete redactor that looks complete -# is worse than one that visibly does nothing, because it stops a reviewer from looking further. -# The fix is structural, not another name to match: once a key line is masked, every following -# line indented STRICTLY DEEPER than that key is masked too, by indentation alone, until the -# indentation returns to the key's own level or shallower. This needs no knowledge of the key's -# name, so it covers a block scalar under any masked key — but ONLY while that key's own line is -# itself inside the hunk being printed. `diff -u` prints just three lines of context, so a block -# scalar's body often reaches this function with its key line left out; there is then nothing to -# anchor to, `masked` is never set, and the body prints in full. A blank line inside a block -# scalar loses the anchor the same way, because a blank diff line measures as indent 0. Both are -# measured and filed as fleetd #639 — do not read this paragraph as a guarantee that a masked -# key's value can never be printed. +# right above them printed a reassuring "". The redactor maps masked continuation lines +# from each complete file before it reads the diff. It then masks a printed line when that file +# line is inside a masked key's value. This covers block-scalar bodies even when the key line is +# outside the printed hunk, and it keeps blank lines inside the value masked. # # `redact` is always fed `diff -u` output, and every line of a unified diff starts with exactly # one of ' ', '+', '-' (the three body markers; '@'/'-'/'+' for the three header-line kinds too). @@ -174,37 +166,102 @@ done # column shallower than it really is, and either wrongly escapes a continuation mask or wrongly # ends one early. Tabs are out of scope: YAML forbids them for indentation, and this is a bounded # fix, not a YAML parser. +map_masked_lines() { + local file="$1" side="$2" line content indent lead key line_number=0 + local masked=0 masked_indent=0 + + case "$side" in + old) OLD_MASKED_LINES=() ;; + new) NEW_MASKED_LINES=() ;; + *) die "internal error: unknown redaction map side $side" ;; + esac + + while IFS= read -r line || [ -n "$line" ]; do + line_number=$((line_number + 1)) + content="$line" + indent=0 + while [ "${content:$indent:1}" = " " ]; do indent=$((indent + 1)); done + + if [ "$masked" = 1 ]; then + if [ -z "${content// /}" ] || [ "$indent" -gt "$masked_indent" ]; then + case "$side" in + old) OLD_MASKED_LINES[$line_number]=1 ;; + new) NEW_MASKED_LINES[$line_number]=1 ;; + esac + continue + fi + masked=0 + fi + + if [[ "$content" =~ ^([[:space:]]*)([A-Za-z0-9_.-]+:) ]]; then + lead="${BASH_REMATCH[1]}" + key="${BASH_REMATCH[2]}" + if [[ "$key" =~ (TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|URI|_KEY) ]]; then + masked=1 + masked_indent="$indent" + fi + fi + done < "$file" +} + redact() { - local line prefix content indent lead key - local masked=0 masked_indent=0 saved_nocasematch=0 + local old_file="$1" new_file="$2" + local line prefix content indent lead key old_line=0 new_line=0 in_hunk=0 + local old_masked new_masked saved_nocasematch=0 shopt -q nocasematch && saved_nocasematch=1 shopt -s nocasematch - sed -E 's#://[^@]*@#://@#g' | while IFS= read -r line || [ -n "$line" ]; do + + map_masked_lines "$old_file" old + map_masked_lines "$new_file" new + + while IFS= read -r line || [ -n "$line" ]; do + if [[ "$line" =~ ^@@\ -([0-9]+)(,([0-9]+))?\ \+([0-9]+)(,([0-9]+))?\ @@ ]]; then + old_line="${BASH_REMATCH[1]}" + new_line="${BASH_REMATCH[4]}" + in_hunk=1 + printf '%s\n' "$line" + continue + fi + case "$line" in [\ +-]*) prefix="${line:0:1}"; content="${line:1}" ;; *) prefix=""; content="$line" ;; esac + old_masked=0 + new_masked=0 + if [ "$in_hunk" = 1 ]; then + case "$prefix" in + ' ') + [ "${OLD_MASKED_LINES[$old_line]:-}" = 1 ] && old_masked=1 + [ "${NEW_MASKED_LINES[$new_line]:-}" = 1 ] && new_masked=1 + old_line=$((old_line + 1)); new_line=$((new_line + 1)) ;; + -) + [ "${OLD_MASKED_LINES[$old_line]:-}" = 1 ] && old_masked=1 + old_line=$((old_line + 1)) ;; + +) + [ "${NEW_MASKED_LINES[$new_line]:-}" = 1 ] && new_masked=1 + new_line=$((new_line + 1)) ;; + esac + fi + indent=0 while [ "${content:$indent:1}" = " " ]; do indent=$((indent + 1)); done - if [ "$masked" = 1 ] && [ "$indent" -gt "$masked_indent" ]; then + if [ "$old_masked" = 1 ] || [ "$new_masked" = 1 ]; then printf '%s%*s\n' "$prefix" "$indent" "" continue fi - masked=0 if [[ "$content" =~ ^([[:space:]]*)([A-Za-z0-9_.-]+:) ]]; then lead="${BASH_REMATCH[1]}" key="${BASH_REMATCH[2]}" if [[ "$key" =~ (TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|URI|_KEY) ]]; then printf '%s%s%s \n' "$prefix" "$lead" "$key" - masked=1 - masked_indent="$indent" continue fi fi - printf '%s\n' "$line" + printf '%s\n' "$line" | sed -E 's#://[^@]*@#://@#g' done [ "$saved_nocasematch" = 1 ] || shopt -u nocasematch } @@ -627,7 +684,7 @@ run_edit() { apply_mode "$cand" "$orig_mode" say "change (redacted)" - diff -u "$backup" "$cand" | redact || true + diff -u "$backup" "$cand" | redact "$backup" "$cand" || true say "install" install_candidate "$cand" "$CONFIG" \ @@ -656,7 +713,7 @@ dry_run_diff() { die "candidate does not parse as valid YAML — this was a --dry-run, nothing would have been installed either" fi say "dry run — diff (redacted), nothing installed" - diff -u "$CONFIG" "$cand" | redact || true + diff -u "$CONFIG" "$cand" | redact "$CONFIG" "$cand" || true rm -f "$cand"; CAND="" return 0 }