Completed
Pull Request — master (#729)
by kota
05:55
created

report.FillWithExploit   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
nop 2
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 report
19
20
import (
21
	"bytes"
22
	"fmt"
23
	"io/ioutil"
24
	"os"
25
	"reflect"
26
	"regexp"
27
	"sort"
28
	"strings"
29
	"time"
30
31
	"github.com/BurntSushi/toml"
32
	c "github.com/future-architect/vuls/config"
33
	"github.com/future-architect/vuls/contrib/owasp-dependency-check/parser"
34
	"github.com/future-architect/vuls/cwe"
35
	"github.com/future-architect/vuls/exploit"
36
	"github.com/future-architect/vuls/gost"
37
	"github.com/future-architect/vuls/models"
38
	"github.com/future-architect/vuls/oval"
39
	"github.com/future-architect/vuls/util"
40
	"github.com/hashicorp/uuid"
41
	gostdb "github.com/knqyf263/gost/db"
42
	cvedb "github.com/kotakanbe/go-cve-dictionary/db"
43
	ovaldb "github.com/kotakanbe/goval-dictionary/db"
44
	exploitdb "github.com/mozqnet/go-exploitdb/db"
45
)
46
47
const (
48
	vulsOpenTag  = "<vulsreport>"
49
	vulsCloseTag = "</vulsreport>"
50
)
51
52
// FillCveInfos fills CVE Detailed Information
53
func FillCveInfos(dbclient DBClient, rs []models.ScanResult, dir string) ([]models.ScanResult, error) {
54
	var filledResults []models.ScanResult
55
	reportedAt := time.Now()
56
	hostname, _ := os.Hostname()
57
	for _, r := range rs {
58
		if c.Conf.RefreshCve || needToRefreshCve(r) {
59
			cpeURIs := []string{}
60
			if len(r.Container.ContainerID) == 0 {
61
				cpeURIs = c.Conf.Servers[r.ServerName].CpeNames
62
				owaspDCXMLPath := c.Conf.Servers[r.ServerName].OwaspDCXMLPath
63
				if owaspDCXMLPath != "" {
64
					cpes, err := parser.Parse(owaspDCXMLPath)
65
					if err != nil {
66
						return nil, fmt.Errorf("Failed to read OWASP Dependency Check XML: %s, %s, %s",
67
							r.ServerName, owaspDCXMLPath, err)
68
					}
69
					cpeURIs = append(cpeURIs, cpes...)
70
				}
71
			} else {
72
				if s, ok := c.Conf.Servers[r.ServerName]; ok {
73
					if con, ok := s.Containers[r.Container.Name]; ok {
74
						cpeURIs = con.Cpes
75
						owaspDCXMLPath := con.OwaspDCXMLPath
76
						if owaspDCXMLPath != "" {
77
							cpes, err := parser.Parse(owaspDCXMLPath)
78
							if err != nil {
79
								return nil, fmt.Errorf("Failed to read OWASP Dependency Check XML: %s, %s, %s",
80
									r.ServerInfo(), owaspDCXMLPath, err)
81
							}
82
							cpeURIs = append(cpeURIs, cpes...)
83
						}
84
					}
85
				}
86
			}
87
88
			if err := FillCveInfo(dbclient, &r, cpeURIs); err != nil {
89
				return nil, err
90
			}
91
			r.Lang = c.Conf.Lang
92
			r.ReportedAt = reportedAt
93
			r.ReportedVersion = c.Version
94
			r.ReportedRevision = c.Revision
95
			r.ReportedBy = hostname
96
			r.Config.Report = c.Conf
97
			r.Config.Report.Servers = map[string]c.ServerInfo{
98
				r.ServerName: c.Conf.Servers[r.ServerName],
99
			}
100
			if err := overwriteJSONFile(dir, r); err != nil {
101
				return nil, fmt.Errorf("Failed to write JSON: %s", err)
102
			}
103
			filledResults = append(filledResults, r)
104
		} else {
105
			util.Log.Debugf("No need to refresh")
106
			filledResults = append(filledResults, r)
107
		}
108
	}
109
110
	if c.Conf.Diff {
111
		prevs, err := loadPrevious(filledResults)
112
		if err != nil {
113
			return nil, err
114
		}
115
116
		diff, err := diff(filledResults, prevs)
117
		if err != nil {
118
			return nil, err
119
		}
120
		filledResults = []models.ScanResult{}
121
		for _, r := range diff {
122
			if err := fillCveDetail(dbclient.CveDB, &r); err != nil {
123
				return nil, err
124
			}
125
			filledResults = append(filledResults, r)
126
		}
127
	}
128
129
	filtered := []models.ScanResult{}
130
	for _, r := range filledResults {
131
		r = r.FilterByCvssOver(c.Conf.CvssScoreOver)
132
		r = r.FilterIgnoreCves()
133
		r = r.FilterUnfixed()
134
		r = r.FilterIgnorePkgs()
135
		if c.Conf.IgnoreUnscoredCves {
136
			r.ScannedCves = r.ScannedCves.FindScoredVulns()
137
		}
138
		filtered = append(filtered, r)
139
	}
140
	return filtered, nil
141
}
142
143
// FillCveInfo fill scanResult with cve info.
144
func FillCveInfo(dbclient DBClient, r *models.ScanResult, cpeURIs []string) error {
145
	util.Log.Debugf("need to refresh")
146
147
	nCVEs, err := FillWithOval(dbclient.OvalDB, r)
148
	if err != nil {
149
		return fmt.Errorf("Failed to fill with OVAL: %s", err)
150
	}
151
	util.Log.Infof("%s: %d CVEs are detected with OVAL",
152
		r.FormatServerName(), nCVEs)
153
154
	for i, v := range r.ScannedCves {
155
		for j, p := range v.AffectedPackages {
156
			if p.NotFixedYet && p.FixState == "" {
157
				p.FixState = "Not fixed yet"
158
				r.ScannedCves[i].AffectedPackages[j] = p
159
			}
160
		}
161
	}
162
163
	nCVEs, err = fillVulnByCpeURIs(dbclient.CveDB, r, cpeURIs)
164
	if err != nil {
165
		return fmt.Errorf("Failed to detect vulns of %s: %s", cpeURIs, err)
166
	}
167
	util.Log.Infof("%s: %d CVEs are detected with CPE", r.FormatServerName(), nCVEs)
168
169
	nCVEs, err = FillWithGost(dbclient.GostDB, r)
170
	if err != nil {
171
		return fmt.Errorf("Failed to fill with gost: %s", err)
172
	}
173
	util.Log.Infof("%s: %d unfixed CVEs are detected with gost",
174
		r.FormatServerName(), nCVEs)
175
176
	util.Log.Infof("Fill CVE detailed information with CVE-DB")
177
	if err := fillCveDetail(dbclient.CveDB, r); err != nil {
178
		return fmt.Errorf("Failed to fill with CVE: %s", err)
179
	}
180
181
	util.Log.Infof("Fill Exploit information with Exploit-DB")
182
	nExploitCve, err := FillWithExploit(dbclient.ExploitDB, r)
183
	if err != nil {
184
		return fmt.Errorf("Failed to fill with exploit: %s", err)
185
	}
186
	util.Log.Infof("%s: %d Exploits are detected with exploit",
187
		r.FormatServerName(), nExploitCve)
188
189
	fillCweDict(r)
190
	return nil
191
}
192
193
// fillCveDetail fetches NVD, JVN from CVE Database
194
func fillCveDetail(driver cvedb.DB, r *models.ScanResult) error {
195
	var cveIDs []string
196
	for _, v := range r.ScannedCves {
197
		cveIDs = append(cveIDs, v.CveID)
198
	}
199
200
	ds, err := CveClient.FetchCveDetails(driver, cveIDs)
201
	if err != nil {
202
		return err
203
	}
204
	for _, d := range ds {
205
		nvd := models.ConvertNvdJSONToModel(d.CveID, d.NvdJSON)
206
		if nvd == nil {
207
			nvd = models.ConvertNvdXMLToModel(d.CveID, d.NvdXML)
208
		}
209
		jvn := models.ConvertJvnToModel(d.CveID, d.Jvn)
210
211
		for cveID, vinfo := range r.ScannedCves {
212
			if vinfo.CveID == d.CveID {
213
				if vinfo.CveContents == nil {
214
					vinfo.CveContents = models.CveContents{}
215
				}
216
				for _, con := range []*models.CveContent{nvd, jvn} {
217
					if con != nil && !con.Empty() {
218
						vinfo.CveContents[con.Type] = *con
219
					}
220
				}
221
				r.ScannedCves[cveID] = vinfo
222
				break
223
			}
224
		}
225
	}
226
	return nil
227
}
228
229
// FillWithOval fetches OVAL database
230
func FillWithOval(driver ovaldb.DB, r *models.ScanResult) (nCVEs int, err error) {
231
	var ovalClient oval.Client
232
	var ovalFamily string
233
234
	switch r.Family {
235
	case c.Debian:
236
		ovalClient = oval.NewDebian()
237
		ovalFamily = c.Debian
238
	case c.Ubuntu:
239
		ovalClient = oval.NewUbuntu()
240
		ovalFamily = c.Ubuntu
241
	case c.RedHat:
242
		ovalClient = oval.NewRedhat()
243
		ovalFamily = c.RedHat
244
	case c.CentOS:
245
		ovalClient = oval.NewCentOS()
246
		//use RedHat's OVAL
247
		ovalFamily = c.RedHat
248
	case c.Oracle:
249
		ovalClient = oval.NewOracle()
250
		ovalFamily = c.Oracle
251
	case c.SUSEEnterpriseServer:
252
		// TODO other suse family
253
		ovalClient = oval.NewSUSE()
254
		ovalFamily = c.SUSEEnterpriseServer
255
	case c.Alpine:
256
		ovalClient = oval.NewAlpine()
257
		ovalFamily = c.Alpine
258
	case c.Amazon, c.Raspbian, c.FreeBSD, c.Windows:
259
		return 0, nil
260
	case c.ServerTypePseudo:
261
		return 0, nil
262
	default:
263
		if r.Family == "" {
264
			return 0, fmt.Errorf("Probably an error occurred during scanning. Check the error message")
265
		}
266
		return 0, fmt.Errorf("OVAL for %s is not implemented yet", r.Family)
267
	}
268
269
	if !ovalClient.IsFetchViaHTTP() && driver == nil {
270
		return 0, nil
271
	}
272
273
	if err = driver.NewOvalDB(ovalFamily); err != nil {
274
		return 0, fmt.Errorf("Failed to New Oval DB. err: %s", err)
275
	}
276
277
	util.Log.Debugf("Check whether oval fetched: %s %s",
278
		ovalFamily, r.Release)
279
	ok, err := ovalClient.CheckIfOvalFetched(driver, ovalFamily, r.Release)
280
	if err != nil {
281
		return 0, err
282
	}
283
	if !ok {
284
		util.Log.Warnf("OVAL entries of %s %s are not found. It's recommended to use OVAL to improve scanning accuracy. For details, see https://github.com/kotakanbe/goval-dictionary#usage , Then report with --ovaldb-path or --ovaldb-url flag", ovalFamily, r.Release)
285
		return 0, nil
286
	}
287
288
	_, err = ovalClient.CheckIfOvalFresh(driver, ovalFamily, r.Release)
289
	if err != nil {
290
		return 0, err
291
	}
292
293
	return ovalClient.FillWithOval(driver, r)
294
}
295
296
// FillWithGost fills CVEs with gost dataabase
297
// https://github.com/knqyf263/gost
298
func FillWithGost(driver gostdb.DB, r *models.ScanResult) (nCVEs int, err error) {
299
	gostClient := gost.NewClient(r.Family)
300
	// TODO chekc if fetched
301
	// TODO chekc if fresh enough
302
	return gostClient.FillWithGost(driver, r)
303
}
304
305
// FillWithExploit fills Exploits with exploit dataabase
306
// https://github.com/mozqnet/go-exploitdb
307
func FillWithExploit(driver exploitdb.DB, r *models.ScanResult) (nExploitCve int, err error) {
308
	// TODO chekc if fetched
309
	// TODO chekc if fresh enough
310
	return exploit.FillWithExploit(driver, r)
311
}
312
313
func fillVulnByCpeURIs(driver cvedb.DB, r *models.ScanResult, cpeURIs []string) (nCVEs int, err error) {
314
	for _, name := range cpeURIs {
315
		details, err := CveClient.FetchCveDetailsByCpeName(driver, name)
316
		if err != nil {
317
			return 0, err
318
		}
319
		for _, detail := range details {
320
			if val, ok := r.ScannedCves[detail.CveID]; ok {
321
				names := val.CpeURIs
322
				names = util.AppendIfMissing(names, name)
323
				val.CpeURIs = names
324
				val.Confidences.AppendIfMissing(models.CpeNameMatch)
325
				r.ScannedCves[detail.CveID] = val
326
			} else {
327
				v := models.VulnInfo{
328
					CveID:       detail.CveID,
329
					CpeURIs:     []string{name},
330
					Confidences: models.Confidences{models.CpeNameMatch},
331
				}
332
				r.ScannedCves[detail.CveID] = v
333
				nCVEs++
334
			}
335
		}
336
	}
337
	return nCVEs, nil
338
}
339
340
func fillCweDict(r *models.ScanResult) {
341
	uniqCweIDMap := map[string]bool{}
342
	for _, vinfo := range r.ScannedCves {
343
		for _, cont := range vinfo.CveContents {
344
			for _, id := range cont.CweIDs {
345
				if strings.HasPrefix(id, "CWE-") {
346
					id = strings.TrimPrefix(id, "CWE-")
347
					uniqCweIDMap[id] = true
348
				}
349
			}
350
		}
351
	}
352
353
	// TODO check the format of CWEID, clean CWEID
354
	// JVN, NVD XML, JSON, OVALs
355
356
	dict := map[string]models.CweDictEntry{}
357
	for id := range uniqCweIDMap {
358
		entry := models.CweDictEntry{}
359
		if e, ok := cwe.CweDictEn[id]; ok {
360
			if rank, ok := cwe.OwaspTopTen2017[id]; ok {
361
				entry.OwaspTopTen2017 = rank
362
			}
363
			entry.En = &e
364
		} else {
365
			util.Log.Debugf("CWE-ID %s is not found in English CWE Dict", id)
366
			entry.En = &cwe.Cwe{CweID: id}
367
		}
368
369
		if c.Conf.Lang == "ja" {
370
			if e, ok := cwe.CweDictJa[id]; ok {
371
				if rank, ok := cwe.OwaspTopTen2017[id]; ok {
372
					entry.OwaspTopTen2017 = rank
373
				}
374
				entry.Ja = &e
375
			} else {
376
				util.Log.Debugf("CWE-ID %s is not found in Japanese CWE Dict", id)
377
				entry.Ja = &cwe.Cwe{CweID: id}
378
			}
379
		}
380
		dict[id] = entry
381
	}
382
	r.CweDict = dict
383
	return
384
}
385
386
const reUUID = "[\\da-f]{8}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{12}"
387
388
// EnsureUUIDs generate a new UUID of the scan target server if UUID is not assigned yet.
389
// And then set the generated UUID to config.toml and scan results.
390
func EnsureUUIDs(configPath string, results models.ScanResults) error {
391
	// Sort Host->Container
392
	sort.Slice(results, func(i, j int) bool {
393
		if results[i].ServerName == results[j].ServerName {
394
			return results[i].Container.ContainerID < results[j].Container.ContainerID
395
		}
396
		return results[i].ServerName < results[j].ServerName
397
	})
398
399
	for i, r := range results {
400
		server := c.Conf.Servers[r.ServerName]
401
		if server.UUIDs == nil {
402
			server.UUIDs = map[string]string{}
403
		}
404
405
		name := ""
406
		if r.IsContainer() {
407
			name = fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName)
408
409
			// Scanning with the -containers-only flag at scan time, the UUID of Container Host may not be generated,
410
			// so check it. Otherwise create a UUID of the Container Host and set it.
411
			serverUUID := ""
412
			if id, ok := server.UUIDs[r.ServerName]; !ok {
413
				serverUUID = uuid.GenerateUUID()
414
			} else {
415
				matched, err := regexp.MatchString(reUUID, id)
416
				if !matched || err != nil {
417
					serverUUID = uuid.GenerateUUID()
418
				}
419
			}
420
			if serverUUID != "" {
421
				server.UUIDs[r.ServerName] = serverUUID
422
			}
423
		} else {
424
			name = r.ServerName
425
		}
426
427
		if id, ok := server.UUIDs[name]; ok {
428
			matched, err := regexp.MatchString(reUUID, id)
429
			if !matched || err != nil {
430
				util.Log.Warnf("UUID is invalid. Re-generate UUID %s: %s", id, err)
431
			} else {
432
				if r.IsContainer() {
433
					results[i].Container.UUID = id
434
					results[i].ServerUUID = server.UUIDs[r.ServerName]
435
				} else {
436
					results[i].ServerUUID = id
437
				}
438
				// continue if the UUID has already assigned and valid
439
				continue
440
			}
441
		}
442
443
		// Generate a new UUID and set to config and scan result
444
		id := uuid.GenerateUUID()
445
		server.UUIDs[name] = id
446
		server = cleanForTOMLEncoding(server, c.Conf.Default)
447
		c.Conf.Servers[r.ServerName] = server
448
449
		if r.IsContainer() {
450
			results[i].Container.UUID = id
451
			results[i].ServerUUID = server.UUIDs[r.ServerName]
452
		} else {
453
			results[i].ServerUUID = id
454
		}
455
	}
456
457
	for name, server := range c.Conf.Servers {
458
		server = cleanForTOMLEncoding(server, c.Conf.Default)
459
		c.Conf.Servers[name] = server
460
	}
461
462
	email := &c.Conf.EMail
463
	if email.SMTPAddr == "" {
464
		email = nil
465
	}
466
467
	slack := &c.Conf.Slack
468
	if slack.HookURL == "" {
469
		slack = nil
470
	}
471
472
	cveDict := &c.Conf.CveDict
473
	ovalDict := &c.Conf.OvalDict
474
	gost := &c.Conf.Gost
475
	exploit := &c.Conf.Exploit
476
	http := &c.Conf.HTTP
477
	if http.URL == "" {
478
		http = nil
479
	}
480
481
	syslog := &c.Conf.Syslog
482
	if syslog.Host == "" {
483
		syslog = nil
484
	}
485
486
	aws := &c.Conf.AWS
487
	if aws.S3Bucket == "" {
488
		aws = nil
489
	}
490
491
	azure := &c.Conf.Azure
492
	if azure.AccountName == "" {
493
		azure = nil
494
	}
495
496
	stride := &c.Conf.Stride
497
	if stride.HookURL == "" {
498
		stride = nil
499
	}
500
501
	hipChat := &c.Conf.HipChat
502
	if hipChat.AuthToken == "" {
503
		hipChat = nil
504
	}
505
506
	chatWork := &c.Conf.ChatWork
507
	if chatWork.APIToken == "" {
508
		chatWork = nil
509
	}
510
511
	saas := &c.Conf.Saas
512
	if saas.GroupID == 0 {
513
		saas = nil
514
	}
515
516
	c := struct {
517
		CveDict  *c.GoCveDictConf `toml:"cveDict"`
518
		OvalDict *c.GovalDictConf `toml:"ovalDict"`
519
		Gost     *c.GostConf      `toml:"gost"`
520
		Exploit  *c.ExploitConf   `toml:"exploit"`
521
		Slack    *c.SlackConf     `toml:"slack"`
522
		Email    *c.SMTPConf      `toml:"email"`
523
		HTTP     *c.HTTPConf      `toml:"http"`
524
		Syslog   *c.SyslogConf    `toml:"syslog"`
525
		AWS      *c.AWS           `toml:"aws"`
526
		Azure    *c.Azure         `toml:"azure"`
527
		Stride   *c.StrideConf    `toml:"stride"`
528
		HipChat  *c.HipChatConf   `toml:"hipChat"`
529
		ChatWork *c.ChatWorkConf  `toml:"chatWork"`
530
		Saas     *c.SaasConf      `toml:"saas"`
531
532
		Default c.ServerInfo            `toml:"default"`
533
		Servers map[string]c.ServerInfo `toml:"servers"`
534
	}{
535
		CveDict:  cveDict,
536
		OvalDict: ovalDict,
537
		Gost:     gost,
538
		Exploit:  exploit,
539
		Slack:    slack,
540
		Email:    email,
541
		HTTP:     http,
542
		Syslog:   syslog,
543
		AWS:      aws,
544
		Azure:    azure,
545
		Stride:   stride,
546
		HipChat:  hipChat,
547
		ChatWork: chatWork,
548
		Saas:     saas,
549
550
		Default: c.Conf.Default,
551
		Servers: c.Conf.Servers,
552
	}
553
554
	// rename the current config.toml to config.toml.bak
555
	info, err := os.Lstat(configPath)
556
	if err != nil {
557
		return fmt.Errorf("Failed to lstat %s: %s", configPath, err)
558
	}
559
	realPath := configPath
560
	if info.Mode()&os.ModeSymlink == os.ModeSymlink {
561
		if realPath, err = os.Readlink(configPath); err != nil {
562
			return fmt.Errorf("Failed to Read link %s: %s", configPath, err)
563
		}
564
	}
565
	if err := os.Rename(realPath, realPath+".bak"); err != nil {
566
		return fmt.Errorf("Failed to rename %s: %s", configPath, err)
567
	}
568
569
	var buf bytes.Buffer
570
	if err := toml.NewEncoder(&buf).Encode(c); err != nil {
571
		return fmt.Errorf("Failed to encode to toml: %s", err)
572
	}
573
	str := strings.Replace(buf.String(), "\n  [", "\n\n  [", -1)
574
	str = fmt.Sprintf("%s\n\n%s",
575
		"# See REAME for details: https://vuls.io/docs/en/usage-settings.html",
576
		str)
577
578
	return ioutil.WriteFile(realPath, []byte(str), 0600)
579
}
580
581
func cleanForTOMLEncoding(server c.ServerInfo, def c.ServerInfo) c.ServerInfo {
582
	if reflect.DeepEqual(server.Optional, def.Optional) {
583
		server.Optional = nil
584
	}
585
586
	if def.User == server.User {
587
		server.User = ""
588
	}
589
590
	if def.Host == server.Host {
591
		server.Host = ""
592
	}
593
594
	if def.Port == server.Port {
595
		server.Port = ""
596
	}
597
598
	if def.KeyPath == server.KeyPath {
599
		server.KeyPath = ""
600
	}
601
602
	if reflect.DeepEqual(server.ScanMode, def.ScanMode) {
603
		server.ScanMode = nil
604
	}
605
606
	if def.Type == server.Type {
607
		server.Type = ""
608
	}
609
610
	if reflect.DeepEqual(server.CpeNames, def.CpeNames) {
611
		server.CpeNames = nil
612
	}
613
614
	if def.OwaspDCXMLPath == server.OwaspDCXMLPath {
615
		server.OwaspDCXMLPath = ""
616
	}
617
618
	if reflect.DeepEqual(server.IgnoreCves, def.IgnoreCves) {
619
		server.IgnoreCves = nil
620
	}
621
622
	if reflect.DeepEqual(server.Enablerepo, def.Enablerepo) {
623
		server.Enablerepo = nil
624
	}
625
626
	for k, v := range def.Optional {
627
		if vv, ok := server.Optional[k]; ok && v == vv {
628
			delete(server.Optional, k)
629
		}
630
	}
631
632
	return server
633
}
634