github.com/quay/claircore@v1.5.28/internal/matcher/match.go (about) 1 package matcher 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "runtime" 9 "sync" 10 "sync/atomic" 11 12 "github.com/quay/zlog" 13 "golang.org/x/sync/errgroup" 14 15 "github.com/quay/claircore" 16 "github.com/quay/claircore/datastore" 17 "github.com/quay/claircore/libvuln/driver" 18 ) 19 20 // BUG(hank) Match and EnrichedMatch have different semantics for errors 21 // returned by their inferior processes: Match runs all of them to completion or 22 // the incoming Context is canceled. EnrichedMatch eagerly cancels all Matchers 23 // upon the first error, ala "errgroup" semantics but purposefully does not 24 // return errors on any Enricher errors. If I recall correctly, this was on 25 // purpose. The Match function is probably obsolete now; we should work on 26 // removing it from the relevant APIs. 27 28 // Match receives an IndexReport and creates a VulnerabilityReport containing matched vulnerabilities 29 func Match(ctx context.Context, ir *claircore.IndexReport, matchers []driver.Matcher, store datastore.Vulnerability) (*claircore.VulnerabilityReport, error) { 30 // the vulnerability report we are creating 31 vr := &claircore.VulnerabilityReport{ 32 Hash: ir.Hash, 33 Packages: ir.Packages, 34 Environments: ir.Environments, 35 Distributions: ir.Distributions, 36 Repositories: ir.Repositories, 37 Vulnerabilities: map[string]*claircore.Vulnerability{}, 38 PackageVulnerabilities: map[string][]string{}, 39 } 40 lim := runtime.GOMAXPROCS(0) 41 42 // extract IndexRecords from the IndexReport 43 records := ir.IndexRecords() 44 // a channel where concurrent controllers will deliver vulnerabilities affecting a package. 45 // maps a package id to a list of vulnerabilities. 46 ctrlC := make(chan map[string][]*claircore.Vulnerability, lim) 47 var errMu sync.Mutex 48 errs := make([]error, 0, lim) 49 // fan out all controllers, write their output to ctrlC, close ctrlC once all writers finish 50 go func() { 51 defer close(ctrlC) 52 var wg sync.WaitGroup 53 wg.Add(len(matchers)) 54 for i := range matchers { 55 m := matchers[i] 56 go func() { 57 defer wg.Done() 58 mc := NewController(m, store) 59 vulns, err := mc.Match(ctx, records) 60 if err != nil { 61 errMu.Lock() 62 errs = append(errs, err) 63 errMu.Unlock() 64 return 65 } 66 // in event of slow reader go routines will block 67 ctrlC <- vulns 68 }() 69 } 70 wg.Wait() 71 }() 72 // loop ranges until ctrlC is closed and fully drained, ctrlC is guaranteed to close 73 for vulnsByPackage := range ctrlC { 74 for pkgID, vulns := range vulnsByPackage { 75 for _, vuln := range vulns { 76 vr.Vulnerabilities[vuln.ID] = vuln 77 vr.PackageVulnerabilities[pkgID] = append(vr.PackageVulnerabilities[pkgID], vuln.ID) 78 } 79 } 80 } 81 return vr, errors.Join(errs...) 82 } 83 84 // Store is the interface that can retrieve Enrichments and Vulnerabilities. 85 type Store interface { 86 datastore.Vulnerability 87 datastore.Enrichment 88 } 89 90 // EnrichedMatch receives an IndexReport and creates a VulnerabilityReport 91 // containing matched vulnerabilities and any relevant enrichments. 92 func EnrichedMatch(ctx context.Context, ir *claircore.IndexReport, ms []driver.Matcher, es []driver.Enricher, s Store) (*claircore.VulnerabilityReport, error) { 93 // the vulnerability report we are creating 94 vr := &claircore.VulnerabilityReport{ 95 Hash: ir.Hash, 96 Packages: ir.Packages, 97 Environments: ir.Environments, 98 Distributions: ir.Distributions, 99 Repositories: ir.Repositories, 100 Vulnerabilities: map[string]*claircore.Vulnerability{}, 101 PackageVulnerabilities: map[string][]string{}, 102 // The Enrichments member isn't constructed here because it's 103 // constructed separately and then added. 104 } 105 // extract IndexRecords from the IndexReport 106 records := ir.IndexRecords() 107 lim := runtime.GOMAXPROCS(0) 108 109 // Set up a pool to run matchers 110 mCh := make(chan driver.Matcher) 111 vCh := make(chan map[string][]*claircore.Vulnerability, lim) 112 mg, mctx := errgroup.WithContext(ctx) // match group, match context 113 for i := 0; i < lim; i++ { 114 mg.Go(func() error { // Worker 115 var m driver.Matcher 116 for m = range mCh { 117 select { 118 case <-mctx.Done(): 119 return mctx.Err() 120 default: 121 } 122 vs, err := NewController(m, s).Match(mctx, records) 123 if err != nil { 124 return fmt.Errorf("matcher error: %w", err) 125 } 126 vCh <- vs 127 } 128 return nil 129 }) 130 } 131 // Set up a pool to watch the matchers and attach results to the report. 132 var vg errgroup.Group // Used for easy grouping. Does cancellation on a previous Context. 133 vg.Go(func() error { // Pipeline watcher 134 Send: 135 for _, m := range ms { 136 select { 137 case <-mctx.Done(): 138 break Send 139 case mCh <- m: 140 } 141 } 142 close(mCh) 143 defer close(vCh) 144 if err := mg.Wait(); err != nil { 145 return err 146 } 147 return nil 148 }) 149 vg.Go(func() error { // Collector 150 for pkgVuln := range vCh { 151 for pkg, vs := range pkgVuln { 152 for _, v := range vs { 153 vr.Vulnerabilities[v.ID] = v 154 vr.PackageVulnerabilities[pkg] = append(vr.PackageVulnerabilities[pkg], v.ID) 155 } 156 } 157 } 158 return nil 159 }) 160 if err := vg.Wait(); err != nil { 161 return nil, err 162 } 163 164 // Set up a pool to run the enrichers and attach results to the report. 165 eCh := make(chan driver.Enricher) 166 type entry struct { 167 kind string 168 msg []json.RawMessage 169 } 170 rCh := make(chan *entry, lim) 171 eg, ectx := errgroup.WithContext(ctx) 172 eg.Go(func() error { // Sender 173 Send: 174 for _, e := range es { 175 select { 176 case eCh <- e: 177 case <-ectx.Done(): 178 break Send 179 } 180 } 181 close(eCh) 182 return nil 183 }) 184 eg.Go(func() error { // Collector 185 em := make(map[string][]json.RawMessage) 186 for e := range rCh { 187 em[e.kind] = append(em[e.kind], e.msg...) 188 } 189 vr.Enrichments = em 190 return nil 191 }) 192 // Use an atomic to track closing the results channel. 193 ct := uint32(lim) 194 for i := 0; i < lim; i++ { 195 eg.Go(func() error { // Worker 196 defer func() { 197 if atomic.AddUint32(&ct, ^uint32(0)) == 0 { 198 close(rCh) 199 } 200 }() 201 var e driver.Enricher 202 for e = range eCh { 203 kind, msg, err := e.Enrich(ectx, getter(s, e.Name()), vr) 204 if err != nil { 205 zlog.Error(ctx). 206 Err(err). 207 Msg("enrichment error") 208 continue 209 } 210 if len(msg) == 0 { 211 zlog.Debug(ctx). 212 Str("name", e.Name()). 213 Msg("enricher reported nothing, skipping") 214 continue 215 } 216 res := entry{ 217 msg: msg, 218 kind: kind, 219 } 220 select { 221 case rCh <- &res: 222 case <-ectx.Done(): 223 return ectx.Err() 224 } 225 } 226 return nil 227 }) 228 } 229 if err := eg.Wait(); err != nil { 230 return nil, err 231 } 232 233 return vr, nil 234 } 235 236 // Getter returns a type implementing driver.EnrichmentGetter. 237 func getter(s datastore.Enrichment, name string) *enrichmentGetter { 238 return &enrichmentGetter{s: s, name: name} 239 } 240 241 type enrichmentGetter struct { 242 s datastore.Enrichment 243 name string 244 } 245 246 var _ driver.EnrichmentGetter = (*enrichmentGetter)(nil) 247 248 func (e *enrichmentGetter) GetEnrichment(ctx context.Context, tags []string) ([]driver.EnrichmentRecord, error) { 249 return e.s.GetEnrichment(ctx, e.name, tags) 250 }