#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Скан-гейт секретов для выгрузки конфигурации/исходников 1С перед коммитом в git.
Ловит: захардкоженные пароли/логины/токены/ключи, длинные base64/hex-литералы,
и файлы ДАННЫХ (.dt/.1cd/.cf/.epf/...), которых в git быть не должно.
Значения маскируются. Exit != 0, если что-то найдено (для pre-push хука)."""
import os, re, sys

DATA_EXT = {".dt", ".1cd", ".cf", ".cfe", ".epf", ".erf", ".zip", ".rar", ".7z", ".bak"}
SCAN_EXT = {".bsl", ".xml", ".mdo", ".txt", ".json", ".os", ".yaml", ".yml", ".ini", ".config"}

KEYWORD = r"(пароль|password|логин|login|passwd|pwd|токен|token|secret|apikey|api[_-]?key|" \
          r"client[_-]?secret|clientsecret|connectionstring|строкаподключения|" \
          r"authorization|bearer|private[_-]?key)"
# секрет = "литерал"  или  секрет : "литерал"
RE_ASSIGN = re.compile(KEYWORD + r"\s*[:=]\s*[\"']([^\"']{4,})[\"']", re.IGNORECASE)
# агрессивно: присваивание переменной с "кредовым" именем = "литерал"
AGGR_NAME = r"(логин|пользовател|сервер|хост|host|url|адрес|токен|token|ключ|key|хеш|hash|" \
            r"сертификат|thumbprint|отпечаток|инн|порт|port|база|database|catalog|dsn|prov)"
RE_AGGR = re.compile(r"\b[A-Za-zА-Яа-я_][A-Za-zА-Яа-я0-9_]*" + AGGR_NAME +
                     r"[A-Za-zА-Яа-я0-9_]*\s*[:=]\s*[\"']([^\"']{3,})[\"']", re.IGNORECASE)
# строка подключения с usr=/pwd=
RE_CONN   = re.compile(r"\b(usr|uid|user|pwd|password)\s*=\s*[^;\"'\s.]{3,}", re.IGNORECASE)
# длинный base64/hex литерал (ключи/сертификаты)
RE_LONG   = re.compile(r"[\"']([A-Za-z0-9+/]{40,}={0,2})[\"']")
# заголовок функции/процедуры — чтобы отслеживать, в каком геттере мы находимся
RE_FUNC_HDR = re.compile(r"^\s*(Процедура|Функция)\s+([A-Za-zА-Яа-я0-9_]+)\s*\(", re.IGNORECASE)
RE_FUNC_END = re.compile(r"^\s*(КонецПроцедуры|КонецФункции)\b", re.IGNORECASE)
# имя геттера, похожее на секрет (ПолучитьТокен, GetPassword, Пачка_ПолучитьТокен...)
GETTER_NAME = re.compile(r"(токен|token|пароль|password|ключ(?!евое)|secret|apikey|api[_-]?key)", re.IGNORECASE)
# Возврат "литерал" / Return "литерал"
RE_RETURN_LIT = re.compile(r"\b(Возврат|Return)\s+[\"']([^\"']{4,})[\"']", re.IGNORECASE)

def mask(s, keep=2):
    s = str(s)
    return (s[:keep] + "***МАСКА***") if len(s) > keep else "***"

def main(root, aggr=False):
    data_hits, secret_hits, long_hits = [], [], []
    for dp, dn, fn in os.walk(root):
        if "/.git" in dp: continue
        for f in fn:
            ext = os.path.splitext(f)[1].lower()
            full = os.path.join(dp, f)
            rel = os.path.relpath(full, root)
            if ext in DATA_EXT:
                data_hits.append(rel); continue
            if ext not in SCAN_EXT: continue
            if "Picture" in rel or "CommonPictures" in rel: continue  # встроенные PNG/base64 — не секреты
            try:
                with open(full, encoding="utf-8", errors="replace") as fh:
                    for i, line in enumerate(fh, 1):
                        for m in RE_ASSIGN.finditer(line):
                            secret_hits.append((rel, i, m.group(1)))
                        for m in RE_CONN.finditer(line):
                            secret_hits.append((rel, i, m.group(0)))
                        if aggr:
                            for m in RE_AGGR.finditer(line):
                                secret_hits.append((rel, i, m.group(2)))
                        for m in RE_LONG.finditer(line):
                            long_hits.append((rel, i, m.group(1)))
            except Exception as e:
                print(f"  ? не прочитан {rel}: {e}")

    print("== 1) Файлы ДАННЫХ (не должны попадать в git) ==")
    print("\n".join("  ⚠ " + h for h in data_hits) if data_hits else "  чисто")
    print("== 2) Захардкоженные секреты (значения замаскированы) ==")
    if secret_hits:
        for rel, i, v in secret_hits[:60]:
            print(f"  ⚠ {rel}:{i}  →  {mask(v)}")
    else:
        print("  чисто")
    print("== 3) Длинные base64/hex литералы (ключи/сертификаты?) ==")
    if long_hits:
        for rel, i, v in long_hits[:40]:
            print(f"  ⚠ {rel}:{i}  →  {mask(v)}")
    else:
        print("  чисто")

    total = len(data_hits) + len(secret_hits) + len(long_hits)
    print("----")
    if total == 0:
        print("ЧИСТО ✅ — можно коммитить"); return 0
    print(f"НАЙДЕНО ПОДОЗРИТЕЛЬНОГО: {total} ❌ — разобрать до push"); return 1

if __name__ == "__main__":
    args = [a for a in sys.argv[1:] if a != "--aggr"]
    aggr = "--aggr" in sys.argv
    if len(args) != 1:
        print("usage: secret_scan.py [--aggr] <каталог_выгрузки>", file=sys.stderr); sys.exit(2)
    sys.exit(main(args[0], aggr=aggr))
