Completed
Pull Request — master (#805)
by kota
05:47
created

report/util.go   F

Size/Duplication

Total Lines 660
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 121
eloc 424
dl 0
loc 660
rs 2
c 0
b 0
f 0

19 Methods

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