mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
* 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>
507 lines
18 KiB
Go
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
|
|
}
|