Completed
Push — master ( 3178c1...9b8a32 )
by kota
05:51
created

report.ovalSupported   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
dl 0
loc 9
rs 10
c 0
b 0
f 0
nop 1
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
	"encoding/json"
23
	"fmt"
24
	"io/ioutil"
25
	"os"
26
	"path/filepath"
27
	"reflect"
28
	"regexp"
29
	"sort"
30
	"strings"
31
	"time"
32
33
	"github.com/future-architect/vuls/config"
34
	"github.com/future-architect/vuls/models"
35
	"github.com/future-architect/vuls/util"
36
	"github.com/gosuri/uitable"
37
	"github.com/olekukonko/tablewriter"
38
)
39
40
const maxColWidth = 100
41
42
func formatScanSummary(rs ...models.ScanResult) string {
43
	table := uitable.New()
44
	table.MaxColWidth = maxColWidth
45
	table.Wrap = true
46
	for _, r := range rs {
47
		var cols []interface{}
48
		if len(r.Errors) == 0 {
49
			cols = []interface{}{
50
				r.FormatServerName(),
51
				fmt.Sprintf("%s%s", r.Family, r.Release),
52
				r.FormatUpdatablePacksSummary(),
53
			}
54
		} else {
55
			cols = []interface{}{
56
				r.FormatServerName(),
57
				"Error",
58
				"",
59
				"Run with --debug to view the details",
60
			}
61
		}
62
		table.AddRow(cols...)
63
	}
64
	return fmt.Sprintf("%s\n", table)
65
}
66
67
func formatOneLineSummary(rs ...models.ScanResult) string {
68
	table := uitable.New()
69
	table.MaxColWidth = maxColWidth
70
	table.Wrap = true
71
	for _, r := range rs {
72
		var cols []interface{}
73
		if len(r.Errors) == 0 {
74
			cols = []interface{}{
75
				r.FormatServerName(),
76
				r.ScannedCves.FormatCveSummary(),
77
				r.ScannedCves.FormatFixedStatus(r.Packages),
78
				r.FormatUpdatablePacksSummary(),
79
				r.FormatExploitCveSummary(),
80
				r.FormatAlertSummary(),
81
			}
82
		} else {
83
			cols = []interface{}{
84
				r.FormatServerName(),
85
				"Error: Scan with --debug to view the details",
86
				"",
87
			}
88
		}
89
		table.AddRow(cols...)
90
	}
91
	return fmt.Sprintf("%s\n", table)
92
}
93
94
func formatList(r models.ScanResult) string {
95
	header := r.FormatTextReportHeadedr()
96
	if len(r.Errors) != 0 {
97
		return fmt.Sprintf(
98
			"%s\nError: Scan with --debug to view the details\n%s\n\n",
99
			header, r.Errors)
100
	}
101
102
	if len(r.ScannedCves) == 0 {
103
		return fmt.Sprintf(`
104
%s
105
No CVE-IDs are found in updatable packages.
106
%s
107
	 `, header, r.FormatUpdatablePacksSummary())
108
	}
109
110
	data := [][]string{}
111
	for _, vinfo := range r.ScannedCves.ToSortedSlice() {
112
		max := vinfo.MaxCvssScore().Value.Score
113
		// v2max := vinfo.MaxCvss2Score().Value.Score
114
		// v3max := vinfo.MaxCvss3Score().Value.Score
115
116
		// packname := vinfo.AffectedPackages.FormatTuiSummary()
117
		// packname += strings.Join(vinfo.CpeURIs, ", ")
118
119
		exploits := ""
120
		if 0 < len(vinfo.Exploits) {
121
			exploits = "   Y"
122
		}
123
124
		data = append(data, []string{
125
			vinfo.CveID,
126
			vinfo.AlertDict.FormatSource(),
127
			fmt.Sprintf("%4.1f", max),
128
			// fmt.Sprintf("%4.1f", v2max),
129
			// fmt.Sprintf("%4.1f", v3max),
130
			fmt.Sprintf("%8s", vinfo.AttackVector()),
131
			fmt.Sprintf("%7s", vinfo.PatchStatus(r.Packages)),
132
			// packname,
133
			fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vinfo.CveID),
134
			exploits,
135
		})
136
	}
137
138
	b := bytes.Buffer{}
139
	table := tablewriter.NewWriter(&b)
