Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cc38903
feat(sbom): mark only entry-point packages as directly attackable
reyreavman Sep 20, 2026
4aafd8d
test(sbom), docs(sbom): cover the attack surface split along the depe…
reyreavman Sep 20, 2026
dfa684b
fix(sbom): keep entry-point packages attackable when the image lists …
reyreavman Sep 20, 2026
efafd78
test(sbom): run merged product SBOMs through the checker in both formats
reyreavman Sep 25, 2026
abe8467
fix(sbom): reject indirect as a security function value
reyreavman Sep 25, 2026
0b4582f
fix(sbom): keep mutually dependent packages attackable when nothing p…
reyreavman Sep 25, 2026
d2ca3be
docs(sbom), test(sbom): stop advertising indirect as a security funct…
reyreavman Sep 25, 2026
1e17936
fix(sbom): regenerate cached SBOMs so every image gets the split atta…
reyreavman Sep 27, 2026
3c0d94d
fix(sbom): keep packages attackable when a service lists them as depe…
reyreavman Sep 27, 2026
111d742
fix(sbom): stop marking the wrong package attackable when merged SBOM…
reyreavman Sep 29, 2026
bd4f178
fix(sbom): reject `security_function: indirect` in the images `sbom m…
reyreavman Sep 29, 2026
53d45a3
fix(sbom): keep merge-internal prefixes out of vulnerability, composi…
reyreavman Sep 29, 2026
90e8ade
docs(sbom): state that sbom merge rejects out-of-domain GOST values
reyreavman Sep 29, 2026
981de73
refactor(sbom): split ref derivation and Tarjan traversal into helpers
reyreavman Sep 29, 2026
1431684
fix(sbom): keep every undeclared reference of a merged SBOM inside it…
reyreavman Sep 30, 2026
1fc3a83
fix(sbom): tell how to recover when a base or imported SBOM fails GOS…
reyreavman Sep 30, 2026
b08ceaa
fix(sbom): drop vulnerability references to packages no input declares
reyreavman Oct 1, 2026
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
7 changes: 4 additions & 3 deletions cmd/werf/sbom/merge/merge_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Two ISPRAS-defined output formats are supported:
- "container": hierarchical — each image becomes a top-level container component with nested packages.
- "oss": flat — all packages from all images are merged into a deduplicated flat list.

GOST properties (attack_surface, security_function) are aggregated bottom-up using the "yes > indirect > no" precedence rule.
GOST properties are aggregated bottom-up: attack_surface with the "yes > indirect > no" precedence rule, security_function with "yes > no". An image SBOM carrying a GOST value outside these domains, such as security_function "indirect" written by an older werf, is rejected; rebuild the image first.

The flags --input, --ispras-format, --app-name, --app-version and --manufacturer are required. The merged SBOM is written to stdout unless --output is given.`

Expand All @@ -23,8 +23,9 @@ The flags --input, --ispras-format, --app-name, --app-version and --manufacturer
"Two ISPRAS-defined output formats are supported:\n" +
"- `container`: hierarchical — each image becomes a top-level container component with nested packages.\n" +
"- `oss`: flat — all packages from all images are merged into a deduplicated flat list.\n\n" +
"GOST properties (`attack_surface`, `security_function`) are aggregated bottom-up using " +
"the `yes > indirect > no` precedence rule.\n\n" +
"GOST properties are aggregated bottom-up: `attack_surface` with the `yes > indirect > no` " +
"precedence rule, `security_function` with `yes > no`. An image SBOM carrying a GOST value outside these domains, " +
"such as `security_function: indirect` written by an older werf, is rejected; rebuild the image first.\n\n" +
"The flags `--input`, `--ispras-format`, `--app-name`, `--app-version` and `--manufacturer` " +
"are required. The merged SBOM is written to stdout unless `--output` is given."

Expand Down
4 changes: 2 additions & 2 deletions docs/_data/werf_yaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ sections:
value: "string"
default: "yes"
description:
en: "Security function property (yes | no | indirect). Default: yes"
ru: "Свойство функции безопасности (yes | no | indirect). По умолчанию: yes"
en: "Security function property (yes | no). Default: yes"
ru: "Свойство функции безопасности (yes | no). По умолчанию: yes"
- &image-section-sbom
name: sbom
description:
Expand Down
2 changes: 1 addition & 1 deletion docs/_includes/reference/cli/werf_sbom_merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Two ISPRAS-defined output formats are supported:
- `container`: hierarchical — each image becomes a top-level container component with nested packages.
- `oss`: flat — all packages from all images are merged into a deduplicated flat list.

