Опирается на правила:
R-SEC-DEP-1…R-SEC-DEP-4иR-SEC-DEP-X1…R-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-директива на непроверенный форк в productiongo.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.
| CVSS | Severity | Действие |
|---|---|---|
| 9.0–10.0 | CRITICAL | Сборка падает, hotfix ≤ 24ч |
| 7.0–8.9 | HIGH | Сборка падает, патч ≤ 2 недели |
| 4.0–6.9 | MEDIUM | Отчёт, патч ≤ 30 дней |
| 0.1–3.9 | LOW | Игнор |
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.json | R-SEC-DEP-X1 | "until": "YYYY-MM-DD" обязательно |
replace на форк без CVE-обоснования в go.mod | R-SEC-DEP-X2 | документировать причину и срок в README |
v0.0.0-YYYYMMDD pre-release в production без пояснения | R-SEC-DEP-X2 | только теговые версии |
govulncheck на каждом PR | R-SEC-DEP-1 | merge в main + nightly + release |
go.sum не закоммичен / в .gitignore | R-SEC-DEP-1 | go.sum всегда в репо |
| Без Renovate / Dependabot | R-SEC-DEP-2 | один из двух обязателен |
| Auto-merge major-обновлений | R-SEC-DEP-2 | major — manual review |
Игнор [informational] finding без проверки call graph | R-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.