Статический анализ безопасности кода (Static Application Security Testing, SAST) — это не просто очередная галочка в чек-листе DevOps. Это реально спасает жопу, когда в production уходит код с SQL-injection или десериализацией враждебного объекта.
Я не один раз видел, как команда обнаруживала критическую уязвимость только после того, как её уже эксплуатировали. Помню один проект в стартапе — писали на Python, пользовались pickle для кеша без валидации. Угадайте, что произошло на боевом сервере? Поэтому когда я слышу "сначала напишем, потом проверим", я уже знаю, чем это закончится.
Давайте разберёмся, как SAST работает для Python, какие инструменты реально помогают, и как встроить проверки в CI/CD без бюрократии.
Что вообще такое SAST и почему Python нужен особый подход
SAST анализирует исходный код без его запуска, ищет паттерны, которые пахнут уязвимостью. Для Python это сложнее, чем для Java или Go, потому что:
Язык динамический. Тип переменной может измениться в runtime. Статический анализатор не всегда понимает, что вы туда подадите.
Много магии через рефлексию и eval-подобные конструкции. exec(), eval(), __import__() — всё это может быть законным, но может быть и дырой.
Экосистема огромная, но не все библиотеки поддерживаются анализаторами. Правила для одной версии pandas могут не сработать для другой.
Но это не значит, что нужно сдаваться. Просто нужны правильные инструменты и понимание того, что ловить.
Основные типы уязвимостей, которые SAST ловит в Python
Давайте на конкретных примерах. Вот самые опасные паттерны:
SQL-injection
# Плохо — классика
user_id = request.args.get('id')
query = f"SELECT * FROM users WHERE id = {user_id}"
db.execute(query)
SAST инструменты сразу видят f-string с переменной в SQL-запросе. Красный флаг.
# Хорошо
user_id = request.args.get('id')
query = "SELECT * FROM users WHERE id = ?"
db.execute(query, (user_id,))
Десериализация ненадежных данных
# Опасно
import pickle
data = request.get_data()
obj = pickle.loads(data) # Враг может подсунуть вредоносный объект
Pickle может выполнить произвольный код при десериализации. SAST кричит. Используйте JSON или другие безопасные форматы.
# Лучше
import json
data = request.get_data()
obj = json.loads(data)
Использование eval и exec
# Убийственно плохо
user_input = request.args.get('expression')
result = eval(user_input)
Это вообще не нужно обсуждать. SAST будет ругаться на всё, что пахнет eval с внешними данными.
Жёсткие пароли и секреты в коде
# Попадётся в git
API_KEY = "sk-12345abcde"
DATABASE_PASSWORD = "admin123"
SAST инструменты ищут паттерны типа password =, api_key =, secret =. Если они содержат хардкодированные значения — флаг.
Использование небезопасных хешей
# Плохо для паролей
import hashlib
hashed = hashlib.md5(password.encode()).hexdigest()
MD5 и SHA1 для паролей — это преступление. Нужны bcrypt, argon2, scrypt.
# Правильно
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
Команды через shell без санитизации
# Опасно
import os
filename = request.args.get('file')
os.system(f"cat {filename}") # Command injection
Враг просто передаст filename = "; rm -rf /" и ваша система пострадает.
# Безопасно
import subprocess
filename = request.args.get('file')
result = subprocess.run(['cat', filename], capture_output=True)
Инструменты SAST для Python: что реально работает
Bandit — король для Python-проектов
Это де-факто стандарт в Python-комьюнити. Специализируется именно на security-проблемах.
pip install bandit
bandit -r ./app
Выдаёт примерно такое:
>> Issue: [B602:shell_injection] Possible shell injection via Popen with shell=True
Severity: HIGH Confidence: MEDIUM
Location: ./app/utils.py:12
Bandit знает про все уязвимости, что я выше перечислил. Плюс про рекурсивные вызовы функций с опасными параметрами.
Pylint с плагинами
Pylint — это больше про качество кода, но с плагинами вроде pylint-django может ловить и security-проблемы.
pip install pylint
pylint ./app
Но честно? Для security Bandit лучше. Pylint более универсальный.
Semgrep — мощный зверь для pattern-matching
Semgrep ищет уязвимости по регулярным выражениям и паттернам. Очень гибкий, можно написать свои правила.
pip install semgrep
semgrep --config=p/security-audit ./app
Это уже посерьёзнее, но и медленнее. Bandit быстрее работает.
Snyk — облачный SaaS с интеграцией в CI/CD
Хороший облачный сервис, но данные уходят в облако. Для российских компаний это может быть проблемой.
Pylint + Flake8 + mypy комбо
Если у вас нет выделенного security-инструмента, можно комбинировать:
- mypy для типизации (ловит часть проблем)
- Flake8 для стиля и простых проблем
- Bandit конкретно для security
Встраиваем SAST в CI/CD pipeline
Вот реальный пример для GitHub Actions:
name: Security Check
on: [push, pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install bandit semgrep
- name: Run Bandit
run: bandit -r ./app -f json -o bandit-report.json
continue-on-error: true
- name: Run Semgrep
run: semgrep --config=p/security-audit ./app
continue-on-error: true
- name: Upload reports
uses: actions/upload-artifact@v3
with:
name: security-reports
path: bandit-report.json
Для GitLab CI:
stages:
- security
sast:
stage: security
image: python:3.10
script:
- pip install bandit
- bandit -r ./app -f json -o bandit-report.json
artifacts:
reports:
sast: bandit-report.json
paths:
- bandit-report.json
allow_failure: true
Ключевой момент: allow_failure: true на начальном этапе. Иначе все существующие проблемы будут блокировать пайплайн. Лучше внедрять постепенно.
Настройка Bandit под реальный проект
Вот конфиг .bandit для игнорирования ложных срабатываний:
# .bandit
exclude_dirs:
- '/tests/'
- '/venv/'
- '/.venv/'
tests:
- B201 # flask_debug_true
- B301 # pickle
- B302 # marshal
- B303 # md5
- B304 # des
- B305 # cipher
- B306 # mktemp_q
- B307 # eval
- B308 # mark_safe
- B309 # httpsconnection
- B310 # url_open
- B311 # random
- B312 # telnetlib
- B313 # xml_bad_etree
- B314 # xml_bad_expat
- B315 # xml_bad_sax
- B316 # xml_bad_pulldom
- B317 # xml_bad_etree
- B318 # xml_bad_etree
- B319 # xml_bad_etree
- B320 # xml_bad_etree
- B321 # ftplib
- B322 # unverified_context
- B323 # unverified_context
- B324 # hashlib
- B325 # tempnam
skips:
- B101 # assert_used (много ложных в тестах)
Запуск с этим конфигом:
bandit -r ./app --ini .bandit
SAST vs DAST: когда нужно что
SAST анализирует код в статике — быстро, но может быть много ложных срабатываний.
DAST (Dynamic Application Security Testing) запускает приложение и пытается его взломать — медленнее, но реальнее.
Правильный подход: оба. SAST в каждый коммит, DAST перед production.
Для DAST в Python можно использовать:
- OWASP ZAP — открытый, мощный
- Burp Suite Community — хороший, но платная версия рекомендуется
- SQLMap — специально для SQL-injection
Типичные ошибки при внедрении SAST
Включили всё сразу и заблокировали все MR. Результат: команда отключает проверки и ненавидит security.
Правильно: начните с высокого severity, потом добавляйте medium и low.
Игнорируют все срабатывания подряд. Это привыкание к noise.
Правильно: разберитесь с каждым срабатыванием, закройте issue или задокументируйте, почему это ложное срабатывание.
Не обновляют правила. Новые уязвимости появляются постоянно.
Правильно: обновляйте инструменты раз в месяц минимум.
Как Distiq помогает с SAST
Если вы уже используете Bandit или другие инструменты, но хотите, чтобы разработчики сразу видели проблемы в PR — это медленно и неудобно. Distiq автоматически анализирует каждый merge request и оставляет инлайн-комментарии с замечаниями. Не нужно ждать завершения пайплайна и копаться в артефактах. Бот сразу укажет на уязвимость прямо в коде — на строчке, где она есть.
Это работает для Python, JavaScript, Java и других языков. Интегрируется за две минуты с GitLab, GitHub или GitVerse. Серверы в России, данные не уходят за рубеж — что важно для compliance.
Берите и внедряйте SAST правильно.
