gendesign/scripts/claude-hooks/test-check-secret-read.py
bot-backend 0e4beb8457
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
chore(hooks): точечный allowlist на три ключа метрик вместо снятия гарда
Владельцу понадобились креды 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.
2026-08-26 13:09:37 +03:00

44 lines
1.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 "ЕСТЬ ПРОВАЛЫ")