/* Vuls - Vulnerability Scanner Copyright (C) 2016 Future Architect, Inc. Japan. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package report import ( "bytes" "fmt" "os" "path/filepath" "strings" "time" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/gosuri/uitable" ) func ensureResultDir(scannedAt time.Time) (path string, err error) { if resultDirPath != "" { return resultDirPath, nil } const timeLayout = "20060102_1504" timedir := scannedAt.Format(timeLayout) wd, _ := os.Getwd() dir := filepath.Join(wd, "results", timedir) if err := os.MkdirAll(dir, 0755); err != nil { return "", fmt.Errorf("Failed to create dir: %s", err) } symlinkPath := filepath.Join(wd, "results", "current") if _, err := os.Lstat(symlinkPath); err == nil { if err := os.Remove(symlinkPath); err != nil { return "", fmt.Errorf( "Failed to remove symlink. path: %s, err: %s", symlinkPath, err) } } if err := os.Symlink(dir, symlinkPath); err != nil { return "", fmt.Errorf( "Failed to create symlink: path: %s, err: %s", symlinkPath, err) } return dir, nil } func toPlainText(scanResult models.ScanResult) (string, error) { serverInfo := scanResult.ServerInfo() var buffer bytes.Buffer for i := 0; i < len(serverInfo); i++ { buffer.WriteString("=") } header := fmt.Sprintf("%s\n%s", serverInfo, buffer.String()) if len(scanResult.KnownCves) == 0 && len(scanResult.UnknownCves) == 0 { return fmt.Sprintf(` %s No unsecure packages. `, header), nil } summary := ToPlainTextSummary(scanResult) scoredReport, unscoredReport := []string{}, []string{} scoredReport, unscoredReport = toPlainTextDetails(scanResult, scanResult.Family) scored := strings.Join(scoredReport, "\n\n") unscored := "" if !config.Conf.IgnoreUnscoredCves { unscored = strings.Join(unscoredReport, "\n\n") } detail := fmt.Sprintf(` %s %s `, scored, unscored, ) text := fmt.Sprintf("%s\n%s\n%s\n", header, summary, detail) return text, nil } // ToPlainTextSummary format summary for plain text. func ToPlainTextSummary(r models.ScanResult) string { stable := uitable.New() stable.MaxColWidth = 84 stable.Wrap = true cves := r.KnownCves if !config.Conf.IgnoreUnscoredCves { cves = append(cves, r.UnknownCves...) } for _, d := range cves { var scols []string switch { case config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore(): summary := d.CveDetail.Jvn.CveTitle() scols = []string{ d.CveDetail.CveID, fmt.Sprintf("%-4.1f (%s)", d.CveDetail.CvssScore(config.Conf.Lang), d.CveDetail.Jvn.CvssSeverity(), ), summary, } case 0 < d.CveDetail.CvssScore("en"): summary := d.CveDetail.Nvd.CveSummary() scols = []string{ d.CveDetail.CveID, fmt.Sprintf("%-4.1f (%s)", d.CveDetail.CvssScore(config.Conf.Lang), d.CveDetail.Nvd.CvssSeverity(), ), summary, } default: scols = []string{ d.CveDetail.CveID, "?", d.CveDetail.Nvd.CveSummary(), } } cols := make([]interface{}, len(scols)) for i := range cols { cols[i] = scols[i] } stable.AddRow(cols...) } return fmt.Sprintf("%s", stable) } func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) { for _, cve := range data.KnownCves { switch config.Conf.Lang { case "en": if 0 < cve.CveDetail.Nvd.CvssScore() { scoredReport = append( scoredReport, toPlainTextDetailsLangEn(cve, osFamily)) } else { scoredReport = append( scoredReport, toPlainTextUnknownCve(cve, osFamily)) } case "ja": if 0 < cve.CveDetail.Jvn.CvssScore() { scoredReport = append( scoredReport, toPlainTextDetailsLangJa(cve, osFamily)) } else if 0 < cve.CveDetail.Nvd.CvssScore() { scoredReport = append( scoredReport, toPlainTextDetailsLangEn(cve, osFamily)) } else { scoredReport = append( scoredReport, toPlainTextUnknownCve(cve, osFamily)) } } } for _, cve := range data.UnknownCves { unscoredReport = append( unscoredReport, toPlainTextUnknownCve(cve, osFamily)) } return } func toPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string { cveID := cveInfo.CveDetail.CveID dtable := uitable.New() dtable.MaxColWidth = 100 dtable.Wrap = true dtable.AddRow(cveID) dtable.AddRow("-------------") dtable.AddRow("Score", "?") dtable.AddRow("NVD", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID)) dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) dlinks := distroLinks(cveInfo, osFamily) for _, link := range dlinks { dtable.AddRow(link.title, link.url) } return fmt.Sprintf("%s", dtable) } func toPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string { cveDetail := cveInfo.CveDetail cveID := cveDetail.CveID jvn := cveDetail.Jvn dtable := uitable.New() //TODO resize dtable.MaxColWidth = 100 dtable.Wrap = true dtable.AddRow(cveID) dtable.AddRow("-------------") if score := cveDetail.Jvn.CvssScore(); 0 < score { dtable.AddRow("Score", fmt.Sprintf("%4.1f (%s)", cveDetail.Jvn.CvssScore(), jvn.CvssSeverity(), )) } else { dtable.AddRow("Score", "?") } dtable.AddRow("Vector", jvn.CvssVector()) dtable.AddRow("Title", jvn.CveTitle()) dtable.AddRow("Description", jvn.CveSummary()) dtable.AddRow("JVN", jvn.Link()) dtable.AddRow("NVD", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID)) dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) dtable.AddRow("CVSS Claculator", cveDetail.CvssV2CalculatorLink("ja")) dlinks := distroLinks(cveInfo, osFamily) for _, link := range dlinks { dtable.AddRow(link.title, link.url) } dtable = addPackageInfos(dtable, cveInfo.Packages) dtable = addCpeNames(dtable, cveInfo.CpeNames) return fmt.Sprintf("%s", dtable) } func toPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string { cveDetail := d.CveDetail cveID := cveDetail.CveID nvd := cveDetail.Nvd dtable := uitable.New() //TODO resize dtable.MaxColWidth = 100 dtable.Wrap = true dtable.AddRow(cveID) dtable.AddRow("-------------") if score := cveDetail.Nvd.CvssScore(); 0 < score { dtable.AddRow("Score", fmt.Sprintf("%4.1f (%s)", cveDetail.Nvd.CvssScore(), nvd.CvssSeverity(), )) } else { dtable.AddRow("Score", "?") } dtable.AddRow("Vector", nvd.CvssVector()) dtable.AddRow("Summary", nvd.CveSummary()) dtable.AddRow("NVD", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID)) dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) dtable.AddRow("CVSS Claculator", cveDetail.CvssV2CalculatorLink("en")) links := distroLinks(d, osFamily) for _, link := range links { dtable.AddRow(link.title, link.url) } dtable = addPackageInfos(dtable, d.Packages) dtable = addCpeNames(dtable, d.CpeNames) return fmt.Sprintf("%s\n", dtable) } type distroLink struct { title string url string } // addVendorSite add Vendor site of the CVE to table func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink { cveID := cveInfo.CveDetail.CveID switch osFamily { case "rhel", "centos": links := []distroLink{ { "RHEL-CVE", fmt.Sprintf("%s/%s", redhatSecurityBaseURL, cveID), }, } for _, advisory := range cveInfo.DistroAdvisories { aidURL := strings.Replace(advisory.AdvisoryID, ":", "-", -1) links = append(links, distroLink{ // "RHEL-errata", advisory.AdvisoryID, fmt.Sprintf(redhatRHSABaseBaseURL, aidURL), }) } return links case "amazon": links := []distroLink{ { "RHEL-CVE", fmt.Sprintf("%s/%s", redhatSecurityBaseURL, cveID), }, } for _, advisory := range cveInfo.DistroAdvisories { links = append(links, distroLink{ // "Amazon-ALAS", advisory.AdvisoryID, fmt.Sprintf(amazonSecurityBaseURL, advisory.AdvisoryID), }) } return links case "ubuntu": return []distroLink{ { "Ubuntu-CVE", fmt.Sprintf("%s/%s", ubuntuSecurityBaseURL, cveID), }, //TODO Ubuntu USN } case "debian": return []distroLink{ { "Debian-CVE", fmt.Sprintf("%s/%s", debianTrackerBaseURL, cveID), }, // TODO Debian dsa } case "FreeBSD": links := []distroLink{} for _, advisory := range cveInfo.DistroAdvisories { links = append(links, distroLink{ "FreeBSD-VuXML", fmt.Sprintf(freeBSDVuXMLBaseURL, advisory.AdvisoryID), }) } return links default: return []distroLink{} } } //TODO // addPackageInfos add package information related the CVE to table func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable.Table { for i, p := range packs { var title string if i == 0 { title = "Package/CPE" } ver := fmt.Sprintf( "%s -> %s", p.ToStringCurrentVersion(), p.ToStringNewVersion()) table.AddRow(title, ver) } return table } func addCpeNames(table *uitable.Table, names []models.CpeName) *uitable.Table { for _, p := range names { table.AddRow("CPE", fmt.Sprintf("%s", p.Name)) } return table }