0cdb6f2c05
inkstone/InkStoneCo#23、#56 同一個病:閘對整條指令字串做關鍵字掃描, 把「檔案內容/留言引用裡剛好提到某個關鍵字」當成「真的在執行」, 同時放過包一層讀取指令、或藏在 heredoc body 裡的真動作。 - 新增共用輔助 hooks/lib/strip_heredoc.py:heredoc body 是資料不是指令, 四支閘(github-contact / main-and-prod-push / stage-before-prod / kbdb-api-wall 的 Bash 分支)呼叫前一律先拿掉 body 再比對。 - main-and-prod-push-guard.sh:修掉跟 release-tag-guard.sh 同款的 「開頭是讀取工具就整條放行」前綴繞過洞;git push 的偵測改成指令位置比對; main/master 目標改用單字邊界,不再誤中 "domain" 這種子字串。 - github-contact-guard.sh:拿掉 gh CLI/git push 判準裡「前面隨便一個空白 就算數」的鬆散邊界,只認真正的指令分隔符。 - kbdb-api-wall-guard.sh:Bash 分支原本引用不存在的 kbdb_cmd_check.py, python3 找不到檔案就吃掉錯誤印 "OK",該分支形同虛設——任何 `wrangler d1 execute` 直打 kbdb 都會被放行。邏輯搬進新檔 hooks/lib/kbdb_cmd_check.py(shlex 分詞、quote-aware),把 .sh 的 參照路徑改過去,補回 Bash 分支的 kbdb-sql-ok 逃生口。 四支各補 InkStoneCo#40 §1 要求的三行中文檔頭。 新增四支可重跑測試(scripts/test-*.sh),共 69 條斷言全過, 含 #23/#56 票上實撞的原始形狀(寫 docs/TESTING.md 的 heredoc、 貼引用 #56 敘述的留言、`grep git push`)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""hooks/lib/kbdb_cmd_check.py -- shared helper for kbdb-api-wall-guard.sh's
|
|
Bash channel (inkstone/ISEP#40 S7: helpers live in lib/, do not count as a
|
|
hand-written gate).
|
|
|
|
What it guards: whether a bash command string contains a real, at-command-
|
|
position invocation of wrangler's D1 execute subcommand (any wrapper such as
|
|
npx/pnpm) targeting a database whose name mentions "kbdb" -- that is a
|
|
direct-SQL bypass of the KBDB API wall (D38, leo 2026-08-07: zero SQL,
|
|
always through the HTTP API).
|
|
|
|
Why this file exists (not just "why the rule exists"): the calling gate,
|
|
hooks/kbdb-api-wall-guard.sh, used to pipe the raw command straight through
|
|
a keyword grep. That produced the exact "keyword shows up vs. real
|
|
instruction" confusion this repo has hit repeatedly (inkstone/InkStoneCo#23:
|
|
the phrase showed up inside a delegation prompt that was *talking about* the
|
|
rule, not breaking it). The fix pattern already proven on
|
|
hooks/release-tag-guard.sh is "only count a keyword when it sits at command
|
|
position" -- this file is that same pattern for the KBDB Bash channel,
|
|
factored out to its own file because embedding shell-escaping-aware parsing
|
|
inline in the .sh caused regressions each time someone touched it (see the
|
|
.sh file's own header for that history).
|
|
|
|
Two extra layers vs. a plain regex:
|
|
1. hooks/lib/strip_heredoc.py runs first (imported below) so a heredoc body
|
|
that merely *mentions* the D1-execute pattern against kbdb as
|
|
documentation text does not count -- same root cause as
|
|
InkStoneCo#23/#56.
|
|
2. Command segmentation uses shlex with punctuation_chars, so a `;`/`&`/`|`
|
|
that appears *inside* a quoted string (e.g. a commit message) does not
|
|
get treated as a command boundary, and text inside quotes is only
|
|
inspected when the quoted text is itself an argument to a command that
|
|
is genuinely at command position (e.g. a `--command` value passed to a
|
|
real wrangler invocation) -- not when it is merely quoted prose
|
|
describing the rule.
|
|
|
|
Genuine parse failures (unbalanced quotes, exotic constructs) fail OPEN
|
|
(print "OK"), matching this repo's stated design discipline: fail-open on
|
|
parse failure, not fail-open on the verdict itself (see release-tag-guard.sh
|
|
header). The caller (kbdb-api-wall-guard.sh) already blocks direct file-path
|
|
writes into kbdb/ paths and DDL/`.prepare(`/`.exec(`/`.batch(` in Write/Edit
|
|
content through its own separate channels -- this file only covers the Bash
|
|
CLI channel.
|
|
|
|
Usage:
|
|
printf '%s' "$CMD" | python3 hooks/lib/kbdb_cmd_check.py
|
|
-> prints exactly "BAD" or "OK" on stdout.
|
|
"""
|
|
import os
|
|
import shlex
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from strip_heredoc import strip_heredocs # noqa: E402
|
|
|
|
_WRAPPERS = {"npx", "pnpm", "yarn", "bunx", "exec", "sudo", "env", "corepack"}
|
|
_DB_HINT = "kbdb"
|
|
_D1_TOKEN = "d1"
|
|
_SUBCMD_TOKEN = "execute"
|
|
|
|
|
|
def _segments(cmd: str):
|
|
"""Split cmd into a list of token-lists, one per "simple command",
|
|
breaking on real shell control operators. Quote-aware via shlex, so a
|
|
control character inside a quoted string does not split. Returns None
|
|
on unparseable input (caller should fail open)."""
|
|
try:
|
|
lex = shlex.shlex(cmd, posix=True, punctuation_chars=True)
|
|
lex.whitespace_split = True
|
|
tokens = list(lex)
|
|
except ValueError:
|
|
return None
|
|
|
|
boundary = {";", "&", "&&", "|", "||", "(", ")", "\n"}
|
|
segments = []
|
|
current = []
|
|
for tok in tokens:
|
|
if tok in boundary:
|
|
if current:
|
|
segments.append(current)
|
|
current = []
|
|
else:
|
|
current.append(tok)
|
|
if current:
|
|
segments.append(current)
|
|
return segments
|
|
|
|
|
|
def _is_kbdb_d1_execute(segment):
|
|
"""True if this one simple command is wrangler's D1 execute subcommand
|
|
(optionally behind a wrapper like npx/sudo/env) aimed at a database
|
|
whose name mentions "kbdb" anywhere in its arguments."""
|
|
i = 0
|
|
# Skip leading VAR=value assignments (env-style prefix).
|
|
while i < len(segment):
|
|
head = segment[i].split("=", 1)[0]
|
|
if "=" in segment[i] and head.replace("_", "").isalnum() and head[:1].isalpha():
|
|
i += 1
|
|
else:
|
|
break
|
|
# Skip known wrappers (npx wrangler ..., sudo wrangler ..., env X=Y wrangler ...).
|
|
while i < len(segment) and os.path.basename(segment[i]) in _WRAPPERS:
|
|
i += 1
|
|
if i >= len(segment):
|
|
return False
|
|
cmdname = os.path.basename(segment[i])
|
|
if cmdname != "wrangler":
|
|
return False
|
|
rest = segment[i + 1 :]
|
|
if _D1_TOKEN not in rest or _SUBCMD_TOKEN not in rest:
|
|
return False
|
|
joined_lower = " ".join(rest).lower()
|
|
return _DB_HINT in joined_lower
|
|
|
|
|
|
def check(cmd: str) -> str:
|
|
stripped = strip_heredocs(cmd)
|
|
segments = _segments(stripped)
|
|
if segments is None:
|
|
return "OK" # parse failure -> fail open, not fail on the verdict
|
|
for seg in segments:
|
|
if _is_kbdb_d1_execute(seg):
|
|
return "BAD"
|
|
return "OK"
|
|
|
|
|
|
def main() -> None:
|
|
cmd = sys.stdin.read()
|
|
print(check(cmd))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|