Опирается на правила: R-SEC-DEP-1R-SEC-DEP-4 и R-SEC-DEP-X1R-SEC-DEP-X2 из Security Style Guide → раздел 2. CVE в зависимостях.

Важно знать

  • govulncheck сканирует go.mod-граф против Go Vulnerability Database (GHSA + Go CVE DB) — понимает, какие функции реально вызываются в коде, а не просто какие пакеты импортированы.
  • Запускается на merge в main + nightly + release-tag, не на каждом PR — база обновляется непрерывно, прогнать на PR бессмысленно: CVE появляется в DB, не из твоего PR.
  • go.sum коммитится всегда — обеспечивает воспроизводимость; go mod tidy в CI фейлит, если go.sum не актуален.
  • Renovate / Dependabot (ecosystem: gomod) — auto-PR на minor/patch, major — manual review.
  • CVSS ≥ 7.0 (HIGH/CRITICAL) ломает сборку; 4.0–6.9 — отчёт + 30 дней; ниже — игнор.
  • Suppressions в govuln-suppressions.json обязательно содержат поле "until" — бессрочное подавление запрещено (R-SEC-DEP-X1).
  • replace-директива на непроверенный форк в production go.mod без CVE-обоснования — запрещена (R-SEC-DEP-X2).
  • Trivy дополнительно сканирует образ по OS-пакетам и зависимостям после docker build (R-SEC-IMG-1).

Уязвимость в зависимости — не в коде, который ты написал. Log4Shell, fasthttp path-traversal, protobuf integer overflow — все «жили» в транзитивных зависимостях. Разработчики не трогали уязвимый код, но получали CVE в продакшне. UCP формулирует обязательный сканер + workflow обновления для Go-стека.

govulncheck

R-SEC-DEP-1: govulncheck обязателен на merge в main, nightly и release.

go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

Ключевое отличие от аналогов на других языках: govulncheck понимает call graph. Если уязвимая функция foo.Bar() не вызывается ни из одного пути в модуле — finding помечается как [informational], не ломает сборку. Это резко снижает шум false positives по транзитивным зависимостям.

Где запускается — расслоение R-SEC-2:

Этапgovulncheck?Причина
Локальная разработкаНетDB обновляется на стороне сервиса, локально кэш устаревший
Pre-commitНетЗанимает секунды, но бесполезно — CVE появится в DB позже
PRНетCVE не появляется из PR
Merge в mainДаBaseline обновляется
NightlyДаНовые CVE в существующих зависимостях
Release tagДаФинальная проверка перед выпуском

CI step для сервиса заказов

# .github/workflows/main.yml (фрагмент — gate на main)
jobs:
  vuln-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.23'
      - name: Install govulncheck
        run: go install golang.org/x/vuln/cmd/govulncheck@latest
      - name: Scan vulnerabilities
        run: govulncheck -json ./... | tee govuln.json
      - name: Fail on HIGH/CRITICAL
        run: |
          # govulncheck -json выдаёт findings с severity; скрипт проверяет CVSS >= 7.0
          python3 ci/check-govuln.py govuln.json

Скрипт ci/check-govuln.py читает govuln.json и завершается с exit code 1, если есть HIGH или CRITICAL findings, не помеченные как [informational]. Такой CI-шаг реализует R-SEC-DEP-3 и R-SEC-1 одним движением.

go.sum и воспроизводимость

go.sum — криптографический манифест всех зависимостей. Без него go build выкачивает модули по мере надобности и не гарантирует, что сборка сегодня и через месяц использует одинаковые байты.

# В CI — проверка актуальности go.sum
go mod tidy
git diff --exit-code go.sum

Если go.sum изменился после go mod tidy — PR не прошёл через механизм изменения зависимостей. Это либо ручное редактирование go.mod без go mod tidy, либо рассинхрон после merge.

R-SEC-DEP-X2 запрещает v0.0.0-YYYYMMDD pre-release в production без объяснения в README — по той же причине: псевдо-версия означает «взяли коммит, а не тег», воспроизводимость под вопросом. replace-директива на форк без CVE-обоснования запрещена аналогично.

Renovate для Go-модулей

R-SEC-DEP-2: Renovate или Dependabot обязателен — авто-PR на minor/patch, major — manual review.

