diff --git a/.github/workflows/dast.yml b/.github/workflows/dast.yml new file mode 100644 index 0000000000..1e10d2e04a --- /dev/null +++ b/.github/workflows/dast.yml @@ -0,0 +1,127 @@ +name: dast + +# Динамический анализ: поднимаем DefectDojo через его штатный docker-compose +# и сканируем OWASP ZAP (baseline + full). Тяжёлый full-scan — по расписанию и вручную. + +on: + workflow_dispatch: + schedule: + - cron: "0 2 * * 1" # еженедельно, пн 02:00 UTC + push: + branches: [ main, master, devsecops-pipeline ] + +permissions: + contents: read + issues: write + security-events: write + +env: + TARGET_URL: http://localhost:8080 + REPORTS_DIR: reports + +jobs: + dast: + name: OWASP ZAP + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - name: Build & start DefectDojo + run: | + mkdir -p "$REPORTS_DIR" + # DefectDojo требует профиль БД; предпочитаем штатные helper-скрипты + if [ -x ./dc-build.sh ] && [ -x ./dc-up-d.sh ]; then + ./dc-build.sh + ./dc-up-d.sh postgres-redis + else + docker compose build + docker compose --profile postgres-redis up -d + fi + echo "Ожидаем инициализацию (миграции, статик, суперюзер)..." + + - name: Wait for service + run: | + for i in $(seq 1 60); do + if curl -skf "$TARGET_URL/login" >/dev/null; then + echo "DefectDojo поднят"; exit 0 + fi + echo "ждём... ($i/60)"; sleep 15 + done + echo "Сервис не поднялся вовремя"; docker compose logs --tail=100; exit 1 + + # --- ZAP baseline (пассивный, быстрый) --- + - name: ZAP Baseline Scan + uses: zaproxy/action-baseline@v0.12.0 + continue-on-error: true + with: + target: ${{ env.TARGET_URL }} + rules_file_name: .zap/rules.tsv + cmd_options: "-J zap-baseline.json -w zap-baseline.md" + allow_issue_writing: true + + # --- ZAP full scan (активный: инъекции, обход и т.п.) --- + - name: ZAP Full Scan + uses: zaproxy/action-full-scan@v0.12.0 + continue-on-error: true + with: + target: ${{ env.TARGET_URL }} + rules_file_name: .zap/rules.tsv + cmd_options: "-J zap-full.json" + allow_issue_writing: false + + # --- ZAP АУТЕНТИФИЦИРОВАННЫЙ скан (browser-based, обходит CSRF Django) --- + - name: ZAP Authenticated Scan + continue-on-error: true + env: + ZAP_USER: ${{ secrets.ZAP_USER }} + ZAP_PASSWORD: ${{ secrets.ZAP_PASSWORD }} + run: | + mkdir -p zap_wrk && chmod 777 zap_wrk + cp .zap/zap-auth.yaml zap_wrk/ + docker run --rm --network host \ + -e ZAP_USER -e ZAP_PASSWORD \ + -v "$PWD/zap_wrk:/zap/wrk:rw" \ + ghcr.io/zaproxy/zaproxy:stable \ + zap.sh -cmd -autorun /zap/wrk/zap-auth.yaml || true + + - name: Collect ZAP reports + if: always() + run: | + cp -v zap-baseline.json "$REPORTS_DIR/" 2>/dev/null || true + cp -v zap-full.json "$REPORTS_DIR/" 2>/dev/null || true + # аутентифицированные отчёты: SARIF для гейта + traditional-json для DefectDojo + cp -v zap_wrk/zap-auth.sarif "$REPORTS_DIR/" 2>/dev/null || true + cp -v zap_wrk/zap-auth.json "$REPORTS_DIR/zap-auth.sarif" 2>/dev/null || true + cp -v zap_wrk/zap-auth-dojo.json "$REPORTS_DIR/" 2>/dev/null || true + + - name: Upload ZAP results to DefectDojo + if: always() + continue-on-error: true + env: + DOJO_URL: ${{ secrets.DOJO_URL }} + DOJO_TOKEN: ${{ secrets.DOJO_TOKEN }} + DOJO_PRODUCT: "DefectDojo (self)" + DOJO_ENGAGEMENT: "DAST ${{ github.ref_name }}" + run: | + python3 -m pip install requests + python3 security/dojo_upload.py --stage dast + + - name: Security Gateway (DAST) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GATE_FAIL_ON: CRITICAL # DAST шумит — блокируем только на critical + run: | + python3 security/gate.py --reports "$REPORTS_DIR" + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: dast-reports + path: | + ${{ env.REPORTS_DIR }} + zap-*.md + + - name: Tear down + if: always() + run: docker compose down -v diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000000..31fd88a6cf --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,148 @@ +name: security + +# SAST + SCA + secrets + IaC/image checks + Security Gateway. +# Все отчёты приводятся к SARIF, публикуются во вкладку Security, +# выгружаются в DefectDojo и проходят через gate.py. +# +# ВНИМАНИЕ: версии сторонних actions приведены как ориентир. Перед сдачей +# зафиксируйте их (лучше — к commit SHA) и проверьте актуальность. + +on: + push: + branches: [ main, master ] + pull_request: + workflow_dispatch: + +permissions: + contents: read + pull-requests: write # для комментариев gate.py в PR + security-events: write # для загрузки SARIF во вкладку Security + +env: + REPORTS_DIR: reports + +jobs: + sast-sca: + name: SAST / SCA / IaC + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # нужна полная история для Gitleaks + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Prepare reports dir + run: mkdir -p "$REPORTS_DIR" + + # --- Установка сканеров --- + - name: Install scanners + run: | + python -m pip install --upgrade pip + pip install semgrep bandit bandit-sarif-formatter njsscan checkov requests + curl -sSL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + trivy --version + + # --- SAST: Semgrep (мультиязычный) --- + - name: Semgrep + continue-on-error: true + run: | + semgrep scan \ + --config p/default --config p/security-audit \ + --config p/python --config p/javascript \ + --sarif --output "$REPORTS_DIR/semgrep.sarif" || true + + # --- SAST: Bandit (Python) --- + - name: Bandit + continue-on-error: true + run: | + bandit -r . -f sarif -o "$REPORTS_DIR/bandit.sarif" \ + -x ./tests,./unittests || true + + # --- SAST: njsscan (JavaScript) --- + - name: njsscan + continue-on-error: true + run: | + njsscan --sarif -o "$REPORTS_DIR/njsscan.sarif" . || true + + # --- SCA: Trivy filesystem (зависимости pip/npm) --- + - name: Trivy filesystem (SCA) + continue-on-error: true + run: | + trivy fs --scanners vuln --severity MEDIUM,HIGH,CRITICAL --format sarif --output "$REPORTS_DIR/trivy-fs.sarif" . || true + + # --- IaC / Dockerfile misconfig: Trivy config --- + - name: Trivy config (IaC) + continue-on-error: true + run: | + trivy config --format sarif --output "$REPORTS_DIR/trivy-config.sarif" . || true + + # --- IaC: Checkov (docker-compose / Dockerfile) --- + - name: Checkov + continue-on-error: true + run: | + checkov -d . --framework dockerfile,docker_compose \ + -o sarif --output-file-path "$REPORTS_DIR" || true + # checkov кладёт results.sarif в каталог — переименуем + [ -f "$REPORTS_DIR/results.sarif" ] && mv "$REPORTS_DIR/results.sarif" "$REPORTS_DIR/checkov.sarif" || true + + # --- Dockerfile lint: Hadolint по ВСЕМ Dockerfile форка --- + - name: Hadolint (all Dockerfiles) + continue-on-error: true + run: | + # (зона роста: пиннинговать версию вместо latest) + curl -sSL -o /tmp/hadolint \ + https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 + chmod +x /tmp/hadolint + files=$(git ls-files | grep -Ei '(^|/)Dockerfile' | sort -u || true) + echo "Найдены Dockerfile: $files" + if [ -n "$files" ]; then + /tmp/hadolint -f sarif $files > "$REPORTS_DIR/hadolint.sarif" || true + fi + + # --- Секреты: Gitleaks --- + - name: Gitleaks + uses: gitleaks/gitleaks-action@v2 + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "false" + with: + # SARIF окажется в results.sarif — скопируем ниже + args: detect --report-format sarif --report-path ${{ env.REPORTS_DIR }}/gitleaks.sarif --redact + + # --- Публикация во вкладку Security (GitHub code scanning) --- + - name: Upload SARIF to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + continue-on-error: true + with: + sarif_file: ${{ env.REPORTS_DIR }} + + # --- Выгрузка в DefectDojo --- + - name: Upload results to DefectDojo + continue-on-error: true + env: + DOJO_URL: ${{ secrets.DOJO_URL }} + DOJO_TOKEN: ${{ secrets.DOJO_TOKEN }} + DOJO_PRODUCT: "DefectDojo (self)" + DOJO_ENGAGEMENT: "CI ${{ github.ref_name }}" + run: | + python security/dojo_upload.py --stage sast + + # --- Сохранить отчёты как артефакт --- + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: sast-reports + path: ${{ env.REPORTS_DIR }} + + # --- SECURITY GATEWAY --- + - name: Security Gateway + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GATE_FAIL_ON: HIGH # Critical/High роняют пайплайн + run: | + python security/gate.py --reports "$REPORTS_DIR" diff --git a/.zap/rules.tsv b/.zap/rules.tsv new file mode 100644 index 0000000000..206e1739a1 --- /dev/null +++ b/.zap/rules.tsv @@ -0,0 +1,7 @@ +# ZAP scan rules — управление уровнем алертов (FAIL / WARN / IGNORE) +# Формат: \t\t +# Полный список ID: https://www.zaproxy.org/docs/alerts/ +# Ниже — пример подавления типичного шума на dev-инстансе. +10015 WARN (Incomplete or No Cache-control Header Set) +10096 IGNORE (Timestamp Disclosure — низкий риск на тестовом стенде) +10027 IGNORE (Information Disclosure - Suspicious Comments) diff --git a/.zap/zap-auth.yaml b/.zap/zap-auth.yaml new file mode 100644 index 0000000000..ab69e85154 --- /dev/null +++ b/.zap/zap-auth.yaml @@ -0,0 +1,76 @@ +# ZAP Automation Framework — АУТЕНТИФИЦИРОВАННЫЙ скан DefectDojo. +# Browser-based авторизация: ZAP реально заполняет форму логина в headless-браузере, +# поэтому Django CSRF-токен (csrfmiddlewaretoken) подхватывается автоматически. +# +# Запуск (см. dast.yml): +# docker run --network host -e ZAP_USER -e ZAP_PASSWORD \ +# -v $PWD/zap_wrk:/zap/wrk:rw ghcr.io/zaproxy/zaproxy:stable \ +# zap.sh -cmd -autorun /zap/wrk/zap-auth.yaml +# ${ZAP_USER}/${ZAP_PASSWORD} подставляются ZAP из переменных окружения. + +env: + contexts: + - name: defectdojo + urls: + - "http://localhost:8080" + includePaths: + - "http://localhost:8080.*" + excludePaths: + - "http://localhost:8080/logout.*" + - ".*/logout/?$" + authentication: + method: "browser" + parameters: + loginPageUrl: "http://localhost:8080/login" + loginPageWait: 5 + browserId: "firefox-headless" + verification: + method: "poll" + loggedInRegex: '\\bLogout\\b' + loggedOutRegex: '\\bLogin\\b' + pollUrl: "http://localhost:8080/" + pollFrequency: 60 + pollUnits: "requests" + sessionManagement: + method: "cookie" + users: + - name: "zaptester" + credentials: + username: "${ZAP_USER}" + password: "${ZAP_PASSWORD}" + parameters: + failOnError: false + failOnWarning: false + progressToStdout: true + +jobs: + - type: spider + parameters: + context: defectdojo + user: "zaptester" + maxDuration: 5 + - type: spiderAjax + parameters: + context: defectdojo + user: "zaptester" + maxDuration: 5 + browserId: "firefox-headless" + - type: passiveScan-wait + parameters: + maxDuration: 5 + - type: activeScan + parameters: + context: defectdojo + user: "zaptester" + maxRuleDurationInMins: 3 + maxScanDurationInMins: 30 + - type: report # для Security Gateway + parameters: + template: "sarif-json" + reportDir: "/zap/wrk" + reportFile: "zap-auth" + - type: report # для нативного парсера DefectDojo + parameters: + template: "traditional-json" + reportDir: "/zap/wrk" + reportFile: "zap-auth-dojo" diff --git a/docs/DEVSECOPS.md b/docs/DEVSECOPS.md new file mode 100644 index 0000000000..ec982bc00a --- /dev/null +++ b/docs/DEVSECOPS.md @@ -0,0 +1,186 @@ +# DevSecOps-пайплайн для DefectDojo + +Дипломная работа по профессии «Специалист по информационной безопасности», трек DevSecOps. + +Документ описывает архитектуру безопасного CI/CD-конвейера, обоснование выбора инструментов, +матрицу покрытия проверками, политику Security Gateway и зоны роста. + +--- + +## 1. Объект защиты + +**Целевой проект:** [DefectDojo](https://github.com/DefectDojo/django-DefectDojo) — открытая +платформа управления уязвимостями (OWASP). + +Проект выбран потому, что удовлетворяет всем требованиям задания: это полноценный веб-сервис +с несколькими языками и фреймворками, реляционной СУБД и кешем. + +| Компонент | Технология | +|---|---| +| Веб-фреймворк | Django (Python) | +| Фронтенд | HTML-шаблоны, JavaScript, Vue-компоненты, SCSS | +| База данных | PostgreSQL | +| Кеш / брокер | Redis | +| Очереди задач | Celery (worker + beat) | +| Веб-сервер | nginx (reverse proxy) | +| Упаковка | Docker / docker-compose | + +Дополнительный плюс выбора: DefectDojo сам является системой менеджмента уязвимостей, поэтому +для этапа «выгрузка результатов в систему менеджмента» поднимается **отдельный** инстанс +DefectDojo (не тот, что тестируется), куда через REST API стекаются все отчёты сканеров. +Это разделяет роли «мишень» и «хаб управления» и исключает конфликт интересов. + +--- + +## 2. Архитектура пайплайна + +```mermaid +flowchart TD + dev[Разработчик: push / pull_request] --> gha[GitHub Actions] + + subgraph CI["CI: workflow security.yml"] + secrets[Gitleaks — секреты] + sast_py[Semgrep + Bandit — Python] + sast_js[Semgrep + njsscan — JS] + sca[Trivy fs — зависимости pip/npm] + iac[Checkov + Trivy config — IaC/Dockerfile] + img[Hadolint + Trivy image — образы] + end + + subgraph CD["CD/DAST: workflow dast.yml"] + build[docker-compose up DefectDojo] + zap[OWASP ZAP baseline + full scan] + end + + gha --> CI + gha --> CD + + CI --> sarif[SARIF-отчёты] + CD --> sarif + sarif --> gate[Security Gateway: gate.py] + sarif --> tab[GitHub Security tab] + sarif --> dojo[(DefectDojo — менеджмент)] + + gate -->|severity >= порога| block[Пайплайн падает,\nрелиз остановлен] + gate -->|комментарий| pr[Комментарий в Pull Request\nс рекомендациями] +``` + +Единый формат обмена — **SARIF** (Static Analysis Results Interchange Format). Все сканеры, +где это возможно, выводят SARIF; это позволяет одним gate-скриптом агрегировать результаты +и одновременно загружать их во вкладку GitHub Security через `codeql-action/upload-sarif`. + +--- + +## 3. Обоснование выбора инструментов + +Принцип отбора: приоритет open-source инструментам с активным сообществом, поддержкой SARIF, +готовыми парсерами в DefectDojo и удобным запуском в CI. Для требования «ни один язык не должен +быть пропущен» базовым выбран мультиязычный Semgrep, усиленный язык-специфичными анализаторами. + +### Этап 2 — SAST + +| Инструмент | Роль | Почему он | Рассмотренные альтернативы | +|---|---|---|---| +| **Semgrep** | Основной мультиязычный SAST | Один инструмент покрывает Python, JS/TS и др.; правила OWASP/CWE; нативный SARIF; парсер в DefectDojo | CodeQL (мощнее, но тяжелее и заточен под GitHub), SonarQube (нужен сервер) | +| **Bandit** | Углублённый SAST для Python | Специализирован на Python-специфике (небезопасная десериализация, `subprocess`, слабая криптография) | pylint-security | +| **njsscan** | SAST для JavaScript | Быстрые проверки Node/JS-паттернов, дополняет Semgrep | ESLint + eslint-plugin-security | +| **Trivy (fs)** | SCA — анализ зависимостей | Единый инструмент для pip и npm; база уязвимостей CVE/GHSA; SARIF | Grype, OWASP Dependency-Check, Snyk (коммерческий) | + +### Этап 3 — DAST + +| Инструмент | Роль | Почему он | Альтернативы | +|---|---|---|---| +| **OWASP ZAP** | Динамический анализ развёрнутого сервиса | Отраслевой стандарт; baseline (пассивный) + full (активный) сканы; Automation Framework для аутентифицированных сканов; парсер в DefectDojo | Nikto (проще, слабее), Nuclei (шаблонный), Burp (коммерческий) | + +### Этап 4 — Security Checks + +| Инструмент | Роль | Почему он | Альтернативы | +|---|---|---|---| +| **Gitleaks** | Поиск секретов в коде и истории | Быстрый, CI-native, настраиваемые правила, поддержка pre-commit | TruffleHog, detect-secrets | +| **Hadolint** | Линтер Dockerfile | Ловит небезопасные и неоптимальные инструкции Dockerfile | dockerfilelint | +| **Trivy (image)** | Скан собранных образов | ОС-пакеты и уязвимости слоёв; один вендор с SCA | Grype, Clair | +| **Trivy config / Checkov** | Проверка IaC и docker-compose на мисконфиги | Правила по CIS и best practices для Docker/compose | kics, terrascan | + +### Этап 5 — Security Gateway + +| Компонент | Роль | +|---|---| +| **gate.py** (собственный скрипт) | Агрегирует SARIF, считает находки по severity, роняет пайплайн при превышении порога | +| **GitHub PR API** | Оставляет в Pull Request комментарий со сводкой и рекомендациями по исправлению | +| **DefectDojo API** | Дедупликация, тренды, жизненный цикл уязвимостей, отчётность | + +--- + +## 4. Матрица покрытия + +Требование задания — покрыть каждый язык и фреймворк проекта. Соответствие: + +| Технология в проекте | SAST | SCA | Прочее | +|---|---|---|---| +| Python / Django | Semgrep, Bandit | Trivy (requirements) | — | +| JavaScript / Vue | Semgrep, njsscan | Trivy (package.json) | — | +| Dockerfile | Trivy config | — | Hadolint | +| docker-compose / IaC | Checkov, Trivy config | — | — | +| Собранные образы | — | Trivy image | — | +| Секреты (все файлы) | — | — | Gitleaks | +| Запущенный сервис (HTTP/API) | — | — | OWASP ZAP | + +Таким образом ни один язык, фреймворк или артефакт сборки не остаётся без проверки. + +--- + +## 5. Политика Security Gateway + +Гейт реализован в `security/gate.py`. Логика: + +1. Собирает все файлы `*.sarif` из отчётов сканеров. +2. Нормализует severity: числовой `security-severity` (шкала CVSS 0–10) → Critical/High/Medium/Low; + при его отсутствии используется `level` (error → High, warning → Medium, note → Low). +3. Сравнивает с порогом `GATE_FAIL_ON` (по умолчанию `HIGH`): наличие находок этого уровня и выше + останавливает пайплайн (exit code 1) — **триггер на остановку релиза**. +4. Формирует markdown-сводку: пишет в `$GITHUB_STEP_SUMMARY` и оставляет комментарий в Pull Request + через GitHub API, включая правило, файл, строку и ссылку на рекомендацию (`helpUri`). + +Пороги настраиваются через переменные окружения и позволяют разную строгость для веток +(например, `HIGH` для feature-веток и `MEDIUM` для `main`/релизов). + +--- + +## 6. Требуемые секреты и переменные (GitHub → Settings → Secrets) + +| Имя | Назначение | +|---|---| +| `DOJO_URL` | URL инстанса DefectDojo-менеджмента (например `https://:8080`) | +| `DOJO_TOKEN` | API-токен DefectDojo (Profile → API v2 Key) | +| `GITHUB_TOKEN` | Стандартный токен Actions (даётся автоматически) для комментариев в PR | + +Для аутентифицированного DAST дополнительно: `ZAP_USER`, `ZAP_PASSWORD` (тестовая учётка). + +--- + +## 7. Зоны роста + +Реалистичные направления развития, которые стоит вынести в защиту как «что дальше»: + +- **SBOM и цепочка поставок:** генерация CycloneDX/SPDX через Syft, подпись образов cosign, + верификация артефактов (SLSA). +- **Policy-as-Code:** вынести правила гейта в OPA/Conftest вместо императивного скрипта. +- **Аутентифицированный DAST:** полноценный обход защищённых страниц DefectDojo через + ZAP Automation Framework (шаблон приложен в `.zap/zap-auth.yaml`). +- **IAST/интерактивное тестирование** во время функциональных тестов. +- **Runtime-безопасность:** Falco/Tetragon на VPS для контроля поведения контейнеров. +- **Baseline и подавление шума:** ведение baseline ложных срабатываний, триаж в DefectDojo, + SLA на устранение по severity. +- **Пиннинг GitHub Actions к commit SHA** вместо тегов — защита от компрометации тегов действий. + +--- + +## 8. Как это отвечает критериям задания + +| Этап | Критерий | Реализация | +|---|---|---| +| 1. CI/CD | Настроенный пайплайн, облако, документация | GitHub Actions, VPS-раннер/облачные раннеры, этот документ | +| 2. SAST | Покрытие, успешные проверки, выгрузка | Semgrep/Bandit/njsscan/Trivy → SARIF → GitHub Security + DefectDojo | +| 3. DAST | Покрытие сервиса, все методы, выгрузка | ZAP baseline+full → DefectDojo | +| 4. Security Checks | Секреты, конфигурации/образы | Gitleaks + Hadolint + Trivy image/config + Checkov | +| 5. Security Gateway | Остановка релиза, комментарии, рекомендации | gate.py: exit-код + комментарий в PR с `helpUri` | diff --git a/docs/THREATMODEL.md b/docs/THREATMODEL.md new file mode 100644 index 0000000000..cc1ae9141f --- /dev/null +++ b/docs/THREATMODEL.md @@ -0,0 +1,76 @@ +# Модель угроз и SLA на устранение + +Документ связывает угрозы DefectDojo (по методологии STRIDE) с конкретными +контролями пайплайна и задаёт сроки устранения (SLA) по уровням severity. + +--- + +## 1. Границы доверия + +Рассматриваются три зоны: + +- **Репозиторий и CI** — исходный код, зависимости, конфигурации, секреты, workflow. +- **Артефакты сборки** — Docker-образы (django, nginx) и docker-compose. +- **Запущенный сервис** — DefectDojo за nginx, с PostgreSQL и Redis, доступный по HTTP. + +Пересечение границ (код → образ → работающий сервис) — точки, где вносятся и +проявляются уязвимости; на каждой из них стоит соответствующий контроль пайплайна. + +--- + +## 2. STRIDE: угрозы и контроли + +| Категория STRIDE | Угроза применительно к DefectDojo | Контроль в пайплайне | +|---|---|---| +| **S — Spoofing** (подмена субъекта) | Утечка учётных данных/токенов в коде; слабая аутентификация форм | Gitleaks (секреты), проверка потоков аутентификации аутентифицированным ZAP, отсутствие хардкода в SAST | +| **T — Tampering** (искажение) | Компрометация зависимостей и базовых образов (supply chain) | Trivy SCA (pip/npm), Trivy image, пиннинг GitHub Actions к SHA; SBOM/cosign — зона роста | +| **R — Repudiation** (отказ от авторства) | Нет аудита изменений и находок | Логи CI, история находок и их жизненный цикл в DefectDojo; подпись коммитов — зона роста | +| **I — Information Disclosure** (раскрытие) | Секреты в коде, подробные ошибки, SQLi с утечкой данных БД | Gitleaks; Semgrep/Bandit (SQLi, небезопасный вывод); ZAP-алерты о раскрытии информации | +| **D — Denial of Service** (отказ в обслуживании) | ReDoS, уязвимые зависимости, ресурсное истощение | Semgrep (небезопасные регулярные выражения), Trivy (CVE зависимостей) | +| **E — Elevation of Privilege** (повышение привилегий) | Инъекции (SQL/командные), небезопасная десериализация, контейнер от root, мисконфиги | Semgrep/Bandit, активный скан ZAP, Trivy config/Checkov, Hadolint (запуск не от root) | + +Такое сопоставление показывает, что каждая категория угроз покрыта минимум одним +автоматическим контролем, а критичные (I и E) — несколькими. + +--- + +## 3. SLA на устранение + +Сроки отсчитываются с момента появления находки в DefectDojo. Действие Security +Gateway определяется порогом `GATE_FAIL_ON` в workflow. + +| Severity | Срок устранения | Действие Gateway | Кто отвечает | +|---|---|---|---| +| Critical | 24–48 часов | Блокировка релиза | Владелец компонента + безопасность | +| High | 7 дней | Блокировка релиза | Владелец компонента | +| Medium | 30 дней | Предупреждение, тикет, комментарий в PR | Команда разработки | +| Low | 90 дней / по возможности | Информирование | Команда разработки | + +Порог блокировки настраивается по веткам: `HIGH` для feature-веток и `security.yml`, +`CRITICAL` для шумного DAST (`dast.yml`). Для релизной ветки рекомендуется ужесточить +до `MEDIUM`. + +--- + +## 4. Триаж в DefectDojo + +Жизненный цикл находки, чтобы SLA работал, а не копился шум: + +1. **New** — находка импортирована сканером. +2. **Triaged** — подтверждена как истинная либо помечена как ложное срабатывание. +3. **Active / False Positive / Risk Accepted** — решение по находке. +4. **Mitigated** — исправление внесено. +5. **Verified** — исправление подтверждено повторным сканом. + +Дедупликация DefectDojo не даёт одной и той же уязвимости порождать новые тикеты при +каждом запуске, а тренды показывают динамику долга по безопасности во времени. + +--- + +## 5. Остаточные риски и зоны роста + +- Ложные срабатывания активного DAST требуют ведения baseline и ручного триажа. +- Полное покрытие бизнес-логики требует аутентифицированных и, при необходимости, + ролевых сценариев (несколько пользователей с разными правами). +- Цепочка поставок усиливается генерацией SBOM, подписью образов cosign и + верификацией по SLSA — вынесено в перспективные направления. diff --git a/security/dojo_upload.py b/security/dojo_upload.py new file mode 100644 index 0000000000..f8d46a269c --- /dev/null +++ b/security/dojo_upload.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Выгрузка отчётов сканеров в DefectDojo через /api/v2/import-scan/. + +Использует auto_create_context — продукт и engagement создаются по имени. +Переменные окружения: DOJO_URL, DOJO_TOKEN, DOJO_PRODUCT, DOJO_ENGAGEMENT. + +Ключевое решение по scan_type: + - Все SARIF-отчёты грузятся УНИВЕРСАЛЬНЫМ парсером "SARIF" (он валиден для + Semgrep, Bandit, njsscan, Trivy, Checkov, Hadolint, Gitleaks — все умеют SARIF). + - ZAP грузится своим НАТИВНЫМ парсером "ZAP Scan" (traditional-json отчёт). + +Так исключён рассинхрон «файл SARIF, а парсер ждёт родной JSON». +Сверить доступные парсеры: GET {DOJO_URL}/api/v2/test_types/ +""" +import argparse +import os +import sys + +import requests + +# report file -> DefectDojo scan_type +REPORTS = { + "sast": { + "reports/semgrep.sarif": "SARIF", + "reports/bandit.sarif": "SARIF", + "reports/njsscan.sarif": "SARIF", + "reports/trivy-fs.sarif": "SARIF", + "reports/trivy-config.sarif": "SARIF", + "reports/checkov.sarif": "SARIF", + "reports/hadolint.sarif": "SARIF", + "reports/gitleaks.sarif": "SARIF", + }, + "dast": { + # нативный парсер ZAP ждёт traditional-json, НЕ sarif + "reports/zap-baseline.json": "ZAP Scan", + "reports/zap-full.json": "ZAP Scan", + "reports/zap-auth-dojo.json": "ZAP Scan", + }, +} + + +def upload(url, token, product, engagement, file_path, scan_type): + if not os.path.isfile(file_path): + print(f"[dojo] нет файла {file_path} — пропуск") + return + endpoint = url.rstrip("/") + "/api/v2/import-scan/" + data = { + "scan_type": scan_type, + "product_name": product, + "product_type_name": "Research", + "engagement_name": engagement, + "auto_create_context": "true", + "active": "true", + "verified": "false", + "minimum_severity": "Low", + "close_old_findings": "false", + } + with open(file_path, "rb") as fh: + files = {"file": (os.path.basename(file_path), fh)} + try: + resp = requests.post( + endpoint, + headers={"Authorization": f"Token {token}"}, + data=data, + files=files, + timeout=120, + verify=os.environ.get("DOJO_VERIFY_TLS", "true").lower() != "false", + ) + except requests.RequestException as exc: + print(f"[dojo] ошибка сети для {file_path}: {exc}") + return + if resp.status_code in (200, 201): + print(f"[dojo] загружено {file_path} как '{scan_type}'") + else: + print(f"[dojo] {file_path}: HTTP {resp.status_code} — {resp.text[:300]}") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--stage", choices=list(REPORTS), required=True) + args = ap.parse_args() + + url = os.environ.get("DOJO_URL") + token = os.environ.get("DOJO_TOKEN") + product = os.environ.get("DOJO_PRODUCT", "DefectDojo (self)") + engagement = os.environ.get("DOJO_ENGAGEMENT", "CI") + + if not (url and token): + print("[dojo] DOJO_URL/DOJO_TOKEN не заданы — выгрузка пропущена") + sys.exit(0) + + for file_path, scan_type in REPORTS[args.stage].items(): + upload(url, token, product, engagement, file_path, scan_type) + + +if __name__ == "__main__": + main() diff --git a/security/gate.py b/security/gate.py new file mode 100644 index 0000000000..5ae6ffe5a3 --- /dev/null +++ b/security/gate.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Security Gateway. + +Агрегирует SARIF-отчёты сканеров, считает находки по severity, при превышении +порога роняет пайплайн (exit 1) и оставляет комментарий со сводкой и +рекомендациями в Pull Request. + +Порог задаётся переменной окружения GATE_FAIL_ON (CRITICAL|HIGH|MEDIUM|LOW). +""" +import argparse +import glob +import json +import os +import sys +import urllib.request +from collections import Counter, defaultdict + +SEV_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + + +def sev_from_result(result, rules): + """SARIF result -> severity bucket.""" + rule_id = result.get("ruleId", "") + props = rules.get(rule_id, {}).get("properties", {}) + score = props.get("security-severity") or result.get("properties", {}).get("security-severity") + if score is not None: + try: + s = float(score) + except ValueError: + s = None + if s is not None: + if s >= 9.0: + return "CRITICAL" + if s >= 7.0: + return "HIGH" + if s >= 4.0: + return "MEDIUM" + return "LOW" + level = result.get("level", "warning") + return {"error": "HIGH", "warning": "MEDIUM", "note": "LOW"}.get(level, "MEDIUM") + + +def load_rules(run): + rules = {} + driver = run.get("tool", {}).get("driver", {}) + for r in driver.get("rules", []): + rules[r.get("id", "")] = r + return rules + + +def parse_sarif(path): + findings = [] + try: + with open(path, encoding="utf-8") as fh: + data = json.load(fh) + except Exception as exc: # noqa: BLE001 + print(f"[gate] пропуск {path}: {exc}") + return findings + for run in data.get("runs", []): + tool = run.get("tool", {}).get("driver", {}).get("name", os.path.basename(path)) + rules = load_rules(run) + for res in run.get("results", []): + rule = rules.get(res.get("ruleId", ""), {}) + loc = "" + locs = res.get("locations", []) + if locs: + phys = locs[0].get("physicalLocation", {}) + uri = phys.get("artifactLocation", {}).get("uri", "") + line = phys.get("region", {}).get("startLine", "") + loc = f"{uri}:{line}" if line else uri + findings.append({ + "tool": tool, + "rule": res.get("ruleId", ""), + "severity": sev_from_result(res, rules), + "message": (res.get("message", {}) or {}).get("text", "")[:200], + "location": loc, + "help": rule.get("helpUri", "") or (rule.get("help", {}) or {}).get("text", "")[:200], + }) + return findings + + +def post_pr_comment(body): + token = os.environ.get("GITHUB_TOKEN") + event_path = os.environ.get("GITHUB_EVENT_PATH") + repo = os.environ.get("GITHUB_REPOSITORY") + if not (token and event_path and repo): + print("[gate] нет данных для комментария в PR — пропуск") + return + try: + with open(event_path, encoding="utf-8") as fh: + event = json.load(fh) + pr = event.get("pull_request", {}).get("number") + except Exception: # noqa: BLE001 + pr = None + if not pr: + print("[gate] это не PR — комментарий не оставляем") + return + url = f"https://api.github.com/repos/{repo}/issues/{pr}/comments" + req = urllib.request.Request( + url, + data=json.dumps({"body": body}).encode("utf-8"), + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + urllib.request.urlopen(req, timeout=30) + print("[gate] комментарий оставлен в PR") + except Exception as exc: # noqa: BLE001 + print(f"[gate] не удалось оставить комментарий: {exc}") + + +def build_report(findings, counts, threshold, blocked): + status = "❌ РЕЛИЗ ОСТАНОВЛЕН" if blocked else "✅ Проверки пройдены" + lines = [f"## Security Gateway — {status}", ""] + lines.append(f"Порог блокировки: **{threshold}** и выше.") + lines.append("") + lines.append("| Severity | Найдено |") + lines.append("|---|---|") + for sev in reversed(SEV_ORDER): + lines.append(f"| {sev} | {counts.get(sev, 0)} |") + lines.append("") + # топ блокирующих находок с рекомендациями + thr_idx = SEV_ORDER.index(threshold) + blocking = [f for f in findings if SEV_ORDER.index(f["severity"]) >= thr_idx] + if blocking: + lines.append("### Блокирующие находки (топ-15)") + lines.append("") + lines.append("| Severity | Инструмент | Правило | Где | Рекомендация |") + lines.append("|---|---|---|---|---|") + for f in blocking[:15]: + rec = f["help"] or "—" + lines.append(f"| {f['severity']} | {f['tool']} | `{f['rule']}` | {f['location']} | {rec} |") + if len(blocking) > 15: + lines.append("") + lines.append(f"…и ещё {len(blocking) - 15}. Полный список — в артефактах и DefectDojo.") + return "\n".join(lines) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--reports", default="reports") + args = ap.parse_args() + + threshold = os.environ.get("GATE_FAIL_ON", "HIGH").upper() + if threshold not in SEV_ORDER: + threshold = "HIGH" + + findings = [] + for path in glob.glob(os.path.join(args.reports, "*.sarif")): + findings.extend(parse_sarif(path)) + + counts = Counter(f["severity"] for f in findings) + thr_idx = SEV_ORDER.index(threshold) + blocked = any(SEV_ORDER.index(f["severity"]) >= thr_idx for f in findings) + + report = build_report(findings, counts, threshold, blocked) + print(report) + + # step summary + summary = os.environ.get("GITHUB_STEP_SUMMARY") + if summary: + with open(summary, "a", encoding="utf-8") as fh: + fh.write(report + "\n") + + post_pr_comment(report) + + if blocked: + print(f"\n[gate] найдены уязвимости уровня {threshold}+ — останавливаем релиз") + sys.exit(1) + print("\n[gate] порог не превышен") + + +if __name__ == "__main__": + main()