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 }