report/util.go   F
last analyzed

Size/Duplication

Total Lines 633
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 117
eloc 406
dl 0
loc 633
rs 2
c 0
b 0
f 0

19 Methods

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