1
|
|
|
/* Vuls - Vulnerability Scanner |
2
|
|
|
Copyright (C) 2016 Future Corporation , Japan. |
3
|
|
|
|
4
|
|
|
This program is free software: you can redistribute it and/or modify |
5
|
|
|
it under the terms of the GNU General Public License as published by |
6
|
|
|
the Free Software Foundation, either version 3 of the License, or |
7
|
|
|
(at your option) any later version. |
8
|
|
|
|
9
|
|
|
This program is distributed in the hope that it will be useful, |
10
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
11
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12
|
|
|
GNU General Public License for more details. |
13
|
|
|
|
14
|
|
|
You should have received a copy of the GNU General Public License |
15
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>. |
16
|
|
|
*/ |
17
|
|
|
|
18
|
|
|
package scan |
19
|
|
|
|
20
|
|
|
import ( |
21
|
|
|
"net" |
22
|
|
|
"strings" |
23
|
|
|
|
24
|
|
|
"github.com/future-architect/vuls/config" |
25
|
|
|
"github.com/future-architect/vuls/models" |
26
|
|
|
"github.com/future-architect/vuls/util" |
27
|
|
|
"golang.org/x/xerrors" |
28
|
|
|
) |
29
|
|
|
|
30
|
|
|
// inherit OsTypeInterface |
31
|
|
|
type bsd struct { |
32
|
|
|
base |
33
|
|
|
} |
34
|
|
|
|
35
|
|
|
// NewBSD constructor |
36
|
|
|
func newBsd(c config.ServerInfo) *bsd { |
37
|
|
|
d := &bsd{ |
38
|
|
|
base: base{ |
39
|
|
|
osPackages: osPackages{ |
40
|
|
|
Packages: models.Packages{}, |
41
|
|
|
VulnInfos: models.VulnInfos{}, |
42
|
|
|
}, |
43
|
|
|
}, |
44
|
|
|
} |
45
|
|
|
d.log = util.NewCustomLogger(c) |
46
|
|
|
d.setServerInfo(c) |
47
|
|
|
return d |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
//https://github.com/mizzy/specinfra/blob/master/lib/specinfra/helper/detect_os/freebsd.rb |
51
|
|
|
func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) { |
52
|
|
|
bsd = newBsd(c) |
53
|
|
|
|
54
|
|
|
// Prevent from adding `set -o pipefail` option |
55
|
|
|
c.Distro = config.Distro{Family: config.FreeBSD} |
56
|
|
|
|
57
|
|
|
if r := exec(c, "uname", noSudo); r.isSuccess() { |
58
|
|
|
if strings.Contains(strings.ToLower(r.Stdout), config.FreeBSD) == true { |
59
|
|
|
if b := exec(c, "freebsd-version", noSudo); b.isSuccess() { |
60
|
|
|
rel := strings.TrimSpace(b.Stdout) |
61
|
|
|
bsd.setDistro(config.FreeBSD, rel) |
62
|
|
|
return true, bsd |
63
|
|
|
} |
64
|
|
|
} |
65
|
|
|
} |
66
|
|
|
util.Log.Debugf("Not FreeBSD. servernam: %s", c.ServerName) |
67
|
|
|
return false, bsd |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
func (o *bsd) checkScanMode() error { |
71
|
|
|
if o.getServerInfo().Mode.IsOffline() { |
72
|
|
|
return xerrors.New("Remove offline scan mode, FreeBSD needs internet connection") |
73
|
|
|
} |
74
|
|
|
return nil |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
func (o *bsd) checkIfSudoNoPasswd() error { |
78
|
|
|
// FreeBSD doesn't need root privilege |
79
|
|
|
o.log.Infof("sudo ... No need") |
80
|
|
|
return nil |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
func (o *bsd) checkDeps() error { |
84
|
|
|
o.log.Infof("Dependencies... No need") |
85
|
|
|
return nil |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
func (o *bsd) preCure() error { |
89
|
|
|
o.log.Infof("Scanning in %s", o.getServerInfo().Mode) |
90
|
|
|
if err := o.detectIPAddr(); err != nil { |
91
|
|
|
o.log.Debugf("Failed to detect IP addresses: %s", err) |
92
|
|
|
} |
93
|
|
|
// Ignore this error as it just failed to detect the IP addresses |
94
|
|
|
return nil |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
func (o *bsd) postScan() error { |
98
|
|
|
return nil |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
func (o *bsd) detectIPAddr() (err error) { |
102
|
|
|
r := o.exec("/sbin/ifconfig", noSudo) |
103
|
|
|
if !r.isSuccess() { |
104
|
|
|
return xerrors.Errorf("Failed to detect IP address: %v", r) |
105
|
|
|
} |
106
|
|
|
o.ServerInfo.IPv4Addrs, o.ServerInfo.IPv6Addrs = o.parseIfconfig(r.Stdout) |
107
|
|
|
return nil |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
func (l *base) parseIfconfig(stdout string) (ipv4Addrs []string, ipv6Addrs []string) { |
111
|
|
|
lines := strings.Split(stdout, "\n") |
112
|
|
|
for _, line := range lines { |
113
|
|
|
line = strings.TrimSpace(line) |
114
|
|
|
fields := strings.Fields(line) |
115
|
|
|
if len(fields) < 4 || !strings.HasPrefix(fields[0], "inet") { |
116
|
|
|
continue |
117
|
|
|
} |
118
|
|
|
ip := net.ParseIP(fields[1]) |
119
|
|
|
if ip == nil { |
120
|
|
|
continue |
121
|
|
|
} |
122
|
|
|
if !ip.IsGlobalUnicast() { |
123
|
|
|
continue |
124
|
|
|
} |
125
|
|
|
if ipv4 := ip.To4(); ipv4 != nil { |
126
|
|
|
ipv4Addrs = append(ipv4Addrs, ipv4.String()) |
127
|
|
|
} else { |
128
|
|
|
ipv6Addrs = append(ipv6Addrs, ip.String()) |
129
|
|
|
} |
130
|
|
|
} |
131
|
|
|
return |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
func (o *bsd) scanPackages() error { |
135
|
|
|
// collect the running kernel information |
136
|
|
|
release, version, err := o.runningKernel() |
137
|
|
|
if err != nil { |
138
|
|
|
o.log.Errorf("Failed to scan the running kernel version: %s", err) |
139
|
|
|
return err |
140
|
|
|
} |
141
|
|
|
o.Kernel = models.Kernel{ |
142
|
|
|
Release: release, |
143
|
|
|
Version: version, |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
rebootRequired, err := o.rebootRequired() |
147
|
|
|
if err != nil { |
148
|
|
|
o.log.Errorf("Failed to detect the kernel reboot required: %s", err) |
149
|
|
|
return err |
150
|
|
|
} |
151
|
|
|
o.Kernel.RebootRequired = rebootRequired |
152
|
|
|
|
153
|
|
|
packs, err := o.scanInstalledPackages() |
154
|
|
|
if err != nil { |
155
|
|
|
o.log.Errorf("Failed to scan installed packages: %s", err) |
156
|
|
|
return err |
157
|
|
|
} |
158
|
|
|
o.Packages = packs |
159
|
|
|
|
160
|
|
|
unsecures, err := o.scanUnsecurePackages() |
161
|
|
|
if err != nil { |
162
|
|
|
o.log.Errorf("Failed to scan vulnerable packages: %s", err) |
163
|
|
|
return err |
164
|
|
|
} |
165
|
|
|
o.VulnInfos = unsecures |
166
|
|
|
return nil |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
func (o *bsd) parseInstalledPackages(string) (models.Packages, models.SrcPackages, error) { |
170
|
|
|
return nil, nil, nil |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
func (o *bsd) rebootRequired() (bool, error) { |
174
|
|
|
r := o.exec("freebsd-version -k", noSudo) |
175
|
|
|
if !r.isSuccess() { |
176
|
|
|
return false, xerrors.Errorf("Failed to SSH: %s", r) |
177
|
|
|
} |
178
|
|
|
return o.Kernel.Release != strings.TrimSpace(r.Stdout), nil |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
func (o *bsd) scanInstalledPackages() (models.Packages, error) { |
182
|
|
|
cmd := util.PrependProxyEnv("pkg version -v") |
183
|
|
|
r := o.exec(cmd, noSudo) |
184
|
|
|
if !r.isSuccess() { |
185
|
|
|
return nil, xerrors.Errorf("Failed to SSH: %s", r) |
186
|
|
|
} |
187
|
|
|
return o.parsePkgVersion(r.Stdout), nil |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
func (o *bsd) scanUnsecurePackages() (models.VulnInfos, error) { |
191
|
|
|
const vulndbPath = "/tmp/vuln.db" |
192
|
|
|
cmd := "rm -f " + vulndbPath |
193
|
|
|
r := o.exec(cmd, noSudo) |
194
|
|
|
if !r.isSuccess(0) { |
195
|
|
|
return nil, xerrors.Errorf("Failed to SSH: %s", r) |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
cmd = util.PrependProxyEnv("pkg audit -F -r -f " + vulndbPath) |
199
|
|
|
r = o.exec(cmd, noSudo) |
200
|
|
|
if !r.isSuccess(0, 1) { |
201
|
|
|
return nil, xerrors.Errorf("Failed to SSH: %s", r) |
202
|
|
|
} |
203
|
|
|
if r.ExitStatus == 0 { |
204
|
|
|
// no vulnerabilities |
205
|
|
|
return nil, nil |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
var packAdtRslt []pkgAuditResult |
209
|
|
|
blocks := o.splitIntoBlocks(r.Stdout) |
210
|
|
|
for _, b := range blocks { |
211
|
|
|
name, cveIDs, vulnID := o.parseBlock(b) |
212
|
|
|
if len(cveIDs) == 0 { |
213
|
|
|
continue |
214
|
|
|
} |
215
|
|
|
pack, found := o.Packages[name] |
216
|
|
|
if !found { |
217
|
|
|
return nil, xerrors.Errorf("Vulnerable package: %s is not found", name) |
218
|
|
|
} |
219
|
|
|
packAdtRslt = append(packAdtRslt, pkgAuditResult{ |
220
|
|
|
pack: pack, |
221
|
|
|
vulnIDCveIDs: vulnIDCveIDs{ |
222
|
|
|
vulnID: vulnID, |
223
|
|
|
cveIDs: cveIDs, |
224
|
|
|
}, |
225
|
|
|
}) |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
// { CVE ID: []pkgAuditResult } |
229
|
|
|
cveIDAdtMap := make(map[string][]pkgAuditResult) |
230
|
|
|
for _, p := range packAdtRslt { |
231
|
|
|
for _, cid := range p.vulnIDCveIDs.cveIDs { |
232
|
|
|
cveIDAdtMap[cid] = append(cveIDAdtMap[cid], p) |
233
|
|
|
} |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
vinfos := models.VulnInfos{} |
237
|
|
|
for cveID := range cveIDAdtMap { |
238
|
|
|
packs := models.Packages{} |
239
|
|
|
for _, r := range cveIDAdtMap[cveID] { |
240
|
|
|
packs[r.pack.Name] = r.pack |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
disAdvs := []models.DistroAdvisory{} |
244
|
|
|
for _, r := range cveIDAdtMap[cveID] { |
245
|
|
|
disAdvs = append(disAdvs, models.DistroAdvisory{ |
246
|
|
|
AdvisoryID: r.vulnIDCveIDs.vulnID, |
247
|
|
|
}) |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
affected := models.PackageFixStatuses{} |
251
|
|
|
for name := range packs { |
252
|
|
|
affected = append(affected, models.PackageFixStatus{ |
253
|
|
|
Name: name, |
254
|
|
|
}) |
255
|
|
|
} |
256
|
|
|
vinfos[cveID] = models.VulnInfo{ |
257
|
|
|
CveID: cveID, |
258
|
|
|
AffectedPackages: affected, |
259
|
|
|
DistroAdvisories: disAdvs, |
260
|
|
|
Confidences: models.Confidences{models.PkgAuditMatch}, |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
return vinfos, nil |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
func (o *bsd) parsePkgVersion(stdout string) models.Packages { |
267
|
|
|
packs := models.Packages{} |
268
|
|
|
lines := strings.Split(stdout, "\n") |
269
|
|
|
for _, l := range lines { |
270
|
|
|
fields := strings.Fields(l) |
271
|
|
|
if len(fields) < 2 { |
272
|
|
|
continue |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
packVer := fields[0] |
276
|
|
|
splitted := strings.Split(packVer, "-") |
277
|
|
|
ver := splitted[len(splitted)-1] |
278
|
|
|
name := strings.Join(splitted[:len(splitted)-1], "-") |
279
|
|
|
|
280
|
|
|
switch fields[1] { |
281
|
|
|
case "?", "=": |
282
|
|
|
packs[name] = models.Package{ |
283
|
|
|
Name: name, |
284
|
|
|
Version: ver, |
285
|
|
|
} |
286
|
|
|
case "<": |
287
|
|
|
candidate := strings.TrimSuffix(fields[6], ")") |
288
|
|
|
packs[name] = models.Package{ |
289
|
|
|
Name: name, |
290
|
|
|
Version: ver, |
291
|
|
|
NewVersion: candidate, |
292
|
|
|
} |
293
|
|
|
case ">": |
294
|
|
|
o.log.Warnf("The installed version of the %s is newer than the current version. *This situation can arise with an out of date index file, or when testing new ports.*", name) |
295
|
|
|
packs[name] = models.Package{ |
296
|
|
|
Name: name, |
297
|
|
|
Version: ver, |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
return packs |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
type vulnIDCveIDs struct { |
305
|
|
|
vulnID string |
306
|
|
|
cveIDs []string |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
type pkgAuditResult struct { |
310
|
|
|
pack models.Package |
311
|
|
|
vulnIDCveIDs vulnIDCveIDs |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
func (o *bsd) splitIntoBlocks(stdout string) (blocks []string) { |
315
|
|
|
lines := strings.Split(stdout, "\n") |
316
|
|
|
block := []string{} |
317
|
|
|
for _, l := range lines { |
318
|
|
|
if len(strings.TrimSpace(l)) == 0 { |
319
|
|
|
if 0 < len(block) { |
320
|
|
|
blocks = append(blocks, strings.Join(block, "\n")) |
321
|
|
|
block = []string{} |
322
|
|
|
} |
323
|
|
|
continue |
324
|
|
|
} |
325
|
|
|
block = append(block, strings.TrimSpace(l)) |
326
|
|
|
} |
327
|
|
|
if 0 < len(block) { |
328
|
|
|
blocks = append(blocks, strings.Join(block, "\n")) |
329
|
|
|
} |
330
|
|
|
return |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
func (o *bsd) parseBlock(block string) (packName string, cveIDs []string, vulnID string) { |
334
|
|
|
lines := strings.Split(block, "\n") |
335
|
|
|
for _, l := range lines { |
336
|
|
|
if strings.HasSuffix(l, " is vulnerable:") { |
337
|
|
|
packVer := strings.Fields(l)[0] |
338
|
|
|
splitted := strings.Split(packVer, "-") |
339
|
|
|
packName = strings.Join(splitted[:len(splitted)-1], "-") |
340
|
|
|
} else if strings.HasPrefix(l, "CVE:") { |
341
|
|
|
cveIDs = append(cveIDs, strings.Fields(l)[1]) |
342
|
|
|
} else if strings.HasPrefix(l, "WWW:") { |
343
|
|
|
splitted := strings.Split(l, "/") |
344
|
|
|
vulnID = strings.TrimSuffix(splitted[len(splitted)-1], ".html") |
345
|
|
|
} |
346
|
|
|
} |
347
|
|
|
return |
348
|
|
|
} |
349
|
|
|
|