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