From 2580815950c0f7cb725fcfe4fcfdbdf2b2a062c7 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Thu, 11 Jun 2026 12:53:23 +0700 Subject: [PATCH] feat(sbom): catalog go.mod packages via go-mod packages directive Signed-off-by: Radmir Khurum --- docs/_data/werf_yaml.yml | 27 ++++ pkg/build/build_phase.go | 21 ++- pkg/build/sbom_step.go | 3 + pkg/config/packages_directive.go | 40 +++-- pkg/config/packages_directive_go_mod_test.go | 143 ++++++++++++++++++ pkg/config/raw_packages_directive.go | 47 ++++-- .../docker_server_backend.go | 1 + pkg/sbom/managedinput/managedinput.go | 100 ++++++++++++ pkg/sbom/managedinput/managedinput_test.go | 102 +++++++++++++ pkg/sbom/{ => managedinput}/suite_test.go | 6 +- pkg/sbom/scanner/cataloger.go | 8 + pkg/sbom/scanner/scan_command.go | 25 +++ pkg/sbom/scanner/scan_command_test.go | 33 ++++ 13 files changed, 531 insertions(+), 25 deletions(-) create mode 100644 pkg/config/packages_directive_go_mod_test.go create mode 100644 pkg/sbom/managedinput/managedinput.go create mode 100644 pkg/sbom/managedinput/managedinput_test.go rename pkg/sbom/{ => managedinput}/suite_test.go (54%) create mode 100644 pkg/sbom/scanner/cataloger.go diff --git a/docs/_data/werf_yaml.yml b/docs/_data/werf_yaml.yml index c91526aa73f..14bfe7c88a0 100644 --- a/docs/_data/werf_yaml.yml +++ b/docs/_data/werf_yaml.yml @@ -583,6 +583,33 @@ sections: all: "/usage/build/stapel/imports.html" - <<: *dockerfile-image-section-final - <<: *image-section-sbom + - name: packages + description: + en: "Set of directives to catalog language package manifests into the SBOM" + ru: "Набор директив для каталогизации манифестов пакетов языков в SBOM" + collapsible: true + isCollapsedByDefault: false + directiveList: + - name: type + value: "string" + description: + en: "Package ecosystem type (go-mod)" + ru: "Тип экосистемы пакетов (go-mod)" + - name: workdir + value: "string" + description: + en: "Directory inside the image that contains the module files (required for go-mod)" + ru: "Директория внутри образа, содержащая файлы модуля (обязательно для go-mod)" + - name: spec + value: "string" + description: + en: "Module file name (for go-mod, default: go.mod)" + ru: "Имя файла модуля (для go-mod, по умолчанию: go.mod)" + - name: lock + value: "string" + description: + en: "Checksum file name (for go-mod, default: go.sum)" + ru: "Имя файла контрольных сумм (для go-mod, по умолчанию: go.sum)" - <<: *meta-section-build-cache-version - name: platform description: diff --git a/pkg/build/build_phase.go b/pkg/build/build_phase.go index 7bcc3c69c09..0420417dc7c 100644 --- a/pkg/build/build_phase.go +++ b/pkg/build/build_phase.go @@ -34,6 +34,7 @@ import ( "github.com/werf/werf/v2/pkg/sbom/externalref" "github.com/werf/werf/v2/pkg/sbom/gomod" sbomImage "github.com/werf/werf/v2/pkg/sbom/image" + "github.com/werf/werf/v2/pkg/sbom/managedinput" "github.com/werf/werf/v2/pkg/sbom/scanner" "github.com/werf/werf/v2/pkg/stapel" "github.com/werf/werf/v2/pkg/storage" @@ -336,13 +337,31 @@ func (phase *BuildPhase) convergeImageSbom(ctx context.Context, name string, ima goModPatcher, } - if err := phase.sbomStep.ConvergeWithMerge(ctx, name, stageDesc, scanner.DefaultSyftScanOptions(), mergeOpts, patchers, primaryImg.TargetPlatform); err != nil { + scanOpts := phase.scanOptionsForImage(primaryImg) + + if err := phase.sbomStep.ConvergeWithMerge(ctx, name, stageDesc, scanOpts, mergeOpts, patchers, primaryImg.TargetPlatform); err != nil { return fmt.Errorf("unable to converge sbom for image %q: %w", name, err) } return nil } +func (phase *BuildPhase) scanOptionsForImage(img *image.Image) scanner.ScanOptions { + scanOpts := scanner.DefaultSyftScanOptions() + + stapelConfig := img.StapelImageConfig + if stapelConfig == nil { + return scanOpts + } + + catalogers := managedinput.ToCatalogers(stapelConfig.ImageBaseConfig().Packages) + for i := range scanOpts.Commands { + scanOpts.Commands[i].Catalogers = catalogers + } + + return scanOpts +} + func (phase *BuildPhase) targetPlatforms(ctx context.Context, forcedTargetPlatforms, commonTargetPlatforms []string, name string, images []*image.Image) ([]string, error) { // TODO: this target platforms assertion could be removed in future versions and now exists only as a additional self-testing code var targetPlatforms []string diff --git a/pkg/build/sbom_step.go b/pkg/build/sbom_step.go index c2cf4d358a6..91e3bed240c 100644 --- a/pkg/build/sbom_step.go +++ b/pkg/build/sbom_step.go @@ -16,6 +16,7 @@ import ( "github.com/werf/werf/v2/pkg/sbom/cyclonedxutil" "github.com/werf/werf/v2/pkg/sbom/cyclonedxutil/gost" sbomImage "github.com/werf/werf/v2/pkg/sbom/image" + "github.com/werf/werf/v2/pkg/sbom/managedinput" "github.com/werf/werf/v2/pkg/sbom/scanner" "github.com/werf/werf/v2/pkg/storage" ) @@ -82,6 +83,8 @@ func (step *sbomStep) ConvergeWithMerge(ctx context.Context, werfImgName string, return fmt.Errorf("parse scanned BOM: %w", err) } + managedinput.FilterBOMBySourcePaths(targetBOM, scanOpts.Commands[0].Catalogers) + if err := gost.Upsert(targetBOM, mergeOpts.Gost); err != nil { return fmt.Errorf("set GOST properties: %w", err) } diff --git a/pkg/config/packages_directive.go b/pkg/config/packages_directive.go index 42cf014e5f4..69e17f7d80e 100644 --- a/pkg/config/packages_directive.go +++ b/pkg/config/packages_directive.go @@ -2,34 +2,50 @@ package config import "fmt" -// PackagesDirectiveType enumerates supported package source types. type PackagesDirectiveType string const ( - PackagesDirectiveTypeOSPM PackagesDirectiveType = "os-pm" + PackagesDirectiveTypeOSPM PackagesDirectiveType = "os-pm" + PackagesDirectiveTypeGoMod PackagesDirectiveType = "go-mod" +) + +const ( + goModDefaultSpec = "go.mod" + goModDefaultLock = "go.sum" ) -// PackagesSpec stores a package specification which is either a file path -// (string) or an inline package list ([]string). type PackagesSpec struct { FilePath string Packages []string } -// PackagesDirective represents a single entry in the image-level packages list. +// GoModSpec describes Go module files inside the image. Workdir is the directory +// holding the module files; Spec and Lock default to "go.mod" and "go.sum". +type GoModSpec struct { + Workdir string + Spec string + Lock string +} + type PackagesDirective struct { - Type PackagesDirectiveType - Spec PackagesSpec + Type PackagesDirectiveType + Spec PackagesSpec + GoMod GoModSpec } func (d *PackagesDirective) validate() error { - if d.Type != PackagesDirectiveTypeOSPM { + switch d.Type { + case PackagesDirectiveTypeOSPM: + if d.Spec.FilePath == "" && len(d.Spec.Packages) == 0 { + return fmt.Errorf("packages spec must not be empty for type %q", d.Type) + } + case PackagesDirectiveTypeGoMod: + if d.GoMod.Workdir == "" { + return fmt.Errorf("the `workdir` is required for type %q", d.Type) + } + default: return fmt.Errorf("unsupported packages type %q", d.Type) } - if d.Spec.FilePath == "" && len(d.Spec.Packages) == 0 { - return fmt.Errorf("packages spec must not be empty for type %q", d.Type) - } - return nil } diff --git a/pkg/config/packages_directive_go_mod_test.go b/pkg/config/packages_directive_go_mod_test.go new file mode 100644 index 00000000000..3583b88238e --- /dev/null +++ b/pkg/config/packages_directive_go_mod_test.go @@ -0,0 +1,143 @@ +package config + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v2" + + "github.com/werf/common-go/pkg/util" +) + +var _ = Describe("rawPackagesDirective go-mod", func() { + var localGitRepo *LocalGitRepoStub + var giterminismManager *GiterminismManagerStub + + BeforeEach(func() { + parentStack = util.NewStack() + localGitRepo = NewLocalGitRepoStub("9d8059842b6fde712c58315ca0ab4713d90761c0") + giterminismManager = NewGiterminismManagerStub(localGitRepo) + }) + + directivesFromYaml := func(yamlMap map[string]interface{}) ([]*PackagesDirective, error) { + rawYaml, err := yaml.Marshal(yamlMap) + Expect(err).To(Succeed()) + + doc := &doc{Content: rawYaml} + rawStapelImage := &rawStapelImage{doc: doc} + + Expect(yaml.UnmarshalStrict(doc.Content, rawStapelImage)).To(Succeed()) + + stapelImage, err := rawStapelImage.toStapelImageDirective(giterminismManager, &Meta{}, "image1") + if err != nil { + return nil, err + } + return stapelImage.Packages, nil + } + + DescribeTable("unmarshal and convert succeed", + func(yamlMap map[string]interface{}, expected []*PackagesDirective) { + packages, err := directivesFromYaml(yamlMap) + Expect(err).To(Succeed()) + + Expect(packages).To(HaveLen(len(expected))) + for i, exp := range expected { + Expect(packages[i].Type).To(Equal(exp.Type)) + Expect(packages[i].GoMod).To(Equal(exp.GoMod)) + } + }, + + Entry("go-mod with only workdir defaults spec and lock", + map[string]interface{}{ + "image": "image1", + "from": "golang:1.23-alpine", + "packages": []map[string]interface{}{ + { + "type": "go-mod", + "workdir": "/app/api", + }, + }, + }, + []*PackagesDirective{ + { + Type: PackagesDirectiveTypeGoMod, + GoMod: GoModSpec{ + Workdir: "/app/api", + Spec: "go.mod", + Lock: "go.sum", + }, + }, + }, + ), + + Entry("go-mod with explicit spec and lock", + map[string]interface{}{ + "image": "image1", + "from": "golang:1.23-alpine", + "packages": []map[string]interface{}{ + { + "type": "go-mod", + "workdir": "/app/cli", + "spec": "go.mod", + "lock": "go.sum", + }, + }, + }, + []*PackagesDirective{ + { + Type: PackagesDirectiveTypeGoMod, + GoMod: GoModSpec{ + Workdir: "/app/cli", + Spec: "go.mod", + Lock: "go.sum", + }, + }, + }, + ), + + Entry("multiple go-mod entries", + map[string]interface{}{ + "image": "image1", + "from": "golang:1.23-alpine", + "packages": []map[string]interface{}{ + { + "type": "go-mod", + "workdir": "/app/api", + }, + { + "type": "go-mod", + "workdir": "/app/cli", + }, + }, + }, + []*PackagesDirective{ + { + Type: PackagesDirectiveTypeGoMod, + GoMod: GoModSpec{Workdir: "/app/api", Spec: "go.mod", Lock: "go.sum"}, + }, + { + Type: PackagesDirectiveTypeGoMod, + GoMod: GoModSpec{Workdir: "/app/cli", Spec: "go.mod", Lock: "go.sum"}, + }, + }, + ), + ) + + DescribeTable("convert to directive fails when required fields are missing", + func(yamlMap map[string]interface{}) { + _, err := directivesFromYaml(yamlMap) + Expect(err).To(HaveOccurred()) + }, + + Entry("go-mod without workdir", + map[string]interface{}{ + "image": "image1", + "from": "golang:1.23-alpine", + "packages": []map[string]interface{}{ + { + "type": "go-mod", + }, + }, + }, + ), + ) +}) diff --git a/pkg/config/raw_packages_directive.go b/pkg/config/raw_packages_directive.go index 6c0cec7f653..54332719b40 100644 --- a/pkg/config/raw_packages_directive.go +++ b/pkg/config/raw_packages_directive.go @@ -5,8 +5,10 @@ import ( ) type rawPackagesDirective struct { - Type string `yaml:"type,omitempty"` - Spec interface{} `yaml:"spec,omitempty"` + Type string `yaml:"type,omitempty"` + Spec interface{} `yaml:"spec,omitempty"` + Workdir string `yaml:"workdir,omitempty"` + Lock string `yaml:"lock,omitempty"` rawStapelImage *rawStapelImage `yaml:"-"` @@ -34,8 +36,8 @@ func (r *rawPackagesDirective) UnmarshalYAML(unmarshal func(interface{}) error) return newDetailedConfigError("the `type` is required for each packages directive entry!", nil, r.docForErrors()) } - if r.Spec == nil { - return newDetailedConfigError("the `spec` is required for each packages directive entry!", nil, r.docForErrors()) + if PackagesDirectiveType(r.Type) == PackagesDirectiveTypeOSPM && r.Spec == nil { + return newDetailedConfigError("the `spec` is required for `os-pm` packages directive entry!", nil, r.docForErrors()) } return nil @@ -53,22 +55,49 @@ func (r *rawPackagesDirective) toDirective() (*PackagesDirective, error) { Type: PackagesDirectiveType(r.Type), } + switch d.Type { + case PackagesDirectiveTypeOSPM: + if err := r.fillOSPMSpec(d); err != nil { + return nil, err + } + case PackagesDirectiveTypeGoMod: + r.fillGoModSpec(d) + } + + if err := d.validate(); err != nil { + return nil, err + } + + return d, nil +} + +func (r *rawPackagesDirective) fillOSPMSpec(d *PackagesDirective) error { switch v := r.Spec.(type) { case string: d.Spec.FilePath = v case []interface{}: packages, err := InterfaceToStringArray(v, nil, r.rawStapelImage.doc) if err != nil { - return nil, err + return err } d.Spec.Packages = packages default: - return nil, fmt.Errorf("unsupported packages spec type %T for type %q", r.Spec, r.Type) + return fmt.Errorf("unsupported packages spec type %T for type %q", r.Spec, r.Type) } - if err := d.validate(); err != nil { - return nil, err + return nil +} + +func (r *rawPackagesDirective) fillGoModSpec(d *PackagesDirective) { + d.GoMod.Workdir = r.Workdir + + d.GoMod.Spec = goModDefaultSpec + if spec, ok := r.Spec.(string); ok && spec != "" { + d.GoMod.Spec = spec } - return d, nil + d.GoMod.Lock = goModDefaultLock + if r.Lock != "" { + d.GoMod.Lock = r.Lock + } } diff --git a/pkg/container_backend/docker_server_backend.go b/pkg/container_backend/docker_server_backend.go index 6fefb8d533f..d76eb4940bf 100644 --- a/pkg/container_backend/docker_server_backend.go +++ b/pkg/container_backend/docker_server_backend.go @@ -496,6 +496,7 @@ func (backend *DockerServerBackend) GenerateSBOM(ctx context.Context, scanOpts s var bomJSON []byte err := logboek.Context(ctx).Default().LogProcess("Scan image %q", scanOpts.Commands[0].SourcePath).DoError(func() error { runArgs := mapSbomScanOptionsToDockerRunCommand(wt.RootDir(), wt.BillsDir(), billNames, scanOpts) + logboek.Context(ctx).Debug().LogF("docker %s\n", strings.Join(runArgs, " ")) if _, err := docker.CliRun_RecordedOutput(ctx, runArgs...); err != nil { return fmt.Errorf("run scanner: %w", err) } diff --git a/pkg/sbom/managedinput/managedinput.go b/pkg/sbom/managedinput/managedinput.go new file mode 100644 index 00000000000..6d6397de920 --- /dev/null +++ b/pkg/sbom/managedinput/managedinput.go @@ -0,0 +1,100 @@ +package managedinput + +import ( + "path" + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/samber/lo" + + "github.com/werf/werf/v2/pkg/config" + "github.com/werf/werf/v2/pkg/sbom/scanner" +) + +type inputResolver struct { + inputType config.PackagesDirectiveType + catalogerName string + sourcePaths func(directive *config.PackagesDirective) []string +} + +var resolvers = []inputResolver{ + { + inputType: config.PackagesDirectiveTypeGoMod, + catalogerName: "go-module-file-cataloger", + sourcePaths: func(directive *config.PackagesDirective) []string { + return []string{ + path.Join(directive.GoMod.Workdir, directive.GoMod.Spec), + path.Join(directive.GoMod.Workdir, directive.GoMod.Lock), + } + }, + }, +} + +func ToCatalogers(packages []*config.PackagesDirective) []scanner.Cataloger { + var catalogers []scanner.Cataloger + + for _, directive := range packages { + res, found := lo.Find(resolvers, func(r inputResolver) bool { + return r.inputType == directive.Type + }) + if !found { + continue + } + + catalogers = append(catalogers, scanner.Cataloger{ + Name: res.catalogerName, + SourcePaths: res.sourcePaths(directive), + }) + } + + return catalogers +} + +func FilterBOMBySourcePaths(bom *cdx.BOM, catalogers []scanner.Cataloger) { + if bom == nil || bom.Components == nil || len(catalogers) == 0 { + return + } + + allowedPaths := make(map[string]struct{}) + for _, cat := range catalogers { + for _, p := range cat.SourcePaths { + allowedPaths[p] = struct{}{} + } + } + + filtered := lo.Filter(*bom.Components, func(comp cdx.Component, _ int) bool { + if !isGoModuleComponent(comp) { + return true + } + return componentMatchesAllowedPaths(comp, allowedPaths) + }) + + *bom.Components = filtered +} + +func isGoModuleComponent(comp cdx.Component) bool { + if comp.Properties == nil { + return false + } + for _, prop := range *comp.Properties { + if prop.Name == "syft:package:type" && prop.Value == "go-module" { + return true + } + } + return false +} + +func componentMatchesAllowedPaths(comp cdx.Component, allowedPaths map[string]struct{}) bool { + if comp.Properties == nil { + return false + } + for _, prop := range *comp.Properties { + if !strings.HasPrefix(prop.Name, "syft:location:") || !strings.HasSuffix(prop.Name, ":path") { + continue + } + if _, ok := allowedPaths[prop.Value]; ok { + return true + } + } + return false +} diff --git a/pkg/sbom/managedinput/managedinput_test.go b/pkg/sbom/managedinput/managedinput_test.go new file mode 100644 index 00000000000..f319847b952 --- /dev/null +++ b/pkg/sbom/managedinput/managedinput_test.go @@ -0,0 +1,102 @@ +package managedinput + +import ( + cdx "github.com/CycloneDX/cyclonedx-go" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/werf/werf/v2/pkg/config" + "github.com/werf/werf/v2/pkg/sbom/scanner" +) + +var _ = Describe("ToCatalogers", func() { + DescribeTable("maps packages directives to syft catalogers", + func(packages []*config.PackagesDirective, expected []scanner.Cataloger) { + Expect(ToCatalogers(packages)).To(Equal(expected)) + }, + + Entry("go-mod entries map to the go-module-file-cataloger", + []*config.PackagesDirective{ + { + Type: config.PackagesDirectiveTypeGoMod, + GoMod: config.GoModSpec{Workdir: "/app/api", Spec: "go.mod", Lock: "go.sum"}, + }, + { + Type: config.PackagesDirectiveTypeGoMod, + GoMod: config.GoModSpec{Workdir: "/app/cli", Spec: "go.mod", Lock: "go.sum"}, + }, + }, + []scanner.Cataloger{ + {Name: "go-module-file-cataloger", SourcePaths: []string{"/app/api/go.mod", "/app/api/go.sum"}}, + {Name: "go-module-file-cataloger", SourcePaths: []string{"/app/cli/go.mod", "/app/cli/go.sum"}}, + }, + ), + + Entry("os-pm entries are skipped", + []*config.PackagesDirective{ + { + Type: config.PackagesDirectiveTypeOSPM, + Spec: config.PackagesSpec{Packages: []string{"curl"}}, + }, + }, + []scanner.Cataloger(nil), + ), + + Entry("nil packages yield no catalogers", + []*config.PackagesDirective(nil), + []scanner.Cataloger(nil), + ), + ) +}) + +var _ = Describe("FilterBOMBySourcePaths", func() { + goModProps := func(path string) *[]cdx.Property { + return &[]cdx.Property{ + {Name: "syft:package:type", Value: "go-module"}, + {Name: "syft:location:0:path", Value: path}, + } + } + + osProps := func() *[]cdx.Property { + return &[]cdx.Property{ + {Name: "syft:package:type", Value: "deb"}, + {Name: "syft:location:0:path", Value: "/var/lib/dpkg/status"}, + } + } + + It("keeps go-module components matching declared paths and removes others", func() { + bom := &cdx.BOM{ + Components: &[]cdx.Component{ + {Name: "github.com/foo/bar", Properties: goModProps("/app/api/go.mod")}, + {Name: "github.com/baz/qux", Properties: goModProps("/vendor/tool/go.mod")}, + {Name: "curl", Properties: osProps()}, + }, + } + + catalogers := []scanner.Cataloger{ + {Name: "go-module-file-cataloger", SourcePaths: []string{"/app/api/go.mod", "/app/api/go.sum"}}, + } + + FilterBOMBySourcePaths(bom, catalogers) + + Expect(*bom.Components).To(HaveLen(2)) + Expect((*bom.Components)[0].Name).To(Equal("github.com/foo/bar")) + Expect((*bom.Components)[1].Name).To(Equal("curl")) + }) + + It("does nothing when no catalogers are provided", func() { + bom := &cdx.BOM{ + Components: &[]cdx.Component{ + {Name: "github.com/foo/bar", Properties: goModProps("/app/api/go.mod")}, + }, + } + + FilterBOMBySourcePaths(bom, nil) + + Expect(*bom.Components).To(HaveLen(1)) + }) + + It("does nothing when BOM is nil", func() { + FilterBOMBySourcePaths(nil, []scanner.Cataloger{{Name: "x", SourcePaths: []string{"/app/go.mod"}}}) + }) +}) diff --git a/pkg/sbom/suite_test.go b/pkg/sbom/managedinput/suite_test.go similarity index 54% rename from pkg/sbom/suite_test.go rename to pkg/sbom/managedinput/suite_test.go index 189578d9d6c..d2ff8e24d9a 100644 --- a/pkg/sbom/suite_test.go +++ b/pkg/sbom/managedinput/suite_test.go @@ -1,4 +1,4 @@ -package sbom +package managedinput import ( "testing" @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestSbom(t *testing.T) { +func TestManagedInput(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Sbom Suite") + RunSpecs(t, "Managed Input Suite") } diff --git a/pkg/sbom/scanner/cataloger.go b/pkg/sbom/scanner/cataloger.go new file mode 100644 index 00000000000..d489d989bb7 --- /dev/null +++ b/pkg/sbom/scanner/cataloger.go @@ -0,0 +1,8 @@ +package scanner + +// Cataloger is a syft cataloger to enable for a scan, together with the in-image +// file paths it targets (e.g. go.mod / go.sum). +type Cataloger struct { + Name string + SourcePaths []string +} diff --git a/pkg/sbom/scanner/scan_command.go b/pkg/sbom/scanner/scan_command.go index e17f4345978..e517bf2f3f4 100644 --- a/pkg/sbom/scanner/scan_command.go +++ b/pkg/sbom/scanner/scan_command.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" + "github.com/samber/lo" + "github.com/werf/common-go/pkg/util" "github.com/werf/werf/v2/pkg/sbom" ) @@ -16,6 +18,7 @@ type ScanCommand struct { OutputStandard sbom.StandardType OutputPath string outputFormat string + Catalogers []Cataloger } func (c ScanCommand) output() string { @@ -57,18 +60,40 @@ func (c ScanCommand) String() string { out.WriteString(fmt.Sprintf("scan %s:%s --output=%s", c.SourceType, c.SourcePath, c.output())) + if selectArg := c.selectCatalogersArg(); selectArg != "" { + out.WriteString(fmt.Sprintf(" %s", selectArg)) + } + return out.String() default: panic(fmt.Sprintf("unsupported scanner type %s", c.scannerType)) } } +func (c ScanCommand) selectCatalogersArg() string { + if len(c.Catalogers) == 0 { + return "" + } + + selectors := lo.Map(c.Catalogers, func(cat Cataloger, _ int) string { + return cat.Name + }) + + return fmt.Sprintf("--select-catalogers=%s", strings.Join(selectors, ",")) +} + func (c ScanCommand) Checksum() string { args := []string{ "scanner_type", c.scannerType.String(), "source_type", c.SourceType.String(), "output_standard", c.OutputStandard.String(), } + + for _, cat := range c.Catalogers { + args = append(args, "cataloger", cat.Name) + args = append(args, cat.SourcePaths...) + } + return util.Sha256Hash(args...) } diff --git a/pkg/sbom/scanner/scan_command_test.go b/pkg/sbom/scanner/scan_command_test.go index 6e7d549970a..4d81a30edcb 100644 --- a/pkg/sbom/scanner/scan_command_test.go +++ b/pkg/sbom/scanner/scan_command_test.go @@ -164,6 +164,39 @@ var _ = Describe("ScanCommand", func() { }, Equal("scan docker:alpine:3.18 --output=cyclonedx-json@1.6"), ), + Entry( + "should add a single cataloger with --select-catalogers", + ScanCommand{ + scannerType: TypeSyft, + scannerExecPath: "/syft", + SourceType: SourceTypeDocker, + SourcePath: "alpine:3.18", + OutputStandard: sbom.StandardTypeCycloneDX16, + OutputPath: "file.json", + outputFormat: "json", + Catalogers: []Cataloger{ + {Name: "go-module-file-cataloger", SourcePaths: []string{"/app/api/go.mod", "/app/api/go.sum"}}, + }, + }, + Equal("/syft scan docker:alpine:3.18 --output=cyclonedx-json@1.6=file.json --select-catalogers=go-module-file-cataloger"), + ), + Entry( + "should join multiple catalogers by comma", + ScanCommand{ + scannerType: TypeSyft, + scannerExecPath: "/syft", + SourceType: SourceTypeDocker, + SourcePath: "alpine:3.18", + OutputStandard: sbom.StandardTypeCycloneDX16, + OutputPath: "file.json", + outputFormat: "json", + Catalogers: []Cataloger{ + {Name: "go-module-file-cataloger"}, + {Name: "javascript-package-cataloger"}, + }, + }, + Equal("/syft scan docker:alpine:3.18 --output=cyclonedx-json@1.6=file.json --select-catalogers=go-module-file-cataloger,javascript-package-cataloger"), + ), ) DescribeTable("Checksum()",