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
5 changes: 0 additions & 5 deletions docs/_data/werf_yaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,6 @@ sections:
en: "/usage/build/process.html#scanning-and-generation-of-sbom-artifacts-experimental"
ru: "/usage/build/process.html#сканирование-и-генерация-sbom-артефактов-experimental"
directives:
- name: fragment
value: "string"
description:
en: "Additional SBOM data in CycloneDX format"
ru: "Дополнительные данные SBOM в формате CycloneDX"
- <<: *gost-directive
- name: imageSpec
description:
Expand Down
31 changes: 0 additions & 31 deletions docs/pages_en/usage/build/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,37 +699,6 @@ build:
standard: cyclonedx@1.6
```

### Per-image configuration (`sbom.fragment`)

Optionally you can provide additional SBOM data for each image via the `sbom.fragment` property. This can be used to manually include components that are not automatically detected by the scanner.

`sbom.fragment` must be a YAML CycloneDX@1.6 document or a partial fragment (e.g., only the `components:` section). werf will build a full CycloneDX@1.6 BOM document by combining the scan results with this fragment.

```yaml
project: werf-sbom-base-image-example
configVersion: 1
build:
sbom:
enable: true
standard: cyclonedx@1.6
---
image: base-image
from: registry.werf.io/werf/scratch:latest
sbom:
fragment: |
components:
- type: library
name: openssl
version: "3.0.0"
purl: pkg:generic/openssl@3.0.0
licenses:
- license:
id: Apache-2.0
hashes:
- alg: SHA-256
content: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
```

The scanning result will be saved as a separate image with the `-sbom` postfix in the local backend storage (for example, Docker), and will also be sent to the container registry if the `--repo` flag is specified.

### GOST security properties (`sbom.gost`)
Expand Down
31 changes: 0 additions & 31 deletions docs/pages_ru/usage/build/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,37 +698,6 @@ build:
standard: cyclonedx@1.6
```

### Конфигурация конкретного образа (`sbom.fragment`)

Опционально можно предоставить дополнительные SBOM-данные для каждого образа с помощью свойства `sbom.fragment`. Это может быть использовано для ручного включения компонентов, которые не были автоматически обнаружены сканером.

`sbom.fragment` должен быть YAML-документом CycloneDX@1.6 или его частичным фрагментом (например, только секция `components:`). werf сформирует полный BOM-документ CycloneDX@1.6, объединив результаты сканирования с этим фрагментом.

```yaml
project: werf-sbom-base-image-example
configVersion: 1
build:
sbom:
enable: true
standard: cyclonedx@1.6
---
image: base-image
from: registry.werf.io/werf/scratch:latest
sbom:
fragment: |
components:
- type: library
name: openssl
version: "3.0.0"
purl: pkg:generic/openssl@3.0.0
licenses:
- license:
id: Apache-2.0
hashes:
- alg: SHA-256
content: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
```

Результат сканирования будет сохранен как отдельный образ с постфиксом `-sbom` в локальном хранилище бекенда (например, Docker), а также отправлен в container registry, если указан флаг `--repo`.

### Свойства безопасности ГОСТ (`sbom.gost`)
Expand Down
9 changes: 3 additions & 6 deletions pkg/build/build_phase.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,18 +303,15 @@ func (phase *BuildPhase) convergeImageSbom(ctx context.Context, name string, ima
return err
}

var fragmentBOM *cdx.BOM
var gostConfig gost.Config
if imgSbom := primaryImg.Sbom(); imgSbom != nil {
fragmentBOM = imgSbom.Document
gostConfig = imgSbom.Gost
}

mergeOpts := cyclonedxutil.MergeOpts{
BaseBOM: baseImageSbom,
ImportBOMs: importImageSboms,
FragmentBOM: fragmentBOM,
Gost: gostConfig,
BaseBOM: baseImageSbom,
ImportBOMs: importImageSboms,
Gost: gostConfig,
}