140
	table.SetHeader([]string{
141
		"CVE-ID",
142
		"CERT",
143
		"CVSS",
144
		// "v3",
145
		// "v2",
146
		"Attack",
147
		"Fixed",
148
		// "Pkg",
149
		"NVD",
150
		"Exploit",
151
	})
152
	table.SetBorder(true)
153
	table.AppendBulk(data)
154
	table.Render()
155
	return fmt.Sprintf("%s\n%s", header, b.String())
156
}
157
158
func formatFullPlainText(r models.ScanResult) (lines string) {
159
	header := r.FormatTextReportHeadedr()
160
	if len(r.Errors) != 0 {
161
		return fmt.Sprintf(
162
			"%s\nError: Scan with --debug to view the details\n%s\n\n",
163
			header, r.Errors)
164
	}
165
166
	if len(r.ScannedCves) == 0 {
167
		return fmt.Sprintf(`
168
%s
169
No CVE-IDs are found in updatable packages.
170
%s
171
	 `, header, r.FormatUpdatablePacksSummary())
172
	}
173
174
	lines = header + "\n"
175
176
	for _, vuln := range r.ScannedCves.ToSortedSlice() {
177
		data := [][]string{}
178
		data = append(data, []string{"Max Score", vuln.FormatMaxCvssScore()})
179
		for _, cvss := range vuln.Cvss3Scores() {
180
			if cvssstr := cvss.Value.Format(); cvssstr != "" {
181
				data = append(data, []string{string(cvss.Type), cvssstr})
182
			}
183
		}
184
185
		for _, cvss := range vuln.Cvss2Scores(r.Family) {
186
			if cvssstr := cvss.Value.Format(); cvssstr != "" {
187
				data = append(data, []string{string(cvss.Type), cvssstr})
188
			}
189
		}
190
191
		data = append(data, []string{"Summary", vuln.Summaries(
192
			config.Conf.Lang, r.Family)[0].Value})
193
194
		mitigation := vuln.Mitigations(r.Family)[0]
195
		if mitigation.Type != models.Unknown {
196
			data = append(data, []string{"Mitigation", mitigation.Value})
197
		}
198
199
		cweURLs, top10URLs := []string{}, []string{}
200
		for _, v := range vuln.CveContents.UniqCweIDs(r.Family) {
201
			name, url, top10Rank, top10URL := r.CweDict.Get(v.Value, r.Lang)
202
			if top10Rank != "" {
203
				data = append(data, []string{"CWE",
204
					fmt.Sprintf("[OWASP Top%s] %s: %s (%s)",
205
						top10Rank, v.Value, name, v.Type)})
206
				top10URLs = append(top10URLs, top10URL)
207
			} else {
208
				data = append(data, []string{"CWE", fmt.Sprintf("%s: %s (%s)",
209
					v.Value, name, v.Type)})
210
			}
211
			cweURLs = append(cweURLs, url)
212
		}
213
214
		vuln.AffectedPackages.Sort()
215
		for _, affected := range vuln.AffectedPackages {
216
			if pack, ok := r.Packages[affected.Name]; ok {
217
				var line string
218
				if pack.Repository != "" {
219
					line = fmt.Sprintf("%s (%s)",
220
						pack.FormatVersionFromTo(affected.NotFixedYet, affected.FixState),
221
						pack.Repository)
222
				} else {
223
					line = fmt.Sprintf("%s",
224
						pack.FormatVersionFromTo(affected.NotFixedYet, affected.FixState),
225
					)
226
				}
227
				data = append(data, []string{"Affected Pkg", line})
228
229
				if len(pack.AffectedProcs) != 0 {
230
					for _, p := range pack.AffectedProcs {
231
						data = append(data, []string{"",
232
							fmt.Sprintf("  - PID: %s %s", p.PID, p.Name)})
233
					}
234
				}
235
			}
236
		}
237
		sort.Strings(vuln.CpeURIs)
238
		for _, name := range vuln.CpeURIs {
239
			data = append(data, []string{"CPE", name})
240
		}
241
242
		for _, confidence := range vuln.Confidences {
243
			data = append(data, []string{"Confidence", confidence.String()})
244
		}
245
246
		links := vuln.CveContents.SourceLinks(
247
			config.Conf.Lang, r.Family, vuln.CveID)
248
		data = append(data, []string{"Source", links[0].Value})
249
250
		if 0 < len(vuln.Cvss2Scores(r.Family)) {
251
			data = append(data, []string{"CVSSv2 Calc", vuln.Cvss2CalcURL()})
252
		}
253
		if 0 < len(vuln.Cvss3Scores()) {
254
			data = append(data, []string{"CVSSv3 Calc", vuln.Cvss3CalcURL()})
255
		}
256
257
		vlinks := vuln.VendorLinks(r.Family)
258
		for name, url := range vlinks {
259
			data = append(data, []string{name, url})
260
		}
261
		for _, url := range cweURLs {
262
			data = append(data, []string{"CWE", url})
263
		}
264
		for _, exploit := range vuln.Exploits {
265
			data = append(data, []string{string(exploit.ExploitType), exploit.URL})
266
		}
267
		for _, url := range top10URLs {
268
			data = append(data, []string{"OWASP Top10", url})
269
		}
270
271
		for _, alert := range vuln.AlertDict.Ja {
272
			data = append(data, []string{"JPCERT Alert", alert.URL})
273
		}
274
275
		for _, alert := range vuln.AlertDict.En {
276
			data = append(data, []string{"USCERT Alert", alert.URL})
277
		}
278
279
		// for _, rr := range vuln.CveContents.References(r.Family) {
280
		// for _, ref := range rr.Value {
281
		// data = append(data, []string{ref.Source, ref.Link})
282
		// }
283
		// }
284
285
		b := bytes.Buffer{}
286
		table := tablewriter.NewWriter(&b)
287
		table.SetColWidth(80)
288
		table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
289
		table.SetHeader([]string{
290
			vuln.CveID,
291
			"",
292
		})
293
		table.SetBorder(true)
294
		table.AppendBulk(data)
295
		table.Render()
296
		lines += b.String() + "\n"
297
	}
298
	return
299
}
300
301
func cweURL(cweID string) string {
302
	return fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html",
303
		strings.TrimPrefix(cweID, "CWE-"))