GOST properties (`attack_surface`, `security_function`) are aggregated bottom-up using the `yes > indirect > no` precedence rule.
GOST properties are aggregated bottom-up: `attack_surface` with the `yes > indirect > no` precedence rule, `security_function` with `yes > no`. An image SBOM carrying a GOST value outside these domains, such as `security_function: indirect` written by an older werf, is rejected; rebuild the image first.

The flags `--input`, `--ispras-format`, `--app-name`, `--app-version` and `--manufacturer` are required. The merged SBOM is written to stdout unless `--output` is given.

Expand Down
8 changes: 5 additions & 3 deletions docs/pages_en/usage/build/sbom.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,12 @@ When building a multi-platform image, werf generates a separate SBOM artifact fo

## GOST security properties (`sbom.gost`)

To comply with GOST safety standards, you can configure mandatory security properties for all components in the SBOM. These properties will be injected into all direct components of the final SBOM. By default, both generated and user-defined SBOMs are enriched with `attackSurface=yes` and `securityFunction=yes`, unless specified otherwise at the project (meta) or image level.
To comply with GOST safety standards, you can configure mandatory security properties for all components in the SBOM. These properties will be injected into the whole component tree of the final SBOM. By default, both generated and user-defined SBOMs are enriched with `attackSurface=yes` and `securityFunction=yes`, unless specified otherwise at the project (meta) or image level.

1. `attackSurface`: The attack surface property (`yes` | `no` | `indirect`).
2. `securityFunction`: The security function property (`yes` | `no` | `indirect`).
2. `securityFunction`: The security function property (`yes` | `no`).

`attackSurface: yes` follows the dependency tree recorded in the SBOM `dependencies` section: it lands on the components nothing else depends on, and every component pulled in by another one is recorded as `indirect`. When the catalogers of an ecosystem report no dependency tree at all, every component is a root and receives `yes`. `no` and `indirect`, and `securityFunction` in all cases, apply unchanged to the whole tree.

You can define these globally in `build.sbom.gost` or per-image in `image.sbom.gost`. Image-level configuration overrides global configuration.

Expand Down Expand Up @@ -175,6 +177,6 @@ Changing GOST properties (`sbom.gost`) does not affect stage digests. Cached sta

[`werf sbom get`]({{ "/reference/cli/werf_sbom_get.html" | true_relative_url }}) retrieves the SBOM for an image described in `werf.yaml` and prints it to stdout. The SBOM is read as an OCI artifact from the container registry, so `--repo` is required. When invoked with an image name, the command runs the standard werf build conveyor: missing stages and SBOM artifacts are created, just like with `werf build` (with the `--require-built-images` flag the command fails instead). You can select a specific version with `--tag` or `--digest` (mutually exclusive) — in this mode the command only downloads the ready-made SBOM and fails if it is not found.

[`werf sbom merge`]({{ "/reference/cli/werf_sbom_merge.html" | true_relative_url }}) assembles a product-level SBOM from several per-image SBOMs. It takes a JSON file that maps image names to sha256 digests, pulls the individual SBOMs from the registry, and merges them into a single CycloneDX document with dependency graphs preserved. Two ISPRAS output formats are available: `container` (hierarchical, each image becomes a top-level component with nested packages) and `oss` (flat, all packages deduplicated into one list). GOST `attack_surface` and `security_function` properties are aggregated bottom-up with the precedence `yes > indirect > no`.
[`werf sbom merge`]({{ "/reference/cli/werf_sbom_merge.html" | true_relative_url }}) assembles a product-level SBOM from several per-image SBOMs. It takes a JSON file that maps image names to sha256 digests, pulls the individual SBOMs from the registry, and merges them into a single CycloneDX document with dependency graphs preserved. Two ISPRAS output formats are available: `container` (hierarchical, each image becomes a top-level component with nested packages) and `oss` (flat, all packages deduplicated into one list). GOST properties are aggregated bottom-up: `attack_surface` with the precedence `yes > indirect > no`, `security_function` with `yes > no`. An image SBOM carrying a GOST value outside these domains — `security_function: indirect` written by an older werf, for instance — is rejected; rebuild the image first.

[`werf sbom validate`]({{ "/reference/cli/werf_sbom_validate.html" | true_relative_url }}) checks a CycloneDX JSON file against ISPRAS schemas. It runs sbom-checker inside a Docker container and reports any violations, split into errors and warnings, with both counts shown in the summary. By default any error or warning fails the validation; pass `--warnings-non-fatal` to keep warnings informational (printed on stderr) so that only errors set a non-zero exit code. Both `oss` and `container` SBOM types are supported.
8 changes: 5 additions & 3 deletions docs/pages_ru/usage/build/sbom.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,12 @@ werf всегда использует индекс на основе тегов

