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  }