304
}
305
306
func cweJvnURL(cweID string) string {
307
	return fmt.Sprintf("http://jvndb.jvn.jp/ja/cwe/%s.html", cweID)
308
}
309
310
func formatChangelogs(r models.ScanResult) string {
311
	buf := []string{}
312
	for _, p := range r.Packages {
313
		if p.NewVersion == "" {
314
			continue
315
		}
316
		clog := p.FormatChangelog()
317
		buf = append(buf, clog, "\n\n")
318
	}
319
	return strings.Join(buf, "\n")
320
}
321
func ovalSupported(r *models.ScanResult) bool {
322
	switch r.Family {
323
	case
324
		config.Amazon,
325
		config.FreeBSD,
326
		config.Raspbian:
327
		return false
328
	}
329
	return true
330
}
331
332
func needToRefreshCve(r models.ScanResult) bool {
333
	if r.Lang != config.Conf.Lang {
334
		return true
335
	}
336
337
	for _, cve := range r.ScannedCves {
338
		if 0 < len(cve.CveContents) {
339
			return false
340
		}
341
	}
342
	return true
343
}
344
345
func overwriteJSONFile(dir string, r models.ScanResult) error {
346
	before := config.Conf.FormatJSON
347
	beforeDiff := config.Conf.Diff
348
	config.Conf.FormatJSON = true
349
	config.Conf.Diff = false
350
	w := LocalFileWriter{CurrentDir: dir}
351
	if err := w.Write(r); err != nil {
352
		return fmt.Errorf("Failed to write summary report: %s", err)
353
	}
354
	config.Conf.FormatJSON = before
355
	config.Conf.Diff = beforeDiff
356
	return nil
357
}
358
359
func loadPrevious(currs models.ScanResults) (prevs models.ScanResults, err error) {
360
	dirs, err := ListValidJSONDirs()
361
	if err != nil {
362
		return
363
	}
364
365
	for _, result := range currs {
366
		filename := result.ServerName + ".json"
367
		if result.Container.Name != "" {
368
			filename = fmt.Sprintf("%s@%s.json", result.Container.Name, result.ServerName)
369
		}
370
		for _, dir := range dirs[1:] {
371
			path := filepath.Join(dir, filename)
372
			r, err := loadOneServerScanResult(path)
373
			if err != nil {
374
				util.Log.Errorf("%s", err)
375
				continue
376
			}
377
			if r.Family == result.Family && r.Release == result.Release {
378
				prevs = append(prevs, *r)
379
				util.Log.Infof("Previous json found: %s", path)
380
				break
381
			} else {
382
				util.Log.Infof("Previous json is different family.Release: %s, pre: %s.%s cur: %s.%s",
383
					path, r.Family, r.Release, result.Family, result.Release)
384
			}
385
		}
386
	}
387
	return prevs, nil
388
}
389
390
func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, err error) {
391
	for _, current := range curResults {
392
		found := false
393
		var previous models.ScanResult
394
		for _, r := range preResults {
395
			if current.ServerName == r.ServerName && current.Container.Name == r.Container.Name {
396
				found = true
397
				previous = r
398
				break
399
			}
400
		}
401
402
		if found {
403
			current.ScannedCves = getDiffCves(previous, current)
404
			packages := models.Packages{}
405
			for _, s := range current.ScannedCves {
406
				for _, affected := range s.AffectedPackages {
407
					p := current.Packages[affected.Name]
408
					packages[affected.Name] = p
409
				}
410
			}
411
			current.Packages = packages
412
		}
413
414
		diffed = append(diffed, current)
415
	}
416
	return diffed, err
417
}
418
419
func getDiffCves(previous, current models.ScanResult) models.VulnInfos {
420
	previousCveIDsSet := map[string]bool{}
421
	for _, previousVulnInfo := range previous.ScannedCves {
422
		previousCveIDsSet[previousVulnInfo.CveID] = true
423
	}
424
425
	new := models.VulnInfos{}
426
	updated := models.VulnInfos{}
427
	for _, v := range current.ScannedCves {
428
		if previousCveIDsSet[v.CveID] {
429
			if isCveInfoUpdated(v.CveID, previous, current) {
430
				updated[v.CveID] = v
431
				util.Log.Debugf("updated: %s", v.CveID)
432
433
				// TODO commented out beause  a bug of diff logic when multiple oval defs found for a certain CVE-ID and same updated_at
434
				// if these OVAL defs have different affected packages, this logic detects as updated.
435
				// This logic will be uncommented after integration with ghost https://github.com/knqyf263/gost
436
				// } else if isCveFixed(v, previous) {
437
				// updated[v.CveID] = v
438
				// util.Log.Debugf("fixed: %s", v.CveID)
439
440
			} else {
441
				util.Log.Debugf("same: %s", v.CveID)
442
			}
443
		} else {
444
			util.Log.Debugf("new: %s", v.CveID)
445
			new[v.CveID] = v
446
		}
447
	}
448
449
	for cveID, vuln := range new {
450
		updated[cveID] = vuln
451
	}
452
	return updated
453
}
454
455
func isCveFixed(current models.VulnInfo, previous models.ScanResult) bool {
456
	preVinfo, _ := previous.ScannedCves[current.CveID]
457
	pre := map[string]bool{}
458
	for _, h := range preVinfo.AffectedPackages {
459
		pre[h.Name] = h.NotFixedYet
460
	}
461
462
	cur := map[string]bool{}
463
	for _, h := range current.AffectedPackages {
464
		cur[h.Name] = h.NotFixedYet
465
	}
466
467
	return !reflect.DeepEqual(pre, cur)
468
}
469
470
func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool {
471
	cTypes := []models.CveContentType{
472
		models.NvdXML,
473
		models.Jvn,
474
		models.NewCveContentType(current.Family),
475
	}
476
477
	prevLastModified := map[models.CveContentType]time.Time{}
478
	preVinfo, ok := previous.ScannedCves[cveID]
479
	if !ok {
480
		return true
481
	}
482
	for _, cType := range cTypes {
483
		if content, ok := preVinfo.CveContents[cType]; ok {
484
			prevLastModified[cType] = content.LastModified
485
		}
486
	}
487
488
	curLastModified := map[models.CveContentType]time.Time{}
489
	curVinfo, ok := current.ScannedCves[cveID]
490
	if !ok {
491
		return true
492
	}
493
	for _, cType := range cTypes {
494
		if content, ok := curVinfo.CveContents[cType]; ok {
495
			curLastModified[cType] = content.LastModified
496
		}
497
	}
498
499
	for _, t := range cTypes {
500
		if !curLastModified[t].Equal(prevLastModified[t]) {
501
			util.Log.Debugf("%s LastModified not equal: \n%s\n%s",
502
				cveID, curLastModified[t], prevLastModified[t])
503
			return true
504
		}
505
	}
506
	return false
507
}
508
509
// jsonDirPattern is file name pattern of JSON directory
510
// 2016-11-16T10:43:28+09:00
511
// 2016-11-16T10:43:28Z
512
var jsonDirPattern = regexp.MustCompile(
513
	`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$`)
