syft/syft/create_sbom_config.go
Keith Zantow 02703d5c80
feat: RHEL EUS detection (#4023)
* feat: rhel eus detection

Signed-off-by: Keith Zantow <kzantow@gmail.com>

* chore: update tests

Signed-off-by: Keith Zantow <kzantow@gmail.com>

* chore: update more tests

Signed-off-by: Keith Zantow <kzantow@gmail.com>

* rename feature detection functions

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Keith Zantow <kzantow@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-07-07 14:11:20 +00:00

507 lines
18 KiB
Go

package syft
import (
"context"
"errors"
"fmt"
"runtime/debug"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/cataloging/filecataloging"
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
// CreateSBOMConfig specifies all parameters needed for creating an SBOM.
type CreateSBOMConfig struct {
// required configuration input to specify how cataloging should be performed
Compliance cataloging.ComplianceConfig
Search cataloging.SearchConfig
Relationships cataloging.RelationshipsConfig
Unknowns cataloging.UnknownsConfig
DataGeneration cataloging.DataGenerationConfig
Packages pkgcataloging.Config
Licenses cataloging.LicenseConfig
Files filecataloging.Config
Parallelism int
CatalogerSelection cataloging.SelectionRequest
// audit what tool is being used to generate the SBOM
ToolName string
ToolVersion string
ToolConfiguration interface{}
packageTaskFactories task.Factories
packageCatalogerReferences []pkgcataloging.CatalogerReference
}
func DefaultCreateSBOMConfig() *CreateSBOMConfig {
return &CreateSBOMConfig{
Compliance: cataloging.DefaultComplianceConfig(),
Search: cataloging.DefaultSearchConfig(),
Relationships: cataloging.DefaultRelationshipsConfig(),
DataGeneration: cataloging.DefaultDataGenerationConfig(),
Packages: pkgcataloging.DefaultConfig(),
Licenses: cataloging.DefaultLicenseConfig(),
Files: filecataloging.DefaultConfig(),
Parallelism: 0, // use default: run in parallel based on number of CPUs
packageTaskFactories: task.DefaultPackageTaskFactories(),
// library consumers are free to override the tool values to fit their needs, however, we have some sane defaults
// to ensure that SBOMs generated don't have missing tool metadata.
ToolName: "syft",
ToolVersion: syftVersion(),
}
}
func syftVersion() string {
// extract the syft version from the go module info from the current binary that is running. This is useful for
// library consumers to at least encode the version of syft that was used to generate the SBOM. Note: we don't
// use the version info from main because it's baked in with ldflags, which we don't control for library consumers.
// This approach won't work in all cases though, such as when the binary is stripped of the buildinfo section.
buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return ""
}
for _, d := range buildInfo.Deps {
if d.Path == "github.com/anchore/syft" && d.Version != "(devel)" {
return d.Version
}
}
return ""
}
// WithTool allows for setting the specific name, version, and any additional configuration that is not captured
// in the syft default API configuration. This could cover inputs for catalogers that were user-provided, thus,
// is not visible to the syft API, but would be useful to see in the SBOM output.
func (c *CreateSBOMConfig) WithTool(name, version string, cfg ...any) *CreateSBOMConfig {
c.ToolName = name
c.ToolVersion = version
c.ToolConfiguration = cfg
return c
}
// WithParallelism allows for setting the number of concurrent cataloging tasks that can be performed at once
func (c *CreateSBOMConfig) WithParallelism(p int) *CreateSBOMConfig {
c.Parallelism = p
return c
}
// WithComplianceConfig allows for setting the specific compliance configuration for cataloging.
func (c *CreateSBOMConfig) WithComplianceConfig(cfg cataloging.ComplianceConfig) *CreateSBOMConfig {
c.Compliance = cfg
return c
}
// WithSearchConfig allows for setting the specific search configuration for cataloging.
func (c *CreateSBOMConfig) WithSearchConfig(cfg cataloging.SearchConfig) *CreateSBOMConfig {
c.Search = cfg
return c
}
// WithRelationshipsConfig allows for defining the specific relationships that should be captured during cataloging.
func (c *CreateSBOMConfig) WithRelationshipsConfig(cfg cataloging.RelationshipsConfig) *CreateSBOMConfig {
c.Relationships = cfg
return c
}
// WithUnknownsConfig allows for defining the specific behavior dealing with unknowns
func (c *CreateSBOMConfig) WithUnknownsConfig(cfg cataloging.UnknownsConfig) *CreateSBOMConfig {
c.Unknowns = cfg
return c
}
// WithDataGenerationConfig allows for defining what data elements that cannot be discovered from the underlying
// target being scanned that should be generated after package creation.
func (c *CreateSBOMConfig) WithDataGenerationConfig(cfg cataloging.DataGenerationConfig) *CreateSBOMConfig {
c.DataGeneration = cfg
return c
}
// WithPackagesConfig allows for defining any specific package cataloging behavior for syft-implemented catalogers.
func (c *CreateSBOMConfig) WithPackagesConfig(cfg pkgcataloging.Config) *CreateSBOMConfig {
c.Packages = cfg
return c
}
// WithLicenseConfig allows for defining any specific license cataloging behavior for syft-implemented catalogers.
func (c *CreateSBOMConfig) WithLicenseConfig(cfg cataloging.LicenseConfig) *CreateSBOMConfig {
c.Licenses = cfg
return c
}
// WithFilesConfig allows for defining file-based cataloging parameters.
func (c *CreateSBOMConfig) WithFilesConfig(cfg filecataloging.Config) *CreateSBOMConfig {
c.Files = cfg
return c
}
// WithoutFiles allows for disabling file cataloging altogether.
func (c *CreateSBOMConfig) WithoutFiles() *CreateSBOMConfig {
c.Files = filecataloging.Config{
Selection: file.NoFilesSelection,
Hashers: nil,
}
return c
}
// WithCatalogerSelection allows for adding to, removing from, or sub-selecting the final set of catalogers by name or tag.
func (c *CreateSBOMConfig) WithCatalogerSelection(selection cataloging.SelectionRequest) *CreateSBOMConfig {
c.CatalogerSelection = selection
return c
}
// WithoutCatalogers removes all catalogers from the final set of catalogers. This is useful if you want to only use
// user-provided catalogers (without the default syft-provided catalogers).
func (c *CreateSBOMConfig) WithoutCatalogers() *CreateSBOMConfig {
c.packageTaskFactories = nil
c.packageCatalogerReferences = nil
return c
}
// WithCatalogers allows for adding user-provided catalogers to the final set of catalogers that will always be run
// regardless of the source type or any cataloger selections provided.
func (c *CreateSBOMConfig) WithCatalogers(catalogerRefs ...pkgcataloging.CatalogerReference) *CreateSBOMConfig {
for i := range catalogerRefs {
// ensure that all package catalogers have the package tag
catalogerRefs[i].Tags = append(catalogerRefs[i].Tags, pkgcataloging.PackageTag)
}
c.packageCatalogerReferences = append(c.packageCatalogerReferences, catalogerRefs...)
return c
}
// makeTaskGroups considers the entire configuration and finalizes the set of tasks to be run. Tasks are run in
// groups, where each task in a group can be run concurrently, while tasks in different groups must be run serially.
// The final set of task groups is returned along with a cataloger manifest that describes the catalogers that were
// selected and the tokens that were sensitive to this selection (both for adding and removing from the final set).
func (c *CreateSBOMConfig) makeTaskGroups(src source.Description) ([][]task.Task, *catalogerManifest, error) {
var taskGroups [][]task.Task
// generate package and file tasks based on the configuration
environmentTasks := c.environmentTasks()
scopeTasks := c.scopeTasks()
relationshipsTasks := c.relationshipTasks(src)
unknownTasks := c.unknownsTasks()
osFeatureDetectionTasks := c.osFeatureDetectionTasks()
pkgTasks, fileTasks, selectionEvidence, err := c.selectTasks(src)
if err != nil {
return nil, nil, err
}
// combine the user-provided and configured tasks
if c.Files.Selection == file.FilesOwnedByPackageSelection {
// special case: we need the package info when we are cataloging files owned by packages
taskGroups = append(taskGroups, pkgTasks, fileTasks)
} else {
taskGroups = append(taskGroups, append(pkgTasks, fileTasks...))
}
// all scope work must be done after all nodes (files and packages) have been cataloged and before the relationship
if len(scopeTasks) > 0 {
taskGroups = append(taskGroups, scopeTasks)
}
// all relationship work must be done after all nodes (files and packages) have been cataloged
if len(relationshipsTasks) > 0 {
taskGroups = append(taskGroups, relationshipsTasks)
}
// all unknowns tasks should happen after all scanning is complete
if len(unknownTasks) > 0 {
taskGroups = append(taskGroups, unknownTasks)
}
// osFeatureDetectionTasks should happen after package scanning is complete
if len(osFeatureDetectionTasks) > 0 {
taskGroups = append(taskGroups, osFeatureDetectionTasks)
}
// identifying the environment (i.e. the linux release) must be done first as this is required for package cataloging
taskGroups = append(
[][]task.Task{
environmentTasks,
},
taskGroups...,
)
var allTasks []task.Task
allTasks = append(allTasks, pkgTasks...)
allTasks = append(allTasks, fileTasks...)
return taskGroups, &catalogerManifest{
Requested: selectionEvidence.Request,
Used: formatTaskNames(allTasks),
}, nil
}
// fileTasks returns the set of tasks that should be run to catalog files.
func (c *CreateSBOMConfig) fileTasks(cfg task.CatalogingFactoryConfig) ([]task.Task, error) {
tsks, err := task.DefaultFileTaskFactories().Tasks(cfg)
if err != nil {
return nil, fmt.Errorf("unable to create file cataloger tasks: %w", err)
}
return tsks, nil
}
// selectTasks returns the set of tasks that should be run to catalog packages and files.
func (c *CreateSBOMConfig) selectTasks(src source.Description) ([]task.Task, []task.Task, *task.Selection, error) {
cfg := task.CatalogingFactoryConfig{
SearchConfig: c.Search,
RelationshipsConfig: c.Relationships,
DataGenerationConfig: c.DataGeneration,
PackagesConfig: c.Packages,
LicenseConfig: c.Licenses,
ComplianceConfig: c.Compliance,
FilesConfig: c.Files,
}
persistentPkgTasks, selectablePkgTasks, err := c.allPackageTasks(cfg)
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to create package cataloger tasks: %w", err)
}
req, err := finalTaskSelectionRequest(c.CatalogerSelection, src)
if err != nil {
return nil, nil, nil, err
}
selectableFileTasks, err := c.fileTasks(cfg)
if err != nil {
return nil, nil, nil, err
}
taskGroups := [][]task.Task{
selectablePkgTasks,
selectableFileTasks,
}
finalTaskGroups, selection, err := task.SelectInGroups(taskGroups, *req)
if err != nil {
return nil, nil, nil, err
}
if deprecatedNames := deprecatedTasks(finalTaskGroups); len(deprecatedNames) > 0 {
log.WithFields("catalogers", strings.Join(deprecatedNames, ", ")).Warn("deprecated catalogers are being used (please remove them from your configuration)")
}
finalPkgTasks := finalTaskGroups[0]
finalFileTasks := finalTaskGroups[1]
finalPkgTasks = append(finalPkgTasks, persistentPkgTasks...)
if len(finalPkgTasks) == 0 && len(finalFileTasks) == 0 {
return nil, nil, nil, fmt.Errorf("no catalogers selected")
}
logTaskNames(finalPkgTasks, "package cataloger")
logTaskNames(finalFileTasks, "file cataloger")
if len(finalPkgTasks) == 0 && len(finalFileTasks) == 0 {
return nil, nil, nil, fmt.Errorf("no catalogers selected")
}
if len(finalPkgTasks) == 0 {
log.Debug("no package catalogers selected")
}
if len(finalFileTasks) == 0 {
if c.Files.Selection != file.NoFilesSelection {
log.Warnf("no file catalogers selected but file selection is configured as %q (this may be unintentional)", c.Files.Selection)
} else {
log.Debug("no file catalogers selected")
}
}
return finalPkgTasks, finalFileTasks, &selection, nil
}
func deprecatedTasks(taskGroups [][]task.Task) []string {
// we want to identify any deprecated catalogers that are being used but default selections will always additionally select `file`
// catalogers. For this reason, we must explicitly remove `file` catalogers in the selection request. This means if we
// deprecate a file cataloger we will need special processing.
_, selection, err := task.SelectInGroups(taskGroups, cataloging.SelectionRequest{DefaultNamesOrTags: []string{pkgcataloging.DeprecatedTag}, RemoveNamesOrTags: []string{filecataloging.FileTag}})
if err != nil {
// ignore the error, as it is not critical
return nil
}
return selection.Result.List()
}
func logTaskNames(tasks []task.Task, kind string) {
// log as tree output (like tree command)
log.Debugf("selected %d %s tasks", len(tasks), kind)
names := formatTaskNames(tasks)
for idx, t := range names {
if idx == len(tasks)-1 {
log.Tracef("└── %s", t)
} else {
log.Tracef("├── %s", t)
}
}
}
func finalTaskSelectionRequest(req cataloging.SelectionRequest, src source.Description) (*cataloging.SelectionRequest, error) {
if len(req.DefaultNamesOrTags) == 0 {
defaultTags, err := findDefaultTags(src)
if err != nil {
return nil, fmt.Errorf("unable to determine default cataloger tag: %w", err)
}
req.DefaultNamesOrTags = append(req.DefaultNamesOrTags, defaultTags...)
req.RemoveNamesOrTags = replaceDefaultTagReferences(defaultTags, req.RemoveNamesOrTags)
req.SubSelectTags = replaceDefaultTagReferences(defaultTags, req.SubSelectTags)
}
return &req, nil
}
func (c *CreateSBOMConfig) allPackageTasks(cfg task.CatalogingFactoryConfig) ([]task.Task, []task.Task, error) {
persistentPackageTasks, selectablePackageTasks, err := c.userPackageTasks(cfg)
if err != nil {
return nil, nil, err
}
tsks, err := c.packageTaskFactories.Tasks(cfg)
if err != nil {
return nil, nil, fmt.Errorf("unable to create package cataloger tasks: %w", err)
}
return persistentPackageTasks, append(tsks, selectablePackageTasks...), nil
}
func (c *CreateSBOMConfig) userPackageTasks(cfg task.CatalogingFactoryConfig) ([]task.Task, []task.Task, error) {
var (
persistentPackageTasks []task.Task
selectablePackageTasks []task.Task
)
for _, catalogerRef := range c.packageCatalogerReferences {
if catalogerRef.Cataloger == nil {
return nil, nil, errors.New("provided cataloger reference without a cataloger")
}
if catalogerRef.AlwaysEnabled {
persistentPackageTasks = append(persistentPackageTasks, task.NewPackageTask(cfg, catalogerRef.Cataloger, catalogerRef.Tags...))
continue
}
if len(catalogerRef.Tags) == 0 {
return nil, nil, errors.New("provided cataloger reference without tags")
}
selectablePackageTasks = append(selectablePackageTasks, task.NewPackageTask(cfg, catalogerRef.Cataloger, catalogerRef.Tags...))
}
return persistentPackageTasks, selectablePackageTasks, nil
}
// scopeTasks returns the set of tasks that should be run to generate additional scope information
func (c *CreateSBOMConfig) scopeTasks() []task.Task {
var tsks []task.Task
if c.Search.Scope == source.DeepSquashedScope {
if t := task.NewDeepSquashedScopeCleanupTask(); t != nil {
tsks = append(tsks, t)
}
}
return tsks
}
// relationshipTasks returns the set of tasks that should be run to generate additional relationships as well as
// prune existing relationships.
func (c *CreateSBOMConfig) relationshipTasks(src source.Description) []task.Task {
var tsks []task.Task
if t := task.NewRelationshipsTask(c.Relationships, src); t != nil {
tsks = append(tsks, t)
}
return tsks
}
// environmentTasks returns the set of tasks that should be run to identify what is being scanned or the context
// of where it is being scanned. Today this is used to identify the linux distribution release for container images
// being scanned.
func (c *CreateSBOMConfig) environmentTasks() []task.Task {
var tsks []task.Task
if t := task.NewEnvironmentTask(); t != nil {
tsks = append(tsks, t)
}
return tsks
}
// unknownsTasks returns a set of tasks that perform any necessary post-processing
// to identify SBOM elements as unknowns
func (c *CreateSBOMConfig) unknownsTasks() []task.Task {
var tasks []task.Task
if t := task.NewUnknownsLabelerTask(c.Unknowns); t != nil {
tasks = append(tasks, t)
}
return tasks
}
// osFeatureDetectionTasks returns a set of tasks that perform post-processing feature detection and update the SBOM accordingly
func (c *CreateSBOMConfig) osFeatureDetectionTasks() []task.Task {
var tasks []task.Task
if t := task.NewOSFeatureDetectionTask(); t != nil {
tasks = append(tasks, t)
}
return tasks
}
func (c *CreateSBOMConfig) validate() error {
if c.Relationships.ExcludeBinaryPackagesWithFileOwnershipOverlap {
if !c.Relationships.PackageFileOwnershipOverlap {
return fmt.Errorf("invalid configuration: to exclude binary packages based on file ownership overlap relationships, cataloging file ownership overlap relationships must be enabled")
}
}
return nil
}
// Create creates an SBOM from the given source with the current SBOM configuration.
func (c *CreateSBOMConfig) Create(ctx context.Context, src source.Source) (*sbom.SBOM, error) {
return CreateSBOM(ctx, src, c)
}
func findDefaultTags(src source.Description) ([]string, error) {
switch m := src.Metadata.(type) {
case source.ImageMetadata:
return []string{pkgcataloging.ImageTag, filecataloging.FileTag}, nil
case source.FileMetadata, source.DirectoryMetadata:
return []string{pkgcataloging.DirectoryTag, filecataloging.FileTag}, nil
case source.SnapMetadata:
return []string{pkgcataloging.InstalledTag, filecataloging.FileTag}, nil
default:
return nil, fmt.Errorf("unable to determine default cataloger tag for source type=%T", m)
}
}
func replaceDefaultTagReferences(defaultTags []string, lst []string) []string {
for i, tag := range lst {
if strings.ToLower(tag) == "default" {
switch len(defaultTags) {
case 0:
lst[i] = ""
case 1:
lst[i] = defaultTags[0]
default:
// remove the default tag and add the individual tags
lst = append(lst[:i], append(defaultTags, lst[i+1:]...)...)
}
}
}
return lst
}