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