gitRepo := phase.Conveyor.GiterminismManager().LocalGitRepo()
Expand Down
6 changes: 0 additions & 6 deletions pkg/build/sbom_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,5 @@ func (step *sbomStep) prepareGostComponents(ctx context.Context, mergeOpts *cycl
}
}

if mergeOpts.FragmentBOM != nil {
if err := gost.Upsert(mergeOpts.FragmentBOM, mergeOpts.Gost); err != nil {
return fmt.Errorf("set GOST properties for fragment BOM: %w", err)
}
}

return nil
}
37 changes: 1 addition & 36 deletions pkg/config/raw_sbom.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
package config

import (
"fmt"
"strings"

yamlv3 "gopkg.in/yaml.v3"
)

type rawSbom struct {
doc *doc `yaml:"-"`

Fragment *string `yaml:"fragment,omitempty"`
Gost *rawGost `yaml:"gost,omitempty"`
Gost *rawGost `yaml:"gost,omitempty"`

UnsupportedAttributes map[string]interface{} `yaml:",inline"`
}
Expand Down Expand Up @@ -41,13 +33,6 @@ func (s *rawSbom) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}

// Soft validation (YAML-level only):
// If `sbom:` section is present, fragment must be present and non-empty,
// and must be valid YAML.
if err := s.validateFragmentYAML(); err != nil {
return err
}

return nil
}

Expand All @@ -58,23 +43,3 @@ func (s *rawSbom) docForErrors() *doc {
// Fallback: avoid panics in error formatting in unexpected edge cases.
return &doc{Content: []byte{}}
}

func (s *rawSbom) validateFragmentYAML() error {
d := s.docForErrors()

if s.Fragment == nil || strings.TrimSpace(*s.Fragment) == "" {
return newDetailedConfigError("`sbom.fragment` is required when `sbom:` section is specified and must not be empty!", nil, d)
}

// Validate fragment YAML by parsing with yaml.v3.
// We expect a YAML mapping at the root (e.g. `components: ...` or a full BOM document).
var fragment map[string]any
if err := yamlv3.Unmarshal([]byte(*s.Fragment), &fragment); err != nil {
return newDetailedConfigError(fmt.Sprintf("`sbom.fragment` must be valid YAML: %s", err), nil, d)
}
if fragment == nil {
return newDetailedConfigError("`sbom.fragment` must be a YAML mapping (e.g. `components: ...`)", nil, d)
}

return nil
}
75 changes: 7 additions & 68 deletions pkg/config/raw_sbom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

