github.com/quay/claircore@v1.5.28/pkg/ovalutil/dpkg.go (about)

     1  package ovalutil
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/quay/goval-parser/oval"
    11  	"github.com/quay/zlog"
    12  	"github.com/rs/zerolog"
    13  
    14  	"github.com/quay/claircore"
    15  )
    16  
    17  // PackageExpansionFunc allows a caller to expand the inserted vulns. For example
    18  // when the OVAL DB reports vulnerabilities from the source package only (Debian). Or
    19  // the name field has a var_ref indicating a variable lookup is needed (Ubuntu).
    20  type PackageExpansionFunc func(def oval.Definition, name *oval.DpkgName) []string
    21  
    22  // DpkgDefsToVulns iterates over the definitions in an oval root and assumes DpkgInfo objects and states.
    23  //
    24  // Each Criterion encountered with an EVR string will be translated into a claircore.Vulnerability
    25  func DpkgDefsToVulns(ctx context.Context, root *oval.Root, protoVulns ProtoVulnsFunc, expansionFunc PackageExpansionFunc) ([]*claircore.Vulnerability, error) {
    26  	ctx = zlog.ContextWithValues(ctx, "component", "ovalutil/DpkgDefsToVulns")
    27  	vulns := make([]*claircore.Vulnerability, 0, 10000)
    28  	pkgcache := map[string]*claircore.Package{}
    29  	cris := []*oval.Criterion{}
    30  	var stats struct {
    31  		Test, Obj, State int
    32  	}
    33  	badvers := make(map[string]string)
    34  
    35  	for _, def := range root.Definitions.Definitions {
    36  		// create our prototype vulnerability
    37  		protoVulns, err := protoVulns(def)
    38  		if err != nil {
    39  			zlog.Debug(ctx).
    40  				Err(err).
    41  				Str("def_id", def.ID).
    42  				Msg("could not create prototype vulnerabilities")
    43  			continue
    44  		}
    45  		// recursively collect criterions for this definition
    46  		cris := cris[:0]
    47  		walkCriterion(ctx, &def.Criteria, &cris)
    48  		// unpack criterions into vulnerabilities
    49  		for _, criterion := range cris {
    50  			test, err := TestLookup(root, criterion.TestRef, func(kind string) bool {
    51  				return kind == "dpkginfo_test"
    52  			})
    53  			switch {
    54  			case errors.Is(err, nil):
    55  			case errors.Is(err, errTestSkip):
    56  				continue
    57  			default:
    58  				stats.Test++
    59  				continue
    60  			}
    61  
    62  			objRefs := test.ObjectRef()
    63  			stateRefs := test.StateRef()
    64  
    65  			// from the dpkginfo_test specification found here: https://oval.mitre.org/language/version5.7/ovaldefinition/documentation/linux-definitions-schema.html
    66  			// The required object element references a dpkginfo_object and the optional state element specifies the data to check.
    67  			// The evaluation of the test is guided by the check attribute that is inherited from the TestType.
    68  			//
    69  			// thus we *should* only need to care about a single dpkginfo_object and optionally a state object providing the package's fixed-in version.
    70  
    71  			objRef := objRefs[0].ObjectRef
    72  			object, err := dpkgObjectLookup(root, objRef)
    73  			switch {
    74  			case errors.Is(err, nil):
    75  			case errors.Is(err, errObjectSkip):
    76  				// We only handle dpkginfo_objects.
    77  				continue
    78  			default:
    79  				if err != nil {
    80  					stats.Obj++
    81  					continue
    82  				}
    83  			}
    84  
    85  			var state *oval.DpkgInfoState
    86  			if len(stateRefs) > 0 {
    87  				stateRef := stateRefs[0].StateRef
    88  				state, err = dpkgStateLookup(root, stateRef)
    89  				if err != nil {
    90  					stats.State++
    91  					continue
    92  				}
    93  				// if EVR tag not present this is not a linux package
    94  				// see oval definitions for more details
    95  				if state.EVR == nil {
    96  					continue
    97  				}
    98  			}
    99  
   100  			for _, protoVuln := range protoVulns {
   101  				name := object.Name
   102  				var ns []string
   103  				ns = append(ns, expansionFunc(def, name)...)
   104  				for _, n := range ns {
   105  					vuln := *protoVuln
   106  					if state != nil {
   107  						// Ubuntu has issues with whitespace, so fix it for them.
   108  						v := strings.TrimSpace(state.EVR.Body)
   109  						if !validVersion.MatchString(v) {
   110  							badvers[n] = v
   111  							continue
   112  						}
   113  						vuln.FixedInVersion = state.EVR.Body
   114  						if state.Arch != nil {
   115  							vuln.ArchOperation = mapArchOp(state.Arch.Operation)
   116  							vuln.Package.Arch = state.Arch.Body
   117  						}
   118  					}
   119  					if pkg, ok := pkgcache[n]; !ok {
   120  						p := &claircore.Package{
   121  							Name: n,
   122  							Kind: claircore.BINARY,
   123  						}
   124  						pkgcache[n] = p
   125  						vuln.Package = p
   126  					} else {
   127  						vuln.Package = pkg
   128  					}
   129  					vulns = append(vulns, &vuln)
   130  				}
   131  			}
   132  		}
   133  	}
   134  	zlog.Debug(ctx).
   135  		Int("test", stats.Test).
   136  		Int("object", stats.Obj).
   137  		Int("state", stats.State).
   138  		Msg("ref lookup failures")
   139  	if ev := zlog.Debug(ctx); ev.Enabled() {
   140  		d := zerolog.Dict()
   141  		for k, v := range badvers {
   142  			d.Str(k, v)
   143  		}
   144  		ev.
   145  			Dict("package-version", d).
   146  			Msg("bogus versions")
   147  	}
   148  	return vulns, nil
   149  }
   150  
   151  // ValidVersion is a regexp that allows all valid Debian version strings.
   152  // It's more permissive than the actual algorithm; see also deb-version(5).
   153  //
   154  // Notably, this allows underscores in the upstream part and doesn't enforce that parts start
   155  // with a numeric.
   156  var validVersion = regexp.MustCompile(`\A([0-9]+:)?[-_A-Za-z0-9.+:~]+(-[A-Za-z0-9+.~]+)?\z`)
   157  
   158  func dpkgStateLookup(root *oval.Root, ref string) (*oval.DpkgInfoState, error) {
   159  	kind, i, err := root.States.Lookup(ref)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	if kind != "dpkginfo_state" {
   164  		return nil, fmt.Errorf("oval: got kind %q: %w", kind, errStateSkip)
   165  	}
   166  	return &root.States.DpkgInfoStates[i], nil
   167  }
   168  
   169  func dpkgObjectLookup(root *oval.Root, ref string) (*oval.DpkgInfoObject, error) {
   170  	kind, i, err := root.Objects.Lookup(ref)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	if kind != "dpkginfo_object" {
   175  		return nil, fmt.Errorf("oval: got kind %q: %w", kind, errObjectSkip)
   176  	}
   177  	return &root.Objects.DpkgInfoObjects[i], nil
   178  }