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