chore(hooks): точечный allowlist на три ключа метрик вместо снятия гарда #3107
2 changed files with 83 additions and 0 deletions
|
|
@ -37,6 +37,42 @@ SECRET_RE = re.compile(
|
|||
)
|
||||
TEMPLATE_SUFFIXES = (".example", ".sample", ".template", ".dist", ".md")
|
||||
|
||||
# --- Точечное исключение (по прямому распоряжению владельца, 2026-08-26) ---------
|
||||
#
|
||||
# Гард целиком НЕ снят: в тех же .env-файлах лежат prod DB-пароли и токены
|
||||
# Forgejo/GlitchTip, и они должны оставаться закрытыми. Разрешено ровно одно —
|
||||
# вытащить значение перечисленных ниже ключей anchored-грепом.
|
||||
#
|
||||
# Условия исключения намеренно жёсткие, чтобы «прочитать один ключ» нельзя было
|
||||
# развернуть в «выгрузить файл»:
|
||||
# - паттерн привязан к началу строки (^KEY=) и ключ входит в ALLOWED_KEYS;
|
||||
# - запрещена инверсия (-v / --invert-match) — иначе выдаст ВСЁ, кроме ключа;
|
||||
# - запрещены пайпы, цепочки, подстановки и редиректы — иначе рядом с
|
||||
# разрешённым грепом можно провезти второй, произвольный, ридер.
|
||||
ALLOWED_KEYS = (
|
||||
"METRICS_UI_PASSWORD",
|
||||
"GRAFANA_ADMIN_USER",
|
||||
"GRAFANA_ADMIN_PASSWORD",
|
||||
)
|
||||
|
||||
_ALLOWED_GREP_RE = re.compile(
|
||||
r"(?i)(?<![\w-])grep\s+(?:-[a-z]+\s+)*['\"]?\^(?:" + "|".join(ALLOWED_KEYS) + r")="
|
||||
)
|
||||
_INVERT_RE = re.compile(r"(?i)(?<![\w-])(?:-v|--invert-match)(?![\w-])")
|
||||
_CHAIN_RE = re.compile(r"[;|&`]|\$\(|>")
|
||||
|
||||
|
||||
def _is_allowed_key_read(cmd: str) -> bool:
|
||||
"""True только для anchored-грепа одного из ALLOWED_KEYS без способов расширить выдачу."""
|
||||
if not _ALLOWED_GREP_RE.search(cmd):
|
||||
return False
|
||||
if _INVERT_RE.search(cmd):
|
||||
return False
|
||||
if _CHAIN_RE.search(cmd):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# Verbs that read a file's content or push it off-box.
|
||||
READ_VERB_RE = re.compile(
|
||||
r"(?i)(?<![\w-])("
|
||||
|
|
@ -72,6 +108,9 @@ def main() -> int:
|
|||
if not hits:
|
||||
return 0
|
||||
|
||||
if _is_allowed_key_read(cmd):
|
||||
return 0
|
||||
|
||||
danger = (
|
||||
READ_VERB_RE.search(cmd)
|
||||
or re.search(r"<\s*\S*\.env\b", cmd, re.IGNORECASE)
|
||||
|
|
|
|||
44
scripts/claude-hooks/test-check-secret-read.py
Normal file
44
scripts/claude-hooks/test-check-secret-read.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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 "ЕСТЬ ПРОВАЛЫ")
|
||||
Loading…
Add table
Reference in a new issue