// renovate.json (для orders-service)
{
  "extends": ["config:base"],
  "schedule": ["before 8am on monday"],
  "labels": ["dependencies"],
  "packageRules": [
    {
      "matchManagers": ["gomod"],
      "matchUpdateTypes": ["minor", "patch"],
      "automerge": true,
      "automergeType": "pr",
      "platformAutomerge": true
    },
    {
      "matchManagers": ["gomod"],
      "matchUpdateTypes": ["major"],
      "automerge": false,
      "labels": ["dependencies", "major-update"]
    }
  ],
  "vulnerabilityAlerts": {
    "enabled": true,
    "labels": ["security"]
  }
}

Renovate обновляет go.mod + go.sum в одном PR — go.sum остаётся актуальным без ручного go mod tidy. Vulnerability alerts создают отдельные PR с меткой security при появлении CVE в используемой версии — даже если minor/patch тег ещё не вышел.

Без Renovate зависимости копятся, через полгода github.com/go-chi/chi отстаёт на несколько minor-версий, обновление требует ревью сразу 20 PR-эквивалентов изменений вместо последовательных маленьких апдейтов.

CVSS → действие

R-SEC-DEP-3: severity per CVSS score.

CVSSSeverityДействие
9.0–10.0CRITICALСборка падает, hotfix ≤ 24ч
7.0–8.9HIGHСборка падает, патч ≤ 2 недели
4.0–6.9MEDIUMОтчёт, патч ≤ 30 дней
0.1–3.9LOWИгнор

Release блокируется только на новые HIGH+ findings относительно baseline (R-SEC-4). Старый долг (CVE без upstream-патча) трекается отдельной задачей — иначе первый неисправленный HIGH останавливает pipeline на недели, и команда начинает отключать сканер.

Suppressions с "until"

R-SEC-DEP-4: каждое подавление имеет срок и обоснование.

// govuln-suppressions.json
{
  "suppressions": [
    {
      "vuln": "GO-2024-3153",
      "reason": "уязвимая функция net/http.(*Transport).roundTrip не вызывается — сервис использует кастомный transport без redirect; govulncheck помечает как [informational]",
      "until": "2026-09-01"
    },
    {
      "vuln": "GO-2025-0042",
      "reason": "CVE в golang.org/x/text/encoding: код-путь orders-service не вызывает affected decoder; ждём golang.org/x/text v0.17.0",
      "until": "2026-07-15"
    }
  ]
}

Поле "until" — дата ISO 8601 (UTC). Без него — нарушение R-SEC-DEP-X1. Квартальный аудит: jq '.suppressions[] | select(.until < now | todate)' govuln-suppressions.json — найти просроченные.

Suppressions через //nolint не применимы к CVE в зависимостях — это инструмент SAST. Для govulncheck suppressions живут только в govuln-suppressions.json.

Trivy дополняет govulncheck

govulncheck покрывает Go-модули. Trivy после docker build покрывает OS-пакеты (Alpine apk, Debian apt) и те же Go-зависимости — второй слой проверки (R-SEC-IMG-1):

# .github/workflows/main.yml (фрагмент — после build)
- name: Scan image
  run: |
    trivy image \
      --exit-code 1 \
      --severity HIGH,CRITICAL \
      --format sarif \
      --output trivy.sarif \
      orders-service:${{ github.sha }}
- name: Upload SARIF
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: trivy.sarif

SARIF попадает в GitHub Security tab (R-SEC-FIND-3) — единый дашборд без дополнительной инфры.

Что запрещено

АнтипаттернПравилоЧто взамен
Suppression без "until" в govuln-suppressions.jsonR-SEC-DEP-X1"until": "YYYY-MM-DD" обязательно
replace на форк без CVE-обоснования в go.modR-SEC-DEP-X2документировать причину и срок в README
v0.0.0-YYYYMMDD pre-release в production без поясненияR-SEC-DEP-X2только теговые версии
govulncheck на каждом PRR-SEC-DEP-1merge в main + nightly + release
go.sum не закоммичен / в .gitignoreR-SEC-DEP-1go.sum всегда в репо
Без Renovate / DependabotR-SEC-DEP-2один из двух обязателен
Auto-merge major-обновленийR-SEC-DEP-2major — manual review
Игнор [informational] finding без проверки call graphR-SEC-FIND-X1проверить, задокументировать, suppression с датой

Куда дальше

  • Container/image-уязвимости — Trivy сканирует образ по OS-пакетам и Go-зависимостям.
  • SAST по коду — gosec и golangci-lint: параллельный слой для исходного кода.
  • Секреты в коде и истории — Gitleaks: pre-commit и CI.
  • Реакция на findings — SLA по severity, suppressions со сроком, SARIF-дашборд.
  • Криптография — golang.org/x/crypto, AES-GCM, crypto/rand.