Skip to content

land: command-redaction-output - #854

Open
aft-alfonso[bot] wants to merge 10 commits into
mainfrom
land/command-redaction-output
Open

aft-alfonso[bot] wants to merge 10 commits into
mainfrom
land/command-redaction-output

Conversation

@aft-alfonso

@aft-alfonso aft-alfonso Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Accepted agent delivery; branch-keyed search descriptor included.

RetriggerView in GreptileConfidence Score: 3/5

The password leak in log_redact.rs should be fixed before merging.

Findings

  1. P1 Security Passwords leak after redirections ▶

Summary

The PR reduces command text kept in diagnostics and improves shortened replies.

  • Background command logs and compression records keep credentials out.
  • Failed Bash replies keep failure lines from the cut output.
  • Edit errors summarize long searches that do not match.
  • Routine cat rewrite declines no longer fill warning logs.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Shell command] --> B[Mask inline credentials]
  B --> C[Shorten spawn log]
  C --> D[Shared log redactor]
  D --> E[Log file]
  D --> F[Stderr]
Loading

Reviews (3) · Last reviewed commit: "mason: end logged curl and mysql command..." · Reviewed by Greptile

ualtinok and others added 7 commits October 24, 2026 13:22
…low-ups (#825, #759)

Tests only, all ignored with "fails today (#N)" reasons; each fails today
for the stated reason when run with --ignored:

- spawn_log_masks_inline_command_credentials (#825): the background spawn
  log line, after the same redactor the log file writer applies, still
  carries NAME=value secrets, credential flags, mysql -p<value>, and a
  GitHub token cut short by the 200-character limit. Also fences seven
  ordinary commands (cargo -p, git log -p, ssh -p, TOKENIZERS_PARALLELISM,
  GIT_AUTHOR_NAME, $VAR references, gcloud --secret=<name>) that any
  broader rule must leave unchanged.
- reply_cap_keeps_failure_lines_from_the_middle_of_unrecognised_output
  (#759): the reply byte cap drops FAILED:/ERROR: lines from the middle of
  output no compressor recognises.
- routine_out_of_root_cat_decline_is_not_a_warning (#759): every cat of a
  file outside the project root writes a WARN line.
- not_found_error_does_not_repeat_a_large_match in batch and edit_match
  (#759): the not-found error pastes the whole match text back.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
Nothing reads compression_events.command (aggregates, status and retention
use counts and token sums only), and a stored command can carry an inline
credential. New events no longer write it: CompressionEventRow loses its
command field and the insert leaves the column NULL.

Rows written by earlier builds are cleared once per database on a writable
open (UPDATE ... SET command = NULL), recorded by a host_state marker so
later opens only read the marker. The column stays and the schema version
is unchanged, so older builds can still open aft.db; read-only opens never
run the cleanup.

The integration test compression_event_records_command_for_diagnostics
pinned the stored command; nothing outside that test read it. It becomes
compression_event_does_not_store_the_command and asserts the opposite
through the real completion path, which is the point of this change.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
The 'bash spawned' INFO line quoted the first 200 characters of the command
with only the shared log patterns applied, so GITHUB_TOKEN=..., export
DB_PASSWORD=..., --password/--token values and mysql -p<value> reached the
log file and the plugin log copy. It also cut before masking, so a token
crossing the 200-character limit lost the length its pattern needs and was
written partly unmasked.

log_redact::redact_command adds command-only shapes on top of aft_redactor:
UPPER_SNAKE secret-named assignments (skipping *_PATH/_FILE/_ID/... names
and $VAR / $(...) references), credential long options (not --secret,
which names a secret), and tool-scoped mysql -p<value>, sshpass -p and
curl -u user:pass. Generic -p flags (cargo, ssh, mkdir) are untouched.
log_bash_spawn masks the whole command, then shortens it.

The fixture holds the 24 masked cases and 803 untouched commands (every
command line from this repo's scripts, workflows and package.json scripts
plus common cargo/git/npm/ssh/docker invocations); the command shapes
change none of them. log_bash_spawn is the only log site that quotes the
shell command.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
A command no compressor recognises gets only the 16 KiB reply cap: a 6 KiB
head, at most twenty tail lines and each stream's final line. Failure lines
in between were dropped (3,000 lines with FAILED: at 1500 and ERROR: at
2200 showed neither), because the compressor-level failure rescue runs only
after a specific compressor matched, and before this cap.

For a known non-zero exit the reply cap now keeps up to twenty failure
lines (4 KiB) from the cut middle, first ones first, under the existing
'[raw failure lines preserved by AFT]' label, just before the final line.
The scan stops at the twentieth line. The block's bytes come out of the
head budget, and the lines are picked again from the resulting cut (at
most three passes) so the reply stays within its cap. Successful commands,
completion previews and raw (uncompressed) replies are unchanged.

The label text moves to compress::PRESERVED_FAILURES_LABEL, shared by both
rescues; the compressor-level output is byte-identical.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
A failed edit's not-found error pasted the whole searched text back
(batch: edit[i] match '<text>' not found in file, and both edit_match
forms), so a large oldString was repeated into the transcript. The host
edit tool reaches the batch form for a single file.

fuzzy_match::describe_searched_text quotes one line of up to 120
characters as before; anything longer becomes its first line cut to 120
characters, an ellipsis and its size, e.g.
  '    let absent_0 = 0;…' (400 lines, 9490 bytes)
The bounded nearest-miss detail after it is unchanged.

large_match_reflow_search_is_linear_and_still_applies asserted the full
message including its 64/256 KiB needle; it now expects the short form,
which is the point of this change. Short-match assertions elsewhere are
unchanged.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
Every cat of a file outside the project root (a host's temp folder such
as $TMPDIR/opencode/...) or of an unreadable shape logged a WARN line,
although the command just runs as native bash. The decline line keeps its
predicate and resolved path at debug, beside the dispatcher's own debug
record. The two tests that pinned the line now capture at debug; the one
named 'logs_warning' is renamed to 'logs_decline'.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
Comment thread crates/aft/src/log_redact.rs Outdated
Comment thread crates/aft/src/log_redact.rs Outdated
…ommands (#825)

Two gaps in redact_command:

- The assignment rule required a letter before the credential word, so a
  bare TOKEN=hunter2 or PASSWORD=hunter2 was logged unmasked. The name may
  now start with the credential word; TOKENIZERS_PARALLELISM and MAX_TOKENS
  still do not match, because the word must be followed by = or _.
- Values stopped inside a shell word: curl -u "admin:my password" masked
  only 'my', the mysql and sshpass rules stopped at a quoted space, and the
  assignment and long-option rules stopped at an escaped quote or after the
  first part of a concatenation ('a'"b"c). Every value pattern now takes the
  whole shell word: single-quoted, double-quoted with escapes,
  backslash-escaped and concatenated runs, up to the next unquoted
  whitespace or shell operator. The curl rule takes the whole user:password
  word and masks what follows the first colon, keeping a wrapping quote pair.

Eleven masked fixtures cover these forms (35 in all); the 803 benign
commands, including cargo test -p and mkdir -p, are still unchanged.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
Comment thread crates/aft/src/log_redact.rs Outdated
ualtinok and others added 2 commits October 24, 2026 14:25
The curl rule matched the first -u value after the curl prefix. A
username-only value (curl -u alice https://one --next -u admin:hunter2
https://two) was left unchanged, but the match had consumed the only curl
prefix, so the later admin:hunter2 was never checked and reached the log.

The rule now takes each curl command span (up to the next shell operator or
newline) and checks every user option inside it, in all of curl's spellings:
-u value, -uvalue, --user value and --user=value. A value without a
password, or with a reference as its password, is still left as written.

Fixtures add a --next command with a later password, two -u values that
both have passwords, --user=admin:pw and -uadmin:pw (39 masked cases);
username-only values join the untouched list, which stays unchanged.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
…825)

The curl command span (\bcurl\b[^;&|\n]*) and the mysql prefix
([^;&|\n]*?) stopped at the first ;, & or |, even inside quotes. In
curl -u "admin:pa;ss" https://x the span ended inside the quoted value, so
only 'pa' was masked and ';ss"' stayed in the log.

Both are now built from whole shell words separated by spaces or tabs
(shell_word!), so an operator or newline ends the command only outside
quotes; redirections and parentheses do not end it. sshpass takes its
value directly after -p and already used whole words.

Fixtures add quoted passwords containing ;, & and | for curl, mysql (also
after a quoted host containing ;) and sshpass, and a curl that follows an
unquoted ; (45 masked). Two commands whose unquoted ; really ends the curl
or mysql command before a later -u or -p join the untouched list.

Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
/// quotes. Redirections and parentheses do not end it.
macro_rules! command_args {
() => {
concat!(r"(?:[ \t]+(?:", shell_word!(), r"|[<>()])+)*")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Passwords leak after redirections

Commands such as curl</dev/null -u admin:hunter2 https://host now leave the password in the log. command_args! requires whitespace immediately after curl, so it stops before the redirection and never reaches -u. The changed COMMAND_MYSQL_PASSWORD has the same problem with mysql</dev/null -u root -phunter2 db. Both forms were covered by the previous patterns.

Allow attached redirections before later arguments and add fixtures for both commands.

How this was verified: Both changed patterns stop at the attached redirection, and the remaining plain password passes through log_bash_spawn and the shared redactor into the log file and stderr.

Knowledge Base Used: Runtime safety and operations

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant