mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* fix(careful): mask git commit -m content to prevent MEDIUM-tier false positives (#1060) MEDIUM-tier checks matched the full command string including quoted -m text. A commit like 'git commit -m fix: prevent drop table in migration' triggered the SQL DROP warning even though no SQL was being executed. Build CMD_MEDIUM by stripping -m quoted content from git commit commands before MEDIUM checks. Real destructive commands outside the quoted string still match, so true positives are unaffected. Add tests: commit message patterns do not warn; chained real DROP/TRUNCATE still warns. * fix(careful): replace sed masking with quote-aware awk scanner (#1060) The sed-based -m content masker had two bypass paths that capy-ai identified: 1. Single-quoted message containing a double-quote boundary: git commit -m 'note -m "' && rm -rf build && echo '"' The double-quote pass matched '-m "' inside the single-quoted string, swallowing '&& rm -rf build && echo '; the rm vanished from CMD_MEDIUM. 2. Double-quoted message with a command substitution: git commit -m "$(rm -rf build)" sed replaced the content including the $(...), so the substitution — which executes in the shell — was masked away. Replace the sed substitution with a quote-aware awk scanner that: - Walks the command string tracking single-quote, double-quote, and backslash state, so it only treats a delimiter as a message boundary when the shell would. - Masks single-quoted messages (always literal) and double-quoted messages that contain no $, backtick, or backslash (safe to mask — no expansion). - Leaves double-quoted messages with $, backtick, or backslash unmasked so command substitutions stay visible to the MEDIUM checks. - Handles -m, -am, --message, and --message= forms. Four new tests added as requested (alongside the original six): - single-quoted message with embedded double-quote + chained rm → warns - commit message with $(rm -rf build) → warns - git push --force chained after commit → warns - git commit -am "drop table notes" → silent (message masked) * fix(careful): read the commit-message masker from a file, so bash 3.2 can parse the hook (#1060) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(careful): scan the command as one string, so a newline still separates commands from the message (#1060) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(careful): mask only git commit's own message; a piped commit or another command's -m stays visible (#1060) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(careful): mask a quoted-delimiter heredoc message; any other substitution or heredoc leaves the command unmasked (#1060) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(careful): a pipe, process substitution or parameter expansion after the message unmasks it; redirections and comments parse like bash (#1060) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): careful masks git commit's own message (#1060, port of #3221) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Bhumika-1432006 <guravbhumika808@gmail.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
126 lines
4.9 KiB
Awk
126 lines
4.9 KiB
Awk
# mask-commit-message.awk — read by careful/bin/check-careful.sh (#1060).
|
|
# Prints the command with the quoted message argument of git commit
|
|
# (-m, -am, --message) replaced by an empty quoted string. The command is
|
|
# scanned as one string: newlines are kept, since bash runs each line as its
|
|
# own command, and quote state carries across lines.
|
|
#
|
|
# Only git commit's own arguments are masked: the scanner splits the command
|
|
# at unquoted ; & | ( ) and newlines, and masks a message flag only in a
|
|
# segment whose first word is git and that has a commit word after it. The &
|
|
# of a redirection (2>&1, &>) does not split. Any pipe after a masked message
|
|
# prints the command unchanged, because git prints the message on stdout.
|
|
# Quotes, backslashes and comments follow bash: a backslash escapes the next
|
|
# character outside single quotes, and nothing inside single quotes; an
|
|
# unquoted # starting a word runs to the end of its line. An unterminated
|
|
# quote or escape prints the command unchanged.
|
|
#
|
|
# A message written as "$(cat <<'EOF' ... EOF )" (a quoted delimiter, so the
|
|
# body is never expanded) is masked whole. Any other command, parameter or
|
|
# process substitution, backtick, $'...' string or heredoc changes how bash
|
|
# reads the quotes after it, so the command is printed unchanged.
|
|
# heredoc_msg S N I — S[I] opens a double-quoted "$(cat <<'DELIM' ... DELIM )"
|
|
# message: the index after its closing quote, else 0.
|
|
function heredoc_msg(s, n, i, p, rest, strip, delim, e, line) {
|
|
rest = substr(s, i + 1)
|
|
if (!match(rest, /^\$\(cat[ \t]+<<-?[ \t]*/)) return 0
|
|
strip = substr(rest, 1, RLENGTH) ~ /<<-/
|
|
p = i + 1 + RLENGTH
|
|
rest = substr(s, p)
|
|
if (match(rest, /^'[A-Za-z_][A-Za-z0-9_]*'/) || match(rest, /^"[A-Za-z_][A-Za-z0-9_]*"/)) delim = substr(rest, 2, RLENGTH - 2)
|
|
else if (match(rest, /^\\[A-Za-z_][A-Za-z0-9_]*/)) delim = substr(rest, 2, RLENGTH - 1)
|
|
else return 0
|
|
p += RLENGTH
|
|
while (p <= n && substr(s, p, 1) ~ /[ \t]/) p++
|
|
if (substr(s, p, 1) != "\n") return 0
|
|
p++
|
|
for (;;) {
|
|
if (p > n) return 0
|
|
e = index(substr(s, p), "\n")
|
|
if (!e) return 0
|
|
line = substr(s, p, e - 1)
|
|
if (strip) sub(/^\t+/, "", line)
|
|
p += e
|
|
if (line == delim) break
|
|
}
|
|
while (p <= n && substr(s, p, 1) ~ /[ \t\n]/) p++
|
|
if (substr(s, p, 2) != ")\"") return 0
|
|
return p + 2
|
|
}
|
|
function handle_msg(s, n, i, j, c, safe) {
|
|
if (i > n) return i
|
|
if (substr(s, i, 1) == "'") {
|
|
j = i + 1
|
|
while (j <= n && substr(s, j, 1) != "'") j++
|
|
if (j > n) return i
|
|
result = result "''"
|
|
return j + 1
|
|
}
|
|
if (substr(s, i, 1) == "\"") {
|
|
if ((j = heredoc_msg(s, n, i)) > 0) { result = result "\"\""; return j }
|
|
j = i + 1; safe = 1
|
|
while (j <= n && (c = substr(s, j, 1)) != "\"") {
|
|
if (c == "$" || c == "`" || c == "\\") safe = 0
|
|
j++
|
|
}
|
|
if (!safe || j > n) return i
|
|
result = result "\"\""
|
|
return j + 1
|
|
}
|
|
return i
|
|
}
|
|
# flag S I — length of the message flag (with its trailing blanks) at S[I], or 0
|
|
function flag(s, i, rest, m) {
|
|
rest = substr(s, i)
|
|
if (rest ~ /^--message=/) return 10
|
|
if (match(rest, /^(--message|-am|-m)[ \t]+/)) return RLENGTH
|
|
return 0
|
|
}
|
|
{ cmd = (NR > 1) ? cmd "\n" $0 : $0 }
|
|
END {
|
|
s = cmd
|
|
n = length(s); sq = 0; dq = 0; bs = 0; i = 1
|
|
atword = 1; words = 0; git = 0; commit = 0; masked = 0; piped = 0; unsure = 0
|
|
while (i <= n) {
|
|
c = substr(s, i, 1)
|
|
if (bs) { result = result c; bs = 0; i++; continue }
|
|
if (sq) { if (c == "'") sq = 0; result = result c; i++; continue }
|
|
two = substr(s, i, 2)
|
|
if (c == "`" || two == "$(" || two == "${") unsure = 1
|
|
if (dq) {
|
|
if (c == "\\") bs = 1
|
|
else if (c == "\"") dq = 0
|
|
result = result c; i++; continue
|
|
}
|
|
if (two == "<<" || two == "$'" || two == "<(" || two == ">(") unsure = 1
|
|
if (atword && c == "#") {
|
|
while (i <= n && substr(s, i, 1) != "\n") { result = result substr(s, i, 1); i++ }
|
|
continue
|
|
}
|
|
if (c == "&" && (substr(s, i - 1, 1) ~ /[<>]/ || substr(s, i + 1, 1) == ">")) { atword = 0; result = result c; i++; continue }
|
|
if (c ~ /[;&|()\n]/) {
|
|
if (c == "|" && masked && substr(s, i + 1, 1) != "|" && substr(s, i - 1, 1) != "|") piped = 1
|
|
atword = 1; words = 0; git = 0; commit = 0
|
|
result = result c; i++; continue
|
|
}
|
|
if (c == " " || c == "\t") { atword = 1; result = result c; i++; continue }
|
|
if (atword) {
|
|
atword = 0; words++
|
|
rest = substr(s, i)
|
|
if (words == 1 && rest ~ /^git([ \t\n;&|()]|$)/) git = 1
|
|
else if (git && !commit && rest ~ /^commit([ \t\n;&|()]|$)/) commit = 1
|
|
else if (commit && (len = flag(s, i)) > 0) {
|
|
result = result substr(s, i, len); i += len
|
|
j = handle_msg(s, n, i)
|
|
if (j > i) masked = 1
|
|
atword = (j == i); i = j
|
|
continue
|
|
}
|
|
}
|
|
if (c == "\\") bs = 1
|
|
else if (c == "'") sq = 1
|
|
else if (c == "\"") dq = 1
|
|
result = result c; i++
|
|
}
|
|
printf "%s", (sq || dq || bs || piped || unsure) ? s : result
|
|
}
|