## Свойства безопасности ГОСТ (`sbom.gost`)

Для соответствия стандартам безопасности ГОСТ можно настроить обязательные свойства безопасности для всех компонентов в SBOM. Эти свойства будут внедрены во все прямые компоненты итогового SBOM. По умолчанию как генерируемый, так и определяемый пользователем SBOM-ы обогащаются значениями `attackSurface=yes` и `securityFunction=yes`, если не задано иное через настройки проекта (meta-уровень) или конкретного образа (image-уровень).
Для соответствия стандартам безопасности ГОСТ можно настроить обязательные свойства безопасности для всех компонентов в SBOM. Эти свойства будут внедрены во всё дерево компонентов итогового SBOM. По умолчанию как генерируемый, так и определяемый пользователем SBOM-ы обогащаются значениями `attackSurface=yes` и `securityFunction=yes`, если не задано иное через настройки проекта (meta-уровень) или конкретного образа (image-уровень).

1. `attackSurface`: Свойство поверхности атаки (`yes` | `no` | `indirect`).
2. `securityFunction`: Свойство функции безопасности (`yes` | `no` | `indirect`).
2. `securityFunction`: Свойство функции безопасности (`yes` | `no`).

`attackSurface: yes` следует дереву зависимостей из секции `dependencies`: значение получают компоненты, от которых ничто не зависит, а каждый компонент, который потянул за собой другой, записывается как `indirect`. Если каталогеры экосистемы вообще не сообщают дерево зависимостей, корнями считаются все компоненты и каждый получает `yes`. Значения `no` и `indirect`, а также `securityFunction` в любом случае, применяются ко всему дереву без изменений.

Эти свойства можно определить глобально в `build.sbom.gost` или для конкретного образа в `image.sbom.gost`. Конфигурация на уровне образа переопределяет глобальную конфигурацию.

Expand Down Expand Up @@ -175,6 +177,6 @@ WERF_EXTERNAL_REFS_SERVER_URL env var is required

[`werf sbom get`]({{ "/reference/cli/werf_sbom_get.html" | true_relative_url }}) получает SBOM образа, описанного в `werf.yaml`, и выводит его в stdout. SBOM читается как OCI-артефакт из container registry, поэтому флаг `--repo` обязателен. При обращении по имени образа команда запускает стандартный сборочный конвейер werf: отсутствующие стадии и SBOM-артефакты досоздаются, как при `werf build` (с флагом `--require-built-images` команда вместо этого завершается ошибкой). Для выбора конкретной версии поддерживаются флаги `--tag` и `--digest` (взаимоисключающие) — в этом режиме команда только выгружает готовый SBOM и завершается ошибкой, если он не найден.

[`werf sbom merge`]({{ "/reference/cli/werf_sbom_merge.html" | true_relative_url }}) собирает SBOM уровня продукта из нескольких пообразных SBOM. На вход принимается JSON-файл с соответствием «имя образа → sha256-дайджест»; команда скачивает отдельные SBOM из registry и объединяет их в один CycloneDX-документ с сохранением графов зависимостей. Поддерживаются два выходных формата ИСПРАС: `container` (иерархический, каждый образ становится компонентом верхнего уровня с вложенными пакетами) и `oss` (плоский, все пакеты дедуплицируются в один список). Свойства ГОСТ `attack_surface` и `security_function` агрегируются снизу вверх по приоритету `yes > indirect > no`.
[`werf sbom merge`]({{ "/reference/cli/werf_sbom_merge.html" | true_relative_url }}) собирает SBOM уровня продукта из нескольких пообразных SBOM. На вход принимается JSON-файл с соответствием «имя образа → sha256-дайджест»; команда скачивает отдельные SBOM из registry и объединяет их в один CycloneDX-документ с сохранением графов зависимостей. Поддерживаются два выходных формата ИСПРАС: `container` (иерархический, каждый образ становится компонентом верхнего уровня с вложенными пакетами) и `oss` (плоский, все пакеты дедуплицируются в один список). Свойства ГОСТ агрегируются снизу вверх: `attack_surface` по приоритету `yes > indirect > no`, `security_function` — `yes > no`. SBOM образа со значением ГОСТ вне этих доменов — например, `security_function: indirect`, записанным старой версией werf, — отклоняется; такой образ нужно сначала пересобрать.

