-
Notifications
You must be signed in to change notification settings - Fork 45
/
discover.go
199 lines (158 loc) · 5.32 KB
/
discover.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
package advisory
import (
"context"
"fmt"
"sort"
"chainguard.dev/melange/pkg/config"
"github.com/chainguard-dev/go-apk/pkg/apk"
"github.com/samber/lo"
"github.com/wolfi-dev/wolfictl/pkg/configs"
v2 "github.com/wolfi-dev/wolfictl/pkg/configs/advisory/v2"
"github.com/wolfi-dev/wolfictl/pkg/index"
"github.com/wolfi-dev/wolfictl/pkg/vuln"
)
type DiscoverOptions struct {
// SelectedPackages is a list of packages to include in search. If empty, all
// packages will be included in search.
SelectedPackages []string
// BuildCfgs is the Index of build configurations on which to operate.
BuildCfgs *configs.Index[config.Configuration]
// AdvisoryDocs is the Index of advisory documents on which to operate.
AdvisoryDocs *configs.Index[v2.Document]
// PackageRepositoryURL is the URL to the distro's package repository (e.g.
// "https://packages.wolfi.dev/os").
PackageRepositoryURL string
// The Arches to select during discovery (e.g. "x86_64").
Arches []string
// VulnerabilityDetector is how Discover finds vulnerabilities for packages.
VulnerabilityDetector vuln.Detector
// VulnEvents is a channel of events that occur during vulnerability discovery.
VulnEvents chan<- interface{}
}
// Discover searches for new vulnerabilities that match packages in a config
// index, and adds new advisories to configs for vulnerabilities that haven't
// been noted yet.
func Discover(ctx context.Context, opts DiscoverOptions) error {
var packagesToLookup []string
// If the user has specified a set of packages to search for, we'll only search
// for those. This also helps us skip the cost of downloading APKINDEXes.
if len(opts.SelectedPackages) >= 1 {
packagesToLookup = opts.SelectedPackages
} else {
packageRepositoryURL := opts.PackageRepositoryURL
if packageRepositoryURL == "" {
return fmt.Errorf("package repository URL must be specified")
}
var apkindexes []*apk.APKIndex
for _, arch := range opts.Arches {
apkindex, err := index.Index(arch, packageRepositoryURL)
if err != nil {
return fmt.Errorf("unable to get APKINDEX for arch %q: %w", arch, err)
}
apkindexes = append(apkindexes, apkindex)
}
packagesToLookup = uniquePackageNamesFromAPKINDEXes(apkindexes)
}
for _, pkg := range packagesToLookup {
if err := ctx.Err(); err != nil {
return err
}
pkg := pkg
err := opts.discoverMatchesForPackage(ctx, pkg)
if err != nil {
return err
}
}
opts.VulnEvents <- vuln.EventMatchingFinished{}
return nil
}
func (opts DiscoverOptions) discoverMatchesForPackage(ctx context.Context, pkg string) error {
opts.VulnEvents <- vuln.EventPackageMatchingStarting{Package: pkg}
matches, err := opts.VulnerabilityDetector.VulnerabilitiesForPackage(ctx, pkg)
if ctx.Err() != nil {
return err
}
if err != nil {
opts.VulnEvents <- vuln.EventPackageMatchingError{Package: pkg, Err: err}
}
matches = opts.filterMatchesForPackage(pkg, matches)
opts.VulnEvents <- vuln.EventPackageMatchingFinished{Package: pkg, Matches: matches}
for i := range matches {
match := matches[i]
err := Create(ctx, Request{
Package: pkg,
VulnerabilityID: match.Vulnerability.ID,
Aliases: nil,
Event: advisoryEventForNewDiscovery(match),
}, CreateOptions{opts.AdvisoryDocs})
if err != nil {
return err
}
}
return nil
}
func (opts DiscoverOptions) filterMatchesForPackage(pkg string, matches []vuln.Match) []vuln.Match {
buildCfgEntry, _ := opts.BuildCfgs.Select().WhereName(pkg).First() //nolint:errcheck
buildCfg := buildCfgEntry.Configuration()
var filteredMatches []vuln.Match
for i := range matches {
match := matches[i]
if !match.CPEFound.VersionRange.Includes(buildCfg.Package.Version) {
continue
}
vulnID := match.Vulnerability.ID
// TODO: We shouldn't need to know about documents here, we should just have a
// query against the total dataset for this package-vuln pair.
advisoryDocuments := opts.AdvisoryDocs.Select().WhereName(pkg)
if advisoryDocuments.Len() == 0 {
filteredMatches = append(filteredMatches, match)
continue
}
// there's an existing advisories config
advCfgEntry, _ := advisoryDocuments.First() //nolint:errcheck
document := advCfgEntry.Configuration()
if _, exists := document.Advisories.GetByVulnerability(vulnID); exists {
// advisory already exists in config
continue
}
filteredMatches = append(filteredMatches, match)
}
return filteredMatches
}
func advisoryEventForNewDiscovery(match vuln.Match) v2.Event {
return v2.Event{
Timestamp: v2.Now(),
Type: v2.EventTypeDetection,
Data: v2.Detection{
Type: v2.DetectionTypeNVDAPI,
Data: v2.DetectionNVDAPI{
CPESearched: match.CPESearched.URI,
CPEFound: match.CPEFound.URI,
},
},
}
}
func uniquePackageNamesFromAPKINDEXes(apkindexes []*apk.APKIndex) []string {
packagesFound := make(map[string]struct{})
for _, apkindex := range apkindexes {
if apkindex == nil {
continue
}
for _, pkg := range apkindex.Packages {
if pkg.Origin == "" {
// This case was caused by a bug in Melange early on, that has since been
// resolved.
continue
}
name := pkg.Origin
// skip redundant recording of package name
if _, ok := packagesFound[name]; ok {
continue
}
packagesFound[name] = struct{}{}
}
}
packagesToLookup := lo.Keys(packagesFound)
sort.Strings(packagesToLookup)
return packagesToLookup
}