From 4fcdea3ccbf31649ea48c4ffaf80ebc681945c38 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 17 May 2017 10:18:13 +0900 Subject: [PATCH] Implement -format-full-text --- models/models.go | 89 ++++++++++++++++++++-- report/util.go | 193 ++++++++++++++++++++--------------------------- util/util.go | 19 +++++ 3 files changed, 181 insertions(+), 120 deletions(-) diff --git a/models/models.go b/models/models.go index ab8bdfe4..c92f8164 100644 --- a/models/models.go +++ b/models/models.go @@ -18,10 +18,12 @@ along with this program. If not, see . package models import ( + "bytes" "fmt" "strings" "time" + "github.com/future-architect/vuls/config" cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) @@ -267,6 +269,21 @@ func (r ScanResult) CveSummary(ignoreUnscoreCves bool) string { high+medium+low+unknown, high, medium, low, unknown) } +// FormatTextReportHeadedr returns header of text report +func (r ScanResult) FormatTextReportHeadedr() string { + serverInfo := r.ServerInfo() + var buf bytes.Buffer + for i := 0; i < len(serverInfo); i++ { + buf.WriteString("=") + } + return fmt.Sprintf("%s\n%s\n%s\t%s\n", + r.ServerInfo(), + buf.String(), + r.CveSummary(config.Conf.IgnoreUnscoredCves), + r.Packages.FormatUpdatablePacksSummary(), + ) +} + // Confidence is a ranking how confident the CVE-ID was deteted correctly // Score: 0 - 100 type Confidence struct { @@ -338,6 +355,17 @@ func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos { return filtered } +// FindScoredVulns return socred vulnerabilities +func (v VulnInfos) FindScoredVulns() VulnInfos { + return v.Find(func(vv VulnInfo) bool { + if 0 < vv.CveContents.MaxCvss2Score().Value.Score || + 0 < vv.CveContents.MaxCvss3Score().Value.Score { + return true + } + return false + }) +} + // VulnInfo holds a vulnerability information and unsecure packages type VulnInfo struct { CveID string @@ -487,6 +515,11 @@ type Cvss2 struct { Severity string } +// Format CVSS Score and Vector +func (c Cvss2) Format() string { + return fmt.Sprintf("%3.1f/%s", c.Score, c.Vector) +} + func cvss2ScoreToSeverity(score float64) string { if 7.0 <= score { return "HIGH" @@ -555,13 +588,18 @@ type CveContentCvss3 struct { Value Cvss3 } -// Cvss3 has CVSS v3 +// Cvss3 has CVSS v3 Score, Vector and Severity type Cvss3 struct { Score float64 Vector string Severity string } +// Format CVSS Score and Vector +func (c Cvss3) Format() string { + return fmt.Sprintf("%3.1f/CVSS:3.0/%s", c.Score, c.Vector) +} + func cvss3ScoreToSeverity(score float64) string { if 9.0 <= score { return "CRITICAL" @@ -627,6 +665,22 @@ func (v CveContents) MaxCvss3Score() CveContentCvss3 { return value } +// FormatMaxCvssScore returns Max CVSS Score +func (v CveContents) FormatMaxCvssScore() string { + v2Max := v.MaxCvss2Score() + v3Max := v.MaxCvss3Score() + if v2Max.Value.Score <= v3Max.Value.Score { + return fmt.Sprintf("%3.1f %s (%s)", + v3Max.Value.Score, + strings.ToUpper(v3Max.Value.Severity), + v3Max.Type) + } + return fmt.Sprintf("%3.1f %s (%s)", + v2Max.Value.Score, + strings.ToUpper(v2Max.Value.Severity), + v2Max.Type) +} + // Titles returns tilte (TUI) func (v CveContents) Titles(lang, myFamily string) (values []CveContentStr) { if lang == "ja" { @@ -694,7 +748,7 @@ func (v CveContents) Summaries(lang, myFamily string) (values []CveContentStr) { } // SourceLinks returns link of source -func (v CveContents) SourceLinks(lang, myFamily string) (values []CveContentStr) { +func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveContentStr) { if lang == "ja" { if cont, found := v[JVN]; found && !cont.Empty() { values = append(values, CveContentStr{JVN, cont.SourceLink}) @@ -707,7 +761,23 @@ func (v CveContents) SourceLinks(lang, myFamily string) (values []CveContentStr) values = append(values, CveContentStr{ctype, cont.SourceLink}) } } - return + + if len(values) == 0 { + return []CveContentStr{{ + Type: NVD, + Value: "https://nvd.nist.gov/vuln/detail/" + cveID, + }} + } + return values +} + +// VendorLink returns link of source +func (v CveContents) VendorLink(myFamily string) CveContentStr { + ctype := NewCveContentType(myFamily) + if cont, ok := v[ctype]; ok { + return CveContentStr{ctype, cont.SourceLink} + } + return CveContentStr{ctype, ""} } // Severities returns Severities @@ -770,17 +840,20 @@ func (v CveContents) References(myFamily string) (values []CveContentRefs) { return } -// CweIDs returns CweIDs +// CweIDs returns related CweIDs of the vulnerability func (v CveContents) CweIDs(myFamily string) (values []CveContentStr) { order := CveContentTypes{NewCveContentType(myFamily)} order = append(order, AllCveContetTypes.Except(append(order)...)...) for _, ctype := range order { if cont, found := v[ctype]; found && 0 < len(cont.CweID) { - values = append(values, CveContentStr{ - Type: ctype, - Value: cont.CweID, - }) + // RedHat's OVAL sometimes contains multiple CWE-IDs separated by spaces + for _, cweID := range strings.Fields(cont.CweID) { + values = append(values, CveContentStr{ + Type: ctype, + Value: cweID, + }) + } } } return diff --git a/report/util.go b/report/util.go index 2752d8b2..405d7fb1 100644 --- a/report/util.go +++ b/report/util.go @@ -27,7 +27,7 @@ import ( "github.com/gosuri/uitable" ) -const maxColWidth = 100 +const maxColWidth = 80 func formatScanSummary(rs ...models.ScanResult) string { table := uitable.New() @@ -80,38 +80,18 @@ func formatOneLineSummary(rs ...models.ScanResult) string { } func formatShortPlainText(r models.ScanResult) string { - stable := uitable.New() - stable.MaxColWidth = maxColWidth - stable.Wrap = true - - vulns := r.ScannedCves - if !config.Conf.IgnoreUnscoredCves { - vulns = r.ScannedCves.Find(func(v models.VulnInfo) bool { - if 0 < v.CveContents.MaxCvss2Score().Value.Score || - 0 < v.CveContents.MaxCvss3Score().Value.Score { - return true - } - return false - }) - } - - var buf bytes.Buffer - for i := 0; i < len(r.ServerInfo()); i++ { - buf.WriteString("=") - } - header := fmt.Sprintf("%s\n%s\n%s\t%s\n\n", - r.ServerInfo(), - buf.String(), - r.CveSummary(config.Conf.IgnoreUnscoredCves), - r.Packages.FormatUpdatablePacksSummary(), - ) - + header := r.FormatTextReportHeadedr() if len(r.Errors) != 0 { return fmt.Sprintf( "%s\nError: Scan with --debug to view the details\n%s\n\n", header, r.Errors) } + vulns := r.ScannedCves + if !config.Conf.IgnoreUnscoredCves { + vulns = vulns.FindScoredVulns() + } + if len(vulns) == 0 { return fmt.Sprintf(` %s @@ -120,61 +100,27 @@ func formatShortPlainText(r models.ScanResult) string { `, header, r.Packages.FormatUpdatablePacksSummary()) } + stable := uitable.New() + stable.MaxColWidth = maxColWidth + stable.Wrap = true for _, vuln := range vulns { - //TODO - // var packsVer string - // for _, name := range vuln.PackageNames { - // // packages detected by OVAL may not be actually installed - // if pack, ok := r.Packages[name]; ok { - // packsVer += fmt.Sprintf("%s\n", - // pack.FormatVersionFromTo()) - // } - // } - // for _, name := range vuln.CpeNames { - // packsVer += name + "\n" - // } - summaries := vuln.CveContents.Summaries(config.Conf.Lang, r.Family) - links := vuln.CveContents.SourceLinks(config.Conf.Lang, r.Family) - if len(links) == 0 { - links = []models.CveContentStr{{ - Type: models.NVD, - Value: "https://nvd.nist.gov/vuln/detail/" + vuln.CveID, - }} - } + links := vuln.CveContents.SourceLinks( + config.Conf.Lang, r.Family, vuln.CveID) cvsses := "" for _, cvss := range vuln.CveContents.Cvss2Scores() { - c2 := cvss.Value - cvsses += fmt.Sprintf("%3.1f/%s (%s)\n", - c2.Score, c2.Vector, cvss.Type) + cvsses += fmt.Sprintf("%s (%s)\n", cvss.Value.Format(), cvss.Type) } - cvsses += fmt.Sprintf("%s\n", vuln.Cvss2CalcURL()) - + cvsses += vuln.Cvss2CalcURL() + "\n" for _, cvss := range vuln.CveContents.Cvss3Scores() { - c3 := cvss.Value - cvsses += fmt.Sprintf("%3.1f/CVSS:3.0/%s (%s)\n", - c3.Score, c3.Vector, cvss.Type) + cvsses += fmt.Sprintf("%s (%s)\n", cvss.Value.Format(), cvss.Type) } if 0 < len(vuln.CveContents.Cvss3Scores()) { - cvsses += fmt.Sprintf("%s\n", vuln.Cvss3CalcURL()) - } - - var maxCvss string - v2Max := vuln.CveContents.MaxCvss2Score() - v3Max := vuln.CveContents.MaxCvss3Score() - if v2Max.Value.Score <= v3Max.Value.Score { - maxCvss = fmt.Sprintf("%3.1f %s (%s)", - v3Max.Value.Score, - strings.ToUpper(v3Max.Value.Severity), - v3Max.Type) - } else { - maxCvss = fmt.Sprintf("%3.1f %s (%s)", - v2Max.Value.Score, - strings.ToUpper(v2Max.Value.Severity), - v2Max.Type) + cvsses += vuln.Cvss3CalcURL() + "\n" } + maxCvss := vuln.CveContents.FormatMaxCvssScore() rightCol := fmt.Sprintf(`%s %s --- @@ -201,53 +147,76 @@ func formatShortPlainText(r models.ScanResult) string { } func formatFullPlainText(r models.ScanResult) string { - serverInfo := r.ServerInfo() - - var buf bytes.Buffer - for i := 0; i < len(serverInfo); i++ { - buf.WriteString("=") - } - header := fmt.Sprintf("%s\n%s\n%s\t%s\n", - r.ServerInfo(), - buf.String(), - r.CveSummary(config.Conf.IgnoreUnscoredCves), - r.Packages.FormatUpdatablePacksSummary(), - ) - + header := r.FormatTextReportHeadedr() if len(r.Errors) != 0 { return fmt.Sprintf( "%s\nError: Scan with --debug to view the details\n%s\n\n", header, r.Errors) } - //TODO - // if len(r.KnownCves) == 0 && len(r.UnknownCves) == 0 { - // return fmt.Sprintf(` - // %s - // No CVE-IDs are found in updatable packages. - // %s - // `, header, r.Packages.FormatUpdatablePacksSummary()) - // } + vulns := r.ScannedCves + if !config.Conf.IgnoreUnscoredCves { + vulns = vulns.FindScoredVulns() + } - // scoredReport, unscoredReport := []string{}, []string{} - // scoredReport, unscoredReport = formatPlainTextDetails(r, r.Family) + if len(vulns) == 0 { + return fmt.Sprintf(` + %s + No CVE-IDs are found in updatable packages. + %s + `, header, r.Packages.FormatUpdatablePacksSummary()) + } - // unscored := "" - // if !config.Conf.IgnoreUnscoredCves { - // unscored = strings.Join(unscoredReport, "\n\n") - // } + table := uitable.New() + table.MaxColWidth = maxColWidth + table.Wrap = true + for _, vuln := range vulns { + table.AddRow(vuln.CveID) + table.AddRow("----------------") + table.AddRow("Max Score", vuln.CveContents.FormatMaxCvssScore()) + for _, cvss := range vuln.CveContents.Cvss2Scores() { + table.AddRow(cvss.Type, cvss.Value.Format()) + } + for _, cvss := range vuln.CveContents.Cvss3Scores() { + table.AddRow(cvss.Type, cvss.Value.Format()) + } + if 0 < len(vuln.CveContents.Cvss2Scores()) { + table.AddRow("CVSSv2 Calc", vuln.Cvss2CalcURL()) + } + if 0 < len(vuln.CveContents.Cvss3Scores()) { + table.AddRow("CVSSv3 Calc", vuln.Cvss3CalcURL()) + } + table.AddRow("Summary", vuln.CveContents.Summaries( + config.Conf.Lang, r.Family)[0].Value) - // scored := strings.Join(scoredReport, "\n\n") - // detail := fmt.Sprintf(` - // %s + links := vuln.CveContents.SourceLinks( + config.Conf.Lang, r.Family, vuln.CveID) + table.AddRow("Source", links[0].Value) - // %s - // `, - // scored, - // unscored, - // ) - // return fmt.Sprintf("%s\n%s\n%s", header, detail, formatChangelogs(r)) - return "" + vendorLink := vuln.CveContents.VendorLink(r.Family) + table.AddRow(fmt.Sprintf("Vendor (%s)", vendorLink.Type), vendorLink.Value) + + for _, v := range vuln.CveContents.CweIDs(r.Family) { + table.AddRow(fmt.Sprintf("%s (%s)", v.Value, v.Type), cweURL(v.Value)) + } + + packsVer := []string{} + for _, name := range vuln.PackageNames { + // packages detected by OVAL may not be actually installed + if pack, ok := r.Packages[name]; ok { + packsVer = append(packsVer, pack.FormatVersionFromTo()) + } + } + for _, name := range vuln.CpeNames { + packsVer = append(packsVer, name) + } + table.AddRow("Package/CPE", strings.Join(packsVer, "\n")) + table.AddRow("Confidence", vuln.Confidence) + + table.AddRow("\n") + } + + return fmt.Sprintf("%s\n%s", header, table) } //TODO @@ -393,10 +362,10 @@ func formatPlainTextDetails(r models.ScanResult, osFamily string) (scoredReport, // return fmt.Sprintf("%s\n", dtable) // } -type distroLink struct { - title string - url string -} +// type distroLink struct { +// title string +// url string +// } // distroLinks add Vendor URL of the CVE to table // func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink { diff --git a/util/util.go b/util/util.go index 3b94e8a6..b7dcbc6e 100644 --- a/util/util.go +++ b/util/util.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" ) // GenWorkers generates goroutine @@ -135,3 +136,21 @@ func Truncate(str string, length int) string { } return str } + +// VendorLink returns a URL of the given OS family and CVEID +//TODO +func VendorLink(family, cveID string) string { + cType := models.NewCveContentType(family) + switch cType { + case models.RedHat: + return "https://access.redhat.com/security/cve/" + cveID + case models.Debian: + return "https://security-tracker.debian.org/tracker/" + cveID + case models.Ubuntu: + return "http://people.ubuntu.com/~ubuntu-security/cve/" + cveID + // case models.FreeBSD: + // return "http://people.ubuntu.com/~ubuntu-security/cve/" + cveID + } + + return "" +}