github.com/quay/claircore@v1.5.28/datastore/postgres/affectedmanifest.go (about) 1 package postgres 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strconv" 8 "time" 9 10 "github.com/jackc/pgtype" 11 "github.com/jackc/pgx/v4" 12 "github.com/jackc/pgx/v4/pgxpool" 13 "github.com/prometheus/client_golang/prometheus" 14 "github.com/prometheus/client_golang/prometheus/promauto" 15 "github.com/quay/zlog" 16 17 "github.com/quay/claircore" 18 ) 19 20 var ( 21 // ErrNotIndexed indicates the vulnerability being queried has a dist or repo not 22 // indexed into the database. 23 ErrNotIndexed = fmt.Errorf("vulnerability containers data not indexed by any scannners") 24 affectedManifestsCounter = promauto.NewCounterVec( 25 prometheus.CounterOpts{ 26 Namespace: "claircore", 27 Subsystem: "indexer", 28 Name: "affectedmanifests_total", 29 Help: "Total number of database queries issued in the AffectedManifests method.", 30 }, 31 []string{"query"}, 32 ) 33 affectedManifestsDuration = promauto.NewHistogramVec( 34 prometheus.HistogramOpts{ 35 Namespace: "claircore", 36 Subsystem: "indexer", 37 Name: "affectedmanifests_duration_seconds", 38 Help: "The duration of all queries issued in the AffectedManifests method", 39 }, 40 []string{"query"}, 41 ) 42 protoRecordCounter = promauto.NewCounterVec( 43 prometheus.CounterOpts{ 44 Namespace: "claircore", 45 Subsystem: "indexer", 46 Name: "protorecord_total", 47 Help: "Total number of database queries issued in the protoRecord method.", 48 }, 49 []string{"query"}, 50 ) 51 protoRecordDuration = promauto.NewHistogramVec( 52 prometheus.HistogramOpts{ 53 Namespace: "claircore", 54 Subsystem: "indexer", 55 Name: "protorecord_duration_seconds", 56 Help: "The duration of all queries issued in the protoRecord method", 57 }, 58 []string{"query"}, 59 ) 60 ) 61 62 // AffectedManifests finds the manifests digests which are affected by the provided vulnerability. 63 // 64 // An exhaustive search for all indexed packages of the same name as the vulnerability is performed. 65 // 66 // The list of packages is filtered down to only the affected set. 67 // 68 // The manifest index is then queried to resolve a list of manifest hashes containing the affected 69 // artifacts. 70 func (s *IndexerStore) AffectedManifests(ctx context.Context, v claircore.Vulnerability, vulnFunc claircore.CheckVulnernableFunc) ([]claircore.Digest, error) { 71 const ( 72 selectPackages = ` 73 SELECT 74 id, 75 name, 76 version, 77 kind, 78 norm_kind, 79 norm_version, 80 module, 81 arch 82 FROM 83 package 84 WHERE 85 name = $1; 86 ` 87 selectAffected = ` 88 SELECT 89 manifest.hash 90 FROM 91 manifest_index 92 JOIN manifest ON 93 manifest_index.manifest_id = manifest.id 94 WHERE 95 package_id = $1 96 AND ( 97 CASE 98 WHEN $2::INT8 IS NULL THEN dist_id IS NULL 99 ELSE dist_id = $2 100 END 101 ) 102 AND ( 103 CASE 104 WHEN $3::INT8 IS NULL THEN repo_id IS NULL 105 ELSE repo_id = $3 106 END 107 ); 108 ` 109 ) 110 ctx = zlog.ContextWithValues(ctx, "component", "datastore/postgres/affectedManifests") 111 112 // confirm the incoming vuln can be 113 // resolved into a prototype index record 114 pr, err := protoRecord(ctx, s.pool, v) 115 switch { 116 case err == nil: 117 // break out 118 case errors.Is(err, ErrNotIndexed): 119 // This is a common case: the system knows of a vulnerability but 120 // doesn't know of any manifests it could apply to. 121 return nil, nil 122 default: 123 return nil, err 124 } 125 126 // collect all packages which may be affected 127 // by the vulnerability in question. 128 pkgsToFilter := []claircore.Package{} 129 130 start := time.Now() 131 rows, err := s.pool.Query(ctx, selectPackages, v.Package.Name) 132 switch { 133 case errors.Is(err, nil): 134 case errors.Is(err, pgx.ErrNoRows): 135 return []claircore.Digest{}, nil 136 default: 137 return nil, fmt.Errorf("failed to query packages associated with vulnerability %q: %w", v.ID, err) 138 } 139 defer rows.Close() 140 affectedManifestsCounter.WithLabelValues("selectPackages").Add(1) 141 affectedManifestsDuration.WithLabelValues("selectPackages").Observe(time.Since(start).Seconds()) 142 143 for rows.Next() { 144 var pkg claircore.Package 145 var id int64 146 var nKind *string 147 var nVer pgtype.Int4Array 148 err := rows.Scan( 149 &id, 150 &pkg.Name, 151 &pkg.Version, 152 &pkg.Kind, 153 &nKind, 154 &nVer, 155 &pkg.Module, 156 &pkg.Arch, 157 ) 158 if err != nil { 159 return nil, fmt.Errorf("failed to scan package: %w", err) 160 } 161 idStr := strconv.FormatInt(id, 10) 162 pkg.ID = idStr 163 if nKind != nil { 164 pkg.NormalizedVersion.Kind = *nKind 165 for i, n := range nVer.Elements { 166 pkg.NormalizedVersion.V[i] = n.Int 167 } 168 } 169 pkgsToFilter = append(pkgsToFilter, pkg) 170 } 171 zlog.Debug(ctx).Int("count", len(pkgsToFilter)).Msg("packages to filter") 172 if err := rows.Err(); err != nil { 173 return nil, fmt.Errorf("error scanning packages: %w", err) 174 } 175 176 // for each package discovered create an index record 177 // and determine if any in-tree matcher finds the record vulnerable 178 var filteredRecords []claircore.IndexRecord 179 for _, pkg := range pkgsToFilter { 180 pr.Package = &pkg 181 match, err := vulnFunc(ctx, &pr, &v) 182 if err != nil { 183 return nil, err 184 } 185 if match { 186 p := pkg // make a copy, or else you'll get a stale reference later 187 filteredRecords = append(filteredRecords, claircore.IndexRecord{ 188 Package: &p, 189 Distribution: pr.Distribution, 190 Repository: pr.Repository, 191 }) 192 } 193 } 194 zlog.Debug(ctx).Int("count", len(filteredRecords)).Msg("vulnerable index records") 195 196 // Query the manifest index for manifests containing the vulnerable 197 // IndexRecords and create a set containing each unique manifest. 198 set := map[string]struct{}{} 199 out := []claircore.Digest{} 200 for _, record := range filteredRecords { 201 v, err := toValues(record) 202 if err != nil { 203 return nil, fmt.Errorf("failed to resolve record %+v to sql values for query: %w", record, err) 204 } 205 206 err = func() error { 207 start := time.Now() 208 rows, err := s.pool.Query(ctx, 209 selectAffected, 210 record.Package.ID, 211 v[2], 212 v[3], 213 ) 214 switch { 215 case errors.Is(err, nil): 216 case errors.Is(err, pgx.ErrNoRows): 217 err = fmt.Errorf("failed to query the manifest index: %w", err) 218 fallthrough 219 default: 220 return err 221 } 222 defer rows.Close() 223 affectedManifestsCounter.WithLabelValues("selectAffected").Add(1) 224 affectedManifestsDuration.WithLabelValues("selectAffected").Observe(time.Since(start).Seconds()) 225 226 for rows.Next() { 227 var hash claircore.Digest 228 err := rows.Scan(&hash) 229 if err != nil { 230 return fmt.Errorf("failed scanning manifest hash into digest: %w", err) 231 } 232 if _, ok := set[hash.String()]; !ok { 233 set[hash.String()] = struct{}{} 234 out = append(out, hash) 235 } 236 } 237 return rows.Err() 238 }() 239 if err != nil { 240 return nil, err 241 } 242 } 243 zlog.Debug(ctx).Int("count", len(out)).Msg("affected manifests") 244 return out, nil 245 } 246 247 // protoRecord is a helper method which resolves a Vulnerability to an IndexRecord with no Package defined. 248 // 249 // it is an error for both a distribution and a repo to be missing from the Vulnerability. 250 func protoRecord(ctx context.Context, pool *pgxpool.Pool, v claircore.Vulnerability) (claircore.IndexRecord, error) { 251 const ( 252 selectDist = ` 253 SELECT id 254 FROM dist 255 WHERE arch = $1 256 AND cpe = $2 257 AND did = $3 258 AND name = $4 259 AND pretty_name = $5 260 AND version = $6 261 AND version_code_name = $7 262 AND version_id = $8; 263 ` 264 selectRepo = ` 265 SELECT id 266 FROM repo 267 WHERE name = $1 268 AND key = $2 269 AND uri = $3; 270 ` 271 timeout = 5 * time.Second 272 ) 273 ctx = zlog.ContextWithValues(ctx, "component", "datastore/postgres/protoRecord") 274 275 protoRecord := claircore.IndexRecord{} 276 // fill dist into prototype index record if exists 277 if (v.Dist != nil) && (v.Dist.Name != "") { 278 start := time.Now() 279 row := pool.QueryRow(ctx, 280 selectDist, 281 v.Dist.Arch, 282 v.Dist.CPE, 283 v.Dist.DID, 284 v.Dist.Name, 285 v.Dist.PrettyName, 286 v.Dist.Version, 287 v.Dist.VersionCodeName, 288 v.Dist.VersionID, 289 ) 290 var id pgtype.Int8 291 err := row.Scan(&id) 292 if err != nil { 293 if !errors.Is(err, pgx.ErrNoRows) { 294 return protoRecord, fmt.Errorf("failed to scan dist: %w", err) 295 } 296 } 297 protoRecordCounter.WithLabelValues("selectDist").Add(1) 298 protoRecordDuration.WithLabelValues("selectDist").Observe(time.Since(start).Seconds()) 299 300 if id.Status == pgtype.Present { 301 id := strconv.FormatInt(id.Int, 10) 302 protoRecord.Distribution = &claircore.Distribution{ 303 ID: id, 304 Arch: v.Dist.Arch, 305 CPE: v.Dist.CPE, 306 DID: v.Dist.DID, 307 Name: v.Dist.Name, 308 PrettyName: v.Dist.PrettyName, 309 Version: v.Dist.Version, 310 VersionCodeName: v.Dist.VersionCodeName, 311 VersionID: v.Dist.VersionID, 312 } 313 zlog.Debug(ctx).Str("id", id).Msg("discovered distribution id") 314 } 315 } 316 317 // fill repo into prototype index record if exists 318 if (v.Repo != nil) && (v.Repo.Name != "") { 319 start := time.Now() 320 row := pool.QueryRow(ctx, selectRepo, 321 v.Repo.Name, 322 v.Repo.Key, 323 v.Repo.URI, 324 ) 325 var id pgtype.Int8 326 err := row.Scan(&id) 327 if err != nil { 328 if !errors.Is(err, pgx.ErrNoRows) { 329 return protoRecord, fmt.Errorf("failed to scan repo: %w", err) 330 } 331 } 332 protoRecordCounter.WithLabelValues("selectDist").Add(1) 333 protoRecordDuration.WithLabelValues("selectDist").Observe(time.Since(start).Seconds()) 334 335 if id.Status == pgtype.Present { 336 id := strconv.FormatInt(id.Int, 10) 337 protoRecord.Repository = &claircore.Repository{ 338 ID: id, 339 Key: v.Repo.Key, 340 Name: v.Repo.Name, 341 URI: v.Repo.URI, 342 } 343 zlog.Debug(ctx).Str("id", id).Msg("discovered repo id") 344 } 345 } 346 347 // we need at least a repo or distribution to continue 348 if (protoRecord.Distribution == nil) && (protoRecord.Repository == nil) { 349 return protoRecord, ErrNotIndexed 350 } 351 352 return protoRecord, nil 353 }