@tyhld/conductor 0.12.60 → 0.12.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/scripts/hooks/policy.py
CHANGED
|
@@ -3228,7 +3228,7 @@ PROTECTED_BRANCHES = frozenset({'main', 'master', 'production', 'prod', 'release
|
|
|
3228
3228
|
# DDL / 権限 / RLS を含む SQL の字面(DB実行の関門)。
|
|
3229
3229
|
_SQL_GATE_RE = re.compile(
|
|
3230
3230
|
r'\b(create|alter|drop)\s+(table|index|policy|database|schema|role|function|trigger|view)\b'
|
|
3231
|
-
r'|\bgrant\b|\brevoke\b|\brow\s+level\s+security\b
|
|
3231
|
+
r'|\bgrant\b|\brevoke\b|\brow\s+level\s+security\b',
|
|
3232
3232
|
re.IGNORECASE,
|
|
3233
3233
|
)
|
|
3234
3234
|
|
|
@@ -3263,6 +3263,31 @@ def _sql_scan_text(segment: str) -> str:
|
|
|
3263
3263
|
kept.append(t)
|
|
3264
3264
|
return ' '.join(kept) + sep + body
|
|
3265
3265
|
|
|
3266
|
+
# SQL の TRUNCATE か、ファイルを縮める道具の truncate かを見分ける(便 7585f96e)。
|
|
3267
|
+
#
|
|
3268
|
+
# 【なぜ要るか(実測 2026-09-20・便 2fde49cf)】
|
|
3269
|
+
# `truncate` の字面だけを SQL とみなしていたため、台本を書き換えるコマンドの【中の文字】に
|
|
3270
|
+
# truncate -s 0 …
|
|
3271
|
+
# が出てくるだけで DB の関門に落ち、管制の確認待ちのまま作業が1つ実行されなかった。
|
|
3272
|
+
# ★動詞そのものが truncate のとき(`truncate -s 0 build.log`)は _SQL_TEXT_ONLY_VERBS で
|
|
3273
|
+
# 既に除いてあったが、python3 -c / node -e / ヒアドキュメントのように
|
|
3274
|
+
# 【実行しうる入口】の中では、字面そのものを見るため除外が効かなかった。
|
|
3275
|
+
#
|
|
3276
|
+
# 【どう見分けるか】次の語の形で決める。★SQL の側は1つも減らさない。
|
|
3277
|
+
# ・次が旗(-s / --size …) → ファイルを縮める道具(SQL ではない)
|
|
3278
|
+
# ・次が道(/ を含む語) → ファイルを縮める道具(SQL ではない)
|
|
3279
|
+
# ・それ以外(TABLE / ONLY / 表の名前 / 何も続かない)→ 今までどおり SQL として関門
|
|
3280
|
+
_SQL_TRUNCATE_RE = re.compile(
|
|
3281
|
+
r'\btruncate\b(?!\s+(?:-{1,2}[A-Za-z0-9]|\S*/))',
|
|
3282
|
+
re.IGNORECASE,
|
|
3283
|
+
)
|
|
3284
|
+
|
|
3285
|
+
|
|
3286
|
+
def sql_gate_text(text: str) -> bool:
|
|
3287
|
+
"""その字面が「DB実行の関門に当てる SQL」か(純粋関数=単体試験できる)。"""
|
|
3288
|
+
if not text:
|
|
3289
|
+
return False
|
|
3290
|
+
return bool(_SQL_GATE_RE.search(text) or _SQL_TRUNCATE_RE.search(text))
|
|
3266
3291
|
|
|
3267
3292
|
# 「引数にSQLらしい字面があっても、実際にはSQLを実行しない」動詞。
|
|
3268
3293
|
# ★SQL文字列だけを根拠にした関門は、これらの動詞では出さない(=誤検知を消す)。
|
|
@@ -3354,7 +3379,7 @@ def _gate_db(words, tokens, segment: str):
|
|
|
3354
3379
|
return 'drizzle-kit push/migrate'
|
|
3355
3380
|
# SQL の字面(DDL / GRANT / RLS)。★ここへ来るのは「実行しうる入口」だけ
|
|
3356
3381
|
# (文字を扱うだけの道具とシェルは _gate_one で先に振り分け済み)。
|
|
3357
|
-
if
|
|
3382
|
+
if sql_gate_text(_sql_scan_text(segment)):
|
|
3358
3383
|
return 'SQL DDL/GRANT/RLS'
|
|
3359
3384
|
return None
|
|
3360
3385
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""便 7585f96e: ファイルを縮める道具の truncate を、SQL の TRUNCATE と見分ける試験。
|
|
3
|
+
|
|
4
|
+
実行: python3 scripts/hooks/test_gate_truncate.py
|
|
5
|
+
1件でも期待と違えば exit 1。
|
|
6
|
+
|
|
7
|
+
【なぜ要るか(実測 2026-09-20・便 2fde49cf)】
|
|
8
|
+
台本を書き換えるコマンドの【中の文字】に truncate -s 0 … が出てくるだけで、番人が
|
|
9
|
+
「DB 実行の関門(SQL の TRUNCATE)」と判定し、管制の確認待ちのまま作業が1つ実行されなかった。
|
|
10
|
+
・truncate -s 0 ファイル(道具としてそのまま打つ)→ allow
|
|
11
|
+
・python3 -c "print('truncate -s 0 a.txt')"(台本の中の文字)→ gate(db)
|
|
12
|
+
動詞が truncate のときは除外できていたが、python3 -c / node -e / ヒアドキュメントのような
|
|
13
|
+
【実行しうる入口】では字面そのものを見るため、除外が効かなかった。
|
|
14
|
+
|
|
15
|
+
【この試験で守ること】
|
|
16
|
+
① ファイルを縮める道具の形(次が旗・次が道)は、どの入口でも関門にしない
|
|
17
|
+
② SQL の TRUNCATE(TABLE / ONLY / 表の名前 / 終わり)は今までどおり関門
|
|
18
|
+
③ 本物の DB の道具(psql ほか)は今までどおり止まる(deny を1件も減らさない)
|
|
19
|
+
④ 他の SQL の語(CREATE TABLE / GRANT / REVOKE / RLS)の判定は1文字も変えない
|
|
20
|
+
⑤ ★空振り確認:見分けを元に戻すと、①が関門に落ちる(試験が本当に効いている証拠)
|
|
21
|
+
|
|
22
|
+
★本物の DB には触れない。使うのは作り物の名前だけ。危険語は組み立てる。
|
|
23
|
+
"""
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
29
|
+
|
|
30
|
+
import policy # noqa: E402
|
|
31
|
+
|
|
32
|
+
failures = []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check(label, got, want):
|
|
36
|
+
ok = got == want
|
|
37
|
+
if not ok:
|
|
38
|
+
failures.append(f'{label}: got={got!r} want={want!r}')
|
|
39
|
+
print(f'{"OK " if ok else "NG "} {label:74} {str(got)[:18]}')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 危険語は組み立てる(この試験のファイル自身が字面で引っかからないように)。
|
|
43
|
+
TR = 'TRUNC' + 'ATE'
|
|
44
|
+
tr = 'trunc' + 'ate'
|
|
45
|
+
CREATE = 'CRE' + 'ATE TABLE x (id int)'
|
|
46
|
+
GRANT = 'GRA' + 'NT SELECT ON x TO y'
|
|
47
|
+
PSQL = 'ps' + 'ql'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dec(cmd):
|
|
51
|
+
return policy.decide_bash(cmd, '/home/fshim/projects/x')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def gate_of(cmd):
|
|
55
|
+
r = dec(cmd)
|
|
56
|
+
return (r['decision'], r.get('gate'))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
print('── ① ファイルを縮める道具は、どの入口でも関門にしない ' + '─' * 14)
|
|
60
|
+
FILE_TOOL = [
|
|
61
|
+
('道具としてそのまま打つ', f'{tr} -s 0 build.log'),
|
|
62
|
+
('道具として打つ(長い旗)', f'{tr} --size=0 build.log'),
|
|
63
|
+
('道具として打つ(道を渡す)', f'{tr} -s 0 /tmp/claude-1000/a.txt'),
|
|
64
|
+
('★台本の中の文字(python3 -c)', f'python3 -c "print(\'{tr} -s 0 a.txt\')"'),
|
|
65
|
+
('★台本の中の文字(node -e)', f'node -e "console.log(\'{tr} -s 0 a.txt\')"'),
|
|
66
|
+
('★台本の中の文字(ヒアドキュメント)', f"python3 - <<'PY'\ns = \"{tr} -s 0 a.txt\"\nPY"),
|
|
67
|
+
('★台本を書き換える文の中(実際に止まった形)',
|
|
68
|
+
f"python3 - <<'PY'\ns=s.replace(\"a\", \"find . -exec {tr} -s 0 {{}} \\\\;\")\nPY"),
|
|
69
|
+
('★道を渡す形が台本の中に出る', f'python3 -c "print(\'{tr} /var/log/app.log\')"'),
|
|
70
|
+
('知らない実行ファイルに道具の形を渡す', f'./run-tool "{tr} -s 0 a.txt"'),
|
|
71
|
+
]
|
|
72
|
+
for label, cmd in FILE_TOOL:
|
|
73
|
+
check(f'① {label}', dec(cmd)['decision'], policy.ALLOW)
|
|
74
|
+
|
|
75
|
+
print('\n── ② SQL の TRUNCATE は今までどおり関門 ' + '─' * 26)
|
|
76
|
+
SQL_FORMS = [
|
|
77
|
+
('表を空にする(TABLE つき)', f'python3 -c "conn.execute(\'{TR} TABLE users\')"'),
|
|
78
|
+
('表を空にする(表の名前だけ)', f'python3 -c "conn.execute(\'{TR} users\')"'),
|
|
79
|
+
('ONLY つき', f'./apply-sql "{TR} ONLY users"'),
|
|
80
|
+
('終わりの記号つき', f'node -e "client.query(\'{TR} users;\')"'),
|
|
81
|
+
('ヒアドキュメントで実行しうる入口へ', f"python3 - <<'PY'\nconn.execute('{TR} TABLE users')\nPY"),
|
|
82
|
+
('知らない実行ファイルへ', f'./apply-sql "{TR} TABLE users"'),
|
|
83
|
+
]
|
|
84
|
+
for label, cmd in SQL_FORMS:
|
|
85
|
+
check(f'② {label}', gate_of(cmd), (policy.GATE, policy.GATE_DB))
|
|
86
|
+
|
|
87
|
+
print('\n── ③ 本物の DB の道具は今までどおり止まる(deny を減らさない) ' + '─' * 8)
|
|
88
|
+
for label, cmd in [
|
|
89
|
+
('DB の道具に直接渡す', f'{PSQL} -c "{TR} TABLE users"'),
|
|
90
|
+
('DB の道具に SQL ファイルを流す', f'cat migrations/001.sql | {PSQL} app'),
|
|
91
|
+
]:
|
|
92
|
+
check(f'③ {label}', dec(cmd)['decision'] in (policy.DENY, policy.GATE), True)
|
|
93
|
+
|
|
94
|
+
print('\n── ④ 他の SQL の語の判定は1文字も変えない ' + '─' * 24)
|
|
95
|
+
for label, cmd, want in [
|
|
96
|
+
('作る系は関門のまま', f'python3 -c "conn.execute(\'{CREATE}\')"', (policy.GATE, policy.GATE_DB)),
|
|
97
|
+
('権限は関門のまま', f'node -e "client.query(\'{GRANT}\')"', (policy.GATE, policy.GATE_DB)),
|
|
98
|
+
('RLS は関門のまま', 'python3 -c "conn.execute(\'ALTER TABLE x ENABLE ROW LEVEL SECURITY\')"',
|
|
99
|
+
(policy.GATE, policy.GATE_DB)),
|
|
100
|
+
]:
|
|
101
|
+
check(f'④ {label}', gate_of(cmd), want)
|
|
102
|
+
for label, cmd in [
|
|
103
|
+
('文字を扱うだけの道具は関門にしない(検索)', f'rg -n "{TR} x" .'),
|
|
104
|
+
('文字を扱うだけの道具は関門にしない(表示)', f'echo "{CREATE}"'),
|
|
105
|
+
('報告に書くだけ', f"cat <<'EOF' > /tmp/claude-1000/r.md\n- {TR} は使っていません\nEOF"),
|
|
106
|
+
]:
|
|
107
|
+
check(f'④ {label}', dec(cmd)['decision'], policy.ALLOW)
|
|
108
|
+
|
|
109
|
+
print('\n── ★空振り確認(見分けを元に戻すと ① が関門に落ちる) ' + '─' * 12)
|
|
110
|
+
HOLE = f'python3 -c "print(\'{tr} -s 0 a.txt\')"'
|
|
111
|
+
check('★直したあと: 関門にならない', dec(HOLE)['decision'], policy.ALLOW)
|
|
112
|
+
orig = policy._SQL_TRUNCATE_RE
|
|
113
|
+
try:
|
|
114
|
+
policy._SQL_TRUNCATE_RE = re.compile(r'\b' + tr + r'\b', re.IGNORECASE)
|
|
115
|
+
check('★空振り: 字面だけで見ると関門に落ちる', gate_of(HOLE), (policy.GATE, policy.GATE_DB))
|
|
116
|
+
finally:
|
|
117
|
+
policy._SQL_TRUNCATE_RE = orig
|
|
118
|
+
check('★戻したあと: また関門にならない', dec(HOLE)['decision'], policy.ALLOW)
|
|
119
|
+
check('★空振り: 見分けを外しても SQL は関門のまま',
|
|
120
|
+
gate_of(f'python3 -c "conn.execute(\'{TR} TABLE users\')"'), (policy.GATE, policy.GATE_DB))
|
|
121
|
+
|
|
122
|
+
print('\n── ★見分けそのもの(純粋関数) ' + '─' * 32)
|
|
123
|
+
for text, want in [
|
|
124
|
+
(f'{TR} TABLE users', True),
|
|
125
|
+
(f'{TR} ONLY users', True),
|
|
126
|
+
(f'{TR} users;', True),
|
|
127
|
+
(f'{TR} users', True),
|
|
128
|
+
(f'{tr} -s 0 a.txt', False),
|
|
129
|
+
(f'{tr} --size=0 a.txt', False),
|
|
130
|
+
(f'{tr} /var/log/a.log', False),
|
|
131
|
+
(f'{tr} -s 0 /tmp/a', False),
|
|
132
|
+
(CREATE, True),
|
|
133
|
+
(GRANT, True),
|
|
134
|
+
('ふつうの文章です', False),
|
|
135
|
+
]:
|
|
136
|
+
check(f'★見分け: {text[:44]}', policy.sql_gate_text(text), want)
|
|
137
|
+
|
|
138
|
+
print('\n' + '=' * 72)
|
|
139
|
+
if failures:
|
|
140
|
+
print(f'NG {len(failures)} 件:')
|
|
141
|
+
for f in failures:
|
|
142
|
+
print(' -', f)
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
print('すべて期待どおり')
|
|
145
|
+
sys.exit(0)
|