Опирается на правила:
R-SEC-SAST-1…R-SEC-SAST-4иR-SEC-SAST-X1из Security Style Guide → раздел 1. SAST по коду.
Важно знать
golangci-lintс набором gosec+staticcheck+errcheck+errorlint+bodyclose обязателен — конфиг в.golangci.yml, не флагами в CI-скрипте.gosecловит SQLi, path traversal, hardcoded credentials, weak crypto, unsafe package, command injection, SSRF,math/randв security-контексте (G404).- HIGH/CRITICAL breaking build:
golangci-lint runбез--exit-code 0,gosec -severity HIGHвозвращает exit code 1.semgrepс go-security rulesets дополняет gosec — пересекаются слабо, оба обязательны.- SARIF из gosec и semgrep загружается в GitHub Security tab через
codeql-action/upload-sarif.- Suppressions через
//nolint:gosecс кодом нарушения, причиной ≥ 30 символов и датойдо: YYYY-MM-DD— без даты не принимается.- Blanket
//nolintбез кода и объяснения — запрещён (R-SEC-SAST-X1).
SAST (Static Application Security Testing) — анализ кода без выполнения. В Go-экосистеме инструментальная цепочка другая, чем в Java, но принципы R-SEC-* одни: сборка падает на HIGH/CRITICAL, suppressions имеют срок, findings идут в единый дашборд. golangci-lint объединяет несколько анализаторов под одним вызовом; gosec — специализированный security-линтер с детектами класса SQLi, path traversal, weak crypto.
Конфигурация golangci-lint
R-SEC-SAST-1: конфиг в корне репо, не флаги в CI.
# .golangci.yml
linters:
enable:
- gosec
- staticcheck
- errcheck
- errorlint
- bodyclose
- exhaustive
linters-settings:
gosec:
severity: high
confidence: high
issues:
max-issues-per-linter: 0
max-same-issues: 0
max-issues-per-linter: 0 и max-same-issues: 0 — без этих флагов golangci-lint обрезает вывод по умолчанию, реальная картина скрыта. В CI — без --exit-code 0:
# .github/workflows/pr.yml (фрагмент)
- name: SAST — golangci-lint
run: golangci-lint run --max-issues-per-linter=0 --max-same-issues=0
Каждый enabled-линтер при finding возвращает exit code 1 → PR-гейт красный.
gosec — security-specific детекты
R-SEC-SAST-2: gosec запускается отдельно с SARIF-выводом для GitHub Code Scanning.
# CI step — PR gate
gosec -fmt sarif -out gosec.sarif -severity HIGH ./...
Что ловит gosec на домене Order/Product/Customer:
G101 — hardcoded credentials
// ПЛОХО — gosec G101
func connectDB() *sql.DB {
db, _ := sql.Open("postgres", "host=localhost password=secret123 dbname=orders")
return db
}
// ХОРОШО
func connectDB(cfg Config) (*sql.DB, error) {
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
return db, nil
}
G201/G202 — SQL-инъекция через конкатенацию строк
// ПЛОХО — gosec G202: конкатенация в SQL-запросе
func (r *OrderRepository) FindByStatus(ctx context.Context, status string) ([]Order, error) {
query := "SELECT id, total FROM orders WHERE status = '" + status + "'"
rows, err := r.db.QueryContext(ctx, query)
// ...
}
// ХОРОШО — sqlc генерирует параметризованные запросы
// query.sql
-- name: FindOrdersByStatus :many
SELECT id, total, created_at FROM orders WHERE status = $1;
С sqlc/pgx конкатенация в SQL-запросах структурно невозможна — генерация заменяет ручной SQL. gosec G201/G202 при этом молчит.
G304 — path traversal
// ПЛОХО — gosec G304: путь от пользователя
func (h *ProductHandler) GetTemplate(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
data, err := os.ReadFile("templates/" + name)
// ...
}
// ХОРОШО — белый список допустимых имён
func (h *ProductHandler) GetTemplate(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
allowed := map[string]bool{"invoice": true, "receipt": true, "catalog": true}
if !allowed[name] {
http.Error(w, "not found", http.StatusNotFound)
return
}
data, err := os.ReadFile(filepath.Join("templates", name+".html"))
// ...
}
G401/G501 — weak crypto
// ПЛОХО — gosec G501: MD5 для security
import "crypto/md5"
func hashCustomerID(id string) string {
h := md5.Sum([]byte(id))
return hex.EncodeToString(h[:])
}
// ХОРОШО — MD5 допустим для non-security (content-id, ETag), не для паролей
// Для паролей — bcrypt (R-SEC-CRYPTO-1):
import "golang.org/x/crypto/bcrypt"
const bcryptCost = 12
func HashPassword(plain string) (string, error) {
b, err := bcrypt.GenerateFromPassword([]byte(plain), bcryptCost)
if err != nil {
return "", fmt.Errorf("hash password: %w", err)
}
return string(b), nil
}
G404 — math/rand в security-контексте
// ПЛОХО — gosec G404: math/rand для токена сброса пароля
import "math/rand"
func GenerateResetToken() string {
return fmt.Sprintf("%d", rand.Intn(1000000))
}
// ХОРОШО — crypto/rand (R-SEC-CRYPTO-2)
import "crypto/rand"
func GenerateResetToken(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generate token: %w", err)
}
return base64.URLEncoding.EncodeToString(b), nil
}
semgrep — дополнительный слой
R-SEC-SAST-2: semgrep с go-security rulesets дополняет gosec — пересечение детектов небольшое, совместное применение даёт более полное покрытие.
# CI step — PR gate
semgrep --config "p/golang" --sarif --output semgrep.sarif ./...
Полезные go-правила semgrep, которые gosec пропускает:
- SSRF через
http.Get(userInput)без валидации URL. - Открытые перенаправления (
http.Redirect(w, r, userParam, 302)). - Небезопасное использование
html/templatevstext/template(XSS). exec.Commandс пользовательским вводом без санитизации.
errcheck и errorlint
R-SEC-SAST-1: errcheck обязателен — Go-специфичная проблема: проигнорированная ошибка от security-операции.
// ПЛОХО — errcheck: игнорирование ошибки bcrypt
func (s *CustomerService) Register(ctx context.Context, cmd RegisterCustomerCommand) error {
hash, _ := bcrypt.GenerateFromPassword([]byte(cmd.Password), 12) // ошибка проглочена
return s.repo.Save(ctx, Customer{PasswordHash: string(hash)})
}
// ХОРОШО
func (s *CustomerService) Register(ctx context.Context, cmd RegisterCustomerCommand) error {
hash, err := bcrypt.GenerateFromPassword([]byte(cmd.Password), 12)
if err != nil {
return fmt.Errorf("hash password: %w", err)
}
return s.repo.Save(ctx, Customer{PasswordHash: string(hash)})
}
errorlint ловит сравнение ошибок через == вместо errors.Is — критично при обёрнутых ошибках (%w):
// ПЛОХО — errorlint: теряет wrapped error
if err == ErrOrderNotFound {
return http.StatusNotFound
}
// ХОРОШО
if errors.Is(err, ErrOrderNotFound) {
return http.StatusNotFound
}
Severity → действие
R-SEC-SAST-3: разная реакция по уровню.
| Severity | Инструмент | Действие |
|---|---|---|
| HIGH/CRITICAL | gosec, semgrep, staticcheck | Сборка падает (exit code 1) |
| MEDIUM | gosec | Отчёт + обязательный комментарий ревьюера |
| LOW | — | Игнорируется в продакшн-конфиге |
golangci-lint run без --exit-code 0 возвращает 1 при любом enabled-finding. Не подавлять через --exit-code 0 — это превращает SAST в дашборд, который никто не смотрит (нарушение R-SEC-1).
Suppressions со сроком
R-SEC-SAST-4: //nolint:gosec с кодом нарушения, причиной и датой.
// ХОРОШО — код + причина + срок
//nolint:gosec // G304: путь пришёл от trusted-конфига (embed.FS fallback), не от пользователя; заменить на embed.FS до: 2026-12-01
f, err := os.Open(cfg.TemplatePath)
if err != nil {
return nil, fmt.Errorf("open template %s: %w", cfg.TemplatePath, err)
}
// ПЛОХО — blanket //nolint без кода (R-SEC-SAST-X1)
//nolint
f, err := os.Open(cfg.TemplatePath)
Квартальный аудит просроченных suppressions:
grep -rn 'до:' . --include='*.go' | grep -v '_test.go'
Для MD5 в non-security контексте (content-id, ETag, не пароль) — suppression с явным объяснением:
//nolint:gosec // G401: sha1 используется только для content-id (ETag, не пароль), не как KDF; crypto/sha256 уже применяется там где нужна криптостойкость; до: 2026-12-01
h := sha1.New()
SARIF и GitHub Code Scanning
R-SEC-FIND-3: SARIF из gosec и semgrep загружается в GitHub Security tab.
# .github/workflows/pr.yml
- name: SAST — gosec
run: gosec -fmt sarif -out gosec.sarif -severity HIGH ./...
- name: SAST — semgrep
run: semgrep --config "p/golang" --sarif --output semgrep.sarif ./...
- name: Upload SARIF — gosec
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: gosec.sarif
- name: Upload SARIF — semgrep
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
Единый дашборд без отдельной инфры. Findings видны в Security tab → Code scanning alerts с фильтрацией по инструменту, severity, статусу.
Где SAST не помогает
SAST имеет фундаментальные ограничения:
- Не понимает runtime-значения:
exec.Command("bash", userInput)— детектируется, но еслиuserInputприходит через несколько слоёв — может не отследить. - False positives на доверенных путях (
cfg.TemplatePath) — нужны suppressions с обоснованием. - Не покрывает бизнес-логику: ABAC bypass, race conditions на уровне сервисного слоя.
SAST — первый слой. Дополняется:
- CVE в зависимостях (
govulncheck, Trivy) — CVE в зависимостях. - Секреты в истории (Gitleaks) — Секреты в коде и истории.
- Container-сканирование (Trivy image) — Container/image-уязвимости.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
//nolint без кода нарушения и причины ≥ 30 символов | R-SEC-SAST-X1 | //nolint:gosec // G304: причина; до: YYYY-MM-DD |
Suppression без даты до: | R-SEC-SAST-4 | дата пересмотра обязательна |
golangci-lint run --exit-code 0 в CI | R-SEC-SAST-3 | без флага — exit code 1 на finding |
| MEDIUM-finding без комментария ревьюера | R-SEC-SAST-3 | обязательный комментарий в PR |
gosec без -severity HIGH | R-SEC-SAST-1 | severity-порог явный |
| Конфиг линтера флагами в CI-скрипте | R-SEC-SAST-1 | .golangci.yml в корне репо |
max-issues-per-linter не выставлен в 0 | R-SEC-SAST-1 | реальная картина findings скрыта |
| Без SARIF-вывода | R-SEC-SAST-2 | gosec -fmt sarif + upload-sarif action |
| SQL-запрос через конкатенацию строк | R-SEC-SAST-2 | sqlc-генерация, параметризованные запросы |
math/rand для токенов/паролей | R-SEC-SAST-2 | crypto/rand |
Куда дальше
- CVE в зависимостях —
govulncheckи Trivy, следующий слой защиты. - Секреты в коде и истории — Gitleaks pre-commit и full-history сканирование.
- Container/image-уязвимости — Trivy image, distroless, non-root user.
- Криптография в коде — bcrypt, AES-GCM,
crypto/rand, JWT с JWKS keyfunc. - Реакция на findings — severity → SLA, квартальный аудит suppressions.