Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/trivy-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: Trivy scan
# Сканирование образа, файловой системы и Dockerfile.
# Отчёты загружаются в GitHub Security (SARIF), а находки уровня CRITICAL/HIGH
# роняют прогон: это «ворота», а не просто отчёт.
# В SARIF попадают те же уровни, что видят «ворота» (limit-severities-for-sarif):
# иначе в Security копились бы находки ниже уровня ворот (LOW/UNKNOWN), которых
# сам прогон не замечает, и отчёт расходился бы с проверкой.
on:
push:
branches: [main]
Expand Down Expand Up @@ -60,6 +63,10 @@ jobs:
output: ${{ matrix.sarif }}
severity: CRITICAL,HIGH
ignore-unfixed: true
# Формат SARIF по умолчанию игнорирует severity: action снимает TRIVY_SEVERITY
# и кладёт в отчёт все находки, поэтому в GitHub Security попадало то, чего
# «ворота» не видят. Вход удерживает уровни отчёта теми же, что у «ворот».
limit-severities-for-sarif: true
exit-code: "0"
- name: Загрузить отчёт в GitHub Security
# В PR из форка токен не имеет права писать в Security: загрузку пропускаем,
Expand Down Expand Up @@ -123,6 +130,10 @@ jobs:
format: sarif
output: trivy-fs.sarif
severity: CRITICAL,HIGH
# Как и у «ворот» ниже, в отчёт не попадает то, для чего ещё нет исправления;
# limit-severities-for-sarif удерживает уровни отчёта теми же, что у «ворот».
ignore-unfixed: true
limit-severities-for-sarif: true
exit-code: "0"
- name: Загрузить отчёт в GitHub Security
if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
Expand Down
3 changes: 2 additions & 1 deletion .trivyignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Исключения для «ворот» Trivy: находки во встроенном в образ сканере
# (usr/local/bin/trivy — копия из aquasec/trivy:0.74.0), а не в коде cio.
# Наш бинарь (usr/local/bin/cio) и пакеты alpine проверяются без исключений,
# находки по ним остаются видимыми в отчёте SARIF (GitHub Security).
# находки уровня CRITICAL/HIGH по ним остаются видимыми в отчёте SARIF
# (GitHub Security): в отчёт попадают те же уровни, что видят «ворота».
# Записи уходят вместе с обновлением версии Trivy (Dependabot: .github/dependabot.yml);
# срок годности (exp) заставляет перепроверить исключения, а не держать их вечно.
CVE-2026-53495 exp:2026-12-31
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ GitHub Actions (`.github/workflows`):
| Workflow | Что проверяет |
| --- | --- |
| `ci.yml` | для каждого инструмента (`cio` и `sqlbrc`): линт (`golangci-lint` с правилами безопасности), достижимые уязвимости зависимостей (`govulncheck`), тесты (`go vet`, `go test -race`, покрытие в артефакте) и сборка бинаря; отдельный job собирает образ каждого инструмента и проверяет, что он запускается под непривилегированным пользователем (`cio`, `sqlbrc`) |
| `trivy-scan.yml` | уязвимости, секреты и конфигурацию образов обоих инструментов (`cio`, `sqlbrc`) и их файловой системы; находки CRITICAL/HIGH роняют прогон, отчёты уходят в GitHub Security |
| `trivy-scan.yml` | уязвимости, секреты и конфигурацию образов обоих инструментов (`cio`, `sqlbrc`) и их файловой системы; находки CRITICAL/HIGH (без исправления) роняют прогон, и те же уровни уходят в GitHub Security |
| `codeql.yml` | статический анализ Go-кода обоих модулей (`cio`, `sqlbrc`), отчёты — в GitHub Security |

Матрицы workflow перечисляют каталоги инструментов (`cio`, `sqlbrc`): новый инструмент попадает
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
- Статический анализ по каждому модулю: `golangci-lint` с набором `gosec`, CodeQL и
`govulncheck` (достижимые уязвимости зависимостей).
- Сканирование: Trivy (файловая система, Dockerfile, образ каждого инструмента) в CI; находки
CRITICAL/HIGH роняют прогон, отчёты — в GitHub Security.
CRITICAL/HIGH (без исправления) роняют прогон, и те же уровни уходят в GitHub Security
(отчёт не показывает находки ниже уровня «ворот»).
- Секреты: проверка всей истории репозитория `gitleaks`; правило `secret-in-layer` ищет
секреты в истории сборки образа. В тестовых данных — только фиктивные значения.
- Релиз: SBOM и attestation сборки рядом с бинарями и образом, подпись образа cosign
Expand Down
5 changes: 4 additions & 1 deletion SQL backup restore clone/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