514
515
// ListValidJSONDirs returns valid json directory as array
516
// Returned array is sorted so that recent directories are at the head
517
func ListValidJSONDirs() (dirs []string, err error) {
518
	var dirInfo []os.FileInfo
519
	if dirInfo, err = ioutil.ReadDir(config.Conf.ResultsDir); err != nil {
520
		err = fmt.Errorf("Failed to read %s: %s",
521
			config.Conf.ResultsDir, err)
522
		return
523
	}
524
	for _, d := range dirInfo {
525
		if d.IsDir() && jsonDirPattern.MatchString(d.Name()) {
526
			jsonDir := filepath.Join(config.Conf.ResultsDir, d.Name())
527
			dirs = append(dirs, jsonDir)
528
		}
529
	}
530
	sort.Slice(dirs, func(i, j int) bool {
531
		return dirs[j] < dirs[i]
532
	})
533
	return
534
}
535
536
// JSONDir returns
537
// If there is an arg, check if it is a valid format and return the corresponding path under results.
538
// If arg passed via PIPE (such as history subcommand), return that path.
539
// Otherwise, returns the path of the latest directory
540
func JSONDir(args []string) (string, error) {
541
	var err error
542
	dirs := []string{}
543
544
	if 0 < len(args) {
545
		if dirs, err = ListValidJSONDirs(); err != nil {
546
			return "", err
547
		}
548
549
		path := filepath.Join(config.Conf.ResultsDir, args[0])
550
		for _, d := range dirs {
551
			ss := strings.Split(d, string(os.PathSeparator))
552
			timedir := ss[len(ss)-1]
553
			if timedir == args[0] {
554
				return path, nil
555
			}
556
		}
557
558
		return "", fmt.Errorf("Invalid path: %s", path)
559
	}
560
561
	// PIPE
562
	if config.Conf.Pipe {
563
		bytes, err := ioutil.ReadAll(os.Stdin)
564
		if err != nil {
565
			return "", fmt.Errorf("Failed to read stdin: %s", err)
566
		}
567
		fields := strings.Fields(string(bytes))
568
		if 0 < len(fields) {
569
			return filepath.Join(config.Conf.ResultsDir, fields[0]), nil
570
		}
571
		return "", fmt.Errorf("Stdin is invalid: %s", string(bytes))
572
	}
573
574
	// returns latest dir when no args or no PIPE
575
	if dirs, err = ListValidJSONDirs(); err != nil {
576
		return "", err
577
	}
578
	if len(dirs) == 0 {
579
		return "", fmt.Errorf("No results under %s",
580
			config.Conf.ResultsDir)
581
	}
582
	return dirs[0], nil
583
}
584
585
// LoadScanResults read JSON data
586
func LoadScanResults(jsonDir string) (results models.ScanResults, err error) {
587
	var files []os.FileInfo
588
	if files, err = ioutil.ReadDir(jsonDir); err != nil {
589
		return nil, fmt.Errorf("Failed to read %s: %s", jsonDir, err)
590
	}
591
	for _, f := range files {
592
		if filepath.Ext(f.Name()) != ".json" || strings.HasSuffix(f.Name(), "_diff.json") {
593
			continue
594
		}
595
596
		var r *models.ScanResult
597
		path := filepath.Join(jsonDir, f.Name())
598
		if r, err = loadOneServerScanResult(path); err != nil {
599
			return nil, err
600
		}
601
		results = append(results, *r)
602
	}
603
	if len(results) == 0 {
604
		return nil, fmt.Errorf("There is no json file under %s", jsonDir)
605
	}
606
	return
607
}
608
609
// loadOneServerScanResult read JSON data of one server
610
func loadOneServerScanResult(jsonFile string) (*models.ScanResult, error) {
611
	var (
612
		data []byte
613
		err  error
614
	)
615
	if data, err = ioutil.ReadFile(jsonFile); err != nil {
616
		return nil, fmt.Errorf("Failed to read %s: %s", jsonFile, err)
617
	}
618
	result := &models.ScanResult{}
619
	if err := json.Unmarshal(data, result); err != nil {
620
		return nil, fmt.Errorf("Failed to parse %s: %s", jsonFile, err)
621
	}
622
	return result, nil
623
}
624