/
kochenov
/
uni
Обзор
Документация
Войти
/
kochenov
/
uni
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
scripts/validate_pr_title.py
104 строки
4 KB
Dmitry Kochenov
fix(validate_pr_title.py): переписать XSS-фильтрацию через список паттернов (svg, math, canvas, style и др.)
17 июл 2026, 01:30
17 июл 2026, 01:30
18f5ffe
Код
Авторство
О чём код?
#!/usr/bin/env python3 """ Валидатор заголовка PR (Conventional Commits v2). Запуск: uv run python scripts/validate_pr_title.py Exit 0 = VALID, Exit 1 = INVALID. """ import os import re import sys BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SCOPES = [ 'kernel', 'platform', 'infra', 'sdk', 'auth', 'users', 'frontend', 'growth', 'scaling', 'enterprise', 'billing', 'business', 'sre', 'final', 'deps', 'step' ] # NOTE: 'docs' недопустим как scope, но фигурирует в TYPES — для обратной совместимости (см. regex) TYPES = [ 'feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'ci', 'revert', 'perf', 'security', 'build' ] def validate_pr_title(title: str) -> tuple[bool, list[str]]: """Валидирует заголовок PR по правилам Conventional Commits v2 для UMP.""" errors = [] if not title: errors.append("Заголовок PR пустой") return False, errors if len(title) > 72: errors.append(f'Заголовок слишком длинный ({len(title)} символов, максимум 72)') return False, errors parts = title.split(':', 1) if len(parts) != 2: errors.append('Неверный формат: заголовок должен быть "type(scope): subject"') return False, errors prefix, subject = parts prefix = prefix.strip() subject = subject.strip() if not subject or subject.isspace(): errors.append('Subject пустой или содержит только пробелы') return False, errors if not subject[0].isupper(): errors.append(f'Subject должен начинаться с заглавной буквы (русской или английской): "{subject[0]}"') return False, errors if subject.endswith('.'): errors.append('Subject не должен заканчиваться на точку') return False, errors dangerous_patterns = [ r'<\s*(script|iframe|object|embed|applet|svg|math|canvas|body|html|link|meta)[^>]*>', r'\b(javascript|data|vbscript|livescript)\s*:', r'on\w+\s*=', r'\beval\s*(', r'document\s*\.\s*write\s*(', r'\bexpression\s*(', r'url\s*\(\s*["\']?\s*(javascript|data):', r'<img[^>]*\bsrc\s*=\s*["\']?\s*data:', r'\bstyle\s*=\s*["\']?\s*[^"\']*\b(expression|url)\s*(' ] if any(re.search(p, subject, re.IGNORECASE) for p in dangerous_patterns): errors.append('Subject содержит потенциально опасные HTML/JS/CSS-конструкции (XSS)') return False, errors if re.search(r'[^a-zA-Zа-яА-ЯёЁ0-9\s-:"\'()\[\]!?,.]', subject) or re.search(r'[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]', subject): errors.append('Subject содержит недопустимые символы') return False, errors prefix_pattern = fr'^({"|".join(TYPES)})\(({"|".join(SCOPES)})\)$' prefix_match = re.match(prefix_pattern, prefix) if not prefix_match: errors.append(f'Неверный префикс "{prefix}". Ожидается: type(scope) где type ∈ {TYPES}, scope ∈ {SCOPES}') return False, errors return True, errors if __name__ == '__main__': title = os.environ.get('PR_TITLE', '') valid, errors = validate_pr_title(title) if valid: print(f'✅ Заголовок PR валиден: {title}') sys.exit(0) else: print(f'❌ Заголовок PR НЕВАЛИДЕН:') for err in errors: print(f' {err}') print() print('Примеры валидных заголовков:') print(' fix(step): исправить ошибку валидации в workflow') print(' feat(kernel): добавить DI-контейнер на основе dishka') print(' docs(README): обновить раздел установки') sys.exit(1)