Пишем свои правила для semgrep
Ранее я уже писал пост про кастомные правила в Semgrep, но вчера благополучно случайно его удалил🥲
А это значит только одно: пора написать новую и уже более сильную статью по этой теме)
Для меня Semgrep по-настоящему раскрывается только в тот момент, когда ты начинаешь писать свои кастомные правила.
Потому что именно здесь инструмент перестаёт быть просто “SAST из коробки” и начинает адаптироваться под логику твоего проекта, твою модель угроз и твои требования к secure coding.
━━━━━━━━━━ Почему кастомные правила важны ━━━━━━━━━━
Готовые правила хороши как базовый уровень.
Они неплохо ловят общие проблемы, типовые паттерны уяз и стандартные anti-patterns. Но почти всегда у них есть предел: они не знают контекст именно вашего приложения.
А в реальных проектах решают как раз детали: • какие вызовы у вас действительно считаются опасными • в каком контексте поведение уже становится рискованным • какие конструкции нужно исключать, чтобы не собирать шум • какое remediation message увидит разработчик • поймёт ли он вообще, что от него хотят исправить
По сути кастомное правило - это уже не просто поиск. Это описание того, что именно ты считаешь важным для безопасности своего кода.
━━━━━━━━━━ С чего обычно начинается своё правило ━━━━━━━━━━
Почти всегда основа начинается с pattern.
Это главный кирпич правила: ты показываешь Semgrep, какую именно конструкцию кода нужно считать совпадением.
Самый простой пример: • pattern: eval(…)
Что здесь важно понять сразу: • pattern ищет не просто строку, а форму конструкции • … означает “здесь может быть что угодно” в допустимом контексте • в случае eval(…) ты говоришь: найди вызов eval, а аргументы уже могут быть разными
Следующий очень полезный уровень - metavariables.
Это шаблонные переменные вида: • $X • $ARG • $FUNC • $OBJ
Они нужны, когда ты хочешь не просто что-то найти, а связать между собой части совпадения: одно и то же имя, конкретный аргумент, объект или вызов.
━━━━━━━━━━ Как строится логика правила ━━━━━━━━━━
Когда правило становится чуть умнее, дальше обычно используются такие операторы:
➤ patterns Это логическое “И”. Нужно, когда должны совпасть сразу несколько условий.
➤ pattern-either Это логическое “ИЛИ”. Удобно, когда опасная конструкция встречается в нескольких формах.
➤ pattern-not Это отсечение шума. Сначала находишь общий случай, потом вырезаешь безопасный вариант.
➤ pattern-inside Полезно, когда конструкция опасна только внутри конкретного контекста: функции, блока, обработчика, middleware и т.д.
➤ pattern-not-inside Одна из самых полезных вещей против false positive. Помогает исключать сценарии, где нужный паттерн уже обёрнут или обработан безопасно.
Если правило становится сложнее, дальше уже подключается metavariable-regex.
Но здесь мой совет простой: сначала мыслить через pattern, а regex использовать только как дофильтрацию, а не как фундамент всего правила.
━━━━━━━━━━ Что ещё важно знать на старте ━━━━━━━━━━
Условно я бы делил правила на 2 уровня: • search rules - когда ты ищешь конкретные конструкции • taint rules - когда нужно отслеживать поток данных от source до sink
И вот taint rules - это уже следующий сильный уровень, потому что там начинается не просто поиск конструкции, а работа с dataflow.
Ещё отдельный полезный момент - generic mode.
Он нужен, когда ты работаешь не с классическим поддерживаемым языком, а с чем-то более текстовым или смешанным: • XML • конфиги • нестандартные файлы • шаблоны со смешанным содержимым
И ещё одна вещь, которую многие зря пропускают, - тестирование правил.
Если правило не тестируется, то со временем оно почти гарантированно начнёт шуметь или что-то пропускать.
И для меня кастомные правила Semgrep - это тот самый момент, когда SAST перестаёт быть просто “чем-то из коробки” и начинает реально подстраиваться под безопасность твоего проекта.