- образ `sqlbrc` собирался без даты сборки: `release-image.yml` не передавал `--build-arg DATE`,
поэтому `sqlbrc --version` из образа печатал «сборка unknown», и сверить контейнер с бинарём
из релиза было нечем. Дата подставляется так же, как в релизных бинарях.
из релиза было нечем. Дата подставляется так же, как в релизных бинарях;
- число активных подключений из вывода `psql` разбирается без сужения `int64` до `int`: значение
вне диапазона `int` трактуется как 0, а не обрезается (CodeQL `go/incorrect-integer-conversion`).
Обрезание дало бы бессмысленный счётчик в списке баз и в сообщениях о завершении подключений.

### Изменилось

Expand Down
16 changes: 14 additions & 2 deletions SQL backup restore clone/internal/pg/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ func (c *Client) ListDatabases(ctx context.Context) ([]Database, error) {
SizeBytes: parseInt64(row.Field(6)),
AllowConn: parseBool(row.Field(7)),
IsTemplate: parseBool(row.Field(8)),
Connections: int(parseInt64(row.Field(9))),
Connections: parseCount(row.Field(9)),
})
}
return databases, nil
Expand Down Expand Up @@ -396,11 +396,23 @@ func parseBool(value string) bool {
}

// parseInt64 переводит значение в число; мусор трактуется как 0, потому что
// размер БД и число подключений — справочные поля.
// размер БД — справочное поле.
func parseInt64(value string) int64 {
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return 0
}
return parsed
}

// parseCount переводит значение из psql в счётчик (число подключений, строк):
// мусор, отрицательное число и переполнение int трактуются как 0. Сужать int64
// до int нельзя: значение приходит извне, и обрезание дало бы бессмысленный
// счётчик вместо отказа.
func parseCount(value string) int {
parsed, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil || parsed < 0 {
return 0
}
return parsed
}
28 changes: 28 additions & 0 deletions SQL backup restore clone/internal/pg/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,31 @@ func TestClientExecLogsStderr(t *testing.T) {
t.Errorf("без stderr вывод не попал в журнал: %q", log.String())
}
}

// TestParseCount проверяет разбор счётчика из вывода psql: число приходит извне,
// поэтому значение вне диапазона int трактуется как 0, а не обрезается сужением
// int64 до int (CodeQL: go/incorrect-integer-conversion).
func TestParseCount(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{name: "ноль", input: "0", want: 0},
{name: "подключения", input: "3", want: 3},
{name: "пробелы вокруг числа", input: " 12 ", want: 12},
{name: "отрицательное", input: "-1", want: 0},
{name: "пустой ввод", input: "", want: 0},
{name: "не число", input: "мусор", want: 0},
{name: "дробное", input: "1e3", want: 0},
{name: "переполнение int64", input: "99999999999999999999999", want: 0},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := parseCount(test.input); got != test.want {
t.Errorf("parseCount(%q) = %d, ожидалось %d", test.input, got, test.want)
}
})
}
}
4 changes: 2 additions & 2 deletions SQL backup restore clone/internal/pg/databases.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (c *Client) Sessions(ctx context.Context, database, pattern string) (int, e
if len(rows) == 0 {
return 0, nil
}
return int(parseInt64(rows[0].Field(0))), nil
return parseCount(rows[0].Field(0)), nil
}

// TerminateBackends завершает активные подключения к базе и возвращает их число.
Expand All @@ -97,7 +97,7 @@ func (c *Client) TerminateBackends(ctx context.Context, database, pattern string
if len(rows) == 0 {
return 0, nil
}
return int(parseInt64(rows[0].Field(0))), nil
return parseCount(rows[0].Field(0)), nil
}

// CreateDatabase создаёт базу с заданными параметрами.
Expand Down
36 changes: 35 additions & 1 deletion SQL backup restore clone/internal/pg/parse_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package pg

import "testing"
import (
"strconv"
"strings"
"testing"
)

// FuzzParsePgVersion проверяет разбор версий: строка приходит из вывода внешней
// утилиты PostgreSQL, поэтому паник быть не должно ни на каком вводе.
Expand Down Expand Up @@ -84,3 +88,33 @@ func FuzzParseRestoreList(f *testing.F) {
}
})
}

// FuzzParseCount проверяет разбор счётчика из вывода psql: значение приходит извне,
// поэтому результат обязан быть неотрицательным, а корректное число — вернуться без
// искажений: сужение int64 до int скрыло бы переполнение и дало бессмысленный счётчик.
func FuzzParseCount(f *testing.F) {
seeds := []string{
"", " ", "0", "1", "3", " 12 ", "-1", "мусор", "1e3", "0x10",
"2147483647", "2147483648", "9223372036854775807", "99999999999999999999999",
}
for _, seed := range seeds {
f.Add(seed)
}

f.Fuzz(func(t *testing.T, value string) {
count := parseCount(value)
if count < 0 {
t.Errorf("отрицательный счётчик %d для %q", count, value)
}

expected, err := strconv.Atoi(strings.TrimSpace(value))
switch {
case err != nil, expected < 0:
if count != 0 {
t.Errorf("некорректное значение %q дало %d, ожидался 0", value, count)
}
case count != expected:
t.Errorf("parseCount(%q) = %d, ожидалось %d", value, count, expected)
}
})
}
Loading