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

     1  package ovalutil
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"regexp"
     8  
     9  	"github.com/quay/goval-parser/oval"
    10  	"github.com/quay/zlog"
    11  
    12  	"github.com/quay/claircore"
    13  )
    14  
    15  type DefinitionType string
    16  
    17  const (
    18  	CVEDefinition        DefinitionType = "cve"
    19  	RHBADefinition       DefinitionType = "rhba"
    20  	RHEADefinition       DefinitionType = "rhea"
    21  	RHSADefinition       DefinitionType = "rhsa"
    22  	UnaffectedDefinition DefinitionType = "unaffected"
    23  	NoneDefinition       DefinitionType = "none"
    24  )
    25  
    26  var moduleCommentRegex, definitionTypeRegex *regexp.Regexp
    27  
    28  func init() {
    29  	moduleCommentRegex = regexp.MustCompile(`(Module )(.*)( is enabled)`)
    30  	definitionTypeRegex = regexp.MustCompile(`^oval\:com\.redhat\.([a-z]+)\:def\:\d+$`)
    31  }
    32  
    33  // ProtoVulnsFunc allows a caller to create prototype vulnerabilities that will be
    34  // copied and further defined for every applicable oval.Criterion discovered.
    35  //
    36  // This allows the caller to use oval.Definition fields and closure syntax when
    37  // defining how a vulnerability should be parsed
    38  type ProtoVulnsFunc func(def oval.Definition) ([]*claircore.Vulnerability, error)
    39  
    40  // RPMDefsToVulns iterates over the definitions in an oval root and assumes RPMInfo objects and states.
    41  //
    42  // Each Criterion encountered with an EVR string will be translated into a claircore.Vulnerability
    43  func RPMDefsToVulns(ctx context.Context, root *oval.Root, protoVulns ProtoVulnsFunc) ([]*claircore.Vulnerability, error) {
    44  	ctx = zlog.ContextWithValues(ctx, "component", "ovalutil/RPMDefsToVulns")
    45  	vulns := make([]*claircore.Vulnerability, 0, 10000)
    46  	cris := []*oval.Criterion{}
    47  	for _, def := range root.Definitions.Definitions {
    48  		// create our prototype vulnerability
    49  		protoVulns, err := protoVulns(def)
    50  		if err != nil {
    51  			zlog.Debug(ctx).
    52  				Err(err).
    53  				Str("def_id", def.ID).
    54  				Msg("could not create prototype vulnerabilities")
    55  			continue
    56  		}
    57  		// recursively collect criterions for this definition
    58  		cris := cris[:0]
    59  		walkCriterion(ctx, &def.Criteria, &cris)
    60  		enabledModules := getEnabledModules(cris)
    61  		if len(enabledModules) == 0 {
    62  			// add default empty module
    63  			enabledModules = append(enabledModules, "")
    64  		}
    65  		// unpack criterions into vulnerabilities
    66  		for _, criterion := range cris {
    67  			// if test object is not rmpinfo_test the provided test is not
    68  			// associated with a package. this criterion will be skipped.
    69  			test, err := TestLookup(root, criterion.TestRef, func(kind string) bool {
    70  				if kind != "rpminfo_test" {
    71  					return false
    72  				}
    73  				return true
    74  			})
    75  			switch {
    76  			case errors.Is(err, nil):
    77  			case errors.Is(err, errTestSkip):
    78  				continue
    79  			default:
    80  				zlog.Debug(ctx).Str("test_ref", criterion.TestRef).Msg("test ref lookup failure. moving to next criterion")
    81  				continue
    82  			}
    83  
    84  			objRefs := test.ObjectRef()
    85  			stateRefs := test.StateRef()
    86  
    87  			// from the rpminfo_test specification found here: https://oval.mitre.org/language/version5.7/ovaldefinition/documentation/linux-definitions-schema.html
    88  			// "The required object element references a rpminfo_object and the optional state element specifies the data to check.
    89  			//  The evaluation of the test is guided by the check attribute that is inherited from the TestType."
    90  			//
    91  			// thus we *should* only need to care about a single rpminfo_object and optionally a state object providing the package's fixed-in version.
    92  
    93  			objRef := objRefs[0].ObjectRef
    94  			object, err := rpmObjectLookup(root, objRef)
    95  			switch {
    96  			case errors.Is(err, nil):
    97  			case errors.Is(err, errObjectSkip):
    98  				// We only handle rpminfo_objects.
    99  				continue
   100  			default:
   101  				zlog.Debug(ctx).
   102  					Err(err).
   103  					Str("object_ref", objRef).
   104  					Msg("failed object lookup. moving to next criterion")
   105  				continue
   106  			}
   107  
   108  			// state refs are optional, so this is not a requirement.
   109  			// if a state object is discovered, we can use it to find
   110  			// the "fixed-in-version"
   111  			var state *oval.RPMInfoState
   112  			if len(stateRefs) > 0 {
   113  				stateRef := stateRefs[0].StateRef
   114  				state, err = rpmStateLookup(root, stateRef)
   115  				if err != nil {
   116  					zlog.Debug(ctx).
   117  						Err(err).
   118  						Str("state_ref", stateRef).
   119  						Msg("failed state lookup. moving to next criterion")
   120  					continue
   121  				}
   122  				// if we find a state, but this state does not contain an EVR,
   123  				// we are not looking at a linux package.
   124  				if state.EVR == nil {
   125  					continue
   126  				}
   127  			}
   128  
   129  			for _, module := range enabledModules {
   130  				for _, protoVuln := range protoVulns {
   131  					vuln := *protoVuln
   132  					vuln.Package = &claircore.Package{
   133  						Name:   object.Name,
   134  						Module: module,
   135  						Kind:   claircore.BINARY,
   136  					}
   137  					if state != nil {
   138  						vuln.FixedInVersion = state.EVR.Body
   139  						if state.Arch != nil {
   140  							vuln.ArchOperation = mapArchOp(state.Arch.Operation)
   141  							vuln.Package.Arch = state.Arch.Body
   142  						}
   143  					}
   144  					vulns = append(vulns, &vuln)
   145  				}
   146  			}
   147  		}
   148  	}
   149  
   150  	return vulns, nil
   151  }
   152  
   153  func mapArchOp(op oval.Operation) claircore.ArchOp {
   154  	switch op {
   155  	case oval.OpEquals:
   156  		return claircore.OpEquals
   157  	case oval.OpNotEquals:
   158  		return claircore.OpNotEquals
   159  	case oval.OpPatternMatch:
   160  		return claircore.OpPatternMatch
   161  	default:
   162  	}
   163  	return claircore.ArchOp(0)
   164  }
   165  
   166  // walkCriterion recursively extracts Criterions from a root Crteria node in a depth
   167  // first manor.
   168  //
   169  // a pointer to a slice header is modified in place when appending
   170  func walkCriterion(ctx context.Context, node *oval.Criteria, cris *[]*oval.Criterion) {
   171  	// recursive to leafs
   172  	for _, criteria := range node.Criterias {
   173  		walkCriterion(ctx, &criteria, cris)
   174  	}
   175  	// search for criterions at current node
   176  	for _, criterion := range node.Criterions {
   177  		c := criterion
   178  		*cris = append(*cris, &c)
   179  	}
   180  }
   181  
   182  func getEnabledModules(cris []*oval.Criterion) []string {
   183  	enabledModules := []string{}
   184  	for _, criterion := range cris {
   185  		matches := moduleCommentRegex.FindStringSubmatch(criterion.Comment)
   186  		if matches != nil && len(matches) > 2 && matches[2] != "" {
   187  			moduleNameStream := matches[2]
   188  			enabledModules = append(enabledModules, moduleNameStream)
   189  		}
   190  	}
   191  	return enabledModules
   192  }
   193  
   194  func rpmObjectLookup(root *oval.Root, ref string) (*oval.RPMInfoObject, error) {
   195  	kind, index, err := root.Objects.Lookup(ref)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	if kind != "rpminfo_object" {
   200  		return nil, fmt.Errorf("oval: got kind %q: %w", kind, errObjectSkip)
   201  	}
   202  	return &root.Objects.RPMInfoObjects[index], nil
   203  }
   204  
   205  func rpmStateLookup(root *oval.Root, ref string) (*oval.RPMInfoState, error) {
   206  	kind, index, err := root.States.Lookup(ref)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	if kind != "rpminfo_state" {
   211  		return nil, fmt.Errorf("bad kind: %s", kind)
   212  	}
   213  	return &root.States.RPMInfoStates[index], nil
   214  }
   215  
   216  // GetDefinitionType parses an OVAL definition and extracts its type from ID.
   217  func GetDefinitionType(def oval.Definition) (DefinitionType, error) {
   218  	match := definitionTypeRegex.FindStringSubmatch(def.ID)
   219  	if len(match) != 2 { // we should have match of the whole string and one submatch
   220  		return "", errors.New("cannot parse definition ID for its type")
   221  	}
   222  	return DefinitionType(match[1]), nil
   223  }