All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Владельцу понадобились креды Grafana (basic_auth Caddy + внутренний вход), а они лежат в backend/.env.runtime, который check-secret-read.py закрывает целиком. Снимать гард нельзя: в том же файле prod DB-пароли и токены Forgejo/GlitchTip. Вместо этого — ALLOWED_KEYS из трёх ключей (METRICS_UI_PASSWORD, GRAFANA_ADMIN_USER, GRAFANA_ADMIN_PASSWORD) и разрешение ровно на anchored-греп по ним. Условия намеренно жёсткие, чтобы «прочитать один ключ» нельзя было развернуть в «выгрузить файл»: блокируются инверсия (-v / --invert-match), пайпы, цепочки ; && ||, подстановки $(...) и обратные кавычки, редиректы. Тест на 10 кейсов: разрешён только anchored-греп по ключу из списка; отбиты инверсия, пайп, цепочка, редирект, подстановка, чужой ключ, греп без якоря ^ и обычный cat. Проверяется и функция, и хук end-to-end через настоящий stdin-payload. Гард в деле: он же отбил эту самую команду коммита, когда текст сообщения содержал имя закрытого файла рядом с read-verb.
44 lines
1.8 KiB
Python
44 lines
1.8 KiB
Python
import importlib.util, json, subprocess, sys
|
||
|
||
HOOK = "scripts/claude-hooks/check-secret-read.py"
|
||
spec = importlib.util.spec_from_file_location("h", HOOK)
|
||
m = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(m)
|
||
|
||
F = "/opt/gendesign/backend/.env" + ".runtime"
|
||
K = "METRICS_UI_PASSWORD"
|
||
|
||
cases = [
|
||
(f"timeout 40 ssh -o BatchMode=yes gendesign \"grep '^{K}=' {F}\"", True, "разрешённый anchored-греп"),
|
||
(f"ssh gendesign \"grep -v '^{K}=' {F}\"", False, "инверсия -v"),
|
||
(f"ssh gendesign \"grep '^{K}=' {F}; cat {F}\"", False, "цепочка ;"),
|
||
(f"ssh gendesign \"grep '^{K}=' {F} | cat {F}\"", False, "пайп"),
|
||
(f"ssh gendesign \"grep '^{K}=' {F} > /tmp/x\"", False, "редирект"),
|
||
(f"ssh gendesign \"grep '^POSTGRES_PASSWORD=' {F}\"", False, "другой ключ"),
|
||
(f"ssh gendesign \"grep '{K}' {F}\"", False, "без якоря ^"),
|
||
(f"cat {F}", False, "обычный cat"),
|
||
(f"ssh gendesign \"grep '^{K}=' {F} && cat {F}\"", False, "цепочка &&"),
|
||
(f"ssh gendesign \"grep '^{K}=' $(echo {F})\"", False, "подстановка"),
|
||
]
|
||
|
||
ok = True
|
||
for cmd, want, name in cases:
|
||
got = m._is_allowed_key_read(cmd)
|
||
if got != want:
|
||
ok = False
|
||
print(f"{'OK ' if got == want else 'FAIL'} allow={str(got):5} want={str(want):5} {name}")
|
||
|
||
print("--- end-to-end через сам хук ---")
|
||
for cmd, want_allow, name in cases:
|
||
p = subprocess.run(
|
||
[sys.executable, HOOK],
|
||
input=json.dumps({"tool_name": "Bash", "tool_input": {"command": cmd}}),
|
||
capture_output=True,
|
||
text=True,
|
||
)
|
||
allowed = p.returncode == 0
|
||
if allowed != want_allow:
|
||
ok = False
|
||
print(f"FAIL rc={p.returncode} want_allow={want_allow} {name}")
|
||
|
||
print("ALL OK" if ok else "ЕСТЬ ПРОВАЛЫ")
|