/
ievlev
/
TOP-AI
Обзор
Документация
Войти
/
ievlev
/
TOP-AI
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
scripts/scan-pdf-secrets.py
128 строк
5 KB
KIevlev
security: удалить материалы с реквизитами доступа и переписать задания в md
27 июл 2026, 21:01
27 июл 2026, 21:01
97d43e8
Код
Авторство
О чём код?
#!/usr/bin/env python3 """Поиск инфраструктурных секретов внутри PDF-вложений репозитория. Текстовые слои PDF, собранных из LaTeX/офисных редакторов, хранятся в сжатых потоках (FlateDecode), поэтому обычный `grep`/`strings` их не видит. Скрипт распаковывает потоки и ищет IP-адреса, внутренние FQDN, реквизиты доступа и ключи объектного хранилища. Использование: python3 scripts/scan-pdf-secrets.py [путь ...] Без аргументов проверяются все *.pdf в репозитории. Код возврата 1, если найдено хотя бы одно совпадение. """ from __future__ import annotations import re import sys import zlib from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent # Адреса, которые допустимы в учебных материалах: они не раскрывают # внутреннюю инфраструктуру и не являются секретами. ALLOWED_HOSTS = { "0.0.0.0", "127.0.0.1", "255.255.255.0", "192.168.1.100", # пример из раздела про /etc/hosts "10.0.0.200", # пример из раздела про /etc/hosts } PATTERNS: list[tuple[str, re.Pattern[bytes]]] = [ ("ipv4", re.compile(rb"(?<![\d.])((?:\d{1,3}\.){3}\d{1,3})(?![\d.])")), ("internal-fqdn", re.compile(rb"[A-Za-z0-9_-]+\.(?:cloud\.ru|mtuci\.[a-z.]+|stud\.[a-z.]+|local)\b")), ("node-hostname", re.compile(rb"\b(?:node|sdp|sber-node)\d*\.[A-Za-z0-9.-]+")), ("credential-keyword", re.compile(rb"(?i)\b(?:password|passwd|login)\s*[:=]\s*\S+")), ("credential-pair", re.compile(rb"\b(?:guest|admin|monitor|root)\s*/\s*\S{3,}")), ("weak-password-literal", re.compile(rb"QWE\d{3}")), ("kerberos-keytab", re.compile(rb"\b\S+\.keytab\b")), ] # Признак того, что распакованный поток содержит текстовый слой, а не # изображение или шрифт: без этой проверки склейка литералов на больших # PDF с картинками расходует минуты и даёт мусорные совпадения. TEXT_OPERATORS = re.compile(rb"\bT[jJdmf]\b|\bBT\b") TEXT_LITERAL = re.compile(rb"\(((?:[^()\\]|\\.){0,4096}?)\)", re.DOTALL) MAX_TEXT_STREAM = 4 * 1024 * 1024 def iter_streams(raw: bytes): """Вернуть содержимое потоков PDF: распакованное, если это возможно.""" for match in re.finditer(rb"stream\r?\n", raw): start = match.end() end = raw.find(b"endstream", start) if end == -1: continue chunk = raw[start:end] try: yield zlib.decompress(chunk) except zlib.error: yield chunk def looks_like_version(value: str) -> bool: """Отличить версию компонента (`3.1.4.0`) от адреса хоста.""" octets = value.split(".") return all(len(o) == 1 for o in octets) or ( int(octets[0]) < 16 and all(int(o) < 16 for o in octets) ) def scan(path: Path) -> list[tuple[str, str]]: raw = path.read_bytes() findings: set[tuple[str, str]] = set() for payload in iter_streams(raw): haystacks = [payload] # Операторы вывода текста разрывают строки на части; склеиваем # содержимое текстовых литералов, чтобы найти разорванные адреса. if len(payload) <= MAX_TEXT_STREAM and TEXT_OPERATORS.search(payload): joined = b"".join(TEXT_LITERAL.findall(payload)) if joined: haystacks.append(joined) for haystack in haystacks: for name, pattern in PATTERNS: for hit in pattern.findall(haystack): value = hit.decode("latin-1", "replace").strip() if name == "ipv4": octets = value.split(".") if any(int(o) > 255 for o in octets if o.isdigit()): continue if value in ALLOWED_HOSTS or looks_like_version(value): continue findings.add((name, value)) return sorted(findings) def main(argv: list[str]) -> int: targets = [Path(a) for a in argv] or sorted(REPO_ROOT.rglob("*.pdf")) total = 0 for path in targets: if not path.is_file(): print(f"пропуск (нет файла): {path}", file=sys.stderr) continue findings = scan(path) resolved = path.resolve() try: rel = resolved.relative_to(REPO_ROOT) except ValueError: rel = path if findings: total += len(findings) print(f"\n[!] {rel}") for name, value in findings: print(f" {name:24} {value}") else: print(f"[ok] {rel}") if total: print(f"\nНайдено совпадений: {total}. Требуется проверка и очистка.") return 1 print("\nСовпадений не найдено.") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))