github.com/anchore/syft@v1.38.2/syft/create_sbom.go (about)

     1  package syft
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"runtime"
     7  	"sort"
     8  
     9  	"github.com/dustin/go-humanize"
    10  	"github.com/scylladb/go-set/strset"
    11  
    12  	"github.com/anchore/go-sync"
    13  	"github.com/anchore/syft/internal/bus"
    14  	"github.com/anchore/syft/internal/licenses"
    15  	"github.com/anchore/syft/internal/sbomsync"
    16  	"github.com/anchore/syft/internal/task"
    17  	"github.com/anchore/syft/syft/artifact"
    18  	"github.com/anchore/syft/syft/cataloging"
    19  	"github.com/anchore/syft/syft/event/monitor"
    20  	"github.com/anchore/syft/syft/pkg"
    21  	"github.com/anchore/syft/syft/sbom"
    22  	"github.com/anchore/syft/syft/source"
    23  )
    24  
    25  // CreateSBOM creates a software bill-of-materials from the given source. If the CreateSBOMConfig is nil, then
    26  // default options will be used.
    27  func CreateSBOM(ctx context.Context, src source.Source, cfg *CreateSBOMConfig) (*sbom.SBOM, error) {
    28  	if cfg == nil {
    29  		cfg = DefaultCreateSBOMConfig()
    30  	}
    31  	if err := cfg.validate(); err != nil {
    32  		return nil, fmt.Errorf("invalid configuration: %w", err)
    33  	}
    34  
    35  	srcMetadata := src.Describe()
    36  
    37  	taskGroups, audit, err := cfg.makeTaskGroups(srcMetadata)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	resolver, err := src.FileResolver(cfg.Search.Scope)
    43  	if err != nil {
    44  		return nil, fmt.Errorf("unable to get file resolver: %w", err)
    45  	}
    46  
    47  	s := sbom.SBOM{
    48  		Source: srcMetadata,
    49  		Descriptor: sbom.Descriptor{
    50  			Name:    cfg.ToolName,
    51  			Version: cfg.ToolVersion,
    52  			Configuration: configurationAuditTrail{
    53  				Search:         cfg.Search,
    54  				Relationships:  cfg.Relationships,
    55  				DataGeneration: cfg.DataGeneration,
    56  				Packages:       cfg.Packages,
    57  				Files:          cfg.Files,
    58  				Licenses:       cfg.Licenses,
    59  				Catalogers:     *audit,
    60  				ExtraConfigs:   cfg.ToolConfiguration,
    61  			},
    62  		},
    63  		Artifacts: sbom.Artifacts{
    64  			Packages: pkg.NewCollection(),
    65  		},
    66  	}
    67  
    68  	// setup everything we need in context: license scanner, executors, etc.
    69  	ctx, err = setupContext(ctx, cfg)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	catalogingProgress := monitorCatalogingTask(src.ID(), taskGroups)
    75  	packageCatalogingProgress := monitorPackageCatalogingTask()
    76  
    77  	builder := sbomsync.NewBuilder(&s, monitorPackageCount(packageCatalogingProgress))
    78  	for i := range taskGroups {
    79  		err = sync.Collect(&ctx, cataloging.ExecutorFile, sync.ToSeq(taskGroups[i]), func(t task.Task) (any, error) {
    80  			return nil, task.RunTask(ctx, t, resolver, builder, catalogingProgress)
    81  		}, nil)
    82  		if err != nil {
    83  			// TODO: tie this to the open progress monitors...
    84  			return nil, fmt.Errorf("failed to run tasks: %w", err)
    85  		}
    86  	}
    87  
    88  	packageCatalogingProgress.SetCompleted()
    89  	catalogingProgress.SetCompleted()
    90  
    91  	return &s, nil
    92  }
    93  
    94  func setupContext(ctx context.Context, cfg *CreateSBOMConfig) (context.Context, error) {
    95  	// configure parallel executors
    96  	ctx = setContextExecutors(ctx, cfg)
    97  
    98  	// configure license scanner
    99  	// skip injecting a license scanner if one already set on context
   100  	if licenses.IsContextLicenseScannerSet(ctx) {
   101  		return ctx, nil
   102  	}
   103  
   104  	return SetContextLicenseScanner(ctx, cfg.Licenses)
   105  }
   106  
   107  // SetContextLicenseScanner creates and sets a license scanner
   108  // on the provided context using the provided license config.
   109  func SetContextLicenseScanner(ctx context.Context, cfg cataloging.LicenseConfig) (context.Context, error) {
   110  	// inject a single license scanner and content config for all package cataloging tasks into context
   111  	licenseScanner, err := licenses.NewDefaultScanner(
   112  		licenses.WithCoverage(cfg.Coverage),
   113  	)
   114  	if err != nil {
   115  		return nil, fmt.Errorf("could not build licenseScanner for cataloging: %w", err)
   116  	}
   117  	ctx = licenses.SetContextLicenseScanner(ctx, licenseScanner)
   118  	return ctx, nil
   119  }
   120  
   121  func setContextExecutors(ctx context.Context, cfg *CreateSBOMConfig) context.Context {
   122  	parallelism := 0
   123  	if cfg != nil {
   124  		parallelism = cfg.Parallelism
   125  	}
   126  	// executor parallelism is: 0 == serial, no goroutines, 1 == max 1 goroutine
   127  	// so if they set 1, we just run in serial to avoid overhead, and treat 0 as default, reasonable max for the system
   128  	// negative is unbounded, so no need for any other special handling
   129  	switch parallelism {
   130  	case 0:
   131  		parallelism = runtime.NumCPU() * 4
   132  	case 1:
   133  		parallelism = 0 // run in serial, don't spawn goroutines
   134  	case -99:
   135  		parallelism = 1 // special case to catch incorrect executor usage during testing
   136  	}
   137  	// set up executors for each dimension we want to coordinate bounds for
   138  	if !sync.HasContextExecutor(ctx, cataloging.ExecutorCPU) {
   139  		ctx = sync.SetContextExecutor(ctx, cataloging.ExecutorCPU, sync.NewExecutor(parallelism))
   140  	}
   141  	if !sync.HasContextExecutor(ctx, cataloging.ExecutorFile) {
   142  		ctx = sync.SetContextExecutor(ctx, cataloging.ExecutorFile, sync.NewExecutor(parallelism))
   143  	}
   144  	return ctx
   145  }
   146  
   147  func monitorPackageCount(prog *monitor.TaskProgress) func(s *sbom.SBOM) {
   148  	return func(s *sbom.SBOM) {
   149  		count := humanize.Comma(int64(s.Artifacts.Packages.PackageCount()))
   150  		prog.AtomicStage.Set(fmt.Sprintf("%s packages", count))
   151  	}
   152  }
   153  
   154  func monitorPackageCatalogingTask() *monitor.TaskProgress {
   155  	info := monitor.GenericTask{
   156  		Title: monitor.Title{
   157  			Default: "Packages",
   158  		},
   159  		ID:            monitor.PackageCatalogingTaskID,
   160  		HideOnSuccess: false,
   161  		ParentID:      monitor.TopLevelCatalogingTaskID,
   162  	}
   163  
   164  	return bus.StartCatalogerTask(info, -1, "")
   165  }
   166  
   167  func monitorCatalogingTask(srcID artifact.ID, tasks [][]task.Task) *monitor.TaskProgress {
   168  	info := monitor.GenericTask{
   169  		Title: monitor.Title{
   170  			Default:      "Catalog contents",
   171  			WhileRunning: "Cataloging contents",
   172  			OnSuccess:    "Cataloged contents",
   173  		},
   174  		ID:            monitor.TopLevelCatalogingTaskID,
   175  		Context:       string(srcID),
   176  		HideOnSuccess: false,
   177  	}
   178  
   179  	var length int64
   180  	for _, tg := range tasks {
   181  		length += int64(len(tg))
   182  	}
   183  
   184  	return bus.StartCatalogerTask(info, length, "")
   185  }
   186  
   187  func formatTaskNames(tasks []task.Task) []string {
   188  	set := strset.New()
   189  	for _, td := range tasks {
   190  		if td == nil {
   191  			continue
   192  		}
   193  		set.Add(td.Name())
   194  	}
   195  	list := set.List()
   196  	sort.Strings(list)
   197  	return list
   198  }