[`werf sbom validate`]({{ "/reference/cli/werf_sbom_validate.html" | true_relative_url }}) проверяет CycloneDX JSON-файл по схемам ИСПРАС. Команда запускает sbom-checker в Docker-контейнере и выводит обнаруженные нарушения, разделяя их на ошибки и предупреждения, а в сводке показывает оба счётчика. По умолчанию валидацию проваливают и ошибки, и предупреждения; передайте `--warnings-non-fatal`, чтобы предупреждения оставались информационными (выводятся в stderr) и на код возврата влияли только ошибки. Поддерживаются типы SBOM `oss` и `container`.
14 changes: 4 additions & 10 deletions pkg/build/sbom_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ func (step *sbomStep) scanCatalogerDir(ctx context.Context, scanOpts scanner.Sca
return bom, nil
}

const sbomArtifactFormatVersion = "5"
const sbomArtifactFormatVersion = "6"

// calculateStableChecksum computes the SBOM artifact cache checksum. Together with the
// parent stage digest it forms the cache key: a previously attached SBOM is reused only
Expand Down Expand Up @@ -388,7 +388,7 @@ func (step *sbomStep) prepareGostComponents(ctx context.Context, mergeOpts *cycl
})
}

// Skip GOST validation and upsert for base/import BOMs when GOST is not configured.
// Skip GOST validation for base/import BOMs when GOST is not configured.
// Without this guard, components from patchers (e.g. PM BOMPatcher) that lack GOST
// properties would fail validation even though GOST is not in use.
if mergeOpts.Gost.AttackSurface.IsUndefined() && mergeOpts.Gost.SecurityFunction.IsUndefined() {
Expand All @@ -397,19 +397,13 @@ func (step *sbomStep) prepareGostComponents(ctx context.Context, mergeOpts *cycl

if mergeOpts.BaseBOM != nil {
if err := gost.Validate(mergeOpts.BaseBOM); err != nil {
return fmt.Errorf("base SBOM validation failed: %w", err)
}
if err := gost.Upsert(mergeOpts.BaseBOM, mergeOpts.Gost); err != nil {
return fmt.Errorf("set GOST properties for base SBOM: %w", err)
return fmt.Errorf("base SBOM validation failed (rebuild the base image with the current werf if its SBOM was built by an older one): %w", err)
}
}

for i, externalBOM := range mergeOpts.ImportBOMs {
if err := gost.Validate(externalBOM); err != nil {
return fmt.Errorf("external SBOM [%d] validation failed: %w", i, err)
}
if err := gost.Upsert(externalBOM, mergeOpts.Gost); err != nil {
return fmt.Errorf("set GOST properties for external SBOM [%d]: %w", i, err)
return fmt.Errorf("external SBOM [%d] validation failed (rebuild the imported image with the current werf if its SBOM was built by an older one): %w", i, err)
}
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/config/raw_gost.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ func (g *rawGost) UnmarshalYAML(unmarshal func(interface{}) error) error {
}

func (g *rawGost) validate() error {
if g.AttackSurface != nil && !gost.IsValidGostValue(*g.AttackSurface) {
if g.AttackSurface != nil && !gost.IsValidAttackSurfaceValue(*g.AttackSurface) {
return newDetailedConfigError(fmt.Sprintf("invalid 'attackSurface' value %q: expected 'yes', 'no' or 'indirect'", *g.AttackSurface), nil, g.doc)
}
if g.SecurityFunction != nil && !gost.IsValidGostValue(*g.SecurityFunction) {
return newDetailedConfigError(fmt.Sprintf("invalid 'securityFunction' value %q: expected 'yes', 'no' or 'indirect'", *g.SecurityFunction), nil, g.doc)
if g.SecurityFunction != nil && !gost.IsValidSecurityFunctionValue(*g.SecurityFunction) {
return newDetailedConfigError(fmt.Sprintf("invalid 'securityFunction' value %q: expected 'yes' or 'no'", *g.SecurityFunction), nil, g.doc)
}
return nil
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/config/raw_gost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,11 @@ var _ = Describe("rawGost", func() {
},
gost.Config{},
HaveOccurred()),
Entry("indirect is rejected for the security function",
map[string]interface{}{
"securityFunction": "indirect",
},
gost.Config{},
MatchError(ContainSubstring("expected 'yes' or 'no'"))),
)
})
12 changes: 11 additions & 1 deletion pkg/config/werf_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ build:
standard: cyclonedx@1.6
gost:
attackSurface: "yes"
securityFunction: indirect
securityFunction: no
deploy:
helmChartDir: .helm
helmChartConfig:
Expand Down Expand Up @@ -396,6 +396,16 @@ project: app
build:
sbom:
standard: cyclonedx@1.6
`),
Entry("indirect security function", `
configVersion: 1
project: app
build:
sbom:
enable: true
standard: cyclonedx@1.6
gost:
securityFunction: indirect
`),
Entry("os-pm packages with workdir", `
image: app
Expand Down
Loading
Loading