var _ = Describe("rawSbom (YAML-level validation)", func() {
DescribeTable(
"fragment and gost validation when sbom section is present",
"gost validation and unknown field handling when sbom section is present",
func(yamlMap map[string]interface{}, expectedSbomPresent bool, unmarshalMatcher, configErrMatcher OmegaMatcher) {
// NOTE: global var used by UnmarshalYAML parent tracking across many config raw structs.
parentStack = util.NewStack()
Expand Down Expand Up @@ -53,88 +53,29 @@ var _ = Describe("rawSbom (YAML-level validation)", func() {
),

Entry(
"should fail when sbom section exists but fragment is missing",
"should succeed when sbom section is empty",
map[string]interface{}{
"image": "image1",
"from": "alpine:3.20",
"sbom": map[string]interface{}{},
},
false,
HaveOccurred(),
BeTrue(),
),

Entry(
"should fail when sbom.fragment is empty",
map[string]interface{}{
"image": "image1",
"from": "alpine:3.20",
"sbom": map[string]interface{}{
"fragment": " ",
},
},
false,
HaveOccurred(),
BeTrue(),
),

Entry(
"should fail when sbom.fragment is not valid YAML",
map[string]interface{}{
"image": "image1",
"from": "alpine:3.20",
"sbom": map[string]interface{}{
"fragment": "components: [",
},
},
false,
HaveOccurred(),
BeTrue(),
),

Entry(
"should fail when sbom.fragment YAML root is not a mapping",
map[string]interface{}{
"image": "image1",
"from": "alpine:3.20",
"sbom": map[string]interface{}{
"fragment": "- a\n- b\n",
},
},
false,
HaveOccurred(),
BeTrue(),
),

Entry(
"should succeed when sbom.fragment contains valid YAML mapping",
map[string]interface{}{
"image": "image1",
"from": "alpine:3.20",
"sbom": map[string]interface{}{
"fragment": "components: []\n",
},
},
true,
Succeed(),
BeFalse(),
),

Entry(
"should succeed when sbom.fragment and gost are valid",
"should fail when sbom.fragment is specified (removed feature)",
map[string]interface{}{
"image": "image1",
"from": "alpine:3.20",
"sbom": map[string]interface{}{
"fragment": "components: []\n",
"gost": map[string]interface{}{
"attackSurface": "yes",
},
"fragment": "components: []",
},
},
true,
Succeed(),
BeFalse(),
false,
HaveOccurred(),
BeTrue(),
),

Entry(
Expand All @@ -143,7 +84,6 @@ var _ = Describe("rawSbom (YAML-level validation)", func() {
"image": "image1",
"from": "alpine:3.20",
"sbom": map[string]interface{}{
"fragment": "components: []\n",
"gost": map[string]interface{}{
"attackSurface": "indirect",
},
Expand All @@ -160,7 +100,6 @@ var _ = Describe("rawSbom (YAML-level validation)", func() {
"image": "image1",
"from": "alpine:3.20",
"sbom": map[string]interface{}{
"fragment": "components: []\n",
"gost": map[string]interface{}{
"attackSurface": "invalid",
},
Expand Down
44 changes: 0 additions & 44 deletions pkg/config/sbom_image.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package config

import (
"fmt"
"strings"

sbomPkg "github.com/werf/werf/v2/pkg/sbom"
"github.com/werf/werf/v2/pkg/sbom/cyclonedxutil"
)

// buildImageSbom builds image-level SBOM configuration based on meta build settings.
Expand Down Expand Up @@ -35,48 +31,8 @@ func buildImageSbom(meta *Meta, raw *rawSbom, d *doc) (*Sbom, error) {
gostConfig = gostConfig.Merge(raw.Gost.toConfig())
}

// If no image-level configs are provided, we return early with the inherited GOST configuration.
if raw == nil {
return &Sbom{
Standard: sbomPkg.StandardTypeCycloneDX16,
Gost: gostConfig,
}, nil
}

// Defensive check: meta-level validation currently allows only CycloneDX@1.6.
if metaSbom.Standard != sbomPkg.StandardTypeCycloneDX16 {
return nil, newDetailedConfigError(
fmt.Sprintf(
"unsupported sbom standard %q for image sbom (only %q is supported)",
metaSbom.Standard.String(),
sbomPkg.StandardTypeCycloneDX16.String(),
),
nil,
d,
)
}

// If fragment is not specified, we return the configuration with GOST only.
if raw.Fragment == nil {
return &Sbom{
Standard: sbomPkg.StandardTypeCycloneDX16,
Gost: gostConfig,
}, nil
}

fragment := strings.TrimSpace(*raw.Fragment)
if fragment == "" {
return nil, newDetailedConfigError("`sbom.fragment` must not be empty if specified", nil, d)
}

bom, err := cyclonedxutil.BuildCycloneDX16BOMFromYAMLFragment([]byte(fragment))
if err != nil {
return nil, newDetailedConfigError(fmt.Sprintf("invalid `sbom.fragment`: %v", err), nil, d)
}

return &Sbom{
Standard: sbomPkg.StandardTypeCycloneDX16,
Document: bom,
Gost: gostConfig,
}, nil
}
Loading
Loading