Опирается на правила:
R-SEC-FIND-1,R-SEC-FIND-2,R-SEC-FIND-3,R-SEC-FIND-X1,R-SEC-1,R-SEC-4из Security Style Guide → раздел 6. Реакция на findings.
Важно знать
- CRITICAL — сборка падает, hotfix ≤ 24 часа; источник: любой инструмент.
- HIGH — сборка падает, патч ≤ 2 недели;
gosec -severity HIGHиgovulncheckвозвращают exit code 1 автоматически.- MEDIUM — отчёт, патч ≤ 30 дней; не блокирует merge.
- LOW — игнор, не отображается в dashboard.
//nolint:gosecбез кода нарушения и обоснования ≥ 30 символов — не принимается (R-SEC-SAST-X1).govuln-suppressions.jsonбез"until"— suppression не засчитывается, считается долгом (R-SEC-FIND-2).- Молчание = долг: finding без suppression накапливается, повторно анализируется на каждом review.
- SARIF из
gosec,semgrep,trivy imageзагружается в GitHub Security tab — единый дашборд без отдельной инфры (R-SEC-FIND-3).
Findings — это входящий поток работы для security backlog. Контракт R-SEC-FIND-* задаёт SLA per severity: команда не тонет в noise (LOW) и реагирует на критичное быстро. Без явного SLA findings накапливаются, дашборд превращается в «море красного», команда перестаёт обращать внимание.
Severity → SLA
R-SEC-FIND-1: четыре уровня, четыре разных действия.
| Severity | Инструменты | SLA | Блокирует сборку |
|---|---|---|---|
| CRITICAL | gosec, govulncheck, Trivy, semgrep | ≤ 24 часа, hotfix | да |
| HIGH | gosec, govulncheck, Trivy | ≤ 2 недели (текущий спринт) | да |
| MEDIUM | gosec, Trivy | ≤ 30 дней | нет |
| LOW | любой | — | нет, игнор |
В CI это достигается без дополнительного кода — инструменты сами возвращают ненулевой exit code:
# PR gate — SAST
gosec -fmt sarif -out gosec.sarif -severity HIGH ./...
# main/nightly/release gate — CVE
govulncheck ./...
# после docker build — образ
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--format sarif \
--output trivy.sarif \
orders-service:${{ github.sha }}
CRITICAL — hotfix ≤ 24 часа
Примеры для сервиса orders:
govulncheckнаходит активно эксплуатируемый CVE вgithub.com/go-chi/chiв runtime.- Gitleaks фиксирует
SBER_API_KEY=prod-...в коммите. gosecG401 наcrypto/md5в пути хеширования пароляCustomer.
Алгоритм:
- Уведомить команду немедленно.
- Создать hotfix branch от main.
- Применить patch или временный workaround.
- Задеплоить в прод в течение 24 часов.
- Провести post-mortem.
HIGH — текущий спринт
Примеры:
- CVSS 7.5 в
pgx/v5транзитивной зависимости (SQL-инъекция). gosecG304 наos.Open(r.URL.Query().Get("path"))в обработчикеProduct.- Trivy HIGH в base image
golang:1.23-alpine.
gh issue create \
--title "HIGH: gosec G304 в ProductHandler.ServeHTTP" \
--label "security/high" \
--body "gosec G304: path построен из query param без валидации. Заменить на allowlist."
MEDIUM — 30 дней
Примеры:
- CVSS 5.3 в
encoding/jsonбез active exploit. - Trivy MEDIUM в OS-пакете alpine без known PoC.
Ticket создаётся, включается в ближайшие спринты. Если через 30 дней не закрыт — автоматически поднимается до HIGH (CI-скрипт или GitHub Action по метке + дате).
LOW — игнор
Примеры:
gosecG304 с confidence LOW на внутреннем инструментальном пути.- Trivy LOW в dev-пакете alpine без производственного влияния.
Не создаём ticket, не показываем в дашборде. Конфиг .golangci.yml фильтрует на уровне инструмента:
linters-settings:
gosec:
severity: high
confidence: high
issues:
max-issues-per-linter: 0
max-same-issues: 0
Suppressions имеют срок
R-SEC-FIND-2: два механизма — //nolint: для SAST, govuln-suppressions.json для CVE.
//nolint для gosec
// adapters/in/http/product_handler.go
//nolint:gosec // G304: путь берётся из cfg.TemplatePath (trusted config, не из запроса);
// заменить на embed.FS до: 2026-12-01
f, err := os.Open(cfg.TemplatePath)
if err != nil {
return fmt.Errorf("open template: %w", err)
}
defer f.Close()
Шаблон: //nolint:gosec // <код>: <обоснование ≥ 30 символов>; <план>; до: <YYYY-MM-DD>.
Blanket //nolint без кода нарушения и без даты — не принимается на review.
govuln-suppressions.json для CVE
{
"suppressions": [
{
"vuln": "GO-2024-3321",
"reason": "уязвимая функция parseOldFormat не вызывается в нашем коде-пути; используем только NewFormat",
"until": "2026-09-01"
}
]
}
Без поля "until" — suppression не засчитывается, CI считает finding открытым.
Квартальный аудит просроченных suppressions
#!/bin/bash
# tools/check-expired-suppressions.sh
NOW=$(date +%Y-%m-%d)
echo "=== просроченные //nolint ==="
grep -rn 'до:' . --include="*.go" | while IFS= read -r line; do
until=$(echo "$line" | grep -oP 'до: \K[\d-]+')
if [[ -n "$until" && "$until" < "$NOW" ]]; then
echo "EXPIRED: $line"
fi
done
echo "=== просроченные govuln-suppressions ==="
if [[ -f govuln-suppressions.json ]]; then
jq -r '.suppressions[] | select(.until < "'"$NOW"'") | "EXPIRED: \(.vuln) (\(.until))"' \
govuln-suppressions.json
fi
Запускать в CI weekly (schedule: cron: '0 9 * * 1'), постить issue в репо при наличии просроченных.
GitHub Code Scanning — SARIF
R-SEC-FIND-3: все инструменты экспортируют SARIF, загружают в GitHub Security tab.
# .github/workflows/pr.yml (фрагмент)
- name: Run gosec
run: gosec -fmt sarif -out gosec.sarif -severity HIGH ./...
- name: Upload gosec SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: gosec.sarif
# .github/workflows/main.yml (фрагмент)
- name: Scan image
run: |
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--format sarif \
--output trivy.sarif \
orders-service:${{ github.sha }}
- name: Upload Trivy SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif
В GitHub Security tab — фильтрация по severity, инструменту, файлу; tracking статуса (open / dismissed / fixed); уведомления. Без отдельной инфры типа SonarQube.
Baseline-механика на release
R-SEC-4: релиз блокируется только на новые findings.
{
"govulncheck": {
"knownVulns": [
{
"id": "GO-2024-3100",
"pkg": "golang.org/x/net",
"firstSeen": "2026-04-10"
}
]
}
}
При release tag CI сравнивает govulncheck -json ./... с baseline:
- Новый vuln ID — блокируем release, создаём ticket.
- Vuln уже в baseline — не блокируем, это управляемый долг.
Без baseline первый CVE в транзитивной зависимости останавливает pipeline на неделю — команда начинает игнорировать security в целом.
«Не уверен, эксплуатируется ли»
R-SEC-FIND-X1: главный антипаттерн.
Плохо — finding остаётся открытым, security team анализирует повторно на каждом PR:
// gosec G304, developer думает: "sqlc генерирует запросы, тут нет инъекции"
// ничего не делает
rows, err := db.QueryContext(ctx, fmt.Sprintf("SELECT * FROM orders WHERE id = %s", id))
Правильно — фикс (предпочтительно для sqlc/pgx):
// core/order/store.go — sqlc генерирует типизированные запросы, G304 не применим
order, err := q.GetOrder(ctx, orderID)
if err != nil {
return nil, fmt.Errorf("get order %d: %w", orderID, err)
}
Правильно — suppression, если фикс невозможен прямо сейчас:
// adapters/in/http/report_handler.go
//nolint:gosec // G304: путь из cfg.ReportDir (trusted config, не из request);
// refactor на embed.FS запланирован в Q4 2026; до: 2026-12-31
f, err := os.Open(filepath.Join(cfg.ReportDir, reportID+".pdf"))
if err != nil {
return fmt.Errorf("open report: %w", err)
}
defer f.Close()
Suppression — не плохо. Плохо — молчание: finding без разрешения накапливает долг без SLA.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
//nolint:gosec без кода нарушения | R-SEC-SAST-X1 | //nolint:gosec // G304: ... с обоснованием |
//nolint:gosec без даты до: | R-SEC-FIND-2 | добавить ; до: YYYY-MM-DD |
govuln-suppressions.json без "until" | R-SEC-FIND-2 | "until": "YYYY-MM-DD" обязательно |
| Игнорирование finding без suppression | R-SEC-FIND-X1 | suppression с обоснованием или фикс |
| CRITICAL не hotfix в 24 часа | R-SEC-FIND-1 | немедленный hotfix |
| HIGH в backlog «когда-нибудь» | R-SEC-FIND-1 | включить в текущий спринт (≤ 2 нед) |
| gosec/Trivy без SARIF-выгрузки | R-SEC-FIND-3 | upload-sarif в каждом CI-шаге |
| Baseline не обновляется на merge в main | R-SEC-4 | auto-update через CI |
Куда дальше
- SAST по коду — конфиг
.golangci.yml, gosec-правила, синтаксис//nolintсо сроком. - CVE в зависимостях —
govulncheck,govuln-suppressions.json, Renovate дляgo.mod. - Секреты в коде и истории — Gitleaks,
.pre-commit-config.yaml, rotate-first при утечке. - Container/image-уязвимости — Trivy, distroless base, digest-pinned образы.
- Криптография в коде —
crypto/rand, AES-GCM,bcryptcost ≥ 12, JWT-верификация.