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 }